Summe / Anzahl / Durchschnitt über Datumsintervall


20

In einer Datenbank mit Transaktionen, die über einen Zeitraum von 18 Monaten Tausende von Entitäten umfasst, möchte ich eine Abfrage ausführen, um jeden möglichen 30-Tage-Zeitraum entity_idmit einer Summe ihrer Transaktionsbeträge und COUNT ihrer Transaktionen in diesem 30-Tage-Zeitraum zu gruppieren Geben Sie die Daten so zurück, dass ich sie dann abfragen kann. Nach vielen Tests erreicht dieser Code viel von dem, was ich will:

SELECT id, trans_ref_no, amount, trans_date, entity_id,
    SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
    COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
  FROM transactiondb;

Und ich werde in einer größeren Abfrage so etwas strukturiert verwenden:

SELECT * FROM (
  SELECT id, trans_ref_no, amount, trans_date, entity_id,
      SUM(amount) OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_total,
      COUNT(id)   OVER(PARTITION BY entity_id, date_trunc('month',trans_date) ORDER BY entity_id, trans_date ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS trans_count
    FROM transactiondb ) q
WHERE trans_count >= 4
AND trans_total >= 50000;

Der Fall, den diese Abfrage nicht abdeckt, liegt vor, wenn die Transaktionszählung mehrere Monate umfassen würde, sich aber immer noch innerhalb von 30 Tagen voneinander befinden würde. Ist diese Art der Abfrage mit Postgres möglich? Wenn ja, begrüße ich jede Eingabe. Viele der anderen Themen behandeln das " Laufen " von Aggregaten, nicht das Rollen .

Aktualisieren

Das CREATE TABLEDrehbuch:

CREATE TABLE transactiondb (
    id integer NOT NULL,
    trans_ref_no character varying(255),
    amount numeric(18,2),
    trans_date date,
    entity_id integer
);

Beispieldaten finden Sie hier . Ich verwende PostgreSQL 9.1.16.

Ideal Ausgang würde SUM(amount)und COUNT()alle Transaktionen über einen zusammenhängenden Zeitraum von 30 Tagen. Siehe dieses Bild zum Beispiel:

Beispiel für Zeilen, die idealerweise in einer "Menge" enthalten wären, aber nicht, weil meine Menge monatlich statisch ist.

Die grüne Datumsmarkierung zeigt an, was in meiner Abfrage enthalten ist. Die gelbe hervorgehobene Zeile gibt Aufzeichnungen darüber an, was ich Teil des Sets werden möchte.

Vorherige Lektüre:


1
Durch every possible 30-day period by entity_idSie kann bedeuten , beginnt die Frist für jeden Tag, also 365 mögliche Perioden in einem (nicht-Sprung) Jahr? Oder möchten Sie Tage mit einer tatsächlichen Transaktion nur einzeln als Beginn einer Periode betrachten entity_id ? In beiden Fällen geben Sie bitte Ihre Tabellendefinition, die Postgres-Version, einige Beispieldaten und das erwartete Ergebnis für das Beispiel an.
Erwin Brandstetter

In der Theorie habe ich jeden Tag gemeint, aber in der Praxis müssen keine Tage berücksichtigt werden, an denen keine Transaktionen stattfinden. Ich habe die Beispieldaten und die Tabellendefinition veröffentlicht.
Tufelkinder

Sie möchten also ab jeder tatsächlichen Transaktion entity_idin einem 30-Tage-Fenster Zeilen desselben akkumulieren . Kann es mehrere Transaktionen für dieselbe geben oder ist diese Kombination eindeutig definiert? Ihre Tabellendefinition hat keine oder keine PK-Einschränkung, aber Einschränkungen scheinen zu fehlen ...(trans_date, entity_id)UNIQUE
Erwin Brandstetter

Die einzige Einschränkung betrifft den idPrimärschlüssel. Pro Unternehmen und Tag können mehrere Transaktionen durchgeführt werden.
Tufelkinder

Informationen zur Datenverteilung: Gibt es Einträge (pro entity_id) für die meisten Tage?
Erwin Brandstetter

Antworten:


26

Die Frage, die Sie haben

Sie könnten Ihre Abfrage mit einer WINDOWKlausel vereinfachen , dies verkürzt jedoch nur die Syntax, ohne den Abfrageplan zu ändern.

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date)
             ORDER BY trans_date
             ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING);
  • Auch mit der etwas schnelleren count(*), da idist sicher definiert NOT NULL?
  • Und das musst ORDER BY entity_iddu schonPARTITION BY entity_id

Sie können jedoch noch weiter vereinfachen:
Fügen Sie ORDER BYder Fensterdefinition überhaupt nichts hinzu , da dies für Ihre Abfrage nicht relevant ist. Dann müssen Sie auch keinen benutzerdefinierten Fensterrahmen definieren:

SELECT id, trans_ref_no, amount, trans_date, entity_id
     , SUM(amount) OVER w AS trans_total
     , COUNT(*)    OVER w AS trans_count
