web-dev-qa-db-fra.com

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

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

select * from table where random() < 0.01;

Mais un autre recommande ceci:

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

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

Quelle approche est la meilleure? Quelles sont les différences? Quel est le meilleur moyen de sélectionner des lignes aléatoires?

290
nanounanue

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

  • Vous avez une colonne d’identification numérique (nombres entiers) avec seulement quelques (ou modérément) lacunes.
  • Evidemment pas ou peu d’écritures.
  • Votre colonne ID doit être indexée! Une clé primaire sert bien.

La requête ci-dessous ne nécessite pas une analyse séquentielle de la grande table, mais uniquement 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 potentiellement coûteuse est la count(*) (pour les énormes tables). Étant donné les spécifications ci-dessus, vous n'en avez pas besoin. Une estimation fera l'affaire, disponible presque sans frais ( explication détaillée ici ):

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

Tant que ct n'est pas beaucoup plus petit que id_span, la requête surperformera d'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'espace id. Vous avez "peu de lacunes", ajoutez donc 10% (assez pour couvrir facilement les blancs) au nombre de lignes à récupérer.

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

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

  • Finalement, coupez les surplus ids qui n’ont pas été mangés par des dupes et des trous Chaque ligne a une chance égale d'être égale.

Version courte

Vous pouvez simplifier cette requête. La CTE dans la requête ci-dessus est uniquement destinée à des fins pédagogiques:

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 plus petit excédent dans la requête de base. S'il y a trop de lacunes afin que nous ne trouvions pas assez de lignes dans la première itération, la rCTE continue à itérer avec le terme récursif. Nous avons encore besoin de relativement peu espaces dans l'espace ID, sinon la récursion risque de s'assécher avant que la limite ne soit atteinte - ou nous devons commencer avec une mémoire tampon suffisamment grande pour défier l'optimisation. performance.

Les doublons sont éliminés par le UNION dans le rCTE.

La LIMIT extérieure fait que le CTE s'arrête dès que nous avons assez 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 remplissions la limite (à moins que la récursion ne soit à sec). Si vous voulez le réécrire, vous rencontrerez un certain nombre de pièges.

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 la table en tant que type polymorphe et utilisez EXECUTE ... Mais cela dépasse le cadre de cette question. Voir:

Alternative possible

SI vos exigences permettent des ensembles identiques d'appels répétés (et nous parlons d'appels répétés), je considérerais un matérialisé voir . Exécutez une fois la requête ci-dessus et écrivez le résultat dans une table. Les utilisateurs obtiennent une sélection quasi aléatoire à la vitesse de la lumière. Actualisez votre choix aléatoire à des intervalles ou des événements de votre choix.

Postgres 9.5 présente TABLESAMPLE SYSTEM (n)

n est un pourcentage. Le manuel:

Les méthodes d'échantillonnage BERNOULLI et SYSTEM acceptent chacune un seul argument, qui est la fraction de la table à échantillonner, exprimée sous la forme d'un pourcentage compris entre 0 et 100 . Cet argument peut être n'importe quelle expression real.

Gras accent mien. C'est très rapide , mais le résultat est pas exactement aléatoire . Le manuel à nouveau:

La méthode SYSTEM est nettement plus rapide que la méthode BERNOULLI lorsque de faibles pourcentages d'échantillonnage sont spécifiés, mais elle peut renvoyer un échantillon moins aléatoire de la table à la suite d'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);

Apparenté, relié, connexe:

ou installez le module supplémentaire tsm_system_rows récupère exactement le nombre de lignes demandées (s'il y en a assez) et permet la syntaxe plus pratique:

SELECT * FROM big TABLESAMPLE SYSTEM_ROWS(1000);

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

Mais ce n'est pas encore vraiment aléatoire.

204
Erwin Brandstetter

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 une grande table1 montre que le ORDER BY trie d’abord la table complète puis choisit les 1000 premiers articles. Le tri d'une grande table non seulement lit cette table, mais implique également la lecture et l'écriture de fichiers temporaires. La where random() < 0.1 n'analyse le tableau complet qu'une seule fois.

Pour les tables volumineuses, il se peut que cela ne soit pas ce que vous souhaitiez, car même une analyse complète de la table peut être longue.

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 est donc renvoyé plus tôt. Bien sûr, cela alourdit un peu le hasard, mais peut-être que cela suffit 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] random renvoie assez de résultats.

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


