Skip to content

Custom Views Query Store panels read the per-interval table instead of re-sorting raw snapshots (#4605) - #4617

Merged
erikdarlingdata merged 6 commits into
devfrom
fix/4605-qs-wide
Sep 28, 2026
Merged

erikdarlingdata merged 6 commits into
devfrom
fix/4605-qs-wide

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

Part of #4605 (the routing half; the role-level temp_file_limit is #4610).

Why

A composed Custom Views panel over query_store_stats dedupes the raw slice on every run: ROW_NUMBER() OVER (PARTITION BY server_id, server_name, database_name, query_id, plan_id, runtime_stats_interval_id, first_execution_time, execution_type_desc, replica_role …) over every raw snapshot in the window, before it aggregates (ComposeCompiler.BuildFactRelation). On a fleet store that sort spilled about 1.8 GB per call and ran to the 60 s read ceiling, and the I/O pushed the collector's own writes past their sweep deadline (#4605). The per-interval table collect.query_store_interval_wide (V145, #3953/#4382) already holds exactly what that dedupe computes: the latest snapshot per interval.

What changes

  • The eligibility check. ComposeRunContext gains QueryStoreWideEligible (default false). The shared composed-panel runner (DarlingWebEndpoints.RunComposedPanelAsync, behind both the web composer and the MCP run_custom_view_panel tool) decides it before compiling. It's true only when ALL of these hold:

    • the panel reads query_store_stats;
    • the window is at least ComposeQueryStoreWideMinWindow (12 h);
    • the store has V145;
    • QueryStoreIntervalWide.ReadsTableAsync passes for every enabled server in scope. That's the same rule the grid and the MCP Query Store reads already use, including "no legacy row in the window" and the literal-end refusal: a literal window end earlier than applied_through stays raw.

    Any fault, pre-V145 schema or failed clause gives false, so the panel stays raw.

  • The relation. When the flag is set, BuildFactRelation reads (SELECT w.*, s.server_name FROM collect.query_store_interval_wide AS w JOIN collect.servers AS s ON s.server_id = w.server_id WHERE w.collection_time >= $1 AND w.collection_time <= $2) AS f.

    • The wide table's identity is the raw dedupe's partition, server_name aside.
    • collect.servers (a server_id primary key with a NOT NULL server_name) restores that name 1:1.
    • Every other case compiles to today's raw SQL, byte for byte.
  • Custom alerts stay raw. CustomAlertEvaluator builds its own run context and never sets the flag, so a custom alert over Query Store keeps reading raw.

  • A diagnostic fix found on the way: the eligibility check's catch arm no longer puts ex.Message into a trace line (WebExceptionTextCensusTests); it logs the exception type only.

Exactness

Custom Views numbers must not move. ComposeQueryStoreWideExactnessLiveTests checks that.

  • The seed: two servers and three databases, through the collector's own write path. It includes an interval that spans an hour boundary and is re-fetched, Regular, Aborted and Exception rows for the same identity, a re-fetched open interval with a growing execution count, and module_name values for a LIKE filter.
  • The comparison: it runs the SAME panel through ComposeCompiler, forced raw and forced wide, and asserts the rows are identical, with values compared exactly (no tolerance).
  • The five cases:
    • a Ranked top-N SUM by query_hash;
    • a minute-grain time series;
    • an hour-grain panel filtered by module_name LIKE;
    • the qs_max_duration_us MAX;
    • a Scalar SUM.
  • The fallbacks: a second fact shows a pending batch and a literal end before applied_through both route to raw.
  • RED:
    • deleting the non-Regular rows for one identity in the wide table makes the Ranked case differ; reverted, it's green again;
    • against dev, the class doesn't compile, since ComposeRunContext has no QueryStoreWideEligible.

The 12-hour minimum

ComposeQueryStoreWideMinWindow adopts the #4382 grid's measured QueryStoreIntervalWide.GridWideMinWindow (12 h). Below it, a panel stays raw: the eligibility round trips cost more than they save on a short window. A composer-specific measurement is part of #4605's remaining work.

Tests

  • ComposeQueryStoreWideExactnessLiveTests (new, live) plus DarlingComposeTests (3 new compile pins: the eligible relation's text; module_name LIKE compiles against f.module_name; the non-eligible case is today's raw text), StorageCommandTimeoutTests, StartupCommandTimeoutTests and WebExceptionTextCensusTests: Total 362, Failed 0 on a TimescaleDB 2.30.1 / PG 18 rig.
  • Build: 0 warnings, 0 errors.

CHANGELOG

SECTION: Changed
ENTRY: - Custom Views Query Store panels over a recent window read the per-interval table instead of re-sorting raw snapshots ([#4617]) - A Query Store panel of 12 hours or more, whose window the store's per-interval table fully covers, now reads that table, with the same numbers, instead of deduplicating every raw snapshot in the window. On a large fleet that dedupe spilled gigabytes of temporary files and could run to the 60-second read timeout.
REF: [#4617]: #4617

…4605)

Adds ComposeQueryStoreWideExactnessLiveTests, a live-Postgres test that
seeds two servers with Query Store rows through the real collector
write path (hour-spanning intervals, mixed Regular/Aborted/Exception
outcomes, a re-fetched open interval, module names for a LIKE filter),
then compiles and runs the same Compose panel twice through the real
compiler -- once forced to raw, once forced to the query_store_interval_wide
route -- and asserts the rows come back identical across a ranked SUM,
a minute-grain time series, an hour-grain LIKE filter, the MAX measure,
and a scalar. A second test proves a pending batch and a literal end
before applied_through both stay on raw. The class fails to compile
against dev (the branch's own eligibility flag does not exist there),
so the compile failure itself is the RED for this proof.

Also fixes a real diagnostic-trace bug the branch had already
introduced: the eligibility check's catch arm logged the exception's
own message text into a web-surface trace, which the exception-text
census forbids. The trace now logs only the exception's type name.

Confirmed the MCP custom-view tool and the web endpoint share the same
compose runner and therefore the same eligibility decision; the custom
alert evaluator builds its own run context and defaults to raw, which
is unchanged and named in the PR body.
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