Repository navigation
Query heatmap resolves preview text for cell winners only, not every row (#4233) - #4295
Conversation
…row (#4233) base now reads query_stats and carries the digest/inline text through the window sorts; the outer SELECT resolves and truncates the preview only for the rn = 1 row of each cell, matching what v_query_stats would have resolved without paying the query_text_dim join for every row in the window. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
…lity Ruling item 2: BothCopies_AreByteIdentical_ThroughTheRnFilter compares ViewerDataService.BuildQueryHeatmapSql against DarlingQueryHeatmapReader's copy directly, normalizing the two differences that predate #4233 (the bin width literal vs $5, the preview width literal vs $7) and cutting before the ORDER BY/LIMIT tail (a third, deliberate difference already pinned by HeatmapSql_CapsFromTheRecentEnd). Proven live: a one-character change to either copy's COALESCE/query_text_dim subquery made this fail before being reverted. Ruling item 4: OldSqlFromDev_AndTheNewBuilder_AgreeOnCellsCountsHashAndPreview runs the pre-#4233 SQL (fetched via git show origin/dev, pinned as a literal) and the new BuildQueryHeatmapSql over one seeded window covering a query_text_dim-resolved winner, a pre-#1767 inline-text winner, and a same-cell tie, and asserts identical cells, counts, top hash and preview. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
DarlingQueryHeatmapTests.cs's OldSqlFromDev_AndTheNewBuilder_AgreeOnCellsCountsHashAndPreview seeded a tie (two rows with equal delta_execution_count in one cell) and then asserted the old SQL and the new builder picked the SAME row's hash and text. Both use ROW_NUMBER() ... ORDER BY delta_execution_count DESC with no second key, and their plans differ, so PostgreSQL does not promise they pick the same row. The assertion had passed so far but could flake. The tie cell now only asserts that each query's winner is one of the two tied rows, with the hash and preview text that belong to that same row. Every other cell still asserts equality. Updated the doc comment's item (c) to match. No product SQL changed: the tie is arbitrary on dev too, and picking a winner would be a behavior change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TszxYhJJbTEh4LrZ56NYo3
Fix (test only, no product SQL touched)
Kept Diagnosis confirmed on the rigBoth queries rank a tie with Proof (both runs)
Build and final test totals (rig: 127.0.0.1:55991, UTC, darlingtest)
With the clean tree (my fix, no product changes),
No full suite run locally; CI runs it. Never ran Installer.Tests. Note on the brief's class nameThe brief said "the class that holds this test" is RigPostgreSQL 18.6 + TimescaleDB, port 55991, Nothing deferred; nothing filed. |
Closes #4233.
Why
The query heatmap's
baseCTE resolved and truncatedquery_textfor every row in the window, through thequery_text_dimjoin inv_query_stats. Then it threw almost all of that work away, because each(time_bin, bucket_index)cell shows only itsrn = 1row.The cost was measured on a seeded rig. It held 180,000
query_statsrows over 24 hours and 1,400 distinctquery_text_dimtexts of about 700 bytes each. The read had aserver_idfilter and no database filter.EXPLAIN (ANALYZE, BUFFERS)took 2035.5 ms, withshared hit=7668, temp read=2186 written=2190. The plan ran aNested Loop Left Jointoquery_text_dimonce per fact row (179,875 probes). It also sorted rows of about 751 bytes, and that sort spilled to disk (an external merge of about 14 MB across workers).EXPLAIN (ANALYZE, BUFFERS)took 241.9 ms, withshared hit≈6924, temp read=1615 written=1618. TheIndex Scanonquery_text_dim_pkeyran 1,012 times (loops=1012), once per winning cell instead of once per row. The sorted rows were 88 to 96 bytes wide instead of about 751.That is about 8 times faster on this rig. The issue's own production numbers, 613 ms to 266 ms, came from warmer buffers and real Query Store data. The change is the same in both cases. The join and the truncation no longer run once per row, and only a narrow key goes through the sorts.
What changes
Both copies of
BuildQueryHeatmapSqlchange the same way: the desktop viewer's chart, and the MCP and web reader behindget_query_heatmap. The differences between the two copies that were already there stay.basenow readsquery_statsdirectly, neverv_query_stats(query_text/query_plan_xml stored inline per row: 94% of a field store — normalize into hash-keyed dimension tables (~135x measured) #1767's view that resolves the payload). It carriesquery_hash, the inlinequery_textandquery_text_digestthroughbase,binnedandranked. Only rows from before query_text/query_plan_xml stored inline per row: 94% of a field store — normalize into hash-keyed dimension tables (~135x measured) #1767 have an inlinequery_text. Before,basecarried a preview column that was already resolved and truncated.SELECT ... FROM ranked WHERE rn = 1resolves the preview only for the row each cell keeps. It usesLEFT(COALESCE(query_text, (SELECT d.query_text FROM query_text_dim d WHERE d.digest = ranked.query_text_digest)), <n>). That is the resolutionv_query_statsdoes, moved to after thern = 1filter.<n>is the literal120in the viewer's copy, and the bound parameter$7in the MCP reader's copy. MCP read tools have no default response-size budget: at default arguments 12 tools return >50 KB and 3 return >100 KB for one server, more than an agent client's per-result cap #4198 made the reader's preview width a parameter, and this PR keeps that parameter in the same place.DarlingQueryHeatmapReader.HeatmapCoverageSqlmoves its twoEXISTSprobes fromv_query_statstoquery_statstoo, so the probe reads the same table as the query. The answer cannot change.v_query_statshas noWHEREclause and no join that changes the row count. So a row exists in one exactly when it exists in the other.BuildQueryHeatmapSqlmethods and onHeatmapCoverageSqlnow describe the new shape and cite Query heatmap resolves and sorts query text for every query-stats row in the window, then keeps it for ~1 in 125: 613 ms vs 266 ms with the text fetched for the winners only (WPF viewer and get_query_heatmap) #4233. The old ones said that the preview was resolved fromv_query_stats, which is no longer true.The results do not change: the same cells, counts, top hash and preview text. When two rows tie for a cell, PostgreSQL returns either one, as before. The live tests below check this.
Lite
Two Lite files read the heatmap:
Lite/Services/LocalDataService.QueryHeatmap.cs(for MCP) andLite/Services/LocalDataService.QueryStats.csnear line 1441 (the desktop chart's own copy). Neither one carries a resolved preview through a window sort, as the Darling copies did.ARG_MAX. So both Lite copies pick the winning row withARG_MAX(query_preview, delta_execution_count) AS top_query_textin a plainGROUP BY, not with aROW_NUMBER()window andWHERE rn = 1.ARG_MAXhas to see the value of every candidate row to pick the winner. So Lite has no way to resolve the text after a filter without adding back the window query thatARG_MAXavoids.query_text_dimtable. It storesquery_textinline on everyquery_statsrow, so it has no join to remove. The cost in this issue, a join per row to a table keyed by hash plus a truncation per row, does not exist in Lite.v_query_statsdoes not resolve anything either.DuckDbInitializerbuilds it as a plainSELECT * FROM query_stats, or, when parquet archiving is on, as aUNION ALL BY NAMEwith the archived parquet files. A Lite read ofquery_statsalone, as in the Darling fix, leaves out the archived rows.Tests
QueryHeatmapSqlTwinParityTests.BothCopies_AreByteIdentical_ThroughTheRnFilter(new, 5 cases, one per metric) calls bothBuildQueryHeatmapSqlmethods directly. It removes the two differences that predate Query heatmap resolves and sorts query text for every query-stats row in the window, then keeps it for ~1 in 125: 613 ms vs 266 ms with the text fetched for the winners only (WPF viewer and get_query_heatmap) #4233: the bin width (a literal against$5) and the preview width (a literal against$7). It checks that both of those replacements happened. It cuts both strings at the finalWHERE rn = 1, because theORDER BYandLIMITafter it differ on purpose (HeatmapSql_CapsFromTheRecentEndpins that). Then it checks that the rest is identical. With one space removed from the viewer's copy, all 5 cases failed and showed the position of the difference. With the space put back, they passed.DarlingQueryHeatmapLiveTests.OldSqlFromDev_AndTheNewBuilder_AgreeOnCellsCountsHashAndPreview(new) runs the SQL from before this PR and the newBuildQueryHeatmapSqlagainst the same seeded window, with the same parameters. The old SQL is a literal in the test, taken fromdevbefore this PR. The window has three cases, each in its own 5-minute bin:query_text_dim(a digest, and no inlinequery_text).query_text, and no digest).delta_execution_count.For every cell, the two queries must return the same bin, bucket and count. For cases 1 and 2, they must also return the same hash and preview, and those must match what the test seeded. For case 3, each query's winner must be one of the two tied rows, with the hash and preview of that same row. The two queries can pick different rows. The test cleans up through
LiveStoreCleanup.RunAsync.Test plan
Darling.Testsbuilds with 0 warnings, before and after the merge ofdev.DarlingQueryHeatmapSurfaceAndSqlTests(27),ViewerQueryHeatmapSqlTests(8),QueryHeatmapSqlTwinParityTests(5),DarlingQueryHeatmapLiveTests(2) andViewerQueriesRestLivePostgresTests(4): 46 of 46 passed against the rig.EXPLAIN (ANALYZE, BUFFERS)before and after, on the seeded rig above: 2035.5 ms to 241.9 ms.The full
Darling.Testssuite ran once, after the merge ofdev(d7fa0e48), against a newdarlingtest. It ran 14,033 tests: 2 failed, 49 skipped and 1 not run. Neither failure is in a file this PR changes:PayloadDimensionLiveTests.PreFilter_SkipsLockingFreshRows_ButStillRefreshesStaleOnes_AndInsertsNewDigestsexpected almost no WAL for a batch of fresh rows, and saw 105,768 bytes. WAL from the rest of the suite can reach that measurement. The rig's log showed a slow checkpoint from earlier work.TrendPayloadBudgetLiveTests.EveryDefaultAnswer_StaysNearTheBudget_AndTheLargestAnswerStaysUnderTheCapgot "Exception while reading from stream" fromget_file_io_trend.On a new
darlingtest, both classes then ran alone and passed: 19 tests, 0 failed.Lite: no Lite file changes. CI runs the Lite suite.
The tie check (
88740dd3): a temporary tie-break in the new builder made it pick the other tied row. The old check then failed on the tie cell (Expected: "0xTIEA", Actual: "0xTIEB"), and the new check passed. With the tie-break removed,DarlingQueryHeatmapLiveTests,QueryHeatmapSqlTwinParityTestsandDocCommentHygieneTestsran 84 tests, 0 failed.DarlingQueryHeatmapSurfaceAndSqlTestsran 27 tests, 0 failed.GitHub Actions CI on
88740dd3: build, Darling PostgreSQL tests and Lite tests all pass.CHANGELOG entry
SECTION: Fixed
ENTRY:
REF:
[Query heatmap resolves preview text for cell winners only, not every row (#4233) #4295]: Query heatmap resolves preview text for cell winners only, not every row (#4233) #4295
🤖 Generated with Claude Code
https://claude.ai/code/session_01FVjn4PBJN71NQXdFo6ZxNQ