Come posso ottimizzare la funzione ORDER BY RAND () di MySQL?


90

Vorrei ottimizzare le mie query, quindi esamino mysql-slow.log.

La maggior parte delle mie query lente contiene ORDER BY RAND(). Non riesco a trovare una vera soluzione per risolvere questo problema. C'è una possibile soluzione su MySQLPerformanceBlog ma non credo che sia sufficiente. Su tabelle scarsamente ottimizzate (o aggiornate di frequente, gestite dall'utente) non funziona o devo eseguire due o più query prima di poter selezionare la mia PHPriga casuale generata.

C'è qualche soluzione per questo problema?

Un esempio fittizio:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
ORDER BY
        RAND()
LIMIT 1

Risposte:


67

Prova questo:

SELECT  *
FROM    (
        SELECT  @cnt := COUNT(*) + 1,
                @lim := 10
        FROM    t_random
        ) vars
STRAIGHT_JOIN
        (
        SELECT  r.*,
                @lim := @lim - 1
        FROM    t_random r
        WHERE   (@cnt := @cnt - 1)
                AND RAND(20090301) < @lim / @cnt
        ) i

Ciò è particolarmente efficace sul MyISAM(dal momento che il COUNT(*)è immediata), ma anche in InnoDBIT di 10volte più efficiente rispetto ORDER BY RAND().

L'idea principale qui è che non ordiniamo, ma invece manteniamo due variabili e calcoliamo il valore running probabilitydi una riga da selezionare nel passaggio corrente.

Vedi questo articolo nel mio blog per maggiori dettagli:

Aggiornare:

Se devi selezionare un solo record casuale, prova questo:

SELECT  aco.*
FROM    (
        SELECT  minid + FLOOR((maxid - minid) * RAND()) AS randid
        FROM    (
                SELECT  MAX(ac_id) AS maxid, MIN(ac_id) AS minid
                FROM    accomodation
                ) q
        ) q2
JOIN    accomodation aco
ON      aco.ac_id =
        COALESCE
        (
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_id > randid
                AND ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        ),
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        )
        )

Questo presume che i tuoi ac_idsiano distribuiti più o meno uniformemente.


Ciao Quassnoi! Prima di tutto, grazie per la tua rapida risposta! Forse è colpa mia ma non è ancora chiara la tua soluzione. Aggiornerò il mio post originale con un esempio concreto e sarò felice se spiegherai la tua soluzione su questo esempio.
fabrik

c'è stato un errore di battitura in "JOIN accomodation aco ON aco.id =" dove aco.id è davvero aco.ac_id. d'altra parte la query corretta non ha funzionato per me perché genera un errore # 1241 - L'operando dovrebbe contenere 1 colonna (e) al quinto SELECT (la quarta sotto-selezione). Ho provato a trovare il problema con le parentesi (se non sbaglio) ma non riesco ancora a trovare il problema.
fabrik

@fabrik: prova ora. Sarebbe davvero utile se avessi pubblicato gli script della tabella in modo che io potessi controllarli prima di pubblicare.
Quassnoi

Grazie, funziona! :) Puoi modificare la parte JOIN ... ON aco.id in JOIN ... ON aco.ac_id così posso accettare la tua soluzione. Grazie ancora! Una domanda: mi chiedo se possibile questo è un caso peggiore come ORDER BY RAND ()? Solo perché questa query ripete alcuni risultati molte volte.
fabrik

1
@Adam: no, è intenzionale, in modo da poter riprodurre i risultati.
Quassnoi

12

Dipende da quanto devi essere casuale. La soluzione che hai collegato funziona abbastanza bene IMO. A meno che tu non abbia grandi lacune nel campo ID, è ancora abbastanza casuale.

Tuttavia, dovresti essere in grado di farlo in una query usando questo (per selezionare un singolo valore):

SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1

Altre soluzioni:

  • Aggiungi un campo mobile permanente chiamato randomalla tabella e riempilo con numeri casuali. È quindi possibile generare un numero casuale in PHP e fare"SELECT ... WHERE rnd > $random"
  • Prendi l'intero elenco di ID e memorizzali nella cache in un file di testo. Leggi il file e scegli un ID casuale da esso.
  • Memorizza nella cache i risultati della query come HTML e conservali per alcune ore.

8
Sono solo io o questa query non funziona? L'ho provato con diverse varianti e tutte lanciano "Uso non valido della funzione di gruppo" ..
Sophivorus

Puoi farlo con una sottoquery SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1ma non sembra funzionare correttamente poiché non restituisce mai l'ultimo record
Mark

11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Sembra che stia facendo il trucco per me
Mark

1

Ecco come lo farei:

SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*)
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != 'draft'
        AND c.acat_slug != 'vendeglatohely'
        AND a.ac_images != 'b:0;';

