skip to content

ClickHouse

3 roadmaps38 questionsupdated

ClickHouse is an open-source columnar engine built for very fast aggregation over huge tables, often serving real-time analytics directly behind a product. Interviewers ask about it where sub-second latency matters and where its MergeTree design imposes rules a warehouse would not.

on this pageshow

guide

overview

~1 min

ClickHouse is an open-source, column-oriented database built to aggregate billions of rows fast enough to sit directly behind a dashboard or a product feature. Interviewers use it to test whether you can live with its rules: data lands as immutable sorted parts, deduplication and rollups happen during background merges rather than at write time, and a table's sort key decides almost every query's cost. A good ClickHouse answer says what happens on disk, when it happens, and what the query has to do about the gap. The hub follows a table's life. [Storage engines](/topics/db-clickhouse-storage-engines) covers the MergeTree family and which variant solves which modelling problem. [Partitioning and indexing](/topics/db-clickhouse-partitioning-indexing) explains how the sort key, the partition key and skipping indices narrow a scan. [Data ingestion](/topics/db-clickhouse-ingestion) is the write path, where batching decides whether the merge pipeline keeps up. [Materialized views](/topics/db-clickhouse-materialized-views) turn inserts into pre-aggregated tables. [The SQL dialect](/topics/db-clickhouse-sql-dialect) holds the arrays, combinators and join modes that make queries efficient, and [query performance](/topics/db-clickhouse-query-performance) is where you read plans and system tables to explain a slow one. Junior rounds ask what a part is and what the engine gives you over simpler ones. Senior and principal rounds become design questions: choosing a sort key, modelling rows that change, sizing inserts, and diagnosing memory or merge pressure. Start with the MergeTree storage model and the sort key; ingestion and materialized views only make sense once parts and merges are clear.

primer

### Parts, not rows A MergeTree table is a set of **parts**: immutable directories of sorted, compressed column files. Each insert adds new ones. Nothing is updated in place. A background process keeps combining small parts into larger ones, and almost every ClickHouse rule traces back to that cycle — how often you insert, how you delete, how you correct data. ### Merges finish the job later Several engines do their real work only when parts merge: collapsing duplicates, summing measures, combining aggregate states. Until a merge touches the relevant rows, a query can see the raw, unmerged version, and merges never reach across partitions. Interviewers want you to say out loud that these engines are **eventually** consistent with their intent, and to name what a correct read does in the meantime. ### The sort key is the index `ORDER BY` fixes the physical order inside each part, and the primary index is **sparse**: one entry per block of rows, not per row. Pruning works best through a leading prefix of that key, so choosing and ordering its columns is the central schema decision. Partitioning is a coarser tool for managing data by whole units, and skipping indices help only when values are already clustered. ### Pre-aggregate on the way in A ClickHouse materialized view reacts to each inserted block and writes a transformed result elsewhere; it never re-reads the source. Paired with **aggregate states** — partial results that can be combined later — it lets a rollup table grow incrementally while staying correct across many inserts. ### Write in bulk, read wide The engine is tuned for large, infrequent inserts and scans over few columns of many rows. Frequent tiny writes, point updates and row-at-a-time lookups fight the design, and answers that treat ClickHouse like a transactional database lose the round.

