From 04d9b45f0a1b5f063f1148fc9d2d7e0f0fd237cf Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Tue, 23 Jun 2026 08:21:16 +0000 Subject: [PATCH] perf(properties): stop reading property_targets and funding_package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both were per-property LATERAL lookups against tables with no index on the join column (property_targets.property_id, funding_package.plan_id), so each seq-scanned 150k-180k rows once per property — ~3.4s of the load for a 291-property portfolio came from property_targets alone. Neither value is actually used by the portfolio table: the "Expected EPC" column derives from currentSapPoints + recommendation SAP, and fundingScheme is fetched separately by the proposals table. Drop both reads (kept as NULL to preserve the PropertyWithRelations shape) and trim the plan LATERAL to the columns used. 291-property portfolio: ~3.5s -> ~85ms. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/portfolio/[slug]/utils.ts | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/app/portfolio/[slug]/utils.ts b/src/app/portfolio/[slug]/utils.ts index 971a700b..c6947ccf 100644 --- a/src/app/portfolio/[slug]/utils.ts +++ b/src/app/portfolio/[slug]/utils.ts @@ -718,9 +718,14 @@ export async function getProperties( p.creation_status AS "creationStatus", p.current_epc_rating AS "currentEpcRating", p.current_sap_points AS "currentSapPoints", - t.epc AS "targetEpc", + -- property_targets is no longer read (the "Expected EPC" column derives + -- from currentSapPoints + recommendation SAP). Kept as NULL to preserve + -- the PropertyWithRelations shape. + NULL AS "targetEpc", pl.id AS "planId", - fp.scheme AS "fundingScheme", + -- funding_package is no longer read here (the proposals table fetches it + -- separately). Kept as NULL to preserve the PropertyWithRelations shape. + NULL AS "fundingScheme", COALESCE(rec.sap_points, 0) AS "totalRecommendationSapPoints", COALESCE(rec.cost, 0) AS "totalRecommendationCost", p.landlord_property_id AS "landlordPropertyId", @@ -736,27 +741,16 @@ export async function getProperties( ${mainfuelSql(sql`epc`)} AS mainfuel, p.lexiscore AS lexiscore FROM property p - -- LATERAL one-row lookups keep this query at "12 properties × index probe" + -- LATERAL one-row lookups keep this query at "N properties × index probe" -- instead of hash-joining the multi-million-row plan/recommendation tables. LEFT JOIN LATERAL ( - SELECT epc FROM property_targets - WHERE property_id = p.id - ORDER BY created_at DESC - LIMIT 1 - ) t ON true - LEFT JOIN LATERAL ( - SELECT * FROM plan + SELECT id, post_sap_points FROM plan WHERE property_id = p.id AND portfolio_id = p.portfolio_id AND is_default = true ORDER BY created_at DESC LIMIT 1 ) pl ON true - LEFT JOIN LATERAL ( - SELECT scheme FROM funding_package - WHERE plan_id = pl.id - LIMIT 1 - ) fp ON true -- Active (default, not-yet-installed) recommendations for the property, -- read denormalised. We aggregate by recommendation.property_id — the only -- indexed access path (there is NO index on recommendation.plan_id, and the