Warum verlangsamt ein höherer MYSQL-LIMIT-Offset die Abfrage?


173

Kurzes Szenario: Eine Tabelle mit mehr als 16 Millionen Datensätzen [2 GB groß]. Je höher der LIMIT-Offset mit SELECT ist, desto langsamer wird die Abfrage, wenn ORDER BY * primary_key * verwendet wird.

So

SELECT * FROM large ORDER BY `id`  LIMIT 0, 30 

dauert weit weniger als

SELECT * FROM large ORDER BY `id` LIMIT 10000, 30 

Das bestellt nur 30 Platten und so oder so. Es ist also nicht der Overhead von ORDER BY.
Beim Abrufen der letzten 30 Zeilen dauert es ungefähr 180 Sekunden. Wie kann ich diese einfache Abfrage optimieren?


HINWEIS: Ich bin der Autor. MySQL bezieht sich in den oben genannten Fällen nicht auf den Index (PRIMARY). Erklärung finden Sie unter dem folgenden Link des Benutzers "Quassnoi".
Rahman

Antworten:


197

Es ist normal, dass höhere Offsets die Abfrage verlangsamen, da die Abfrage die ersten OFFSET + LIMITDatensätze abzählen muss (und nur LIMITdiese nimmt). Je höher dieser Wert ist, desto länger läuft die Abfrage.

Die Abfrage kann nicht direkt ausgeführt werden, OFFSETda erstens die Datensätze unterschiedlich lang sein können und zweitens Lücken zu gelöschten Datensätzen bestehen können. Es muss jeden Datensatz auf seinem Weg überprüfen und zählen.

Angenommen, es idhandelt sich PRIMARY KEYum eine MyISAMTabelle, können Sie sie mit diesem Trick beschleunigen:

SELECT  t.*
FROM    (
        SELECT  id
        FROM    mytable
        ORDER BY
                id
        LIMIT 10000, 30
        ) q
JOIN    mytable t
ON      t.id = q.id

Siehe diesen Artikel:


7
MySQL "Early Row Lookup" -Verhalten war die Antwort, warum es so lange spricht. Durch den von Ihnen angegebenen Trick werden nur übereinstimmende IDs (direkt vom Index) gebunden, wodurch nicht benötigte Zeilensuchen von zu vielen Datensätzen gespeichert werden. Das hat den Trick gemacht, Hurra!
Rahman

4
@harald: was genau meinst du mit "nicht arbeiten"? Dies ist eine reine Leistungsverbesserung. Wenn kein Index von verwendet werden kann ORDER BYoder der Index alle benötigten Felder abdeckt, benötigen Sie diese Problemumgehung nicht.
Quassnoi

6
@ f055: Die Antwort lautet "Beschleunigen", nicht "Sofort machen". Hast du den ersten Satz der Antwort gelesen?
Quassnoi

3
Ist es möglich, so etwas für InnoDB auszuführen?
NeverEndingQueue

3
@Lanti: Bitte poste es als separate Frage und vergiss nicht, es mit zu markieren postgresql. Dies ist eine MySQL-spezifische Antwort.
Quassnoi

220

Ich hatte selbst genau das gleiche Problem. Angesichts der Tatsache, dass Sie eine große Menge dieser Daten und nicht einen bestimmten Satz von 30 erfassen möchten, führen Sie wahrscheinlich eine Schleife aus und erhöhen den Versatz um 30.

Was Sie stattdessen tun können, ist:

  1. Halten Sie die letzte ID eines Datensatzes (30) (z. B. lastId = 530).
  2. Fügen Sie die Bedingung hinzu WHERE id > lastId limit 0,30

Sie können also immer einen NULL-Offset haben. Sie werden von der Leistungsverbesserung begeistert sein.


Funktioniert das, wenn es Lücken gibt? Was ist, wenn Sie keinen einzigen eindeutigen Schlüssel haben (z. B. einen zusammengesetzten Schlüssel)?
Xaisoft

