web-dev-qa-db-fra.com

sélection rapide de lignes aléatoires dans Postgres

J'ai une table dans postgres qui contient quelques millions de lignes. J'ai vérifié sur Internet et j'ai trouvé ce qui suit

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

cela fonctionne, mais c'est vraiment lent ... existe-t-il un autre moyen de créer cette requête ou un moyen direct de sélectionner une ligne aléatoire sans lire tout le tableau? À propos, "myid" est un entier mais peut être un champ vide.

merci

84
Juan

Vous voudrez peut-être expérimenter avec OFFSET, comme dans

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

Le N est le nombre de lignes dans mytable. Vous devrez peut-être d'abord faire une SELECT COUNT(*) pour déterminer la valeur de N.

Mise à jour (par Antony Hatchkins)

Vous devez utiliser floor ici:

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

Considérons une table de 2 rangées; random()*N génère 0 <= x < 2 et, par exemple, SELECT myid FROM mytable OFFSET 1.7 LIMIT 1; renvoie 0 ligne en raison de l'arrondissement implicite au plus proche int.

92
NPE

PostgreSQL 9.5 a introduit une nouvelle approche pour une sélection d’échantillons beaucoup plus rapide: TABLESAMPLE

La syntaxe est

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

Ce n'est pas la solution optimale si vous souhaitez uniquement sélectionner une ligne, car vous devez connaître le nombre de jours du tableau pour calculer le pourcentage exact.

Pour éviter un compte lent et utiliser TABLESAMPLE rapide pour les tables de 1 ligne à des milliards de lignes, vous pouvez effectuer les opérations suivantes:

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

Cela n’aura peut-être pas l’air si élégant, mais c’est probablement plus rapide que les autres réponses.

Pour décider si vous souhaitez utiliser BERNULLI ou SYSTEM, lisez la différence à propos de http://blog.2ndquadrant.com/tablesample-in-postgresql-9-5-2/

43
alfonx

J'ai essayé cela avec une sous-requête et cela a bien fonctionné. Offset, du moins dans Postgresql v8.4.4, fonctionne correctement.

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;
34
John Coryat

Vous devez utiliser floor:

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

Vérifiez ce lien pour quelques options différentes. http://www.depesz.com/index.php/2007/09/16/my-ibly-on-getting-random-row/

Mise à jour: (A.Hatchkins)

Le résumé du (très) long article est le suivant.

L'auteur énumère quatre approches:

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

2) ORDER BY id where id>=random()*N LIMIT 1 - non uniforme s'il y a des lacunes

3) colonne aléatoire - doit être mise à jour de temps en temps

4) custom agrégat aléatoire - méthode rusée, pourrait être lente: random () doit être généré N fois

et suggère d'améliorer la méthode n ° 2 en utilisant

5) ORDER BY id where id=random()*N LIMIT 1 avec les requêtes suivantes si le résultat est vide.

14
Kuberchaun

J'ai mis au point une solution très rapide sans TABLESAMPLE. Beaucoup plus rapide que OFFSET random()*N LIMIT 1. Il ne nécessite même pas le nombre de tables.

L'idée est de créer un index d'expression avec des données aléatoires mais prévisibles, par exemple md5(primary key).

Voici un test avec des exemples de données de 1 million de lignes:

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;

Résultat:

 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

Cette requête peut parfois (avec une probabilité d'environ 1/Number_of_rows) renvoyer 0 ligne. Elle doit donc être vérifiée et réexécutée. De plus, les probabilités ne sont pas exactement les mêmes - certaines lignes sont plus probables que d'autres.

En comparaison:

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

Les résultats varient beaucoup, mais peuvent être très mauvais:

 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)
3
Tometzky

Le moyen le plus simple et le plus rapide d'extraire une ligne aléatoire consiste à utiliser le tsm_system_rows extension:

CREATE EXTENSION IF NOT EXISTS tsm_system_rows;

Ensuite, vous pouvez sélectionner le nombre exact de lignes que vous souhaitez:

SELECT myid  FROM mytable TABLESAMPLE SYSTEM_ROWS(1);

Ceci est disponible avec PostgreSQL 9.5 et versions ultérieures.

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

2
daamien