FROM   transactiondb
WINDOW w AS (PARTITION BY entity_id, date_trunc('month',trans_date);

Einfacher, schneller, aber immer noch eine bessere Version von dem, was Sie haben , mit statischen Monaten.

Die Abfrage, die Sie möchten

... ist nicht klar definiert, daher werde ich auf diesen Annahmen aufbauen:

Zählen Sie Transaktionen und Beträge für jeden 30-Tage-Zeitraum innerhalb der ersten und letzten Transaktion entity_id. Schließen Sie führende und nachfolgende Perioden ohne Aktivität aus, schließen Sie jedoch alle möglichen 30-Tage-Perioden innerhalb dieser äußeren Grenzen ein.

SELECT entity_id, trans_date
     , COALESCE(sum(daily_amount) OVER w, 0) AS trans_total
     , COALESCE(sum(daily_count)  OVER w, 0) AS trans_count
FROM  (
   SELECT entity_id
        , generate_series (min(trans_date)::timestamp
                         , GREATEST(min(trans_date), max(trans_date) - 29)::timestamp
                         , interval '1 day')::date AS trans_date
   FROM   transactiondb 
   GROUP  BY 1
   ) x
LEFT JOIN (
   SELECT entity_id, trans_date
        , sum(amount) AS daily_amount, count(*) AS daily_count
   FROM   transactiondb
   GROUP  BY 1, 2
   ) t USING (entity_id, trans_date)
WINDOW w AS (PARTITION BY entity_id ORDER BY trans_date
             ROWS BETWEEN CURRENT ROW AND 29 FOLLOWING);

In dieser Liste sind alle 30-Tage-Zeiträume entity_idmit Ihren Aggregaten und trans_dateals erster Tag (einschließlich) des Zeitraums aufgeführt. Um Werte für jede einzelne Zeile zu erhalten, verbinden Sie die Basistabelle erneut ...

Die grundlegende Schwierigkeit ist die gleiche wie hier beschrieben:

Die Frame-Definition eines Fensters kann nicht von den Werten der aktuellen Zeile abhängen.

Und lieber generate_series()mit timestampEingabe aufrufen :

Die Abfrage, die Sie tatsächlich möchten

Nach Aktualisierung und Diskussion der Fragen:
Sammeln Sie Zeilen derselben entity_idin einem 30-Tage-Fenster, beginnend mit jeder tatsächlichen Transaktion.

Da Ihre Daten nur spärlich verteilt sind, sollte es effizienter sein, einen Self-Join mit einer Bereichsbedingung auszuführen , zumal Postgres 9.1 noch keine LATERALJoins hat:

SELECT t0.id, t0.amount, t0.trans_date, t0.entity_id
     , sum(t1.amount) AS trans_total, count(*) AS trans_count
FROM   transactiondb t0
JOIN   transactiondb t1 USING (entity_id)
WHERE  t1.trans_date >= t0.trans_date
AND    t1.trans_date <  t0.trans_date + 30  -- exclude upper bound
-- AND    t0.entity_id = 114284  -- or pick a single entity ...
GROUP  BY t0.id  -- is PK!
ORDER  BY t0.trans_date, t0.id

SQL-Geige.

Ein rollendes Fenster kann nur für die meisten Tage (in Bezug auf die Leistung) mit Daten sinnvoll sein.

Dies gilt nicht Aggregat Duplikate auf (trans_date, entity_id)pro Tag, aber alle Zeilen des gleichen Tages werden immer in den 30-Tage - Fenstern enthalten.

Für einen großen Tisch könnte ein Abdeckungsindex wie dieser einiges helfen:

CREATE INDEX transactiondb_foo_idx
ON transactiondb (entity_id, trans_date, amount);

Die letzte Spalte amountist nur nützlich, wenn Sie Index-Scans erhalten. Sonst lass es fallen.

Es wird jedoch nicht verwendet, wenn Sie die gesamte Tabelle auswählen. Es würde Abfragen für eine kleine Teilmenge unterstützen.


Das sieht wirklich gut aus, testet es jetzt auf die Daten und versucht zu verstehen, was Ihre Abfrage tatsächlich tut ...
Tufelkinder

@tufelkinder: Es wurde eine Lösung für die aktualisierte Frage hinzugefügt.
Erwin Brandstetter

Überprüfen Sie es jetzt. Ich bin fasziniert, dass es in SQL Fiddle ausgeführt wird ... Wenn ich versuche, es direkt auf meiner Transaktions- column "t0.amount" must appear in the GROUP BY clause...
DB auszuführen

@tufelkinder: Ich habe den Testfall auf 100 Zeilen reduziert. sqlfiddle begrenzt die Größe der Testdaten. Jake (der Autor) hat das Limit vor ein paar Monaten gesenkt, damit die Website nicht so schnell zum Stillstand kommt.
Erwin Brandstetter

1
Entschuldigen Sie die Verzögerung, die zum Testen in der vollständigen Datenbank erforderlich ist. Ihre Antwort war wie immer hervorragend ausführlich und lehrreich. Vielen Dank!
Tufelkinder
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.