PostgreSQL - ruft die Zeile ab, die den Max-Wert für eine Spalte enthält

96

Ich habe es mit einer Postgres-Tabelle ("Lives" genannt) zu tun, die Datensätze mit Spalten für time_stamp, usr_id, transaction_id und living_remaining enthält. Ich benötige eine Abfrage, die mir für jede usr_id die aktuellste Lebenssumme gibt

  1. Es gibt mehrere Benutzer (verschiedene usr_id's)
  2. time_stamp ist keine eindeutige Kennung: Manchmal treten Benutzerereignisse (zeilenweise in der Tabelle) mit demselben time_stamp auf.
  3. trans_id ist nur für sehr kleine Zeitbereiche eindeutig: Im Laufe der Zeit wiederholt es sich
  4. verbleibende Leben (für einen bestimmten Benutzer) können im Laufe der Zeit sowohl zunehmen als auch abnehmen

Beispiel:

Zeitstempel | Leben bleibt | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10:00 | 2 | 3 | 3    
  10:00 | 1 | 2 | 4    
  11:00 | 4 | 1 | 5    
  11:00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

Da ich für jede angegebene usr_id auf andere Spalten der Zeile mit den neuesten Daten zugreifen muss, benötige ich eine Abfrage, die ein Ergebnis wie das folgende liefert:

Zeitstempel | Leben bleibt | usr_id | trans_id
-----------------------------------------
  11:00 | 3 | 1 | 6    
  10:00 | 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

Wie bereits erwähnt, kann jede usr_id Leben gewinnen oder verlieren, und manchmal treten diese Ereignisse mit Zeitstempel so nahe beieinander auf, dass sie denselben Zeitstempel haben! Daher funktioniert diese Abfrage nicht:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

Stattdessen muss ich sowohl time_stamp (erste) als auch trans_id (zweite) verwenden, um die richtige Zeile zu identifizieren. Ich muss diese Informationen dann auch von der Unterabfrage an die Hauptabfrage übergeben, die die Daten für die anderen Spalten der entsprechenden Zeilen bereitstellt. Dies ist die gehackte Abfrage, die ich zur Arbeit gebracht habe:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

Okay, das funktioniert, aber ich mag es nicht. Es erfordert eine Abfrage innerhalb einer Abfrage, einen Self-Join, und es scheint mir, dass es viel einfacher sein könnte, wenn Sie die Zeile abrufen, die MAX mit dem größten Zeitstempel und der größten trans_id gefunden hat. Die Tabelle "lebt" enthält zig Millionen zu analysierende Zeilen. Daher möchte ich, dass diese Abfrage so schnell und effizient wie möglich ist. Ich bin insbesondere bei RDBM und Postgres neu, daher weiß ich, dass ich die richtigen Indizes effektiv nutzen muss. Ich bin ein bisschen verloren, wie man optimiert.

Ich habe hier eine ähnliche Diskussion gefunden . Kann ich eine Art von Postgres ausführen, die einer Oracle-Analysefunktion entspricht?

Alle Ratschläge zum Zugriff auf verwandte Spalteninformationen, die von einer Aggregatfunktion (wie MAX) verwendet werden, zum Erstellen von Indizes und zum Erstellen besserer Abfragen sind sehr willkommen!

PS Sie können Folgendes verwenden, um meinen Beispielfall zu erstellen:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);
Joshua Berry
quelle
Josh, Sie mögen vielleicht nicht die Tatsache, dass sich die Abfrage selbst verbindet usw., aber das ist in Bezug auf das RDBMS in Ordnung.
Vladr
1
Was der Self-Join tatsächlich übersetzt, ist eine einfache Indexzuordnung, bei der das innere SELECT (das mit MAX) den Index scannt, der irrelevante Einträge wegwirft, und bei dem das äußere SELECT nur den Rest der Spalten aus der Tabelle abruft entsprechend dem eingegrenzten Index.
Vladr
Vlad, danke für die Tipps und Erklärungen. Es hat mir die Augen geöffnet, wie ich das Innenleben der Datenbank verstehen und Abfragen optimieren kann. Quassnoi, danke für die tolle Abfrage und den Tipp zum Primärschlüssel; Bill auch. Sehr hilfreich.
Joshua Berry
Danke, dass du mir gezeigt hast, wie man MAX BY2 Spalten bekommt !

