Échantillons aléatoires simples à partir D'une base de données Sql
Comment puis-je prendre un échantillon aléatoire simple efficace en SQL? La base de données en question tourne MySQL; ma table est au moins 200.000 lignes, et je veux un échantillon aléatoire simple d'environ 10.000.
la réponse" évidente" est à:
SELECT * FROM table ORDER BY RAND() LIMIT 10000
pour les grandes tables, c'est trop lent: il appelle RAND() pour chaque rang (qui le met déjà à O(n)), et les trie, ce qui le rend O(N lg n) au mieux. Y a-t-il un moyen de le faire plus rapidement que O(n)?
Note : comme le souligne Andrew Mao dans les commentaires, si vous utilisez cette approche sur SQL Server, vous devriez utiliser la fonction T-SQL NEWID(), car Rand () peut retourner la même valeur pour toutes les lignes .
EDIT: 5 ANS PLUS TARD
j'ai rencontré ce problème à nouveau avec une plus grande table, et j'ai fini par utiliser une version de la solution de @ignorant, avec deux modifications:
- échantillonner les lignes à 2-5x la taille de mon échantillon désiré, à peu de frais D'ordre Par RAND ()
- sauvegarder le résultat de RAND() dans une colonne indexée sur chaque insertion/mise à jour. (Si votre ensemble de données n'est pas très chargé, vous devrez peut-être trouver un autre moyen de garder cette colonne à jour.)
pour prendre un échantillon de 1000 articles d'une table, je compte les lignes et j'échantillonne le résultat jusqu'à, en moyenne, 10.000 lignes avec la colonne frozen_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 actuelle implique plus de travail pour m'assurer que je ne sous-échantillonne pas, et pour envelopper manuellement rand_high autour, mais l'idée de base est " coupez votre N au hasard à quelques milliers.")
bien que cela fasse quelques sacrifices, cela me permet d'échantillonner la base de données en bas en utilisant un balayage d'index, jusqu'à ce qu'elle soit assez petite pour commander par RAND() à nouveau.
9 réponses
il y a une discussion très intéressante sur ce genre de sujet ici: http://www.titov.net/2005/09/21/do-not-use-order-by-rand-or-how-to-get-random-rows-from-table /
je pense qu'avec absolument aucune hypothèse sur la table que votre O(n lg n) est la meilleure solution. Bien qu'en fait avec un bon optimiseur ou une technique légèrement différente la requête que vous énumérez peut être un peu mieux, O (M*N) où m est le nombre de lignes aléatoires désirées, comme il ne serait pas nécessaire de trier l'ensemble du grand tableau, il pourrait juste rechercher les plus petits m fois. Mais pour le genre de chiffres que vous avez posté, 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, indexée, dans le tableau
-
le nombre de lignes aléatoires que vous voulez sélectionner (m) est beaucoup plus petit que le nombre de lignes dans le tableau (n)
-
la clé primaire unique est un entier qui varie de 1 À n sans aucune discontinuité
"
avec seulement les hypothèses 1 et 2 je pense que cela peut être fait dans O(n), bien que vous aurez besoin d'écrire un index entier à la table pour correspondre à l'hypothèse 3, donc ce n'est pas nécessairement un O(N) rapide. Si nous pouvons en outre supposer quelque chose d'autre agréable sur la table, nous pouvons faire la tâche en O(M log m). Hypothèse 3 serait une belle propriété supplémentaire facile à travailler avec. Avec un générateur de nombres aléatoires agréable qui a garanti aucun doublon lors de la génération des nombres m dans une rangée, une solution O(m) serait possible.
étant donné les trois hypothèses, l'idée de base est de générer des 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 en face de moi en ce moment, donc en un peu de pseudocode, ça aurait l'air 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 mieux à la sorte 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.
- il créera un nombre aléatoire pour chaque ligne. Le nombre est entre 0 et 1
- il évalue s'il faut afficher cette ligne si le nombre généré est entre 0 et .3 (30%).
cela suppose que rand() génère des nombres dans une distribution uniforme. C'est le le moyen le plus rapide pour ce faire.
j'ai vu que quelqu'un avait recommandé Cette solution et ils ont été abattus sans preuve.. voici ce que je dirais -
- C'est O(n), mais aucun tri n'est requis, donc il est plus rapide que le O(N lg n)
-
mysql est très capable de générer des nombres aléatoires pour chaque ligne. Essayez ceci -
sélectionnez rand() à partir de INFORMATION_SCHEMA.Tableaux limite 10;
comme la base de données en question Est mySQL, c'est la bonne solution.
plus rapide que L'ordre de RAND ()
j'ai testé cette méthode pour être beaucoup plus rapide que ORDER BY RAND()
, donc il fonctionne dans O(n) temps, et fait si impressionnant rapide.
de http://technet.microsoft.com/en-us/library/ms189108%28v=sql.105%29.aspx :
Non-MSSQL version -- Je n'ai pas testé ce
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 du nombre exact de percents ou des enregistrements pour être sélectionné, estimez votre pourcentage avec une certaine marge de sécurité, puis plumer au hasard des enregistrements excédentaires de l'ensemble résultant, en utilisant la méthode plus coûteuse ORDER BY RAND()
.
Encore Plus Rapide
j'ai été en mesure d'améliorer encore plus sur cette méthode parce que j'ai eu une gamme bien connue de valeur de colonne indexée.
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 cela dynamiquement dans votre programme pour obtenir un ensemble différent pour chaque requête exécutée. Cette sélection de sous-ensemble sera O (N) , qui peut être de plusieurs ordres de grandeur plus petit 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 sur commande de 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 de SQL (notamment Redshift).
http://technet.microsoft.com/en-us/library/ms189108 (v=sql.105).aspx
il suffit d'utiliser
WHERE RAND() < 0.1
pour obtenir 10% des enregistrements ou
WHERE RAND() < 0.01
pour obtenir 1% des enregistrements, etc.
en commençant par l'observation que nous pouvons récupérer les ID d'une table (par ex. count 5) basé sur un ensemble:
select *
from table_name
where _id in (4, 1, 2, 5, 3)
nous pouvons en 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 ids ont des trous, alors le premier arraylist indices
est le résultat d'une requête sql sur les ids.
je tiens à souligner que toutes ces solutions semblent échantillonnage sans remplacement. En sélectionnant les lignes K supérieures à partir d'un tri aléatoire ou en se joignant à une table qui contient des Clés uniques dans l'ordre aléatoire vous obtiendrez un échantillon aléatoire généré sans remplacement.
si vous voulez que votre échantillon soit indépendant, vous aurez besoin d'échantillon avec remplacement. Voir Question 25451034 pour un exemple de la façon d'utiliser une jointure d'une manière similaire à la solution de user12861. La solution est écrite pour T-SQL, mais le concept fonctionne dans N'importe quel db SQL.
si vous avez besoin exactement des lignes m
, il est réaliste de penser que vous allez générer votre sous-ensemble D'IDs en dehors de SQL. La plupart des méthodes exigent à un certain point pour sélectionner la" nth " entrée, et les tables SQL ne sont vraiment pas des tableaux du tout. La supposition que les touches sont consécutives afin de simplement joindre les ints aléatoires entre 1 et le nombre est également difficile à satisfaire - MySQL par exemple ne supporte pas nativement, et les conditions de serrure sont... tricky .
voici un O(max(n, m lg n))
- temps, O(n)
- solution spatiale en supposant des clés BTREE simples:
- Récupérer toutes les valeurs de la colonne de la clé de la table de données dans n'importe quel ordre dans un tableau dans votre langage de script favori dans
O(n)
- effectuer un Fisher-Yates shuffle , s'arrêtant après
m
swaps, et extraire le subarray[0:m-1]
dansϴ(m)
1519220920" - "Rejoindre" la subarray avec l'ensemble de données d'origine (par exemple
SELECT ... WHERE id IN (<subarray>)
) dansO(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 mélange est limité en dessous de n
et m lg n
et n'affecte pas le comportement asymptotique.
en pseudo-code 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])
peut-être que tu pourrais faire
SELECT * FROM table LIMIT 10000 OFFSET FLOOR(RAND() * 190000)