web-dev-qa-db-fra.com

Calculer la somme mobile sur une période de 7 jours consécutifs sur PostgreSQL

J'ai besoin d'obtenir la somme mobile sur une période de 7 jours pour chaque ligne (1 ligne par jour).

Par exemple:

| 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

J'ai besoin de cela dans une requête qui renvoie les lignes avec une somme mobile de 7 jours et la date du dernier jour de la plage de la somme. Par exemple, jour = 2016-02-10, somme 17.

Jusqu'à présent, je l'ai, mais cela ne fonctionne pas pleinement:

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$;

J'utilise PostgreSQL 9.4.5. Il peut y avoir plusieurs lignes avec la même date. S'il y a un écart (un jour manque), la plage de 7 jours consécutifs sera toujours suivie.

8
josesigna

La solution la plus propre est de loin d'utiliser la fonction de fenêtre sum avec 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;

L'important est de générer le calendrier en days CTE et de s'y associer afin de ne manquer aucun jour pour lequel il n'y a pas de données.

Exemple

Par exemple, si je crée des données de test avec 20 enregistrements au cours des 14 derniers jours:

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);;

Et ajoutez également une valeur avant cela:

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

Utilisez ensuite la requête ci-dessus:

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;

Et obtenez les résultats pour tout le mois:

    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)
10
hruske

J'ai fini par utiliser une boucle FOR, une table TEMP et SELECT sur la table temp une fois la boucle for terminée:

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;

Mais j'imagine qu'il existe une façon plus propre de faire un 7-roulement-somme consécutive que cela.

0
josesigna

Une requête SQL récursive qui va 7 en profondeur peut fonctionner, mais je ne sais pas à quel point elle serait efficace.

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;

Non testé pour la syntaxe ou quoi que ce soit.

0
Joishi Bodio