Antworten:

90

In einer Tabelle mit 158.000 Pseudozufallszeilen (usr_id gleichmäßig zwischen 0 und 10.000 verteilt) trans_id gleichmäßig zwischen 0 und 30 verteilt),

Unter Abfragekosten beziehe ich mich unten auf die Kostenschätzung des kostenbasierten Optimierers von Postgres (mit den Standardwerten von Postgres xxx_cost), bei der es sich um eine gewichtete Funktionsschätzung der erforderlichen E / A- und CPU-Ressourcen handelt. Sie können dies erreichen, indem Sie PgAdminIII starten und "Query / Explain (F7)" für die Abfrage ausführen, wobei "Query / Explain options" auf "Analyze" gesetzt ist.

  • Quassnoy Abfrage hat eine Kostenschätzung von 745k (!) Und schließt in 1,3 Sekunden (bei einer Verbindung Index auf ( usr_id, trans_id, time_stamp))
  • Bills Abfrage hat eine Kostenschätzung von 93.000 und wird in 2,9 Sekunden abgeschlossen (bei einem zusammengesetzten Index für ( usr_id, trans_id)).
  • Abfrage # 1 unten hat einen Kostenvoranschlag von 16K und schließt in 800 ms (eine Verbindung Index auf (angegeben usr_id, trans_id, time_stamp))
  • Abfrage # 2 unten hat einen Kostenvoranschlag von 14 K, und schließt in 800 ms (eine Verbindung Funktionsindex auf (angegeben usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • Dies ist Postgres-spezifisch
  • Abfrage # 3 unten (Postgres 8.4+) einen Kostenvoranschlag und Ausführungszeit vergleichbar (oder besser als) Abfrage # 2 (angegeben auf eine Verbindung , Index ( usr_id, time_stamp, trans_id)); Es hat den Vorteil, dass die livesTabelle nur einmal gescannt wird. Wenn Sie work_mem vorübergehend erhöhen (falls erforderlich) , um die Sortierung im Speicher zu berücksichtigen , ist es bei weitem die schnellste aller Abfragen.

Alle oben genannten Zeiten beinhalten das Abrufen der vollständigen Ergebnismenge von 10.000 Zeilen.

Ihr Ziel ist eine minimale Kostenschätzung und eine minimale Ausführungszeit für Abfragen, wobei der Schwerpunkt auf den geschätzten Kosten liegt. Die Ausführung von Abfragen kann erheblich von den Laufzeitbedingungen abhängen (z. B. ob relevante Zeilen bereits vollständig im Speicher zwischengespeichert sind oder nicht), während dies bei der Kostenschätzung nicht der Fall ist. Denken Sie andererseits daran, dass die Kostenschätzung genau das ist, eine Schätzung.

Die beste Ausführungszeit für Abfragen wird erzielt, wenn eine dedizierte Datenbank ohne Last ausgeführt wird (z. B. Spielen mit pgAdminIII auf einem Entwicklungs-PC). Die Abfragezeit variiert in der Produktion basierend auf der tatsächlichen Maschinenlast / Datenzugriffsverteilung. Wenn eine Abfrage etwas schneller (<20%) als die andere erscheint, aber viel höhere Kosten verursacht, ist es im Allgemeinen klüger, die mit höherer Ausführungszeit und geringeren Kosten auszuwählen.

