PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

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

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) lacunes.
  • Évidemment pas 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'un parcours séquentiel de la grande table, mais uniquement d'un parcours 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 coûteuse est le count(*) (pour les grandes tables). Compte tenu des spécifications ci-dessus, vous n'en avez pas besoin. Un devis fera très bien 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 inférieur à 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érer des nombres aléatoires dans le id espace. Vous avez "peu d'espaces vides", ajoutez donc 10 % (assez pour couvrir facilement les blancs) au nombre de lignes à récupérer.

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

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

  • Enfin couper le surplus id s qui n'ont pas été mangés par les dupes et les lacunes. Chaque ligne a une chance complètement égale à cueillir.

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 lacunes 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
   )
TABLE  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 d'espaces et que nous ne trouvons pas assez 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 des lacunes dans l'espace d'identification ou la récursivité peuvent s'épuiser avant que la limite ne soit atteinte - ou nous devons commencer avec un tampon suffisamment grand qui défie l'objectif d'optimisation des performances.

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

Le LIMIT extérieur arrête 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 tant que nous n'avons pas atteint la limite (à moins que la récursivité ne s'épuise). Il y a un certain nombre de pièges ici si vous allez 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
  LANGUAGE plpgsql VOLATILE ROWS 1000 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
   )
   TABLE  random_pick
   LIMIT  _limit;
END
$func$;

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 :

  • Refactoriser une fonction PL/pgSQL pour renvoyer la sortie de diverses requêtes SELECT

Alternative possible

SI vos exigences autorisent des ensembles identiques pour des répétitions appels (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. Actualisez votre choix aléatoire à intervalles ou événements de votre choix.

Postgres 9.5 introduit TABLESAMPLE SYSTEM (n)

n est un pourcentage. Le manuel :

Le BERNOULLI et SYSTEM les méthodes d'échantillonnage acceptent chacune un argument unique 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 quel real -expression valuée.

Bold emphase mienne. C'est très rapide , mais le résultat n'est pas exactement aléatoire . Encore le manuel :

Le SYSTEM méthode est nettement plus rapide que le 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 d'effets de regroupement.

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

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

Connexe :

  • Un moyen rapide de découvrir le nombre de lignes d'une table dans PostgreSQL

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