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. 1. Vì sao ClickHouse thống trị analytics production năm 2026
    1. Ba câu hỏi quyết định trước khi đụng vào ClickHouse
  2. 2. Hành trình tiến hoá — từ Yandex 2016 tới Cloud-Native 2026
  3. 3. Kiến trúc cốt lõi — Column store, vectorized execution và MergeTree
    1. 3.1 Part, granule, mark — ba khái niệm phải thuộc lòng
      1. Nguyên tắc thiết kế ORDER BY
    2. 3.2 Vectorized execution — vì sao 1 tỷ hàng/giây
  4. 4. SharedMergeTree — Storage-compute separation đúng nghĩa
  5. 5. Parallel Replicas — Query scale-out tuyến tính
    1. Parallel Replicas và consistency
  6. 6. Projection, Materialized View và Refreshable MV — Ba công cụ tăng tốc query
    1. 6.1 Projection — tầng index thứ hai ngay trong part
    2. 6.2 Materialized View — Pre-aggregate realtime với AggregatingMergeTree
  7. 7. Tiered Storage, TTL và quản lý lifecycle dữ liệu
  8. 8. Ingestion patterns — Async insert, Kafka engine, Native protocol
    1. 8.1 Batch insert phía client
    2. 8.2 Async insert — gom ngay tại server
    3. 8.3 Kafka Engine và Refreshable MV — đường ống nội tại
  9. 9. Query tuning — Sparse index, skip index và các anti-pattern thường gặp
    1. 9.1 EXPLAIN indexes — luôn bắt đầu từ đây
    2. 9.2 Skip indexes — bổ sung khi ORDER BY không đủ
    3. 9.3 Bốn anti-pattern giết cluster nhanh nhất
      1. Những điều tuyệt đối không làm
  10. 10. So sánh với thế giới OLAP 2026 — StarRocks, Doris, Druid, Pinot, DuckDB
  11. 11. Ba use case production điển hình — cách tổ chức bảng
    1. 11.1 Observability — Logs, Metrics, Traces
    2. 11.2 Product analytics — Sự kiện user và funnel
    3. 11.3 Realtime user-facing dashboard
  12. 12. Checklist production — Những thứ không được quên khi go-live
    1. Trước khi mở cluster cho traffic thật
  13. 13. Lời kết — ClickHouse là công cụ, không phải viên đạn bạc
  14. 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 ProjectionMaterialized 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.

1Bhàng/giây scan được trên cluster tầm trung
100×nén trung bình cho log text so với raw
~50msp50 latency query analytic trên bảng nghìn tỷ dòng
25.xnhánh phát triển 2026 với SharedMergeTree stable

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.

2016 — Yandex mã nguồn mở ClickHouse
Dự án nội bộ dành cho Yandex.Metrica bắt đầu từ 2009 được public hoá. Ngay từ bản đầu tiên đã có columnar storage, vectorized execution, LZ4/ZSTD compression và MergeTree engine.
2019 — ReplicatedMergeTree + ZooKeeper
Mô hình replication dựa trên ZooKeeper cho metadata. Đưa ClickHouse từ "single node khổng lồ" sang cluster nhiều shard — trở thành mặc định cho gần một thập kỷ kế tiếp.
2021 — ClickHouse Keeper thay ZooKeeper
Viết lại bằng C++ dùng Raft, tương thích giao thức ZooKeeper. Loại bỏ JVM overhead, đơn giản hoá vận hành và cho phép chạy embedded trong cùng binary.
2022 — ClickHouse Cloud GA
Mô hình storage và compute tách biệt trên S3/GCS/ABS. Lần đầu tiên ClickHouse có thể auto-scale compute mà không phải rebalance shard — tiền đề cho SharedMergeTree.
2023 — SharedMergeTree preview
Engine mới không còn khái niệm replica sở hữu part cục bộ. Mọi replica cùng đọc từ object storage, coordinator nhẹ, không cần ZooKeeper per-part. Chính thức GA trên cloud giữa năm 2024.
2024 — Parallel Replicas stable
Một query được chia nhỏ ra nhiều replica cùng shard để xử lý song song, thay vì chỉ một replica phụ trách một shard. Thời gian query với cluster 10 node giảm tuyến tính theo số replica.
Q4 2025 — ClickHouse 25.x LTS
Hợp nhất Lightweight UPDATE (patch parts), projection dùng vectorized read, query cache cross-node, Json type native kiểu variant, và mở khoá Iceberg/Delta Lake read native như bảng local.
2026 — Chuẩn hoá với hệ sinh thái mở
ClickHouse 25.x ổn định tích hợp Apache Iceberg catalog (REST + AWS Glue), Delta Lake v3, Hudi. Altinity, Tinybird, Double.cloud, ClickHouse Cloud trở thành bốn nhà cung cấp managed lớn. Hệ sinh thái engineering tool (dbt-clickhouse, chDB embedded, clickhouse-local) chín muồi.

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"]
Write-path MergeTree và read-path vectorized trong cùng một bảng

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
SharedMergeTree — replica stateless, dữ liệu đặt trên object storage
Đặc điểmReplicatedMergeTreeSharedMergeTree
Part lưu ở đâuĐĩa cục bộ mỗi replicaObject storage chung
Thêm replica mớiCầ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 KeeperTỉ lệ thuận với số partChỉ metadata mỏng
Storage costNhân với số replicaMột bản duy nhất trên S3
Read latency nóngĐĩa NVMe cục bộ — thấp nhấtCần cache trên SSD/RAM
Use case mạnhSelf-host bare-metal ổ NVMeCloud-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ấtKhi nào dùngNhược điểm
ProjectionMột bản "view" lưu kèm part gốc, có ORDER BY hoặc GROUP BY riêngTăng tốc query có filter trên cột khác với primary keyTăng write cost, không cross-partition
Materialized ViewTrigger mỗi lần INSERT vào bảng nguồn, ghi vào bảng đíchPre-aggregate realtime (roll-up theo phút, giờ, ngày)Logic pipeline chặt, không backfill dễ
Refreshable MVChạy SELECT theo lịch, ghi đè bảng đíchReport nặng, join phức tạp, không cần realtimeKhô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/2400000

Con 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

EngineMô hìnhĐiểm mạnhĐiểm yếu so với ClickHouseFit nhất
StarRocks 3.xMPP + materialized view mạnh, native IcebergJoin phức tạp nhanh hơn, có pipeline executorCộng đồng nhỏ hơn, ít sẵn có ở SaaSData warehouse đa nguồn, join nhiều bảng lớn
Apache Doris 2.xMPP fork từ Palo, tương thích MySQL protocolQuản trị quen thuộc với team MySQL/OLTPCộng đồng quốc tế còn mỏng, feature parity chậm hơnTeam đã có hệ MySQL, muốn báo cáo realtime
Apache DruidTime-series OLAP với data server nhiều loại roleStreaming ingestion cực khoẻ, time filter rất tinhVận hành phức tạp (nhiều service), SQL hạn chế hơnTime-series, session, clickstream ngắn
Apache PinotUser-facing analytics, star-tree indexLatency p99 ổn định cho query đơn giảnÍt mềm dẻo với query ad-hocDashboard hướng người dùng cuối, QPS cao
DuckDB 1.xEmbedded OLAP trong processZero ops, chạy local, Parquet nativeKhông cluster, không concurrent writeAnalytics local, data science, edge
Snowflake / BigQueryManaged cloud DWH, storage-compute táchZero ops, SQL đầy đủ nhấtCost/query cao, không ingest < 1sEnterprise 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/RESTORE native 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