sélection rapide de lignes aléatoires dans Postgres

95

J'ai une table en 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 ... y a-t-il un autre moyen de faire cette requête, ou un moyen direct de sélectionner une ligne aléatoire sans lire toute la table? Au fait, «myid» est un entier mais il peut s'agir d'un champ vide.

Juan
la source
1
Si vous souhaitez sélectionner plusieurs lignes aléatoires, consultez cette question: stackoverflow.com/q/8674718/247696
Flimm

Réponses:

97

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

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

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

Mise à jour (par Antony Hatchkins)

Vous devez utiliser floorici:

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

Prenons un tableau de 2 lignes; random()*Ngénère 0 <= x < 2et, par exemple, SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;retourne 0 lignes en raison de l'arrondi implicite à l'entier le plus proche.

NPE
la source
Cela a du sens d'utiliser un N inférieur à SELECT COUNT(*)?, je veux dire, ne pas utiliser toutes les valeurs du tableau mais seulement une partie d'entre elles?
Juan
@Juan Cela dépend de vos besoins.
NPE
en utilisant EXPLAIN SELECT ...des valeurs différentes de N donne le même coût pour la requête, alors je suppose qu'il vaut mieux choisir la valeur maximale de N.
Juan
3
voir un bug dans ma réponse ci
Antony Hatchkins
2
Cela a une erreur par une erreur. Il ne retournera jamais la première ligne et générera une erreur 1 / COUNT (*) car il tentera de renvoyer la ligne après la dernière ligne.
Ian
60

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 ne voulez qu'une seule ligne sélectionnée, car vous devez connaître le COUNT du tableau pour calculer le pourcentage exact.

Pour éviter un COUNT lent et utiliser TABLESAMPLE rapide pour les tables de 1 ligne à des milliards de lignes, vous pouvez faire:

 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'a peut-être pas l'air si élégant, mais est probablement plus rapide que toutes les autres réponses.

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

Alfonx
la source
2
C'est beaucoup plus rapide et plus facile que toute autre réponse - celle-ci devrait être au sommet.
Hayden Schiff
1
Pourquoi ne pouvez-vous pas simplement utiliser une sous-requête pour obtenir le décompte? SELECT * FROM my_table TABLESAMPLE SYSTEM(SELECT 1/COUNT(*) FROM my_table) LIMIT 1;?
machineghost
2
@machineghost "Pour éviter un COUNT lent ..." ... Si vos données sont si petites, que vous pouvez compter dans un temps raisonnable, foncez! :-)
alfonx
2
@machineghost Utilisé SELECT reltuples FROM pg_class WHERE relname = 'my_table'pour l'estimation du nombre.
Hynek -Pichi- Vychodil
@ Hynek-Pichi-Vychodil très bonne entrée! Pour s'assurer que l'estimation n'est pas obsolète, elle doit être effectuée récemment sous VACUUM ANALYZEd .. mais une bonne base de données doit quand même être correctement analysée .. Et tout dépend du cas d'utilisation spécifique. Habituellement, les tables énormes ne poussent pas si vite ... Merci!
alfonx
34

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

select * from mytable offset random() * (select count(*) from mytable) limit 1 ;
John Coryat
la source
En fait, la v8.4 est essentielle pour que cela fonctionne, ne fonctionne pas pour <= 8.3.
Antony Hatchkins
1
voir un bug dans ma réponse ci
Antony Hatchkins
30

Vous devez utiliser floor:

