Files
HealthLog/prisma/migrations/0372_pulse_hourly_mean_day/migration.sql
T

114 lines
4.1 KiB
SQL

-- Pulse: a day is the mean of its hours' means, a coarser bucket the mean of
-- its days.
--
-- A watch samples pulse about every 5 to 10 minutes at rest and about every
-- 5 seconds in a workout, so the plain mean over a day's readings let one
-- workout hour outweigh the other twenty-three. The rollup writer now stores a
-- PULSE DAY bucket's `mean` as the mean of its per-hour means, and a WEEK /
-- MONTH / YEAR bucket's as the mean of its days' values (see
-- `src/lib/measurements/day-statistic.ts`). Nothing rewrites a stored bucket
-- on its own: the boot backfill and the freshness check fill only missing
-- days, so rows written before this release would keep the reading mean
-- until a write happened to touch their day. This migration recomputes them.
--
-- Scope, exactly:
-- - only rows of type PULSE, in every granularity;
-- - only the `mean` column. `count`, `min_value`, `max_value`, `sum_value`,
-- `sd`, `slope`, `r2` and the regression sums stay over every reading, as
-- the writer keeps them;
-- - hours, days, weeks, months and years are `date_trunc` on `measured_at`
-- in the session zone, exactly as the writer groups them, per user / source
-- over non-deleted readings.
--
-- Recomputing from `measurements` makes the migration idempotent: a rerun
-- derives the same means whatever the column held before. A stored row whose
-- readings are all gone is left as it is; the writer prunes such rows on the
-- next fold of their range.
--
-- The hour means are built once into a temporary table, so the readings are
-- scanned once for all four granularities.
--
-- Reversibility (down): recompute `mean` as `AVG(value)` over the same groups,
-- or refold the PULSE rollups on a release before this one. No column changes
-- shape.
CREATE TEMPORARY TABLE pulse_hour_means AS
SELECT
m."user_id" AS user_id,
m."source" AS source,
date_trunc('hour', m."measured_at") AS hr,
AVG(m."value") AS hour_mean
FROM measurements m
WHERE m."type" = 'PULSE'
AND m."deleted_at" IS NULL
GROUP BY m."user_id", m."source", date_trunc('hour', m."measured_at");
CREATE TEMPORARY TABLE pulse_day_means AS
SELECT
h.user_id,
h.source,
date_trunc('day', h.hr) AS day,
AVG(h.hour_mean) AS day_mean
FROM pulse_hour_means h
GROUP BY h.user_id, h.source, date_trunc('day', h.hr);
CREATE INDEX pulse_day_means_key ON pulse_day_means (user_id, source, day);
-- DAY buckets: the mean of the day's hour means.
UPDATE "measurement_rollups" r
SET "mean" = d.day_mean
FROM pulse_day_means d
WHERE r."granularity" = 'DAY'
AND r."type" = 'PULSE'
AND r."user_id" = d.user_id
AND r."source" = d.source
AND r."bucket_start" = d.day;
-- WEEK buckets: the mean of the week's day values.
UPDATE "measurement_rollups" r
SET "mean" = s.mean
FROM (
SELECT d.user_id, d.source, date_trunc('week', d.day) AS bucket_start,
AVG(d.day_mean) AS mean
FROM pulse_day_means d
GROUP BY d.user_id, d.source, date_trunc('week', d.day)
) s
WHERE r."granularity" = 'WEEK'
AND r."type" = 'PULSE'
AND r."user_id" = s.user_id
AND r."source" = s.source
AND r."bucket_start" = s.bucket_start;
-- MONTH buckets.
UPDATE "measurement_rollups" r
SET "mean" = s.mean
FROM (
SELECT d.user_id, d.source, date_trunc('month', d.day) AS bucket_start,
AVG(d.day_mean) AS mean
FROM pulse_day_means d
GROUP BY d.user_id, d.source, date_trunc('month', d.day)
) s
WHERE r."granularity" = 'MONTH'
AND r."type" = 'PULSE'
AND r."user_id" = s.user_id
AND r."source" = s.source
AND r."bucket_start" = s.bucket_start;
-- YEAR buckets.
UPDATE "measurement_rollups" r
SET "mean" = s.mean
FROM (
SELECT d.user_id, d.source, date_trunc('year', d.day) AS bucket_start,
AVG(d.day_mean) AS mean
FROM pulse_day_means d
GROUP BY d.user_id, d.source, date_trunc('year', d.day)
) s
WHERE r."granularity" = 'YEAR'
AND r."type" = 'PULSE'
AND r."user_id" = s.user_id
AND r."source" = s.source
AND r."bucket_start" = s.bucket_start;
DROP TABLE pulse_day_means;
DROP TABLE pulse_hour_means;