Gelegentlich tritt die Aufgabe auf, zusammenhĂ€ngende Daten nach einer Reihe von SchlĂŒsseln zu suchen, bis wir die benötigte Gesamtanzahl von DatensĂ€tzen erreicht haben..
Ein besonders âlebendigerâ Beispiel â die Darstellung von den 20 Ă€ltesten Aufgaben,die auf der Liste der Mitarbeiter stehen, (zum Beispiel innerhalb einer Abteilung). FĂŒr verschiedene Management âDashboardsâ mit kurzen Zusammenfassungen ĂŒber Arbeitsbereiche ist ein Ă€hnliches Thema hĂ€ufig erforderlich. In diesem Artikel besprechen wir die Umsetzung einer ânaivenâ Lösung fĂŒr diese Aufgabe in PostgreSQL, eine âintelligentereâ und eine ganz komplexe Algorithmus

âSchleifeâ in SQL mit der Bedingung zum Ausstieg aus den gefundenen Daten, die sowohl fĂŒr das allgemeine Wissen als auch fĂŒr die Anwendung in anderen Ă€hnlichen FĂ€llen nĂŒtzlich sein kann.Lass uns einen Testdatensatz aus
nehmen. Damit die ausgegebenen DatensĂ€tze nicht âspringenâ von Mal zu Mal bei Ăbereinstimmungen der sortierten Werte, erweitern wir den thematischen Index durch HinzufĂŒgen des PrimĂ€rschlĂŒssels. Das verleiht ihm sofort Einzigartigkeit und garantiert uns eine eindeutige Reihenfolge der Sortierung:CREATE INDEX ON task(owner_id, task_date, id); -- und entfernen wir den alten DROP INDEX task_owner_id_task_date_idx;
Wie es sich anhört, so wird es geschrieben.ZunÀchst skizzieren wir die einfachste Variante der Abfrage, indem wir die IDs der Bearbeiter
als Array als Eingabeparameter ĂŒbergeben. :
Ein wenig traurig â wir hatten nur 20 DatensĂ€tze bestellt, und der Index-Scan hat uns 
960 Zeilen zurĂŒckgegeben, die wir dann noch sortieren mussten⊠Lass uns versuchen, weniger zu lesen.unnest + ARRAY.
Der erste Gedanke, der uns helfen wird â wenn wir nur
20 sortierte DatensĂ€tze benötigen, reicht es aus, nicht mehr als 20 sortierte in derselben Reihenfolge fĂŒr jeden SchlĂŒssel zu lesen. Zum GlĂŒck gibt es einen geeigneten Index (owner_id, task_date, id) bei uns. Wir werden dasselbe Extraktions- und âColumn-Switchingâ-Mechanismus verwenden, der fĂŒr den gesamten Datensatz der Tabelle geeignet ist, was auch in
der Fall ist. Ebenso wenden wir die Aggregation in ein Array mit der Funktion ARRAY() an.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 wir mussten 4.5 Mal weniger Daten:
lesen. 
Materialisierung von TabellendatensÀtzen durch CTE. Ich möchte darauf hinweisen, dass es in einigen FÀllen
wichtig sein kann.Ich möchte darauf hinweisen, dass es in einigen FĂ€llen Der Versuch, sofort mit den Feldern des Eintrags nach seiner Suche in einer Unterabfrage zu arbeiten, ohne sie in CTE zu "verpacken", kann zu der "Multiplikation" des InitPlans verhĂ€ltnismĂ€Ăig zur Anzahl dieser Felder fĂŒhren:
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: geteilte Treffer=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: geteilte Treffer=4
-> Index-Scan mit task_owner_id_task_date_id_idx auf task 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: geteilte Treffer=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: geteilte Treffer=4
-> Index-Scan mit task_owner_id_task_date_id_idx auf task 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: geteilte Treffer=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: geteilte Treffer=4
-> Index-Scan mit task_owner_id_task_date_id_idx auf task 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: geteilte Treffer=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: geteilte Treffer=4
-> Index-Scan mit task_owner_id_task_date_id_idx auf task 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: geteilte Treffer=4
Der gleiche Eintrag wurde 4 Mal "gesucht"⊠Bis einschlieĂlich PostgreSQL 11 kommt dieses Verhalten regelmĂ€Ăig vor, und die Lösung besteht darin, ihn in CTE zu "verpacken", was in diesen Versionen eine uneingeschrĂ€nkte Grenze fĂŒr den Optimierer darstellt.
Rekursiver Akkumulator
In der vorherigen Version haben wir insgesamt 200 Zeilen fĂŒr die benötigten 20. Es sind nicht mehr 960, aber noch weniger â geht das?
Lassen Sie uns versuchen, das Wissen zu nutzen, dass wir insgesamt 20 EintrĂ€ge benötigen. Das heiĂt, wir werden die Datenauslese nur bis zur Erreichung der benötigten Anzahl iterieren.
Schritt 1: Ausgangsliste
Offensichtlich sollte unsere âZielâ-Liste von 20 EintrĂ€gen mit den âerstenâ EintrĂ€gen eines unserer owner_id-SchlĂŒssel beginnen. Lassen Sie uns daher zunĂ€chst solche finden, âdie allererstenâ fĂŒr jeden SchlĂŒssel und sie in die Liste aufnehmen, sortiert in der Reihenfolge, die wir wĂŒnschen â (task_date, id).

