web-dev-qa-db-fra.com

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

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 de order by il renverra 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.

29
goric

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

SELECT *
FROM Northwind..Orders 
ORDER BY NEWID()
23
Nomad

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. Quand quelqu'un en a envie, il ajoute que vous devriez vraiment envelopper NewID() dans CheckSum(), vous savez, pour les performances!

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 ...

enter image description here

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 de Order By newid() sur ce tableau 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 méthode newid() tombe en panne. Alors, qu'est-ce qu'un garçon à faire?

Rencontrez TABLESAMPLE ()

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

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

L'idée derrière l'exemple 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 une Top(). L'idée est que vous pouvez faire apparaître votre table ginormous plus petite avant au tri coûteux.

Personnellement, je l'ai utilisé pour limiter la taille de ma table. Donc, sur cette table de millions de lignes faisant top(20)...TABLESAMPLE(20 PERCENT) la requête tombe à 5600 lectures en 1600ms. Il y a aussi une option REPEATABLE() 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 juste que cela devrait être ajouté à la discussion. J'espère que cela aide quelqu'un.

16
EBarr

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 de Rand() - dans de nombreux contextes, elle n'est exécutée qu'une fois par instruction, donc ORDER BY Rand() n'aura 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 de personnes 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 clause ORDER BY, 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 plusieurs de nouvelles lignes dans un même statut avec des identifiants uniques au fur et à mesure de leur clé, donc:

SELECT display_name FROM tr_person ORDER BY NEWID()

ordonne les noms "au hasard".

Autre 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 classement, ou encapsulez une autre fonction comme CHECKSUM() dans SQL Server (il peut y avoir une petite différence de performances par rapport à cela aussi car la commande sera effectuée sur des valeurs 32 bits et non sur 128 bits, mais si l'avantage de cela l'emporte sur le coût de l'exécution de CHECKSUM() par valeur, je vous laisse tester).

Note latérale

Si vous voulez un ordre arbitraire mais quelque peu répétable, triez-le 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 des 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 pourrait être utile si vous voulez qu'une fonction retourne quelque chose de aléatoire et que "random-ish" soit 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 spoulée vers tempdb avant que le nombre de lignes demandé ne soit relu 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.

16
David Spillett

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;

Range query

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'));

random rows query

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 scans 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0 
 Table 'Workfile'. Nombre de scans 0, lectures logiques 0, lectures physiques 0, lectures anticipées 0 
 
 Temps d'exécution SQL Server: 
 Temps CPU = 0 ms, temps écoulé = 9 ms. 

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

3
Paul White 9

Comme je l'ai expliqué dans cet article , afin de 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 ensemble 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 ensembles 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 TABLESAMPLE dans 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:

enter image description here

Et les lignes suivantes dans la table song:

| 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 fonction NEWID, 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'appel de fonction NEWID utilisé par la clause ORDER BY.

0
Vlad Mihalcea