Sélectionnez n lignes aléatoires dans la table SQL Server


309

J'ai une table SQL Server contenant environ 50 000 lignes. Je souhaite sélectionner environ 5 000 de ces lignes au hasard. J'ai pensé à une manière compliquée, créer une table temporaire avec une colonne "nombre aléatoire", copier ma table dans celle-ci, parcourir la table temporaire et mettre à jour chaque ligne avec RAND(), puis sélectionner dans cette table où la colonne de nombre aléatoire < 0,1. Je cherche un moyen plus simple de le faire, dans une seule déclaration si possible.

Cet article suggère d'utiliser la NEWID()fonction. Cela semble prometteur, mais je ne vois pas comment je pourrais sélectionner de manière fiable un certain pourcentage de lignes.

Quelqu'un a déjà fait ça avant? Des idées?


3
MSDN a un bon article qui couvre un grand nombre de ces problèmes: Sélection aléatoire de lignes à partir d'une grande table
KyleMit

Réponses:


387
select top 10 percent * from [yourtable] order by newid()

En réponse au commentaire "pure corbeille" concernant les grandes tables: vous pouvez le faire comme ça pour améliorer les performances.

select  * from [yourtable] where [yourPk] in 
(select top 10 percent [yourPk] from [yourtable] order by newid())

Le coût de ceci sera l'analyse des valeurs clés plus le coût de jointure, qui sur une grande table avec un petit pourcentage de sélection devrait être raisonnable.


1
J'aime beaucoup mieux cette approche que d'utiliser l'article auquel il a fait référence.
JoshBerke

14
Il est toujours bon de garder à l'esprit que newid () n'est pas un très bon générateur de nombres pseudo-aléatoires, du moins pas aussi bon que rand (). Mais si vous avez juste besoin d'échantillons vaguement aléatoires et que vous ne vous souciez pas des qualités mathématiques et autres, ce sera suffisant. Sinon, vous avez besoin de: stackoverflow.com/questions/249301/…
user12861

1
Hum, désolé si c'est évident .. mais à quoi se [yourPk]réfère-t-il? EDIT: Nvm, compris ... Clé primaire. Durrr
Snailer

4
newid - guid est conçu pour être unique mais pas aléatoire .. approche incorrecte
Brans Ds

2
avec un grand nombre de lignes, par exemple plus d'un million, le newid()coût d'E / S de l'estimation de tri sera très élevé et affectera les performances.
aadi1295

81

Selon vos besoins, TABLESAMPLEvous obtiendrez des performances presque aussi aléatoires et meilleures. ceci est disponible sur MS SQL Server 2005 et versions ultérieures.

TABLESAMPLE renverra des données à partir de pages aléatoires au lieu de lignes aléatoires et par conséquent, les deos ne récupéreront même pas les données qu'elles ne renverront pas.

Sur une très grande table, j'ai testé

select top 1 percent * from [tablename] order by newid()

a pris plus de 20 minutes.

select * from [tablename] tablesample(1 percent)

a pris 2 minutes.

Les performances s'amélioreront également sur des échantillons plus petits TABLESAMPLEalors que ce ne sera pas le cas avec newid().

Veuillez garder à l'esprit que ce n'est pas aussi aléatoire que la newid()méthode mais vous donnera un échantillonnage décent.

Voir la page MSDN .


7
Comme souligné par Rob Boek ci-dessous, l'échantillonnage des tableaux regroupe les résultats, et n'est donc pas un bon moyen d'obtenir un petit nombre de résultats aléatoires
Oskar Austegard

Vous vous posez la question de savoir comment cela fonctionne: sélectionnez les 1% * supérieurs dans l'ordre de [tablename] par newid () car newid () n'est pas une colonne dans [tablename]. Le serveur SQL ajoute-t-il en interne la colonne newid () sur chaque ligne, puis effectue un tri?
FrenkyB

L'échantillon de table était la meilleure réponse pour moi car je faisais une requête complexe sur une très grande table. Il ne fait aucun doute que cela a été remarquablement rapide. J'ai eu une variation dans le nombre d'enregistrements retournés car je l'ai exécuté plusieurs fois, mais tous étaient dans une marge d'erreur acceptable.
jessier3

38

newid () / order by fonctionnera, mais sera très coûteux pour les grands ensembles de résultats car il doit générer un identifiant pour chaque ligne, puis les trier.

