it-swarm.it

Qual è il modo migliore per ottenere un ordine casuale?

Ho una query in cui desidero ordinare i record risultanti in modo casuale. Utilizza un indice cluster, quindi se non includo un order by probabilmente restituirà i record nell'ordine di quell'indice. Come posso garantire un ordine di riga casuale?

Capisco che probabilmente non sarà "veramente" casuale, lo pseudo-casuale è abbastanza buono per le mie esigenze.

29
goric

ORDER BY NEWID () ordina i record in modo casuale. Un esempio qui

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

Questa è una vecchia domanda, ma a mio avviso manca un aspetto della discussione: PERFORMANCE. ORDER BY NewId() è la risposta generale. Quando qualcuno ha voglia, aggiungono che dovresti davvero avvolgere NewID() in CheckSum(), sai, per prestazioni!

Il problema con questo metodo è che ti viene comunque garantita una scansione completa dell'indice e quindi un tipo completo di dati. Se hai lavorato con qualsiasi volume di dati serio, questo può rapidamente diventare costoso. Guarda questo tipico piano di esecuzione e nota come l'ordinamento richiede il 96% del tuo tempo ...

enter image description here

Per darti un'idea di come questa scala, ti darò due esempi da un database con cui lavoro.

  • Tabella A: dispone di 50.000 righe su 2500 pagine di dati. La query casuale genera 145 letture in 42ms.
  • Tabella B: ha 1,2 milioni di righe su 114.000 pagine di dati. L'esecuzione di Order By newid() su questa tabella genera 53.700 letture e richiede 16 secondi.

La morale della storia è che se hai tabelle di grandi dimensioni (pensa a miliardi di righe) o devi eseguire questa query frequentemente, il metodo newid() si rompe. Quindi cosa deve fare un ragazzo?

Scopri TABLESAMPLE ()

In SQL 2005 è stata creata una nuova funzionalità chiamata TABLESAMPLE. Ho visto solo n articolo che ne discute l'uso ... dovrebbe essercene di più. MSDN Documenti qui . Innanzitutto un esempio:

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

L'idea alla base dell'esempio della tabella è di fornire circa la dimensione del sottoinsieme richiesta. SQL numera ogni pagina di dati e seleziona l'X percento di tali pagine. Il numero effettivo di righe che ottieni può variare in base a ciò che esiste nelle pagine selezionate.

Quindi come lo uso? Seleziona una dimensione del sottoinsieme che copre più del numero di righe necessarie, quindi aggiungi Top(). L'idea è che puoi rendere la tua tabella enorme più piccola precedente all'ordinamento costoso.

Personalmente lo sto usando per limitare effettivamente le dimensioni del mio tavolo. Quindi su quel milione di righe che eseguono top(20)...TABLESAMPLE(20 PERCENT) la query scende a 5600 letture in 1600ms. C'è anche un'opzione REPEATABLE() in cui puoi passare un "Seme" per la selezione della pagina. Ciò dovrebbe comportare una selezione del campione stabile.

Comunque, ho pensato che questo dovrebbe essere aggiunto alla discussione. Spero che aiuti qualcuno.

16
EBarr

Il primo suggerimento di Pradeep Adiga, ORDER BY NEWID(), va bene e qualcosa che ho usato in passato per questo motivo.

Fai attenzione usando Rand() - in molti contesti viene eseguito solo una volta per istruzione, quindi ORDER BY Rand() non avrà alcun effetto (dato che stai ottenendo lo stesso risultato da Rand () per ogni riga ).

Per esempio:

SELECT display_name, Rand() FROM tr_person

restituisce ogni nome dalla nostra tabella personale e un numero "casuale", che è lo stesso per ogni riga. Il numero varia ogni volta che si esegue la query, ma è lo stesso per ogni riga ogni volta.

Per dimostrare che lo stesso è il caso di Rand() utilizzato in una clausola ORDER BY, Provo:

SELECT display_name FROM tr_person ORDER BY Rand(), display_name

I risultati sono ancora ordinati per nome indicando che il campo di ordinamento precedente (quello che si prevede sia casuale) non ha alcun effetto, quindi presumibilmente ha sempre lo stesso valore.

L'ordinamento per NEWID() funziona comunque, perché se NEWID () non fosse sempre rivalutato lo scopo degli UUID verrebbe interrotto quando si inserivano molti nuove righe in uno statemnt con identificatori univoci mentre digitano, quindi:

SELECT display_name FROM tr_person ORDER BY NEWID()

ordina i nomi "in modo casuale".

Altro DBMS

Quanto sopra vale per MSSQL (almeno nel 2005 e nel 2008, e se ricordo bene anche il 2000). Una funzione che restituisce un nuovo UUID dovrebbe essere valutata ogni volta in tutti i DBMS NEWID () è sotto MSSQL ma vale la pena verificarlo nella documentazione e/o dai tuoi test. Il comportamento di altre funzioni con risultati arbitrari, come Rand (), ha maggiori probabilità di variare tra DBMS, quindi controlla di nuovo la documentazione.

