Nhập môn
ClickHouse

Từ lưu trữ theo cột → MergeTree → phân tích dữ liệu thực chiến

OLAP · Phân tích dữ liệu theo cột

Bối cảnh & tư duy nền tảng

Bối cảnh và tư duy nền tảng

Mục tiêu: Hiểu ClickHouse là gì và loại tác vụ mà ClickHouse phục vụ

ClickHouse là gì?

Định nghĩa

  • Column-oriented DBMS — hệ quản trị cơ sở dữ liệu hướng cột

  • Thuộc nhóm OLAP — Online Analytical Processing

  • Tối ưu quét, lọc và tổng hợp dữ liệu trên hàng triệu → hàng tỷ dòng

Loại tác vụ phục vụ

Dashboard Log analytics Event analytics Báo cáo Time-series

ClickHouse phục vụ phân tích dữ liệu (OLAP) — quét và tổng hợp trên bộ dữ liệu lớn.

Bối cảnh và tư duy nền tảng

Mục tiêu: Nhận diện điểm nghẽn khi dữ liệu phân tích tăng quy mô

Vấn đề ClickHouse giải quyết

100 triệu request/ngày → 3 tỷ bản ghi / 30 ngày

Dashboard cần trả lời

  • Request hôm nay? API chậm nhất? P95?

  • Lỗi 5xx? Lưu lượng theo giờ?

Khi dữ liệu tăng

  • GROUP BY / COUNT nặng dần

  • Quét hàng triệu dòng — OLTP không đủ nhanh

Điểm chính: ClickHouse cho phân tích trên dữ liệu rất lớn (hàng tỷ bản ghi).

Bối cảnh và tư duy nền tảng

Mục tiêu: Giới thiệu case study api_logs — xuyên suốt bài học

Case study: Nền tảng giám sát API

Bài toán

50M request/ngày · 20 dịch vụ · Dashboard: lưu lượng, P95/P99, tỷ lệ lỗi, các API/quốc gia có lưu lượng cao nhất

PostgreSQL xử lý OLTP — nhưng GROUP BY trên tỷ dòng log thì chậm.

ClickHouse giải quyết thế nào?

Lưu trữ theo cột → ít đọc · ORDER BY → bỏ qua dữ liệu · MergeTree + ghi theo lô · MV → dashboard · mở rộng cụm khi cần

ChươngQuay lại case study
Lưu trữ theo cộtVì sao api_logs phù hợp lưu trữ theo cột?
ORDER BYThiết kế api_logs theo truy vấn dashboard
Tổng hợpP95 độ trễ, tỷ lệ lỗi
MV / pipelineDữ liệu thô → 1m/1h → Grafana
Distributed50M/ngày → 500M/ngày

Một câu chuyện duy nhất: mỗi chương trả lời một mảnh — cuối bài ghép thành hệ thống giám sát API hoàn chỉnh.

Bối cảnh và tư duy nền tảng

Mục tiêu: Phân biệt OLTP và OLAP — nền tảng để hiểu vị trí của ClickHouse

OLTP vs OLAP

OLTP — Online Transaction Processing

  • INSERT / UPDATE / DELETE

  • Transaction, consistency

  • Truy vấn nhỏ, đọc ít dòng

  • Update thường xuyên

  • Ví dụ: PostgreSQL, MySQL

Ví dụ query OLTP
SELECT * FROM users WHERE id = 123;

OLAP — Online Analytical Processing

  • Scan + Aggregate

  • Đọc nhiều dòng, nhưng chỉ một số cột

  • GROUP BY, COUNT, AVG, P95

  • Dữ liệu chủ yếu ghi thêm, ít cập nhật

  • Ví dụ: ClickHouse

Ví dụ query OLAP
SELECT country, count()
FROM api_logs
GROUP BY country;

ClickHouse nằm ở phía OLAP — tối ưu quét, lọc, tổng hợp hàng tỷ bản ghi.

Bối cảnh và tư duy nền tảng

Mục tiêu: Phá bỏ sai lầm phổ biến — ClickHouse không thay PostgreSQL

ClickHouse không phải "PostgreSQL nhưng nhanh hơn"

Sai lầm: "ClickHouse là PostgreSQL nhưng nhanh hơn."Không phải.

PostgreSQL OLTP ClickHouse OLAP
Vai trò CRUD, transaction — dữ liệu nghiệp vụ Quét + tổng hợp — dữ liệu phân tích
Bảng ví dụ users · orders · payments api_logs · order_events · metrics

Kiến trúc phổ biến

App PostgreSQL ETL / CDC ClickHouse Grafana

Kết luận: Nhiều hệ thống không cần chọn một trong hai: PostgreSQL phục vụ giao dịch, còn ClickHouse phục vụ phân tích.

Vì sao ClickHouse nhanh?

Mục tiêu: 7 yếu tố tạo nên tốc độ ClickHouse

Công thức tốc độ của ClickHouse

# Yếu tố Vai trò
1Lưu theo cộtChỉ đọc cột cần — giảm I/O
2Nén dữ liệuNén theo cột — ít dung lượng đĩa
3Dữ liệu sắp xếpDữ liệu liên quan nằm gần nhau
4Chỉ mục thưaBỏ qua khối không liên quan
5Xử lý vectorLô + SIMD
6Song songNhiều lõi CPU
7Tổng hợp trướcTruy vấn nhẹ hơn

Công thức: Đọc ít × nén tốt × bỏ qua vùng thừa × vector + song song → phân tích cực nhanh

Vì sao ClickHouse nhanh?

Mục tiêu: Cách ClickHouse lưu trữ khác database truyền thống

Lưu theo dòng vs lưu theo cột

Lưu theo dòng (PostgreSQL, MySQL)

Mỗi dòng nằm liền kề trên đĩa:

DòngDữ liệu vật lý
11, An, 25, VN, 1000
22, Bình, 30, VN, 1500
33, John, 27, US, 2500

Tốt khi đọc toàn bộ dòng

Lưu theo cột (ClickHouse)

Mỗi cột được lưu riêng biệt:

CộtDữ liệu vật lý
countryVN, VN, US
salary1000, 1500, 2500
age25, 30, 27

Tốt khi đọc ít cột trên nhiều dòng

Case study: SELECT api, quantile(0.95)(duration_ms) … chỉ đọc 2 cột trên tỷ dòng.

Vì sao ClickHouse nhanh?

Mục tiêu: Hiểu tại sao lưu theo cột phù hợp cho phân tích

Vì sao lưu theo cột hợp phân tích?

Ví dụ giản lược: giả sử mỗi cột có kích thước tương đương — truy vấn chỉ cần 2 trong 100 cột.

Bảng users — 100 cột, 1 tỷ dòng — truy vấn chỉ cần 2 cột
SELECT country, avg(salary) FROM users GROUP BY country;
id name age country ✓ salary ✓ … 95 cột khác

Lưu theo dòng

  • Đọc tất cả 100 cột ≈ 100 GB

  • 98% dữ liệu lãng phí

Lưu theo cột

  • Chỉ country + salary ≈ 2 GB

  • 50× tiết kiệm I/O và RAM

Điểm chính: Phân tích thường chỉ đọc vài cột — lưu theo cột tránh đọc dữ liệu thừa.

Vì sao ClickHouse nhanh?

Mục tiêu: Hiểu vì sao lưu theo cột giúp ClickHouse nén dữ liệu cực tốt

Nén dữ liệu trong ClickHouse

Khía cạnh Lưu theo cột Lưu theo dòng
Nguyên lý Cùng kiểu dữ liệu → nằm cạnh nhau → nén tốt Xen kẽ nhiều kiểu → khó nén
Ví dụ VN,VN,VN,US,US,JP3×VN, 2×US, 1×JP 1,An,25,VN,1000 → Int + String xen kẽ
Log / sự kiện Dữ liệu lặp lại nhiều → lưu theo cột nén 3–10× (1 TB thô → ~100–333 GB)

