Quelle est la meilleure façon d'obtenir une commande aléatoire?

27

J'ai une requête où je veux que les enregistrements résultants soient commandés au hasard. Il utilise un index clusterisé, donc si je n'inclus pas, order byil retournera probablement des enregistrements dans l'ordre de cet index. Comment puis-je garantir un ordre de ligne aléatoire?

Je comprends qu'il ne sera probablement pas "vraiment" aléatoire, le pseudo-aléatoire est assez bon pour mes besoins.

goric
la source

Réponses:

19

ORDER BY NEWID () triera les enregistrements de manière aléatoire. Un exemple ici

SELECT *
FROM Northwind..Orders 
ORDER BY NEWID()
Nomade
la source
7
ORDER BY NEWID () est effectivement aléatoire, mais pas statistiquement aléatoire. Il y a une petite différence, et la plupart du temps, la différence n'a pas d'importance.
mrdenny
4
Du point de vue des performances, cela est assez lent - vous pouvez obtenir une amélioration significative par ORDER BY CHECKSUM (NEWID ())
Miles D
1
@mrdenny - Sur quoi basez-vous le "pas statistiquement aléatoire"? La réponse ici dit qu'elle finit par être utilisée CryptGenRandomà la fin. dba.stackexchange.com/a/208069/3690
Martin Smith
15

La première suggestion de Pradeep Adiga ORDER BY NEWID(), est très bien et quelque chose que j'ai utilisé dans le passé pour cette raison.

Soyez prudent avec l'utilisation RAND()- dans de nombreux contextes, elle n'est exécutée qu'une fois par instruction, elle ORDER BY RAND()n'aura donc aucun effet (car vous obtenez le même résultat de RAND () pour chaque ligne).

Par exemple:

SELECT display_name, RAND() FROM tr_person

renvoie chaque nom de notre table person et un nombre "aléatoire", qui est le même pour chaque ligne. Le nombre varie chaque fois que vous exécutez la requête, mais il est le même pour chaque ligne à chaque fois.

Pour montrer que c'est la même chose avec RAND()utilisé dans une ORDER BYclause, j'essaye:

SELECT display_name FROM tr_person ORDER BY RAND(), display_name

Les résultats sont toujours classés par nom, ce qui indique que le champ de tri précédent (celui qui devrait être aléatoire) n'a aucun effet et a donc probablement toujours la même valeur.

La commande par NEWID()fonctionne cependant, car si NEWID () n'était pas toujours réévalué, le but des UUID serait rompu lors de l'insertion de nombreuses nouvelles lignes dans un même statut avec des identifiants uniques lors de leur clé, donc:

SELECT display_name FROM tr_person ORDER BY NEWID()

ne commande les noms « au hasard ».

Autres SGBD

Ce qui précède est vrai pour MSSQL (2005 et 2008 au moins, et si je me souviens bien 2000 également). Une fonction renvoyant un nouvel UUID doit être évaluée à chaque fois dans tous les SGBD NEWID () est sous MSSQL mais cela vaut la peine de le vérifier dans la documentation et / ou par vos propres tests. Le comportement d'autres fonctions de résultats arbitraires, comme RAND (), est plus susceptible de varier entre les SGBD, alors vérifiez à nouveau la documentation.

J'ai également vu que l'ordre des valeurs UUID était ignoré dans certains contextes, car la base de données suppose que le type n'a pas d'ordre significatif. Si vous trouvez que c'est le cas, convertissez explicitement l'UUID en un type de chaîne dans la clause de commande, ou encapsulez une autre fonction autour de celle-ci comme CHECKSUM()dans SQL Server (il peut y avoir une petite différence de performances par rapport à cela car la commande sera effectuée sur une valeur 32 bits et non une valeur 128 bits, bien que si l'avantage de cela l'emporte sur le coût de fonctionnement CHECKSUM()par valeur, je vous laisse tester).

Note latérale

