La mejor manera de seleccionar filas aleatorias PostgreSQL

345

Quiero una selección aleatoria de filas en PostgreSQL, probé esto:

select * from table where random() < 0.01;

Pero algunos otros recomiendan esto:

select * from table order by random() limit 1000;

Tengo una mesa muy grande con 500 millones de filas, quiero que sea rápida.

¿Qué enfoque es mejor? ¿Cuáles son las diferencias? ¿Cuál es la mejor manera de seleccionar filas aleatorias?

nanounanue
fuente
1
Hola Jack, gracias por tu respuesta, el tiempo de ejecución es más lento en orden, pero me gustaría saber cuál es la diferencia, si la hay ...
nanounanue
Uhhh ... de nada. Entonces, ¿has intentado comparar los diferentes enfoques?
También hay formas mucho más rápidas. Todo depende de sus requisitos y de con qué tiene que trabajar. ¿Necesitas exactamente 1000 filas? ¿La tabla tiene una identificación numérica? ¿Sin brechas / pocas / muchas? ¿Qué tan importante es la velocidad? ¿Cuántas solicitudes por unidad de tiempo? ¿Cada solicitud necesita un conjunto diferente o pueden ser iguales para un segmento de tiempo definido?
Erwin Brandstetter
66
La primera opción "(random () <0.01)" es matemáticamente incorrecta ya que no podría obtener filas en respuesta si ningún número aleatorio está por debajo de 0.01, eso podría suceder en cualquier caso (aunque menos probable), no importa cuán grande sea la tabla o más alto el umbral. La segunda opción siempre es correcta
Herme
1
Si desea seleccionar solo una fila, vea esta pregunta: stackoverflow.com/q/5297396/247696
Flimm

Respuestas:

230

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

  • Tiene una columna de identificación numérica (números enteros) con solo unos pocos (o moderadamente pocos) huecos.
  • Obviamente ninguna o pocas operaciones de escritura.
  • ¡Su columna de ID tiene que ser indexada! Una clave primaria sirve muy bien.

La consulta a continuación no necesita una exploración secuencial de la tabla grande, solo una exploración 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 la count(*)(para tablas enormes). Dadas las especificaciones anteriores, no lo necesita. Una estimación estará bien, disponible casi sin costo ( explicación detallada aquí ):

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

Mientras ctno sea mucho más pequeño 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 idespacio. Tiene "pocos espacios", por lo tanto, agregue un 10% (suficiente para cubrir fácilmente los espacios en blanco) al número de filas para recuperar.

  • Cada uno idse puede elegir varias veces por casualidad (aunque es muy poco probable con un gran espacio de identificación), por lo tanto, agrupe los números generados (o use DISTINCT).

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

  • Finalmente, elimine los excedentes idque no hayan sido comidos por engaños y huecos. Cada fila tiene una oportunidad completamente igual de ser elegido.

Version 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;

Refina con rCTE

Especialmente si no está tan seguro sobre las brechas 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
   )
SELECT *
FROM   random_pick
LIMIT  1000;  -- actual limit

Podemos trabajar con un excedente más pequeño en la consulta base. Si hay demasiados espacios, por lo que no encontramos suficientes filas en la primera iteración, el rCTE continúa iterando con el término recursivo. Todavía necesitamos relativamente pocos espacios en el espacio de ID o la recursión puede agotarse antes de que se alcance el límite, o tenemos que comenzar con un búfer lo suficientemente grande que desafíe el propósito de optimizar el rendimiento.

Los duplicados son eliminados por el UNIONen el rCTE.

El exterior LIMIThace 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 cumplamos el límite (a menos que la recursión se seque). Hay una serie de dificultades aquí si va a reescribirlo.

Envolver en función

Para uso repetido con parámetros variables:

CREATE OR REPLACE FUNCTION f_random_sample(_limit int = 1000, _gaps real = 1.03)
  RETURNS SETOF big 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
   )
   SELECT *
   FROM   random_pick
   LIMIT  _limit;
END
$func$  LANGUAGE plpgsql VOLATILE ROWS 1000;

Llamada:

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

Incluso podría hacer que este genérico funcione para 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:

Alternativa posible