Điểm chính: Các giá trị cùng kiểu, thường lặp lại hoặc tương quan → codec/encoding (LZ4, ZSTD, dictionary…) khai thác redundancy tốt hơn.

Vì sao ClickHouse nhanh?

Mục tiêu: Hiểu cách ClickHouse xử lý dữ liệu theo block để tận dụng tối đa phần cứng

Thực thi vector hóa (Vectorized Execution)

Xử lý hàng nghìn giá trị cùng lúc theo vector/block — không phải từng dòng.

Từng dòng (truyền thống)
for row in rows:
    sum += row.value
# overhead vòng lặp/hàm
Theo khối (ClickHouse)
process_vector(column_chunk)
# batch/lần · SIMD · cache locality

Dữ liệu theo cột

Vector / Block

CPU + SIMD

Kết quả

Điểm chính: Thực thi vector hóa tăng tính cục bộ dữ liệu, giảm chi phí trên từng giá trị và tận dụng SIMD.

Vì sao ClickHouse nhanh?

Mục tiêu: Hiểu cách ClickHouse xử lý truy vấn song song

Xử lý song song

Ví dụ: 16 lõi CPU xử lý đồng thời

T1khối 1
T2khối 2
T3khối 3
T4khối 4
T5khối 5
T6khối 6
T7khối 7
T8khối 8
T9khối 9
T10khối 10
T11khối 11
T12khối 12
T13khối 13
T14khối 14
T15khối 15
T16khối 16

Gộp kết quả từng phần

Ví dụ truy vấn
SELECT country, count()
FROM api_logs
GROUP BY country;
  • Tận dụng nhiều lõi CPU (ví dụ 16) đồng thời

  • Dữ liệu chia thành khối riêng biệt

  • Kết quả từng phần được gộp cuối cùng

Phân tích rất dễ song song hóa — tận dụng nhiều lõi CPU (tùy max_threads), chia khối và gộp kết quả.

Kiến trúc lưu trữ cốt lõi

ClickHouse Course — Phần 3

Kiến trúc lưu trữ cốt lõi

Mục tiêu: Hiểu kiến trúc tổng quan của ClickHouse server

Kiến trúc tổng quan ClickHouse

Tầng kết nối / client

clickhouse-client · HTTP · JDBC · Python · Java

Xử lý truy vấn

SQL Parser → Planner → Executor

Storage Engine

MergeTree · ReplacingMergeTree · Distributed

Đĩa / SSD / lưu trữ đối tượng

Client kết nối

  • clickhouse-client — CLI gốc

  • HTTP interface — gửi SQL qua HTTP

  • JDBC / Java — Ứng dụng JVM

  • Python — clickhouse-connect (official)

  • Grafana — Dashboard theo thời gian thực

  • Metabase / Superset — công cụ BI

HTTP interface là cách đơn giản nhất để bắt đầu — không cần cài driver.

Kiến trúc lưu trữ cốt lõi

Mục tiêu: Hiểu ENGINE và các engine chính trong ClickHouse

Table Engine là gì?

MergeTree

Engine nền tảng — ghi part và gộp trong nền.

ReplacingMergeTree

Dedup khi merge; dùng FINAL khi cần kết quả tại thời điểm truy vấn.

SummingMergeTree

Tự tổng hợp số khi merge.

AggregatingMergeTree

Lưu trạng thái tổng hợp — dùng với MV.

Distributed

Điều phối truy vấn trên cụm — gửi xuống shard, gộp kết quả.

