selección rápida de filas al azar en Postgres

98

Tengo una tabla en postgres que contiene un par de millones de filas. He comprobado en Internet y he encontrado lo siguiente

SELECT myid FROM mytable ORDER BY RANDOM() LIMIT 1;

Funciona, pero es muy lento ... ¿hay otra forma de hacer esa consulta, o una forma directa de seleccionar una fila aleatoria sin leer toda la tabla? Por cierto, 'myid' es un número entero pero puede ser un campo vacío.

Juan
fuente
1
Si desea seleccionar varias filas aleatorias, consulte esta pregunta: stackoverflow.com/q/8674718/247696
Flimm

Respuestas:

99

Es posible que desee experimentar con OFFSET, como en

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

El Nes el número de filas en mytable. Es posible que primero deba hacer una SELECT COUNT(*)para averiguar el valor de N.

Actualización (por Antony Hatchkins)

Debes usar flooraquí:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;

Considere una tabla de 2 filas; random()*Ngenera 0 <= x < 2y, por ejemplo, SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;devuelve 0 filas debido al redondeo implícito al int más cercano.

NPE
fuente
¿Tiene sentido usar una N menor que SELECT COUNT(*)?, quiero decir, no usar todos los valores en la tabla sino solo una parte de ellos?
Juan
@Juan Eso depende de sus requisitos.
NPE
usar EXPLAIN SELECT ...con diferentes valores de N da el mismo costo para la consulta, entonces supongo que es mejor ir por el valor máximo de N.
Juan
3
vea una
corrección de errores
2
Esto tiene un error de uno. Nunca devolverá la primera fila y generará un error 1 / COUNT (*) porque intentará devolver la fila después de la última fila.
Ian
62

PostgreSQL 9.5 introdujo un nuevo enfoque para una selección de muestras mucho más rápida: TABLESAMPLE

La sintaxis es

SELECT * FROM my_table TABLESAMPLE BERNOULLI(percentage);
SELECT * FROM my_table TABLESAMPLE SYSTEM(percentage);

Esta no es la solución óptima si solo desea seleccionar una fila, porque necesita saber el CONTADOR de la tabla para calcular el porcentaje exacto.

Para evitar un RECUENTO lento y usar TABLESAMPLE rápido para tablas de 1 fila a miles de millones de filas, puede hacer:

 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.000001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.00001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.0001) LIMIT 1;
 -- if you got no result:
 SELECT * FROM my_table TABLESAMPLE SYSTEM(0.001) LIMIT 1;
 ...

Puede que esto no parezca tan elegante, pero probablemente sea más rápido que cualquiera de las otras respuestas.

Para decidir si desea utilizar BERNULLI oder SYSTEM, lea acerca de la diferencia en http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/

alfonx
fuente
2
Esto es mucho más rápido y fácil que cualquier otra respuesta; esta debería estar en la parte superior.
Hayden Schiff
1
¿Por qué no puede usar una subconsulta para obtener el recuento? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
machineghost
2
@machineghost "Para evitar un RECUENTO lento ..." ... Si sus datos son tan pequeños que puede contar en un tiempo razonable, ¡adelante! :-)
alfonx
2
@machineghost Se utiliza SELECT reltuples FROM pg_class WHERE relname = 'my_table'para estimar el recuento.
Hynek -Pichi- Vychodil
@ Hynek-Pichi-Vychodil muy buena entrada! Para asegurarse de que la estimación no esté desactualizada, tiene que ser ANALIZADA DE VACÍO recientemente ... pero una buena base de datos debe analizarse adecuadamente de todos modos ... Y todo depende del caso de uso específico. Por lo general, las mesas enormes no crecen tan rápido ... ¡Gracias!
alfonx
34

Intenté esto con una subconsulta y funcionó bien. Offset, al menos en Postgresql v8.4.4 funciona bien.

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;
John Coryat
fuente
De hecho, la v8.4 es esencial para que esto funcione, no funciona para <= 8.3.
Antony Hatchkins
1
vea una
corrección de errores
32

Necesitas usar floor:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;
Antony Hatchkins
fuente
Considere una tabla de 2 filas; random()*Ngenera 0 <= x <2 y, por ejemplo, SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;devuelve 0 filas debido al redondeo implícito al int más cercano.
Antony Hatchkins
Desafortunadamente, esto no funciona si desea usar un LÍMITE más alto ... Necesito obtener 3 elementos, por lo que necesito usar la sintaxis ORDER BY RANDOM ().
Alexis Wilke
1
Tres consultas consecutivas seguirán siendo más rápidas que una order by random(), aproximadamente como 3*O(N) < O(NlogN): las cifras reales serán ligeramente diferentes debido a los índices.
Antony Hatchkins
Mi problema es que los 3 elementos tienen que ser distinto, y es WHERE myid NOT IN (1st-myid)y WHERE myid NOT IN (1st-myid, 2nd-myid)no funcionaría ya que la decisión es tomada por la compensación. Hmmm ... Creo que podría reducir N en 1 y 2 en el segundo y tercer SELECT.
Alexis Wilke
¿Podría usted o alguien ampliar esta respuesta con una respuesta a por qué necesito usar floor()? ¿Qué ventaja ofrece?
ADTC
14

