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 utilisezDISTINCT
). -
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)
Où n
est un pourcentage. Le manuel :
Le
BERNOULLI
etSYSTEM
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 quelreal
-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 leBERNOULLI
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.