Skip to main content

ClickHouse Writer operational considerations

Scaling writes to a single table

The ParallelThreads property distributes work by target table name, and each target table is written by only one writer instance; if a writer targets only one table, increasing ParallelThreads beyond 1 has no effect. To scale writes to one large or high-throughput table beyond a single writer instance, place a Router in front of N independent ClickHouse Writer targets that all write to the same ClickHouse table. The Router evaluates a CASE expression per event and sends each event to exactly one of the N output streams; ClickHouse simply receives inserts from N independent clients. In internal testing, this pattern improved throughput on a single hot table several-fold versus one writer.

Golden rule: The routing expression must hash- or modulo-partition on the table's ClickHouse sorting key (its ORDER BY columns), so that every change for a given key value always lands on the same writer instance. This is different from true round-robin (next event to next writer, by count), which does not guarantee that. Each ClickHouse Writer instance maintains its own version or sign state and reconciles its own batches independently, so if two writers touch the same key concurrently, their batches can interleave and clobber each other.

Table engine (mode)

Order-sensitive?

Routing rule

MergeTree — Merge mode

Yes

Partition by sorting key. Each batch is a read-modify-write (DELETE-then-INSERT) against the live table; concurrent writers on one key clobber each other.

ReplacingMergeTree

Yes

Partition by sorting key. __STRIIM_VERSION is a per-writer-instance monotonic sequence; splitting one key across writers can produce colliding or non-comparable versions.

CollapsingMergeTree

Yes

Partition by sorting key. The sign column's collapse depends on consecutive same-key rows in stored order; scattering a key across writers breaks the cancel/state pairing.

CoalescingMergeTree (v25.6+)

Yes

Partition by sorting key. "Latest wins" is determined by physical insertion order, not a version, plus a separate tombstone DELETE pass.

MergeTree — Append Only mode

No

Round-robin is safe. Inserts are commutative, independent, immutable rows.

SummingMergeTree

No

Round-robin is safe. Summation is commutative and associative.

AggregatingMergeTree

No

Round-robin is safe for standard combinable aggregate functions (sum, min, max, uniq, quantile, groupArray).

Replicated and Distributed table engines are orthogonal to this decision: routing happens on the Striim side, and all N writers still write to the one target table. Replication only copies whatever data parts were produced, and Distributed sharding is a separate, ClickHouse-side fan-out axis — if a Distributed table's sharding key differs from its sorting key, an order-sensitive engine can still see a key out of order, so the hash-by-sorting-key rule above still applies.

Example: hash-partitioning a numeric sorting key

The modulo technique works cleanly only when the sorting key is a single numeric column. This example routes one Oracle source table to five ReplacingMergeTree writer targets, all mapped to the same ClickHouse table analytics.orders, partitioned on the numeric sorting key order_id:

CREATE SOURCE OrdersSource USING Global.DatabaseReader (
    ConnectionURL: 'jdbc:oracle:thin:@//db-host:1521/ORCL',
    Username: 'striim', Password: '********',
    Tables: 'SALES.ORDERS', FetchSize: 10000
) OUTPUT TO RawOrdersStream;

-- Hash/modulo-partition on the SORTING KEY (order_id) -- NOT round-robin.
-- GETDATA reads the after-image; GETBEFORE reads the before-image so a
-- DELETE (whose key lives only in the before-image) still routes to the
-- same writer as that key's other changes.
CREATE OR REPLACE ROUTER Orders_router INPUT FROM RawOrdersStream s
CASE
  WHEN (getIndexOfColumn(s, "order_id") >= 0 AND ABS(TO_LONG(GETDATA(s, "order_id")))   % 5 = 1)
    OR (GETBEFORE(s, "order_id") IS NOT NULL AND ABS(TO_LONG(GETBEFORE(s, "order_id"))) % 5 = 1) THEN ROUTE TO S1,
  WHEN (... % 5 = 2) THEN ROUTE TO S2,
  WHEN (... % 5 = 3) THEN ROUTE TO S3,
  WHEN (... % 5 = 4) THEN ROUTE TO S4,
  -- The '% 5 = 0' branch is essential: without it, those events are discarded.
  WHEN (... % 5 = 0) THEN ROUTE TO S5
;

CREATE TARGET OrdersWriter1 USING Global.ClickHouseWriter (
    connectionProfileName: 'analytics_ch_cp',
    Tables: 'SALES.ORDERS,analytics.orders',
    TableEngine: 'ReplacingMergeTree',
    UploadPolicy: 'eventcount:50000,interval:30s'
) INPUT FROM S1;
-- Repeat for OrdersWriter2..5, each INPUT FROM S2..S5, all with the same
-- connectionProfileName, Tables mapping, and TableEngine.

Guardrails: Use getIndexOfColumn(s, "<key>") >= 0 so tables that lack the key column fall through instead of indexing a missing field, and use ABS(...) so a negative key value does not produce a negative residue. Keep routing conditions mutually exclusive and always include the modulo-zero branch (or an ELSE) — a Router forwards a copy of each event to every matching WHEN, so overlapping or incomplete conditions can duplicate or silently drop events.

Composite, GUID, or string sorting keys

Striim's TQL expression language has no hash, checksum, or string-concatenation function (no MD5, SHA, CRC32, generic HASH, or even CONCAT) that a Router CASE expression can call, so there is no pure-TQL way to partition a composite, GUID, or string sorting key. Compute a stable shard identifier upstream instead, in a custom continuous query or Open Processor: derive a deterministic shard ID from the real sorting key (reading it from the before-image for deletes), write it into the event with putUserData(event, '__shard', shardId), and have the Router partition on USERDATA(s, "__shard") % N instead.