Inoltre ho visto l'ordinamento in base a valori UUID ignorati in alcuni contesti poiché il DB presume che il tipo non abbia un ordinamento significativo. Se trovi che questo è il caso esplicito cast dell'UUID in un tipo di stringa nella clausola ordering o avvolgi altre funzioni come CHECKSUM() in SQL Server (potrebbe esserci una piccola differenza di prestazioni anche da questo poiché l'ordinamento verrà eseguito su valori a 32 bit e non a 128 bit, anche se il vantaggio di questo supera il costo di eseguire CHECKSUM() per valore prima, ti lascio testare).

nota a margine

Se si desidera un ordinamento arbitrario ma in qualche modo ripetibile, ordinare in base a un sottoinsieme relativamente incontrollato dei dati nelle righe stesse. Ad esempio, uno di questi o questi restituiranno i nomi in un ordine arbitrario ma ripetibile:

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)

Gli ordini arbitrari ma ripetibili non sono spesso utili nelle applicazioni, sebbene possano essere utili nel test se si desidera testare un po 'di codice sui risultati in una varietà di ordini ma si desidera poter ripetere ogni esecuzione allo stesso modo più volte (per ottenere tempi medi) risultati su più esecuzioni o test che una correzione apportata al codice rimuove un problema o un'inefficienza precedentemente evidenziata da un determinato set di risultati di input o solo per verificare che il codice sia "stabile" in quanto restituisce lo stesso risultato ogni volta se inviato gli stessi dati in un determinato ordine).

Questo trucco può anche essere usato per ottenere risultati più arbitrari da funzioni, che non consentono chiamate non deterministiche come NEWID () all'interno del loro corpo. Ancora una volta, questo non è qualcosa che probabilmente sarà utile nel mondo reale ma potrebbe tornare utile se vuoi che una funzione restituisca qualcosa di casuale e "random-ish" sia abbastanza buono (ma fai attenzione a ricordare le regole che determinano quando le funzioni definite dall'utente vengono valutate, cioè di solito solo una volta per riga, oppure i risultati potrebbero non essere quelli previsti/richiesti).

Performance

Come sottolinea EBarr, possono esserci problemi di prestazioni con uno qualsiasi dei precedenti. Per più di alcune righe sei quasi garantito di vedere l'output di spooling su tempdb prima che il numero richiesto di righe venga letto nel giusto ordine, il che significa che anche se stai cercando la top 10 potresti trovare un indice completo scan (o peggio, table scan) avviene insieme a un enorme blocco di scrittura su tempdb. Pertanto, può essere di vitale importanza, come nella maggior parte delle cose, fare un benchmark con dati realistici prima di utilizzarli in produzione.

16
David Spillett

Molte tabelle hanno una colonna ID numerica indicizzata relativamente densa (pochi valori mancanti).

Questo ci consente di determinare l'intervallo di valori esistenti e di scegliere le righe utilizzando valori ID generati casualmente in quell'intervallo. Funziona meglio quando il numero di righe da restituire è relativamente piccolo e l'intervallo di valori ID è densamente popolato (quindi la possibilità di generare un valore mancante è abbastanza piccola).

Per illustrare, il codice seguente seleziona 100 utenti casuali distinti dalla tabella degli utenti Stack Overflow, che ha 8.123.937 righe.

Il primo passo è determinare l'intervallo di valori ID, un'operazione efficiente grazie all'indice:

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

Il piano legge una riga da ciascuna estremità dell'indice.

Ora generiamo 100 ID casuali distinti nell'intervallo (con le righe corrispondenti nella tabella degli utenti) e restituiamo quelle righe:

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

Il piano mostra che in questo caso erano necessari 601 numeri casuali per trovare 100 righe corrispondenti. È abbastanza veloce:

 Tabella "Utenti". Conteggio scansioni 1, letture logiche 1937, letture fisiche 2, letture read-ahead 408 
 Tabella 'Worktable'. Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0 
 Tabella 'File di lavoro'. Conteggio scansioni 0, letture logiche 0, letture fisiche 0, letture read-ahead 0 
 
 Tempi di esecuzione di SQL Server: 
 Tempo CPU = 0 ms, tempo trascorso = 9 ms. 

Provalo su Stack Exchange Data Explorer.

3
Paul White 9

Come ho spiegato in questo articolo , per mescolare il set di risultati SQL, è necessario utilizzare una chiamata di funzione specifica del database.

Si noti che l'ordinamento di un set di risultati di grandi dimensioni utilizzando una funzione RANDOM potrebbe rivelarsi molto lento, quindi assicurarsi di farlo su set di risultati di piccole dimensioni.

Se devi mescolare un set di risultati di grandi dimensioni e limitarlo in seguito, è meglio usare SQL Server TABLESAMPLE in SQL Server invece di una funzione casuale nella clausola ORDER BY.

Quindi, supponendo che abbiamo la seguente tabella di database:

enter image description here

E le seguenti righe nella tabella 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                      |

Su SQL Server, è necessario utilizzare la funzione NEWID, come illustrato dal seguente esempio:

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

Quando eseguiamo la suddetta query SQL su SQL Server, otteniamo il seguente set di risultati:

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

Si noti che i brani vengono elencati in ordine casuale, grazie alla chiamata di funzione NEWID utilizzata dalla clausola ORDER BY.

0
Vlad Mihalcea