Es stellt sich regelmĂ€Ăig die Aufgabe, verwandte Daten anhand einer Reihe von SchlĂŒsseln zu finden, bis wir die benötigte Gesamtanzahl an DatensĂ€tzen erreicht haben..
Das gĂ€ngigste Beispiel ist, die 20 Ă€ltesten Aufgaben anzuzeigen,die auf der Liste der Mitarbeiter stehen, (zum Beispiel innerhalb einer Abteilung). FĂŒr verschiedene Management-Dashboards mit kurzen Zusammenfassungen zu den Arbeitsbereichen ist ein solches Thema hĂ€ufig erforderlich. In diesem Artikel betrachten wir die Implementierung einer ânaivenâ Lösung fĂŒr eine solche Aufgabe in PostgreSQL sowie eine âintelligentereâ und ganz komplexe Algorithmus-âSchleifeâ in SQL mit einem Ausstiegsbedingung basierend auf gefundenen Daten,

die sowohl fĂŒr die allgemeine Weiterbildung als auch fĂŒr die Anwendung in anderen Ă€hnlichen FĂ€llen nĂŒtzlich sein kann. Lassen Sie uns einen Beispieldatensatz ausdem vorherigen Artikel nehmen. Um sicherzustellen, dass die ausgegebenen DatensĂ€tze bei Ăbereinstimmung der sortierten Werte nicht von Mal zu Mal âhĂŒpfenâ,
erweitern wir den Index, indem wir den PrimĂ€rschlĂŒssel hinzufĂŒgen. . Um sicherzustellen, dass die ausgegebenen EintrĂ€ge bei gleichen sortierten Werten nicht "springen", erweitern wir den Index durch die HinzufĂŒgung des PrimĂ€rschlĂŒssels.Das verleiht ihm zudem sofortige Einzigartigkeit und garantiert uns eine eindeutige Sortierreihenfolge:
INDEX FĂR task(owner_id, task_date, id) ERSTELLEN;
-- und entfernen wir den alten
INDEX task_owner_id_task_date_idx HEBEN;So wie es klingt, so wird es geschrieben
ZunĂ€chst entwerfen wir die einfachste Anfrage, indem wir die IDs der AusfĂŒhrer ĂŒbergeben :
SELECT
*
FROM
task
WHERE
owner_id = ANY('{1,2,4,8,16,32,64,128,256,512}'::integer[])
ORDER BY
task_date, id
LIMIT 20; 
Es ist etwas enttĂ€uschend â wir haben nur 20 DatensĂ€tze angefordert, aber der Index-Scan hat uns 960 Zeilen zurĂŒckgegeben, die wir dann auch noch sortieren mussten⊠Lassen Sie uns versuchen, weniger zu lesen.
unnest + ARRAY
Der erste Gedanke, der uns helfen wird â wenn wir nur 20 sortierte DatensĂ€tze brauchen, reicht es, nicht mehr als 20 sortierte in derselben Reihenfolge pro SchlĂŒssel zu lesen. GlĂŒcklicherweise haben wir einen geeigneten Index (owner_id, task_date, id). Wir nutzen dasselbe Extraktions- und âSpaltenumwandlungsâ-Mechanismus
der vollstÀndigen Tabelle , wie auch in. Zudem wenden wir die Aggregation in ein Array mit der Funktion ARRAY() WITH T AS ( SELECT unnest(ARRAY( SELECT t FROM task t WHERE owner_id = unnest ORDER BY task_date, id LIMIT 20 -- hier begrenzen wir... )) r FROM unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[]) ) SELECT (r).* FROM T ORDER BY (r).task_date, (r).id LIMIT 20; -- ... und hier auch - ebenfalls:
WITH T AS (
SELECT
unnest(ARRAY(
SELECT
t
FROM
task t
WHERE
owner_id = unnest
ORDER BY
task_date, id
LIMIT 20 -- hier begrenzen wir...
)) r
FROM
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
SELECT
(r).*
FROM
T
ORDER BY
(r).task_date, (r).id
LIMIT 20; -- ... und hier ebenfalls - 
Oh, das ist schon viel besser! 40 % schneller und 4,5 Mal weniger Daten musste gelesen werden.
Materialisierung von TabelleneintrĂ€gen ĂŒber CTEIch möchte darauf hinweisen, dass in einigen FĂ€llen der Versuch, sofort mit den Feldern des Datensatzes nach dessen Suche im Unterabfrage zu arbeiten, ohne in CTE zu âverpackenâ, zu âVervielfachungâ des InitPlans proportional zur Anzahl dieser Felder fĂŒhren kann:
SELECT
((
SELECT
t
FROM
task t
WHERE
owner_id = 1
ORDER BY
task_date, id
LIMIT 1
).*);Ergebnis (Kosten=4.77..4.78 Zeilen=1 Breite=16) (tatsÀchliche Zeit=0.063..0.063 Zeilen=1 Schleifen=1)
Puffer: Shared Hit=16
InitPlan 1 (gibt $0 zurĂŒck)
-> Limit (Kosten=0.42..1.19 Zeilen=1 Breite=48) (tatsÀchliche Zeit=0.031..0.032 Zeilen=1 Schleifen=1)
Puffer: Shared Hit=4
-> Index-Scan mithilfe von task_owner_id_task_date_id_idx auf Aufgabe t (Kosten=0.42..387.57 Zeilen=500 Breite=48) (tatsÀchliche Zeit=0.030..0.030 Zeilen=1 Schleifen=1)
Index-Bedingung: (owner_id = 1)
Puffer: Shared Hit=4
InitPlan 2 (gibt $1 zurĂŒck)
-> Limit (Kosten=0.42..1.19 Zeilen=1 Breite=48) (tatsÀchliche Zeit=0.008..0.009 Zeilen=1 Schleifen=1)
Puffer: Shared Hit=4
-> Index-Scan mithilfe von task_owner_id_task_date_id_idx auf Aufgabe t_1 (Kosten=0.42..387.57 Zeilen=500 Breite=48) (tatsÀchliche Zeit=0.008..0.008 Zeilen=1 Schleifen=1)
Index-Bedingung: (owner_id = 1)
Puffer: Shared Hit=4
InitPlan 3 (gibt $2 zurĂŒck)
-> Limit (Kosten=0.42..1.19 Zeilen=1 Breite=48) (tatsÀchliche Zeit=0.008..0.008 Zeilen=1 Schleifen=1)
Puffer: Shared Hit=4
-> Index-Scan mithilfe von task_owner_id_task_date_id_idx auf Aufgabe t_2 (Kosten=0.42..387.57 Zeilen=500 Breite=48) (tatsÀchliche Zeit=0.008..0.008 Zeilen=1 Schleifen=1)
Index-Bedingung: (owner_id = 1)
Puffer: Shared Hit=4
InitPlan 4 (gibt $3 zurĂŒck)
-> Limit (Kosten=0.42..1.19 Zeilen=1 Breite=48) (tatsÀchliche Zeit=0.009..0.009 Zeilen=1 Schleifen=1)
Puffer: Shared Hit=4
-> Index-Scan mithilfe von task_owner_id_task_date_id_idx auf Aufgabe t_3 (Kosten=0.42..387.57 Zeilen=500 Breite=48) (tatsÀchliche Zeit=0.009..0.009 Zeilen=1 Schleifen=1)
Index-Bedingung: (owner_id = 1)
Puffer: Shared Hit=4
Die gleiche Datensatz wurde viermal âabgefragtâ... Bis PostgreSQL 11 war dieses Verhalten regelmĂ€Ăig anzutreffen, und die Lösung besteht darin, sie in ein CTE zu âwickelnâ, was in diesen Versionen eine eindeutige Grenze fĂŒr den Optimierer darstellt.
Rekursiver Akkumulator
In der vorherigen Version haben wir insgesamt gelesen 200 Zeilen fĂŒr die benötigten 20. Nicht mehr 960, aber noch weniger â ist das möglich?
Lassen Sie uns das Wissen nutzen, dass wir benötigen insgesamt 20 DatensĂ€tze. Das heiĂt, wir werden die Datenauslesung nur bis zur Erreichung der benötigten Menge iterieren.
Schritt 1: Startliste
Offensichtlich sollte unsere âZielâ-Liste aus 20 DatensĂ€tzen mit den âerstenâ DatensĂ€tzen eines unserer owner_id-SchlĂŒssel beginnen. Daher finden wir zuerst solche âallererstenâ fĂŒr jeden der SchlĂŒssel und tragen sie in die Liste ein, sortiert nach der Reihenfolge, die wir wollen â (task_date, id).

Schritt 2: Finden der ânĂ€chstenâ DatensĂ€tze
Nun, wenn wir den ersten Datensatz aus unserer Liste nehmen und anfangen, weiter im Index zu âgehenâ, wĂ€hrend wir den owner_id-SchlĂŒssel beibehalten, dann sind alle gefundenen DatensĂ€tze genau die nĂ€chsten in der resultierenden Auswahl. NatĂŒrlich nur, bis wir den Anwendungskey des zweiten Datensatzes in der Liste ĂŒberschreiten.
Wenn es so aussieht, dass wir den zweiten Eintrag "ĂŒberschritten" haben, dann sollte der letzte gelesene Eintrag anstelle des ersten in die Liste aufgenommen werden (mit derselben owner_id), danach sortieren wir die Liste erneut.

Das bedeutet, dass wir immer nur einen Eintrag pro SchlĂŒssel in der Liste haben (wenn die EintrĂ€ge aufgebraucht sind und wir die "Grenze" nicht ĂŒberschritten haben, wird der erste Eintrag einfach entfernt und es wird nichts hinzugefĂŒgt), und diese sind immer nach dem aufsteigenden Anwendungs-SchlĂŒssel (task_date, id) sortiert.

Schritt 3: Wir filtern und "entfalten" die EintrÀge
In einigen Zeilen unserer rekursiven Auswahl rv werden EintrĂ€ge dupliziert â zuerst finden wir solche, die "die Grenze des zweiten Eintrags der Liste ĂŒberschreiten", und setzen sie dann als den ersten in der Liste ein. Das erste Auftreten muss gefiltert werden.
Schrecklicher endgĂŒltiger Anfragesatz
MIT REKURSIVE T AS (
-- #1 : FĂŒgen wir die "ersten" EintrĂ€ge fĂŒr jeden SchlĂŒssel in die Liste ein
MIT wrap AS ( -- "materialisieren" wir die DatensÀtze, damit der Zugriff auf die Felder keine Multiplikation von InitPlan/SubPlan erzeugt
MIT T AS (
AUSWĂHLEN
(
AUSWĂHLEN
r
VON
task r
WO
owner_id = unnest
BESTELLEN NACH
task_date, id
LIMIT 1
) r
VON
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
AUSWĂHLEN
array_agg(r BESTELLEN NACH (r).task_date, (r).id) list -- sortieren die Liste in der gewĂŒnschten Reihenfolge
VON
T
)
AUSWĂHLEN
list
, list[1] rv
, FALSE not_cross
, 0 size
VON
wrap
UNION ALL
-- #2 : Lesen wir die EintrĂ€ge des ersten SchlĂŒssels, bis wir ĂŒber den zweiten SchlĂŒssel hinausgehen
AUSWĂHLEN
FALL
-- wenn nichts fĂŒr den ersten Eintrag gefunden wurde
WENN X._r IST NICHT UNTERSCHIEDLICH VON NULL DANN
T.list[2:] -- entfernen wir ihn aus der Liste
-- wenn wir den zweiten SchlĂŒssel nicht ĂŒberschritten haben
WENN X.not_cross THEN
T.list -- ziehen wir einfach die gleiche Liste ohne Modifikationen weiter
-- wenn der zweite Eintrag nicht mehr in der Liste ist
WENN T.list[2] IST NULL DANN
-- einfach eine leere Liste zurĂŒckgeben
'{}'
-- sortieren das Dictionary um, entfernen den ersten Eintrag und fĂŒgen den letzten der gefundenen hinzu
SONST (
AUSWĂHLEN
coalesce(T.list[2] || array_agg(r BESTELLEN NACH (r).task_date, (r).id), '{}')
VON
unnest(T.list[3:] || X._r) r
)
END
, X._r
, X.not_cross
, T.size + X.not_cross::integer
VON
T
, LATERAL(
MIT wrap AS ( -- "materialisieren" wir die DatensÀtze
AUSWĂHLEN
FALL
-- wenn wir trotzdem ĂŒber den zweiten Eintrag hinausgegangen sind
WENN NICHT T.not_cross
-- dann ist der benötigte Datensatz der erste aus der Liste
DANN T.list[1]
SONST ( -- wenn wir ihn nicht ĂŒberschritten haben, ist der SchlĂŒssel wie im vorherigen Eintrag geblieben - orientieren wir uns daran
AUSWĂHLEN
_r
VON
task _r
WO
owner_id = (rv).owner_id UND
(task_date, id) > ((rv).task_date, (rv).id)
BESTELLEN NACH
task_date, id
LIMIT 1
)
END _r
)
AUSWĂHLEN
_r
, FALL
-- wenn der zweite Eintrag nicht mehr in der Liste ist, aber wir wenigstens etwas gefunden haben
WENN list[2] IST NULL UND _r IST NICHT UNTERSCHIEDLICH VON NULL DANN
TRUE
SONST -- nichts gefunden oder "ĂŒberschritten"
coalesce(((_r).task_date, (_r).id) < ((list[2]).task_date, (list[2]).id), FALSE)
END not_cross
VON
wrap
) X
WO
T.size < 20 UND -- begrenzen wir hier die Anzahl
T.list IST NICHT UNTERSCHIEDLICH VON '{}' -- oder bis die Liste nicht mehr existiert
)
-- #3 : "entfalten" wir die EintrÀge - die Reihenfolge ist durch die Erstellung garantiert
AUSWĂHLEN
(rv).*
VON
T
WO
not_cross; -- nehmen wir nur die "nicht ĂŒberschreitenden" EintrĂ€ge 
So haben wir 50% LesevorgĂ€nge gegen 20% Laufzeit eingetauscht. Das bedeutet, wenn Sie Grund zur Annahme haben, dass das Lesen lange dauern könnte (z.B. wenn die Daten oft nicht im Cache sind und von der Festplatte abgerufen werden mĂŒssen), kann man durch diesen Ansatz weniger auf das Lesen angewiesen sein.
In jedem Fall hat sich die Laufzeit besser ergeben als im ânaivenâ ersten Ansatz. Welche dieser 3 Varianten Sie verwenden möchten, liegt an Ihnen.
Quelle: habr.com
