From 748e30146455c542d717195ee018793392b626f8 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Tue, 22 Sep 2026 22:32:24 -0400 Subject: [PATCH] Steer /agg off the sort-based GroupAggregate plan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pf_logid arrives pre-sorted off its index, so the planner picks an Incremental Sort GroupAggregate on the strength of that "free" sort — but comparing 30+ grouping columns per row makes it 5-7x slower in practice than a HashAggregate. Confirmed via EXPLAIN ANALYZE on fc_osm_skinny_34: 34-50s sorted vs ~7-10s hashed, independent of work_mem or stats (ANALYZE didn't change the plan choice). Scoped to this transaction only, since disabling incremental sort globally could hurt some other query's plan. Co-Authored-By: Claude Sonnet 5 --- routes/operations.js | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/routes/operations.js b/routes/operations.js index c4a5107..950b54d 100644 --- a/routes/operations.js +++ b/routes/operations.js @@ -517,6 +517,8 @@ module.exports = function(pool) { // result loads as one small native Perspective table indexed on pf_gkey and // the WASM view still does all rollup/expand/collapse locally. router.get('/versions/:id/agg', async (req, res) => { + let client; + let committed = false; try { const ctx = await getContext(parseInt(req.params.id), 'get_agg'); // Before the GROUP BY, not after: the territory column need not be @@ -526,7 +528,23 @@ module.exports = function(pool) { territory_clause: buildTerritoryClause(sessionTerritory(req), ctx.territoryCol, 't') || 'TRUE', }); - const { rows } = await runSQL(sql); + + // The GROUP BY spans every in_grain column plus pf_logid and the + // pf.log/pf.version columns joined in for segment/bucket — 30+ keys + // on a wide source. The planner sees pf_logid arrive pre-sorted off + // its index and picks a sort-based GroupAggregate on the strength of + // that "free" sort, but comparing that many columns per row makes it + // 5-7x slower in practice than a HashAggregate — confirmed via + // EXPLAIN ANALYZE on fc_osm_skinny_34 (34-50s sorted vs ~7s hashed, + // independent of work_mem or stats). Scoped to this transaction only: + // disabling incremental sort globally could hurt some other query's plan. + client = await pool.connect(); + await client.query('BEGIN'); + await client.query('SET LOCAL enable_sort = off'); + await client.query('SET LOCAL enable_incremental_sort = off'); + const { rows } = await runSQL(sql, client); + await client.query('COMMIT'); + committed = true; res.setHeader('Content-Type', 'application/vnd.apache.arrow.stream'); res.setHeader('X-Row-Count', String(rows.length)); @@ -545,6 +563,11 @@ module.exports = function(pool) { console.error(err); if (!res.headersSent) res.status(err.status || 500).json({ error: err.message }); else res.destroy(); + } finally { + if (client) { + if (!committed) try { await client.query('ROLLBACK'); } catch {} + client.release(); + } } });