Start with three to five writer instances, measure throughput, and increase only if the single table is still the bottleneck. Choose a divisor that spreads your key space evenly; a skewed key distribution produces one hot writer regardless of the routing scheme.

Note: Enabling application recovery disables Striim's built-in parallel threads. If you use this Router-based pattern together with application recovery, validate the resulting exactly-once and checkpoint semantics carefully across all N independent writers before relying on it in production. This pattern is built manually in TQL today; the Flow Designer may not generate the router-plus-N-targets topology for you.

Monitor ClickHouse Writer

ClickHouse Writer publishes metrics through Striim's standard monitoring features so you can confirm the application is healthy and verify that data is flowing as expected.

Top-level metrics

Metric

Description

accepted

Total number of events accepted by ClickHouse Writer.

input / output

Events read into, and written out of, the component.

targetAcked

Events ClickHouse has acknowledged as written.

targetFreshness

How far behind the source the target currently is.

targetOutput

Events successfully delivered to the target.

latestActivity

Timestamp of the most recent write activity.

no.ofEventsAcceptedPerInterval

Throughput of accepted events per monitoring interval.

discardedEventCount

Events discarded because they did not match any mapping in Tables, or matched an entry in Excluded Tables.

Behavior: If the application halts before it reaches the RUNNING state, these metrics remain empty or zero rather than showing stale values. An application restart (stop/start) does not reset these counters — they continue accumulating from where they left off — and neither does a quiesce/resume cycle. A full restart of the Striim server does reset them to zero.

Batch-level metrics

  • Total Batches Created and Total Batches Uploaded.

  • Avg Event Count Per Batch, Avg Waiting Time in Queue (ms), and Avg Integration Time (ms).

  • Last batch info: number of inserts, updates, deletes, and PKUPDATEs in the most recent batch; Batch Event Count; Total events merged; Batch Accumulation Time (ms); Total Integration Time (ms).

Per-table metrics

Selecting an individual target table in the monitoring panel additionally shows:

  • Mapped Source Table.

  • Avg Merge Time (ms), Avg Compaction Time (ms), and Avg In-Mem Compaction Time (ms), and the Last successful merge time — relevant for engines that rely on background merges.

  • Last Applied DDL Time and Last Applied DDL Statement, and a running count of DDL events applied to that table, updated whenever a schema evolution change is processed.

Note: A nonzero count of events matched by IgnorableExceptionCode is worth investigating on its own — it means Striim logged and skipped events rather than writing or retrying them.

Troubleshoot ClickHouse Writer

This topic describes known issues with ClickHouse Writer, their symptoms, and how to resolve them.

CollapsingMergeTree shows incorrect results after an application restart

Symptom

After a Striim application restart or recovery, a table using CollapsingMergeTree shows rows that should have been deleted or updated still present, duplicate rows for what should be a unique key, or aggregate values (such as count() or sum()) that are too high or negative. The ClickHouse server log may show a logical error referencing an unexpected count of +1 or -1 rows for a sorting key.

Cause

ClickHouse Writer provides at-least-once processing. When an application recovers, it resumes from its last checkpoint and replays every event since that checkpoint, which can cause already-written events to be written again. CollapsingMergeTree tracks row state using only the Sign column and has no version or deduplication mechanism, so a replayed event re-emits an identical Sign row and can unbalance the +1/-1 count for a sorting key: an extra +1 leaves a phantom row visible and inflates counts, while an extra or orphaned -1 leaves a negative sum that does not self-correct.

Important: Running OPTIMIZE TABLE ... FINAL or querying with SELECT ... FINAL only collapses the Sign rows that are physically present; neither operation can invent a missing partner row or discard a genuine duplicate, so this kind of imbalance is not corrected automatically.

Detect the issue

Run the following query, replacing <sorting_key_cols> and <table> with your table's sorting key columns and table name. A sign_sum of 0 or 1 is healthy; a value greater than 1 indicates a phantom row; a value less than 0 indicates an orphaned row.

SELECT
    <sorting_key_cols>,
    sum(Sign) AS sign_sum,
    count()   AS rows
FROM <table>
GROUP BY <sorting_key_cols>
HAVING sign_sum < 0 OR sign_sum > 1
ORDER BY sign_sum;

Resolution

  • If your pipeline uses application recovery, prefer ReplacingMergeTree (which is idempotent under replay because it uses a monotonic version and a soft-delete flag) or MergeTree with Mode=MERGE (which reconciles to the same final state on replay) instead of CollapsingMergeTree.

  • If you must use CollapsingMergeTree, treat it as not safe to replay: after a recovery event, use the detection query above to find affected keys, and reload or re-snapshot the corresponding rows from the source. There is no automatic repair for this condition.

Target table or column not found because of identifier case mismatch

Symptom

A pipeline runs successfully against one ClickHouse server but fails against another with what appears to be the same schema, typically with a table-not-found error, or with a column silently left unmapped. This commonly appears when moving a pipeline built and tested on a developer workstation to a production Linux server.

Cause

Identifier case sensitivity is determined by ClickHouse and, for databases and tables, by the underlying server's file system, not by ClickHouse Writer; ClickHouse Writer matches and issues identifiers exactly as configured and never changes their case. Column names are always case-sensitive, regardless of operating system. Database and table names follow the case sensitivity of the ClickHouse server's file system: case-sensitive file systems (typical on Linux) treat differently cased names as different tables, while case-insensitive file systems (typical on macOS and Windows) resolve them to the same table.

Resolution

Configure database, table, and column names in ClickHouse Writer using the exact case defined in ClickHouse. Do not rely on case-insensitive matching for database or table names, since it holds only on case-insensitive file systems and fails against a case-sensitive production Linux server.