Skip to content

Refreshable Materialized Views

ClickHouse refreshable materialized views (RMVs) periodically re-execute a SELECT on a schedule, rather than firing per-INSERT like regular incremental MVs. They’ve been production-ready since ClickHouse 24.10.

chkit models an RMV as a regular materializedView() with an extra refresh field:

import { materializedView } from '@chkit/core'
const dailyReport = materializedView({
database: 'analytics',
name: 'daily_report_mv',
to: { database: 'analytics', name: 'daily_report' },
refresh: {
every: '1 DAY',
offset: '2 HOUR',
},
as: 'SELECT toDate(ts) AS day, count() AS total FROM analytics.events GROUP BY day',
})

Without a refresh field, the definition is a plain incremental materialized view.

FieldTypeDescription
everystringCalendar-aligned schedule (e.g., '1 HOUR', '30 SECOND', '1 DAY'). Mutually exclusive with after.
afterstringRelative schedule — after the previous refresh completed. Mutually exclusive with every.
offsetstringShifts calendar-aligned time (e.g., '2 HOUR' with every: '1 DAY' refreshes at 02:00 UTC).
randomizestringAdds jitter to avoid thundering herd. '30 SECOND' spreads refreshes over a 30-second window.
dependsOnArray<{ database, name }>Chain refreshes: run only after the listed MVs finish. Only valid with every — not after.
settingsRecord<string, string | number>Refresh settings. Common keys: refresh_retries, refresh_retry_initial_backoff_ms, refresh_retry_max_backoff_ms.
appendbooleanWhen true, refreshes INSERT rather than REPLACE. See below.
emptybooleanSuppress the initial refresh on creation.

Intervals use the ClickHouse form: <integer> <unit> where <unit> is SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, or YEAR. chkit canonicalizes '1 hour''1 HOUR' automatically.

ModeBehaviorAtomicity
Default (append: false, omitted)Truncate and replace — the target table is atomically swapped with the refresh resultAtomic — readers always see a complete, consistent snapshot
append: trueINSERT — rows accumulate on each refreshNon-atomic, like a regular INSERT SELECT

APPEND is useful for periodic snapshots where you want history, e.g.:

const hourlySnapshot = materializedView({
database: 'analytics',
name: 'hourly_snapshot_mv',
to: { database: 'analytics', name: 'hourly_snapshots' },
refresh: {
every: '1 HOUR',
append: true,
},
as: 'SELECT now() AS snapshot_ts, org_id, count() AS cnt FROM analytics.events GROUP BY org_id',
})

Use dependsOn to ensure an RMV runs only after its upstream MVs have refreshed:

const hourlyBase = materializedView({
database: 'analytics',
name: 'hourly_base_mv',
to: { database: 'analytics', name: 'hourly_base' },
refresh: { every: '1 HOUR' },
as: 'SELECT ...',
})
const hourlyAggregate = materializedView({
database: 'analytics',
name: 'hourly_aggregate_mv',
to: { database: 'analytics', name: 'hourly_aggregate' },
refresh: {
every: '1 HOUR',
dependsOn: [{ database: 'analytics', name: 'hourly_base_mv' }],
},
as: 'SELECT ... FROM analytics.hourly_base GROUP BY ...',
})

DEPENDS ON is only supported with REFRESH EVERY — ClickHouse rejects it when paired with REFRESH AFTER. chkit enforces this at generate / check time (refresh_depends_on_requires_every).

These come from the ClickHouse server’s actual behavior, not the docs. Each rule has a validation code you’ll see in chkit check --json.

ALTER TABLE … MODIFY REFRESH cannot add or remove APPEND. Server returns: “Adding or removing APPEND is not supported.”

Any change to refresh.append between the old and new definition triggers drop + recreate (risk caution). Schedule-only changes stay as a single MODIFY REFRESH.

Rule 2 — Re-include APPEND on refresh-only changes

Section titled “Rule 2 — Re-include APPEND on refresh-only changes”

Omitting APPEND in MODIFY REFRESH for an existing APPEND MV is interpreted as “remove APPEND” and rejected. chkit always re-emits APPEND in the generated ALTER when the MV has append: true, so you never need to think about it.

Rule 3 — Non-APPEND RMV + replicated target is rejected

Section titled “Rule 3 — Non-APPEND RMV + replicated target is rejected”

On ClickHouse Cloud and any SharedMergeTree / Replicated* target, a non-APPEND RMV is refused outright with: “This combination doesn’t work: refreshable materialized view, no APPEND, non-replicated database, replicated table. Each refresh would replace the replicated table locally, but other replicas wouldn’t see it. Refusing to create.”

chkit validates this at generate / check time whenever the target table is defined in the same schema. Validation code: refresh_append_required_for_replicated_target. Fix: set refresh.append = true, or target a non-replicated table.

For the dailyReport example at the top, chkit generate produces:

CREATE MATERIALIZED VIEW IF NOT EXISTS analytics.daily_report_mv
REFRESH EVERY 1 DAY OFFSET 2 HOUR TO analytics.daily_report AS
SELECT toDate(ts) AS day, count() AS total FROM analytics.events GROUP BY day;
ChangeMigration emittedRisk
Adding refresh to an existing non-refreshable MVDROP TABLE ... SYNC + CREATE MATERIALIZED VIEWcaution
Removing refreshDROP TABLE ... SYNC + CREATE MATERIALIZED VIEWcaution
Toggling appendDROP TABLE ... SYNC + CREATE MATERIALIZED VIEW (Rule 1)caution
Changing every / after / offset / randomize / dependsOn / settings onlyALTER TABLE ... MODIFY REFRESH ...caution
Changing as, to, or commentDROP TABLE ... SYNC + CREATE MATERIALIZED VIEWcaution
No changeNo operation

Check refresh status in system.view_refreshes:

SELECT database, view, status, last_success_time, last_refresh_time, next_refresh_time, read_rows, written_rows
FROM system.view_refreshes
WHERE database = 'analytics';

Manual control:

SYSTEM REFRESH VIEW analytics.daily_report_mv; -- trigger immediate refresh
SYSTEM STOP VIEW analytics.daily_report_mv; -- pause scheduling
SYSTEM START VIEW analytics.daily_report_mv; -- resume scheduling
SYSTEM CANCEL VIEW analytics.daily_report_mv; -- cancel in-flight refresh
ClickHouse versionStatus
23.12 – 24.9Experimental — required allow_experimental_refreshable_materialized_view = 1
24.9APPEND mode added
24.10 +Production-ready — no flag required

chkit targets 24.10 and above for refreshable MV support.