Repository navigation
Description
Update: root cause identified. See "Actual query and root cause" below — this replaces the original "no network request fires" theory, which turned out to be a symptom, not the cause.
Description
The Search page's filter sidebar for ServiceName (and presumably any other top-level column) does not reliably surface all distinct values. At high ingest volume, specific known-good values never appear in the filter list or its search box, no matter how they're typed.
This used to work correctly on an older HyperDX version (roughly 3-4 months ago, ClickHouse-side ingest volume was significantly lower at the time). It broke gradually as traffic grew — not because of a version regression, but because of how the underlying query is structured.
Actual query and root cause
Captured from the browser (/api/clickhouse-proxy, fired once when the ServiceName filter is opened):
WITH sampledData AS (
SELECT ServiceName AS param0
FROM default.otel_traces
WHERE Timestamp >= fromUnixTimestamp64Milli(<start>)
AND Timestamp <= fromUnixTimestamp64Milli(<end>)
LIMIT 3000000
)
SELECT groupUniqArray(10000)(param0) AS param0
FROM sampledDataThis does not compute SELECT DISTINCT ServiceName (or an aggregated GROUP BY) over the full time range. It takes an unordered LIMIT 3,000,000 slice of raw rows from the time range, then extracts up to 10,000 distinct ServiceName values from that slice only. Any ServiceName not present in the first 3,000,000 rows the scan happens to read is invisible to the filter — permanently, since no further request fires when the user types in the filter's search box (confirmed via Network tab: zero new requests on typing, only client-side filtering of the one cached result).
Why this is volume-dependent
The otel_traces table (standard ClickStack/OTel schema) is defined as:
ORDER BY (ServiceName, SpanName, toDateTime(Timestamp))ServiceName is the first sort key, so an unordered scan reads rows in roughly that physical order. In our environment, one single ServiceName alone produced 52 million rows in a 16-minute window — more than 17x the entire 3,000,000-row sample budget by itself. Total ingest across all services in that same window is well over 100M+ rows. A fixed 3,000,000-row sample therefore covers under ~2% of the window, and gets exhausted by whichever services are read "early" (by part/thread order) long before the scan reaches services that sort later or simply live in parts read later — regardless of how important or high-volume those services actually are.
Three to four months ago, with far lower ingest volume, the same fixed 3,000,000-row budget would have covered the large majority (or all) of the window's rows, so effectively every ServiceName showed up — hence "it worked like butter" on an older deployment. As traffic grew, the same code path started silently dropping services with no error, no truncation warning, and no way to recover them via the search box (since search doesn't issue a new query).
Environment
- HyperDX version: 2.42.0 (self-hosted via ClickStack docker-compose)
- Source: OTel traces (
otel_traces),ORDER BY (ServiceName, SpanName, Timestamp), hourly partitions ServiceNamecardinality: ~1,300 distinct values in a 16-minute window (consumer/job-per-feature naming, not a bounded microservice list)- Row volume: 52M+ rows for a single
ServiceNamein a 16-minute window; 100M+ total across all services in the same window
Steps to reproduce
- Have a table where one or a few
ServiceName/SpanNamevalues dominate row volume (common for high-traffic OTel traces tables). - Open the Search page's
ServiceNamefilter in the sidebar. - Search for a
ServiceNameknown to exist in ClickHouse for the selected time range, but with lower read-order priority relative to the dominant service(s). - Observe it never appears, and typing in the search box fires no new network request.
Expected
The filter should surface all distinct values that actually exist in the selected time range, or at minimum should sample representatively (e.g., via GROUP BY/DISTINCT with a result-count limit, not an input-row-count limit), and/or should issue a fresh, filtered query when the user searches instead of silently filtering a possibly-incomplete cached list.
Actual
Deterministically misses ServiceName values once per-window row volume for other services exceeds the fixed 3,000,000-row sample budget. No error or truncation indicator is shown to the user. Typing to search never re-queries the server.
Suggested fix
Replace the LIMIT <input rows> + groupUniqArray pattern with a query whose limit is on the output (distinct values), not the input (raw rows read) — e.g. SELECT ServiceName FROM otel_traces WHERE <time range> GROUP BY ServiceName LIMIT 10000, or SELECT DISTINCT ServiceName ... LIMIT 10000. This scans the full time range and returns correct results up to the 10k value cap, instead of an arbitrary, volume-dependent, order-dependent slice. If the row-limited sampling exists specifically to bound query cost on very large tables, consider sampling proportionally/randomly (e.g. SAMPLE clause) rather than taking the physically-first N rows, and/or surfacing a "results may be incomplete" indicator when the input-row cap is hit.
Workaround
Bypass the sidebar facet entirely and filter directly via the query bar (ServiceName:"exact-value" in Lucene mode, or ServiceName = 'exact-value' in SQL mode) — this goes straight to ClickHouse and isn't affected by the sampling issue.
Activity
[-]Filter sidebar value search does nothing for non-Map (top-level) columns — no network request fires[/-][+]Filter sidebar ServiceName values are incomplete — unordered `LIMIT` sample misses services as ingest volume grows[/+]henricop commentedon Nov 1, 2026
I can help close the technical decision before any PR. My proposed benchmark is a synthetic ClickHouse dataset with one dominant service plus valid services that occur only beyond the current 3,000,000-row input limit, then compare:
SELECT DISTINCT ServiceName ... LIMIT 10000;GROUP BY ServiceName ... LIMIT 10000;For each variant I'd report result completeness, elapsed time, rows read, peak memory, and behavior at high cardinality, using the same table/order/filter conditions as the production query. If an output cap remains, the UI should make truncation explicit and filtered search should still find an existing service. Is that the evidence you need to choose a query shape and move the issue from
Needs FeedbacktoAccepting PR? I will not start an implementation while the current label remains.