ENGINE khi tạo bảng
CREATE TABLE api_logs_local (
  timestamp   DateTime64(3),
  service     LowCardinality(String),
  api         LowCardinality(String),
  method      LowCardinality(String),
  status_code UInt16,
  duration_ms UInt32,
  client_id   LowCardinality(String),
  country     LowCardinality(String),
  trace_id    String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (timestamp, api);

ENGINE quyết định cách dữ liệu lưu, ghi, đọcmerge.

Kiến trúc lưu trữ cốt lõi

Mục tiêu: MergeTree — engine cốt lõi và nguyên lý hoạt động

MergeTree: engine quan trọng nhất

  • Part dữ liệu: Dữ liệu mới được ghi thành các part; nhiều part nhỏ sau đó được merge trong nền.

  • Gộp nền: Part nhỏ gom thành part lớn hơn.

  • Engine nền tảng: Hầu hết bảng triển khai dùng MergeTree.

Đây là lý do tên engine là "Merge" + "Tree".

      flowchart TD
          I1[INSERT 1] --> P1[Part 1]
          I2[INSERT 2] --> P2[Part 2]
          I3[INSERT 3] --> P3[Part 3]
          P1 --> MG[Gộp nền]
          P2 --> MG
          P3 --> MG
          MG --> PF[Part lớn hơn]
                  

Một INSERT chạm nhiều partition có thể tạo part cho từng partition.

Kéo để pan · Cuộn để zoom

Điểm chính: MergeTree phân mảnh dữ liệu rồi gom nền — tối ưu ghi và đọc phân tích.

Kiến trúc lưu trữ cốt lõi

Mục tiêu: Hiểu tại sao part dữ liệu gần như bất biến và điều này ảnh hưởng thế nào đến đọc/ghi

Part dữ liệu và lưu trữ bất biến

Chu kỳ dữ liệu

Insert → Tạo part mới

Cập nhật → Mutation ghi lại các part/cột bị ảnh hưởng

Xóa → Mutation / lightweight delete

Gộp → Gom part nền → part mới

Lợi ích

  • Đọc nhanh — ít bị tranh chấp với ghi

  • Ghi tuần tự — chỉ ghi thêm

  • Nén tốt — dữ liệu cùng kiểu liền kề

  • Đồng thời tốt — part bất biến giúp giảm nhu cầu khóa dữ liệu kiểu row-store

Đánh đổi

  • Không lý tưởng cho cập nhật / xóa liên tục

  • Không phù hợp CRUD giao dịch

Điểm chính: Part dữ liệu sau khi tạo là bất biến — ClickHouse tối ưu cho đọc và ghi tuần tự, không phải sửa đổi thường xuyên.

Kiến trúc lưu trữ cốt lõi

Mục tiêu: Các cơ chế update/delete hiện đại — không phải “append-only tuyệt đối”

Update & delete trong ClickHouse

Nhu cầuCơ chếGhi chú
Bulk updateALTER TABLE … UPDATE (mutation)Chạy nền, không realtime
Upsert / CDCReplacingMergeTree + versionDedup khi merge; FINAL/argMax() khi cần ngay
Sửa lẻ thường xuyênLightweight updateUPDATE có mục tiêu mà không cần rewrite toàn bộ cột; vẫn không biến ClickHouse thành OLTP
Xóa theo điều kiệnLightweight delete / mutationƯu tiên TTL / DROP PARTITION cho log

Triết lý vẫn là append-first — nhưng ClickHouse 2026 có đủ công cụ khi nghiệp vụ bắt buộc sửa/xóa. Log analytics (api_logs) vẫn nên append + TTL.

Kiến trúc lưu trữ cốt lõi

Mục tiêu: Hiểu vì sao batch insert quan trọng với kiến trúc MergeTree

Vì sao nên insert theo batch?

Insert từng row

  • Mỗi request → một INSERT riêng

  • Nếu dùng sync insert / async_insert=0: 100K INSERT nhỏ → quá nhiều part

  • Quá tải metadata, áp lực merge nền cao

  • CPU & disk I/O lãng phí cho merge liên tục

Batch insert

  • Gom 100K bản ghi thành vài lô lớn

  • Tạo ít parts, mỗi part nhiều dữ liệu

  • Metadata nhẹ, merge nhanh và ít tốn tài nguyên

  • Compression tốt hơn — dữ liệu đồng dạng liền kề

Kiến trúc khuyên dùng — thêm tầng đệm

App

hiện tại

Buffer

Vector / Kafka / App batch

ClickHouse

ít lần ghi, nhiều dữ liệu

Nguyên tắc: ClickHouse thích ít lần ghi, mỗi lần nhiều dữ liệu — gom lô client, async_insert (26.3+), hoặc tầng đệm (Vector/Kafka).

Kiến trúc lưu trữ cốt lõi

Mục tiêu: async_insert và khi nào vẫn cần tầng đệm bên ngoài

async_insert — bật mặc định từ ClickHouse 26.3

Bộ đệm phía máy chủ

Từ 26.3 LTS, async_insert bật mặc định — server gom các lần ghi nhỏ rồi flush thành lô lớn.

Gom lô client: ≥1K dòng/INSERT; lý tưởng 10K–100K.

Bật / kiểm tra
SET async_insert = 1;
SET wait_for_async_insert = 1;

INSERT INTO api_logs FORMAT JSONEachRow
  '{...}';

Ba đường ghi

  • Gom lô client — app gom rồi INSERT

  • async_insert — client gửi lẻ, server đệm

  • Vector / Kafka — hàng đợi, backpressure

Vector / Kafka khi nào?

Khi cần hàng đợi bền vững, định tuyến, biến đổi — tách nguồn khỏi ClickHouse.

Điểm chính: Chọn gom lô client, async_insert, hoặc luồng xử lý tùy độ phức tạp. api_logs có thể nhận log qua Vector hoặc agent nhẹ.

Thiết kế bảng: điểm sống còn

Mục tiêu: Hiểu ORDER BY không chỉ là sắp xếp kết quả mà là quyết định vật lý dữ liệu

ORDER BY trong ClickHouse

PostgreSQL

  • ORDER BY khi query

  • Không ảnh hưởng cách lưu disk

ClickHouse (MergeTree)

  • ORDER BY khi tạo bảng

  • Sắp xếp vật lý trong từng part

  • Ảnh hưởng lượng dữ liệu phải đọc

Ví dụ: tạo bảng với ORDER BY
CREATE TABLE api_logs (
  timestamp   DateTime64(3),
  service     LowCardinality(String),
  api         LowCardinality(String),
  method      LowCardinality(String),
  status_code UInt16,
  duration_ms UInt32,
  client_id   LowCardinality(String),
  country     LowCardinality(String),
  trace_id    String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (timestamp, api);

Điểm chính: ORDER BY = thiết kế schema khi tạo bảng — chọn sai = mất hiệu năng.

Thiết kế bảng: điểm sống còn

Mục tiêu: ORDER BY sắp xếp vật lý dữ liệu trên đĩa

Ví dụ ORDER BY (timestamp, api)

timestampapistatus
10:00/auth200
10:00/payment200
10:00/user200
10:01/auth200
10:01/payment500

timestamp trước → trong cùng timestamp mới sắp xếp api

Bỏ qua granule ngoài khoảng timestamp

Query dashboard
SELECT count() AS requests
FROM api_logs
WHERE timestamp >= today()
  AND api = '/payment';

Ý chính: Cột đầu ORDER BY khớp điều kiện lọc chính → bỏ qua nhiều granule không cần đọc.

Thiết kế bảng: điểm sống còn

Mục tiêu: Phân biệt đúng nghĩa PRIMARY KEY trong OLTP và ClickHouse

Primary Key không giống PostgreSQL

PostgreSQL ClickHouse
Ví dụ PRIMARY KEY (id) PRIMARY KEY (timestamp, api)
Giá trị Duy nhất — mỗi giá trị 1 lần Cho phép trùng — bình thường
Vai trò Ràng buộc — DB tự kiểm tra Chỉ mục thưa — 1 granule ≈ 8.192 dòng; primary index lưu mark cho mỗi granule
Mục đích Tra cứu đúng 1 dòng Bỏ qua khối dữ liệu không cần đọc
vs ORDER BY Nếu khai báo riêng, PRIMARY KEY phải là prefix của ORDER BY. Không khai báo → ClickHouse dùng sorting key làm primary key (mặc định 1 granule ≈ 8.192 dòng).

Cách nghĩ: OLTP hỏi "tìm đúng 1 dòng" — ClickHouse hỏi "bỏ được bao nhiêu dữ liệu không cần đọc?"

Thiết kế bảng: điểm sống còn

Mục tiêu: Hiểu cách ClickHouse bỏ qua dữ liệu không cần thiết

Chỉ mục thưa và khối dữ liệu

      flowchart LR
          IDX[Primary Index] --> G1[Khối 1]
          IDX --> G2[Khối 2]
          IDX --> G3[Khối 3]
          G1 --> M1[granule ~8192 dòng]
          G2 --> M2[granule ~8192 dòng]
          G3 --> M3[granule ~8192 dòng]
                  

Kéo để pan · Cuộn để zoom

ClickHouse vs PostgreSQL

  • PostgreSQL: B-tree → vị trí từng dòng

  • ClickHouse: Chỉ mục thưa → granule (~8K dòng)

Tại sao quan trọng?

  • Bỏ qua khối không liên quan

  • Primary index nhỏ so với dữ liệu gốc

WHERE timestamp >= '10:30' — nếu timestamp là cột dẫn đầu sorting key → chỉ đọc granule từ mark tương ứng.

Thiết kế bảng: điểm sống còn

Mục tiêu: Hiểu cách PARTITION BY chia dữ liệu và khi nào nên dùng

PARTITION BY — Chia vùng dữ liệu

Xóa dữ liệu cũ

Xóa partition thay vì scan từng row

TTL và vòng đời dữ liệu

TTL đánh dấu dữ liệu đủ điều kiện hết hạn; xóa vật lý diễn ra trong merge nền. Partition hỗ trợ xóa theo tháng.

Loại bỏ phân vùng không liên quan

Lọc thời gian → bỏ partition không liên quan

Sao lưu và di chuyển

Sao chép / chuyển cả partition

api_logs — partition & quản lý
-- api_logs đã có PARTITION BY toYYYYMM(timestamp)

ALTER TABLE api_logs
  DROP PARTITION '202601';

Lưu ý: PARTITION BY để quản lý dữ liệu, không phải chỉ mục chính — ORDER BY ảnh hưởng rất lớn đến lượng dữ liệu phải đọc.

Thiết kế bảng: điểm sống còn

Mục tiêu: Phân biệt rõ vai trò của PARTITION BY và ORDER BY

PARTITION BY vs ORDER BY

Hai khái niệm khác nhau — chi tiết PARTITION BY ở slide trước.

PARTITION BYORDER BY
Vai tròChia partitionThứ tự vật lý trong part
Hữu íchDROP PARTITION, pruningTốc độ truy vấn, skip index
TTLCơ chế riêng
Sai lầmQuá nhiều partition nhỏCột cardinality cao đầu key
api_logs — case study
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)   -- quản lý theo tháng
ORDER BY (timestamp, api);         -- tối ưu truy vấn theo thời gian

Quy tắc: ORDER BY → truy vấn nhanh/chậm. PARTITION BY → thao tác/xóa phân vùng; TTL cho thời gian lưu giữ.

Thiết kế bảng

Mục tiêu: Hiểu tại sao chọn đúng kiểu dữ liệu giúp tối ưu dung lượng và hiệu năng

Chọn kiểu dữ liệu hợp lý

Schema chưa tối ưu khi chuyển từ OLTP

CộtThường dùng
status_codeInt64
duration_msInt64
countryString
timestampString

Cách nghĩ ClickHouse

CộtNên dùngB
status_codeUInt162
duration_msUInt324
countryLowCardinalitydict
timestampDateTime64(3)8

Điểm chính: Kiểu nhỏ → dung lượng nhỏ → bộ nhớ đệm hiệu quả. status_code: Int64 → UInt16 giảm từ 8 byte xuống 2 byte (). Bool giúp schema rõ nghĩa true/false — không phải mẹo giảm kích thước so với UInt8.

Thiết kế bảng: điểm sống còn

Mục tiêu: Sử dụng LowCardinality để tối ưu lưu trữ và truy vấn cho cột có ít giá trị khác nhau.

LowCardinality

Khái niệm

  • Cột có ít giá trị khác nhau

  • Mã hóa từ điển: lưu số thay vì chuỗi lặp lại

  • Ví dụ: VN→1, US→2, JP→3

Phù hợp: country, method, service, browser — giữ status_code là UInt16

Cột enum trên api_logs
-- api_logs đã tạo — tối ưu cột ít giá trị:
service  LowCardinality(String),
country  LowCardinality(String),
method   LowCardinality(String),
api      LowCardinality(String)

Lợi ích: Giảm dung lượng, tăng tốc nén và GROUP BY / lọc trên cột có ít giá trị khác nhau.

Thiết kế bảng: điểm sống còn

Mục tiêu: Hiểu đánh đổi khi dùng Nullable và JSON trong ClickHouse

Nullable và JSON

Nullable

  • Tiện khi dữ liệu có thể null

  • Thêm cột phụ (bitmap null) cho mỗi Nullable

  • Giảm hiệu quả nén, tốn I/O + CPU

Lưu ý: Ưu tiên giá trị mặc định (0, '') thay Nullable khi có thể.

JSON

  • Hữu ích cho dữ liệu bán cấu trúc

  • Phù hợp log, sự kiện, telemetry

  • Truy vấn được trường lồng nhau trực tiếp

  • Không phải lựa chọn mặc định cho mọi trường hợp

Nguyên tắc: Trường truy vấn thường xuyên → tách thành cột riêng rõ ràng.

Điểm chính: Nullable có chi phí hiệu năng; JSON tiện cho dữ liệu bán cấu trúc — với trường ổn định và được truy vấn thường xuyên, tách thành cột riêng thường tối ưu hơn.

Truy vấn phân tích cơ bản

Phần 5

Truy vấn phân tích cơ bản

Mục tiêu: Giới thiệu bộ dữ liệu sẽ dùng xuyên suốt các bài tập

Bộ dữ liệu ví dụ: api_logs

Cột Kiểu Mô tả
timestampDateTime64(3)Thời điểm request (ms)
serviceLowCardinality(String)Dịch vụ backend
client_idLowCardinality(String)Client gọi API
apiLowCardinality(String)Route chuẩn hóa
methodLowCardinality(String)GET / POST / …
status_codeUInt16HTTP status
duration_msUInt32Độ trễ (ms)
countryLowCardinality(String)Quốc gia
trace_idStringID trace phân tán

100M+ bản ghi · 9 cột · MergeTree

PARTITION BY toYYYYMM(timestamp)

ORDER BY (timestamp, api)

Điểm chính: Mô phỏng log API thực tế — mỗi request 1 dòng, phù hợp cho các ví dụ truy vấn phía sau.

Truy vấn phân tích cơ bản

Mục tiêu: Truy vấn đếm và lọc theo thời gian

Đếm & lọc theo thời gian

Cùng pattern count() — thu hẹp phạm vi đọc bằng WHERE

Hôm nay
SELECT count()
FROM api_logs
WHERE timestamp >= today();
Một khoảng cụ thể
SELECT count()
FROM api_logs
WHERE timestamp >= '2024-01-01'
  AND timestamp <  '2024-02-01';

Điểm chính: ClickHouse dùng partition pruning + primary index để loại bỏ phần lớn dữ liệu trước khi scan — sau đó xử lý các granule còn lại theo vector và song song.

Truy vấn phân tích cơ bản

Mục tiêu: GROUP BY cơ bản để phân tích traffic

GROUP BY

Cùng pattern — chỉ đổi dimension để nhóm

Top API theo lưu lượng
SELECT api, count() AS requests
FROM api_logs GROUP BY api
ORDER BY requests DESC LIMIT 10;
Top country theo request
SELECT country, count() AS requests
FROM api_logs GROUP BY country
ORDER BY requests DESC LIMIT 10;
Top client theo request
SELECT client_id, count() AS requests
FROM api_logs GROUP BY client_id
ORDER BY requests DESC LIMIT 10;

GROUP BY + ORDER BY ... DESC LIMIT 10 để lấy các giá trị đứng đầu.

Truy vấn phân tích cơ bản

Mục tiêu: Các phép tổng hợp cơ bản trong ClickHouse

Các phép tổng hợp cơ bản

avg()

Trung bình

min()

Nhỏ nhất

max()

Lớn nhất

sum()

Tổng

count()

Đếm

Độ trễ trung bình theo API
SELECT api, avg(duration_ms) AS avg_latency
FROM api_logs
GROUP BY api;
Độ trễ min / max theo API
SELECT api,
  min(duration_ms) AS min_lat,
  max(duration_ms) AS max_lat
FROM api_logs
GROUP BY api;

Mẹo: ClickHouse chỉ đọc cột cần thiết, xử lý song song trên nhiều lõi CPU.

Truy vấn phân tích cơ bản

Mục tiêu: Hiểu phân vị (P50, P95, P99) và cách dùng trong giám sát

Phân vị: P50, P95, P99

Trung bình ≈ 303 ms với dữ liệu: 20, 21, 20, 22, 19, 20, 2000 ms — bị lệch bởi 1 giá trị ngoại lệ

Cú pháp ClickHouse
SELECT
    api,
    quantile(0.50)(duration_ms) AS p50,
    quantile(0.95)(duration_ms) AS p95,
    quantile(0.99)(duration_ms) AS p99
FROM api_logs
GROUP BY api;
Chỉ sốVí dụÝ nghĩa
P50120msTrung vị — độ trễ điển hình
P95300ms95% request không vượt quá
P99900ms99% request không vượt quá

Nhiều mức: quantiles(0.5, 0.95, 0.99)(duration_ms).

Với SLO về latency, P95/P99 thường hữu ích hơn trung bình để quan sát tail latency. Dashboard api_logs: P95 độ trễ và tỷ lệ lỗi theo API.

Truy vấn phân tích cơ bản

Mục tiêu: Tổng hợp dữ liệu theo khoảng thời gian cho dashboard

Tổng hợp theo thời gian

Lượng request theo phút
SELECT
  toStartOfMinute(timestamp) AS minute,
  count() AS requests
FROM api_logs
GROUP BY minute
ORDER BY minute;
Hàm nhóm thời gian Dùng khi
toStartOfMinute()Dashboard theo thời gian thực
toStartOfHour()Giám sát theo giờ
toStartOfDay()Báo cáo ngày
toStartOfWeek() / toStartOfMonth()Xu hướng dài hạn
toStartOfInterval(t, INTERVAL 5 MINUTE)Khoảng thời gian tùy chỉnh

Mẹo: Dùng toStartOf* để nhóm theo thời gian — kết quả đã được gom theo từng khoảng.

Truy vấn phân tích cơ bản

Mục tiêu: Tính toán có điều kiện — đếm, tổng hợp, trung bình theo bộ lọc

Tổng hợp có điều kiện

So với OLTP: Thay SUM(CASE WHEN …) bằng countIf() — ngắn gọn, rõ nghĩa và là cách viết tự nhiên trong ClickHouse.

Hàm có điều kiện

Tỷ lệ lỗi & độ trễ
SELECT
  count() AS total,
  countIf(status_code BETWEEN 500 AND 599) AS errors,
  round(errors / total, 4) AS error_rate,
  countIf(status_code = 404) AS not_found,
  avgIf(duration_ms, status_code < 400) AS avg_latency
FROM api_logs;

Điểm chính: Tính tỷ lệ lỗi, độ trễ đã lọc… trong 1 truy vấn — thay vì chạy nhiều truy vấn riêng.

Truy vấn phân tích cơ bản

Mục tiêu: Hiểu cách đếm giá trị duy nhất và đánh đổi giữa tốc độ và độ chính xác

Đếm giá trị khác nhau

Hàm Đặc điểm Khi nào dùng
uniq() Ước lượng · nhanh · ít RAM Dashboard, báo cáo hàng ngày
uniqExact() Chính xác · tốn RAM/thời gian hơn Tính phí, quyết toán chính xác
Ví dụ: đếm client duy nhất
SELECT uniq(client_id) FROM api_logs;

Điểm chính: uniq() ước lượng, tiết kiệm bộ nhớ. uniqExact() chính xác khi cần — không gắn một tỷ lệ chính xác cố định.

Thiết kế analytics pipeline

Mục tiêu: Mô hình dữ liệu thô → tổng hợp và chọn bảng cho dashboard

Bảng thô và bảng tổng hợp

Mô hình nhiều tầng

api_logs

Dữ liệu thô — đầy đủ chi tiết

api_metrics_1m

Tổng hợp theo phút

api_metrics_1h

Tổng hợp theo giờ

api_metrics_1d

Tổng hợp theo ngày

Khoảng thời gianBảng truy vấn
Điều tra chi tiết / drill-down ngắnapi_logs (thô)
Vài giờ → vài ngàyapi_metrics_1m
Nhiều ngày → vài thángapi_metrics_1h
Tháng → nămapi_metrics_1d

Khoảng càng rộng → tầng tổng hợp càng cao → quét ít dữ liệu hơn. Ví dụ trên — tùy độ phân giải dashboard và thời gian lưu giữ thực tế. Materialized View tự điền bảng tổng hợp khi ghi vào bảng thô.

Điểm chính: Bảng thô cho điều tra chi tiết; bảng tổng hợp giữ dashboard nhanh dù dữ liệu lớn.

Thiết kế analytics pipeline

Mục tiêu: Hiểu SummingMergeTree — engine tự động tổng hợp dữ liệu cộng dồn

SummingMergeTree

Phù hợp khi nào?

  • Dữ liệu có thể cộng dồn (sum) theo key

  • Ví dụ: số request theo phút, tổng byte, doanh thu

  • Các dòng cùng ORDER BY key → gộp nền thành 1 dòng tổng

Trước merge

3 dòng cùng key

Sau merge

1 dòng = tổng

Bước 1 — Tạo bảng đích
CREATE TABLE api_metrics_1m
(
  minute          DateTime,
  api             LowCardinality(String),
  requests        UInt64,
  total_duration  UInt64
)
ENGINE = SummingMergeTree((requests, total_duration))
ORDER BY (minute, api);

Merge là bất đồng bộ: các dòng chưa chắc đã gộp ngay — truy vấn thường vẫn dùng SUM() / GROUP BY khi cần kết quả đúng tức thời.

Điểm chính: MV tạo tổng hợp cục bộ theo từng block INSERT → SummingMergeTree tiếp tục cộng các dòng cùng key khi merge nền → truy vấn dùng SUM()/GROUP BY để luôn đúng.

Thiết kế analytics pipeline

Mục tiêu: Materialized View — ghi tổng hợp vào bảng đích khi INSERT

Materialized View

Chi phí tính toán: lúc truy vấnlúc ghi

MV ghi vào bảng đích (sau khi tạo api_metrics_1m)
CREATE MATERIALIZED VIEW api_metrics_1m_mv
TO api_metrics_1m
AS SELECT
    toStartOfMinute(timestamp) AS minute,
    api,
    count() AS requests,
    sum(duration_ms) AS total_duration
FROM api_logs
GROUP BY minute, api;

Kích hoạt khi INSERT · không xử lý dữ liệu lịch sử

Luồng: api_logs → INSERT → api_metrics_1m_mvapi_metrics_1m → Grafana

MV lưu lượng + tổng độ trễ theo API/phút. P95/P99 cần AggregatingMergeTree.

Thiết kế analytics pipeline

Mục tiêu: Hiểu cách AggregatingMergeTree lưu và gộp trạng thái trung gian

AggregatingMergeTree

Hàm lưu trạng tháiHàm gộpMục đích
avgStateavgMergeTrung bình
uniqStateuniqMergeĐếm giá trị khác nhau
quantileStatequantileMergePhân vị
      flowchart LR
          Raw[api_logs] -->|MV avgState| M1[metrics_1m]
          M1 -->|MV/query rollup| H1[metrics_1h]
          H1 -->|MV/query rollup| D1[metrics_1d]
                  

Kéo để pan · Cuộn để zoom

Điểm chính: Engine chỉ merge states cùng sorting key — đổi độ phân giải phút/giờ/ngày cần MV hoặc pipeline tầng tiếp theo, không tự xảy ra khi merge.

Thiết kế analytics pipeline

Mục tiêu: Hiểu kiến trúc luồng ghi log thực tế với ClickHouse

Kiến trúc logging thực tế

Tầng bộ đệm

Tách app khỏi DB · lọc/thử lại · 10k log → 1 INSERT

Tầng phân tích

Lưu & tổng hợp · MV thời gian thực · dashboard & API

Điểm chính: Ghi có thể qua gom lô phía client, async_insert (bộ đệm phía máy chủ, mặc định từ 26.3) hoặc Vector/Kafka khi cần hàng đợi, thử lại, định tuyến.

      flowchart LR
          App[Application] -->|logs| Buffer[Vector]
          Buffer -->|batch| CH[(ClickHouse)]
          CH --> GRF[Grafana]
          CH --> API[Analytics]
                  

Kéo để pan · Cuộn để zoom

Thiết kế analytics pipeline

Mục tiêu: Hiểu mô hình dùng hai cơ sở dữ liệu để tách tác vụ giao dịch và tác vụ phân tích

Kiến trúc phân tích dữ liệu nghiệp vụ

      flowchart LR
          App[Application] --> PG[(PostgreSQL OLTP)]
          App -->|logs| CH[(ClickHouse OLAP)]
          PG -->|CDC/ETL| CH
          CH --> Dash[Grafana / BI]
                  

Kéo để pan · Cuộn để zoom

Luồng case study api_logs

  • App → PostgreSQL (users, billing)

  • Logs → ClickHouse (analytics)

  • CDC (tùy chọn) → enrich chiều phân tích

  • Grafana ← MV từ api_logs

Đã giới thiệu ở Phần 1 — đây là kiến trúc end-to-end của case study.

Điểm chính: OLTP và OLAP bổ sung nhau — không thay thế mù quáng.

Tối ưu truy vấn và schema

ClickHouse Course

Tối ưu truy vấn và schema

Mục tiêu: Thay đổi tư duy khi tối ưu truy vấn ClickHouse

Cách nghĩ khi tối ưu

Câu hỏi chính: "Truy vấn này khiến ClickHouse phải đọc bao nhiêu dòng, bao nhiêu dung lượng?"

Cách nghĩ OLTP

  • "Có index không?"

  • Tìm đúng dòng nhanh

  • Tra cứu chính xác

Cách nghĩ ClickHouse

  • "Bỏ qua được bao nhiêu dữ liệu?"

  • Đọc ít dung lượng nhất

  • Tổng hợp nhanh nhất

Mục tiêu: Đọc ít dữ liệu nhất → xử lý phần còn lại nhanh nhất

Ví dụ: 1 tỷ dòng → lọc + nhóm → 200 dòng kết quả

Tối ưu truy vấn và schema

Mục tiêu: Hiểu cách chỉ mục bỏ qua dữ liệu giúp giảm lượng dữ liệu đọc

Chỉ mục bỏ qua dữ liệu

Minh họa: Bỏ qua block

Block A: 200, 201, 204 — Bỏ qua

Block B: 500, 502, 503 — Đọc

Block C: 404, 499 — Bỏ qua

Dùng sau khi thiết kế primary key tốt. WHERE status_code = 500 → chỉ đọc Block B.

Tạo chỉ mục bỏ qua
ALTER TABLE api_logs
ADD INDEX idx_status status_code
  TYPE set(100) GRANULARITY 4;

ALTER TABLE api_logs
ADD INDEX idx_trace trace_id
  TYPE bloom_filter(0.01) GRANULARITY 4;

ALTER TABLE api_logs MATERIALIZE INDEX idx_status;
ALTER TABLE api_logs MATERIALIZE INDEX idx_trace;

MATERIALIZE INDEX cho dữ liệu cũ. Bloom filter cho trace_id.

Điểm chính: Chỉ mục bỏ qua giúp bỏ qua khối không liên quan — giảm dung lượng đọc.

Tối ưu truy vấn và schema

Mục tiêu: Lightweight projection — thứ tự sắp xếp phụ

Lightweight projection

Cách hình dung

Bảng api_logs
 ├─ ORDER BY (timestamp, api)
 └─ proj_by_client
     ORDER BY client_id + _part_offset

Không sao chép toàn bộ hàng — chỉ khóa sắp xếp + offset.

Thêm projection
ALTER TABLE api_logs
  ADD PROJECTION proj_by_client (
    SELECT _part_offset
    ORDER BY client_id, timestamp
  );

ALTER TABLE api_logs
  MATERIALIZE PROJECTION proj_by_client;

Truy vấn theo client_id dùng projection tự động.

Điểm chính: Bổ sung khi một ORDER BY không phục vụ hết các mẫu truy vấn.

Tối ưu truy vấn và schema

Mục tiêu: Hiểu cách PREWHERE tối ưu I/O khi truy vấn trên bảng rộng

PREWHERE — Đọc cột dùng để lọc trước

Không PREWHERE
SELECT api, duration_ms
FROM api_logs
WHERE status_code = 500
  AND timestamp >= today();

Lọc bằng status_code, timestamp

Đọc api, duration_ms trên toàn bộ dòng → I/O nhiều hơn

PREWHERE
SELECT api, duration_ms
FROM api_logs
PREWHERE status_code = 500
  AND timestamp >= today();

Lọc trước bằng status_code, timestamp

Chỉ đọc api, duration_ms trên dòng còn lại → I/O ít hơn

Điểm chính: PREWHERE giảm I/O đĩa — bộ tối ưu có thể tự chuyển điều kiện vào PREWHERE khi phù hợp.

Tối ưu truy vấn và schema

Mục tiêu: Sử dụng EXPLAIN để kiểm tra kế hoạch truy vấn và hiệu quả bỏ qua dữ liệu

EXPLAIN — Xem kế hoạch truy vấn

EXPLAIN indexes = 1
EXPLAIN indexes = 1
SELECT api, duration_ms
FROM api_logs
WHERE api = '/payment'
  AND timestamp >= today();

Kết quả mẫu: Granules 12 / 10.000

Kiểm tra pruning — primary key & skip index có hiệu lực?

EXPLAIN PIPELINE
EXPLAIN PIPELINE
SELECT api, count()
FROM api_logs
WHERE timestamp >= today()
GROUP BY api;

Khi cần: xem luồng xử lý & mức song song

CPU/RAM bị giới hạn? · đối chiếu system.query_log

Điểm chính: indexes=1 kiểm chứng pruning; PIPELINE kiểm chứng cách thực thi — ít granule thường nhanh hơn.

Tối ưu truy vấn và schema

Mục tiêu: Sử dụng system.query_log để tối ưu truy vấn thực tế

system.query_log

Truy vấn chậm gần đây
SELECT query, query_duration_ms,
 read_rows, read_bytes, memory_usage
FROM system.query_log
WHERE type = 'QueryFinish'
  AND event_time >= now() - INTERVAL 1 HOUR
ORDER BY query_duration_ms DESC
LIMIT 20;
CộtÝ nghĩa
query_duration_msThời gian chạy
read_rowsSố dòng đọc
read_bytesDung lượng đọc
memory_usageRAM sử dụng

Đừng chỉ nhìn thời gian — xem read_rows, read_bytes, memory_usage.

0,2s × 800M dòng × 100 lần/phút = rất tốn tài nguyên.

Điểm chính: Dùng số liệu thật — số dòng đọc, dung lượng, RAM — để tối ưu.

Tối ưu truy vấn và schema

Mục tiêu: Sử dụng system.parts để giám sát lưu trữ và phát hiện vấn đề

system.parts

Số parts

Part dữ liệu đang hoạt động · quá nhiều → tăng kích thước mỗi lần ghi

Số dòng

Tổng dòng mỗi bảng · so sánh kỳ vọng

Dung lượng & nén

bytes_on_disk — dung lượng trên đĩa; so data_uncompressed_bytes / data_compressed_bytes để đo tỷ lệ nén

Truy vấn giám sát lưu trữ
SELECT
    database, table,
    count() AS parts,
    sum(rows) AS rows,
    formatReadableSize(sum(bytes_on_disk)) AS disk_size,
    round(sum(data_uncompressed_bytes)
/ sum(data_compressed_bytes), 2) AS compression_ratio
FROM system.parts
WHERE active
GROUP BY database, table;

Ghi chú: Quá nhiều part nhỏ = dấu hiệu cần gom lô ghi hoặc dùng bộ đệm (Vector/Kafka)

Tối ưu truy vấn và schema

Mục tiêu: Nhận diện các lỗi thường gặp khi truy vấn ClickHouse

Truy vấn sai lầm thường gặp

SELECT *

  • Đọc mọi cột — mất lợi ích column storage

  • Luôn chỉ chọn cột cần thiết

GROUP BY cardinality cao

  • Hash table lớn → tốn RAM/CPU

  • LIMIT sau GROUP BY không giảm chi phí grouping

LIMIT không phải ‘thần chú’

  • Có thể vẫn xử lý phần lớn dữ liệu — kiểm tra EXPLAIN

Dashboard scan bảng thô lặp lại

  • Refresh liên tục trên tỷ dòng → dùng MV, tổng hợp phút/giờ

Điểm chính: Luôn đặt câu hỏi: truy vấn đọc bao nhiêu dung lượng? Có thể tổng hợp trước không?

Distributed ClickHouse

Sharding · Replication · Cluster

Distributed ClickHouse

Mục tiêu: Biết khi nào cần chuyển từ một máy chủ sang cluster

Khi nào cần mở rộng cluster?

# Chiến lược mở rộng Ghi chú / Máy chủ mạnh
Bắt đầu với 1 máy chủ mạnhVí dụ cấu hình: 32–64 lõi CPU · 128 GB RAM · NVMe
Tối ưu schemaKiểu dữ liệu, ORDER BY, LowCardinality
Tối ưu truy vấnPREWHERE, chỉ mục bỏ qua, tổng hợp
Nâng cấp phần cứngThêm CPU, RAM, chuyển NVMe
Mở rộng clusterKhi năng lực xử lý, thông lượng hoặc HA yêu cầu; schema/truy vấn tốt giúp giảm số node

Sai lầm: Nhầm "ClickHouse = phải cluster ngay" — hãy tối ưu trước khi thêm máy chủ!

Quy tắc: Với tải công việc vừa và chưa cần HA, một máy mạnh thường là điểm bắt đầu đơn giản. Mở rộng ngang khi năng lực xử lý, HA hoặc thông lượng yêu cầu — tối ưu đọc ít dữ liệu trước khi thêm máy chủ.

Distributed ClickHouse

Mục tiêu: Hiểu cách ClickHouse phân mảnh dữ liệu qua sharding

Sharding

Chia dữ liệu theo shard

  • Shard là tập con logic — có thể có nhiều replica trên mỗi shard

  • Distributed dùng biểu thức phân shard — hàm băm chỉ là cách phổ biến

Tăng năng lực xử lý

  • Truy vấn song song trên shard → tăng thông lượng

  • Thêm shard = thêm dung lượng + CPU (dữ liệu cũ không tự rebalance)

Ví dụ: cityHash64(user_id) — engine Distributed map theo shard; với 3 shard cùng trọng số, mỗi shard ~1/3 dữ liệu.

      flowchart LR
          C[Client] --> R{Router}
          R --> S1[Shard 1]
          R --> S2[Shard 2]
          R --> S3[Shard 3]
          S1 --> M[Merge]
          S2 --> M
          S3 --> M
                  

Kéo để pan · Cuộn để zoom

Distributed ClickHouse

Mục tiêu: Hiểu replication trong ClickHouse — sao chép dữ liệu để đảm bảo khả năng sẵn sàng.

Replication

Nhiều bản sao

  • Replication = sao chép dữ liệu cho cùng một shard

  • Mỗi replica chứa cùng một tập dữ liệu

Khả năng sẵn sàng cao (HA)

  • Nếu replica A lỗi, client/load balancer có thể định tuyến truy vấn sang replica B

  • Replication mặc định bất đồng bộ — các replica có thể tạm thời lệch nhau và sẽ hội tụ dần. insert_quorum có thể yêu cầu dữ liệu được ghi trên nhiều replica trước khi INSERT được xác nhận thành công.

Shard 1

Replica A

data ✗

Lỗi!

Replica B

data ✓

Vẫn hoạt động

Client có thể chuyển sang replica B khi A gặp sự cố

Điểm chính: Replication ≠ Sharding — Sharding chia dữ liệu để mở rộng dung lượng/thông lượng, replication sao chép để HA.

Distributed ClickHouse

Mục tiêu: Phân biệt sharding (scale) và replication (HA)

Sharding vs Replication

Sharding — Scale

  • Chia dữ liệu thành nhiều shard

  • Mỗi shard giữ một phần dữ liệu khác nhau

  • Tăng dung lượng & thông lượng

Replication — HA

  • Sao chép cùng dữ liệu lên nhiều node

  • Mỗi replica = bản sao đầy đủ

  • Tăng khả năng sẵn sàng

3 shards × 2 replicas = 6 nodes

Shard 1

R1

R2

Shard 2

R1

R2

Shard 3

R1

R2

Sharding chia để mở rộng · Replication sao chép để HA · Triển khai thực tế dùng cả hai.

Distributed ClickHouse

Mục tiêu: Hiểu cách Distributed Table quản lý truy vấn xuyên cluster

Distributed Table

Bảng cục bộ + lớp Distributed
CREATE TABLE api_logs_local ( /* 9 cột */ )
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)
ORDER BY (timestamp, api);