MergeTree
The core table engine family: data stored as sorted, compressed, immutable parts that background merges combine. Indexing, partitions, TTL and replication belong to this family.
Data part
An immutable directory of per-column files plus index files, created by one insert block or one merge. Never modified, only merged into larger parts or removed.
Sorting key
The ORDER BY expression of a MergeTree table. It sets row order inside every part and, by default, the columns of the primary index.
Sparse primary index
An index holding one entry per granule rather than per row, small enough to stay in memory and used to skip whole granules.
Granule
The smallest block of rows ClickHouse reads, set by index_granularity. Pruning decisions are made per granule, never per row.
Partition key
The PARTITION BY expression grouping parts into independent units that can be dropped, detached or expired whole. Merges stay within one partition.
Data-skipping index
A secondary structure storing a per-block summary, such as min/max or a bloom filter, so blocks that cannot match a filter are skipped.
ReplacingMergeTree
A MergeTree variant that keeps one row per sorting key, optionally the highest version, but only once a merge brings duplicates together.
AggregatingMergeTree
A MergeTree variant that stores aggregate-function states and combines rows sharing a sorting key during merges; the usual target of a rollup view.
Aggregate function combinator
A suffix that changes an aggregate function's behaviour, such as -If for a condition or -State and -Merge for storing and finishing partial results.
Materialized view
In ClickHouse, an insert trigger: its SELECT runs on each newly inserted block and writes the output into a separate target table.
FINAL
A query modifier that applies the engine's merge logic at read time, returning merged results at the cost of extra CPU and memory.
Mutation
An ALTER UPDATE or ALTER DELETE that rewrites the affected parts in the background; heavy, asynchronous and unsuited to frequent row changes.

Trace one batch of events. The client sends a large INSERT; each block is split by partition and every piece becomes a new part, sorted by the table's sort key and written column by column with its sparse index. Any materialized view attached to that table runs against the same block, and its output becomes a new part in the view's target table — so one client insert can create several parts across tables. Background merges then combine parts per partition, and engines such as ReplacingMergeTree or AggregatingMergeTree apply their logic while they merge. A read walks the same structures in reverse: partition pruning removes whole groups of parts, the primary index narrows each remaining part to matching granules, skipping indices drop more, and only the requested columns are decompressed. The engine then aggregates across many threads, and on a cluster a Distributed table sends the query to each shard and merges the partial results on the node that received it. Every section attaches to that path: engines and keys define the parts, ingestion sets how fast they arrive, views and state combinators decide what is precomputed, and performance work finds which stage costs the most. A small rollup shows the ideas meeting — a sort key that serves the filter, a view writing partial states, and a read that finishes them: ```sql CREATE TABLE events (ts DateTime, site_id UInt32, user_id UInt64) ENGINE = MergeTree PARTITION BY toYYYYMM(ts) ORDER BY (site_id, ts); CREATE TABLE daily_users (day Date, site_id UInt32, users AggregateFunction(uniq, UInt64)) ENGINE = AggregatingMergeTree ORDER BY (site_id, day); CREATE MATERIALIZED VIEW daily_users_mv TO daily_users AS SELECT toDate(ts) AS day, site_id, uniqState(user_id) AS users FROM events GROUP BY day, site_id; SELECT day, uniqMerge(users) AS users FROM daily_users WHERE site_id = 42 GROUP BY day ORDER BY day; ``` The final query still groups, because several unmerged parts may each hold a partial state for the same day; the merge function combines them whether or not a background merge has run.

  1. Storage Engines →

    Parts, merges and the MergeTree variants are the model every other section assumes, including why some engines only deduplicate eventually.

  2. Partitioning & Indexing →

    How the sort key, sparse index and partitions decide what a query reads; the schema choice interviewers probe hardest.

  3. Data Ingestion →

    The write path: batching, async inserts and the Kafka engine, and why too many small inserts break a table.

  4. Materialized Views →

    Insert-time rollups and their surprises, once parts and inserts are clear.

  5. SQL Dialect & Functions →

    Combinators, arrays, join strictness and FINAL: the idioms that make queries efficient rather than merely portable.

  6. Query Performance →

    Reading plans and system tables to diagnose slow or memory-hungry queries, which draws on every earlier section.

  • Treating ReplacingMergeTree as a unique constraint: duplicates survive until a merge meets them, so a correct read needs FINAL or an argMax-style query.

  • Describing a ClickHouse materialized view as a cached query that refreshes; a standard one only transforms newly inserted blocks and never sees existing rows.

  • Sending many small inserts per second and then blaming the server for "Too many parts"; batch on the client or enable async inserts instead.

  • Picking a fine-grained partition key such as a daily one for modest data, which multiplies parts and merges without speeding up queries the sort key already serves.

  • Putting a high-cardinality column first in ORDER BY when most queries filter on something else; pruning depends on the leading key columns.

  • Scheduling OPTIMIZE TABLE ... FINAL or frequent mutations as routine maintenance; both rewrite whole parts and do not scale with data volume.

  • Adding a skipping index to a column whose values are scattered across the table and expecting it to prune anything.

  • Raising the memory limit first when a query fails on memory, instead of finding the high-cardinality GROUP BY or oversized join side that caused it.

