Semplici esempi casuali da un database SQL


93

Come prendo un campione casuale semplice ed efficiente in SQL? Il database in questione esegue MySQL; la mia tabella è di almeno 200.000 righe e voglio un semplice campione casuale di circa 10.000.

La risposta "ovvia" è:

SELECT * FROM table ORDER BY RAND() LIMIT 10000

Per le tabelle di grandi dimensioni, è troppo lento: richiede RAND()ogni riga (che la mette già a O (n)) e le ordina, rendendola O (n lg n) nella migliore delle ipotesi. C'è un modo per farlo più velocemente di O (n)?

Nota : come sottolinea Andrew Mao nei commenti, se stai usando questo approccio su SQL Server, dovresti usare la funzione T-SQL NEWID(), perché RAND () può restituire lo stesso valore per tutte le righe .

MODIFICA: 5 ANNI DOPO

Mi sono imbattuto di nuovo in questo problema con una tabella più grande e ho finito per utilizzare una versione della soluzione di @ ignorant, con due modifiche:

  • Campiona le righe fino a 2-5 volte la dimensione del campione desiderata, a poco prezzo ORDER BY RAND()
  • Salva il risultato di RAND()in una colonna indicizzata a ogni inserimento / aggiornamento. (Se il tuo set di dati non è molto ricco di aggiornamenti, potresti dover trovare un altro modo per mantenere aggiornata questa colonna.)

Per prendere un campione di 1000 elementi di una tabella, conto le righe e campionamento il risultato fino a, in media, 10.000 righe con la colonna 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

