Skip to content

Commit a4834ba

Browse files
committed
migrate+web: covering summary index + DISTINCT ON for index-only latest-per-series (PR-5.1.5 fix c)
Measuring 006s idx_query_measurements_summary on prod showed the planner would not use it: value_ns (the >0 filter + the projection) was off-index, so it meant a per-row heap fetch over a groups ~870K rows -- the planner fell back to bitmap+sort (~8.6s warm for tpcds). migration 007 replaces the index with the same key columns + INCLUDE (value_ns), and collectQuerySummary switches from a commits-join row_number() window to DISTINCT ON (query_idx, engine, format) ORDER BY commit_timestamp DESC. Measured on prod: Index Only Scan, Heap Fetches: 0, ~1.95s warm (down from 8.6s). 007 also drops the one-off idx_qm_summary_test created by hand to measure the design (no-op outside prod). Docker-verified: web vitest 211 + migrate pytest 24 (count 6->7) green; tsc/eslint/prettier/ruff clean. Signed-off-by: "Connor Tsui" <connor@spiraldb.com>
1 parent 162d47e commit a4834ba

3 files changed

Lines changed: 67 additions & 37 deletions

File tree

benchmarks-website/web/lib/summary.ts

Lines changed: 30 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -352,21 +352,23 @@ async function collectQuerySummary(
352352
// penalty model: each series scores the geomean of `(10 + value) / (10 +
353353
// best)` over every query, imputing a penalty where the series has no value.
354354
//
355-
// "Latest" orders by the DENORMALIZED `commit_timestamp` (migration 006,
356-
// PR-5.1.5 fix c) instead of joining `commits` and ordering by `c.timestamp`,
357-
// so the whole window is backed by `idx_query_measurements_summary`
358-
// (dataset, dataset_variant, scale_factor, storage, query_idx, engine, format,
359-
// commit_timestamp DESC): the sargable group filter seeks the index prefix and
360-
// the remaining (query_idx, engine, format, commit_timestamp DESC) order
361-
// matches the PARTITION BY + ORDER BY, so the latest row per series is the
362-
// first index entry per partition -- no `commits` join, no full-history scan,
363-
// no work_mem window spill (was ~6.2s warm for tpcds at the prod seed).
364-
// `NULLS LAST` keeps a transient NULL (a row inserted by a writer not yet
365-
// populating `commit_timestamp`, before the post-deploy re-backfill) from
366-
// winning "latest"; the timestamp value itself is the same `commits.timestamp`
367-
// the join used, so the same-second-tie behavior (an accepted tradeoff) is
368-
// unchanged. `$1` = dataset, `$2` = storage; variant/scale params append only
369-
// when non-null.
355+
// "Latest per series" is a `DISTINCT ON (query_idx, engine, format)` ordered by
356+
// the DENORMALIZED `commit_timestamp DESC` (migrations 006/007, PR-5.1.5 fix c)
357+
// instead of joining `commits` and `row_number()`-windowing the whole group.
358+
// The covering index `idx_query_measurements_summary` (dataset, dataset_variant,
359+
// scale_factor, storage, query_idx, engine, format, commit_timestamp DESC)
360+
// INCLUDE (value_ns) makes this an Index Only Scan: the sargable group filter
361+
// seeks the index prefix, the remaining (query_idx, engine, format,
362+
// commit_timestamp DESC) order matches the DISTINCT ON + ORDER BY, and value_ns
363+
// (both the `> 0` filter and the projection) is read from the index leaf so
364+
// there is no heap fetch. ~1.95s warm for tpcds at the prod seed, vs ~6.2s for
365+
// the original commits-join `row_number()` window (006's non-covering index was
366+
// ignored because value_ns forced a per-row heap fetch -- see 007). `NULLS LAST`
367+
// keeps a transient NULL (a row from a writer not yet populating
368+
// `commit_timestamp`, before the post-deploy re-backfill) from winning "latest";
369+
// the timestamp value is the same `commits.timestamp` the join used, so the
370+
// same-second-tie behavior (an accepted tradeoff) is unchanged. `$1` = dataset,
371+
// `$2` = storage; variant/scale params append only when non-null.
370372
const params: unknown[] = [dataset, storage];
371373
const variantPred =
372374
datasetVariant === null
@@ -377,24 +379,20 @@ async function collectQuerySummary(
377379
? 'q.scale_factor IS NULL'
378380
: `q.scale_factor = $${params.push(scaleFactor)}`;
379381
const text = `
380-
WITH latest AS (
381-
SELECT q.query_idx,
382-
q.engine || ':' || q.format AS series,
383-
q.value_ns::float8 AS value_ns,
384-
row_number() OVER (
385-
PARTITION BY q.query_idx, q.engine, q.format
386-
ORDER BY q.commit_timestamp DESC NULLS LAST
387-
) AS rn
388-
FROM query_measurements q
389-
WHERE q.dataset = $1
390-
AND ${variantPred}
391-
AND ${scalePred}
392-
AND q.storage = $2
393-
AND q.value_ns > 0
394-
)
395382
SELECT query_idx, series, value_ns
396-
FROM latest
397-
WHERE rn = 1
383+
FROM (
384+
SELECT DISTINCT ON (q.query_idx, q.engine, q.format)
385+
q.query_idx AS query_idx,
386+
q.engine || ':' || q.format AS series,
387+
q.value_ns::float8 AS value_ns
388+
FROM query_measurements q
389+
WHERE q.dataset = $1
390+
AND ${variantPred}
391+
AND ${scalePred}
392+
AND q.storage = $2
393+
AND q.value_ns > 0
394+
ORDER BY q.query_idx, q.engine, q.format, q.commit_timestamp DESC NULLS LAST
395+
) latest
398396
ORDER BY query_idx, series
399397
`;
400398
const rows = (
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-- SPDX-License-Identifier: Apache-2.0
2+
-- SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
-- migrate-schema: requires-superuser
5+
-- DROP/CREATE INDEX on `query_measurements` (owned by the RDS master), so this
6+
-- carries the same requires-superuser marker as 005/006 and is applied by the
7+
-- master, the operator path PR-5.0 used.
8+
9+
-- Read-path performance follow-up to 006 (PR-5.1.5 fix c). Measuring the 006
10+
-- `idx_query_measurements_summary` against the prod seed showed the planner would
11+
-- not use it for the per-group "latest value per series" summary: `value_ns`
12+
-- (both the `value_ns > 0` filter and the projected value) was off the index, so
13+
-- using it meant a heap fetch for every one of a group's ~870K rows -- more
14+
-- expensive than the bitmap+sort the planner fell back to (~8.6s warm for tpcds).
15+
-- Adding `value_ns` as an INCLUDE (non-key) payload makes the index COVER the
16+
-- summary, so a `DISTINCT ON (query_idx, engine, format) ... ORDER BY
17+
-- commit_timestamp DESC` resolves to an Index Only Scan (Heap Fetches: 0) at
18+
-- ~1.95s warm. (A loose-index/recursive-CTE skip scan would reach ms but at a
19+
-- large SQL-complexity cost; ~2s under the bounded-concurrency summary fan-out is
20+
-- the pragmatic target for a CDN-fronted dashboard.)
21+
DROP INDEX IF EXISTS idx_query_measurements_summary;
22+
CREATE INDEX IF NOT EXISTS idx_query_measurements_summary
23+
ON query_measurements (dataset, dataset_variant, scale_factor, storage,
24+
query_idx, engine, format, commit_timestamp DESC)
25+
INCLUDE (value_ns);
26+
27+
-- Cleanup: a one-off `idx_qm_summary_test` was created by hand on prod to measure
28+
-- the covering-index design before formalizing it here. It does not exist in
29+
-- testcontainers (this DROP is a no-op there) or any other environment; dropping
30+
-- it keeps prod in sync with the migration set.
31+
DROP INDEX IF EXISTS idx_qm_summary_test;

scripts/test_migrate_schema.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -682,11 +682,11 @@ def test_apply_uses_public_schema_under_custom_search_path(
682682

683683

684684
def test_real_migrations_apply_cleanly(conn: psycopg.Connection) -> None:
685-
"""The real migrations (`001` through `006`) apply against vanilla Postgres
685+
"""The real migrations (`001` through `007`) apply against vanilla Postgres
686686
and are recorded in the ledger in order."""
687687
applied = runner.apply(conn, REPO_MIGRATIONS_DIR)
688688

689-
assert applied == 6
689+
assert applied == 7
690690
with conn.cursor() as cur:
691691
cur.execute("SELECT filename FROM public._applied_migrations ORDER BY filename")
692692
assert [row[0] for row in cur.fetchall()] == [
@@ -696,6 +696,7 @@ def test_real_migrations_apply_cleanly(conn: psycopg.Connection) -> None:
696696
"004_ingest_role.sql",
697697
"005_read_role.sql",
698698
"006_read_path_perf.sql",
699+
"007_summary_covering_index.sql",
699700
]
700701

701702

@@ -704,7 +705,7 @@ def test_real_migrations_idempotent(conn: psycopg.Connection) -> None:
704705
first = runner.apply(conn, REPO_MIGRATIONS_DIR)
705706
second = runner.apply(conn, REPO_MIGRATIONS_DIR)
706707

707-
assert first == 6
708+
assert first == 7
708709
assert second == 0
709710

710711

@@ -1516,8 +1517,8 @@ def test_real_migrations_apply_as_non_superuser_createrole_master(
15161517
assert is_super is False, "fidelity requires a non-superuser master login"
15171518

15181519
applied = runner.apply(master, REPO_MIGRATIONS_DIR)
1519-
assert applied == 6, (
1520-
"all six real migrations must apply under the non-superuser master"
1520+
assert applied == 7, (
1521+
"all seven real migrations must apply under the non-superuser master"
15211522
)
15221523

15231524
# Verify, on the superuser connection, that the bootstrap produced a usable

0 commit comments

Comments
 (0)