SI sus requisitos permiten conjuntos idénticos para llamadas repetidas (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 cuasialeatoria a la velocidad del rayo. Actualice su selección aleatoria a intervalos o eventos de su elección.

Postgres 9.5 presenta TABLESAMPLE SYSTEM (n)

Donde nes un porcentaje. El manual:

Los métodos de muestreo BERNOULLIy SYSTEMaceptan cada uno un argumento único que es la fracción de la tabla a muestrear, expresada como un porcentaje entre 0 y 100 . Este argumento puede ser cualquier realexpresión valorada.

El énfasis en negrita es mío. Es muy rápido , pero el resultado no es exactamente al azar . El manual nuevamente:

El SYSTEMmétodo es significativamente más rápido que el BERNOULLImé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:

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);

Ver la respuesta de Evan para más detalles.

Pero eso todavía no es exactamente al azar.

Erwin Brandstetter
fuente
¿Dónde se define la tabla t ? ¿Debería r en lugar de t ?
Luc M
1
@LucM: se define aquí: JOIN bigtbl tque es la abreviatura de JOIN bigtbl AS t. tes un alias de tabla para bigtbl. Su propósito es acortar la sintaxis, pero no sería necesaria en este caso particular. Simplifiqué la consulta en mi respuesta y agregué una versión simple.
Erwin Brandstetter
¿Cuál es el propósito del rango de valores de generate_series (1,1100)?
Impresionante-o
@ Awesome-o: El objetivo es recuperar 1000 filas, empiezo con un 10% adicional para compensar algunas brechas o (poco probable pero posible) duplicar números aleatorios ... la explicación está en mi respuesta.
Erwin Brandstetter
Erwin, publiqué una variación de su "Posible alternativa": stackoverflow.com/a/23634212/430128 . Estaría interesado en tus pensamientos.
Raman
100

Puede examinar y comparar el plan de ejecución de ambos utilizando

EXPLAIN select * from table where random() < 0.01;
EXPLAIN select * from table order by random() limit 1000;

Una prueba rápida en una tabla grande 1 muestra que la ORDER BYprimera ordena la tabla completa y luego selecciona los primeros 1000 elementos. Ordenar una tabla grande no solo lee esa tabla sino que también implica leer y escribir archivos temporales. El where random() < 0.1único escanea la tabla completa una vez.

Para tablas grandes, esto podría no ser lo que desea, ya que incluso un escaneo completo de la tabla puede llevar mucho tiempo.

Una tercera propuesta sería

select * from table where random() < 0.01 limit 1000;

Éste detiene el escaneo de la tabla tan pronto como se han encontrado 1000 filas y, por lo tanto, regresa antes. Por supuesto, esto empantana un poco la aleatoriedad, pero tal vez esto sea lo suficientemente bueno en su caso.

Editar: además de estas consideraciones, puede consultar las preguntas ya hechas para esto. El uso de la consulta [postgresql] randomdevuelve bastantes resultados.

Y un artículo vinculado de depez que describe varios enfoques más:


1 "grande" como en "la tabla completa no cabe en la memoria".

AH
fuente
1
Buen punto sobre escribir el archivo temporal para hacer el pedido. Eso es un gran éxito de hecho. ¡Supongo que podríamos hacer random() < 0.02y luego barajar esa lista, entonces limit 1000! El tipo será menos costoso en unos pocos miles de filas (lol).
Donald Miner
El "select * from table where random () <0.05 limit 500;" Es uno de los métodos más fáciles para postgresql. Hicimos uso de esto en uno de nuestros proyectos donde necesitábamos seleccionar el 5% de los resultados y no más de 500 filas a la vez para el procesamiento.
tgharold
¿Por qué en el mundo consideraría una exploración completa O (n) para recuperar una muestra en una tabla de filas de 500 m? Es ridículamente lento en mesas grandes y completamente innecesario.
mafu
76

orden postgresql por random (), seleccione filas en orden aleatorio:

select your_columns from your_table ORDER BY random()

orden postgresql por random () con un distintivo:

select * from 
  (select distinct your_columns from your_table) table_alias
ORDER BY random()

orden postgresql por límite aleatorio una fila:

select your_columns from your_table ORDER BY random() limit 1
Eric Leschinski
fuente
1
select your_columns from your_table ORDER BY random() limit 1tómese ~ 2 minutos para ejecutar en 45mil filas
nguyên
¿Hay alguna manera de acelerar esto?
CpILL
43

Comenzando con PostgreSQL 9.5, hay una nueva sintaxis dedicada a obtener elementos aleatorios de una tabla:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

Este ejemplo le dará un 5% de elementos de mytable.

Ver más explicaciones en esta publicación de blog: http://www.postgresql.org/docs/current/static/sql-select.html

