MySQL-Paginierung ohne Doppelabfrage?

115

Ich habe mich gefragt, ob es eine Möglichkeit gibt, die Anzahl der Ergebnisse einer MySQL-Abfrage zu ermitteln und gleichzeitig die Ergebnisse zu begrenzen.

So wie Paginierung funktioniert (so wie ich es verstehe), mache ich zuerst so etwas

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

Nachdem ich die num_rows (Abfrage) erhalten habe, habe ich die Anzahl der Ergebnisse. Aber um meine Ergebnisse tatsächlich einzuschränken, muss ich eine zweite Abfrage durchführen wie:

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

Meine Frage: Gibt es überhaupt eine Möglichkeit, die Gesamtzahl der angegebenen Ergebnisse abzurufen UND die in einer einzelnen Abfrage zurückgegebenen Ergebnisse zu begrenzen? Oder eine effizientere Art, dies zu tun. Vielen Dank!

atp
quelle
7
Obwohl Sie COUNT (*) nicht in query2
dlofrodloh

Antworten:

66

Nein, so viele Anwendungen, die paginieren möchten, müssen dies tun. Es ist zuverlässig und kugelsicher, obwohl die Abfrage zweimal durchgeführt wird. Sie können die Anzahl jedoch für einige Sekunden zwischenspeichern, was sehr hilfreich ist.

Die andere Möglichkeit besteht darin, die SQL_CALC_FOUND_ROWSKlausel zu verwenden und dann aufzurufen SELECT FOUND_ROWS(). Abgesehen von der Tatsache, dass Sie den FOUND_ROWS()Aufruf anschließend tätigen müssen, gibt es ein Problem damit: Es gibt einen Fehler in MySQL , der kitzelt und sich auf ORDER BYAbfragen auswirkt, wodurch er in großen Tabellen viel langsamer ist als der naive Ansatz von zwei Abfragen.

staticsan
quelle
2
Es ist jedoch nicht ganz rennbedingungssicher, es sei denn, Sie führen die beiden Abfragen innerhalb einer Transaktion durch. Dies ist jedoch im Allgemeinen kein Problem.
NickZoic
Mit "zuverlässig" meinte ich, dass SQL selbst immer das gewünschte Ergebnis zurückgibt, und mit "kugelsicher" meinte ich, dass es keine MySQL-Fehler gibt, die die Verwendung von SQL behindern. Im Gegensatz zur Verwendung von SQL_CALC_FOUND_ROWS mit ORDER BY und LIMIT gemäß dem von mir erwähnten Fehler.
Statik
5
Bei komplexen Abfragen ist die Verwendung von SQL_CALC_FOUND_ROWS zum Abrufen der Anzahl in derselben Abfrage fast immer langsamer als die Ausführung von zwei separaten Abfragen. Dies liegt daran, dass alle Zeilen unabhängig vom Limit vollständig abgerufen werden müssen. Dann werden nur die in der LIMIT-Klausel angegebenen zurückgegeben. Siehe auch meine Antwort mit Links.
Thomasrutter
Abhängig vom Grund, aus dem Sie dies benötigen, möchten Sie möglicherweise auch daran denken, die Gesamtergebnisse nicht abzurufen. Es wird immer üblicher, Auto-Paging-Methoden zu implementieren. Websites wie Facebook, Twitter, Bing und Google verwenden diese Methode seit Ewigkeiten.
Thomas B
68

Ich mache fast nie zwei Abfragen.

Geben Sie einfach eine Zeile mehr als erforderlich zurück, zeigen Sie nur 10 auf der Seite an, und wenn mehr als angezeigt werden, zeigen Sie die Schaltfläche "Weiter" an.

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.

Ihre Anfrage sollte zuerst in der Reihenfolge der relevantesten zurückgegeben werden. Wahrscheinlich kümmern sich die meisten Leute nicht darum, von 412 auf Seite 236 zu gehen.

Wenn Sie eine Google-Suche durchführen und Ihre Ergebnisse nicht auf der ersten Seite angezeigt werden, wechseln Sie wahrscheinlich zu Seite zwei und nicht zu neun.

