Meilleure façon de sélectionner des lignes aléatoires PostgreSQL

345

Je veux une sélection aléatoire de lignes dans PostgreSQL, j'ai essayé ceci:

select * from table where random() < 0.01;

Mais certains autres recommandent ceci:

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

J'ai une très grande table avec 500 millions de lignes, je veux que ce soit rapide.

Quelle approche est la meilleure? Quelles sont les différences? Quelle est la meilleure façon de sélectionner des lignes aléatoires?

nanounanue
la source
1
Salut Jack, merci pour votre réponse, le temps d'exécution est plus lent dans l'ordre, mais je voudrais savoir quelle est la différence le cas échéant ...
nanounanue
Uhhh ... vous êtes les bienvenus. Alors, avez-vous essayé de comparer les différentes approches?
Il existe également des moyens beaucoup plus rapides. Tout dépend de vos besoins et de ce avec quoi vous devez travailler. Avez-vous besoin exactement de 1000 lignes? La table a-t-elle un identifiant numérique? Avec pas / peu / beaucoup de lacunes? Quelle est l'importance de la vitesse? Combien de demandes par unité de temps? Chaque demande a-t-elle besoin d'un ensemble différent ou peut-elle être la même pour une tranche de temps définie?
Erwin Brandstetter
6
La première option "(random () <0,01)" est mathématiquement incorrecte car vous ne pouvez obtenir aucune ligne en réponse si aucun nombre aléatoire n'est inférieur à 0,01, cela peut se produire dans tous les cas (quoique moins probable), quelle que soit la taille du tableau ou supérieur au seuil. La deuxième option a toujours raison
Herme
1
Si vous souhaitez sélectionner une seule ligne, consultez cette question: stackoverflow.com/q/5297396/247696
Flimm

Réponses:

230