CREATE TABLE api_logs_dist AS api_logs_local
ENGINE = Distributed(
    cluster_3shard, default,
    api_logs_local,
    cityHash64(client_id)
);
      flowchart TB
          C[Client] --> D[api_logs_dist]
          D --> S1[Shard 1]
          D --> S2[Shard 2]
          D --> S3[Shard 3]
          S1 --> R[Merge]
          S2 --> R
          S3 --> R
          R --> C
                  

Kéo để pan · Cuộn để zoom

Cần cluster — demo single-node không chạy. Dữ liệu lưu tại api_logs_local trên shard.

Lưu ý: api_logs_dist định tuyến truy vấn; cityHash64(client_id) phân shard theo client.

Distributed ClickHouse

Mục tiêu: Sao chép dữ liệu trong cluster

ReplicatedMergeTree & ClickHouse Keeper

ReplicatedMergeTree

Biến thể của MergeTree có replication — ghi vào một replica; các replica khác fetch/replicate part bất đồng bộ.

ClickHouse Keeper

Dịch vụ điều phối metadata/replication, distributed DDL và leader coordination; dữ liệu part được các replica tự đồng bộ.

      flowchart TB
          K[Keeper] -.-> R1[Replica 1]
          K -.-> R2[Replica 2]
          K -.-> R3[Replica 3]
          note[Replicas fetch parts from peers asynchronously]
          R1 -.-> note
          R2 -.-> note
          R3 -.-> note
                  