SELECT myid FROM mytable OFFSET floor(random()*N) LIMIT 1;
Antony Hatchkins
la source
Prenons un tableau de 2 lignes; random()*Ngénère 0 <= x <2 et, par exemple, SELECT myid FROM mytable OFFSET 1.7 LIMIT 1;retourne 0 lignes en raison de l'arrondi implicite à l'int plus proche.
Antony Hatchkins
Malheureusement, cela ne fonctionne pas si vous souhaitez utiliser une LIMITE plus élevée ... J'ai besoin de 3 éléments donc je dois utiliser la syntaxe ORDER BY RANDOM ().
Alexis Wilke
1
Trois requêtes consécutives seront toujours plus rapides qu'une order by random(), à peu près comme 3*O(N) < O(NlogN)- les chiffres réels seront légèrement différents en raison des indices.
Antony Hatchkins
Mon problème est que les 3 éléments doivent être distincts et WHERE myid NOT IN (1st-myid)et WHERE myid NOT IN (1st-myid, 2nd-myid)ne travaillerait depuis la décision est prise par le OFFSET. Hmmm ... Je suppose que je pourrais réduire N de 1 et 2 dans les deuxième et troisième SELECT.
Alexis Wilke
Pourriez-vous ou quelqu'un d'élargir cette réponse en expliquant pourquoi j'ai besoin d'utiliser floor()? Quel avantage offre-t-il?
ADTC
14

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

Mettre à jour: (A. Hatchkins)

Le résumé de l'article (très) long 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) agrégat aléatoire personnalisé - méthode astucieuse, peut ê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 des requêtes ultérieures si le résultat est vide.

Kuberchaun
la source
Je me demande pourquoi ils n'ont pas couvert OFFSET? Il est hors de question d'utiliser un ORDER juste pour obtenir une ligne aléatoire. Heureusement, OFFSET est bien couvert dans les réponses.
androidguy
4

Le moyen le plus simple et le plus rapide de récupérer une ligne aléatoire est d'utiliser l' tsm_system_rowsextension:

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

Daamien
la source
1
Attention, ce n'est pas complètement aléatoire. Sur les tables plus petites, je l'ai toujours renvoyé les premières lignes dans l'ordre.
Ben Aubin
1
oui cela est clairement expliqué dans la documentation (lien ci-dessus): «Comme la méthode d'échantillonnage SYSTEM intégrée, SYSTEM_ROWS effectue un échantillonnage au niveau des blocs, de sorte que l'échantillon n'est pas complètement aléatoire mais peut être soumis à des effets de clustering, surtout si seulement un petit le nombre de lignes est demandé. ». Si vous avez un petit jeu de données, le ORDER BY random() LIMIT 1;devrait être assez rapide.
daamien
J'ai vu ça. Je voulais juste que cela soit clair pour toute personne qui ne clique pas sur le lien ou si le lien meurt à l'avenir.
Ben Aubin
1
Il convient également de noter que cela ne fonctionnera que pour sélectionner des lignes aléatoires dans une table et ALORS filtrer, par opposition / par rapport à l'exécution d'une requête, puis à sélectionner un ou plusieurs enregistrements au hasard.
nomen
3

Je suis venu avec une solution très rapide sans TABLESAMPLE. Beaucoup plus rapide que OFFSET random()*N LIMIT 1. Il ne nécessite même pas de compte de table.

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 1M 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 considérablement, mais peuvent être assez 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)
Tometzky
la source
2
Rapide, oui. Vraiment aléatoire, non. Une valeur md5 qui se trouve être la valeur immédiatement supérieure après une autre valeur existante a une très faible chance d'être sélectionnée, tandis que les valeurs après un grand écart dans l'espace numérique ont une chance beaucoup plus grande (plus grande par le nombre de valeurs possibles entre les deux) . La distribution résultante n'est pas aléatoire.
Erwin Brandstetter
très intéressant, pourrait-il fonctionner dans le cas d'utilisation d'une requête de type loterie: la requête doit examiner tous les billets disponibles et ne renvoyer au hasard qu'un seul billet. puis-je utiliser un verrou pessimiste (sélectionnez ... pour la mise à jour) avec votre technique?
Mathieu
Pour tout ce qui concerne la loterie, vous devez vraiment utiliser un échantillonnage aléatoire équitable et cryptographiquement sécurisé - par exemple, choisissez un nombre aléatoire entre 1 et max (id) jusqu'à ce que vous trouviez l'ID existant. La méthode de cette réponse n'est ni juste ni sûre - elle est rapide. Utilisable pour des choses comme «obtenir au hasard 1% des lignes pour tester quelque chose», ou «afficher 5 entrées aléatoires».
Tometzky