TABLESAMPLE () est bon du point de vue des performances, mais vous obtiendrez un agrégat de résultats (toutes les lignes d'une page seront retournées).

Pour un véritable échantillon aléatoire plus performant, la meilleure façon est de filtrer les lignes de manière aléatoire. J'ai trouvé l'exemple de code suivant dans l'article de documentation en ligne de SQL Server Limitation des ensembles de résultats à l'aide de TABLESAMPLE :

Si vous voulez vraiment un échantillon aléatoire de lignes individuelles, modifiez votre requête pour filtrer les lignes au hasard, au lieu d'utiliser TABLESAMPLE. Par exemple, la requête suivante utilise la fonction NEWID pour renvoyer environ un pour cent des lignes de la table Sales.SalesOrderDetail:

SELECT * FROM Sales.SalesOrderDetail
WHERE 0.01 >= CAST(CHECKSUM(NEWID(),SalesOrderID) & 0x7fffffff AS float)
              / CAST (0x7fffffff AS int)

La colonne SalesOrderID est incluse dans l'expression CHECKSUM afin que NEWID () évalue une fois par ligne pour réaliser l'échantillonnage sur une ligne. L'expression CAST (CHECKSUM (NEWID (), SalesOrderID) & 0x7fffffff AS float / CAST (0x7fffffff AS int) est évaluée comme une valeur flottante aléatoire comprise entre 0 et 1.

Lorsqu'il est exécuté sur une table avec 1 000 000 lignes, voici mes résultats:

SET STATISTICS TIME ON
SET STATISTICS IO ON

/* newid()
   rows returned: 10000
   logical reads: 3359
   CPU time: 3312 ms
   elapsed time = 3359 ms
*/
SELECT TOP 1 PERCENT Number
FROM Numbers
ORDER BY newid()

/* TABLESAMPLE
   rows returned: 9269 (varies)
   logical reads: 32
   CPU time: 0 ms
   elapsed time: 5 ms
*/
SELECT Number
FROM Numbers
TABLESAMPLE (1 PERCENT)

/* Filter
   rows returned: 9994 (varies)
   logical reads: 3359
   CPU time: 641 ms
   elapsed time: 627 ms
*/    
SELECT Number
FROM Numbers
WHERE 0.01 >= CAST(CHECKSUM(NEWID(), Number) & 0x7fffffff AS float) 
              / CAST (0x7fffffff AS int)

SET STATISTICS IO OFF
SET STATISTICS TIME OFF

Si vous pouvez vous en sortir en utilisant TABLESAMPLE, cela vous donnera les meilleures performances. Sinon, utilisez la méthode newid () / filter. newid () / order by devrait être le dernier recours si vous avez un grand ensemble de résultats.


J'ai vu cet article aussi et je l'ai essayé sur mon code, il semble qu'il ne NewID()soit évalué qu'une seule fois, au lieu de par ligne, ce que je n'aime pas ...
Andrew Mao

23

La sélection aléatoire de lignes dans une grande table sur MSDN propose une solution simple et bien articulée qui répond aux problèmes de performances à grande échelle.

  SELECT * FROM Table1
  WHERE (ABS(CAST(
  (BINARY_CHECKSUM(*) *
  RAND()) as int)) % 100) < 10

Très intéressant. Après avoir lu l'article, je ne comprends pas vraiment pourquoi RAND()ne retourne pas la même valeur pour chaque ligne (ce qui irait à l' BINARY_CHECKSUM()encontre de la logique). Est-ce parce qu'il est appelé dans une autre fonction plutôt que de faire partie de la clause SELECT?
John M Gant

Cette requête s'est exécutée sur une table avec 6MM de lignes en moins d'une seconde.
Mark Melville

2
J'ai exécuté cette requête sur une table avec 35 entrées et j'en ai gardé très souvent deux dans le jeu de résultats. Cela peut être un problème rand()ou une combinaison des éléments ci-dessus - mais je me suis détourné de cette solution pour cette raison. De plus, le nombre de résultats variait de 1 à 5, ce qui pourrait donc ne pas être acceptable dans certains scénarios.
Oliver

RAND () ne renvoie-t-il pas la même valeur pour chaque ligne?
Salsepareille