Wenn Sie erwarten, dass zum Zeitpunkt der Ausführung der Abfrage keine Konkurrenz um den Speicher auf Ihrem Produktionscomputer besteht (z. B. werden der RDBMS-Cache und der Dateisystem-Cache nicht durch gleichzeitige Abfragen und / oder Dateisystemaktivitäten überlastet), dann die Abfragezeit, die Sie erhalten haben im Standalone-Modus (z. B. pgAdminIII auf einem Entwicklungs-PC) ist repräsentativ. Wenn das Produktionssystem in Konflikt gerät, verschlechtert sich die Abfragezeit proportional zum geschätzten Kostenverhältnis, da die Abfrage mit den niedrigeren Kosten nicht so stark vom Cache abhängt, während die Abfrage mit den höheren Kosten dieselben Daten immer wieder überprüft (Auslösen) zusätzliche E / A in Abwesenheit eines stabilen Caches), z.

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

Vergessen Sie nicht, ANALYZE livesnach dem Erstellen der erforderlichen Indizes einmal auszuführen .


Abfrage Nr. 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

Abfrage Nr. 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

Update 2013/01/29

Ab Version 8.4 unterstützt Postgres die Fensterfunktion , sodass Sie etwas schreiben können, das so einfach und effizient ist wie:

Abfrage Nr. 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );
vladr
quelle
Meinen Sie mit einem zusammengesetzten Index für (usr_id, trans_id, times_tamp) so etwas wie "CREATE INDEX living_blah_idx ON lebt (usr_id, trans_id, time_stamp)"? Oder sollte ich drei separate Indizes für jede Spalte erstellen? Ich sollte mich an die Standardeinstellung "USING btree" halten, oder?
Joshua Berry
1
Ja zur ersten Wahl: Ich meine CREATE INDEX living_blah_idx ON lebt (usr_id, trans_id, time_stamp). :) Prost.
Vladr
Vielen Dank für den Kostenvergleich vladr! Sehr vollständige Antwort!
Adam
@vladr Ich bin gerade auf deine Antwort gestoßen. Ich bin etwas verwirrt, wie Sie sagen, Abfrage 1 kostet 16.000 und Abfrage 2 14.000. Aber weiter unten in der Tabelle sagen Sie, dass Abfrage 1 Kosten von 5.000 und Abfrage 2 Kosten von 50.000 verursacht. Welche Abfrage wird bevorzugt verwendet? :) danke
Houman
1
@Kave, die Tabelle enthält ein hypothetisches Abfragepaar, um ein Beispiel zu veranschaulichen, nicht die beiden Abfragen des OP. Umbenennen, um Verwirrung zu vermeiden.
Vladr
77

Ich würde eine saubere Version vorschlagen, die auf DISTINCT ON(siehe Dokumente ) basiert :

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;
Marco
quelle
6
Dies ist eine sehr kurze und fundierte Antwort. Hat auch eine gute Referenz! Dies sollte die akzeptierte Antwort sein.
Prakhar Agrawal
Dies schien für mich bei meiner etwas anderen Anwendung zu funktionieren, wo sonst nichts wäre. Sollte auf jeden Fall für mehr Sichtbarkeit angehoben werden.
Jim Factor
8

Hier ist eine andere Methode, die zufällig keine korrelierten Unterabfragen oder GROUP BY verwendet. Ich bin kein Experte für PostgreSQL-Leistungsoptimierung, daher schlage ich vor, dass Sie sowohl diese als auch die von anderen Leuten angegebenen Lösungen ausprobieren, um herauszufinden, welche für Sie besser funktioniert.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

Ich gehe davon aus, dass dies trans_idzumindest über einen bestimmten Wert von eindeutig ist time_stamp.

Bill Karwin
quelle
4

Ich mag den Stil von Mike Woodhouses Antwort auf der anderen Seite, die Sie erwähnt haben. Es ist besonders prägnant , wenn die Sache immer maximiert wird , ist nur eine einzige Spalte, wobei in diesem Fall die Unterabfrage nur verwenden kann , MAX(some_col)und GROUP BYdie anderen Spalten, aber in Ihrem Fall haben Sie eine 2-Teilmenge maximiert werden, können Sie immer noch so tun , indem Sie ORDER BYPlus LIMIT 1stattdessen (wie von Quassnoi gemacht):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

Ich finde die Verwendung der WHERE (a, b, c) IN (subquery)Zeilenkonstruktorsyntax hilfreich, da dadurch weniger Wortschatz benötigt wird.