1 "grand" comme dans "la table complète ne rentrera pas dans la mémoire".

89
A.H.

postgresql order by random (), sélectionne les lignes dans un ordre aléatoire:

select your_columns from your_table ORDER BY random()

postgresql order by random () avec un distinct:

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

postgresql order by random limit une ligne:

select your_columns from your_table ORDER BY random() limit 1
70
Eric Leschinski

À partir de PostgreSQL 9.5, une nouvelle syntaxe est dédiée à l'extraction d'éléments aléatoires 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

36
Mickaël Le Baillif

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 de manière aléatoire ou non. Cela va être O(N) car il suffit de vérifier chaque enregistrement une fois.

select * from table order by random() limit 1000; va trier toute la table, puis choisir les 1000 premiers. Mis à part toute magie vaudou en coulisse, la commande par est O(N * log N).

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


Notez qu'il existe un meilleur moyen de mélanger un ensemble de données que de le trier de manière aléatoire: The Fisher-Yates Shuffle , qui s'exécute dans O(N). Implémenter le shuffle en SQL semble être un défi, cependant.

27
Donald Miner

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;
15
Bogdan Surai
_select * from table order by random() limit 1000;
_

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

tsm_system_rows

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 argument entier unique qui correspond au nombre maximal de lignes à lire. L'exemple résultant contiendra toujours exactement ce nombre de lignes, à moins que la table ne contienne pas assez de lignes, auquel cas la table entière est sélectionnée. À l'instar de la méthode d'échantillonnage SYSTEM intégrée, SYSTEM_ROWS effectue un échantillonnage au niveau du bloc, de sorte que l'échantillon ne soit pas complètement aléatoire, mais puisse être soumis à des effets de regroupement, en particulier si le nombre de lignes est faible. demandé

D'abord installer l'extension

_CREATE EXTENSION tsm_system_rows;
_

Ensuite, votre requête,

_SELECT *
FROM table
TABLESAMPLE SYSTEM_ROWS(1000);
_
9
Evan Carroll

Si vous voulez juste une ligne, vous pouvez utiliser un offset calculé à partir de count.

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

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

Disons, par exemple, que vous ne voulez 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 randomisé).

En supposant qu'il s'agisse de la table d'entrée:

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

Renseignez la table ID_VALUES selon vos besoins. Ensuite, comme décrit par Erwin, créez une vue matérialisée qui randomise la table ID_VALUES 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 pouvant figurer dans la table id_values.

Pour obtenir (et "consommer") des valeurs aléatoires, utilisez UPDATE-RETURNING sur id_values, en sélectionnant id_values à partir de id_values_randomized avec une jointure et en appliquant les critères souhaités pour obtenir uniquement les informations pertinentes. possibilités. 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 LIMIT si nécessaire - si vous n'avez besoin que d'une valeur aléatoire à la fois, remplacez LIMIT par 1.

Avec les index appropriés sur id_values, je pense que UPDATE-RETURNING devrait s'exécuter très rapidement avec une charge faible. Il renvoie des valeurs aléatoires avec un aller-retour à la base de données. Les critères pour les lignes "éligibles" peuvent être aussi complexes que nécessaire. De nouvelles lignes peuvent être ajoutées à tout moment à la table id_values. Elles deviendront accessibles à l'application dès que la vue matérialisée sera actualisée (ce qui peut probablement être exécuté à une heure creuse). La création et l'actualisation de la vue matérialisée seront lents, mais il ne devra être exécuté que lorsque de nouveaux identifiants seront ajoutés à la table id_values.

2
Raman

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

pg_sample - extrait un petit jeu de données exemple à partir 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 très rapide, je ne connais pas le caractère aléatoire .

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

Ajoutez une colonne appelée r avec le type serial. Index r.

Supposons que nous avons 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-les ASC 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 la table à partir de https://stackoverflow.com/a/7945274/1271094 .

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

0
MK Yung

Une leçon de mon expérience:

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

Je pensais que l’approche offset serait plus rapide car elle permettrait d’économiser du temps de tri dans Postgres. Il s'avère que non.

0
user10375