Berechnen Sie die rollierende Summe über einen Zeitraum von 7 aufeinander folgenden Tagen in PostgreSQL

9

Ich muss die fortlaufende Summe über einen Zeitraum von 7 Tagen für jede Zeile (1 Zeile pro Tag) erhalten.

Zum Beispiel:

| Date       | Count | 7-Day Rolling Sum |
------------------------------------------
| 2016-02-01 | 1     | 1
| 2016-02-02 | 1     | 2
| 2016-02-03 | 2     | 4
| 2016-02-04 | 2     | 6
| 2016-02-05 | 2     | 8
| 2016-02-06 | 2     | 10
| 2016-02-07 | 2     | 12
| 2016-02-08 | 2     | 13 --> here we start summing from 02-02
| 2016-02-09 | 2     | 14 --> here we start summing from 02-03
| 2016-02-10 | 5     | 17 --> here we start summing from 02-04

Ich brauche dies in einer Abfrage, die die Zeilen mit der fortlaufenden 7-Tage-Summe und dem Datum des letzten Tages des Summenbereichs zurückgibt. Beispiel: Tag = 2016-02-10, Summe 17.

Bisher habe ich das, aber es funktioniert nicht vollständig:

DO
$do$
DECLARE 
    curr_date date;
    num bigint;
BEGIN
FOR curr_date IN (SELECT date_trunc('day', d)::date FROM generate_series(CURRENT_DATE-31, CURRENT_DATE-1, '1 day'::interval) d)
LOOP 
    SELECT curr_date, SUM(count)
    FROM generate_series (curr_date-8, curr_date-1, '1 day'::interval) d
    LEFT JOIN m.ping AS p ON p.date = d
    LEFT JOIN m.ping_type AS pt ON pt.id = p.ping_type_id
    LEFT JOIN m.ping_frequency AS pf ON pf.id = p.ping_frequency_id
    WHERE
        pt.url_slug = 'active' AND
        pf.url_slug = 'weekly';
END LOOP;
END
$do$;

Ich verwende PostgreSQL 9.4.5. Es können mehrere Zeilen mit demselben Datum vorhanden sein. Wenn eine Lücke besteht (ein Tag fehlt), wird der Bereich von 7 aufeinander folgenden Tagen weiterhin eingehalten.

josesigna
quelle
Sie können die Summe alle 3 Zeilen einer Tabelle im Stapelüberlauf anzeigen.
Luan Huynh

Antworten:

11

Die mit Abstand sauberste Lösung ist die Verwendung der Fensterfunktion summit rows between:

with days as (
        SELECT date_trunc('day', d)::date as day
        FROM generate_series(CURRENT_DATE-31, CURRENT_DATE-1, '1 day'::interval) d ),
    counts as (
        select 
            days.day,
            sum((random()*5)::integer) num
        FROM days
        -- left join other tables here to get counts, I'm using random
        group by days.day
    )
select
    day,
    num,
    sum(num) over (order by day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW)
from counts
order by day;

Der wichtige Teil besteht darin, den Zeitrahmen in daysCTE zu generieren und sich diesem anzuschließen, um keine Tage zu verpassen, für die keine Daten vorliegen.

Beispiel

Wenn ich beispielsweise in den letzten 14 Tagen einige Testdaten mit 20 Datensätzen erstelle:

SELECT (current_date - ((random()*14)::integer::text || 'days')::interval)::date as day, (random()*7)::integer as num
into test_data from generate_series(1, 20);;

Und fügen Sie vorher einen Wert hinzu:

insert into test_data values ((current_date - '25 days'::interval), 5);

Verwenden Sie dann die obige Abfrage:

with days as (
        SELECT date_trunc('day', d)::date as day
        FROM generate_series(CURRENT_DATE-31, CURRENT_DATE-1, '1 day'::interval) d ),
    counts as (
        select 
            days.day,
            sum(t.num) num
        FROM days
        left join test_data t on t.day = days.day
        group by days.day
    )
select
    day,
    num,
    sum(num) over (order by day rows between 6 preceding and current row)
from counts
order by day;

Und erhalten Sie die Ergebnisse für den ganzen Monat:

    day     | num | sum 
