Comment prendre un échantillon aléatoire simple et efficace en SQL? La base de données en question exécute MySQL; ma table compte au moins 200 000 lignes et je veux un échantillon aléatoire simple d'environ 10 000.
La réponse "évidente" consiste à:
SELECT * FROM table ORDER BY Rand() LIMIT 10000
Pour les grandes tables, c'est trop lent: il appelle Rand () pour chaque ligne (ce qui le place déjà à O (n)), et les trie, ce qui en fait O (n lg n) au mieux. Existe-t-il un moyen de le faire plus rapidement que O (n)?
Remarque : Comme le souligne Andrew Mao dans les commentaires, si vous utilisez cette approche sur SQL Server, vous devez utiliser la fonction T-SQL NEWID ( ), car Rand () peut renvoyer la même valeur pour toutes les lignes .
MODIFICATION: 5 ANS PLUS TARD
J'ai rencontré à nouveau ce problème avec une table plus grande et j'ai fini par utiliser une version de la solution de @ ignorant, avec deux réglages:
Pour prendre un échantillon de 1000 éléments d'un tableau, je compte les lignes et échantillonne le résultat jusqu'à, en moyenne, 10 000 lignes avec la colonne freez_Rand:
SELECT COUNT(*) FROM table; -- Use this to determine Rand_low and Rand_high
SELECT *
FROM table
WHERE frozen_Rand BETWEEN %(Rand_low)s AND %(Rand_high)s
ORDER BY Rand() LIMIT 1000
(Mon implémentation réelle implique plus de travail pour m'assurer que je ne sous-échantillonne pas et pour envelopper manuellement Rand_high, mais l'idée de base est de "réduire votre N au hasard à quelques milliers.")
Bien que cela fasse des sacrifices, cela me permet d'échantillonner la base de données à l'aide d'une analyse d'index, jusqu'à ce qu'elle soit suffisamment petite pour ORDER BY Rand ().
Il y a une discussion très intéressante sur ce type de problème ici: http://www.titov.net/2005/09/21/do-not-use-order-by-Rand -ou-comment-obtenir-des-lignes-aléatoires-de-table /
Je pense sans aucune hypothèse sur la table que votre solution O (n lg n) est la meilleure. Bien qu'en fait avec un bon optimiseur ou une technique légèrement différente, la requête que vous répertoriez soit un peu meilleure, O (m * n) où m est le nombre de lignes aléatoires souhaitées, car il ne serait pas nécessaire de trier le grand tableau entier , il pourrait simplement rechercher le plus petit nombre de fois. Mais pour le type de chiffres que vous avez publié, m est plus grand que lg n de toute façon.
Trois hypothèses que nous pourrions essayer:
il y a une clé primaire unique et indexée dans la table
le nombre de lignes aléatoires que vous souhaitez sélectionner (m) est beaucoup plus petit que le nombre de lignes du tableau (n)
la clé primaire unique est un entier compris entre 1 et n sans espace
Avec seulement les hypothèses 1 et 2, je pense que cela peut être fait en O (n), bien que vous deviez écrire un index entier dans la table pour correspondre à l'hypothèse 3, donc ce n'est pas nécessairement un O (n) rapide. Si nous pouvons ADDITIONNELLEMENT supposer autre chose de Nice sur la table, nous pouvons faire la tâche dans O (m log m). L'hypothèse 3 serait une belle propriété supplémentaire pour travailler. Avec un générateur de nombres aléatoires Nice qui ne garantissait aucun doublon lors de la génération de m nombres consécutifs, une solution O(m) serait possible.
Compte tenu des trois hypothèses, l'idée de base est de générer m nombres aléatoires uniques entre 1 et n, puis de sélectionner les lignes avec ces clés dans le tableau. Je n'ai pas mysql ou quoi que ce soit devant moi en ce moment, donc en légèrement pseudocode cela ressemblerait à quelque chose comme:
create table RandomKeys (RandomKey int)
create table RandomKeysAttempt (RandomKey int)
-- generate m random keys between 1 and n
for i = 1 to m
insert RandomKeysAttempt select Rand()*n + 1
-- eliminate duplicates
insert RandomKeys select distinct RandomKey from RandomKeysAttempt
-- as long as we don't have enough, keep generating new keys,
-- with luck (and m much less than n), this won't be necessary
while count(RandomKeys) < m
NextAttempt = Rand()*n + 1
if not exists (select * from RandomKeys where RandomKey = NextAttempt)
insert RandomKeys select NextAttempt
-- get our random rows
select *
from RandomKeys r
join table t ON r.RandomKey = t.UniqueKey
Si vous étiez vraiment préoccupé par l'efficacité, vous pourriez envisager de faire la génération de clés aléatoires dans une sorte de langage procédural et d'insérer les résultats dans la base de données, car presque tout autre que SQL serait probablement meilleur pour le type de boucle et de génération de nombres aléatoires requis .
Je pense que la solution la plus rapide est
select * from table where Rand() <= .3
Voici pourquoi je pense que cela devrait faire l'affaire.
Cela suppose que Rand () génère des nombres dans une distribution uniforme. C'est le moyen le plus rapide de le faire.
J'ai vu que quelqu'un avait recommandé cette solution et ils ont été abattus sans preuve .. voici ce que je dirais à ce sujet -
mysql est très capable de générer des nombres aléatoires pour chaque ligne. Essaye ça -
sélectionnez Rand () dans INFORMATION_SCHEMA.TABLES limit 10;
Puisque la base de données en question est mySQL, c'est la bonne solution.
J'ai testé cette méthode pour être beaucoup plus rapide que ORDER BY Rand()
, donc elle s'exécute en O (n) temps, et le fait d'une manière impressionnante rapidement .
De http://technet.Microsoft.com/en-us/library/ms189108%28v=sql.105%29.aspx :
Version non MSSQL - Je n'ai pas testé cela
SELECT * FROM Sales.SalesOrderDetail
WHERE 0.01 >= Rand()
Version MSSQL:
SELECT * FROM Sales.SalesOrderDetail
WHERE 0.01 >= CAST(CHECKSUM(NEWID(), SalesOrderID) & 0x7fffffff AS float) / CAST (0x7fffffff AS int)
Cela sélectionnera ~ 1% des enregistrements. Donc, si vous avez besoin d'un nombre exact de pourcentages ou d'enregistrements à sélectionner, estimez votre pourcentage avec une certaine marge de sécurité, puis extrayez au hasard les enregistrements en excès de l'ensemble résultant, en utilisant la méthode ORDER BY Rand()
la plus chère.
J'ai pu améliorer encore cette méthode car j'avais une plage de valeurs de colonne indexée bien connue.
Par exemple, si vous avez une colonne indexée avec des entiers uniformément répartis [0..max], vous pouvez l'utiliser pour sélectionner au hasard N petits intervalles. Faites-le dynamiquement dans votre programme pour obtenir un ensemble différent pour chaque exécution de requête. Cette sélection de sous-ensembles sera O (N) , qui peut être de plusieurs ordres de grandeur plus petite que votre ensemble de données complet.
Dans mon test, j'ai réduit le temps nécessaire pour obtenir 20 (sur 20 mil) enregistrements d'échantillon de 3 minutes en utilisant ORDER BY Rand () jusqu'à 0,0 secondes !
Apparemment, dans certaines versions de SQL, il y a une commande TABLESAMPLE
, mais ce n'est pas dans toutes les implémentations SQL (notamment Redshift).
http://technet.Microsoft.com/en-us/library/ms189108 (v = sql.105) .aspx
Utilisez simplement
WHERE Rand() < 0.1
pour obtenir 10% des enregistrements ou
WHERE Rand() < 0.01
pour obtenir 1% des enregistrements, etc.
Je tiens à souligner que toutes ces solutions semblent échantillonner sans remplacement. La sélection des K premières lignes d'un tri aléatoire ou la jonction à une table contenant des clés uniques dans un ordre aléatoire produira un échantillon aléatoire généré sans remplacement.
Si vous souhaitez que votre échantillon soit indépendant, vous devrez échantillonner avec remplacement. Voir Question 25451034 pour un exemple de procédure à suivre en utilisant un JOIN d'une manière similaire à la solution de user12861. La solution est écrite pour T-SQL, mais le concept fonctionne dans n'importe quelle base de données SQL.
En commençant par l'observation que nous pouvons récupérer les identifiants d'une table (par exemple, le compte 5) sur la base d'un ensemble:
select *
from table_name
where _id in (4, 1, 2, 5, 3)
nous pouvons arriver au résultat que si nous pouvions générer la chaîne "(4, 1, 2, 5, 3)"
, alors nous aurions un moyen plus efficace que Rand()
.
Par exemple, en Java:
ArrayList<Integer> indices = new ArrayList<Integer>(rowsCount);
for (int i = 0; i < rowsCount; i++) {
indices.add(i);
}
Collections.shuffle(indices);
String inClause = indices.toString().replace('[', '(').replace(']', ')');
Si les identifiants ont des lacunes, alors l'arraylist initiale indices
est le résultat d'une requête SQL sur les identifiants.
Si vous avez besoin d'exactement m
lignes, de manière réaliste, vous générerez votre sous-ensemble d'ID en dehors de SQL. La plupart des méthodes nécessitent à un moment donné de sélectionner l'entrée "nth", et les tables SQL ne sont vraiment pas des tableaux du tout. L'hypothèse selon laquelle les clés sont consécutives afin de simplement joindre des entrées aléatoires entre 1 et le nombre est également difficile à satisfaire - MySQL par exemple ne le prend pas en charge nativement, et les conditions de verrouillage sont ... délicat =.
Voici une solution O(max(n, m lg n))
- time, O(n)
- space en supposant des touches BTREE simples:
O(n)
m
, et extrayez le sous-tableau [0:m-1]
Dans ϴ(m)
SELECT ... WHERE id IN (<subarray>)
) dans O(m lg n)
Toute méthode qui génère le sous-ensemble aléatoire en dehors de SQL doit avoir au moins cette complexité. La jointure ne peut pas être plus rapide que O(m lg n)
avec BTREE (donc les revendications O(m)
sont fantastiques pour la plupart des moteurs) et le shuffle est délimité en dessous de n
et m lg n
Et n'affecte pas le comportement asymptotique.
En pseudocode pythonique:
ids = sql.query('SELECT id FROM t')
for i in range(m):
r = int(random() * (len(ids) - i))
ids[i], ids[i + r] = ids[i + r], ids[i]
results = sql.query('SELECT * FROM t WHERE id IN (%s)' % ', '.join(ids[0:m-1])