Si vous voulez un ordre arbitraire mais quelque peu répétable, triez par un sous-ensemble relativement incontrôlé des données dans les lignes elles-mêmes. Par exemple, l'un ou l'autre retournera les noms dans un ordre arbitraire mais répétable:

SELECT display_name FROM tr_person ORDER BY CHECKSUM(display_name), display_name -- order by the checksum of some of the row's data
SELECT display_name FROM tr_person ORDER BY SUBSTRING(display_name, LEN(display_name)/2, 128) -- order by part of the name field, but not in any an obviously recognisable order)

Les commandes arbitraires mais répétables ne sont pas souvent utiles dans les applications, mais peuvent être utiles pour tester si vous voulez tester du code sur les résultats dans une variété d'ordres mais que vous voulez pouvoir répéter chaque exécution de la même manière plusieurs fois (pour obtenir un timing moyen résultats sur plusieurs exécutions, ou tester qu'un correctif que vous avez apporté au code supprime un problème ou une inefficacité précédemment mis en évidence par un jeu de résultats d'entrée particulier, ou simplement pour tester que votre code est "stable", c'est-à-dire qu'il renvoie le même résultat à chaque fois si envoyé les mêmes données dans un ordre donné).

Cette astuce peut également être utilisée pour obtenir des résultats plus arbitraires à partir de fonctions, qui n'autorisent pas les appels non déterministes comme NEWID () dans leur corps. Encore une fois, ce n'est pas quelque chose qui est susceptible d'être souvent utile dans le monde réel, mais qui pourrait être utile si vous voulez qu'une fonction renvoie quelque chose de aléatoire et "random-ish" est assez bon (mais attention à vous rappeler les règles qui déterminent lorsque les fonctions définies par l'utilisateur sont évaluées, c'est-à-dire généralement une seule fois par ligne, ou vos résultats peuvent ne pas être ceux que vous attendez / exigez).

Performance

Comme le souligne EBarr, il peut y avoir des problèmes de performance avec l'un des éléments ci-dessus. Pour plus de quelques lignes, vous êtes presque assuré de voir la sortie mise en file d'attente vers tempdb avant la lecture du nombre de lignes demandé dans le bon ordre, ce qui signifie que même si vous recherchez le top 10, vous pouvez trouver un index complet l'analyse (ou pire, l'analyse de table) se produit avec un énorme bloc d'écriture dans tempdb. C'est pourquoi il peut être d'une importance vitale, comme pour la plupart des choses, de comparer des données réalistes avant de les utiliser en production.

David Spillett
la source
14

C'est une vieille question, mais un aspect de la discussion manque, à mon avis - PERFORMANCE. ORDER BY NewId()est la réponse générale. Lorsque l'imagination de quelqu'un get ils ajoutent que vous devriez vraiment envelopper NewID()dans CheckSum(), vous le savez, pour la performance!

Le problème avec cette méthode, c'est que vous êtes toujours garanti une analyse complète de l'index, puis un tri complet des données. Si vous avez travaillé avec un volume de données sérieux, cela peut rapidement devenir coûteux. Regardez ce plan d'exécution typique, et notez comment le tri prend 96% de votre temps ...

entrez la description de l'image ici

Pour vous donner une idée de la façon dont cela évolue, je vais vous donner deux exemples d'une base de données avec laquelle je travaille.

  • TableA - a 50 000 lignes sur 2 500 pages de données. La requête aléatoire génère 145 lectures en 42 ms.
  • Tableau B - contient 1,2 million de lignes sur 114 000 pages de données. L'exécution Order By newid()sur cette table génère 53 700 lectures et prend 16 secondes.

La morale de l'histoire est que si vous avez de grandes tables (pensez à des milliards de lignes) ou si vous devez exécuter cette requête fréquemment, la newid()méthode tombe en panne. Alors, qu'est-ce qu'un garçon à faire?

Rencontrez TABLESAMPLE ()