SET @sql := CONCAT('
  SELECT  a.ac_id,
        a.ac_status,
        a.ac_name,
        a.ac_status,
        a.ac_images
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != ''draft''
        AND c.acat_slug != ''vendeglatohely''
        AND a.ac_images != ''b:0;''
  LIMIT ', @r, ', 1');

PREPARE stmt1 FROM @sql;

EXECUTE stmt1;


la mia tabella non è continua perché viene modificata spesso. per esempio, attualmente il primo ID è 121.
fabrik

3
La tecnica sopra non si basa sul fatto che i valori id siano continui. Sceglie un numero casuale tra 1 e COUNT (*), non 1 e MAX (id) come alcune altre soluzioni.
Bill Karwin il

1
L'uso di OFFSET(a cosa @rserve) non evita una scansione, fino a una scansione completa della tabella.
Rick James

@RickJames, è vero. Se dovessi rispondere a questa domanda oggi, farei la query per chiave primaria. L'uso di un offset con LIMIT esegue la scansione di molte righe. Query per chiave primaria, sebbene molto più veloce, non garantisce una possibilità pari di scegliere ogni riga: favorisce le righe che seguono gli spazi vuoti.
Bill Karwin

1

(Sì, verrò ammaccato per non avere abbastanza carne qui, ma non puoi essere vegano per un giorno?)

Caso: AUTO_INCREMENT consecutivo senza spazi, 1 riga restituita
Caso: AUTO_INCREMENT consecutivo senza spazi, 10 righe
Caso: AUTO_INCREMENT con spazi, 1 riga restituito
Caso: colonna FLOAT extra per randomizzazione
Caso: UUID o colonna MD5

Questi 5 casi possono essere resi molto efficienti per tavoli di grandi dimensioni. Vedi il mio blog per i dettagli.


0

Questo ti darà una singola sottoquery che utilizzerà l'indice per ottenere un id casuale, quindi l'altra query si attiverà ottenendo la tua tabella unita.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
AND accomodation.ac_id IS IN (
        SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1
)

0

La soluzione per il tuo esempio fittizio sarebbe:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation,
        JOIN 
            accomodation_category 
            ON accomodation.ac_category = accomodation_category.acat_id
        JOIN 
            ( 
               SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id
            ) AS Choices 
            USING (ac_id)
WHERE   accomodation.ac_id >= Choices.ac_id 
        AND accomodation.ac_status != 'draft'
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
LIMIT 1

Per saperne di più sulle alternative a ORDER BY RAND(), dovresti leggere questo articolo .


0

Sto ottimizzando molte query esistenti nel mio progetto. La soluzione di Quassnoi mi ha aiutato ad accelerare molto le domande! Tuttavia, trovo difficile incorporare la suddetta soluzione in tutte le query, specialmente per query complicate che coinvolgono molte sottoquery su più tabelle di grandi dimensioni.

Quindi sto usando una soluzione meno ottimizzata. Fondamentalmente funziona allo stesso modo della soluzione di Quassnoi.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / [accomodation_table_row_count]
LIMIT $size

$size * $factor / [accomodation_table_row_count]calcola la probabilità di scegliere una riga casuale. Il rand () genererà un numero casuale. La riga verrà selezionata se rand () è minore o uguale alla probabilità. Ciò esegue efficacemente una selezione casuale per limitare la dimensione della tabella. Poiché esiste la possibilità che restituisca un valore inferiore al conteggio limite definito, è necessario aumentare la probabilità per assicurarsi di selezionare un numero sufficiente di righe. Quindi moltiplichiamo $ size per $ factor (di solito imposto $ factor = 2, funziona nella maggior parte dei casi). Finalmente facciamo il filelimit $size

Il problema ora sta elaborando accomodation_table_row_count . Se conosciamo la dimensione della tabella, POTREBBE codificare la dimensione della tabella. Questo sarebbe il più veloce, ma ovviamente non è l'ideale. Se stai usando Myisam, ottenere il conteggio delle tabelle è molto efficiente. Dato che sto usando innodb, sto solo facendo un semplice conteggio + selezione. Nel tuo caso, sarebbe simile a questo:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / (select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`))
LIMIT $size

La parte difficile sta elaborando la giusta probabilità. Come puoi vedere, il codice seguente calcola in realtà solo la dimensione approssimativa della tabella temporanea (in effetti, troppo approssimativa!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))Ma puoi perfezionare questa logica per dare un'approssimazione della dimensione della tabella più vicina. Nota che è meglio selezionare OVER piuttosto che sotto-selezionare le righe. cioè se la probabilità è impostata su un valore troppo basso, rischi di non selezionare abbastanza righe.

Questa soluzione è più lenta della soluzione di Quassnoi poiché dobbiamo ricalcolare la dimensione della tabella. Tuttavia, trovo questa codifica molto più gestibile. Questo è un compromesso tra accuratezza + prestazioni e complessità di codifica . Detto questo, su tabelle di grandi dimensioni questo è ancora di gran lunga più veloce di Order by Rand ().

Nota: se la logica della query lo consente, eseguire la selezione casuale il prima possibile prima di qualsiasi operazione di join.


-1
function getRandomRow(){
    $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT);
    $res = getRowById($id);
    if(!empty($res))
    return $res;
    return getRandomRow();
}

//rowid is a key on table
function getRowById($rowid=false){

   return db select from table where rowid = $rowid; 
}
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.