Schritt 2: Finden der ânĂ€chstenâ EintrĂ€ge
Wenn wir jetzt den ersten Eintrag aus unserer Liste nehmen und anfangen, weiter im Index zu âschreitenâ unter Beibehaltung des owner_id-SchlĂŒssels, dann sind alle gefundenen EintrĂ€ge genau die nĂ€chsten in der Ergebnismenge. NatĂŒrlich nur solange wir den Anwendungs-SchlĂŒssel nicht ĂŒberschreiten zweite Eintragung in der Liste.
Wenn es so aussieht, als ob wir die zweite Eintragung "ĂŒberschritten" haben, dann sollte der zuletzt gelesene Eintrag anstelle des ersten in die Liste eingefĂŒgt werden (mit der gleichen owner_id), danach wird die Liste erneut sortiert.

Das bedeutet, dass in der Liste immer nicht mehr als ein Eintrag fĂŒr jeden der SchlĂŒssel vorhanden ist (wenn die EintrĂ€ge aufgebraucht sind und wir nicht "ĂŒberschritten" haben, verschwindet einfach der erste Eintrag aus der Liste und es wird nichts hinzugefĂŒgt), wobei sie immer sortiert sind in aufsteigender Reihenfolge des Anwendungs-SchlĂŒssels (task_date, id).