Kéo để pan · Cuộn để zoom

So sánh và chọn đúng công cụ

ClickHouse Course

So sánh và chọn đúng công cụ

Mục tiêu: Phân biệt khi nào dùng ClickHouse vs PostgreSQL

Khi nào dùng gì?

Không lặp bảng so sánh đầy đủ — dùng hướng dẫn lựa chọn theo loại tác vụ.

Tình huống Chọn Ví dụ case study
CRUD, transaction, FKPostgreSQLUsers, API keys, billing
Tổng hợp tỷ dòng logClickHouseapi_logs P95, tỷ lệ lỗi
Full-text search phức tạpElasticsearch / OpenSearchTìm kiếm log message (Phần 9)
Báo cáo SQL trên kho dữ liệuSnowflake / BigQueryBI cross-team (Phần 9)

Điểm chính: PostgreSQL + ClickHouse là một kiến trúc phổ biến khi hệ thống cần cả OLTP và phân tích dữ liệu — PG + CH cho api_logs là ví dụ điển hình.

So sánh và chọn đúng công cụ

Mục tiêu: Chọn đúng công cụ cho phân tích log và tìm kiếm văn bản

ClickHouse vs Elasticsearch/OpenSearch

Đánh giá định tính theo ClickHouse 26.3 — không phải benchmark số