------------+-----+-----
 2016-01-31 |     |    
 2016-02-01 |     |    
 2016-02-02 |     |    
 2016-02-03 |     |    
 2016-02-04 |     |    
 2016-02-05 |     |    
 2016-02-06 |   5 |   5
 2016-02-07 |     |   5
 2016-02-08 |     |   5
 2016-02-09 |     |   5
 2016-02-10 |     |   5
 2016-02-11 |     |   5
 2016-02-12 |     |   5
 2016-02-13 |     |    
 2016-02-14 |     |    
 2016-02-15 |     |    
 2016-02-16 |     |    
 2016-02-17 |     |    
 2016-02-18 |   2 |   2
 2016-02-19 |   5 |   7
 2016-02-20 |     |   7
 2016-02-21 |   4 |  11
 2016-02-22 |  15 |  26
 2016-02-23 |   1 |  27
 2016-02-24 |   1 |  28
 2016-02-25 |   2 |  28
 2016-02-26 |   4 |  27
 2016-02-27 |   9 |  36
 2016-02-28 |   5 |  37
 2016-02-29 |  11 |  33
 2016-03-01 |   5 |  37
(31 rows)
hruske
quelle
Dies ist vollkommen gut, wenn keine Lücken und nur 1 Reihe pro Tag vorhanden sind. Wenn es Lücken oder mehrere Zeilen pro Tag gibt (wie im OP beschrieben) und wir die Summe für die Zeilen möchten, die in den letzten 7 Tagen (von heute bis vor 6 Tagen) fallen, funktioniert dies nicht. Ich bin mir nicht sicher, wonach das OP strebt, auch nach deren Klärung, also bekomme eine +1.
Ypercubeᵀᴹ
Nein, das ist okay. Deshalb muss man mitmachen daysund deshalb gibt es eine Gruppe von in counts.
Hruske
Ah, ich dachte du hättest das nur um die zufälligen Daten zu produzieren. Vielleicht können Sie klarer machen, welcher Teil die Daten und was die Berechnung ist. Scheint richtig, ja.
Ypercubeᵀᴹ
0

Endete mit einer FOR-Schleife, einer TEMP-Tabelle und SELECT in der temporären Tabelle, sobald die for-Schleife fertig ist:

DO
$do$
DECLARE 
    curr_date DATE;
BEGIN

-- Create temp table to hold results
DROP TABLE IF EXISTS rolling_7day_sum;
CREATE TEMP TABLE rolling_7day_sum (
    date DATE,
    count BIGINT
);

-- Iterate dates and get 7 day rolling sum for each
FOR curr_date IN (SELECT date_trunc('day', d)::date FROM generate_series(
    -- Get earliest date from table
    (
        SELECT date FROM m.ping AS p
            LEFT JOIN m.ping_type AS pt ON pt.id = p.ping_type_id
            LEFT JOIN m.ping_frequency AS pf ON pf.id = p.ping_frequency_id
        WHERE
            pt.url_slug = 'active' AND
            pf.url_slug = 'weekly'
        ORDER BY date ASC
        LIMIT 1
    ), CURRENT_DATE-1, '1 day'::interval) d)
LOOP
    INSERT INTO rolling_7day_sum 
        SELECT curr_date, SUM(count)
            FROM generate_series (curr_date-8, curr_date-1, '1 day'::interval) d
                LEFT JOIN m.ping AS p ON p.date = d
                LEFT JOIN m.ping_type AS pt ON pt.id = p.ping_type_id
                LEFT JOIN m.ping_frequency AS pf ON pf.id = p.ping_frequency_id
            WHERE
                pt.url_slug = 'active' AND
                pf.url_slug = 'weekly';
END LOOP;
END
$do$;

SELECT date, count FROM rolling_7day_sum ORDER BY date ASC;

Aber ich stelle mir vor, dass es einen saubereren Weg gibt, eine 7-aufeinanderfolgende Rolling-Summe zu machen als diesen.

josesigna
quelle
Sie könnten möglicherweise eine rekursive Abfrage durchführen, die 7 Ebenen tief geht.
Joishi Bodio
0

Eine rekursive SQL-Abfrage mit einer Tiefe von 7 kann funktionieren, aber ich weiß nicht, wie effizient sie wäre.

WITH RECURSIVE totals(start_day, end_day, total, depth) AS (
    SELECT date, date, count, 1 FROM table
  UNION ALL
    SELECT
      t.start_day,
      t.start_day + INTERVAL '1 day',
      total + COALESCE((SELECT count FROM table WHERE date = t.start_day + INTERVAL '1 day'), 0),
      t.depth + 1
    FROM totals t
) SELECT
  *
FROM totals
WHERE end_day = '2016-03-01' AND depth = 7;

Nicht auf Syntax oder ähnliches getestet.

Joishi Bodio
quelle