Schritt 3: Wir filtern und "entfalten" die EintrÀge
In einigen Zeilen unserer rekursiven Auswahl werden einige EintrĂ€ge rv dupliziert â zuerst finden wir solche, die "die Grenze des 2. Eintrags der Liste ĂŒberschreiten", und dann setzen wir sie als den 1. aus der Liste ein. Das erste Erscheinen muss herausgefiltert werden.
Schrecklicher endgĂŒltiger Query
MIT REKURSIVEN T AS (
-- #1 : Wir fĂŒgen die "ersten" EintrĂ€ge fĂŒr jeden der SchlĂŒssel des Satzes in die Liste ein
MIT wrap AS ( -- "materialisieren" wir die DatensÀtze, damit der Zugriff auf die Felder keine Multiplikation von InitPlan/SubPlan verursacht
MIT T AS (
SELECT
(
SELECT
r
FROM
task r
WHERE
owner_id = unnest
ORDER BY
task_date, id
LIMIT 1
) r
FROM
unnest('{1,2,4,8,16,32,64,128,256,512}'::integer[])
)
SELECT
array_agg(r ORDER BY (r).task_date, (r).id) list -- wir sortieren die Liste in der gewĂŒnschten Reihenfolge
FROM
T
)
SELECT
list
, list[1] rv
, FALSE not_cross
, 0 size
FROM
wrap
UNION ALL
-- #2 : Wir lesen die EintrĂ€ge des ersten SchlĂŒssels, bis wir den zweiten ĂŒberschreiten
SELECT
CASE
-- wenn fĂŒr den ersten SchlĂŒssel nichts gefunden wurde
WHEN X._r IS NOT DISTINCT FROM NULL THEN
T.list[2:] -- wir entfernen ihn aus der Liste
-- wenn wir den Anwendungs-SchlĂŒssel des zweiten Eintrags NICHT ĂŒberschritten haben
WHEN X.not_cross THEN
T.list -- wir ziehen einfach die gleiche Liste ohne Modifikationen durch
-- wenn in der Liste der zweite Eintrag nicht mehr vorhanden ist
WHEN T.list[2] IS NULL THEN
-- wir geben einfach eine leere Liste zurĂŒck
'{}'
-- wir sortieren das Wörterbuch neu, indem wir den ersten Eintrag entfernen und den letzten der gefundenen hinzufĂŒgen
ELSE (
SELECT
coalesce(T.list[2] || array_agg(r ORDER BY (r).task_date, (r).id), '{}')
FROM
unnest(T.list[3:] || X._r) r
)
END
, X._r
, X.not_cross
, T.size + X.not_cross::integer
FROM
T
, LATERAL(
MIT wrap AS ( -- "materialisieren" wir den Eintrag
SELECT
CASE
-- wenn wir doch ĂŒber den zweiten Eintrag "geschritten" sind
WHEN NOT T.not_cross
-- dann ist der benötigte Eintrag der erste aus der Liste
THEN T.list[1]
ELSE ( -- wenn wir nicht ĂŒberschritten haben, bleibt der SchlĂŒssel wie im vorherigen Eintrag - wir verlassen uns darauf
SELECT
_r
FROM
task _r
WHERE
owner_id = (rv).owner_id AND
(task_date, id) > ((rv).task_date, (rv).id)
ORDER BY
task_date, id
LIMIT 1
)
END _r
)
SELECT
_r
, CASE
-- wenn der zweite Eintrag nicht mehr in der Liste ist, wir aber trotzdem etwas gefunden haben
WHEN list[2] IS NULL AND _r IS DISTINCT FROM NULL THEN
TRUE
ELSE -- wir haben nichts gefunden oder "ĂŒberschritten"
coalesce(((_r).task_date, (_r).id) < ((list[2]).task_date, (list[2]).id), FALSE)
END not_cross
FROM
wrap
) X
WHERE
T.size < 20 AND -- hier begrenzen wir die Anzahl
T.list IS DISTINCT FROM '{}' -- oder solange die Liste nicht leer ist
)
-- #3 : Wir "entfalten" die EintrÀge - die Reihenfolge ist durch den Aufbau garantiert
SELECT
(rv).*
FROM
T
WHERE
not_cross; -- wir nehmen nur die "nicht ĂŒberschreitenden" EintrĂ€ge 
So haben wir 50 % der Datenlesungen gegen 20 % der AusfĂŒhrungszeit getauscht.Das bedeutet, wenn Sie Grund zur Annahme haben, dass das Lesen langwierig sein könnte (zum Beispiel, wenn die Daten hĂ€ufig nicht im Cache sind und von der Festplatte abgerufen werden mĂŒssen), können Sie in dieser Weise weniger vom Lesen abhĂ€ngen.
Auf jeden Fall war die AusfĂŒhrungszeit besser als im "naiven" ersten Ansatz. Welchen dieser 3 AnsĂ€tze Sie wĂ€hlen, bleibt Ihnen ĂŒberlassen.
Quelle: habr.com