Compte tenu de vos spécifications (plus d'informations supplémentaires dans les commentaires),

  • Vous avez une colonne d'ID numérique (nombres entiers) avec seulement quelques (ou modérément peu) lacunes.
  • Évidemment, aucune ou peu d'opérations d'écriture.
  • Votre colonne ID doit être indexée! Une clé primaire sert bien.

La requête ci-dessous n'a pas besoin d'une analyse séquentielle de la grande table, seulement une analyse d'index.

Tout d'abord, obtenez des estimations pour la requête principale:

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 seule partie éventuellement chère est la count(*)(pour les tables énormes). Compte tenu des spécifications ci-dessus, vous n'en avez pas besoin. Une estimation fera très bien l'affaire, disponible presque gratuitement ( explication détaillée ici ):

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

Tant qu'elle ctn'est pas beaucoup plus petite que id_span, la requête surpassera les autres approches.

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
  • Générez des nombres aléatoires dans l' idespace. Vous avez "quelques lacunes", alors ajoutez 10% (assez pour couvrir facilement les blancs) au nombre de lignes à récupérer.

  • Chacun idpeut être sélectionné plusieurs fois par hasard (bien que très peu probable avec un grand espace d'identification), alors regroupez les numéros générés (ou utilisez DISTINCT).

  • Rejoignez le ids à la grande table. Cela devrait être très rapide avec l'index en place.

  • Enfin, coupez les surplus idqui n'ont pas été mangés par les dupes et les lacunes. Chaque ligne a une chance tout à fait égale d'être sélectionnée.

Version courte

Vous pouvez simplifier cette requête. Le CTE dans la requête ci-dessus est uniquement à des fins éducatives:

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

Affiner avec rCTE

Surtout si vous n'êtes pas sûr des écarts et des estimations.

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

Nous pouvons travailler avec un surplus plus petit dans la requête de base. S'il y a trop de lacunes et que nous ne trouvons pas suffisamment de lignes dans la première itération, le rCTE continue à itérer avec le terme récursif. Nous avons encore besoin de relativement peu lacunes dans l'espace ID ou la récursivité peut se tarir avant que la limite soit atteinte - ou nous devons commencer avec un tampon suffisamment grand qui défie le but d'optimiser les performances.

Les doublons sont éliminés par le UNIONdans le rCTE.

L'extérieur LIMITfait arrêter le CTE dès que nous avons suffisamment de lignes.

Cette requête est soigneusement rédigée pour utiliser l'index disponible, générer des lignes réellement aléatoires et ne pas s'arrêter jusqu'à ce que nous atteignions la limite (à moins que la récursivité ne sèche). Il y a un certain nombre d'écueils ici si vous voulez le réécrire.

Envelopper dans la fonction

Pour une utilisation répétée avec des paramètres 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;

Appel:

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

Vous pouvez même faire en sorte que ce générique fonctionne pour n'importe quelle table: prenez le nom de la colonne PK et de la table comme type polymorphe et utilisez EXECUTE... Mais cela dépasse le cadre de cette question. Voir:

Alternative possible

SI vos besoins permettent des ensembles identiques pour les appels répétés (et nous parlons d'appels répétés), je considérerais une vue matérialisée . Exécutez la requête ci-dessus une fois et écrivez le résultat dans une table. Les utilisateurs obtiennent une sélection quasi aléatoire à la vitesse de l'éclair. Rafraîchissez votre choix aléatoire à intervalles ou événements de votre choix.

Postgres 9.5 présente TABLESAMPLE SYSTEM (n)

nest un pourcentage. Le manuel:

Les méthodes BERNOULLIet d' SYSTEMéchantillonnage acceptent chacune un seul argument qui est la fraction du tableau à échantillonner, exprimée en pourcentage entre 0 et 100 . Cet argument peut être n'importe realquelle expression évaluée.

Accentuation sur moi. C'est très rapide , mais le résultat n'est pas exactement aléatoire . Le manuel à nouveau:

La SYSTEMméthode est nettement plus rapide que la BERNOULLIméthode lorsque de petits pourcentages d'échantillonnage sont spécifiés, mais elle peut renvoyer un échantillon moins aléatoire du tableau en raison des effets de regroupement.

Le nombre de lignes renvoyées peut varier énormément. Pour notre exemple, pour obtenir environ 1000 lignes:

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

En relation:

Ou installez le module supplémentaire tsm_system_rows pour obtenir exactement le nombre de lignes demandées (s'il y en a assez) et autorisez la syntaxe la plus pratique:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

Voir la réponse d' Evan pour plus de détails.

Mais ce n'est toujours pas exactement aléatoire.

Erwin Brandstetter
la source
Où est définie la table t ? Doit-il r au lieu de t ?
Luc M
1
@LucM: Il est défini ici:, JOIN bigtbl tqui est l'abréviation de JOIN bigtbl AS t. test un alias de table pour bigtbl. Son but est de raccourcir la syntaxe mais cela ne serait pas nécessaire dans ce cas particulier. J'ai simplifié la requête dans ma réponse et ajouté une version simple.
Erwin Brandstetter
Quel est le but de la plage de valeurs de generate_series (1,1100)?
Awesome-o
@ Awesome-o: Le but est de récupérer 1000 lignes, je commence avec 10% supplémentaires pour compenser quelques lacunes ou (improbable mais possible) des nombres aléatoires en double ... l'explication est dans ma réponse.
Erwin Brandstetter le
Erwin, j'ai publié une variante de votre "Alternative possible": stackoverflow.com/a/23634212/430128 . Serait intéressé par vos pensées.
Raman
100

Vous pouvez examiner et comparer le plan d'exécution des deux en utilisant

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

Un test rapide sur un grand tableau 1 montre que le ORDER BYpremier trie le tableau complet puis sélectionne les 1000 premiers éléments. Le tri d'une grande table lit non seulement cette table mais implique également la lecture et l'écriture de fichiers temporaires. La where random() < 0.1seule analyse le tableau complet une fois.

Pour les grandes tables, cela peut ne pas être ce que vous voulez, car même une analyse complète de la table peut prendre trop de temps.

Une troisième proposition serait

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

Celui-ci arrête l'analyse de la table dès que 1000 lignes ont été trouvées et revient donc plus tôt. Bien sûr, cela réduit un peu le caractère aléatoire, mais c'est peut-être suffisant dans votre cas.

Edit: Outre ces considérations, vous pouvez consulter les questions déjà posées à ce sujet. L'utilisation de la requête [postgresql] randomrenvoie plusieurs hits.

Et un article lié de depez décrivant plusieurs autres approches:


1 "grand" comme dans "le tableau complet ne rentrera pas dans la mémoire".

AH
la source
1
Bon point sur l'écriture du fichier temporaire pour faire la commande. C'est vraiment un grand succès. Je suppose que nous pourrions faire random() < 0.02, puis mélanger cette liste, alors limit 1000! Le tri sera moins cher sur quelques milliers de lignes (lol).
Donald Miner
Le "select * from table where random () <0.05 limit 500;" est l'une des méthodes les plus simples pour postgresql. Nous l'avons utilisé dans l'un de nos projets où nous devions sélectionner 5% des résultats et pas plus de 500 lignes à la fois pour le traitement.
tgharold
Pourquoi diable envisageriez-vous jamais une analyse complète O (n) pour récupérer un échantillon sur une table de 500 mètres? Il est ridiculement lent sur les grandes tables et totalement inutile.
mafu
76

ordre postgresql par random (), sélectionnez les lignes dans un ordre aléatoire:

select your_columns from your_table ORDER BY random()

ordre postgresql par random () avec un distinct:

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

ordre postgresql par limite aléatoire d'une ligne:

select your_columns from your_table ORDER BY random() limit 1
Eric Leschinski
la source
1
select your_columns from your_table ORDER BY random() limit 1prendre ~ 2 minutes pour exécuter sur 45mil
nguyên
existe-t-il un moyen d'accélérer cela?
CpILL
43

À partir de PostgreSQL 9.5, il existe une nouvelle syntaxe dédiée à l'obtention d'éléments aléatoires à partir d'une table:

SELECT * FROM mytable TABLESAMPLE SYSTEM (5);

Cet exemple vous donnera 5% d'éléments de mytable.

Voir plus d'explications sur ce billet de blog: http://www.postgresql.org/docs/current/static/sql-select.html

Mickaël Le Baillif
la source
3
Une note importante de la documentation: "La méthode SYSTEM effectue un échantillonnage au niveau du bloc avec chaque bloc ayant la chance spécifiée d'être sélectionné; toutes les lignes de chaque bloc sélectionné sont retournées. La méthode SYSTEM est beaucoup plus rapide que la méthode BERNOULLI lorsque de petits pourcentages d'échantillonnage sont spécifiés, mais il peut renvoyer un échantillon moins aléatoire de la table en raison des effets de regroupement. "
Tim
1
Est-il possible de spécifier un nombre de lignes au lieu d'un pourcentage?
Flimm
4
Vous pouvez utiliser TABLESAMPLE SYSTEM_ROWS(400)pour obtenir un échantillon de 400 lignes aléatoires. Vous devez activer l' extension intégréetsm_system_rows pour utiliser cette instruction.
Mickaël Le Baillif
ne fonctionne malheureusement pas avec la suppression.
Gadelkareem
27

Celui avec le ORDER BY va être le plus lent.

select * from table where random() < 0.01;va enregistrement par enregistrement, et décide de le filtrer au hasard ou non. Cela va être O(N)dû au fait qu'il n'a besoin de vérifier chaque enregistrement qu'une seule fois.

select * from table order by random() limit 1000;va trier la table entière, puis choisir les 1000 premiers. En dehors de toute magie vaudou dans les coulisses, l'ordre est O(N * log N).

L'inconvénient de celui- random() < 0.01ci est que vous obtiendrez un nombre variable d'enregistrements de sortie.


Remarque, il existe une meilleure façon de mélanger un ensemble de données que de trier par hasard: le Fisher-Yates Shuffle , qui s'exécute O(N). Cependant, implémenter le shuffle dans SQL semble être tout un défi.

Donald Miner
la source
3
Il n'y a aucune raison pour que vous ne puissiez pas ajouter une limite 1 à la fin de votre premier exemple. Le seul problème est qu'il y a un risque que vous ne récupériez aucun enregistrement, vous devez donc en tenir compte dans votre code.
Relequestual
Le problème avec le Fisher-Yates est que vous devez avoir l'ensemble de données en mémoire afin de le sélectionner. Non réalisable pour les très grands ensembles de données :(
CpILL
16

Voici une décision qui fonctionne pour moi. Je suppose que c'est très simple à comprendre et à exécuter.

SELECT 
  field_1, 
  field_2, 
  field_2, 
  random() as ordering
FROM 
  big_table
WHERE 
  some_conditions
ORDER BY
  ordering 
LIMIT 1000;
Bogdan Surai
la source
6
Je pense que cette solution fonctionne comme ORDER BY random()ce qui fonctionne mais peut ne pas être efficace lorsque vous travaillez avec une grande table.
Anh Cao
15
select * from table order by random() limit 1000;

Si vous savez combien de lignes vous souhaitez, consultez tsm_system_rows.

tsm_system_rows

Le module fournit la méthode d'échantillonnage de table SYSTEM_ROWS, qui peut être utilisée dans la clause TABLESAMPLE d'une commande SELECT.

Cette méthode d'échantillonnage de table accepte un seul argument entier qui est le nombre maximal de lignes à lire. L'échantillon résultant contiendra toujours exactement autant de lignes, sauf si la table ne contient pas suffisamment de lignes, auquel cas la table entière est sélectionnée. Comme la méthode d'échantillonnage SYSTEM intégrée, SYSTEM_ROWS effectue un échantillonnage au niveau du bloc, de sorte que l'échantillon n'est pas complètement aléatoire mais peut être soumis à des effets de regroupement, en particulier si seul un petit nombre de lignes est demandé.

Installez d'abord l'extension

CREATE EXTENSION tsm_system_rows;

Ensuite, votre requête,

SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
Evan Carroll
la source
2
J'ai ajouté un lien vers votre réponse ajoutée, c'est une amélioration notable par rapport à la SYSTEMméthode intégrée .
Erwin Brandstetter
Je viens de répondre à une question ici (enregistrement unique aléatoire) au cours de laquelle j'ai effectué une analyse comparative et des tests considérables des extensions tsm_system_rowset tsm_system_time. Pour autant que je puisse voir, ils sont pratiquement inutiles pour quoi que ce soit, mais une sélection absolument minimale de lignes aléatoires. Je vous serais reconnaissant de bien vouloir jeter un rapide coup d'œil et de commenter la validité ou non de mon analyse.
Vérace
6

Si vous ne voulez qu'une seule ligne, vous pouvez utiliser un offsetdérivé calculé à partir de count.

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

Une variante de la vue matérialisée «Alternative possible» décrite par Erwin Brandstetter est possible.

Supposons, par exemple, que vous ne souhaitiez pas de doublons dans les valeurs aléatoires renvoyées. Vous devrez donc définir une valeur booléenne sur la table primaire contenant votre ensemble de valeurs (non aléatoire).

En supposant que c'est la table d'entrée:

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

Remplissez le ID_VALUEStableau selon vos besoins. Ensuite, comme décrit par Erwin, créez une vue matérialisée qui randomise la ID_VALUEStable une fois:

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

Notez que la vue matérialisée ne contient pas la colonne utilisée, car celle-ci deviendra rapidement obsolète. La vue ne doit pas non plus contenir d'autres colonnes qui peuvent se trouverid_values tableau.

Afin d'obtenir (et « consommer ») des valeurs aléatoires, un UPDATE utiliser REVIENT sur id_values, la sélection id_valuesde id_values_randomizedavec une jointure, et appliquer les critères souhaités pour obtenir que des possibilités pertinentes. Par exemple:

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;

Modifiez LIMITsi nécessaire - si vous n'avez besoin que d'une seule valeur aléatoire à la fois, passez LIMITà 1.

Avec les index appropriés id_values, je pense que la MISE À JOUR-RETOUR devrait s'exécuter très rapidement avec peu de charge. Il renvoie des valeurs aléatoires avec un aller-retour de base de données. Les critères pour les lignes "éligibles" peuvent être aussi complexes que requis. De nouvelles lignes peuvent être ajoutées à la id_valuestable à tout moment et elles deviendront accessibles à l'application dès que la vue matérialisée sera actualisée (ce qui peut probablement être exécuté à un moment hors pointe). La création et l'actualisation de la vue matérialisée seront lentes, mais elles ne doivent être exécutées que lorsque de nouveaux identifiants sont ajoutés à la id_valuestable.

Raman
la source
très intéressant. Est-ce que cela fonctionnerait si je dois non seulement sélectionner mais aussi mettre à jour en utilisant select..for update avec un pg_try_advisory_xact_lock? (c'est-à-dire que j'ai besoin de plusieurs lectures ET écritures simultanées)
Mathieu
1

Une leçon de mon expérience:

offset floor(random() * N) limit 1n'est pas plus rapide que order by random() limit 1.

Je pensais que l' offsetapproche serait plus rapide car elle devrait faire gagner du temps au tri dans Postgres. Il s'avère que ce n'était pas le cas.

user10375
la source
0

Ajoutez une colonne appelée ravec type serial. Index r.

Supposons que nous ayons 200 000 lignes, nous allons générer un nombre aléatoire n, où 0 <n <<= 200 000.

Sélectionnez les lignes avec r > n, triez-lesASC et sélectionnez la plus petite.

Code:

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

Le code est explicite. La sous-requête au milieu est utilisée pour estimer rapidement le nombre de lignes de table à partir de https://stackoverflow.com/a/7945274/1271094 .

Au niveau de l'application, vous devez exécuter à nouveau l'instruction si n> le nombre de lignes ou vous devez sélectionner plusieurs lignes.

MK Yung
la source
J'aime ça parce qu'il est court et élégant :) Et j'ai même trouvé un moyen de l'améliorer: EXPLAIN ANALYZE me dit que comme ça, un index PKEY ne sera pas utilisé car random () renvoie un double, alors que le PKEY a besoin d'un BIGINT.
fxtentacle
select * from YOUR_TABLE where r> (select (select reltuples :: bigint AS estimation from pg_class where oid = 'public.YOUR_TABLE' :: regclass) * random ()) :: BIGINT order by r asc limit (1);
fxtentacle
0

Je sais que je suis un peu en retard à la fête, mais je viens de trouver cet outil génial appelé pg_sample :

pg_sample - extraire un petit ensemble de données échantillon d'une base de données PostgreSQL plus grande tout en maintenant l'intégrité référentielle.

J'ai essayé cela avec une base de données de 350 millions de lignes et c'était vraiment rapide, je ne sais pas sur le caractère aléatoire .

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