SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“

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

SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“

„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, dem vorherigen Artikelerweitern 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. 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;:

Ein wenig traurig — wir hatten nur 20 DatensĂ€tze bestellt, und der Index-Scan hat uns

SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“
[auf explain.tensor.ru anschauen]

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 im vorherigen ArtikelOh, das ist schon viel besser! 40% schneller, und wir mussten 4.5 Mal weniger Daten:

lesen.

SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“
[auf explain.tensor.ru anschauen]

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).

SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“

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.

SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“

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).

SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“

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

SQL HowTo: Schreiben einer While-Schleife direkt in die Abfrage, oder „Elementare Drei-Wege-VerknĂŒpfung“
[auf explain.tensor.ru anschauen]

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

60GB SSD 8Gb DDR4