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