Bohrturm
quelle
41
Wenn ich es auf der ersten Seite einer Google-Abfrage nicht finde, gehe ich normalerweise zu Seite neun.
Phil
3
@Phil Ich habe das schon mal gehört, aber warum?
TK123
5
Ein bisschen spät, aber hier ist meine Argumentation. Einige Suchanfragen werden von suchmaschinenoptimierten Linkfarmen dominiert. Die ersten Seiten sind also die verschiedenen Farmen, die um Position 1 kämpfen. Das nützliche Ergebnis ist wahrscheinlich immer noch mit der Abfrage verbunden, nur nicht ganz oben.
Phil
4
COUNTist eine Aggregatfunktion. Wie geben Sie die Anzahl und alle Ergebnisse in einer Abfrage zurück? Die obige Abfrage gibt nur 1 Zeile zurück, unabhängig davon, auf welche LIMITeingestellt ist. Wenn Sie hinzufügen GROUP BY, gibt es alle Ergebnisse zurück, aber das COUNTwird ungenau sein
pixelfreak
2
Dies ist einer der von Percona empfohlenen Ansätze: percona.com/blog/2008/09/24/…
techdude
26

Ein anderer Ansatz zur Vermeidung von Doppelabfragen besteht darin, zuerst alle Zeilen für die aktuelle Seite mit einer LIMIT-Klausel abzurufen und dann nur dann eine zweite COUNT (*) -Abfrage durchzuführen, wenn die maximale Anzahl von Zeilen abgerufen wurde.

In vielen Anwendungen ist das wahrscheinlichste Ergebnis, dass alle Ergebnisse auf eine Seite passen und die Paginierung eher die Ausnahme als die Norm ist. In diesen Fällen ruft die erste Abfrage nicht die maximale Anzahl von Ergebnissen ab.

Beispielsweise werden Antworten auf eine Stapelüberlauffrage selten auf eine zweite Seite übertragen. Kommentare zu einer Antwort überschreiten selten das Limit von 5 oder so, um sie alle anzuzeigen.

In diesen Anwendungen können Sie also einfach zuerst eine Abfrage mit einem LIMIT durchführen. Solange dieses Limit nicht erreicht ist, wissen Sie genau, wie viele Zeilen vorhanden sind, ohne dass eine zweite COUNT (*) - Abfrage erforderlich ist decken die meisten Situationen ab.

thomasrutter
quelle
1
@thomasrutter Ich hatte den gleichen Ansatz, entdeckte jedoch heute einen Fehler damit. Die letzte Ergebnisseite enthält dann nicht die Paginierungsdaten. Nehmen wir an, jede Seite sollte 25 Ergebnisse haben, die letzte Seite wird wahrscheinlich nicht so viele haben, sagen wir, sie hat 7 ... das bedeutet, dass die Anzahl (*) niemals ausgeführt wird und daher keine Paginierung für die Seite angezeigt wird Benutzer.
Duellsy
2
Nein - wenn Sie sagen, 200 Ergebnisse in, fragen Sie die nächsten 25 ab und Sie erhalten nur 7 zurück. Dies bedeutet, dass die Gesamtzahl der Ergebnisse 207 beträgt und Sie daher keine weitere Abfrage mit COUNT (*) durchführen müssen. weil du schon weißt, was es sagen wird. Sie haben alle Informationen, die Sie benötigen, um die Paginierung anzuzeigen. Wenn Sie ein Problem mit der Paginierung haben, die dem Benutzer nicht angezeigt wird, liegt irgendwo anders ein Fehler vor.
Thomasrutter
15

In den meisten Situationen ist es viel schneller und weniger ressourcenintensiv, dies in zwei separaten Abfragen zu tun, als in einer, obwohl dies nicht intuitiv zu sein scheint.

