Tune queries over samples()
Every query over samples() processes roughly flows × buckets-per-flow rows, because the view expands each flow into one row per bucket.
Three habits keep that cost small.
Prerequisites
- A ClickHouse with the new query analyzer enabled, which it is by default on the tested versions.
SELECTonflows, the rollups and thesamplesview.
Steps
-
Use a rollup when the buckets are a minute or coarser. A
samples(ival = 60)query and a rollup query differ in attribution, proportional against start-minute, but usually not in what a dashboard shows, and the rollup reads thousands of rows instead of exploding millions. Reach forsamples()only for sub-minute buckets or exact proportional attribution.-- the same WHERE works against either tableSELECT application, sum(bytes) AS bytesFROM riptide.flows_by_application_1mWHERE timestamp >= now() - INTERVAL 7 DAYGROUP BY application ORDER BY bytes DESC LIMIT 10;Expected output:
┌─application────┬─────bytes─┐│ https │ 478494402 ││ shilp │ 125477552 ││ │ 27140197 ││ pdl-datastream │ 17677323 ││ http │ 3634316 ││ ssh │ 2993540 ││ us-cli │ 1839315 ││ domain │ 1792648 ││ distinct │ 1468376 ││ matahari │ 1428485 │└────────────────┴───────────┘ -
Bound the flow interval and the raw record time, and keep top-N mapping on raw keys. Inside the view the bucketed
timestampshadows the raw column, so neitherWHERE timestamp …nor bounds ondeltaSwitchedandlastSwitchedcan use the table's partition or primary index; those bounds only filter rows before the explosion, which is still worthwhile. The raw, indexed column stays addressable asflow.timestamp: bound it too, widened by the longest flow duration you expect, and the query prunes partitions like a direct table read.-- top-10 source AS as bps series over five minutes at 60 s bucketsWITH top AS (SELECT srcAs FROM riptide.flowsWHERE timestamp >= toDateTime('2026-09-21 14:25:00') AND timestamp <= toDateTime('2026-09-21 14:30:00')AND lastSwitched >= toDateTime('2026-09-21 14:25:00') AND deltaSwitched <= toDateTime('2026-09-21 14:30:00')GROUP BY srcAs ORDER BY sum(bytes) DESC LIMIT 10)SELECT time, if(asn IS NULL, 'Other', toString(asn)) AS series, b -- labels: hundreds of rowsFROM (SELECT timestamp AS time,if(srcAs IN top, toNullable(srcAs), NULL) AS asn, -- raw-key mapping: millions of rowssum(bytes) * 8 / 60 AS bFROM riptide.samples(ival = 60)WHERE timestamp >= toDateTime('2026-09-21 14:25:00') AND timestamp <= toDateTime('2026-09-21 14:30:00')AND lastSwitched >= toDateTime('2026-09-21 14:25:00') AND deltaSwitched <= toDateTime('2026-09-21 14:30:00') -- pre-explosion row filterAND flow.timestamp >= toDateTime('2026-09-21 14:25:00') - INTERVAL 1 HOUR -- partition and index pruning on theAND flow.timestamp <= toDateTime('2026-09-21 14:30:00') + INTERVAL 1 HOUR -- raw column, widened by max flow durationGROUP BY time, asn) ORDER BY time;Expected output:
┌──────────────────────────time─┬─series─┬──────────────────b─┐│ 2026-09-21 14:25:00.000000000 │ 65423 │ 1677911.6 ││ 2026-09-21 14:26:00.000000000 │ 65423 │ 1881021.8666666667 ││ 2026-09-21 14:27:00.000000000 │ 65423 │ 1789946.5333333334 ││ 2026-09-21 14:28:00.000000000 │ 65423 │ 1622516.2666666666 ││ 2026-09-21 14:29:00.000000000 │ 65423 │ 44973730.666666664 ││ 2026-09-21 14:30:00.000000000 │ 65423 │ 29193.333333333332 │└───────────────────────────────┴────────┴────────────────────┘NULLis the 'Other' sentinel because it lies outside the value space: an unknown AS arrives assrcAs = 0, which is a real series, often the largest, and must not be folded into 'Other'. -
Render labels after aggregating, not per row. The inner
GROUP BYabove runs over the exploded rows, millions of them, so group on raw binary columns (addresses, AS numbers, tuples) and build display strings (IPv6NumToString,concat, hostname fallbacks) in the outer select, which sees only a few hundred aggregated rows. For high-cardinality keys such as conversations this alone is worth about 1.5×; formatted-string group keys also force ClickHouse's slowest aggregation path. -
Fill gaps when a panel needs them. The view does not emit zero rows for empty buckets; use
ORDER BY time WITH FILL STEP <ival>or Grafana's null-as-zero option.SELECT toStartOfMinute(timestamp) AS time, sum(bytes) AS bytesFROM riptide.flows_by_application_1mWHERE timestamp >= toDateTime('2026-09-21 14:18:00') AND timestamp < toDateTime('2026-09-21 14:24:00')GROUP BY timeORDER BY time WITH FILL FROM toDateTime('2026-09-21 14:18:00') TO toDateTime('2026-09-21 14:24:00') STEP 60;Expected output:
┌────────────────time─┬────bytes─┐│ 2026-09-21 14:18:00 │ 0 ││ 2026-09-21 14:19:00 │ 0 ││ 2026-09-21 14:20:00 │ 4869417 ││ 2026-09-21 14:21:00 │ 32638460 ││ 2026-09-21 14:22:00 │ 26847096 ││ 2026-09-21 14:23:00 │ 13328169 │└─────────────────────┴──────────┘
Related
- Rollups for how the two attribution models differ.
- ClickHouse reference for the rollup tables.
Open questions
- The bundled top-10 dashboards predate step 3 and still render their series labels per exploded row; rewriting them is tracked in #346.