j_random_hacker
quelle
3

Actaully gibt es eine hackige Lösung für dieses Problem. Angenommen, Sie möchten den größten Baum jedes Waldes in einer Region auswählen.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

Wenn Sie Bäume nach Wäldern gruppieren, wird eine unsortierte Liste von Bäumen angezeigt, und Sie müssen den größten finden. Als erstes sollten Sie die Zeilen nach ihrer Größe sortieren und die erste Ihrer Liste auswählen. Es mag ineffizient erscheinen, aber wenn Sie Millionen von Zeilen haben, ist es ziemlich schneller als die Lösungen, die JOINs und WHEREBedingungen enthalten.

Übrigens, beachten Sie, dass ORDER_BYfor array_aggin Postgresql 9.0 eingeführt wird

Burak Emre
quelle
Sie haben einen Fehler. Sie müssen ORDER BY tree_size.size DESC schreiben. Für die Aufgabe des Autors wird der Code auch so aussehen: SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky
2

In Postgressql 9.5 gibt es eine neue Option namens DISTINCT ON

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Es eliminiert doppelte Zeilen und lässt nur die erste Zeile übrig, wie in der ORDER BY-Klausel definiert.

siehe die offizielle Dokumentation

Eden
quelle
1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

Durch das Erstellen eines Index für (usr_id, time_stamp, trans_id)wird diese Abfrage erheblich verbessert.

Sie sollten immer, immer eine Art PRIMARY KEYin Ihren Tabellen haben.

Quassnoi
quelle
0

Ich denke, Sie haben hier ein großes Problem: Es gibt keinen monoton ansteigenden "Zähler", der garantiert, dass eine bestimmte Zeile später als eine andere passiert ist. Nehmen Sie dieses Beispiel:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

Aus diesen Daten können Sie nicht ermitteln, welcher Eintrag der letzte ist. Ist es der zweite oder der letzte? Es gibt keine sort- oder max () -Funktion, die Sie auf diese Daten anwenden können, um die richtige Antwort zu erhalten.

Das Erhöhen der Auflösung des Zeitstempels wäre eine große Hilfe. Da das Datenbankmodul Anforderungen serialisiert, können Sie bei ausreichender Auflösung sicherstellen, dass keine zwei Zeitstempel gleich sind.

Alternativ können Sie eine trans_id verwenden, die sich sehr, sehr lange nicht verlängert. Wenn Sie eine trans_id haben, die überrollt, können Sie (für denselben Zeitstempel) nicht feststellen, ob trans_id 6 aktueller als trans_id 1 ist, es sei denn, Sie führen komplizierte Berechnungen durch.

Barry Brown
quelle
Ja, idealerweise wäre eine Sequenzspalte (Autoincrement) in Ordnung.
Vladr
Die Annahme von oben war, dass trans_id für kleine Zeitschritte nicht überrollen würde. Ich bin damit einverstanden, dass die Tabelle einen eindeutigen Primärindex benötigt - wie eine sich nicht wiederholende trans_id. (PS Ich bin froh, dass ich jetzt genug Karma / Reputationspunkte habe, um zu kommentieren!)
Joshua Berry
Vlad gibt an, dass trans_id einen ziemlich kurzen Zyklus hat, der häufig umschlägt. Selbst wenn Sie nur die beiden mittleren Zeilen aus meiner Tabelle berücksichtigen (trans_id = 6 und 1), können Sie immer noch nicht sagen, welche die aktuellste ist. Daher funktioniert die Verwendung von max (trans_id) für einen bestimmten Zeitstempel nicht.
Barry Brown
Ja, ich verlasse mich auf die Garantie des Anwendungsautors, dass das Tupel (time_stamp, trans_id) für einen bestimmten Benutzer eindeutig ist. Wenn dies nicht der Fall ist, muss "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ..." zu "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM werden. .. WO ... GRUPPE VON l1.usr_id, ...
vladr
0

Eine andere Lösung, die Sie vielleicht nützlich finden.

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
Turbcool
quelle