Repository navigation
fix(app): let the Database query drawer use the span attribute skip index - #3351
archat-hash wants to merge 1 commit into
Conversation
…ndex
Opening a database statement filtered on the coalesced attribute lookup alone.
No skip index covers that expression, so every tile in the drawer read each
granule in the selected window, which is what times out on a busy source.
Prefilter with an expression an index does cover, in the form the table
supports: `<Name>AttributeItems` where the schema has that column, otherwise
`mapValues(<Name>Attributes)`. The prefilter matches a superset of the spans
and the coalesced comparison still decides, so results are unchanged. It also
helps where neither is indexed, by short-circuiting the coalesce.
Measured on 4M spans, one statement, caches off:
granules rows read bytes CPU
before 499/499 4,001,873 191.8MiB 2945ms
after 26/499 212,992 22.4MiB 256ms
The older schema reached the same ratios through its mapValues index. Both
were confirmed end to end against ClickHouse 26.8 and 26.1.
JSON attribute columns keep the previous condition, and statements are now
escaped, so one containing a quote no longer breaks the query.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 1f51945 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
@archat-hash is attempting to deploy a commit to the HyperDX Team on Vercel. A member of the Team first needs to authorize it. |
| return undefined; | ||
| } | ||
|
|
||
| const itemsColumn = getAttributeItemsColumn(attributesField, columns); |
There was a problem hiding this comment.
🟠 major — items prefilter skips the version, column-kind and index checks that the existing KV-items rewrite applies
The items branch is picked whenever a column named <X>AttributeItems exists. It does not check the server version, whether the column is an ALIAS/MATERIALIZED key=value column, or whether it has a text(tokenizer='array') index. populateValidKvTextIndices in packages/common-utils/src/queryParser.ts:1239 does check these, and supportsDirectReadMap in core/clickhouseVersion.ts:121 deliberately turns has(<ALIAS items>, …) off on 26.2 < 26.2.19.43, 26.3 < 26.3.12.3 and 26.4 < 26.4.3.37. On those versions an ALIAS items column has no direct_read, so ClickHouse recomputes the arrayMap for every row. The stock traces schema can exist there, since 26.2 can create it, but the PR only measured 26.8 and 26.1. The separator is also hardcoded as = instead of being parsed from the column expression. Fix: drop getAttributeItemsColumn and the items kind. Emit the prefilter as (SpanAttributes['db.query.text'] = '…' OR SpanAttributes['db.statement'] = '…') and let rewriteSqlFilterWithKvItems (core/renderChartConfig.ts:391, already run on every sql filter in renderWhere) turn it into has(items, concat(…)) only when the lookup allows it. That also picks up hasAny on 26.5+. Keep the mapValues form only for schemas without a KV items index.
Nothing blocks merge automatically, but a maintainer will expect this fixed if it is a real defect in code this PR changes. If it is about surrounding code, reply and say so instead of patching. Do not widen the PR. How to respond
| filters.push({ | ||
| type: 'sql', | ||
| condition: `${expressions?.service} IN ('${service}')`, | ||
| condition: `${expressions.service} IN ('${service}')`, |
There was a problem hiding this comment.
🔵 minor — Service filter is still built from an unescaped string
This PR escapes the statement, but the service condition it also edits still interpolates service raw. A service name containing ' or \ produces broken SQL, and every tile in the drawer errors. Wrap it as '${escapeSqlString(service)}' (from @hyperdx/common-utils/dist/core/utils), as makeDbStatementCondition does.
Advisory. Fix if it is a small defect in code this PR changes; otherwise reply in the thread. Do not widen the PR or touch files it did not already change. How to respond
PR Review
3 finding(s): 🔴 0 critical · 🟠 1 major · 🔵 2 minor 2 posted as inline comment(s) on the changed lines. 1 listed below. Findings outside the changed lines1 minor
Severity is the reviewer's own estimate and is used for ordering, not filtering. No finding blocks merge automatically; a maintainer decides. |
|
| const itemsColumn = attributesField.replace(/Attributes$/, 'AttributeItems'); | ||
| const exists = | ||
| itemsColumn !== attributesField && | ||
| columns.some(column => column.name === itemsColumn); | ||
| return exists ? itemsColumn : undefined; |
There was a problem hiding this comment.
Custom query details disappear
getAttributeItemsColumn selects a column by name alone, but makeIndexPrefilter assumes its values use = and come from the selected attribute map. If a custom SpanAttributeItems column uses :, it stores db.statement:SELECT 1. The new condition looks for db.statement=SELECT 1 and removes matching spans from every drawer query.
Use the existing parseKvItemsExpression and parseKvItemsCastExpression helpers to check the source map and separator before selecting the column, or fall back to mapValues.
| function getAttributeItemsColumn( | ||
| attributesField: string, | ||
| columns: ColumnMeta[], | ||
| ): string | undefined { |
There was a problem hiding this comment.
Dashboard file exceeds its limit
The added helpers grow serviceDashboard.ts to 308 lines. The repository guide requires files to stay under 300 lines, with an exception only for tests. Move the new statement-condition helpers into a sibling module to satisfy this requirement before merging.
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Deep Review✅ No critical issues found. The change is well-scoped: escaping is applied consistently at every new user-input interpolation site, the 🟡 P2 -- recommended
🔵 P3 nitpicks (2)
Reviewers (1 completed): security. The orchestrator synthesized the remaining findings from direct analysis of the diff; the correctness, performance, adversarial, kieran-typescript, testing, maintainability, project-standards, agent-native, and learnings reviewers were dispatched but had not returned before structured output was required, so this report under-represents their lenses. Testing gaps: no coverage for the component-level early-return guard; no assertion that user-controlled No finding blocks merge automatically. A maintainer will expect P0/P1 findings in code this PR changes to be fixed; P2/P3 are your call -- fix or reply. Never fix findings about surrounding code here; reply instead. Do not widen the PR. How to respond |
Fixes #3034. Follow-up to my comment there; opening this so the reasoning and the numbers are reviewable even if the direction turns out to be different.
The problem
Every tile in the Services → Database query drawer filters by the coalesced statement lookup:
EXPLAIN indexes = 1selects no skip index for it, so each tile reads every granule in the window. The p95 tile is the one that reaches the 60s timeout, but all eight queries a click fires scan the same way.The fix
Prefilter with an expression an index does cover, picked from the table's own columns:
has(<Name>AttributeItems, 'db.query.text=…') OR has(…, 'db.statement=…')where the schema has that column (ClickHouse >= 26.2);has(mapValues(<Name>Attributes), '…'), the map-value index the issue refers to.The prefilter matches a superset of the spans and the coalesced comparison still decides, so results are unchanged. JSON attribute columns and non-map expressions keep the previous condition.
Measured
4M spans, one statement, query and condition caches off:
Same ratios at 20M spans (2477 → 113 granules). Verified end to end on ClickHouse 26.8 and 26.1, identical p95 and row counts in every pair.
Why this shape, and what I rejected
SpanAttributes['db.query.text']directly: loses spans that still carry the olderdb.statement, and on the newer schema it only reaches the key index (1983/2164 granules, 8%).ServiceName/SpanName, which the table's ORDER BY does cover: reaches the same 113 granules, but the top-N query has to collect and pass those values, costing it ~10% CPU and ~3% bytes for no gain over the prefilter.With all skip indexes disabled the prefilter still cuts CPU about 3x by short-circuiting the coalesce before it runs per row, so it does not punish schemas where nothing matches.
Limits
The gain is pruning, so a statement present in most granules gains little.
has(mapValues(…))also matches a span whose unrelated attribute equals the statement text — harmless, the coalesced comparison still decides, it only prunes less.Not addressed
The Database tab's own three queries (top-N and the two per-query charts) still scan the window. They rank across all statements, so there is nothing to prune by; that needs pre-aggregation and did not belong in this PR.
Tests
Both prefilter forms, the fallbacks, escaping, and that the search page's filter parser still reads the condition back as the same filter.
yarn ci:unitinpackages/apppasses except three suites that fail onmaintoo (DBEditTimeChartForm,DashboardFiltersModal,MetricNameSelectSearch).One note on the dev setup
docker/clickhouse/local/config.xmldeclares aprometheus_api_v1HTTP handler, which ClickHouse < 26.2 rejects at startup, so the compat schema path cannot be exercised locally without temporarily removing that block. Flagging in case that is unintentional.🤖 Generated with Claude Code