Merge pull request #374 from Hestia-Homes/perf/measures-plan-default-index

perf(db): covering partial index for the reporting measures aggregate
This commit is contained in:
KhalimCK 2026-07-08 13:57:19 +01:00 committed by GitHub
commit 153c64195b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 12202 additions and 0 deletions

View file

@ -0,0 +1,21 @@
-- Covering partial index for the reporting "where the money goes" aggregate.
-- The query joins the latest plan per property to its recommendations ON
-- plan_id, filters to default = true AND already_installed = false, and sums
-- estimated_cost / counts property_id per measure_type. The plain
-- idx_recommendation_plan_id only locates the rows; the aggregate then
-- heap-fetches every recommendation to apply the filter and read these
-- columns, which times out on large portfolios (e.g. 434). Carrying
-- measure_type, property_id and estimated_cost as key columns makes the whole
-- aggregate index-only.
--
-- OPS: `recommendation` is large, so build this CONCURRENTLY out-of-band on
-- prod BEFORE this migration runs, so the migration no-ops there (matches the
-- 0258 idx_plan_recommendations_recommendation_id pattern). A plain
-- CREATE INDEX here would lock writes on the table. Fresh environments build
-- it inline via IF NOT EXISTS.
--
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS "idx_recommendation_plan_default"
-- ON "recommendation" USING btree ("plan_id","measure_type","property_id","estimated_cost")
-- WHERE "default" = true AND "already_installed" = false;
CREATE INDEX IF NOT EXISTS "idx_recommendation_plan_default" ON "recommendation" USING btree ("plan_id","measure_type","property_id","estimated_cost") WHERE "recommendation"."default" = true AND "recommendation"."already_installed" = false;

File diff suppressed because it is too large Load diff

View file

@ -1856,6 +1856,13 @@
"when": 1783446767021,
"tag": "0265_uprn_corrections",
"breakpoints": true
},
{
"idx": 266,
"version": "7",
"when": 1783511384352,
"tag": "0266_hesitant_bromley",
"breakpoints": true
}
]
}

View file

@ -118,6 +118,26 @@ export const recommendation = pgTable(
sql`${table.default} = true AND ${table.alreadyInstalled} = false`,
),
// Covering partial index for the reporting "where the money goes"
// aggregate, which joins the latest plan per property to its
// recommendations ON plan_id, filters to the default active set, and sums
// estimated_cost / counts property_id per measure_type. The plain
// idx_recommendation_plan_id only locates the rows — the aggregate then
// heap-fetches every recommendation to apply the filter and read these
// columns, which times out on large portfolios. Carrying measure_type,
// property_id and estimated_cost as key columns (drizzle-orm has no
// INCLUDE) makes the whole aggregate index-only.
index("idx_recommendation_plan_default")
.on(
table.planId,
table.measureType,
table.propertyId,
table.estimatedCost,
)
.where(
sql`${table.default} = true AND ${table.alreadyInstalled} = false`,
),
index("idx_recommendation_material_id").on(table.materialId),
],
);