Overview
UpsertCH adds two table engines to a ClickHouse® 26.10 server:
| Engine | Use it for | How it stores and reads |
|---|---|---|
UpsertMergeTree |
Versioned latest-state data that also needs analytical queries | Columnar on disk. One visible winning row per partition-local UNIQUE KEY is maintained during writes, so ordinary SELECTs need no FINAL. |
MemCache |
Bounded latest-state caches that can be rebuilt from an upstream source | One winning row per composite key in RAM, served through exact key and lookup-index reads. Persisted to disk by default. |
Requirements
- Linux
amd64. - A CPU with x86-64-v3 plus PCLMUL. On AWS,
t3,c6i,m7i,r6iandr7iqualify. ARM / Graviton instances are not supported.
Getting UpsertCH
UpsertCH will be distributed as an AWS Marketplace AMI with the engines preinstalled, running in your own AWS account. The listing is not live yet; see planned pricing.
UpsertMergeTree
Columnar, disk-backed tables with one visible winning row per UNIQUE KEY within each
partition. Ordinary reads see the winners without FINAL or waiting for background
merges; superseded physical rows are reclaimed later by maintenance.
CREATE TABLE player_state
(
tenant_id UInt64,
player_id UInt64,
version UInt64,
status String
)
ENGINE = UpsertMergeTree(version)
PARTITION BY tenant_id
ORDER BY (tenant_id, player_id)
UNIQUE KEY (tenant_id, player_id);
INSERT INTO player_state VALUES (1, 42, 1, 'old');
INSERT INTO player_state VALUES (1, 42, 2, 'new');
INSERT INTO player_state VALUES (1, 42, 1, 'late old value');
SELECT tenant_id, player_id, version, status
FROM player_state
WHERE tenant_id = 1 AND player_id = 42;
-- (1, 42, 2, 'new'): the late lower version does not replace the winner
Both ORDER BY and a nonempty UNIQUE KEY are required. The unique key
picks the rows to replace; the sorting key controls physical order and pruning, and the two need
not match. No experimental setting is needed to create the table.
Versions and replacement
- The optional engine argument names one stored, non-null
UInt64,DateTimeorDateTime64column (MATERIALIZEDallowed;Nullable,LowCardinalityandALIASnot). - A higher version wins. Equal versions fall back to later commit order, then row position within the block. Without a version argument, commit order decides.
- Every insert is a complete replacement row — omitted columns take their defaults, not the previous values.
- Unique-key columns are plain column identifiers of non-null integer,
String,FixedStringor tuple types built from those.
Partitions and uniqueness
- Uniqueness is per partition. The same key in two partitions gives two visible rows, even with
FINAL. - Route every version of a logical key to the same partition — partitioning by an update timestamp defeats this.
UNIQUE KEYchooses a winner; it does not reject duplicates as a constraint violation.
Writes and durability
- Inserts are synchronous, even when
async_insertis requested, and success means a durable commit (the engine enforcesfsync_after_insertandfsync_part_directoryon one local disk). - A large statement can commit in several units; an error can leave earlier units committed. It is not one transaction with its materialized views.
- Retry with the same version and payload.
insert_deduplication_tokenis supported.
Deletes, TTL and schema changes
DELETE FROM player_state WHERE tenant_id = 1 AND player_id = 42;
- Deletes are synchronous and remove the version floor, so a later insert can recreate the key with a lower version. They do not propagate to materialized-view targets.
TTL … DELETE, selected column and skipping-index changes, partition operations and projections are supported. TTL runs in background maintenance.- Partial-column updates are not supported.
MemCache
MemCache is an alias of the OverwriteCache engine — same implementation,
arguments, settings and persistence format. It keeps one winning row per composite key in RAM
and serves exact key and lookup-index reads.
CREATE TABLE latest_cache
(
tenant_id UInt64,
id UInt64,
version UInt64,
value String
)
ENGINE = MemCache(version)
KEYS (tenant_id, id)
INDEX (tenant_id)
SETTINGS max_memory_bytes = 1073741824, persist_mode = 'sync';
INSERT INTO latest_cache VALUES (1, 42, 1, 'old');
INSERT INTO latest_cache VALUES (1, 42, 2, 'new');
INSERT INTO latest_cache VALUES (1, 42, 1, 'late old value');
-- Complete primary key
SELECT value FROM latest_cache WHERE tenant_id = 1 AND id = 42;
-- 'new'
-- Declared lookup index on tenant_id
SELECT id, value FROM latest_cache WHERE tenant_id = 1 ORDER BY id;
-- (42, 'new')
One version-column argument and a nonempty KEYS tuple are required, and the version
column cannot also be a key. OverwriteCache(version) selects the same engine.
Winner selection
- A higher version replaces the stored row; a lower version is ignored.
- Equal versions compare
equal_version_tiebreak_columnsin order; on a full tie the existing row stays. - Each insert is a whole replacement row. There is no partial
UPDATEor compare-and-swap.
Lookup indexes and functions
Every query must constrain all key columns with = or IN, or use a
supported predicate on a declared lookup index. Unrestricted and partial-key scans are rejected.
Index columns must belong to KEYS, and a composite index does not create indexes on
its individual columns.
ALTER TABLE latest_cache ADD INDEX (id);
SELECT tenant_id, value FROM latest_cache WHERE id = 42;
ALTER TABLE latest_cache DROP INDEX (id);
-- Scalar lookup: NULL for a missing key
SELECT overwriteCacheGetOrNull(
'latest_cache', 'value', toUInt64(1), toUInt64(42));
-- 'new'
overwriteCacheGet returns the type's default for a missing key instead of
NULL. Both functions reject tables with an effective row policy — use
SELECT there.
Persistence and memory
| Setting | Behavior |
|---|---|
persist_mode='async' | Default. Publish in memory and queue persistence; an abnormal termination can lose an unflushed tail. |
persist_mode='sync' | Acknowledge a write only after it is durable. Disk throughput affects latency. |
persist_mode='none' | RAM only; contents are lost on restart. |
max_memory_bytes | Bounds engine-accounted memory. New work is rejected at the limit — rows are not evicted. |
equal_version_tiebreak_columns | Comma-separated columns that resolve equal versions. |
compress_segments | Default 0. Saves memory at the cost of decompressing a segment per lookup. |
A clean shutdown drains the persistence queue; on attach, segments are replayed and indexes rebuilt in memory. Persistence is local only — no replication or failover.
Deletes
DELETE FROM latest_cache WHERE tenant_id = 1 AND id = 42;
Deletes are synchronous, follow the same lookup restrictions as SELECT, and remove
the version floor. They do not propagate to materialized-view targets.
MemCache → UpsertMergeTree pipeline
A materialized view can forward every insert from a hot MemCache table to a durable
UpsertMergeTree table. Carry deletions as versioned data (for example an
is_deleted flag) because engine deletes are not forwarded.
CREATE TABLE live_state (id UInt64, version UInt64, value String, is_deleted UInt8)
ENGINE = MemCache(version) KEYS (id)
SETTINGS max_memory_bytes = 1073741824, persist_mode = 'sync';
CREATE TABLE durable_state (id UInt64, version UInt64, value String, is_deleted UInt8)
ENGINE = UpsertMergeTree(version) ORDER BY id UNIQUE KEY id;
CREATE MATERIALIZED VIEW live_state_to_durable TO durable_state AS
SELECT id, version, value, is_deleted FROM live_state;
INSERT INTO live_state VALUES (42, 1, 'old', 0);
INSERT INTO live_state VALUES (42, 2, 'new', 0);
INSERT INTO live_state VALUES (42, 3, '', 1); -- logical delete
SELECT count() FROM durable_state WHERE id = 42 AND is_deleted = 0;
-- 0
- Give each logical change a higher version. On equal versions the engines differ:
MemCachekeeps the existing row,UpsertMergeTreetakes the later commit. - Filter
is_deletedwhen reading, not inside the view, or the target never receives the deletion. - There is no joint commit across the two tables; keep view errors visible and plan reconciliation.
Verify the engines
SELECT name FROM system.table_engines
WHERE name IN ('MemCache', 'OverwriteCache', 'UpsertMergeTree')
ORDER BY name;
Compatibility & limits
SHOW CREATE TABLEkeeps the nameMemCache;system.tables.enginereportsOverwriteCache. Older servers that only knowOverwriteCachecannot attach metadata namingMemCache.- There is no replicated
UpsertMergeTreevariant. - SQL
BACKUP/RESTORE,CHECK TABLE, sampling, streaming and background disk moves are gated forUpsertMergeTree. Back up a stopped service's data, metadata, configuration and executable together. - Queries still use
MergeTreeparts, indexes and maintenance; part count and cold index I/O can affect latency. No fixed OLTP latency is guaranteed.