Loại tác vụElasticsearch / OpenSearchClickHouse 26.3
Xếp hạng / fuzzy searchRất mạnhKhông phải trọng tâm
Lọc token / multi-tokenRất mạnhMạnh — text index GA (26.2+)
Phrase search theo vị tríRất mạnhChưa có (26.3)
Lọc + tổng hợp lượng lớnMạnhRất mạnh
Phân tích bằng SQLKhông phải trọng tâmRất mạnh

Chọn Elasticsearch khi: tìm kiếm, xếp hạng, fuzzy, hệ sinh thái search

Chọn ClickHouse khi: observability — lọc + tổng hợp (+ token filter cơ bản)

Điểm chính: Phân tích log = lọc + tổng hợp → ClickHouse. Sản phẩm search-first → Elasticsearch.

So sánh và chọn đúng công cụ

Mục tiêu: So sánh ClickHouse OSS, ClickHouse Cloud và Cloud Data Warehouse phổ biến

ClickHouse vs Cloud Data Warehouse

Tiêu chí ClickHouse OSS ClickHouse Cloud BigQuery / Snowflake / Redshift
Vận hành Tự triển khai · linh hoạt tinh chỉnh Dịch vụ được quản lý · storage/compute tách rời Dịch vụ được quản lý · ít vận hành hơn
Độ trễ Thường tốt khi được cấu hình phù hợp Thường tốt cho phân tích thời gian thực Phụ thuộc loại tác vụ
Khả năng co giãn Mở rộng cụm theo thiết kế Compute scale linh hoạt Scale tài nguyên linh hoạt
Chi phí Hạ tầng có thể dự đoán Trả phí theo mức sử dụng Mô hình tính phí tùy dịch vụ: bytes/query, credits/warehouse, node/RPU…

