Skip to content

WIP: perf(cdc-graph): bound cdc_batches scan and add composite index#4554

Closed
dtunikov wants to merge 1 commit into
mainfrom
perf/cdc-graph-query-optimization
Closed

WIP: perf(cdc-graph): bound cdc_batches scan and add composite index#4554
dtunikov wants to merge 1 commit into
mainfrom
perf/cdc-graph-query-optimization

Conversation

@dtunikov

@dtunikov dtunikov commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

The CDCGraph query range-joins generate_series ticks against peerdb_stats.cdc_batches:

LEFT JOIN peerdb_stats.cdc_batches
  ON start_time >= tm AND start_time < tm + $1::INTERVAL
  AND flow_name = $4

There's no global lower bound on start_time — the only constraint on which rows are touched comes from the per-bucket join condition. The existing indexes (HASH(flow_name) and btree(start_time)) can't be combined in a single scan, so a likely plan pulls every batch for the flow across all history via the hash index and then joins it down to ~30 buckets. For a long-running mirror that means millions of rows scanned to render a small graph.

Changes

  1. Query rewrite (flow/cmd/mirror_status.go) — pull the flow's batches for the window into a bounded CTE (single scan) via an explicit start_time >= lower_bound.lb filter, then keep the same range join over that small set. Semantics are unchanged, including calendar-correct month stepping from generate_series.

  2. Composite index (V55) — (flow_name, start_time) so the bounded scan becomes a single tight index range scan. The existing idx_cdc_batches_flow_name HASH index is now redundant and could be dropped in a follow-up (left in place here to keep this change low-risk).

Notes

  • Output shape and values are identical to the previous query; only the access path changes.

🤖 Generated with Claude Code

@dtunikov dtunikov changed the title perf(cdc-graph): bound cdc_batches scan and add composite index WIP: perf(cdc-graph): bound cdc_batches scan and add composite index Jul 8, 2026
…raph

The CDCGraph query range-joins generate_series ticks against
peerdb_stats.cdc_batches per bucket. The pre-existing indexes are a HASH
on flow_name and a btree on start_time, which cannot be combined in a
single scan, so each per-bucket probe scans a start_time range and
filters flow_name as a recheck.

Add a composite (flow_name, start_time) index so each probe is a single
tight index range scan. Benchmarked against ~1.05M batches for one flow:
buffers touched for the 30h window drop from 3844 to ~180 (~20x less
I/O), with matching reductions in cache pressure on large tables.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dtunikov dtunikov force-pushed the perf/cdc-graph-query-optimization branch from 98352aa to 75e7f81 Compare July 8, 2026 16:44
@dtunikov

dtunikov commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator Author

Benchmarked prev vs. rewritten query — narrowed scope to index-only

Spun up a throwaway Postgres, loaded ~1.05M cdc_batches rows for one flow (~1.55M total across flows), recreated the pre-PR indexes (HASH(flow_name) + btree(start_time)), and ran EXPLAIN (ANALYZE, BUFFERS) on both queries.

Buffers touched — 30h (ONE_HOUR) window:

prev query rewritten query
without composite index (prod today) 3844 3844
with composite index 180 177

Execution time (JIT off, isolates real work):

window prev rewritten
30h 0.6 ms 0.8 ms
30mo (whole table) 136 ms 175 ms

Findings

  1. The composite (flow_name, start_time) index is the actual win — ~20× fewer buffers on the hour window — and it benefits the existing query identically.
  2. The query rewrite was a net negative. Postgres inlines the CTE so the plan is identical, but the CTE double-reference + extra JIT functions made it 15–28% slower. The "scan all history via the flow_name hash index" risk I originally wrote it around never materializes: the pre-existing btree(start_time) already bounds every per-bucket probe.
  3. Separately, wall-clock with default JIT on is ~57 ms and is almost entirely JIT compilation (real work 0.6 ms), triggered by a large row misestimate from the range join. Out of scope here; noting for follow-up.

Result: reverted the query rewrite; this PR now only adds the composite index (migration V55).

@dtunikov dtunikov closed this Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant