PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Optimieren Sie die GROUP BY-Abfrage, um die neueste Zeile pro Benutzer abzurufen

Für die beste Leseleistung benötigen Sie einen mehrspaltigen Index:

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

Um Nur-Index-Scans durchzuführen möglich, fügen Sie die ansonsten nicht benötigte Spalte payload hinzu in einem überdeckenden Index mit dem INCLUDE -Klausel (Postgres 11 oder höher):

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Siehe:

  • Helfen abdeckende Indizes in PostgreSQL beim JOIN-Spalten?

Fallback für ältere Versionen:

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Warum DESC NULLS LAST ?

  • Unbenutzter Index in Datumsbereichsabfrage

Für wenige Zeilen pro user_id oder kleine Tische DISTINCT ON ist normalerweise am schnellsten und einfachsten:

  • Erste Zeile in jeder GROUP BY-Gruppe auswählen?

Für viele Zeilen pro user_id ein Index-Skip-Scan (oder loser Index-Scan ) ist (viel) effizienter. Das ist bis Postgres 12 nicht implementiert - die Arbeit an Postgres 14 ist im Gange. Aber es gibt Möglichkeiten, es effizient zu emulieren.

Allgemeine Tabellenausdrücke erfordern Postgres 8.4+ .
LATERAL erfordert Postgres 9.3+ .
Die folgenden Lösungen gehen über das hinaus, was im Postgres-Wiki behandelt wird .

1. Keine separate Tabelle mit eindeutigen Benutzern

Mit einem separaten users Tabelle, Lösungen in 2. unten sind in der Regel einfacher und schneller. Überspringen.

1a. Rekursiver CTE mit LATERAL mitmachen

WITH RECURSIVE cte AS (
   (                                -- parentheses required
   SELECT user_id, log_date, payload
   FROM   log
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT l.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT l.user_id, l.log_date, l.payload
      FROM   log l
      WHERE  l.user_id > c.user_id  -- lateral reference
      AND    log_date <= :mydate    -- repeat condition
      ORDER  BY l.user_id, l.log_date DESC NULLS LAST
      LIMIT  1
      ) l
   )
TABLE  cte
ORDER  BY user_id;

Dies ist einfach, um beliebige Spalten abzurufen, und wahrscheinlich am besten in aktuellem Postgres. Weitere Erläuterungen in Kapitel 2a. unten.

1b. Rekursiver CTE mit korrelierter Unterabfrage