Interviewers expect you to place ClickHouse among analytical systems, not only tune it. Against cloud warehouses such as Snowflake and BigQuery, the usual trade is latency and cost control for convenience: ClickHouse rewards careful schema and sort-key design with sub-second queries at high concurrency, while a warehouse accepts looser modelling and offers richer joins and updates with less tuning. Against real-time OLAP engines such as Apache Druid and Apache Pinot it competes for the same user-facing analytics workload; the conversation there is about ingestion model, operational footprint and SQL completeness. It is not a replacement for a transactional database such as PostgreSQL, which stays the system of record while ClickHouse holds a copy shaped for analysis. In practice it sits downstream of an event stream or object store. Kafka feeds it through the Kafka table engine or an external connector, files arrive from S3-compatible storage through table functions, and change data capture brings rows over from operational databases. Grafana and BI tools read from it; it runs self-hosted or as a managed service from ClickHouse, Inc. A defensible choice names the workload: append-heavy events, wide scans and aggregations favour ClickHouse; frequent row updates, strict constraints and complex multi-way joins point elsewhere.

explore

report an issue with this guide →

questions

page 1 of 2

In ClickHouse, what happens on disk each time you INSERT into a MergeTree table?

level: juniorimportance: must knowfreq 78%

answer

  1. writes are append-only, never in place
  2. one directory per inserted block
  3. sorted, compressed, one file per column
  4. something in the background combines them
  5. parts are the unit queries must open

basics

~20 s

Each inserted block becomes a brand-new immutable data part: a directory of sorted, compressed per-column files plus index files. Existing files are never modified; a background merge process later combines parts into fewer, larger ones.

solid answer

~40 s

A MergeTree INSERT is append-only. The server buffers the incoming rows into blocks, sorts each block by the table's `ORDER BY` key, compresses each column into its own file, writes a complete new **part** directory, and then atomically publishes it into the set of active parts. Nothing already on disk is rewritten, so inserts never block concurrent readers. One statement can produce more than one part — you get roughly one part per block per partition touched, so an insert spanning many partition values creates many parts at once. Background merge threads then repeatedly merge small parts into bigger sorted parts. Because every query has to open and scan all active parts, the health of a ClickHouse table depends on inserts arriving in large batches rather than row-by-row.

code

