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?
Compte tenu de vos spécifications (plus des informations supplémentaires dans les commentaires),
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 id
s à la grande table. Cela devrait être très rapide avec l'index en place.
Finalement, coupez les surplus id
s qui n’ont pas été mangés par des dupes et des trous Chaque ligne a une chance égale d'être égale.
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;
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.
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:
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.
TABLESAMPLE SYSTEM (n)
Où n
est un pourcentage. Le manuel:
Les méthodes d'échantillonnage
BERNOULLI
etSYSTEM
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 expressionreal
.
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éthodeBERNOULLI
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.
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".
select your_columns from your_table ORDER BY random()
select * from
(select distinct your_columns from your_table) table_alias
ORDER BY random()
select your_columns from your_table ORDER BY random() limit 1
À 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
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.
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;
_select * from table order by random() limit 1000;
_
Si vous savez combien de lignes vous voulez, consultez 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);
_
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));
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
.
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
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.
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.