Heute gibt es keine komplizierten Fälle oder ausgeklügelte Algorithmen in SQL. Alles wird sehr einfach sein, auf dem Niveau von Kapitän Offensichtlich – wir machen eine Ansicht des Ereignisprotokolls mit einer zeitlichen Sortierung.
Das heißt, hier liegt eine Tabelle in der Datenbank events, und sie hat das Feld ts — genau zu der Zeit, nach der wir diese Einträge geordnet anzeigen möchten:
CREATE TABLE events(
id
serial
PRIMARY KEY
, ts
timestamp
, data
json
);
CREATE INDEX ON events(ts DESC);Es ist klar, dass wir dort nicht nur ein Dutzend Einträge haben werden, deshalb brauchen wir in irgendeiner Form Paging-Navigation.
#0. «Я у мамы погроммист»
cur.execute("SELECT * FROM events;")
rows = cur.fetchall();
rows.sort(key=lambda row: row.ts, reverse=True);
limit = 26
print(rows[offset:offset+limit]);
Das ist fast kein Scherz – es kommt selten vor, aber es gibt es in der Wildnis. Manchmal ist es nach der Arbeit mit ORM schwierig, auf die "direkte" Arbeit mit SQL umzuschalten.
Aber lassen Sie uns zu häufigeren und weniger offensichtlichen Problemen übergehen.
#1. OFFSET
SELECT
...
FROM
events
ORDER BY
ts DESC
LIMIT 26 OFFSET $1; -- 26 - Einträge pro Seite, $1 - Beginn der SeiteWoher kommt hier die Zahl 26? Das ist die ungefähre Anzahl der Einträge, um einen Bildschirm zu füllen. Genauer gesagt sind es 25 sichtbare Einträge plus 1, der signalisiert, dass es in der Auswahl noch etwas gibt und es sinnvoll ist, weiterzugehen.
Natürlich kann dieser Wert nicht in den Anfragekörper "eingebaut" werden, sondern als Parameter übergeben werden. Aber in diesem Fall kann der Planner von PostgreSQL nicht darauf vertrauen, dass es relativ wenige Einträge geben sollte — und wird möglicherweise einen ineffizienten Plan wählen.
Und solange die Ansicht des Protokolls in der Anwendung als Wechseln zwischen visuellen "Seiten" umgesetzt ist, bemerkt niemand lange etwas Verdächtiges. Genau bis zu dem Zeitpunkt, an dem in der Suche nach benutzerfreundlichem UI/UX entschieden wird, die Schnittstelle auf "unendliches Scrollen" umzustellen — das heißt, alle Einträge des Protokolls werden als eine Liste angezeigt, die der Benutzer nach oben und unten scrollen kann.
Und bei einem weiteren Test werden Sie dabei ertappt, dass Sie Einträge im Protokoll duplizieren. Warum, weil die Tabelle einen ordentlichen Index hat (ts), auf den Ihre Anfrage sich stützt?
Genau deshalb, weil Sie nicht berücksichtigt haben, dass ts kein eindeutiger Schlüssel in dieser Tabelle ist. Tatsächlich sind die Werte dafür nicht einzigartig, wie bei jeder „Zeit“ unter realen Bedingungen - daher kann ein und derselbe Eintrag in zwei benachbarten Abfragen leicht von Seite zu Seite „springen“, aufgrund einer anderen Endreihenfolge innerhalb der Sortierung des gleichen Schlüssels.
Tatsächlich verbirgt sich hier ein weiteres Problem, das viel schwieriger zu erkennen ist - einige Einträge werden nicht angezeigt überhaupt! Denn „duplizierte“ Einträge haben einen anderen Platz eingenommen. Eine detaillierte Erklärung mit schönen Bildern finden Sie .
Index erweitern
Ein schlauer Entwickler versteht - der Schlüssel des Indexes muss eindeutig sein, und die einfachste Möglichkeit ist, ihn um ein bereits eindeutiges Feld zu erweitern, wobei sich PK hervorragend eignet:
CREATE UNIQUE INDEX ON events(ts DESC, id DESC);Und die Abfrage mutiert:
SELECT
...
ORDER BY
ts DESC, id DESC
LIMIT 26 OFFSET $1;#2. Переход на «курсоры»
Einige Zeit später kommt ein DBA zu Ihnen und „freut“ sich darüber, dass Ihre Abfragen , und überhaupt, es wird Zeit, auf die Navigation vom zuletzt angezeigten Wert umzuschwenken.Ihre Abfrage mutiert erneut:
SELECT
...
WHERE
(ts, id) < ($1, $2) -- die zuletzt erhaltenen Werte aus dem vorherigen Schritt
ORDER BY
ts DESC, id DESC
LIMIT 26;Sie atmen erleichtert auf, bis es…
#3. Чистка индексов
Denn eines Tages las Ihr DBA und erkannte, dass ein „nicht letzter“ Zeitstempel - das ist nicht gut.. Und er kam erneut zu Ihnen - jetzt mit dem Gedanken, dass dieser Index doch wieder zurückverwandelt werden sollte in (ts DESC).
Aber was ist mit dem ursprünglichen Problem des „Springens“ der Einträge zwischen den Seiten?.. Alles ganz einfach - man muss Blöcke mit einer nicht festen Anzahl von Einträgen auswählen!
Eigentlich, wer verbietet uns, nicht „genau 26“, sondern „mindestens 26“ zu lesen? Beispielsweise so, dass im nächsten Block Einträge mit eindeutig anderen Werten erscheinen, ts dann wird es nämlich auch keine Probleme mit dem „Springen“ von Einträgen zwischen Blöcken geben!
So erreichen Sie das:
SELECT
...
WHERE
ts = coalesce((
SELECT
ts
FROM
events
WHERE
ts < $1
ORDER BY
ts DESC
LIMIT 1 OFFSET 25
), '-infinity')
ORDER BY
ts DESC;Was passiert hier überhaupt?
- Wir gehen 25 Einträge „nach unten“ und erhalten den „Grenzwert“
ts. - Wenn dort bereits nichts mehr ist, ersetzen wir den NULL-Wert durch
-infinity. - Wir ziehen den gesamten Wertebereich zwischen dem erhaltenen Wert
tsund dem übergebenen Parameter $1 (dem vorherigen „letzten“ dargestellten Wert) ab. - Wenn der Block mit weniger als 26 Einträgen zurückgekehrt ist, ist er der letzte.
Oder dasselbe als Bild:

Da wir jetzt nicht über einen bestimmten "Start" verfügen, steht uns nichts im Wege, diese Anfrage umzukehren und eine dynamische Nachladung von Datenblöcken von einem "Fixpunkt" in beide Richtungen zu realisieren – sowohl nach unten als auch nach oben.
Hinweis
- Ja, in diesem Fall greifen wir zweimal auf den Index zu, aber alles "rein nach Index". Daher führt die verschachtelte Abfrage nur zu einem zusätzlichen Index Only Scan..
- Es ist offensichtlich, dass diese Methode nur dann verwendet werden kann, wenn Sie Werte haben,
tsdie sich nur zufällig überschneiden, und es nicht viele sind.. Wenn Ihr typischer Fall jedoch "eine Million Einträge um 00:00:00.000" ist, sollten Sie das nicht tun. Das heißt, Sie sollten einen solchen Fall vermeiden. Aber falls es doch so gekommen ist, verwenden Sie die Variante mit dem erweiterten Index.
Quelle: habr.com