Mickaël Le Baillif
fuente
3
Una nota importante de los documentos: "El método SYSTEM realiza un muestreo a nivel de bloque con cada bloque que tiene la posibilidad especificada de ser seleccionado; se devuelven todas las filas en cada bloque seleccionado. El método SYSTEM es significativamente más rápido que el método BERNOULLI cuando pequeños porcentajes de muestreo se especifican, pero puede devolver una muestra menos aleatoria de la tabla como resultado de los efectos de agrupación ".
Tim
1
¿Hay alguna forma de especificar un número de filas en lugar de un porcentaje?
Flimm
44
Puede usar TABLESAMPLE SYSTEM_ROWS(400)para obtener una muestra de 400 filas aleatorias. Debe habilitar la extensión integradatsm_system_rows para usar esta declaración.
Mickaël Le Baillif
desafortunadamente no funciona con eliminar.
Gadelkareem
27

El que tenga ORDER BY será el más lento.

select * from table where random() < 0.01;va registro por registro y decide filtrarlo al azar o no. Esto va a ser O(N)porque solo necesita verificar cada registro una vez.

select * from table order by random() limit 1000;va a ordenar toda la mesa, luego elegir las primeras 1000. Aparte de cualquier magia vudú detrás de escena, el orden es O(N * log N).

La desventaja de random() < 0.01esto es que obtendrá un número variable de registros de salida.


Tenga en cuenta que hay una mejor manera de barajar un conjunto de datos que la ordenación aleatoria: The Fisher-Yates Shuffle , que se ejecuta O(N). Sin embargo, implementar el shuffle en SQL parece un gran desafío.