RAND()renvoie la même valeur pour chaque ligne (c'est pourquoi cette solution est rapide). Cependant, les lignes avec des sommes de contrôle binaires qui sont très proches les unes des autres courent un risque élevé de générer des résultats de somme de contrôle similaires, provoquant une agglutination lorsqu'elle RAND()est petite. Par exemple, (ABS(CAST((BINARY_CHECKSUM(111,null,null) * 0.1) as int))) % 100== SELECT (ABS(CAST((BINARY_CHECKSUM(113,null,null) * 0.1) as int))) % 100. Si vos données souffrent de ce problème, multipliez BINARY_CHECKSUMpar 9923.
Brian

12

Ce lien présente une comparaison intéressante entre Orderby (NEWID ()) et d'autres méthodes pour les tables avec 1, 7 et 13 millions de lignes.

Souvent, lorsque des questions sur la façon de sélectionner des lignes aléatoires sont posées dans des groupes de discussion, la requête NEWID est proposée; il est simple et fonctionne très bien pour les petites tables.

SELECT TOP 10 PERCENT *
  FROM Table1
  ORDER BY NEWID()

Cependant, la requête NEWID présente un gros inconvénient lorsque vous l'utilisez pour de grandes tables. La clause ORDER BY entraîne la copie de toutes les lignes de la table dans la base de données tempdb, où elles sont triées. Cela provoque deux problèmes:

  1. L'opération de tri a généralement un coût élevé qui lui est associé. Le tri peut utiliser beaucoup d'E / S disque et peut s'exécuter pendant une longue période.
  2. Dans le pire des cas, tempdb peut manquer d'espace. Dans le meilleur des cas, tempdb peut occuper une grande quantité d'espace disque qui ne sera jamais récupérée sans une commande de réduction manuelle.

Ce dont vous avez besoin est un moyen de sélectionner des lignes au hasard qui n'utilisera pas tempdb et ne deviendra pas beaucoup plus lent à mesure que la table s'agrandit. Voici une nouvelle idée sur la façon de procéder:

SELECT * FROM Table1
  WHERE (ABS(CAST(
  (BINARY_CHECKSUM(*) *
  RAND()) as int)) % 100) < 10

L'idée de base de cette requête est que nous voulons générer un nombre aléatoire entre 0 et 99 pour chaque ligne du tableau, puis choisir toutes les lignes dont le nombre aléatoire est inférieur à la valeur du pourcentage spécifié. Dans cet exemple, nous voulons environ 10% des lignes sélectionnées au hasard; par conséquent, nous choisissons toutes les lignes dont le nombre aléatoire est inférieur à 10.

Veuillez lire l'article complet dans MSDN .


2
Salut Deumber, bien trouvé, vous pourriez l'étoffer car les réponses de lien uniquement sont susceptibles d'être supprimées.
bummi

1
@bummi Je l'ai changé pour éviter d'être un lien uniquement réponse :)
QMaster

C'est la meilleure réponse. 'ORDER BY NEWID ()' fonctionne dans la plupart des cas (tables plus petites), mais comme les repères du lien réfracté montrent clairement qu'il prend du retard à mesure que la table grandit
Pedram Bashiri

10

Si vous (contrairement à l'OP) avez besoin d'un nombre spécifique d'enregistrements (ce qui rend l'approche CHECKSUM difficile) et souhaitez un échantillon plus aléatoire que TABLESAMPLE en lui-même, et souhaitez également une meilleure vitesse que CHECKSUM, vous pouvez vous contenter d'une fusion des Les méthodes TABLESAMPLE et NEWID (), comme ceci:

DECLARE @sampleCount int = 50
SET STATISTICS TIME ON

SELECT TOP (@sampleCount) * 
FROM [yourtable] TABLESAMPLE(10 PERCENT)
ORDER BY NEWID()

SET STATISTICS TIME OFF

Dans mon cas, c'est le compromis le plus simple entre le caractère aléatoire (ce n'est pas vraiment, je sais) et la vitesse. Faites varier le pourcentage (ou les lignes) TABLESAMPLE selon le cas - plus le pourcentage est élevé, plus l'échantillon est aléatoire, mais attendez-vous à une baisse linéaire de la vitesse. (Notez que TABLESAMPLE n'acceptera pas de variable)


9

Commandez simplement la table par un nombre aléatoire et obtenez les 5 000 premières lignes en utilisant TOP.

SELECT TOP 5000 * FROM [Table] ORDER BY newid();

METTRE À JOUR

Je viens de l'essayer et un newid()appel suffit - pas besoin de tous les lancers et de tous les calculs.


10
La raison pour laquelle «tous les modèles et tous les calculs» sont utilisés est pour de meilleures performances.
hkf

6

C'est une combinaison de l'idée de départ et d'une somme de contrôle, qui me semble donner des résultats correctement aléatoires sans le coût de NEWID ():

SELECT TOP [number] 
FROM table_name
ORDER BY RAND(CHECKSUM(*) * RAND())

3

Dans MySQL, vous pouvez faire ceci:

SELECT `PRIMARY_KEY`, rand() FROM table ORDER BY rand() LIMIT 5000;

3
Cela ne fonctionnera pas. Étant donné que l'instruction select est atomique, elle n'attrape qu'un seul nombre aléatoire et le duplique pour chaque ligne. Vous devrez le réensemencer sur chaque ligne pour le forcer à changer.
Tom H

4
Mmm ... j'adore les différences de fournisseurs. Select est atomique sur MySQL, mais je suppose d'une manière différente. Cela fonctionnera dans MySQL.
Jeff Ferland,

2

