ClickHouse 2026 - Kiến trúc OLAP Sub-second với SharedMergeTree, Parallel Replicas và Storage-Compute Separation cho Analytics Petabyte
Posted on: 4/17/2026 3:10:28 AM
Table of contents
- 1. Vì sao ClickHouse thống trị analytics production năm 2026
- 2. Hành trình tiến hoá — từ Yandex 2016 tới Cloud-Native 2026
- 3. Kiến trúc cốt lõi — Column store, vectorized execution và MergeTree
- 4. SharedMergeTree — Storage-compute separation đúng nghĩa
- 5. Parallel Replicas — Query scale-out tuyến tính
- 6. Projection, Materialized View và Refreshable MV — Ba công cụ tăng tốc query
- 7. Tiered Storage, TTL và quản lý lifecycle dữ liệu
- 8. Ingestion patterns — Async insert, Kafka engine, Native protocol
- 9. Query tuning — Sparse index, skip index và các anti-pattern thường gặp
- 10. So sánh với thế giới OLAP 2026 — StarRocks, Doris, Druid, Pinot, DuckDB
- 11. Ba use case production điển hình — cách tổ chức bảng
- 12. Checklist production — Những thứ không được quên khi go-live
- 13. Lời kết — ClickHouse là công cụ, không phải viên đạn bạc
- 14. Nguồn tham khảo
1. Vì sao ClickHouse thống trị analytics production năm 2026
Trong bảy năm kể từ khi Yandex mã nguồn mở ClickHouse vào giữa 2016, đây vẫn là một dự án analytical database có nhịp phát triển chóng mặt nhất hệ sinh thái open-source — trung bình mỗi tháng một bản release, đồng thời là OLAP engine được nhắc tới nhiều nhất trong các bài viết engineering của Cloudflare, GitHub, Uber, eBay, Spotify, Tinybird, PostHog, Plausible, Sentry hay Highlight. Đến năm 2026 với bản 25.x, ClickHouse không còn là lựa chọn "dành cho đội dữ liệu đã chán Hadoop". Nó đã trở thành mặc định cho ba tầng use case rất khác nhau: observability (log, metric, trace), product analytics (event tracking kiểu Mixpanel, Amplitude) và realtime user-facing dashboard có độ trễ sub-second trên dữ liệu mới thêm giây trước.
Bài viết này là một cẩm nang sâu cho kiến trúc sư và senior engineer — không chỉ giải thích "ClickHouse là gì" mà đi thẳng vào những quyết định khó nhất khi đưa một cluster vào sản xuất tại quy mô trăm tỷ hàng/ngày: thiết kế primary key thưa (sparse), lựa chọn giữa replicated MergeTree truyền thống và SharedMergeTree cloud-native, khi nào bật Parallel Replicas, cách tổ chức Projection và Materialized View, chiến lược tiered storage với S3, phòng thủ trước "too many parts", và sân chơi mới giữa ClickHouse Cloud và các hậu duệ như StarRocks 3, Apache Doris 2.x, Druid, Apache Pinot hay DuckDB dạng embedded.
Ba câu hỏi quyết định trước khi đụng vào ClickHouse
Workload của bạn có ghi nặng hơn cập nhật không (append-only hoặc near-append)? Query thực sự là aggregate/filter cột trên khoảng thời gian chứ không phải đọc từng dòng theo khoá? Có chấp nhận mô hình eventual consistency ở mức giây cho các view tổng hợp không? Nếu cả ba là "có", ClickHouse gần như luôn là lựa chọn đúng. Nếu một trong ba là "không", cân nhắc Postgres, TiDB, Snowflake hoặc giữ kiến trúc transactional cũ.
2. Hành trình tiến hoá — từ Yandex 2016 tới Cloud-Native 2026
Biết được đã đi qua những giai đoạn nào giúp lý giải vì sao SharedMergeTree xuất hiện, vì sao Parallel Replicas lại là câu trả lời quan trọng cho workload heavy scan, và vì sao analytics không còn là đặc quyền của Data Warehouse truyền thống.
3. Kiến trúc cốt lõi — Column store, vectorized execution và MergeTree
Khác với hệ thống OLTP truyền thống (Postgres, MySQL, SQL Server) tổ chức dữ liệu theo hàng, ClickHouse lưu mỗi cột vào file riêng, kèm mark file làm chỉ mục thưa. Khi truy vấn chỉ đọc 3/200 cột, đĩa chỉ đụng tới 1.5% dữ liệu — đây là cội rễ của mọi ưu thế hiệu năng. Công thức tiếp theo là vectorized execution: thay vì xử lý từng tuple, ClickHouse đọc các block (mặc định 65,536 hàng) và áp toán tử SIMD trên cả block. Cuối cùng là MergeTree — engine lưu trữ lấy cảm hứng từ LSM-tree của Bigtable/Cassandra nhưng tối giản để phục vụ analytical write.
graph TB
INSERT["INSERT INTO events VALUES ..."] --> PART_NEW["New part: part_1_1_0"]
PART_NEW --> DISK["Disk / Object Storage"]
DISK --> MERGE{"Background merge"}
MERGE --> PART_L1["part_1_20_1 (level 1)"]
PART_L1 --> MERGE2{"Merge tiếp"}
MERGE2 --> PART_L2["part_1_100_2 (level 2)"]
PART_L2 --> MERGE3["... đến khi đạt size tối đa"]
SELECT["SELECT ... FROM events WHERE ..."] --> PRIMARY_IDX["Sparse primary index (mark)"]
PRIMARY_IDX --> SKIP_IDX["Skip indexes (minmax, bloom, set)"]
SKIP_IDX --> READ_PARTS["Đọc chỉ các part + granule cần thiết"]
READ_PARTS --> VEC["Vectorized execution trên block 65K dòng"]
VEC --> AGGR["Aggregate / Join / Window"]
AGGR --> OUTPUT["Client"]
3.1 Part, granule, mark — ba khái niệm phải thuộc lòng
Mỗi lần INSERT, ClickHouse tạo một part mới trên đĩa gồm toàn bộ cột của bảng, các file index và metadata. Tên part có dạng all_<min_block>_<max_block>_<level>. Nhiều part nhỏ là bạn của write throughput nhưng là kẻ thù của read — vì query phải mở mọi part, đọc primary index, gộp kết quả. Background merge gom nhiều part nhỏ thành part lớn, đồng thời áp các engine phụ (Replacing, Collapsing, Aggregating) nếu bảng khai báo.
Bên trong một part, dữ liệu được chia thành các granule (mặc định 8,192 hàng). Mỗi granule có một entry trong mark file của từng cột — chính là sparse primary index. ClickHouse không lưu index cho từng dòng (như B-tree của Postgres) mà chỉ ghi giá trị đầu mỗi granule, nhờ đó index nhỏ đủ để nằm hoàn toàn trong RAM ngay cả khi bảng có nghìn tỷ dòng.
CREATE TABLE events
(
user_id UInt64,
event_time DateTime64(3, 'UTC'),
event_name LowCardinality(String),
project_id UInt32,
properties Map(String, String),
ip IPv6,
country_code FixedString(2)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(event_time)
ORDER BY (project_id, event_name, event_time)
SETTINGS index_granularity = 8192;Ba điều đáng chú ý trong ví dụ trên:
- PARTITION BY tháng. Partition không phải index — nó quyết định cách phân hoạch vật lý ở thư mục đĩa, giúp thao tác drop/TTL theo block thời gian cực rẻ. Tránh partition quá nhỏ (giờ, ngày trên bảng ít dữ liệu) — sẽ đẻ ra quá nhiều part.
- ORDER BY là primary key. Thứ tự cột quan trọng: prefix càng selective với truy vấn phổ biến thì skip càng mạnh. Quy tắc phổ biến: cột có cardinality thấp trước, cột thời gian sau.
- LowCardinality, FixedString, Map — các data type nhỏ gọn giúp compress ratio đẹp và CPU cache friendly hơn hẳn String thô.
Nguyên tắc thiết kế ORDER BY
Đặt câu hỏi: "Nếu tôi chỉ được chọn 2-3 cột để filter đa số truy vấn của tôi, đó là cột nào?" — đó chính là ứng viên ORDER BY. Đừng cố gắng đặt mọi cột có thể filter vào đây; index thưa không giống B-tree, chỉ có prefix mới thực sự skip được data. Với phần còn lại, dùng skip index (minmax, bloom filter, set, ngrambf) hoặc projection.
3.2 Vectorized execution — vì sao 1 tỷ hàng/giây
Với mỗi block (65,536 hàng), engine chạy một chuỗi processor (source, filter, expression, aggregating, merge, sink). Mỗi processor nhận block, biến đổi tại chỗ, và đẩy sang processor tiếp theo — gần như một pipeline tương tự Apache Arrow. Phần lớn toán tử (so sánh, cộng trừ, hash, count) được compile thành SIMD với AVX2/AVX-512 khi runtime detect được CPU flag. Đó là lý do một server 64 core có thể scan hàng chục GB/giây khi dữ liệu không nén nóng trên page cache.
4. SharedMergeTree — Storage-compute separation đúng nghĩa
Mô hình ReplicatedMergeTree kinh điển có một vấn đề mà mọi team vận hành đều từng đụng phải: khi thêm một replica mới vào cluster, replica đó phải fetch toàn bộ part từ các replica khác — với bảng 50TB, có thể mất ngày. Hơn nữa, mỗi thao tác trên part (merge, mutation) đòi hỏi phối hợp qua ZooKeeper/Keeper — số lượng znode tăng tuyến tính với số part, dễ đụng giới hạn ensemble.
SharedMergeTree giải quyết căn cơ bằng cách tách hẳn storage khỏi compute. Toàn bộ part đều nằm trên object storage (S3, GCS, Azure Blob, MinIO). Mọi replica cùng đọc/ghi vào cùng một tập part. Metadata được lưu ở một nơi nhẹ (Keeper hoặc distributed metadata service), chỉ còn những đoạn ngắn như "danh sách part active", "lock merge" chứ không phải từng chunk file.
graph TB
subgraph Compute["Compute layer (stateless)"]
R1["Replica 1"]
R2["Replica 2"]
R3["Replica 3"]
end
subgraph Meta["Metadata (Keeper)"]
META["Active parts, merge locks"]
end
subgraph Storage["Object storage"]
S3["S3 / GCS / ABS"]
end
R1 --- META
R2 --- META
R3 --- META
R1 --- S3
R2 --- S3
R3 --- S3
CLIENT["Client / Load balancer"] --> R1
CLIENT --> R2
CLIENT --> R3
| Đặc điểm | ReplicatedMergeTree | SharedMergeTree |
|---|---|---|
| Part lưu ở đâu | Đĩa cục bộ mỗi replica | Object storage chung |
| Thêm replica mới | Cần fetch toàn bộ part (giờ tới ngày) | Sẵn sàng gần như tức thời |
| Tải lên Keeper | Tỉ lệ thuận với số part | Chỉ metadata mỏng |
| Storage cost | Nhân với số replica | Một bản duy nhất trên S3 |
| Read latency nóng | Đĩa NVMe cục bộ — thấp nhất | Cần cache trên SSD/RAM |
| Use case mạnh | Self-host bare-metal ổ NVMe | Cloud-native, auto-scale, multi-tenant |
Điểm trade-off duy nhất của SharedMergeTree là cold read — nếu page cache hoặc local disk cache không giữ part, mọi đọc sẽ đi ra S3 với latency 10-50ms. Cluster 25.x giải quyết bằng persistent local cache (filesystem cache) có thể cấu hình kích thước theo GB và chính sách eviction LRU-K. Với workload dashboard, recent data nằm cache nóng là đủ; chỉ query lịch sử cũ mới phải đi S3 — đây gần như đúng 100% use case thực tế.
5. Parallel Replicas — Query scale-out tuyến tính
Trong mô hình phân shard truyền thống, một query SELECT count(*) FROM events WHERE event_time > now() - INTERVAL 1 DAY được phân xuống mỗi shard, mỗi shard chỉ dùng một replica. Nếu có 10 shard và mỗi shard có 3 replica, thực chất 20 máy chỉ ngồi chờ. Parallel Replicas đổi cuộc chơi: query được chia nhỏ theo set of granules, mỗi replica trong cùng shard tự nhận một phần công việc độc lập. Thời gian giảm gần tuyến tính theo số replica tham gia.
-- Bật parallel replicas cho phiên làm việc
SET allow_experimental_parallel_reading_from_replicas = 2;
SET max_parallel_replicas = 6;
SET parallel_replicas_for_non_replicated_merge_tree = 1;
-- Truy vấn trên bảng nghìn tỷ dòng
SELECT
toStartOfHour(event_time) AS hour,
event_name,
count() AS cnt
FROM events_distributed
WHERE event_time BETWEEN now() - INTERVAL 7 DAY AND now()
GROUP BY hour, event_name
ORDER BY hour DESC, cnt DESC
LIMIT 100;Cần chú ý vài điều kiện để Parallel Replicas hiệu quả: bảng đủ lớn (range scan đáng chia nhỏ), mạng giữa replica băng thông cao, và có một coordinator chịu tổng hợp trung gian. Với query nhỏ (đọc vài triệu hàng), overhead phối hợp lại lớn hơn lợi ích — ClickHouse 25.x tự động rơi về mô hình cũ khi ước lượng granule nhỏ hơn ngưỡng (parallel_replicas_min_number_of_granules_to_enable).
Parallel Replicas và consistency
Vì mỗi replica có thể ở thời điểm replication khác nhau vài giây, kết quả Parallel Replicas đôi khi khác nhau vài trăm hàng so với replica chậm nhất. Với analytics là chấp nhận được, nhưng nếu đang chạy báo cáo tài chính phải chính xác tuyệt đối, hãy dùng single replica hoặc bật max_replica_delay_for_distributed_queries phù hợp.
6. Projection, Materialized View và Refreshable MV — Ba công cụ tăng tốc query
Khi primary key ORDER BY không phục vụ được mọi truy vấn, ClickHouse cho ba công cụ rất khác nhau để tăng tốc. Nhiều team dùng cả ba trong cùng một cluster — hiểu trade-off giúp chọn đúng:
| Công cụ | Bản chất | Khi nào dùng | Nhược điểm |
|---|---|---|---|
| Projection | Một bản "view" lưu kèm part gốc, có ORDER BY hoặc GROUP BY riêng | Tăng tốc query có filter trên cột khác với primary key | Tăng write cost, không cross-partition |
| Materialized View | Trigger mỗi lần INSERT vào bảng nguồn, ghi vào bảng đích | Pre-aggregate realtime (roll-up theo phút, giờ, ngày) | Logic pipeline chặt, không backfill dễ |
| Refreshable MV | Chạy SELECT theo lịch, ghi đè bảng đích | Report nặng, join phức tạp, không cần realtime | Không incremental, có độ trễ |
6.1 Projection — tầng index thứ hai ngay trong part
ALTER TABLE events ADD PROJECTION by_country (
SELECT
country_code,
toStartOfHour(event_time) AS hour,
count() AS cnt,
uniqState(user_id) AS users
GROUP BY country_code, hour
);
ALTER TABLE events MATERIALIZE PROJECTION by_country;Khi query SELECT country_code, sum(cnt) FROM events WHERE country_code = 'VN' GROUP BY country_code chạy, optimizer tự chọn projection by_country thay vì đọc từ bảng gốc. Người dùng không cần biết projection tồn tại — một đặc điểm cực hay cho team data muốn tối ưu mà không ép developer viết lại SQL.
6.2 Materialized View — Pre-aggregate realtime với AggregatingMergeTree
CREATE TABLE events_hourly
(
project_id UInt32,
hour DateTime,
event_name LowCardinality(String),
cnt AggregateFunction(count),
users AggregateFunction(uniq, UInt64)
)
ENGINE = AggregatingMergeTree
ORDER BY (project_id, hour, event_name);
CREATE MATERIALIZED VIEW events_hourly_mv
TO events_hourly
AS SELECT
project_id,
toStartOfHour(event_time) AS hour,
event_name,
countState() AS cnt,
uniqState(user_id) AS users
FROM events
GROUP BY project_id, hour, event_name;
-- Query tổng hợp cực rẻ nhờ bảng roll-up
SELECT
event_name,
sumMerge(cnt) AS total,
uniqMerge(users) AS uu
FROM events_hourly
WHERE project_id = 42
AND hour >= now() - INTERVAL 7 DAY
GROUP BY event_name
ORDER BY total DESC
LIMIT 20;AggregatingMergeTree cùng họ hàm *State/*Merge là công cụ mạnh nhất của ClickHouse. Hàm uniqState không lưu list user thật mà chỉ lưu HyperLogLog state (~64KB cho độ chính xác xấp xỉ 0.5%). Merge state sau đó đúng về toán học. Tương tự, quantilesState dùng t-digest, topKState dùng Count-min, argMinState/argMaxState cho pivoting — toàn bộ là sketch toán học có tính kết hợp.
7. Tiered Storage, TTL và quản lý lifecycle dữ liệu
Một bảng log production điển hình tăng 1-10TB mỗi ngày. Không ai giữ mọi thứ mãi mãi, nhưng xoá thủ công thì nguy hiểm. ClickHouse cung cấp cơ chế TTL và tiered storage nằm ngay trong DDL — mỗi part có thể tự di chuyển giữa các đĩa theo tuổi, hoặc tự roll-up, hoặc tự xoá.
CREATE TABLE logs
(
ts DateTime,
level LowCardinality(String),
service LowCardinality(String),
message String,
trace_id String
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(ts)
ORDER BY (service, ts)
TTL
ts + INTERVAL 7 DAY TO VOLUME 'warm',
ts + INTERVAL 30 DAY TO VOLUME 'cold',
ts + INTERVAL 180 DAY DELETE,
ts + INTERVAL 1 DAY GROUP BY service, toStartOfHour(ts)
SET message = any(message), trace_id = any(trace_id)
SETTINGS storage_policy = 'hot_warm_cold';Chính sách trên vừa chạy log aggregation sau một ngày, vừa move part sang volume warm (SSD rẻ hơn) sau 7 ngày, sang cold (S3) sau 30 ngày, và delete sau 6 tháng. Tất cả đều do background thread đảm nhận, không block query. Storage policy khai báo trong config.xml:
<storage_configuration>
<disks>
<hot><path>/var/lib/clickhouse/hot/</path></hot>
<warm><path>/var/lib/clickhouse/warm/</path></warm>
<cold>
<type>s3</type>
<endpoint>https://s3.ap-southeast-1.amazonaws.com/my-ch-cold/</endpoint>
<access_key_id>...</access_key_id>
<secret_access_key>...</secret_access_key>
<metadata_path>/var/lib/clickhouse/disks/cold/</metadata_path>
</cold>
</disks>
<policies>
<hot_warm_cold>
<volumes>
<hot><disk>hot</disk></hot>
<warm><disk>warm</disk></warm>
<cold><disk>cold</disk></cold>
</volumes>
</hot_warm_cold>
</policies>
</storage_configuration>8. Ingestion patterns — Async insert, Kafka engine, Native protocol
Sai lầm phổ biến nhất khi mới dùng ClickHouse là INSERT mỗi event thành một request riêng. Mỗi request đẻ một part — cluster sẽ "too many parts" trong vài phút. Ba mô hình ingestion đúng trong sản xuất 2026:
8.1 Batch insert phía client
Đơn giản nhất: gom tại ứng dụng thành batch 10,000 – 200,000 hàng rồi insert một lần. Dùng native protocol (port 9000) thay vì HTTP nếu cần throughput tối đa. Thư viện chính thống của .NET (ClickHouse.Client, ClickHouse.Driver), Node.js (@clickhouse/client), Go (clickhouse-go/v2) đều hỗ trợ bulk.
8.2 Async insert — gom ngay tại server
-- Client gửi INSERT bình thường
INSERT INTO events VALUES (...);
-- Server gộp các INSERT trong window (mặc định 200ms/1MB)
SET async_insert = 1;
SET wait_for_async_insert = 1;Async insert cực hữu dụng khi hàng nghìn microservice cùng gửi INSERT không kiểm soát được kích thước batch phía client. ClickHouse buffer trong RAM, flush theo ngưỡng thời gian hoặc kích thước. Kết hợp wait_for_async_insert = 0 (fire-and-forget) giảm hẳn latency của producer nhưng chấp nhận mất một vài batch nếu server crash trước khi flush — đủ an toàn cho logs nhưng không hợp cho dữ liệu tài chính.
8.3 Kafka Engine và Refreshable MV — đường ống nội tại
CREATE TABLE events_kafka
(
raw String
)
ENGINE = Kafka
SETTINGS
kafka_broker_list = 'broker-1:9092,broker-2:9092',
kafka_topic_list = 'events',
kafka_group_name = 'ch_consumer',
kafka_format = 'JSONAsString',
kafka_max_block_size = 65536;
CREATE MATERIALIZED VIEW events_mv TO events AS
SELECT
JSONExtractUInt(raw, 'user_id') AS user_id,
parseDateTime64BestEffort(JSONExtractString(raw, 'ts')) AS event_time,
JSONExtractString(raw, 'event_name') AS event_name,
...
FROM events_kafka;Kafka Engine trên 25.x có KIP-848 consumer group mới (cooperative sticky), giảm rebalance pause từ vài giây xuống vài trăm ms — hữu ích cho cluster đang scale hàng ngày. Một số team tách hẳn tầng Kafka→ClickHouse bằng Vector hoặc Redpanda Connect để không ràng buộc lifecycle consumer vào ClickHouse, đổi lại thêm thành phần phải vận hành.
9. Query tuning — Sparse index, skip index và các anti-pattern thường gặp
9.1 EXPLAIN indexes — luôn bắt đầu từ đây
EXPLAIN indexes = 1
SELECT count() FROM events
WHERE project_id = 42
AND event_name = 'page_view'
AND event_time >= now() - INTERVAL 1 DAY;
-- Output (rút gọn):
-- MergeTreeThread
-- Indexes:
-- PrimaryKey
-- Keys: project_id, event_name, event_time
-- Condition: and((project_id = 42), (event_name = 'page_view'))
-- Parts: 42/1200
-- Granules: 1834/2400000Con số "Parts: 42/1200" nghĩa là ClickHouse chỉ đọc 42 part trong tổng 1200 — phần còn lại bị skip nhờ partition pruning. "Granules: 1834/2400000" cho thấy primary index thưa đã loại 99.92% dữ liệu. Nếu tỉ lệ này cao (đọc gần full table), là dấu hiệu cần xem lại ORDER BY hoặc thêm skip index.
9.2 Skip indexes — bổ sung khi ORDER BY không đủ
ALTER TABLE events
ADD INDEX idx_country country_code TYPE set(50) GRANULARITY 4,
ADD INDEX idx_props_vip properties['vip'] TYPE bloom_filter(0.01) GRANULARITY 4,
ADD INDEX idx_ip_minmax ip TYPE minmax GRANULARITY 8;
ALTER TABLE events MATERIALIZE INDEX idx_country;Bốn loại skip index hay dùng: minmax (số, thời gian), set(N) (cột cardinality thấp – trung), bloom_filter (cột high-cardinality cần equality), ngrambf/tokenbf (search like). Skip index không thay thế primary key mà chỉ tinh chỉnh sau cùng — luôn đặt cột selective nhất vào ORDER BY trước.
9.3 Bốn anti-pattern giết cluster nhanh nhất
Những điều tuyệt đối không làm
- INSERT từng dòng một qua HTTP. Luôn gom batch hoặc bật async_insert.
- Partition theo giờ hoặc cao hơn nữa cho bảng lớn. Một tháng partition đẻ ra 720 thư mục, mỗi cái có part + part index — merge sẽ không kịp.
- ALTER DELETE/UPDATE trên bảng lớn như cách OLTP làm. Mutation trong ClickHouse rewrite part — với 50TB bảng nghĩa là viết lại 50TB. Dùng
Lightweight DELETE(25.x ổn định) hoặc Replacing/Collapsing engine cho use case xoá/thay thế. - SELECT * trên dashboard tool. Mỗi cột không cần là một khối dữ liệu bị đọc thừa. Luôn liệt kê cột.
10. So sánh với thế giới OLAP 2026 — StarRocks, Doris, Druid, Pinot, DuckDB
| Engine | Mô hình | Điểm mạnh | Điểm yếu so với ClickHouse | Fit nhất |
|---|---|---|---|---|
| StarRocks 3.x | MPP + materialized view mạnh, native Iceberg | Join phức tạp nhanh hơn, có pipeline executor | Cộng đồng nhỏ hơn, ít sẵn có ở SaaS | Data warehouse đa nguồn, join nhiều bảng lớn |
| Apache Doris 2.x | MPP fork từ Palo, tương thích MySQL protocol | Quản trị quen thuộc với team MySQL/OLTP | Cộng đồng quốc tế còn mỏng, feature parity chậm hơn | Team đã có hệ MySQL, muốn báo cáo realtime |
| Apache Druid | Time-series OLAP với data server nhiều loại role | Streaming ingestion cực khoẻ, time filter rất tinh | Vận hành phức tạp (nhiều service), SQL hạn chế hơn | Time-series, session, clickstream ngắn |
| Apache Pinot | User-facing analytics, star-tree index | Latency p99 ổn định cho query đơn giản | Ít mềm dẻo với query ad-hoc | Dashboard hướng người dùng cuối, QPS cao |
| DuckDB 1.x | Embedded OLAP trong process | Zero ops, chạy local, Parquet native | Không cluster, không concurrent write | Analytics local, data science, edge |
| Snowflake / BigQuery | Managed cloud DWH, storage-compute tách | Zero ops, SQL đầy đủ nhất | Cost/query cao, không ingest < 1s | Enterprise BI, báo cáo định kỳ, không realtime |
Không có "đánh bại toàn diện" ở đây. Quy tắc kinh nghiệm: nếu workload là append-only + filter + aggregate với throughput cao và latency sub-second, ClickHouse đúng 90% trường hợp. Nếu phải join 10 bảng lớn thường xuyên, cân nhắc StarRocks. Nếu chỉ cần phân tích trong một process, DuckDB tiện hơn nhiều. Chọn đúng công cụ đầu tiên tiết kiệm sáu tháng vật vã hơn bất kỳ buổi tuning nào.
11. Ba use case production điển hình — cách tổ chức bảng
11.1 Observability — Logs, Metrics, Traces
Mô hình OpenTelemetry + ClickHouse đang là lựa chọn mặc định của các công ty quy mô trung thay vì Elasticsearch + Prometheus + Jaeger. Bảng log dùng LowCardinality cho service/level, String cho message (compress ZSTD thường đạt 20-40×), và Array/Map cho labels. Bảng trace dùng trace_id String + skip index bloom filter cho tìm kiếm theo id. Mô hình này có sẵn trong OpenTelemetry Collector (exporter clickhouse) và SigNoz, HyperDX, Highlight.io hay chDB-based là các sản phẩm open-source triển khai trên nó.
11.2 Product analytics — Sự kiện user và funnel
PostHog, Plausible, Tinybird, June, Datafold, Mixpanel đều xây trên ClickHouse. Bảng events chính giữ mọi sự kiện với Map cho properties. Phân tích funnel, retention, A/B test dùng các hàm chuyên biệt như windowFunnel, retention, sequenceMatch — đây là những hàm cực khó mô phỏng trên Postgres hay MySQL với cùng hiệu năng.
-- Funnel 3 bước: view -> add_to_cart -> purchase trong 1 giờ
SELECT
level,
count() AS users
FROM (
SELECT
user_id,
windowFunnel(3600)(
event_time,
event_name = 'view',
event_name = 'add_to_cart',
event_name = 'purchase'
) AS level
FROM events
WHERE event_time >= today() - 7
GROUP BY user_id
)
GROUP BY level
ORDER BY level DESC;11.3 Realtime user-facing dashboard
Các SaaS tài chính, game analytics, marketing attribution trả về dashboard cho end user — mỗi user nhìn một chart của riêng họ, latency phải dưới 1 giây cho dù bảng nghìn tỷ dòng. Mẫu chung: partition theo ngày, ORDER BY (tenant_id, metric_time) để query per-tenant là prefix scan, materialized view roll-up xuống phút/giờ, query dashboard chạm MV chứ không phải raw. Với cluster 25.x + Parallel Replicas, có thể đạt p99 dưới 300ms cho query aggregate 30 ngày.
12. Checklist production — Những thứ không được quên khi go-live
Trước khi mở cluster cho traffic thật
- Đặt primary key dựa trên truy vấn, không phải dựa trên "id". Primary key của OLTP khác OLAP.
- Cấu hình memory limit:
max_memory_usage,max_server_memory_usage,max_bytes_before_external_group_byđể query lớn rớt về đĩa thay vì OOM kill server. - Bật query log và query_thread_log — kỷ luật tuning không thể thiếu log.
- Backup: dùng
BACKUP/RESTOREnative 25.x xuống S3, schedule theo ngày, giữ 30 bản. - Giám sát với ClickHouse Keeper ensemble 3/5 node, không bao giờ chạy một Keeper.
- TLS + user quota + row policy: ClickHouse có đầy đủ RBAC, row-level, column-level policy — dùng đi.
- Giới hạn mỗi user bằng quota (số byte đọc, số query/phút) để không bị một team query nặng sập cả cluster.
- Metric tối thiểu: parts per partition, merge queue, replication lag, distributed delay, memory usage per query — xuất sang Prometheus qua
prometheus_endpoint. - Upgrade strategy: đi từng minor, test trên staging có replay production query thật, đừng nhảy lớn.
- ClickHouse Cloud hay self-host? — tính TCO ở mốc 100TB, managed thường đắt gấp 3-5× self-host bare-metal nhưng đổi lại team nhỏ gọn. Với team dưới 3 DBA, managed hầu như luôn là quyết định đúng.
13. Lời kết — ClickHouse là công cụ, không phải viên đạn bạc
ClickHouse đã đi đúng một chặng đường mà rất ít dự án open-source làm được: từ "công cụ nội bộ của một công ty Nga" trở thành hạ tầng analytics chuẩn mực của ngành, cạnh tranh sòng phẳng với Snowflake và BigQuery ở những nơi cần realtime. Năm 2026, với SharedMergeTree ổn định, Parallel Replicas stable, Lightweight DELETE hoàn thiện, và tích hợp sẵn Iceberg/Delta, ClickHouse đang đẩy chính nó vào vùng "data platform đầy đủ" chứ không chỉ là "fast analytics DB".
Nhưng công cụ nào cũng có giới hạn. ClickHouse không phải OLTP, không làm ACID đa bảng, không làm distributed transaction. Đưa nó vào đúng vai trò — append-heavy, aggregate-heavy, time-range filter — thì hiệu năng và cost/performance gần như không đối thủ. Ép nó thay Postgres cho hệ thống đơn hàng thì sẽ có những đêm không ngủ. Kiến trúc sư giỏi là người biết chỗ nào dùng OLTP, chỗ nào dùng OLAP, chỗ nào dùng cache, chỗ nào dùng search engine — và biết đường nối giữa chúng qua CDC, Kafka, hoặc dual-write đúng cách. ClickHouse là một mảnh ghép mạnh mẽ trong bộ công cụ đó, nhưng chỉ là một mảnh ghép.
Nếu bạn đang chuẩn bị lựa chọn OLAP engine cho team của mình năm 2026, hãy bắt đầu bằng benchmark trên workload thật — không phải TPC-H cho có — và đánh giá chi phí vận hành dài hạn chứ không chỉ giá hàng tháng. Càng có dữ liệu thực tế sớm, quyết định càng vững.
14. Nguồn tham khảo
- ClickHouse Official Documentation
- ClickHouse Release 25.x — SharedMergeTree, Lightweight DELETE
- MergeTree Engine Family Deep Dive
- SharedMergeTree Architecture (ClickHouse Cloud)
- Parallel Replicas Configuration
- Query Parallelism Best Practices
- OpenTelemetry ClickHouse Exporter
- PostHog on Building Product Analytics with ClickHouse
- Tinybird — Materialized Views in Practice
- ClickHouse Source on GitHub
Thiết kế Notification System 2026 - Fanout, Priority Queue, Idempotency và Template Engine cho triệu push/email/SMS mỗi ngày
Microsoft Orleans 9 trên .NET 10 - Virtual Actors, Distributed Grains và Kiến trúc Stateful Cloud-Native cho Game, IoT và AI Agent
Disclaimer: The opinions expressed in this blog are solely my own and do not reflect the views or opinions of my employer or any affiliated organizations. The content provided is for informational and educational purposes only and should not be taken as professional advice. While I strive to provide accurate and up-to-date information, I make no warranties or guarantees about the completeness, reliability, or accuracy of the content. Readers are encouraged to verify the information and seek independent advice as needed. I disclaim any liability for decisions or actions taken based on the content of this blog.