Donald Miner
fuente
3
Sin embargo, no hay ninguna razón por la que no pueda agregar un Límite 1 al final de su primer ejemplo. El único problema es que existe la posibilidad de que no recuperes registros, por lo que deberías considerarlo en tu código.
Relequestual
El problema con Fisher-Yates es que necesita tener todo el conjunto de datos en la memoria para poder seleccionarlo. No es factible para conjuntos de datos muy grandes :(
CpILL
16

Aquí hay una decisión que me funciona. Supongo que es muy simple de entender y ejecutar.

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;
Bogdan Surai
fuente
66
Creo que esta solución está funcionando como lo ORDER BY random()que funciona, pero podría no ser eficiente cuando se trabaja con una tabla grande.
Anh Cao
15
select * from table order by random() limit 1000;

Si sabe cuántas filas desea, consulte tsm_system_rows.

tsm_system_rows

El módulo proporciona el método de muestreo de tabla SYSTEM_ROWS, que se puede utilizar en la cláusula TABLESAMPLE de un comando SELECT.

Este método de muestreo de tabla acepta un argumento entero único que es el número máximo de filas para leer. La muestra resultante siempre contendrá exactamente esa cantidad de filas, a menos que la tabla no contenga suficientes filas, en cuyo caso se selecciona toda la tabla. Al igual que el método de muestreo SYSTEM incorporado, SYSTEM_ROWS realiza un muestreo a nivel de bloque, de modo que la muestra no es completamente aleatoria pero puede estar sujeta a efectos de agrupación, especialmente si solo se solicita un pequeño número de filas.

Primero instala la extensión

CREATE EXTENSION tsm_system_rows;

Entonces tu consulta,

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
Evan Carroll
fuente
2
Agregué un enlace a su respuesta agregada, es una mejora notable sobre el SYSTEMmétodo incorporado .
Erwin Brandstetter
Acabo de responder una pregunta aquí (registro único aleatorio) durante el cual realicé pruebas comparativas y pruebas considerables de las extensiones tsm_system_rowsy tsm_system_time. Hasta donde puedo ver, son prácticamente inútiles para cualquier cosa que no sea una selección absolutamente mínima de filas aleatorias. Le agradecería que pudiera echar un vistazo rápido y comentar sobre la validez o no de mi análisis.
Vérace
6

Si desea solo una fila, puede usar un offsetderivado calculado de count.

select * from table_name limit 1
offset floor(random() * (select count(*) from table_name));
Nelo Mitranim
fuente
2

Una variación de la vista materializada "Posible alternativa" esbozada por Erwin Brandstetter .

Digamos, por ejemplo, que no desea duplicados en los valores aleatorios que se devuelven. Por lo tanto, deberá establecer un valor booleano en la tabla primaria que contenga su conjunto de valores (no aleatorio).

Asumiendo que esta es la tabla de entrada:

id_values  id  |   used
           ----+--------
           1   |   FALSE
           2   |   FALSE
           3   |   FALSE
           4   |   FALSE
           5   |   FALSE
           ...

Rellene la ID_VALUEStabla según sea necesario. Luego, como lo describe Erwin, cree una vista materializada que aleatorice la ID_VALUEStabla una vez:

CREATE MATERIALIZED VIEW id_values_randomized AS
  SELECT id
  FROM id_values
  ORDER BY random();

Tenga en cuenta que la vista materializada no contiene la columna utilizada, ya que rápidamente quedará desactualizada. La vista tampoco necesita contener otras columnas que puedan estar en elid_values tabla.

Para obtener (y "consumir") valores aleatorios, use un ACTUALIZACIÓN-RETORNO activado id_values, seleccionando id_valuesdesde id_values_randomizedcon una combinación y aplicando los criterios deseados para obtener solo las posibilidades relevantes. Por ejemplo:

UPDATE id_values
SET used = TRUE
WHERE id_values.id IN 
  (SELECT i.id
    FROM id_values_randomized r INNER JOIN id_values i ON i.id = r.id
    WHERE (NOT i.used)
    LIMIT 5)
RETURNING id;

Cambie LIMITsegún sea necesario: si solo necesita un valor aleatorio a la vez, cambie LIMITa1 .

Con los índices adecuados id_values, creo que UPDATE-RETURNING debería ejecutarse muy rápidamente con poca carga. Devuelve valores aleatorios con una base de datos de ida y vuelta. Los criterios para las filas "elegibles" pueden ser tan complejos como sea necesario. Se pueden agregar nuevas filas a la id_valuestabla en cualquier momento, y la aplicación podrá acceder a ellas tan pronto como se actualice la vista materializada (que probablemente pueda ejecutarse en un momento de poca actividad). La creación y actualización de la vista materializada será lenta, pero solo debe ejecutarse cuando se agreguen nuevos ID a la id_valuestabla.

Raman
fuente
muy interesante. ¿Funcionaría si necesito no solo seleccionar sino también actualizar usando select..para actualizar con pg_try_advisory_xact_lock? (es decir, necesito muchas lecturas y escrituras concurrentes)
Mathieu
1

Una lección de mi experiencia:

offset floor(random() * N) limit 1No es más rápido que order by random() limit 1.

Pensé que el offsetenfoque sería más rápido porque debería ahorrar el tiempo de ordenar en Postgres. Resulta que no lo fue.

usuario10375
fuente
0

Agregue una columna llamada rcon tipo serial. Índice r.

Supongamos que tenemos 200,000 filas, vamos a generar un número aleatorio n, donde 0 <n <<= 200, 000.

Seleccione filas con r > n, ordénelasASC y seleccione la más pequeña.

Código:

select * from YOUR_TABLE 
where r > (
    select (
        select reltuples::bigint AS estimate
        from   pg_class
        where  oid = 'public.YOUR_TABLE'::regclass) * random()
    )
order by r asc limit(1);

El código se explica por sí mismo. La subconsulta en el medio se usa para estimar rápidamente los recuentos de filas de la tabla desde https://stackoverflow.com/a/7945274/1271094 .

En el nivel de aplicación, debe ejecutar la instrucción nuevamente si n> el número de filas o necesita seleccionar varias filas.

MK Yung
fuente
Me gusta esto porque es corto y elegante :) E incluso encontré una manera de mejorarlo: EXPLAIN ANALYZE me dice que así, un índice PKEY no se usará porque random () devuelve un doble, mientras que PKEY necesita un BIGINT.
fxtentacle
seleccione * de YOUR_TABLE donde r> (select (select reltuples :: bigint AS estimado de pg_class where oid = 'public.YOUR_TABLE' :: regclass) * random ()) :: BIGINT orden por r asc limit (1);
fxtentacle
0

Sé que llego un poco tarde a la fiesta, pero acabo de encontrar esta increíble herramienta llamada pg_sample :

pg_sample - extraer un pequeño conjunto de datos de muestra de una base de datos PostgreSQL más grande mientras se mantiene la integridad referencial.

Intenté esto con una base de datos de 350M filas y fue realmente rápido, no sé sobre la aleatoriedad .

./pg_sample --limit="small_table = *" --limit="large_table = 100000" -U postgres source_db | psql -U postgres target_db
Daniel Gerber
fuente