Điểm chính: So sánh cùng mô hình triển khai: Cloud vs DW được quản lý, hoặc OSS tự vận hành.

Case study xuyên suốt

Mục tiêu: Mô tả bài toán thực tế — nền tảng giám sát API đa dịch vụ

Case study: Nền tảng giám sát API

Bối cảnh: Nhiều dịch vụ — mỗi request = 1 dòng log · 300 client · 20 dịch vụ · 50M log/ngày

Dashboard: lưu lượng, độ trễ P95/P99, tỷ lệ lỗi, top API/quốc gia/client · Sau 1 năm: ~18,25 tỷ log ≈ 5,5 TB dữ liệu thô (giả sử ~300 byte/log trước nén)

Mô hình:
Nghiệp vụ: App → PostgreSQL
Log: App → Vector → ClickHouse → Grafana
CDC: PostgreSQL → ClickHouse (tùy chọn)

      flowchart TB
          BE[Backend 20 dịch vụ] --> V[Vector gom lô]
          V --> CH[api_logs]
          CH --> MV[MV 1m / 1h / 1d]
          MV --> GF[Grafana]
                  

Kéo để pan · Cuộn để zoom

Case study xuyên suốt

Mục tiêu: Schema đề xuất cho bảng api_logs trong case study

Schema đề xuất cho api_logs

