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