sql · 15 lines
sql
CREATE TABLE events
(
    event_time DateTime,
    user_id    UInt64,
    action     LowCardinality(String)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (user_id, event_time);

INSERT INTO events VALUES (now(), 1, 'click');

SELECT partition, name, rows, level, bytes_on_disk
FROM system.parts
WHERE table = 'events' AND active;

go deeper

for a junior

Be able to say that ClickHouse writes a new immutable, sorted, compressed directory per inserted block and merges them later. Knowing that inserts should be batched is the expected takeaway.

for a middle

Explain the mechanics: block sizing, one part per block per partition, the sparse index inside a part, and how background merges bound the part count over time.

for a senior

Show you can reason about the read side too — per-part scan overhead, compression loss from tiny parts — and that you size batches and partition keys to control parts per second in production.

for a principal

Own the write-path contract across teams: who batches, what the per-table insert rate budget is, and how partition-key choices in one team's schema turn into merge-pool pressure for everyone on the cluster.

## The unit of storage: a data part In ClickHouse's MergeTree family, table data does not live in one big file. It lives in **parts**. A part is a directory on disk containing the rows of one write, already sorted by the table's `ORDER BY` (sorting) key, with each column stored in its own compressed file, alongside a sparse primary-index file and mark files that map index granules to offsets inside the column files. Every part belongs to exactly one partition — the value of the `PARTITION BY` expression, if the table declares one. Part directory names encode that partition plus a block-number range and a merge level, which is why you see names like `all_1_1_0` or `202408_15_20_1`. Parts are **immutable**. Once written, a part's files are never edited in place. Everything else in the engine follows from that choice. ## What an INSERT actually does When an INSERT arrives, the server: 1. accumulates the incoming rows into one or more in-memory blocks; 2. sorts each block by the table's sorting key; 3. applies the per-column compression codecs; 4. writes a complete new part directory and flushes it to disk; 5. atomically adds that part to the in-memory list of active parts, at which point it becomes visible to queries. No existing file is touched, so readers see either the whole part or none of it, and a crashed or failed insert leaves at most an unreferenced temporary directory that is cleaned up later. The count of parts produced is **not** one per statement. It is roughly one per block per partition. Two things drive it: block sizing settings such as `max_insert_block_size` and `min_insert_block_size_rows` / `min_insert_block_size_bytes`, and how many distinct partition values the inserted rows span. A single INSERT of a million rows spread over 300 daily partitions writes hundreds of parts, not one — this is why ClickHouse guards inserts with `max_partitions_per_insert_block` and why a badly chosen partition key turns ordinary loads into part storms. ```sql INSERT INTO events VALUES (...); SELECT partition, name, rows, level FROM system.parts WHERE table = 'events' AND active; ``` ## Background merges Because each write leaves a new part, a background pool continuously merges parts within the same partition into larger sorted parts, re-running the merge sort over their sorting keys and re-compressing the result. The old parts stay on disk, inactive, until they are dropped. Merging is what keeps the part count bounded, keeps compression ratios good (long runs of similar values compress far better inside one big part than split across many small ones), and, for the specialised engines, is where row collapsing or aggregation actually happens. Merging is background work with finite throughput. Inserts are foreground work with none. If inserts create parts faster than merges retire them, the part count grows without bound — the classic ClickHouse production failure. ## Why the part count matters for reads A query must consider **every active part** of every partition it did not prune away. For each part the engine opens files, reads the sparse primary index, decides which granules to read, and spins up read tasks. That per-part overhead is small but not free, and it is paid regardless of how few rows the part holds. A table with 40 well-merged parts and a table with 40,000 tiny parts can hold identical data and differ by orders of magnitude in scan time and memory. ## Practical consequences - **Batch your writes.** Aim for tens of thousands to hundreds of thousands of rows per INSERT rather than one row per statement, and keep the per-table insert rate low (on the order of one insert per second, not thousands). If the producer cannot batch, let the server do it with `async_insert`. - **Do not partition finely.** Partitioning by hour or by a high-cardinality id multiplies parts per insert. Monthly or daily partitioning is the usual choice; the sorting key, not the partition key, is what makes queries fast. - **Expect eventual, not immediate, tidiness.** Right after a big load the table will hold many parts; they shrink in number over the following minutes as merges run. `OPTIMIZE TABLE ... FINAL` forces the issue but rewrites data and is expensive, so it is a maintenance tool, not part of the load path. - **Watch it directly.** `system.parts` shows active parts and their sizes; `system.merges` shows merges in flight; `system.part_log` records each part's creation and merge history. The mental model to carry into an interview: an INSERT in ClickHouse is a file write, not a row update, and everything about ingestion tuning is about controlling how many files you create per second.

  • Does one INSERT statement always create exactly one part?
    No. It creates roughly one part per block per partition touched. A large insert can be split into several blocks by `max_insert_block_size`, and rows spanning many `PARTITION BY` values produce a separate part per partition. Inserting a day's data across 300 daily partitions writes hundreds of parts from a single statement, which is why `max_partitions_per_insert_block` exists as a guard.
  • If parts are immutable, how does ClickHouse handle a DELETE or UPDATE?
    Through mutations, which rewrite whole parts in the background rather than editing rows, or through lightweight deletes that mark rows with a hidden mask and let a later merge physically drop them. Either way the work is proportional to the parts touched, not the rows changed, so frequent small updates are a poor fit for this engine.
  • Where do you look to see how many parts a table currently has?
    `system.parts` filtered on the table with `active = 1` gives one row per live part, with row counts and sizes; aggregating by partition shows where parts are piling up. `system.merges` shows merges currently running, and `system.part_log` gives the historical trail of part creation, merging and removal.

saying these in an interview costs you the question

  • Thinks an INSERT appends rows into existing column files
  • Believes one INSERT statement always produces exactly one part
  • Assumes parts are merged synchronously before the insert returns
  • Says row-by-row inserts are fine because ClickHouse is fast
  • Confuses partitions with parts and uses the terms interchangeably

context

open as a page

In ClickHouse, what does the MergeTree engine give you that the Log and Memory engines do not?

level: juniorimportance: must knowfreq 72%

basics

~20 s

MergeTree stores rows as sorted, compressed, immutable parts merged in the background, and alone supports the sparse index, partitions, TTL and replication. Log engines are index-free append-only files; Memory keeps rows in RAM and loses them on restart.

open as a page

Why does a ClickHouse MergeTree table start rejecting inserts with "Too many parts"?

level: middleimportance: must knowfreq 80%

basics

~20 s

Because inserts are creating parts faster than background merges can combine them. ClickHouse first delays and then rejects inserts once a partition's active part count crosses the parts_to_delay_insert and parts_to_throw_insert thresholds, protecting query performance and the merge pool.

open as a page

When a block is inserted into a ClickHouse table, what data can its materialized view's SELECT actually see?

level: middleimportance: must knowfreq 70%

basics

~20 s

Only the rows in the block just inserted. A ClickHouse materialized view runs its SELECT as if that single block were the whole source table, so it never sees existing rows, and merges, mutations and TTL deletions do not trigger it at all.

open as a page

Why does a ClickHouse materialized view write uniqState(user_id) instead of uniq(user_id)?

level: middleimportance: must knowfreq 68%

basics

~20 s

Because the view aggregates only the block being inserted, its output is partial. The -State combinator stores a mergeable intermediate aggregate instead of a final number, so AggregatingMergeTree can combine partial results from many inserts; readers finish the job with uniqMerge.

open as a page

In a ClickHouse MergeTree table, how do PRIMARY KEY and ORDER BY differ?

level: middleimportance: must knowfreq 75%

basics

~20 s

In ClickHouse MergeTree, ORDER BY sets the physical sort order of rows inside every part; PRIMARY KEY only defines the sparse index and must be a prefix of ORDER BY. If PRIMARY KEY is omitted it equals ORDER BY.

open as a page

How does ClickHouse's sparse primary index use granules to skip data in a query?

level: middleimportance: must knowfreq 72%

basics

~20 s

ClickHouse splits each part into granules of index_granularity rows (8192 by default) and stores one index entry per granule holding its first row's key values. A filter on a leading key column binary-searches those entries and reads only the matching granules.

open as a page

In ClickHouse, what does EXPLAIN PIPELINE show that EXPLAIN PLAN does not?

level: middleimportance: must knowfreq 64%

basics

~20 s

EXPLAIN PLAN prints the logical steps of a ClickHouse query — read, filter, aggregate, sort. EXPLAIN PIPELINE prints the physical processors those steps become, each with a multiplier showing how many parallel streams run it, so you can see where parallelism collapses.

open as a page

What does ClickHouse's max_threads setting control, and when does raising it not help?

level: middleimportance: must knowfreq 58%

basics

~20 s

max_threads caps how many threads ClickHouse uses to process one query on one server, defaulting to the machine's physical core count. Raising it cannot help when there are too few mark ranges to split, when the query is I/O-bound, or when the expensive stage is single-stream.

open as a page

In ClickHouse, what do the -If and -Array aggregate combinators do to a function like sum?

level: middleimportance: must knowfreq 65%

basics

~20 s

Combinators are suffixes appended to any aggregate function name. -If adds a final condition argument so only rows where it is true are aggregated, as in sumIf(amount, status = 'paid'). -Array aggregates over the elements of an array column as if each element were a row.

open as a page

When does a ClickHouse ReplacingMergeTree table actually remove a duplicate row?

level: middleimportance: must knowfreq 78%

basics

~20 s

Only when a background merge brings the duplicate rows together, and merges never cross partitions. Until then both rows are returned. ReplacingMergeTree gives eventual deduplication, not a uniqueness constraint, so use FINAL or argMax for correct reads now.

open as a page

In ClickHouse, what does adding FINAL to a SELECT do, and what does it cost?

level: seniorimportance: must knowfreq 62%

basics

~20 s

FINAL makes ClickHouse apply the table engine's merge logic at query time, so a ReplacingMergeTree returns one row per sorting key and a CollapsingMergeTree returns collapsed rows. It pays for that by merging overlapping parts on every query, costing extra CPU, memory and time.

open as a page

In ClickHouse, how does a MATERIALIZED VIEW differ from an ordinary VIEW?

level: juniorimportance: should knowfreq 65%

basics

~20 s

An ordinary ClickHouse VIEW stores only a query and re-runs it at read time. A MATERIALIZED VIEW is an insert trigger: it transforms every newly inserted block and writes the result into a separate physical target table.

open as a page

In a ClickHouse MergeTree table, what does the PARTITION BY clause actually do?

level: juniorimportance: should knowfreq 65%

basics

~20 s

PARTITION BY splits a ClickHouse MergeTree table into independent groups of parts, usually by month. It enables dropping or detaching data as a whole unit, per-partition TTL, and coarse pruning — it is a data-management tool, not a substitute for the sorting key.

open as a page

In ClickHouse, where do you look up how long a finished query ran and how much it read?

level: juniorimportance: should knowfreq 52%

basics

~20 s

The system.query_log table holds one row per query event, with query_duration_ms, read_rows, read_bytes and memory_usage. Filter on type = 'QueryFinish', and run SYSTEM FLUSH LOGS first because entries are buffered for a few seconds before they appear.

open as a page

In ClickHouse, how do arrayMap and arrayFilter with lambdas transform an Array column?

level: juniorimportance: should knowfreq 72%

basics

~20 s

They apply a lambda written as x -> expression to every element of an array inside a single row. arrayMap returns a new array of transformed elements, arrayFilter returns only the elements where the lambda is true. Neither changes the row count.

open as a page

What does ClickHouse's async_insert setting change about the INSERT write path?

level: middleimportance: should knowfreq 58%

basics

~20 s

With async_insert enabled, the server collects rows from many small concurrent inserts into an in-memory buffer per table and shape, then flushes one part when the buffer hits a size or time limit. It turns server-side batching on for clients that cannot batch themselves.

open as a page

How do you bulk-load Parquet files from S3 into ClickHouse with the s3 table function?

level: middleimportance: should knowfreq 45%

basics

~20 s

Run INSERT INTO target SELECT ... FROM s3(url, format), where the URL may contain glob patterns matching many files. ClickHouse reads and parses the files in parallel; s3Cluster spreads that work across every node of a cluster instead of one.

open as a page

In ClickHouse, what does ARRAY JOIN do to a row, and how does LEFT ARRAY JOIN differ?

level: middleimportance: should knowfreq 55%

basics

~20 s

ARRAY JOIN expands each row into one row per element of the named array, so a row with five elements becomes five rows. Rows whose array is empty disappear. LEFT ARRAY JOIN keeps them, emitting one row with the element type's default value.

open as a page

In ClickHouse, what do the ANY, ALL and ASOF JOIN strictness modes mean?

level: middleimportance: should knowfreq 55%

basics

~20 s

ALL is the ANSI behaviour and the default: every matching right row produces a result row. ANY returns at most one right-hand match per left row, capping fan-out. ASOF joins on equality plus one inequality, returning the closest preceding row — the time-series as-of lookup.

open as a page

In ClickHouse, when would you choose SummingMergeTree over AggregatingMergeTree?

level: middleimportance: should knowfreq 58%

basics

~10 s

SummingMergeTree when every measure is additive and you only need sums; AggregatingMergeTree when you need non-additive results like distinct counts, quantiles or averages, which require storing mergeable aggregate states instead of plain numbers.

open as a page

How do you ingest a Kafka topic into ClickHouse using the Kafka table engine?

level: seniorimportance: should knowfreq 55%

basics

~20 s

Create three objects: a Kafka engine table that consumes the topic, a MergeTree table that stores the data, and a materialized view that reads from the first and inserts into the second. The Kafka table is a consumer, not storage — selecting from it consumes messages.

open as a page

In ClickHouse, how does chaining materialized views turn one insert into writes across several tables?

level: seniorimportance: should knowfreq 45%

basics

~20 s

A materialized view fires on any insert into its source table, including writes made by another view. So a view on a rollup table cascades: one client INSERT synchronously executes every view in the chain and creates a new part in each target table, multiplying insert latency and part count.

open as a page

Why can retrying a failed INSERT into a Replicated ClickHouse table leave its materialized view missing rows?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Replicated MergeTree deduplicates identical inserted blocks. If the source write succeeded but the view's write failed, the retry's block is recognised as a duplicate and skipped entirely, so the view never runs again. The deduplicate_blocks_in_dependent_materialized_views setting makes the view's target deduplicate independently.

open as a page

In ClickHouse, how do you choose the column order for a MergeTree ORDER BY key?

level: seniorimportance: should knowfreq 58%

basics

~20 s

Lead with the column almost every query filters on, then order the rest so that low-cardinality columns come before high-cardinality ones. Only a leading prefix of the key prunes granules, so the first column decides how much data a typical query reads.

open as a page

When does a ClickHouse data-skipping index such as minmax or bloom_filter actually help?

level: seniorimportance: should knowfreq 52%

basics

~20 s

A ClickHouse skipping index helps only when the indexed column's values are physically clustered under the table's sorting key. It stores summaries per block of granules and drops blocks that cannot match; on values scattered evenly across the table it eliminates nothing and only adds cost.

open as a page

A ClickHouse table partitioned by toYYYYMMDD(event_time) now rejects inserts with "Too many parts" — why?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Each insert creates at least one part per partition it touches, and merges never combine parts across partitions. A daily partition key multiplies the active partitions, so parts accumulate faster than background merges can consolidate them and ClickHouse throttles then rejects inserts.

open as a page

How does ClickHouse execute a query against a Distributed table across shards?

level: seniorimportance: should knowfreq 44%

basics

~20 s

The node receiving the query rewrites it against each shard's local table, sends it to one replica per shard, and merges what comes back. Shards return partially aggregated state rather than raw rows, so the initiator does the final merge — which is why it can become the bottleneck.

open as a page

A ClickHouse GROUP BY aborts with MEMORY_LIMIT_EXCEEDED — how do you diagnose and fix it?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Read the exception to see which limit fired — per query, per user or server-wide — then find the real consumer: usually a high-cardinality GROUP BY or a huge JOIN right side. Fix by shrinking the state, enabling spill to disk, or lowering threads, before raising max_memory_usage.

open as a page

In ClickHouse, what does uniqState return, and how do you turn stored states back into a number?

level: seniorimportance: should knowfreq 45%

basics

~20 s

uniqState returns an opaque intermediate aggregation state of type AggregateFunction(uniq, ...), not a count. You store those states and finalize them later with the matching -Merge function, uniqMerge, or with finalizeAggregation for a single state.

open as a page

showing 1–30 of 38