Je n'ai pas encore tout à fait vu cette variation dans les réponses. J'avais une contrainte supplémentaire là où j'avais besoin, étant donné une graine initiale, de sélectionner le même ensemble de lignes à chaque fois.

Pour MS SQL:

Exemple minimum:

select top 10 percent *
from table_name
order by rand(checksum(*))

Temps d'exécution normalisé: 1,00

Exemple avec NewId ():

select top 10 percent *
from table_name
order by newid()

Temps d'exécution normalisé: 1,02

NewId()est insignifiamment plus lent que rand(checksum(*)), il est donc possible que vous ne souhaitiez pas l'utiliser avec de grands jeux d'enregistrements.

Sélection avec semence initiale:

declare @seed int
set @seed = Year(getdate()) * month(getdate()) /* any other initial seed here */

select top 10 percent *
from table_name
order by rand(checksum(*) % @seed) /* any other math function here */

Si vous devez sélectionner le même ensemble en fonction d'une graine, cela semble fonctionner.


Y a-t-il un avantage à utiliser @seed spécial contre RAND ()?
QMaster

absolument, vous avez utilisé le paramètre de départ et le remplissez par paramètre de date, la fonction RAND () fait de même, sauf en utilisant la valeur d'heure complète, je veux savoir s'il est avantageux d'utiliser un paramètre créé pratique comme un départ au-dessus de RAND () ou non?
QMaster

Ah !. OK, c'était une exigence du projet. J'avais besoin de générer une liste de n lignes aléatoires de manière déterministe. Fondamentalement, le leadership voulait savoir quelles lignes «aléatoires» nous sélectionnerions quelques jours avant que les lignes soient sélectionnées et traitées. En créant une valeur de départ basée sur l'année / le mois, je pouvais garantir que tout appel à la requête cette année retournerait la même liste "aléatoire". Je sais, c'était étrange et il y avait probablement de meilleures façons mais cela a fonctionné ...
klyd

HAHA :) Je vois, mais je pense que la signification générale des enregistrements sélectionnés au hasard n'est pas les mêmes enregistrements sur différentes requêtes en cours d'exécution.
QMaster


0

Il semble que newid () ne peut pas être utilisé dans la clause where, donc cette solution nécessite une requête interne:

SELECT *
FROM (
    SELECT *, ABS(CHECKSUM(NEWID())) AS Rnd
    FROM MyTable
) vw
WHERE Rnd % 100 < 10        --10%

0

Je l'utilisais dans la sous-requête et cela m'a renvoyé les mêmes lignes dans la sous-requête

 SELECT  ID ,
            ( SELECT TOP 1
                        ImageURL
              FROM      SubTable 
              ORDER BY  NEWID()
            ) AS ImageURL,
            GETUTCDATE() ,
            1
    FROM    Mytable

puis j'ai résolu avec l'inclusion de la variable de la table parent où

SELECT  ID ,
            ( SELECT TOP 1
                        ImageURL
              FROM      SubTable 
              Where Mytable.ID>0
              ORDER BY  NEWID()
            ) AS ImageURL,
            GETUTCDATE() ,
            1
    FROM    Mytable

Notez la condition où


0

Le langage de traitement côté serveur utilisé (par exemple PHP, .net, etc.) n'est pas spécifié, mais s'il s'agit de PHP, récupérez le nombre requis (ou tous les enregistrements) et au lieu de randomiser dans la requête, utilisez la fonction de lecture aléatoire de PHP. Je ne sais pas si .net a une fonction équivalente mais s'il l'utilise alors si vous utilisez .net

ORDER BY RAND () peut avoir toute une pénalité de performance, selon le nombre d'enregistrements impliqués.


Je ne me souviens pas exactement de ce que j'utilisais à l'époque, mais je travaillais probablement en C #, peut-être sur un serveur, ou peut-être dans une application cliente, pas sûr. C # n'a rien de directement comparable à la lecture aléatoire de PHP, mais cela pourrait être fait en appliquant des fonctions de l'objet Random dans une opération Select, en ordonnant le résultat, puis en prenant les dix premiers pour cent. Mais il faudrait lire la table entière du disque sur le serveur DB et la transmettre sur le réseau, pour éliminer 90% de ces données. Le traiter directement dans la base de données est presque certainement plus efficace.
John M Gant

-2

Cela fonctionne pour moi:

SELECT * FROM table_name
ORDER BY RANDOM()
LIMIT [number]

9
@ user537824, avez-vous essayé cela sur SQL Server? RANDOM n'est pas une fonction et LIMIT n'est pas un mot clé. La syntaxe SQL Server pour ce que vous faites serait select top 10 percent from table_name order by rand(), mais cela ne fonctionne pas non plus car rand () renvoie la même valeur sur toutes les lignes.
John M Gant
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.