Impaginazione di MySQL senza doppia query?


115

Mi chiedevo se ci fosse un modo per ottenere il numero di risultati da una query MySQL e allo stesso tempo limitare i risultati.

Per come funziona l'impaginazione (per come la intendo io), prima faccio qualcosa di simile

query = SELECT COUNT(*) FROM `table` WHERE `some_condition`

Dopo aver ottenuto num_rows (query), ho il numero di risultati. Ma poi per limitare effettivamente i miei risultati, devo fare una seconda query come:

query2 = SELECT COUNT(*) FROM `table` WHERE `some_condition` LIMIT 0, 10

La mia domanda: è comunque possibile recuperare il numero totale di risultati che sarebbero forniti E limitare i risultati restituiti in una singola query? O un modo più efficiente per farlo. Grazie!


7
Anche se non avresti COUNT (*) nella query2
dlofrodloh

Risposte:


66

No, è quante applicazioni che vogliono impaginare devono farlo. È affidabile ea prova di proiettile, sebbene esegua la query due volte. Ma puoi memorizzare nella cache il conteggio per alcuni secondi e questo ti aiuterà molto.

L'altro modo è usare la SQL_CALC_FOUND_ROWSclausola e quindi chiamare SELECT FOUND_ROWS(). a parte il fatto che devi mettere la FOUND_ROWS()chiamata in seguito, c'è un problema con questo: c'è un bug in MySQL che questo solletica che influenza le ORDER BYquery rendendole molto più lente su tabelle di grandi dimensioni rispetto all'approccio ingenuo di due query.


2
Tuttavia, non è del tutto a prova di gara, a meno che tu non esegua le due query all'interno di una transazione. Questo in genere non è un problema, però.
NickZoic

Con "affidabile" intendevo che l'SQL stesso restituirà sempre il risultato desiderato, e con "a prova di proiettile" intendevo che non ci sono bug di MySQL che ostacolano ciò che puoi usare SQL. A differenza dell'utilizzo di SQL_CALC_FOUND_ROWS con ORDER BY e LIMIT, in base al bug che ho menzionato.
staticsan

5
Nelle query complesse, l'utilizzo di SQL_CALC_FOUND_ROWS per recuperare il conteggio nella stessa query sarà quasi sempre più lento rispetto all'esecuzione di due query separate. Questo perché significa che tutte le righe dovranno essere recuperate per intero, indipendentemente dal limite, quindi verranno restituite solo quelle specificate nella clausola LIMIT. Vedi anche la mia risposta che ha collegamenti.
thomasrutter

A seconda del motivo per cui ne hai bisogno, potresti anche voler pensare di non recuperare i risultati totali. Sta diventando una pratica più comune implementare metodi di paging automatico. Siti come Facebook, Twitter, Bing e Google utilizzano questo metodo da secoli.
Thomas B

68

Non faccio quasi mai due domande.

Restituire semplicemente una riga in più di quella necessaria, visualizzare solo 10 sulla pagina e, se sono presenti più righe, visualizzare un pulsante "Avanti".

SELECT x, y, z FROM `table` WHERE `some_condition` LIMIT 0, 11
// iterate through and display 10 rows.

// if there were 11 rows, display a "Next" button.

La tua query dovrebbe essere restituita per prima nell'ordine più pertinente. È probabile che alla maggior parte delle persone non interesserà andare a pagina 236 di 412.

Quando esegui una ricerca su Google e i tuoi risultati non sono nella prima pagina, probabilmente vai alla pagina due, non alla nove.


42
In realtà, se non lo trovo sulla prima pagina di una query di Google, di solito salto alla pagina nove.
Phil

3
@Phil l'ho sentito prima, ma perché farlo?
TK123

5
Un po 'tardi, ma ecco il mio ragionamento. Alcune ricerche sono dominate da link farm ottimizzate per i motori di ricerca. Quindi le prime pagine sono le diverse fattorie che combattono per la posizione numero 1, il risultato utile è probabilmente ancora associato alla query, ma non in alto.
Phil

4
COUNTè una funzione aggregata. Come restituisci il conteggio e tutti i risultati in una query? La query precedente restituirà solo 1 riga, indipendentemente da cosa LIMITsia impostata. Se aggiungi GROUP BY, restituirà tutti i risultati ma COUNTnon sarà accurato
pixelfreak

2
Questo è uno degli approcci consigliati da Percona: percona.com/blog/2008/09/24/…
techdude

27

Un altro approccio per evitare la doppia query consiste nel recuperare tutte le righe per la pagina corrente utilizzando prima una clausola LIMIT, quindi eseguire solo una seconda query COUNT (*) se è stato recuperato il numero massimo di righe.

In molte applicazioni, il risultato più probabile sarà che tutti i risultati rientrino in una pagina e dover eseguire l'impaginazione è l'eccezione piuttosto che la norma. In questi casi, la prima query non recupererà il numero massimo di risultati.

Ad esempio, le risposte a una domanda di stackoverflow raramente si riversano su una seconda pagina. I commenti su una risposta raramente superano il limite di 5 o giù di lì richiesto per mostrarli tutti.