Dans SQL 2005, une nouvelle fonctionnalité appelée a TABLESAMPLEété créée. Je n'ai vu qu'un seul article discutant de son utilisation ... il devrait y en avoir plus. Documents MSDN ici . D'abord un exemple:

SELECT Top (20) *
FROM Northwind..Orders TABLESAMPLE(20 PERCENT)
ORDER BY NEWID()

L'idée derrière l'échantillon de table est de vous donner approximativement la taille de sous-ensemble que vous demandez. SQL numérote chaque page de données et sélectionne X pour cent de ces pages. Le nombre réel de lignes que vous récupérez peut varier en fonction de ce qui existe dans les pages sélectionnées.

Alors, comment dois-je l'utiliser? Sélectionnez une taille de sous-ensemble qui couvre plus que le nombre de lignes dont vous avez besoin, puis ajoutez a Top(). L'idée est que vous pouvez faire paraître votre table ginormous plus petite avant le tri coûteux.

Personnellement, je l'ai utilisé pour limiter la taille de ma table. Ainsi, sur cette table de millions de lignes, top(20)...TABLESAMPLE(20 PERCENT)la requête tombe à 5600 lectures en 1600 ms. Il y a aussi une REPEATABLE()option où vous pouvez passer un "Seed" pour la sélection de page. Il devrait en résulter une sélection d'échantillon stable.

Quoi qu'il en soit, je pensais simplement que cela devrait être ajouté à la discussion. J'espère que cela aide quelqu'un.

EBarr
la source
Ce serait bien de pouvoir écrire une requête à ordre aléatoire évolutive qui non seulement évolue mais fonctionne avec de petits ensembles de données. Il semble que vous devez basculer manuellement entre avoir et ne pas avoir en TABLESAMPLE()fonction de la quantité de données dont vous disposez. Je ne pense pas que cela TABLESAMPLE(x ROWS)garantirait même qu'au moins des x lignes soient renvoyées car la documentation indique: «Le nombre réel de lignes renvoyées peut varier considérablement. Si vous spécifiez un petit nombre, tel que 5, vous risquez de ne pas recevoir de résultats dans l'échantillon. »- la ROWSsyntaxe n'est-elle donc vraiment qu'un masque à l' PERCENTintérieur?
binki
Bien sûr, l'auto-magie est agréable. En pratique, j'ai rarement vu une échelle de table à 5 lignes sur des millions de lignes sans préavis. TABLESAMPLE () semble baser la sélection du nombre de pages dans un tableau, donc la taille de ligne donnée influence ce qui revient. Le point de l'échantillon de table, au moins comme je le vois, est de vous donner un bon sous-ensemble à partir duquel vous pouvez sélectionner - un peu comme une table dérivée.
EBarr
3

De nombreuses tables ont une colonne d'ID numérique indexée relativement dense (quelques valeurs manquantes).

Cela nous permet de déterminer la plage de valeurs existantes et de choisir des lignes à l'aide de valeurs d'ID générées de manière aléatoire dans cette plage. Cela fonctionne mieux lorsque le nombre de lignes à renvoyer est relativement petit et que la plage de valeurs d'ID est densément peuplée (donc les chances de générer une valeur manquante sont suffisamment petites).

Pour illustrer, le code suivant choisit 100 utilisateurs aléatoires distincts dans la table d'utilisateurs Stack Overflow, qui contient 8 123 937 lignes.

La première étape consiste à déterminer la plage de valeurs ID, une opération efficace grâce à l'index:

DECLARE 
    @MinID integer,
    @Range integer,
    @Rows bigint = 100;

--- Find the range of values
SELECT
    @MinID = MIN(U.Id),
    @Range = 1 + MAX(U.Id) - MIN(U.Id)
FROM dbo.Users AS U;

Requête de plage

Le plan lit une ligne à chaque extrémité de l'index.

Maintenant, nous générons 100 ID aléatoires distincts dans la plage (avec des lignes correspondantes dans la table des utilisateurs) et renvoyons ces lignes:

