PostgreSQL: Generieren Sie eine Reihe von Daten für jede Gruppe in einer Tabelle

7

Ich habe eine balancesTabelle in PostgreSQL 9.3, die so aussieht:

CREATE TABLE balances (
  user_id INT
, balance INT
, as_of_date DATE
);

INSERT INTO balances (user_id, balance, as_of_date) VALUES
  (1, 100, '2016-01-03')
, (1,  50, '2016-01-02')
, (1,  10, '2016-01-01')
, (2, 200, '2016-01-01')
, (3,  30, '2016-01-03');

Es enthält nur Salden für Daten, an denen ein Benutzer eine Transaktion durchgeführt hat. Ich brauche es, um eine Zeile für jeden Benutzer mit ihrem Kontostand an jedem Datum in einem bestimmten Datumsbereich zu enthalten.

  • Wenn der Benutzer für ein bestimmtes Datum keine Zeile im Bereich hat, muss ich sein Guthaben vom Vortag verwenden.
  • Wenn der Benutzer sein Konto nach einem bestimmten Datum im Bereich erstellt hat, muss vermieden werden, dass eine Zeile für diese Benutzer- / Datumskombination erstellt wird.

Ich kann auf eine accountsTabelle verweisen , um Benutzer zu erhalten create_date:

CREATE TABLE accounts (
  user_id INT
, create_date DATE
);

INSERT INTO accounts (user_id, create_date) VALUES
  (1, '2015-12-01')
, (2, '2015-12-31')
, (3, '2016-01-03');

Mein gewünschtes Ergebnis sieht so aus:

+---------+---------+--------------------------+
| user_id | balance |        as_of_date        |
+---------+---------+--------------------------+
|       1 |     100 | 2016-01-03T00:00:00.000Z |
|       1 |      50 | 2016-01-02T00:00:00.000Z |
|       1 |      10 | 2016-01-01T00:00:00.000Z |
|       2 |     200 | 2016-01-03T00:00:00.000Z |
|       2 |     200 | 2016-01-02T00:00:00.000Z |
|       2 |     200 | 2016-01-01T00:00:00.000Z |
|       3 |      30 | 2016-01-03T00:00:00.000Z |
+---------+---------+--------------------------+

Beachten Sie, dass für Benutzer 2 Zeilen für 2016-01-02und hinzugefügt wurden 2016-01-03, die den vorherigen Saldo von übertragen 2016-01-01. und dass für Benutzer 3, der am erstellt wurde, keine Zeilen hinzugefügt wurden 2016-01-03.

Um eine Reihe von Daten in einem Datumsbereich zu generieren, kann ich Folgendes verwenden:

SELECT d.date FROM GENERATE_SERIES('2016-01-01', '2016-01-03', '1 day'::INTERVAL) d

... aber ich habe LEFT JOINProbleme damit, diese Serie mit jedem Satz von Zeilen zu gruppieren, die nach gruppiert sind user_id.

Shaun Scovil
quelle
Was ist mit Benutzern, deren erste Transaktion später als ihre erfolgt created_at? Listen Sie sie mit Saldo 0 für diese ersten Tage auf? Oder mit NULL? Oder erst bei der ersten Transaktion auflisten? Oder nicht möglich?
Erwin Brandstetter
Sie haben Recht, in diesem Fall wäre ein Saldo von Null angemessen.
Shaun Scovil

Antworten:

5

1. CROSS JOIN, LEFT JOIN LATERALzu subquery

SELECT a.user_id, COALESCE(b.balance, 0) AS balance, d.as_of_date
FROM   (
   SELECT d::date AS as_of_date  -- cast to date right away
   FROM   generate_series(timestamp '2016-01-01', '2016-01-03', interval '1 day') d
   ) d
JOIN   accounts a ON a.create_date <= d.as_of_date
LEFT   JOIN LATERAL (
   SELECT balance
   FROM   balances
   WHERE  user_id = a.user_id
   AND    as_of_date <= d.as_of_date
   ORDER  BY as_of_date DESC
   LIMIT  1
   ) b ON true
ORDER  BY a.user_id, d.as_of_date;

Gibt das gewünschte Ergebnis zurück - außer dass dies as_of_dateein tatsächliches dateund kein timestampähnliches Ergebnis in Ihrem Beispiel ist. Das sollte angemessener sein.

Benutzer, die bereits erstellt wurden, aber noch keine Transaktionen haben, werden mit einem Saldo von 0 aufgelistet. Sie haben nicht definiert, wie mit dem Eckfall umgegangen werden soll.

Verwenden Sie die timestampEingabe lieber für generate_series():

Für die Leistung ist es entscheidend, dass Sie dies mit einem mehrspaltigen Index sichern:

CREATE INDEX balances_multi_idx ON balances (user_id, as_of_date DESC, balance);

Wir hatten gerade diese Woche einen sehr ähnlichen Fall bei SO:

Weitere Erklärungen finden Sie dort.

2. CROSS JOIN, LEFT JOIN, Fensterfunktionen

SELECT user_id
     , COALESCE(max(balance) OVER (PARTITION BY user_id, grp
                                   ORDER BY as_of_date), 0) AS balance
     , as_of_date
FROM  (
   SELECT a.user_id, b.balance, d.as_of_date
        , count(b.user_id) OVER (PARTITION BY user_id ORDER BY as_of_date) AS grp
   FROM   (
      SELECT d::date AS as_of_date  -- cast to date right away
      FROM   generate_series(timestamp '2016-01-01', '2016-01-03', interval '1 day') d
      ) d
   JOIN   accounts a ON a.create_date <= d.as_of_date
   LEFT   JOIN balances b USING (user_id, as_of_date)
   ) sub
ORDER  BY user_id, as_of_date;

Gleiches Ergebnis. Wenn Sie den oben genannten mehrspaltigen Index haben und nur Index-Scans daraus erhalten können, ist die erste Lösung höchstwahrscheinlich schneller.

Das Hauptmerkmal ist die laufende Anzahl von Werten, um Gruppen zu bilden. Da count () keine NULL-Werte zählt, fallen alle Daten ohne Kontostand in dieselbe Gruppe ( grp) wie der letzte Kontostand. Verwenden Sie dann einen einfachen, max()über denselben Fensterrahmen verlängerten Rahmen, um grpdie letzte Waage für baumelnde Lücken zu kopieren.

Verbunden:

Erwin Brandstetter
quelle
Großartig, danke. Werde es am Montag testen.
Shaun Scovil
1

Wenn das Gleichgewicht monoton ist, erhöht sich etwas wie:

SELECT b.user_id, max(b.balance) as balance, d.as_of_date 
FROM GENERATE_SERIES('2016-01-01', '2016-01-03', '1 day'::INTERVAL) d (as_of_date)
LEFT JOIN balances b
    on b.as_of_date <= d.as_of_date
GROUP BY b.user_id, d.as_of_date    
ORDER BY b.user_id, d.as_of_date desc

sollte tun. Das Problem ist für den allgemeinen Fall wahrscheinlich etwas einfacher, wenn Sie anstelle der Salden pro Datum Zugriff auf die einzelnen Transaktionen haben.

Lennart
quelle
Ich mache tatsächlich mehrere andere Schritte, um in meinem Beispiel an den Tisch zu gelangen, der den Tagesendstand für jedes Konto an jedem Datum darstellt, an dem eine Transaktion stattgefunden hat. Ich kann mich nicht darauf verlassen, dass das Gleichgewicht mit der Zeit zunimmt oder so etwas. Denken Sie an Bankguthaben für Tausende von Benutzern.
Shaun Scovil