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
Phần 1
Mục tiêu: Hiểu ClickHouse là gì và loại tác vụ mà ClickHouse phục vụ
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
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.
Mục tiêu: Nhận diện điểm nghẽn khi dữ liệu phân tích tăng quy mô
100 triệu request/ngày → 3 tỷ bản ghi / 30 ngày
Request hôm nay? API chậm nhất? P95?
Lỗi 5xx? Lưu lượng theo giờ?
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).
Mục tiêu: Giới thiệu case study api_logs — xuyên suốt bài học
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.
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ương | Quay lại case study |
|---|---|
| Lưu trữ theo cột | Vì sao api_logs phù hợp lưu trữ theo cột? |
| ORDER BY | Thiết kế api_logs theo truy vấn dashboard |
| Tổng hợp | P95 độ trễ, tỷ lệ lỗi |
| MV / pipeline | Dữ liệu thô → 1m/1h → Grafana |
| Distributed | 50M/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.
Mục tiêu: Phân biệt OLTP và OLAP — nền tảng để hiểu vị trí của ClickHouse
INSERT / UPDATE / DELETE
Transaction, consistency
Truy vấn nhỏ, đọc ít dòng
Update thường xuyên
Ví dụ: PostgreSQL, MySQL
SELECT * FROM users WHERE id = 123;
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
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.
Mục tiêu: Phá bỏ sai lầm phổ biến — ClickHouse không thay PostgreSQL
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
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.
Mục tiêu: 7 yếu tố tạo nên tốc độ ClickHouse
| # | Yếu tố | Vai trò |
|---|---|---|
| 1 | Lưu theo cột | Chỉ đọc cột cần — giảm I/O |
| 2 | Nén dữ liệu | Nén theo cột — ít dung lượng đĩa |
| 3 | Dữ liệu sắp xếp | Dữ liệu liên quan nằm gần nhau |
| 4 | Chỉ mục thưa | Bỏ qua khối không liên quan |
| 5 | Xử lý vector | Lô + SIMD |
| 6 | Song song | Nhiều lõi CPU |
| 7 | Tổng hợp trước | Truy 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
Mục tiêu: Cách ClickHouse lưu trữ khác database truyền thống
Mỗi dòng nằm liền kề trên đĩa:
| Dòng | Dữ liệu vật lý |
|---|---|
| 1 | 1, An, 25, VN, 1000 |
| 2 | 2, Bình, 30, VN, 1500 |
| 3 | 3, John, 27, US, 2500 |
Tốt khi đọc toàn bộ dòng
Mỗi cột được lưu riêng biệt:
| Cột | Dữ liệu vật lý |
|---|---|
country | VN, VN, US |
salary | 1000, 1500, 2500 |
age | 25, 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.
Mục tiêu: Hiểu tại sao lưu theo cột phù hợp cho 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.
users — 100 cột, 1 tỷ dòng — truy vấn chỉ cần 2 cộtSELECT country, avg(salary) FROM users GROUP BY country;
id
name
age
country ✓
salary ✓
… 95 cột khác
Đọc tất cả 100 cột ≈ 100 GB
98% dữ liệu lãng phí
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.
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
| 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,JP → 3×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.
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
Xử lý hàng nghìn giá trị cùng lúc theo vector/block — không phải từng dòng.
for row in rows:
sum += row.value
# overhead vòng lặp/hàm
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.
Mục tiêu: Hiểu cách ClickHouse xử lý truy vấn song song
Gộp kết quả từng phầ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ả.
ClickHouse Course — Phần 3
Mục tiêu: Hiểu kiến trúc tổng quan của ClickHouse server
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
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.
Mục tiêu: Hiểu ENGINE và các engine chính trong ClickHouse
Engine nền tảng — ghi part và gộp trong nền.
Dedup khi merge; dùng FINAL khi cần kết quả tại thời điểm truy vấn.
Tự tổng hợp số khi merge.
Lưu trạng thái tổng hợp — dùng với MV.
Điều phối truy vấn trên cụm — gửi xuống shard, gộp kết quả.
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, đọc và merge.
Mục tiêu: MergeTree — engine cốt lõi và nguyên lý hoạt động
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.
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
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
Đọ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
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.
Mục tiêu: Các cơ chế update/delete hiện đại — không phải “append-only tuyệt đối”
| Nhu cầu | Cơ chế | Ghi chú |
|---|---|---|
| Bulk update | ALTER TABLE … UPDATE (mutation) | Chạy nền, không realtime |
| Upsert / CDC | ReplacingMergeTree + version | Dedup khi merge; FINAL/argMax() khi cần ngay |
| Sửa lẻ thường xuyên | Lightweight update | UPDATE 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ện | Lightweight 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.
Mục tiêu: Hiểu vì sao batch insert quan trọng với kiến trúc MergeTree
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
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ề
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).
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.3Từ 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.
SET async_insert = 1;
SET wait_for_async_insert = 1;
INSERT INTO api_logs FORMAT JSONEachRow
'{...}';
Gom lô client — app gom rồi INSERT
async_insert — client gửi lẻ, server đệm
Vector / Kafka — hàng đợi, backpressure
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ẹ.
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 ClickHouseORDER BY khi query
Không ảnh hưởng cách lưu disk
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
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.
Mục tiêu: ORDER BY sắp xếp vật lý dữ liệu trên đĩa
ORDER BY (timestamp, api)| timestamp | api | status |
|---|---|---|
| 10:00 | /auth | 200 |
| 10:00 | /payment | 200 |
| 10:00 | /user | 200 |
| 10:01 | /auth | 200 |
| 10:01 | /payment | 500 |
timestamp trước → trong cùng timestamp mới sắp xếp api
Bỏ qua granule ngoài khoảng timestamp
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.
Mục tiêu: Phân biệt đúng nghĩa PRIMARY KEY trong OLTP và ClickHouse
| 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?"
Mục tiêu: Hiểu cách ClickHouse bỏ qua dữ liệu không cần thiết
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
PostgreSQL: B-tree → vị trí từng dòng
ClickHouse: Chỉ mục thưa → granule (~8K dò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.
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ệuXóa partition thay vì scan từng row
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.
Lọc thời gian → bỏ partition không liên quan
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.
Mục tiêu: Phân biệt rõ vai trò của PARTITION BY và ORDER BY
PARTITION BY vs ORDER BYHai khái niệm khác nhau — chi tiết PARTITION BY ở slide trước.
PARTITION BY | ORDER BY | |
|---|---|---|
| Vai trò | Chia partition | Thứ tự vật lý trong part |
| Hữu ích | DROP PARTITION, pruning | Tốc độ truy vấn, skip index |
| TTL | Cơ chế riêng | — |
| Sai lầm | Quá nhiều partition nhỏ | Cột cardinality cao đầu key |
api_logs — case studyENGINE = 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ữ.
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
| Cột | Thường dùng |
|---|---|
status_code | Int64 |
duration_ms | Int64 |
country | String |
timestamp | String |
| Cột | Nên dùng | B |
|---|---|---|
status_code | UInt16 | 2 |
duration_ms | UInt32 | 4 |
country | LowCardinality | dict |
timestamp | DateTime64(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 (4×). 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.
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.
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
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.
Mục tiêu: Hiểu đánh đổi khi dùng Nullable và JSON trong ClickHouse
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ể.
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.
Phần 5
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
api_logs| Cột | Kiểu | Mô tả |
|---|---|---|
timestamp | DateTime64(3) | Thời điểm request (ms) |
service | LowCardinality(String) | Dịch vụ backend |
client_id | LowCardinality(String) | Client gọi API |
api | LowCardinality(String) | Route chuẩn hóa |
method | LowCardinality(String) | GET / POST / … |
status_code | UInt16 | HTTP status |
duration_ms | UInt32 | Độ trễ (ms) |
country | LowCardinality(String) | Quốc gia |
trace_id | String | ID 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.
Mục tiêu: Truy vấn đếm và lọc theo thời gian
Cùng pattern count() — thu hẹp phạm vi đọc bằng WHERE
SELECT count()
FROM api_logs
WHERE timestamp >= today();
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.
Mục tiêu: GROUP BY cơ bản để phân tích traffic
Cùng pattern — chỉ đổi dimension để nhóm
SELECT api, count() AS requests
FROM api_logs GROUP BY api
ORDER BY requests DESC LIMIT 10;
SELECT country, count() AS requests
FROM api_logs GROUP BY country
ORDER BY requests DESC LIMIT 10;
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.
Mục tiêu: Các phép tổng hợp cơ bản trong ClickHouse
avg()
Trung bình
min()
Nhỏ nhất
max()
Lớn nhất
sum()
Tổng
count()
Đếm
SELECT api, avg(duration_ms) AS avg_latency
FROM api_logs
GROUP BY 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.
Mục tiêu: Hiểu phân vị (P50, P95, P99) và cách dùng trong giám sát
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ệ
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 |
|---|---|---|
| P50 | 120ms | Trung vị — độ trễ điển hình |
| P95 | 300ms | 95% request không vượt quá |
| P99 | 900ms | 99% 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.
Mục tiêu: Tổng hợp dữ liệu theo khoảng thời gian cho dashboard
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.
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
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.
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.
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
| 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 |
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.
Mục tiêu: Mô hình dữ liệu thô → tổng hợp và chọn bảng cho dashboard
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 gian | Bảng truy vấn |
|---|---|
| Điều tra chi tiết / drill-down ngắn | api_logs (thô) |
| Vài giờ → vài ngày | api_metrics_1m |
| Nhiều ngày → vài tháng | api_metrics_1h |
| Tháng → năm | api_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.
Mục tiêu: Hiểu SummingMergeTree — engine tự động tổng hợp dữ liệu cộng dồn
SummingMergeTreeDữ 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
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.
Mục tiêu: Materialized View — ghi tổng hợp vào bảng đích khi INSERT
Chi phí tính toán: lúc truy vấn → lúc ghi
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_mv → api_metrics_1m → Grafana
MV lưu lượng + tổng độ trễ theo API/phút. P95/P99 cần AggregatingMergeTree.
Mục tiêu: Hiểu cách AggregatingMergeTree lưu và gộp trạng thái trung gian
| Hàm lưu trạng thái | Hàm gộp | Mục đích |
|---|---|---|
avgState | avgMerge | Trung bình |
uniqState | uniqMerge | Đếm giá trị khác nhau |
quantileState | quantileMerge | Phâ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.
Mục tiêu: Hiểu kiến trúc luồng ghi log thực tế với ClickHouse
Tách app khỏi DB · lọc/thử lại · 10k log → 1 INSERT
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
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
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
api_logsApp → 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.
ClickHouse Course
Mục tiêu: Thay đổi tư duy khi tối ưu truy vấn ClickHouse
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ó index không?"
Tìm đúng dòng nhanh
Tra cứu chính xác
"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ả
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
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.
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.
Mục tiêu: Lightweight projection — thứ tự sắp xếp phụ
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.
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.
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ướcPREWHERESELECT 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
PREWHERESELECT 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.
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ấnEXPLAIN indexes = 1EXPLAIN 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 PIPELINEEXPLAIN 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.
Mục tiêu: Sử dụng system.query_log để tối ưu truy vấn thực tế
system.query_logSELECT 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_ms | Thời gian chạy |
read_rows | Số dòng đọc |
read_bytes | Dung lượng đọc |
memory_usage | RAM 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.
Mục tiêu: Sử dụng system.parts để giám sát lưu trữ và phát hiện vấn đề
system.partsPart dữ liệu đang hoạt động · quá nhiều → tăng kích thước mỗi lần ghi
Tổng dòng mỗi bảng · so sánh kỳ vọng
bytes_on_disk — dung lượng trên đĩa; so data_uncompressed_bytes / data_compressed_bytes để đo tỷ lệ nén
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)
Mục tiêu: Nhận diện các lỗi thường gặp khi truy vấn ClickHouse
Đọc mọi cột — mất lợi ích column storage
Luôn chỉ chọn cột cần thiết
Hash table lớn → tốn RAM/CPU
LIMIT sau GROUP BY không giảm chi phí grouping
Có thể vẫn xử lý phần lớn dữ liệu — kiểm tra EXPLAIN
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?
Sharding · Replication · Cluster
Mục tiêu: Biết khi nào cần chuyển từ một máy chủ sang 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ạnh | Ví dụ cấu hình: 32–64 lõi CPU · 128 GB RAM · NVMe |
| ② | Tối ưu schema | Kiểu dữ liệu, ORDER BY, LowCardinality |
| ③ | Tối ưu truy vấn | PREWHERE, chỉ mục bỏ qua, tổng hợp |
| ④ | Nâng cấp phần cứng | Thêm CPU, RAM, chuyển NVMe |
| ⑤ | Mở rộng cluster | Khi 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ủ.
Mục tiêu: Hiểu cách ClickHouse phân mảnh dữ liệu qua sharding
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
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
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 = 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
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.
Mục tiêu: Phân biệt sharding (scale) và replication (HA)
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
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.
Mục tiêu: Hiểu cách Distributed Table quản lý truy vấn xuyên cluster
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.
Mục tiêu: Sao chép dữ liệu trong cluster
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ộ.
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
ClickHouse Course
Mục tiêu: Phân biệt khi nào dùng ClickHouse vs PostgreSQL
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, FK | PostgreSQL | Users, API keys, billing |
| Tổng hợp tỷ dòng log | ClickHouse | api_logs P95, tỷ lệ lỗi |
| Full-text search phức tạp | Elasticsearch / OpenSearch | Tìm kiếm log message (Phần 9) |
| Báo cáo SQL trên kho dữ liệu | Snowflake / BigQuery | BI 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.
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
Đánh giá định tính theo ClickHouse 26.3 — không phải benchmark số
| Loại tác vụ | Elasticsearch / OpenSearch | ClickHouse 26.3 |
|---|---|---|
| Xếp hạng / fuzzy search | Rất mạnh | Không phải trọng tâm |
| Lọc token / multi-token | Rất mạnh | Mạnh — text index GA (26.2+) |
| Phrase search theo vị trí | Rất mạnh | Chưa có (26.3) |
| Lọc + tổng hợp lượng lớn | Mạnh | Rất mạnh |
| Phân tích bằng SQL | Không phải trọng tâm | Rấ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.
Mục tiêu: So sánh ClickHouse OSS, ClickHouse Cloud và Cloud Data Warehouse phổ biến
| 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.
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ụ
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
Mục tiêu: Schema đề xuất cho bảng api_logs trong case study
api_logs| Cột | Kiểu | Ghi chú |
|---|---|---|
timestamp | DateTime64(3) | Thời điểm request (ms) |
service | LowCardinality(String) | Dịch vụ backend |
client_id | LowCardinality(String) | Client gọi API |
api | LowCardinality(String) | Route chuẩn hóa |
method | LowCardinality(String) | GET / POST / … |
status_code | UInt16 | HTTP status |
duration_ms | UInt32 | Độ trễ (ms) |
country | LowCardinality(String) | Quốc gia |
trace_id | String | ID trace phân tán |
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.
Mục tiêu: Các truy vấn chính phục vụ dashboard api_logs
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.
Mục tiêu: Thiết kế luồng tổng hợp trước cho dashboard nhanh
Điều tra chi tiết, gỡ lỗi
Drill-down vài giờ
Dashboard realtime, cảnh báo SLO
Vài giờ → vài 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
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í
7–30 ngày
Chi tiết từng request — gỡ lỗi / điều tra lỗi
6–12 tháng
Dashboard thời gian thực
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.
ALTER TABLE api_logs
MODIFY TTL timestamp
+ INTERVAL 30 DAY;
ALTER TABLE api_logs
DROP PARTITION '202601';
Phần 11
Mục tiêu: Checklist khi thiết kế bảng ClickHouse
Dữ liệu chủ yếu ghi thêm hay cập nhật?
Event log → append + TTL. CDC → ReplacingMergeTree.
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).
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.
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.
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.
Mục tiêu: Checklist khi truy vấn chậm hoặc cần tối ưu hiệu năng
Truy vấn đọc bao nhiêu dòng/dung lượng?
Xem read_rows, read_bytes trong system.query_log
Có đọc cột thừa không?
Chỉ SELECT cột cần thiết, tránh SELECT *
Loại bỏ phân vùng hoạt động chưa?
Filter theo partition key → partition pruning; sorting key giúp skip granule
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ả
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
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ể.
Tổng kết: 7 điều quan trọng nhất cần ghi 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á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