Backfill a rollup
A rollup only covers traffic inserted after its materialized view was created; a view does not backfill.
To cover older flows rows, insert them yourself with the grouping the view uses.
Prerequisites
- The rollup and its view exist, see Rollups.
- A user with
SELECTonflowsandINSERTon the rollup target. - The
flowsrows to backfill are still inside the raw retention window (30 days by default). - A time range the view has never aggregated. A
SummingMergeTreesums rows with the same key, so a range the view already covered would be counted twice.
Steps
-
Read the view's own
SELECT. It is the grouping the backfill must reproduce, and it changes when a release adds a dimension, so read it from the server rather than from this page.SELECT as_select FROM system.tablesWHERE database = 'riptide' AND name = 'flows_by_application_1m_mv'FORMAT TSVRaw;Expected output:
SELECT f.tenant AS tenant, f.organisation AS organisation, toStartOfMinute(f.timestamp) AS timestamp, f.zone AS zone, ifNull(f.application, '') AS application, f.protocol AS protocol, f.samplingInterval AS samplingInterval, toString(f.flowProtocol) AS flowProtocol, sum(f.bytes) AS bytes, sum(f.packets) AS packets, count() AS flowCount, sumIf(f.bytes, f.direction = 'INGRESS') AS bytesIn, sumIf(f.bytes, f.direction = 'EGRESS') AS bytesOut, sumIf(f.packets, f.direction = 'INGRESS') AS packetsIn, sumIf(f.packets, f.direction = 'EGRESS') AS packetsOut, groupBitOr(toUInt8(multiIf(f.samplingProvenance = 'record', 1, f.samplingProvenance = 'options', 2, f.samplingProvenance = 'header', 4, f.samplingProvenance = 'derived', 8, f.samplingProvenance = 'fallback', 16, f.samplingProvenance = 'assumed', 32, 0))) AS samplingProvenanceMask FROM riptide.flows AS f GROUP BY tenant, organisation, timestamp, zone, application, protocol, samplingInterval, flowProtocol -
List the target's columns, so you can check that step 3 names them all.
SELECT arrayStringConcat(groupArray(name), ', ') AS columnsFROM (SELECT name, position FROM system.columnsWHERE database = 'riptide' AND table = 'flows_by_application_1m' ORDER BY position);Expected output:
tenant, organisation, timestamp, zone, application, protocol, samplingInterval, flowProtocol, bytes, packets, flowCount, bytesIn, bytesOut, packetsIn, packetsOut, samplingProvenanceMask -
Write the insert. Name the target columns, in the order of your
SELECT, and bound the range with aWHEREon the rawtimestamp.INSERT INTO riptide.flows_by_application_1m(tenant, organisation, timestamp, zone, application, protocol, samplingInterval, flowProtocol,bytes, packets, flowCount, bytesIn, bytesOut, packetsIn, packetsOut, samplingProvenanceMask)SELECT f.tenant AS tenant, f.organisation AS organisation, toStartOfMinute(f.timestamp) AS timestamp, f.zone AS zone,ifNull(f.application, '') AS application, f.protocol AS protocol, f.samplingInterval AS samplingInterval,toString(f.flowProtocol) AS flowProtocol,sum(f.bytes) AS bytes, sum(f.packets) AS packets, count() AS flowCount,sumIf(f.bytes, f.direction = 'INGRESS') AS bytesIn, sumIf(f.bytes, f.direction = 'EGRESS') AS bytesOut,sumIf(f.packets, f.direction = 'INGRESS') AS packetsIn, sumIf(f.packets, f.direction = 'EGRESS') AS packetsOut,groupBitOr(toUInt8(multiIf(f.samplingProvenance = 'record', 1, f.samplingProvenance = 'options', 2,f.samplingProvenance = 'header', 4, f.samplingProvenance = 'derived', 8,f.samplingProvenance = 'fallback', 16, f.samplingProvenance = 'assumed', 32, 0))) AS samplingProvenanceMaskFROM riptide.flows AS fWHERE f.timestamp >= toDateTime('2026-09-01 00:00:00') AND f.timestamp < toDateTime('2026-09-15 00:00:00')GROUP BY tenant, organisation, timestamp, zone, application, protocol, samplingInterval, flowProtocol;Expected output:
Ok.An
INSERT INTO … SELECTwithout a column list is positional: ClickHouse matches by ordinal, never by name, and refuses a wrong count withNUMBER_OF_COLUMNS_DOESNT_MATCH. Riptide keeps an upgraded rollup in the same physical order as a fresh one by adding columns withAFTER, so a positional insert against a riptide-managed target is correct, but nothing in the statement tells you whether that held. Once you name the columns, the target's physical order stops mattering, and the order you write them in must match yourSELECT: ClickHouse pairs the nth name with the nth expression. -
Verify that no column was omitted.
SELECT count() AS hand_written_rowsFROM riptide.flows_by_application_1mWHERE samplingInterval = 0 AND flowProtocol != '';Expected output:
┌─hand_written_rows─┐│ 0 │└───────────────────┘An omitted column takes its type default, so the rows land with
samplingInterval = 0. A materialized view cannot produce a row with a rate of0and a known protocol, so that pair is the fingerprint of a hand-written backfill, and those rows escape theWHERE samplingInterval > 0predicate every corrected total uses, see Query sampling-corrected volume. -
Verify the totals against the raw rows for the backfilled range.
SELECT(SELECT sum(bytes) FROM riptide.flowsWHERE timestamp >= toDateTime('2026-09-21 14:25:00') AND timestamp < toDateTime('2026-09-21 14:30:00')) AS raw_bytes,(SELECT sum(bytes) FROM riptide.flows_by_application_1mWHERE timestamp >= toDateTime('2026-09-21 14:25:00') AND timestamp < toDateTime('2026-09-21 14:30:00')) AS rollup_bytes;Expected output:
┌─raw_bytes─┬─rollup_bytes─┐│ 389588452 │ 389588452 │└───────────┴──────────────┘
A shift, as opposed to an omission, is usually loud: flowProtocol is a LowCardinality(String) between the dimensions and the UInt64 measures, so a shift that reaches it fails with CANNOT_PARSE_TEXT rather than writing anything.
A shift is only silent where every column it crosses is numeric, and even then each column takes its neighbour's value, so a shifted samplingInterval holds another column's number, not 0, and step 5 catches it.
Related
- Rollups for why a view does not backfill and how a rollup gains dimensions.
- ClickHouse reference for the table layout.
Open questions
- The
INSERTin step 3 was not run against a server while writing this page; its output is ClickHouse's usualOk.. Steps 1, 2, 4 and 5 were run against ClickHouse 26.7.