sql >> Base de Datos >  >> RDS >> PostgreSQL

La mejor manera de seleccionar filas aleatorias PostgreSQL

Dadas sus especificaciones (más información adicional en los comentarios),

  • Tiene una columna de ID numérico (números enteros) con solo unos pocos (o moderadamente pocos) espacios.
  • Obviamente ninguna o pocas operaciones de escritura.
  • ¡Tu columna de ID tiene que estar indexada! Una clave principal sirve muy bien.

La consulta a continuación no necesita un escaneo secuencial de la tabla grande, solo un escaneo de índice.

Primero, obtenga estimaciones para la consulta principal:

SELECT count(*) AS ct              -- optional
     , min(id)  AS min_id
     , max(id)  AS max_id
     , max(id) - min(id) AS id_span
FROM   big;

La única parte posiblemente costosa es el count(*) (para mesas grandes). Dadas las especificaciones anteriores, no lo necesita. Una estimación funcionará bien, disponible casi sin costo (explicación detallada aquí):

SELECT reltuples AS ct FROM pg_class
WHERE oid = 'schema_name.big'::regclass;

Mientras ct no es mucho menor que id_span , la consulta superará a otros enfoques.

WITH params AS (
   SELECT 1       AS min_id           -- minimum id <= current min id
        , 5100000 AS id_span          -- rounded up. (max_id - min_id + buffer)
    )
SELECT *
FROM  (
   SELECT p.min_id + trunc(random() * p.id_span)::integer AS id
   FROM   params p
         ,generate_series(1, 1100) g  -- 1000 + buffer
   GROUP  BY 1                        -- trim duplicates
) r
JOIN   big USING (id)
LIMIT  1000;                          -- trim surplus
  • Genera números aleatorios en el id espacio. Tiene "pocos espacios en blanco", así que agregue un 10 % (suficiente para cubrir fácilmente los espacios en blanco) al número de filas para recuperar.

  • Cada id se puede elegir varias veces al azar (aunque es muy poco probable con un gran espacio de identificación), así que agrupe los números generados (o use DISTINCT ).

  • Únete al id s a la mesa grande. Esto debería ser muy rápido con el índice en su lugar.

  • Finalmente recorte el excedente id s que no han sido comidos por tontos y lagunas. Cada fila tiene una oportunidad completamente igual para ser elegido.

Versión corta

Puedes simplificar esta consulta El CTE en la consulta anterior es solo para fines educativos:

SELECT *
FROM  (
   SELECT DISTINCT 1 + trunc(random() * 5100000)::integer AS id
   FROM   generate_series(1, 1100) g
   ) r
JOIN   big USING (id)
LIMIT  1000;

Refinar con rCTE

Especialmente si no está tan seguro acerca de las lagunas y las estimaciones.

WITH RECURSIVE random_pick AS (
   SELECT *
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   generate_series(1, 1030)  -- 1000 + few percent - adapt to your needs
      LIMIT  1030                      -- hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss

   UNION                               -- eliminate dupe
   SELECT b.*
   FROM  (
      SELECT 1 + trunc(random() * 5100000)::int AS id
      FROM   random_pick r             -- plus 3 percent - adapt to your needs
      LIMIT  999                       -- less than 1000, hint for query planner
      ) r
   JOIN   big b USING (id)             -- eliminate miss
   )
TABLE  random_pick
LIMIT  1000;  -- actual limit

Podemos trabajar con un superávit más pequeño en la consulta base. Si hay demasiados espacios en blanco y no encontramos suficientes filas en la primera iteración, el rCTE continúa iterando con el término recursivo. Todavía necesitamos relativamente pocos las brechas en el espacio de ID o la recursividad pueden agotarse antes de alcanzar el límite, o tenemos que comenzar con un búfer lo suficientemente grande que desafía el propósito de optimizar el rendimiento.

Los duplicados son eliminados por UNION en el rCTE.

El exterior LIMIT hace que el CTE se detenga tan pronto como tengamos suficientes filas.

Esta consulta está cuidadosamente redactada para usar el índice disponible, generar filas realmente aleatorias y no detenerse hasta que alcancemos el límite (a menos que la recursividad se agote). Hay una serie de trampas aquí si va a reescribirlo.

Envolver en función

Para uso repetido con diferentes parámetros:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big
  LANGUAGE plpgsql VOLATILE ROWS 1000 AS
$func$
DECLARE
   _surplus  int := _limit * _gaps;
   _estimate int := (           -- get current estimate from system
      SELECT c.reltuples * _gaps
      FROM   pg_class c
      WHERE  c.oid = 'big'::regclass);
BEGIN
   RETURN QUERY
   WITH RECURSIVE random_pick AS (
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   generate_series(1, _surplus) g
         LIMIT  _surplus           -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses

      UNION                        -- eliminate dupes
      SELECT *
      FROM  (
         SELECT 1 + trunc(random() * _estimate)::int
         FROM   random_pick        -- just to make it recursive
         LIMIT  _limit             -- hint for query planner
         ) r (id)
      JOIN   big USING (id)        -- eliminate misses
   )
   TABLE  random_pick
   LIMIT  _limit;
END
$func$;

Llamar:

SELECT * FROM f_random_sample();
SELECT * FROM f_random_sample(500, 1.05);

Incluso podría hacer que esto sea genérico para que funcione con cualquier tabla:tome el nombre de la columna PK y la tabla como tipo polimórfico y use EXECUTE ... Pero eso está más allá del alcance de esta pregunta. Ver:

  • Refactorice una función PL/pgSQL para devolver el resultado de varias consultas SELECT

Posible alternativa

SI sus requisitos permiten conjuntos idénticos para repetir llamadas (y estamos hablando de llamadas repetidas) consideraría una vista materializada . Ejecute la consulta anterior una vez y escriba el resultado en una tabla. Los usuarios obtienen una selección casi aleatoria a la velocidad de la luz. Actualice su selección aleatoria en intervalos o eventos de su elección.

Postgres 9.5 presenta TABLESAMPLE SYSTEM (n)

Donde n es un porcentaje El manual:

El BERNOULLI y SYSTEM cada uno de los métodos de muestreo acepta un solo argumento que es la fracción de la tabla a muestrear, expresada como un porcentaje entre 0 y 100 . Este argumento puede ser cualquier real -expresión valorada.

Énfasis en negrita mío. Es muy rápido , pero el resultado no es exactamente aleatorio . El manual de nuevo:

El SYSTEM El método es significativamente más rápido que el BERNOULLI método cuando se especifican pequeños porcentajes de muestreo, pero puede devolver una muestra menos aleatoria de la tabla como resultado de los efectos de agrupamiento.

El número de filas devueltas puede variar enormemente. Para nuestro ejemplo, para obtener aproximadamente 1000 filas:

SELECT * FROM big TABLESAMPLE SYSTEM ((1000 * 100) / 5100000.0);

Relacionado:

  • Manera rápida de descubrir el recuento de filas de una tabla en PostgreSQL

O instale el módulo adicional tsm_system_rows para obtener exactamente el número de filas solicitadas (si hay suficientes) y permitir la sintaxis más conveniente:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Vea la respuesta de Evan para más detalles.

Pero eso todavía no es exactamente aleatorio.