(La mia implementazione effettiva richiede più lavoro per assicurarmi di non sottocampionare e per racchiudere manualmente rand_high, ma l'idea di base è "tagliare a caso il tuo N a poche migliaia")

Sebbene ciò comporti alcuni sacrifici, mi consente di campionare il database utilizzando una scansione dell'indice, finché non è abbastanza piccolo da poterlo ORDER BY RAND()nuovamente.


3
Non funziona nemmeno in SQL Server perché RAND()restituisce lo stesso valore a ogni chiamata successiva.
Andrew Mao

1
Buon punto: aggiungerò una nota che gli utenti di SQL Server dovrebbero utilizzare ORDER BY NEWID () invece.
ojrac

È ancora terribilmente inefficiente perché deve ordinare tutti i dati. Una tecnica di campionamento casuale per una certa percentuale è migliore, ma anche dopo aver letto un sacco di post qui, non ho trovato una soluzione accettabile che sia sufficientemente casuale.
Andrew Mao

Se leggi la domanda, ti sto chiedendo specificamente perché ORDER BY RAND () è O (n lg n).
ojrac,

La risposta di muposat di seguito è ottima se non sei troppo ossessionato dalla casualità statistica di RAND ().
Josh Greifer

Risposte:


25

C'è una discussione molto interessante su questo tipo di problema qui: http://www.titov.net/2005/09/21/do-not-use-order-by-rand-or-how-to-get-random-rows-from-table/

Penso, senza alcuna ipotesi sulla tabella, che la tua soluzione O (n lg n) sia la migliore. Sebbene in realtà con un buon ottimizzatore o una tecnica leggermente diversa la query che elenchi potrebbe essere un po 'migliore, O (m * n) dove m è il numero di righe casuali desiderate, poiché non è necessario ordinare l'intero array di grandi dimensioni , potrebbe cercare solo le m volte più piccole. Ma per il tipo di numeri che hai postato, m è comunque più grande di lg n.

Tre ipotesi che potremmo provare:

  1. c'è una chiave primaria univoca, indicizzata, nella tabella

  2. il numero di righe casuali che si desidera selezionare (m) è molto inferiore al numero di righe nella tabella (n)

  3. la chiave primaria univoca è un numero intero compreso tra 1 an senza spazi

Con solo le ipotesi 1 e 2, penso che questo possa essere fatto in O (n), anche se dovrai scrivere un intero indice sulla tabella per far corrispondere l'ipotesi 3, quindi non è necessariamente un veloce O (n). Se possiamo INOLTRE presumere qualcos'altro di carino sulla tabella, possiamo eseguire il compito in O (m log m). L'assunzione 3 sarebbe una proprietà aggiuntiva facile e piacevole con cui lavorare. Con un bel generatore di numeri casuali che non garantisse duplicati durante la generazione di m numeri di fila, sarebbe possibile una soluzione O (m).

Date le tre ipotesi, l'idea di base è generare m numeri casuali univoci compresi tra 1 e n, quindi selezionare le righe con quelle chiavi dalla tabella. Non ho mysql o altro davanti a me in questo momento, quindi in un po 'di pseudocodice questo sarebbe qualcosa del tipo:


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

Se fossi veramente preoccupato per l'efficienza, potresti prendere in considerazione la possibilità di generare la chiave casuale in una sorta di linguaggio procedurale e inserire i risultati nel database, poiché quasi qualsiasi cosa diversa da SQL sarebbe probabilmente migliore nel tipo di loop e generazione di numeri casuali richiesti .


Consiglierei di aggiungere un indice univoco alla selezione della chiave casuale e forse ignorare i duplicati sull'inserto, quindi puoi sbarazzarti delle cose distinte e l'unione sarà più veloce.
Sam Saffron

Penso che l'algoritmo del numero casuale potrebbe utilizzare alcune modifiche: un vincolo UNIQUE come menzionato, o semplicemente generare 2 * m numeri e SELECT DISTINCT, ORDER BY id (first-come-first-serve, quindi questo si riduce al vincolo UNIQUE ) LIMITE m. Mi piace.
ojrac

Per quanto riguarda l'aggiunta di un indice univoco alla selezione della chiave casuale e quindi l'ignoranza dei duplicati durante l'inserimento, ho pensato che questo potesse riportarti al comportamento O (m ^ 2) invece di O (m lg m) per un ordinamento. Non sono sicuro dell'efficienza con cui il server mantiene l'indice quando inserisce righe casuali una alla volta.
user12861

Per quanto riguarda i suggerimenti per generare numeri 2 * m o qualcosa del genere, volevo che un algoritmo funzionasse con sicurezza, qualunque cosa accada. C'è sempre la (minima) possibilità che i tuoi 2 * m numeri casuali abbiano più di m duplicati, quindi non ne avrai abbastanza per la tua query.
user12861

1
Come si ottiene il numero di righe nella tabella?
Fantastico-o

54

Penso che la soluzione più veloce sia

select * from table where rand() <= .3

Ecco perché penso che questo dovrebbe fare il lavoro.

  • Creerà un numero casuale per ogni riga. Il numero è compreso tra 0 e 1
  • Valuta se visualizzare quella riga se il numero generato è compreso tra 0 e 0,3 (30%).

Ciò presuppone che rand () generi numeri in una distribuzione uniforme. È il modo più rapido per farlo.

Ho visto che qualcuno aveva consigliato quella soluzione e sono stati abbattuti senza prove .. ecco cosa direi a questo -

  • Questo è O (n) ma non è richiesto alcun ordinamento quindi è più veloce di O (n lg n)
  • mysql è molto in grado di generare numeri casuali per ogni riga. Prova questo -

    seleziona rand () dal limite di INFORMATION_SCHEMA.TABLES 10;

Poiché il database in questione è mySQL, questa è la soluzione giusta.


1
Innanzitutto, hai il problema che questo non risponde davvero alla domanda, poiché ottiene un numero semi-casuale di risultati restituiti, vicino a un numero desiderato ma non necessariamente esattamente quel numero, invece di un numero preciso di risultati desiderato.
user12861

1
Successivamente, per quanto riguarda l'efficienza, il tuo è O (n), dove n è il numero di righe nella tabella. Non è così buono come O (m log m), dove m è il numero di risultati desiderati e m << n. Potresti comunque avere ragione che sarebbe più veloce in pratica, perché come dici tu generare rand () e confrontarli con una costante POTREBBE essere molto veloce. Dovresti provarlo per scoprirlo. Con tavoli più piccoli potresti vincere. Con tabelle enormi e un numero molto inferiore di risultati desiderati ne dubito.
user12861

1
Mentre @ user12861 ha ragione sul fatto che non ottiene il numero esatto giusto, è un buon modo per ridurre il set di dati alla giusta dimensione approssimativa.
ojrac

1
In che modo il database risponde alla seguente query - SELECT * FROM table ORDER BY RAND() LIMIT 10000 ? Deve prima creare un numero casuale per ogni riga (uguale alla soluzione che ho descritto), quindi ordinarlo .. le specie sono costose! Questo è il motivo per cui questa soluzione SARÀ più lenta di quella che ho descritto, poiché non è richiesto alcun tipo. Puoi aggiungere un limite alla soluzione che ho descritto e non ti darà più di quel numero di righe. Come qualcuno ha correttamente sottolineato, non ti darà la dimensione del campione ESATTA, ma con campioni casuali, ESATTO molto spesso non è un requisito rigoroso.
ignorante

C'è un modo per specificare il numero minimo di righe?
CMCDragonkai


4

Basta usare

WHERE RAND() < 0.1 

per ottenere il 10% dei record o

WHERE RAND() < 0.01 

per ottenere l'1% dei record, ecc.


1
Questo chiamerà RAND per ogni riga, rendendolo O (n). Il poster stava cercando qualcosa di meglio di quello.
user12861

1
Non solo, ma RAND()restituisce lo stesso valore per le chiamate successive (almeno su MSSQL), il che significa che otterrai l'intera tabella o nessuna di essa con quella probabilità.
Andrew Mao

4

Più veloce di ORDER BY RAND ()

Ho testato questo metodo per essere molto più veloce di ORDER BY RAND(), quindi funziona in O (n) tempo e lo fa in modo straordinariamente veloce.

Da http://technet.microsoft.com/en-us/library/ms189108%28v=sql.105%29.aspx :

Versione non MSSQL : non l'ho testata

SELECT * FROM Sales.SalesOrderDetail
WHERE 0.01 >= RAND()

Versione MSSQL:

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

Questo selezionerà ~ 1% dei record. Quindi, se è necessario selezionare il numero esatto di percentuali o record, stima la percentuale con un certo margine di sicurezza, quindi estrai casualmente i record in eccesso dal set risultante, utilizzando il più costosoORDER BY RAND() metodo .

Ancora più veloce

Sono stato in grado di migliorare ulteriormente questo metodo perché avevo un noto intervallo di valori delle colonne indicizzate.

Ad esempio, se si dispone di una colonna indicizzata con interi distribuiti uniformemente [0..max], è possibile utilizzarla per selezionare N piccoli intervalli in modo casuale. Eseguire questa operazione dinamicamente nel programma per ottenere un set diverso per ogni esecuzione di query. Questa selezione di sottoinsieme sarà O (N) , che può essere inferiore di molti ordini di grandezza rispetto al set di dati completo.

Nel mio test ho ridotto il tempo necessario per ottenere 20 record di campioni (su 20 mil) da 3 minuti utilizzando ORDER BY RAND () fino a 0,0 secondi !


1

Voglio sottolineare che tutte queste soluzioni sembrano campionare senza sostituzione. La selezione delle prime K righe da un ordinamento casuale o l'unione a una tabella che contiene chiavi univoche in ordine casuale produrrà un campione casuale generato senza sostituzione.

Se vuoi che il tuo campione sia indipendente, dovrai campionare con la sostituzione. Vedere la domanda 25451034 per un esempio di come eseguire questa operazione utilizzando un JOIN in un modo simile alla soluzione di user12861. La soluzione è scritta per T-SQL, ma il concetto funziona in qualsiasi database SQL.


0

Partendo dall'osservazione che possiamo recuperare gli id ​​di una tabella (es. Count 5) in base a un set:

select *
from table_name
where _id in (4, 1, 2, 5, 3)

possiamo arrivare al risultato che se potessimo generare la stringa "(4, 1, 2, 5, 3)", allora avremmo un modo più efficiente diRAND() .

Ad esempio, in 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(']', ')');

Se gli ID presentano spazi vuoti, l'arraylist iniziale indicesè il risultato di una query SQL sugli ID.


0

Se hai bisogno di mrighe esatte , realisticamente genererai il tuo sottoinsieme di ID al di fuori di SQL. La maggior parte dei metodi richiede a un certo punto di selezionare la voce "n-esima" e le tabelle SQL non sono affatto array. Anche l'ipotesi che le chiavi siano consecutive per unire solo int casuali tra 1 e il conteggio è difficile da soddisfare: MySQL ad esempio non lo supporta in modo nativo e le condizioni di blocco sono ... complicate .

Ecco una soluzione O(max(n, m lg n))-time, O(n)-space assumendo solo semplici tasti BTREE:

  1. Recupera tutti i valori della colonna chiave della tabella dati in qualsiasi ordine in un array nel tuo linguaggio di scripting preferito in O(n)
  2. Eseguire un riordino Fisher-Yates , fermandosi dopo mgli swap, ed estrarre il sottoarray [0:m-1]inϴ(m)
  3. "Unisci" il sottoarray con il set di dati originale (ad esempio SELECT ... WHERE id IN (<subarray>)) inO(m lg n)

Qualsiasi metodo che genera il sottoinsieme casuale al di fuori di SQL deve avere almeno questa complessità. Il join non può essere più veloce che O(m lg n)con BTREE (quindi le O(m)affermazioni sono fantastiche per la maggior parte dei motori) e lo shuffle è delimitato sotto ne m lg nnon influisce sul comportamento asintotico.

Nello pseudocodice pitonico:

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

0

Seleziona 3000 record casuali in Netezza:

WITH IDS AS (
     SELECT ID
     FROM MYTABLE;
)

SELECT ID FROM IDS ORDER BY mt_random() LIMIT 3000

Oltre ad aggiungere alcune note specifiche del dialetto SQL, non penso che questo risponda alla domanda su come interrogare un campione casuale di righe senza "ORDER BY rand () LIMIT $ 1".
ojrac

0

Provare

SELECT TOP 10000 * FROM table ORDER BY NEWID()

Questo darebbe i risultati desiderati, senza essere troppo complicato?


Nota che NEWID()è specifico di T-SQL.
Peter O.

Mie scuse. È. Grazie È comunque utile sapere se qualcuno viene qui cercando come me in un modo migliore e STA usando T-SQL
Northernlad

ORDER BY NEWID()è funzionalmente uguale a ORDER BY RAND()- chiama RAND()ogni riga dell'insieme - O (n) - e quindi ordina l'intera cosa - O (n lg n). In altre parole, questa è la soluzione peggiore su cui questa domanda sta cercando di migliorare.
ojrac

0

In alcuni dialetti come Microsoft SQL Server, PostgreSQL e Oracle (ma non MySQL o SQLite), puoi fare qualcosa come

select distinct top 10000 customer_id from nielsen.dbo.customer TABLESAMPLE (20000 rows) REPEATABLE (123);

La ragione per non fare a (10000 rows)meno di topè che la TABLESAMPLElogica ti dà un numero estremamente inesatto di righe (come a volte il 75%, a volte l'1,25% di volte), quindi vuoi sovracampionare e selezionare il numero esatto che desideri. Il REPEATABLE (123)è per fornire un seme casuale.


-4

Forse potresti farlo

SELECT * FROM table LIMIT 10000 OFFSET FLOOR(RAND() * 190000)

1
Sembra che questo selezionerebbe una fetta casuale dei miei dati; Sto cercando qualcosa di un po 'più complicato: 10.000 righe distribuite casualmente.
ojrac

Quindi la tua unica opzione, se vuoi farlo nel database, è ORDER BY rand ().
staticsan
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.