Files
HealthLog/prisma/migrations/0362_measurement_reference_indexes/migration.sql
T
Marc-André Bombeck 93b6c086b4 fix(restore): let a large restore outlast the request statement timeout
A restore of an account with 1.9 million readings, most of them deleted
by the nightly consolidation and still awaiting the purge, was cancelled
in its clearing step: `DELETE FROM measurements` ran past the 60 s
statement timeout every pooled connection carries, and the whole restore
rolled back with a message that named no cause (#1031).

Most of that statement was not the delete. Two columns point into
`measurements` with ON DELETE SET NULL and had no index, so Postgres
scanned `personal_records` and `ecg_recordings` once per deleted reading:
38 of 40 s on a seeded account with 300 personal records. Migration 0362
indexes both, which also speeds up the tombstone purge and account
deletion.

The restore transaction now sets its own statement and idle-in-transaction
limits to the transaction budget with SET LOCAL, and the budget counts
the rows the account holds as well as the rows the file carries. A
rolled-back restore names what the database refused it over (timeout,
lock, constraint, storage, connection, other) on the job row, the
console, the audit row and the wide event.
2026-09-27 11:24:41 +02:00

28 lines
1.4 KiB
SQL

-- Index the two columns that point into `measurements`.
--
-- `personal_records.source_measurement_id` and `ecg_recordings.measurement_id`
-- are foreign keys into `measurements` with ON DELETE SET NULL, and neither
-- column had an index. Postgres enforces such a key with a trigger that runs
-- once per deleted measurement and looks the referencing rows up by that
-- column. Without an index every lookup is a sequential scan of the whole
-- referencing table, so deleting N readings costs N scans.
--
-- On a seeded account of 1.89 million readings with 300 personal records, the
-- restore's `DELETE FROM measurements WHERE user_id = $1` spent 1.5 s deleting
-- and 38 s in these two triggers. On a slower host that crossed the 60 s
-- statement timeout every connection carries (`src/lib/db.ts`), the restore
-- was cancelled in its clearing step and rolled back (#1031). The tombstone
-- purge and account deletion delete readings the same way and paid the same
-- cost per row.
--
-- Additive only: `CREATE INDEX IF NOT EXISTS`, no data change. Not
-- CONCURRENTLY, because Prisma Migrate runs a migration in a transaction; both
-- tables are small next to `measurements`, and `migrate deploy` runs at boot
-- before the app serves traffic.
CREATE INDEX IF NOT EXISTS "personal_records_source_measurement_id_idx"
ON "personal_records" ("source_measurement_id");
CREATE INDEX IF NOT EXISTS "ecg_recordings_measurement_id_idx"
ON "ecg_recordings" ("measurement_id");