Optimierung der GROUP BY-Abfrage zum abrufen der aktuellen Zeile pro Benutzer
Ich habe die folgenden log-Tabelle für Benutzer-Nachrichten (vereinfachte form) in Postgres 9.2:
CREATE TABLE log (
log_date DATE,
user_id INTEGER,
payload INTEGER
);
Beinhaltet es einen Datensatz pro Benutzer und pro Tag. Es werden etwa 500K Datensätze pro Tag für 300 Tage. Nutzlast immer größer wird für jeden Benutzer (wenn das von Bedeutung ist).
Möchte ich effizient abrufen, den neuesten Datensatz für jeden Benutzer vor einem bestimmten Datum. Meine Abfrage ist:
SELECT user_id, max(log_date), max(payload)
FROM log
WHERE log_date <= :mydate
GROUP BY user_id
was extrem langsam ist. Ich habe auch versucht:
SELECT DISTINCT ON(user_id), log_date, payload
FROM log
WHERE log_date <= :mydate
ORDER BY user_id, log_date DESC;
die hat den gleichen plan-und ist genauso langsam.
Bisher habe ich einen einzigen index auf log(log_date)
, aber hilft nicht viel.
Und ich habe eine users
Tabelle mit allen Benutzern enthalten. Ich möchte auch, um das Ergebnis abzurufen, die für einige Nutzer (die mit payload > :value
).
Gibt es einen anderen index sollte ich verwenden, um diese Fahrt, oder auf irgendeine andere Art zu erreichen, was ich will?
- Versuchen Sie, einen index auf
(user_id, aggr_date)
oder eine weitere aufuser_id
allein. Auch für alle performance-relevanten Fragen Lesen Sie bitte dies: wiki.postgresql.org/wiki/Slow_Query_Questions
Du musst angemeldet sein, um einen Kommentar abzugeben.
Für beste Leseleistung benötigen Sie eine mehrspaltigen index:
Machen index-scans möglich, hinzufügen, die sonst nicht notwendig Spalte
payload
:Warum
DESC NULLS LAST
?Für paar Zeilen pro
user_id
oder kleine TischeDISTINCT ON
ist in der Regel am schnellsten und einfachsten:Für viele Zeilen pro
user_id
eine index skip scan (oder lose index-scan) ist (viel) mehr effizient. Das ist nicht implementiert, die bis zu Postgres-12 - die Arbeit ist im Gange für Postgres 13. Aber es gibt Möglichkeiten, ihn nachzuahmen effizient.Common Table Expressions erfordern Postgres 8.4+.
LATERAL
erfordert Postgres 9.3+.Die folgenden Lösungen, die über das hinausgehen, was in der Postgres-Wiki.
1. Keine separate Tabelle mit eindeutigen Benutzer
Mit einem separaten
users
Tabelle, Lösungen in 2. unten sind in der Regel einfacher und schneller. Fahren Sie Fort.1a. Rekursive CTE mit
LATERAL
joinDies ist ganz einfach abrufen beliebiger Spalten und wahrscheinlich am besten in der aktuellen Postgres. Mehr Erklärung in Kapitel 2a. unten.
1b. Rekursive CTE mit korrelierte Unterabfrage
Bequem abrufen einer einzelne Spalte oder die ganze Reihe. Im Beispiel wird der gesamte Zeilentyp der Tabelle. Andere Varianten sind möglich.
Geltend zu machen, eine Zeile wurde gefunden, die in der vorherigen iteration, testen einer einzelnen NICHT-NULL-Spalte (wie primary key).
Weitere Erklärung für diese Abfrage in Kapitel 2b. unten.
Verwandte:
2. Mit separaten
users
TabelleTabelle layout fast nicht wichtig, wie lange, wie genau eine Zeile pro einschlägigen
user_id
gewährleistet ist. Beispiel:Idealerweise wird die Tabelle physisch sortiert in sync mit der
log
Tabelle. Siehe:Oder es ist klein genug (niedrige Kardinalität), dass es kaum zählt. Sonst, Sortieren von Zeilen in der Abfrage weiter helfen zu können Optimierung der Leistung. Siehe Gang Liang hinaus. Wenn die körperliche Sortierreihenfolge der
users
Tabelle geschieht entsprechend der index auflog
, kann dies unerheblich sein.2a.
LATERAL
joinJOIN SEITLICHEN
erlaubt Referenz vorhergehendenFROM
Elemente auf das gleiche query-Ebene. Siehe:Ergebnisse in einem index (-nur) look-up pro Benutzer.
Gibt keine Zeile für den Benutzer fehlt in der
users
Tabelle. In der Regel, eine foreign key Einschränkung, die referenzielle Integrität durchsetzen würde, ausschließen.Auch keine Zeile für Benutzer ohne entsprechenden Eintrag in
log
- entsprechend der ursprünglichen Frage. Um diese Benutzer in das Ergebnis verwendenLEFT JOIN LATERAL ... ON true
stattCROSS JOIN LATERAL
:Verwenden
LIMIT n
stattLIMIT 1
abrufen mehr als einer Zeilen (aber nicht alle) pro Benutzer.Effektiv, alle diese das gleiche tun:
Den letzten eine geringere Priorität haben, wenn. Explizite
JOIN
bindet vor dem Komma. Der feine Unterschied kann Fragen mit mehr Tabellen verbinden. Siehe:2b. Korrelierte Unterabfrage
Gute Wahl zum abrufen einer einzelne Spalte von einem einzelne Zeile. Code-Beispiel:
Das gleiche ist möglich für mehrere Spalten, aber Sie brauchen mehr smarts:
Wie
LEFT JOIN LATERAL
oben, diese Variante umfasst alle Benutzer, auch ohne Einträge inlog
. Sie erhaltenNULL
fürcombo1
, die können Sie ganz einfach filter mit einemWHERE
- Klausel in der äußeren Abfrage, wenn es sein muss.- Fehler: in der äußeren Abfrage, Sie können nicht unterscheiden, ob die Unterabfrage nicht eine Zeile oder Spalte alle Werte zufällig von NULL - gleiche Ergebnis. Sie brauchen eine
NOT NULL
Spalte in der Unterabfrage zu vermeiden, diese Zweideutigkeit.Einer korrelierten Unterabfrage kann nur zurückgeben ein einzelnen Wert. Sie können wickeln Sie mehrere Spalten in einem zusammengesetzten Typ. Aber zerlegen Sie es später, Postgres-Forderungen eines bekannten zusammengesetzten Typ. Anonyme Einträge kann nur zerlegt werden, die Bereitstellung einer spaltendefinitionsliste.
Verwenden Sie einen registrierten Typ wie der Zeilentyp einer vorhandenen Tabelle. Oder registrieren Sie einen composite-Typ explizit (und dauerhaft) mit
CREATE TYPE
. Oder erstellen Sie eine temporäre Tabelle (fiel automatisch am Ende der Sitzung) zu registrieren, deren Zeilen-Art vorübergehend. Cast-syntax:(log_date, payload)::combo
Schließlich wollen wir nicht zerlegen
combo1
auf das gleiche query-Ebene. Durch eine Schwachstelle in der query-Planer würde dies bewerten die Unterabfrage einmal für jede Spalte (wahr noch in Postgres 12). Stattdessen machen Sie es mit einer Unterabfrage und zersetzen sich in der äußeren Abfrage.Verwandte:
Demonstrieren, alle 4 Abfragen mit 100k log-Einträge und 1k-Benutzer:
db<>fiddle hier - pg 11
Alte sqlfiddle - pg 9.6
Dies ist nicht eine standalone-Antwort, sondern ein Kommentar von @Erwin ist Antwort. Für 2a, die seitlichen Beispiel mit join, die Abfrage kann verbessert werden, indem die Sortierung der
users
Tabelle nutzen, um die Lokalität der index auflog
.Die Begründung ist, dass die index-lookup ist teuer, wenn
user_id
Werte sind zufällig. Durch aussortierenuser_id
ersten, die anschließende seitliche beitreten würde, wie ein einfacher scan, auf den index derlog
. Auch wenn beide Abfrage-Pläne so Aussehen, wie, die Laufzeit würde sich viel vor allem für große Tabellen.Die Kosten für die Sortierung ist minimal, vor allem, wenn ein index auf der
user_id
Feld.Vielleicht einen anderen index für die Tabelle helfen würde. Try this one:
log(user_id, log_date)
. Ich bin nicht positiv, dass Postgres wird optimal auf die Verwendung mitdistinct on
.So, ich würde mit dem stick-index und versuchen Sie diese version:
Diese ersetzen sollte die Sortierung/Gruppierung mit index look-ups. Es könnte schneller sein.