WITH RECURSIVE cte AS (
   (                                           -- parentheses required
   SELECT l AS my_row                          -- whole row
   FROM   log l
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT l                            -- whole row
           FROM   log l
           WHERE  l.user_id > (c.my_row).user_id
           AND    l.log_date <= :mydate        -- repeat condition
           ORDER  BY l.user_id, l.log_date DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
   )
SELECT (my_row).*                              -- decompose row
FROM   cte
WHERE  (my_row).user_id IS NOT NULL
ORDER  BY (my_row).user_id;

Bequem zum Abrufen einer einzelnen Spalte oder die ganze Reihe . Das Beispiel verwendet den gesamten Zeilentyp der Tabelle. Andere Varianten sind möglich.

Um zu bestätigen, dass eine Zeile in der vorherigen Iteration gefunden wurde, testen Sie eine einzelne NOT NULL-Spalte (wie den Primärschlüssel).

Weitere Erläuterungen zu dieser Abfrage in Kapitel 2b. unten.

Verwandte:

  • Letzte N zugehörige Zeilen pro Zeile abfragen
  • GRUPPE NACH einer Spalte, während in PostgreSQL nach einer anderen sortiert wird

2. Mit separaten users Tabelle

Das Tabellenlayout spielt kaum eine Rolle, solange genau eine Zeile pro relevanter user_id vorhanden ist ist garantiert. Beispiel:

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

Idealerweise ist die Tabelle physikalisch synchron zum log sortiert Tisch. Siehe:

  • Optimieren Sie den Abfragebereich für Postgres-Zeitstempel

Oder es ist klein genug (niedrige Kardinalität), dass es kaum eine Rolle spielt. Andernfalls kann das Sortieren von Zeilen in der Abfrage helfen, die Leistung weiter zu optimieren. Siehe Gang Liangs Ergänzung. Wenn die physische Sortierreihenfolge der users Tabelle zufällig mit dem Index auf log übereinstimmt , dies ist möglicherweise irrelevant.

2a. LATERAL mitmachen

SELECT u.user_id, l.log_date, l.payload
FROM   users u
CROSS  JOIN LATERAL (
   SELECT l.log_date, l.payload
   FROM   log l
   WHERE  l.user_id = u.user_id         -- lateral reference
   AND    l.log_date <= :mydate
   ORDER  BY l.log_date DESC NULLS LAST
   LIMIT  1
   ) l;

JOIN LATERAL ermöglicht es, vorangestelltes FROM zu referenzieren Elemente auf der gleichen Abfrageebene. Siehe:

  • Was ist der Unterschied zwischen LATERAL JOIN und einer Unterabfrage in PostgreSQL?

Führt zu einer (nur) Indexsuche pro Benutzer.

Gibt keine Zeile für Benutzer zurück, die in users fehlen Tisch. Typischerweise ein Fremdschlüssel Einschränkung, die die referentielle Integrität erzwingt, würde dies ausschließen.

Auch keine Zeile für Benutzer ohne übereinstimmenden Eintrag im log - in Übereinstimmung mit der ursprünglichen Frage. Um diese Benutzer im Ergebnis zu behalten, verwenden Sie LEFT JOIN LATERAL ... ON true statt CROSS JOIN LATERAL :

  • Rufen Sie eine Set-zurückgebende Funktion mehrmals mit einem Array-Argument auf

Verwenden Sie LIMIT n statt LIMIT 1 um mehr als eine Zeile abzurufen (aber nicht alle) pro Benutzer.

Effektiv tun alle das Gleiche:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

Letzteres hat jedoch eine niedrigere Priorität. Explizites JOIN bindet vor Komma. Dieser feine Unterschied kann bei mehr Join-Tischen eine Rolle spielen. Siehe:

  • "ungültiger Verweis auf FROM-Klauseleintrag für Tabelle" in Postgres-Abfrage

2b. Korrelierte Unterabfrage

Gute Wahl, um eine einzelne Spalte abzurufen aus einer einzelnen Zeile . Codebeispiel:

  • Gruppenweise maximale Abfrage optimieren

Dasselbe ist für mehrere Spalten möglich , aber Sie brauchen mehr Intelligenz:

CREATE TEMP TABLE combo (log_date date, payload int);

SELECT user_id, (combo1).*              -- note parentheses
FROM (
   SELECT u.user_id
        , (SELECT (l.log_date, l.payload)::combo
           FROM   log l
           WHERE  l.user_id = u.user_id
           AND    l.log_date <= :mydate
           ORDER  BY l.log_date DESC NULLS LAST
           LIMIT  1) AS combo1
   FROM   users u
   ) sub;

Wie LEFT JOIN LATERAL oben enthält diese Variante alle Benutzer, auch ohne Einträge im log . Sie erhalten NULL für combo1 , die Sie einfach mit einem WHERE filtern können Klausel in der äußeren Abfrage, wenn nötig.
Spitze:in der äußeren Abfrage kann man nicht unterscheiden, ob die Unterabfrage eine Zeile nicht gefunden hat oder alle Spaltenwerte zufällig NULL sind - gleiches Ergebnis. Sie benötigen ein NOT NULL Spalte in der Unterabfrage, um diese Mehrdeutigkeit zu vermeiden.

Eine korrelierte Unterabfrage kann nur einen einzelnen Wert zurückgeben . Sie können mehrere Spalten in einen zusammengesetzten Typ umschließen. Aber um es später zu zerlegen, verlangt Postgres einen bekannten zusammengesetzten Typ. Anonyme Datensätze können nur zerlegt werden, wenn eine Spaltendefinitionsliste bereitgestellt wird.
Verwenden Sie einen registrierten Typ wie den Zeilentyp einer vorhandenen Tabelle. Oder registrieren Sie einen zusammengesetzten Typ explizit (und dauerhaft) mit CREATE TYPE . Oder erstellen Sie eine temporäre Tabelle (die am Ende der Sitzung automatisch gelöscht wird), um ihren Zeilentyp vorübergehend zu registrieren. Umwandlungssyntax:(log_date, payload)::combo

Schließlich wollen wir combo1 nicht zerlegen auf der gleichen Abfrageebene. Aufgrund einer Schwäche im Abfrageplaner würde dies die Unterabfrage einmal für jede Spalte auswerten (immer noch in Postgres 12). Machen Sie stattdessen eine Unterabfrage und zerlegen Sie sie in die äußere Abfrage.

Verwandte:

  • Erhalte Werte aus der ersten und letzten Zeile pro Gruppe

Demonstration aller 4 Abfragen mit 100.000 Protokolleinträgen und 1.000 Benutzern:
db<>fiddle here - Seite 11
Altes sqlfiddle