8
Es ist möglicherweise nicht für alle offensichtlich, dass dies nur funktioniert, wenn Ihre Ergebnismenge nach diesem Schlüssel in aufsteigender Reihenfolge sortiert ist (bei absteigender Reihenfolge funktioniert dieselbe Idee, aber ändern Sie> lastid in <lastid). Es spielt keine Rolle, ob es die ist Primärschlüssel oder ein anderes Feld (oder eine Gruppe von Feldern)
Eloff

Gut gemacht, dieser Mann! Eine sehr einfache Lösung, die mein Problem gelöst hat :-)
oodavid

30
Nur ein Hinweis, dass Limit / Offset häufig in paginierten Ergebnissen verwendet wird und das Halten von lastId einfach nicht möglich ist, da der Benutzer zu jeder Seite springen kann, nicht immer zur nächsten Seite. Mit anderen Worten, der Versatz muss häufig dynamisch basierend auf Seite und Begrenzung berechnet werden, anstatt einem kontinuierlichen Muster zu folgen.
Tom


17

MySQL kann nicht direkt zum 10000. Datensatz (oder zum 80000. Byte als Vorschlag) wechseln, da nicht davon ausgegangen werden kann, dass es so gepackt / geordnet ist (oder dass es kontinuierliche Werte in 1 bis 10000 hat). Obwohl dies in Wirklichkeit so sein könnte, kann MySQL nicht davon ausgehen, dass es keine Lücken / Lücken / gelöschten IDs gibt.

Wie Bob bemerkt hat, muss MySQL also 10000 Zeilen abrufen (oder 10000. Einträge des Index durchlaufen id), bevor die 30 gefunden werden, die zurückgegeben werden sollen.

EDIT : Um meinen Standpunkt zu veranschaulichen

Beachten Sie, dass obwohl

SELECT * FROM large ORDER BY id LIMIT 10000, 30 

wäre langsam (äh) ,

SELECT * FROM large WHERE id >  10000 ORDER BY id LIMIT 30 

wäre schnell (er) und würde die gleichen Ergebnisse zurückgeben, vorausgesetzt, es fehlen keine ids (dh Lücken).


2
Das ist richtig. Aber warum dauert es so lange, wenn diese ID in einem Index (Primärschlüssel) enthalten ist, da sie durch "id" begrenzt ist? Das Optimierungsprogramm sollte direkt auf diesen Index verweisen und dann die Zeilen mit übereinstimmenden IDs (die aus diesem Index stammen) abrufen
Rahman

1
Wenn Sie eine WHERE-Klausel für id verwendet haben, kann diese bis zu dieser Marke reichen. Wenn Sie jedoch ein nach ID geordnetes Limit festlegen, ist dies nur ein relativer Zähler für den Anfang, sodass der gesamte Weg quer verlaufen muss.
Riedsio

Sehr guter Artikel eversql.com/…
Pažout

Arbeitete für mich @Riedsio Danke.
Mahesh Kajale

8

Ich habe ein interessantes Beispiel gefunden, um SELECT-Abfragen zu optimieren. ORDER BY id LIMIT X, Y. Ich habe 35 Millionen Zeilen, also dauerte es ungefähr 2 Minuten, um eine Reihe von Zeilen zu finden.

Hier ist der Trick:

select id, name, address, phone
FROM customers
WHERE id > 990
ORDER BY id LIMIT 1000;

Setzen Sie einfach das WO mit der letzten ID, die Sie erhalten haben, um die Leistung erheblich zu steigern. Für mich war es von 2 Minuten bis 1 Sekunde :)

Weitere interessante Tricks hier: http://www.iheavy.com/2013/06/19/3-ways-to-optimize-for-paging-in-mysql/

Es funktioniert auch mit Strings


1
Dies funktioniert nur für Tabellen, in denen keine Daten gelöscht werden
miro

1
@miro Das stimmt nur, wenn Sie unter der Annahme arbeiten, dass Ihre Abfrage zufällige Seiten nachschlagen kann, was dieses Poster meiner Meinung nach nicht voraussetzt. Obwohl mir diese Methode in den meisten Fällen der realen Welt nicht gefällt, funktioniert sie mit Lücken, solange Sie sie immer auf der zuletzt erhaltenen ID basieren.
Gremio