Wenn Sie SQL_CALC_FOUND_ROWS verwenden, wird Ihre Abfrage bei großen Tabellen viel langsamer und sogar deutlich langsamer als bei der Ausführung von zwei Abfragen, die erste mit COUNT (*) und die zweite mit LIMIT. Der Grund dafür ist , dass SQL_CALC_FOUND_ROWS die LIMIT - Klausel angewendet werden verursacht nach Abrufen der Zeilen anstelle von zuvor , sodass die gesamte Zeile für alle möglichen Ergebnisse bevor die Grenzwerte . Dies kann von einem Index nicht erfüllt werden, da er die Daten tatsächlich abruft.

Wenn Sie den Ansatz mit zwei Abfragen wählen, wobei der erste nur COUNT (*) abruft und nicht tatsächlich Daten abruft, kann dies viel schneller erfüllt werden, da normalerweise Indizes verwendet werden können und die tatsächlichen Zeilendaten nicht abgerufen werden müssen jede Zeile, die es betrachtet. Dann muss die zweite Abfrage nur die ersten Zeilen $ offset + $ limit betrachten und dann zurückkehren.

Dieser Beitrag aus dem MySQL-Performance-Blog erklärt dies weiter:

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

Weitere Informationen zur Optimierung der Paginierung finden Sie in diesem Beitrag und in diesem Beitrag .

thomasrutter
quelle
2

Meine Antwort mag verspätet sein, aber Sie können die zweite Abfrage (mit dem Limit) überspringen und die Informationen einfach durch Ihr Back-End-Skript filtern. In PHP können Sie beispielsweise Folgendes tun:

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

Aber wenn Sie Tausende von Datensätzen berücksichtigen müssen, wird dies natürlich sehr schnell ineffizient. Eine vorberechnete Anzahl ist vielleicht eine gute Idee.

Hier ist eine gute Lektüre zu diesem Thema: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf

Kama
quelle
Link ist tot, ich denke, das ist der richtige: percona.com/files/presentations/ppc2009/… . Wird nicht bearbeitet, da ich nicht sicher bin, ob dies der Fall ist.
Hektorg87
1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
Cris McLaughlin
quelle
16
Diese Abfrage gibt nur die Gesamtzahl der Datensätze in der Tabelle zurück. nicht die Anzahl der Datensätze, die der Bedingung entsprechen.
Lawrence Barsanti
1
Die Gesamtzahl der Datensätze wird für die Paginierung benötigt (@Lawrence).
Imme
Oh, nun, fügen Sie einfach die whereKlausel zur inneren Abfrage hinzu und Sie erhalten die richtige "Summe" zusammen mit den ausgelagerten Ergebnissen (Seite wird mit der limitKlausel ausgewählt
Erenor Paz
Die Anzahl der
Unterabfragen
1

Für alle, die 2020 nach einer Antwort suchen. Gemäß MySQL-Dokumentation:

"Der SQL_CALC_FOUND_ROWS-Abfragemodifikator und die zugehörige FOUND_ROWS () -Funktion sind ab MySQL 8.0.17 veraltet und werden in einer zukünftigen MySQL-Version entfernt. Als Ersatz sollten Sie Ihre Abfrage mit LIMIT ausführen und anschließend eine zweite Abfrage mit COUNT (*). und ohne LIMIT, um festzustellen, ob zusätzliche Zeilen vorhanden sind. "

Ich denke, das regelt das.

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

Igor K.
quelle
0

Sie können den größten Teil der Abfrage in einer Unterabfrage wiederverwenden und auf einen Bezeichner setzen. Zum Beispiel würde eine Filmabfrage, die Filme findet, die die Reihenfolge der Buchstaben nach Laufzeit enthalten, auf meiner Website so aussehen.

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;

Beachten Sie, dass ich kein Datenbankexperte bin und hoffe, dass jemand dies ein bisschen besser optimieren kann. Da es direkt über die SQL-Befehlszeilenschnittstelle ausgeführt wird, benötigen beide auf meinem Laptop ~ 0,02 Sekunden.

Philip Rollins
quelle
-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10
John
quelle
3
Dies beantwortet die Frage nicht und eine Bestellung per Rand ist eine wirklich schlechte Idee.
Dan Walmsley