Cột Kiểu Ghi chú
timestampDateTime64(3)Thời điểm request (ms)
serviceLowCardinality(String)Dịch vụ backend
client_idLowCardinality(String)Client gọi API
apiLowCardinality(String)Route chuẩn hóa
methodLowCardinality(String)GET / POST / …
status_codeUInt16HTTP status
duration_msUInt32Độ trễ (ms)
countryLowCardinality(String)Quốc gia
trace_idStringID trace phân tán

Cấu hình bảng

ENGINE MergeTree

PARTITION BY toYYYYMM(timestamp)

ORDER BY (timestamp, api)

LowCardinality / UInt — giảm dung lượng

Append-only · 9 cột · đơn giản hóa cho bài học.

Case study

Mục tiêu: Các truy vấn chính phục vụ dashboard api_logs

Truy vấn dashboard

Request / phút — lưu lượng theo phút (24h)

SELECT toStartOfMinute(timestamp) AS t, count() AS requests
FROM api_logs
WHERE timestamp >= now() - INTERVAL 1 DAY
GROUP BY t ORDER BY t;

Tỷ lệ lỗi — API có tỷ lệ HTTP 5xx cao nhất

SELECT api, countIf(status_code BETWEEN 500 AND 599) AS errors,
  round(errors / count(), 4) AS error_rate
FROM api_logs WHERE timestamp >= today()
GROUP BY api
HAVING count() >= 100
ORDER BY error_rate DESC;

P95 / P99 — độ trễ phân vị theo API

SELECT api, quantile(0.95)(duration_ms) AS p95,
  quantile(0.99)(duration_ms) AS p99
FROM api_logs WHERE timestamp >= today() GROUP BY api;

API chậm — top 10 độ trễ trung bình

SELECT api, avg(duration_ms) AS avg_ms
FROM api_logs WHERE timestamp >= today()
GROUP BY api ORDER BY avg_ms DESC LIMIT 10;

Top quốc gia — top 10 theo request

SELECT country, count() AS cnt
FROM api_logs WHERE timestamp >= today()
GROUP BY country ORDER BY cnt DESC LIMIT 10;

PARTITION BY toYYYYMM(timestamp) + ORDER BY (timestamp, api) — thu hẹp theo timestamp trước khi aggregate theo api/country.

Case study xuyên suốt

Mục tiêu: Thiết kế luồng tổng hợp trước cho dashboard nhanh

Chiến lược tổng hợp trước

Bảng thô

Điều tra chi tiết, gỡ lỗi

Drill-down vài giờ

Tổng hợp theo phút

Dashboard realtime, cảnh báo SLO

Vài giờ → vài ngày

Theo giờ / ngày

Báo cáo dài hạn, xu hướng

Ngày → tháng → năm

Dùng Materialized View tự động tổng hợp khi ghi → SummingMergeTree / AggregatingMergeTree

Case study xuyên suốt

Mục tiêu: Chiến lược giữ dữ liệu theo tầng — cân bằng truy vấn và chi phí

Chiến lược giữ dữ liệu

Log thô

7–30 ngày

Chi tiết từng request — gỡ lỗi / điều tra lỗi

Tổng hợp (phút/giờ)

6–12 tháng

Dashboard thời gian thực

Tổng hợp (ngày/tháng)

1–3 năm

Báo cáo dài hạn, so sánh theo năm (YoY)

Nguyên tắc: Dữ liệu thô giữ ngắn hạn, dữ liệu tổng hợp giữ dài hạn. TTL đánh dấu dữ liệu đủ điều kiện hết hạn sau 30 ngày — xóa vật lý diễn ra trong merge nền. PARTITION BY toYYYYMM() giúp xóa nguyên tháng nhanh.

TTL — hết hạn sau 30 ngày
ALTER TABLE api_logs
MODIFY TTL timestamp
  + INTERVAL 30 DAY;
DROP PARTITION
ALTER TABLE api_logs
  DROP PARTITION '202601';

Checklist & Tổng kết

Phần 11

Checklist và tổng kết

Mục tiêu: Checklist khi thiết kế bảng ClickHouse

Checklist thiết kế bảng

1

Dữ liệu chủ yếu ghi thêm hay cập nhật?

Event log → append + TTL. CDC → ReplacingMergeTree.

2

Truy vấn & ORDER BY?

Thiết kế ORDER BY từ mẫu lọc phổ biến; ưu tiên bên trái cột giúp loại bỏ nhiều dữ liệu. api_logs: (timestamp, api).

3

Chính sách lưu giữ và PARTITION BY?

PARTITION BY khi cần quản lý vòng đời/xóa theo nhóm; TTL cho thời gian lưu giữ; DROP PARTITION khi xóa nguyên phân vùng.

4

Kiểu dữ liệu và cardinality?

Kiểu nhỏ nhất đủ biểu diễn; DateTime64(N) theo độ chính xác; LowCardinality(String) cho chuỗi ít giá trị khác nhau.

5

MV / projection / TTL?

MV cho dashboard; projection cho mẫu truy vấn thứ hai; TTL giảm chi phí lưu trữ.

Nguyên tắc: Bắt đầu từ truy vấn → điều kiện lọc → ORDER BY / PARTITION BY / kiểu dữ liệu. Áp dụng lại cho api_logs từ đầu bài.

Checklist và tổng kết

Mục tiêu: Checklist khi truy vấn chậm hoặc cần tối ưu hiệu năng

Checklist tối ưu truy vấn

1

Truy vấn đọc bao nhiêu dòng/dung lượng?

Xem read_rows, read_bytes trong system.query_log

2

Có đọc cột thừa không?

Chỉ SELECT cột cần thiết, tránh SELECT *

3

Loại bỏ phân vùng hoạt động chưa?

Filter theo partition key → partition pruning; sorting key giúp skip granule

4

Khóa sắp xếp khớp bộ lọc không?

Lọc trên cột trái của ORDER BY → bỏ qua khối dữ liệu hiệu quả

5

GROUP BY có quá nhiều giá trị khác nhau?

Quá nhiều nhóm → bảng trạng thái tổng hợp/hash table lớn → tốn RAM/CPU — dùng GROUP BY có chọn lọc hoặc tổng hợp trước

6

Có thể tổng hợp trước không?

Materialized View / bảng tổng hợp → đẩy tính toán sang lúc ghi dữ liệu

Nguyên tắc: Đọc ít dữ liệu nhất → xử lý nhanh nhất → tổng hợp trước nếu có thể.

Checklist và tổng kết

Tổng kết: 7 điều quan trọng nhất cần ghi nhớ

Những điều phải nhớ

1. ClickHouse là OLAP — không phải OLTP

2. Lưu trữ theo cột là một trong những nền tảng chính giúp ClickHouse xử lý truy vấn phân tích hiệu quả

3. MergeTree là engine quan trọng nhất

4. ORDER BY ảnh hưởng rất lớn đến lượng dữ liệu ClickHouse phải đọc

5. Tránh nhiều synchronous INSERT nhỏ — gom lô phía client hoặc dùng async_insert

6. Materialized View giúp dashboard theo thời gian thực

7. Không dùng ClickHouse như OLTP database

Công thức tư duy cuối bài

Cách nghĩ PostgreSQL

"Làm sao tìm đúng dòng
nhanh nhất?"

Cách nghĩ ClickHouse

"Làm sao đọc ít dữ liệu nhất,
và xử lý nhanh nhất?"

Công thức

Lưu trữ theo cột + Nén dữ liệu + Chỉ mục thưa + Xử lý theo vector + Song song + Tổng hợp trước

Phân tích cực nhanh