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 ct
n'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' id
espace. Vous avez "quelques lacunes", alors ajoutez 10% (assez pour couvrir facilement les blancs) au nombre de lignes à récupérer.
Chacun id
peut ê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 id
s à la grande table. Cela devrait être très rapide avec l'index en place.
Enfin, coupez les surplus id
qui 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 UNION
dans le rCTE.
L'extérieur LIMIT
fait 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.
Où n
est un pourcentage. Le manuel:
Les méthodes BERNOULLI
et 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 real
quelle 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 SYSTEM
méthode est nettement plus rapide que la BERNOULLI
mé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.