Consulte este enlace para ver algunas opciones diferentes. http://www.depesz.com/index.php/2007/09/16/my- Thoughts-on-getting-random-row/

Actualizar: (A. Hatchkins)

El resumen del (muy) extenso artículo es el siguiente.

El autor enumera cuatro enfoques:

1) ORDER BY random() LIMIT 1; - lento

2) ORDER BY id where id>=random()*N LIMIT 1- no uniforme si hay espacios

3) columna aleatoria: debe actualizarse de vez en cuando

4) agregado aleatorio personalizado - método astuto, podría ser lento: random () debe generarse N veces

y sugiere mejorar el método n. ° 2 utilizando

5) ORDER BY id where id=random()*N LIMIT 1 con consultas posteriores si el resultado está vacío.

Kuberchaun
fuente
Me pregunto por qué no cubrieron OFFSET. Usar un ORDER está fuera de discusión solo para obtener una fila aleatoria. Afortunadamente, OFFSET está bien cubierto en las respuestas.
androidguy
4

La forma más fácil y rápida de obtener una fila aleatoria es usar la tsm_system_rowsextensión:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

Luego puede seleccionar el número exacto de filas que desea:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

Está disponible con PostgreSQL 9.5 y versiones posteriores.

Ver: https://www.postgresql.org/docs/current/static/tsm-system-rows.html

daamien
fuente
1
Advertencia justa, esto no es completamente aleatorio. En tablas más pequeñas, siempre he tenido que devolver las primeras filas en orden.
Ben Aubin
1
sí, esto se explica claramente en la documentación (enlace arriba): «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 agrupamiento, especialmente si solo es un pequeño se solicita el número de filas. ». Si tiene un conjunto de datos pequeño, ORDER BY random() LIMIT 1;debería ser lo suficientemente rápido.
daamien
Vi eso. Solo quería dejar en claro a cualquiera que no haga clic en el enlace o si el enlace muere en el futuro.
Ben Aubin
1
También vale la pena señalar que esto solo funcionará para seleccionar filas aleatorias de una tabla y LUEGO filtrar, en lugar de / en comparación con ejecutar una consulta y luego elegir uno o algunos registros al azar.
nomen
3

Se me ocurrió una solución muy rápida sin TABLESAMPLE. Mucho más rápido que OFFSET random()*N LIMIT 1. Ni siquiera requiere contar la mesa.

La idea es crear un índice de expresión con datos aleatorios pero predecibles, por ejemplo md5(primary key).

Aquí hay una prueba con datos de muestra de 1 millón de filas:

create table randtest (id serial primary key, data int not null);

insert into randtest (data) select (random()*1000000)::int from generate_series(1,1000000);

create index randtest_md5_id_idx on randtest (md5(id::text));

explain analyze
select * from randtest where md5(id::text)>md5(random()::text)
order by md5(id::text) limit 1;

Resultado:

 Limit  (cost=0.42..0.68 rows=1 width=8) (actual time=6.219..6.220 rows=1 loops=1)
   ->  Index Scan using randtest_md5_id_idx on randtest  (cost=0.42..84040.42 rows=333333 width=8) (actual time=6.217..6.217 rows=1 loops=1)
         Filter: (md5((id)::text) > md5((random())::text))
         Rows Removed by Filter: 1831
 Total runtime: 6.245 ms

Esta consulta puede a veces (con una probabilidad de 1 / Number_of_rows) devolver 0 filas, por lo que es necesario verificarla y volver a ejecutarla. Además, las probabilidades no son exactamente las mismas: algunas filas son más probables que otras.

Para comparacion:

explain analyze SELECT id FROM randtest OFFSET random()*1000000 LIMIT 1;

Los resultados varían ampliamente, pero pueden ser bastante malos:

 Limit  (cost=1442.50..1442.51 rows=1 width=4) (actual time=179.183..179.184 rows=1 loops=1)
   ->  Seq Scan on randtest  (cost=0.00..14425.00 rows=1000000 width=4) (actual time=0.016..134.835 rows=915702 loops=1)
 Total runtime: 179.211 ms
(3 rows)
Tometzky
fuente
2
Rápido, sí. Verdaderamente aleatorio, no. Un valor md5 que resulta ser el siguiente valor mayor después de otro valor existente tiene una probabilidad muy pequeña de ser elegido, mientras que los valores después de una gran brecha en el espacio numérico tienen una probabilidad mucho mayor (mayor por el número de valores posibles en el medio) . La distribución resultante no es aleatoria.
Erwin Brandstetter
muy interesante, ¿podría funcionar en un caso de uso de una consulta similar a la de una lotería? La consulta debe buscar en todos los boletos disponibles y devolver aleatoriamente solo UN solo boleto. también puedo usar un candado pesimista (seleccione ... para actualizar) con su técnica?
Mathieu
Para cualquier cosa relacionada con la lotería, realmente debe usar un muestreo aleatorio justo y criptográficamente seguro; por ejemplo, elija un número aleatorio entre 1 y max (id) hasta que encuentre la identificación existente. El método de esta respuesta no es justo ni seguro, es rápido. Se puede usar para cosas como "obtener un 1% de filas al azar para probar algo" o "mostrar 5 entradas al azar".
Tometzky