5

Der zeitaufwändige Teil der beiden Abfragen besteht darin, die Zeilen aus der Tabelle abzurufen. Logischerweise müssen in der LIMIT 0, 30Version nur 30 Zeilen abgerufen werden. In der LIMIT 10000, 30Version werden 10000 Zeilen ausgewertet und 30 Zeilen zurückgegeben. Während des Datenlesevorgangs kann eine Optimierung durchgeführt werden. Beachten Sie jedoch Folgendes:

Was wäre, wenn Sie eine WHERE-Klausel in den Abfragen hätten? Die Engine muss alle qualifizierten Zeilen zurückgeben, die Daten sortieren und schließlich die 30 Zeilen abrufen.

Betrachten Sie auch den Fall, in dem Zeilen nicht in der ORDER BY-Sequenz verarbeitet werden. Alle qualifizierenden Zeilen müssen sortiert werden, um zu bestimmen, welche Zeilen zurückgegeben werden sollen.


1
Ich frage mich nur, warum das Abrufen dieser 10000 Zeilen Zeit in Anspruch nimmt. Der für dieses Feld verwendete Index (ID, bei dem es sich um einen Primärschlüssel handelt) sollte das Abrufen dieser Zeilen so schnell machen, wie das Suchen dieses PK-Index für Datensatz Nr. 10000, was wiederum schnell sein soll, wenn die Datei auf den Offset multipliziert mit der Länge des Indexdatensatzes gesucht wird (dh 10000 * 8 = Byte Nr. 80000 - vorausgesetzt, 8 ist die Länge des Indexdatensatzes)
Rahman

@Rahman - Die einzige Möglichkeit, über die 10000 Zeilen hinaus zu zählen, besteht darin, sie nacheinander zu überschreiten. Dies kann nur einen Index beinhalten, aber es dauert noch einige Zeit, bis die Indexzeilen durchlaufen sind. Es gibt keine MyISAM- oder InnoDB-Struktur, die (in allen Fällen) korrekt nach 10000 "suchen" kann. Der 10000 * 8-Vorschlag geht davon aus, dass (1) MyISAM, (2) ein Datensatz mit fester Länge und (3) niemals aus der Tabelle gelöscht werden . Wie auch immer, MyISAM-Indizes sind BTrees, also würde es nicht funktionieren.
Rick James

Wie diese Antwort sagte, glaube ich, ist der wirklich langsame Teil die Zeilensuche, die nicht die Indizes durchläuft (was sich natürlich auch summiert, aber bei weitem nicht so viel wie die Zeilensuche auf der Festplatte). Aufgrund der für dieses Problem bereitgestellten Problemumgehungsabfragen treten die Zeilensuchen meines Erachtens häufig auf, wenn Sie Spalten außerhalb des Index auswählen - auch wenn sie nicht Teil der Reihenfolge nach oder wo-Klausel sind. Ich habe keinen Grund gefunden, warum dies notwendig ist, aber es scheint der Grund zu sein, warum einige der Problemumgehungen helfen.
Gremio

1

Für diejenigen, die an einem Vergleich und Zahlen interessiert sind :)

Experiment 1: Der Datensatz enthält ungefähr 100 Millionen Zeilen. Jede Zeile enthält mehrere BIGINT-, TINYINT- und zwei TEXT-Felder (absichtlich) mit etwa 1k Zeichen.

  • Blau: = SELECT * FROM post ORDER BY id LIMIT {offset}, 5
  • Orange: = @ Quassnois Methode. SELECT t.* FROM (SELECT id FROM post ORDER BY id LIMIT {offset}, 5) AS q JOIN post t ON t.id = q.id
  • Natürlich erscheint die dritte Methode ... WHERE id>xxx LIMIT 0,5hier nicht, da es sich um eine konstante Zeit handeln sollte.

Experiment 2: Ähnliches, außer dass eine Reihe nur 3 BIGINTs hat.

  • grün: = das blaue vorher
  • rot: = die Orange vorher

Geben Sie hier die Bildbeschreibung ein

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.