Quindi in queste applicazioni puoi semplicemente eseguire prima una query con un LIMIT, quindi finché tale limite non viene raggiunto, sai esattamente quante righe ci sono senza la necessità di eseguire una seconda query COUNT (*), che dovrebbe coprire la maggior parte delle situazioni.


1
@thomasrutter Ho avuto lo stesso approccio, ma oggi ho scoperto un difetto. La pagina finale dei risultati non avrà quindi i dati di impaginazione. cioè, diciamo che ogni pagina dovrebbe avere 25 risultati, l'ultima pagina probabilmente non ne avrà così tanti, diciamo che ne ha 7 ... ciò significa che il conteggio (*) non verrà mai eseguito, e quindi nessuna impaginazione verrà mostrata al utente.
duellante

2
No - se dici 200 risultati, interroghi i successivi 25 e ottieni solo 7 indietro, il che ti dice che il numero totale di risultati è 207 e quindi non devi fare un'altra query con COUNT (*) perché sai già cosa dirà. Hai tutte le informazioni necessarie per mostrare l'impaginazione. Se hai un problema con l'impaginazione che non viene mostrata all'utente, allora hai un bug da qualche altra parte.
thomasrutter

15

Nella maggior parte delle situazioni è molto più veloce e meno dispendioso in termini di risorse farlo in due query separate che farlo in una, anche se ciò sembra controintuitivo.

Se usi SQL_CALC_FOUND_ROWS, per tabelle di grandi dimensioni la tua query è molto più lenta, significativamente più lenta anche rispetto all'esecuzione di due query, la prima con un COUNT (*) e la seconda con un LIMIT. Il motivo è che SQL_CALC_FOUND_ROWS fa sì che la clausola LIMIT venga applicata dopo recuperato le righe anziché prima, quindi recupera l'intera riga per tutti i risultati possibili prima di applicare i limiti. Questo non può essere soddisfatto da un indice perché recupera effettivamente i dati.

Se adotti l'approccio delle due query, la prima che recupera solo COUNT (*) e non recupera effettivamente i dati effettivi, questo può essere soddisfatto molto più rapidamente perché di solito può utilizzare gli indici e non deve recuperare i dati della riga effettiva per ogni riga che guarda. Quindi, la seconda query deve solo esaminare le prime righe $ offset + $ limit e quindi restituire.

Questo post dal blog sulle prestazioni di MySQL lo spiega ulteriormente:

http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

Per ulteriori informazioni sull'ottimizzazione dell'impaginazione, controlla questo post e questo post .


2

La mia risposta potrebbe essere in ritardo, ma puoi saltare la seconda query (con il limite) e filtrare le informazioni tramite il tuo script di back-end. In PHP, ad esempio, potresti fare qualcosa come:

if($queryResult > 0) {
   $counter = 0;
   foreach($queryResult AS $result) {
       if($counter >= $startAt AND $counter < $numOfRows) {
            //do what you want here
       }
   $counter++;
   }
}

Ma ovviamente, quando si hanno migliaia di record da considerare, diventa inefficiente molto rapidamente. Il conteggio pre-calcolato potrebbe essere una buona idea da esaminare.

Ecco una buona lettura sull'argomento: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf


Link è morto, immagino che questo sia quello corretto: percona.com/files/presentations/ppc2009/… . Non modificherò perché non sono sicuro che lo sia.
hectorg87

1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10

16
Questa query restituisce solo il numero totale di record nella tabella; non il numero di record che soddisfano la condizione.
Lawrence Barsanti

1
Il numero totale di record è ciò che è necessario per l'impaginazione (@Lawrence).
imme

Oh, beh, aggiungi semplicemente la whereclausola alla query interna e ottieni il "totale" corretto insieme ai risultati impaginati (la pagina è selezionata con la limitclausola
Erenor Paz

il conteggio delle
sottoquery

1

Per chiunque cerchi una risposta nel 2020. Come da documentazione MySQL:

"Il modificatore di query SQL_CALC_FOUND_ROWS e la relativa funzione FOUND_ROWS () sono deprecati a partire da MySQL 8.0.17 e verranno rimossi in una futura versione di MySQL. In sostituzione, considerando di eseguire la query con LIMIT, quindi una seconda query con COUNT (*) e senza LIMIT per determinare se sono presenti righe aggiuntive. "

Immagino che questo risolva tutto.

https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_found-rows


0

È possibile riutilizzare la maggior parte della query in una sottoquery e impostarla su un identificatore. Ad esempio, una query di film che trova film contenenti l'ordinamento della lettera per runtime sarebbe simile a questa sul mio sito.

SELECT Movie.*, (
    SELECT Count(1) FROM Movie
        INNER JOIN MovieGenre 
        ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
    WHERE Title LIKE '%s%'
) AS Count FROM Movie 
    INNER JOIN MovieGenre 
    ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
WHERE Title LIKE '%s%' LIMIT 8;

Tieni presente che non sono un esperto di database e spero che qualcuno sarà in grado di ottimizzarlo un po 'meglio. Così com'è, eseguendolo direttamente dall'interfaccia della riga di comando SQL, entrambi impiegano ~ 0,02 secondi sul mio laptop.


-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10

3
Questo non risponde alla domanda e un ordine di Rand è davvero una pessima idea.
Dan Walmsley
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.