WITH Random (ID) AS
(
    -- Find @Rows distinct random user IDs that exist
    SELECT DISTINCT TOP (@Rows)
        Random.ID
    FROM dbo.Users AS U
    CROSS APPLY
    (
        -- Random ID
        VALUES (@MinID + (CONVERT(integer, CRYPT_GEN_RANDOM(4)) % @Range))
    ) AS Random (ID)
    WHERE EXISTS
    (
        SELECT 1
        FROM dbo.Users AS U2
            -- Ensure the row continues to exist
            WITH (REPEATABLEREAD)
        WHERE U2.Id = Random.ID
    )
)
SELECT
    U3.Id,
    U3.DisplayName,
    U3.CreationDate
FROM Random AS R
JOIN dbo.Users AS U3
    ON U3.Id = R.ID
-- QO model hint required to get a non-blocking flow distinct
OPTION (MAXDOP 1, USE HINT ('FORCE_LEGACY_CARDINALITY_ESTIMATION'));

requête de lignes aléatoires

Le plan montre que dans ce cas, 601 nombres aléatoires étaient nécessaires pour trouver 100 lignes correspondantes. C'est assez rapide:

Tableau «Utilisateurs». Nombre de balayages 1, lectures logiques 1937, lectures physiques 2, lectures anticipées 408
Tableau «Table de travail». Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0
Tableau «Fichier de travail». Nombre de balayages 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0

 Temps d'exécution de SQL Server:
   Temps CPU = 0 ms, temps écoulé = 9 ms.

Essayez-le sur l'explorateur de données Exchange Stack.

Paul White dit GoFundMonica
la source
0

Comme je l'ai expliqué dans cet article , pour mélanger l'ensemble de résultats SQL, vous devez utiliser un appel de fonction spécifique à la base de données.

Notez que le tri d'un grand jeu de résultats à l'aide d'une fonction RANDOM peut s'avérer très lent, alors assurez-vous de le faire sur de petits jeux de résultats.

Si vous devez mélanger un ensemble de résultats volumineux et le limiter par la suite, il est préférable d'utiliser SQL Server TABLESAMPLEdans SQL Server au lieu d'une fonction aléatoire dans la clause ORDER BY.

Donc, en supposant que nous ayons la table de base de données suivante:

entrez la description de l'image ici

Et les lignes suivantes du songtableau:

| id | artist                          | title                              |
|----|---------------------------------|------------------------------------|
| 1  | Miyagi & Эндшпиль ft. Рем Дигга | I Got Love                         |
| 2  | HAIM                            | Don't Save Me (Cyril Hahn Remix)   |
| 3  | 2Pac ft. DMX                    | Rise Of A Champion (GalilHD Remix) |
| 4  | Ed Sheeran & Passenger          | No Diggity (Kygo Remix)            |
| 5  | JP Cooper ft. Mali-Koa          | All This Love                      |

Sur SQL Server, vous devez utiliser la NEWIDfonction, comme illustré par l'exemple suivant:

SELECT
    CONCAT(CONCAT(artist, ' - '), title) AS song
FROM song
ORDER BY NEWID()

Lors de l'exécution de la requête SQL susmentionnée sur SQL Server, nous allons obtenir le jeu de résultats suivant:

| song                                              |
|---------------------------------------------------|
| Miyagi & Эндшпиль ft. Рем Дигга - I Got Love      |
| JP Cooper ft. Mali-Koa - All This Love            |
| HAIM - Don't Save Me (Cyril Hahn Remix)           |
| Ed Sheeran & Passenger - No Diggity (Kygo Remix)  |
| 2Pac ft. DMX - Rise Of A Champion (GalilHD Remix) |

Notez que les chansons sont répertoriées dans un ordre aléatoire, grâce à l' NEWIDappel de fonction utilisé par la clause ORDER BY.

Vlad Mihalcea
la source