Nhập môn
ClickHouse

Từ column storage → MergeTree → analytics thực chiến

OLAP · Column-oriented analytics

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à workload nó 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 scan, filter, aggregate trên hàng triệu → hàng tỷ dòng

Phục vụ workload

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

ClickHouse phục vụ workload analytics — OLAP scan/aggregate trên dữ liệu lớn.

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

Mục tiêu: Nhận diện pain point khi dữ liệu phân tích tăng quy mô

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

100M requests/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? Traffic 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: API Monitoring Platform

Bài toán

50M request/ngày · 20 services · Dashboard: traffic, P95/P99, error rate, top API/quốc gia

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?

Columnar → ít đọc · ORDER BY → skip · MergeTree + batch · MV → dashboard · Cluster khi scale

ChươngQuay lại case study
Column storageVì sao api_logs hợp columnar?
ORDER BYThiết kế api_logs theo query dashboard
AggregationP95 latency, error rate
MV / pipelineRaw → 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 monitor 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

  • Query nhỏ, đọc ít rows

  • 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 rows, đọc ít columns

  • GROUP BY, COUNT, AVG, P95

  • Data thường append, ít update

  • Ví dụ: ClickHouse

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

ClickHouse nằm ở phía OLAP — tối ưu scan, filter, aggregate hàng tỷ records.

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

Không phải "PostgreSQL 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ụ Scan, aggregate — 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: Không phải PostgreSQL hoặc ClickHouse — mà thường là cả hai.

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?

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–300 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

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
# 1 phép/dòng · cache miss
Theo khối (ClickHouse)
process_vector(column_chunk)
# 1 phép/khối · SIMD + cache hit

Column Data

Dữ liệu từng cột

Vector / Block

Nhiều giá trị/lần

CPU + SIMD

Xử lý song song

Result

Kết quả gộp

Điểm chính: Vectorized execution tận dụng CPU cache và SIMD — nhanh hơn nhiều lần so với row-by-row.

Vì sao ClickHouse nhanh?

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

Xử lý song song

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 events
GROUP BY country;
  • Tận dụng 16 lõi CPU đồ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 16 lõi CPU, 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

Client Layer

clickhouse-client · HTTP API · JDBC · Python · Java

Query Processing

SQL Parser → Planner → Executor

Storage Engine

MergeTree · ReplacingMergeTree · Distributed

Disk / SSD / Object Storage

Client kết nối

  • clickhouse-client — CLI gốc

  • HTTP API — REST interface

  • JDBC / Java — Ứng dụng JVM

  • Python — clickhouse-driver

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

  • Metabase / Superset — BI tools

HTTP API 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 parts, merge nền.

ReplacingMergeTree

Insert version mới; dedup khi merge — dùng FINAL/argMax() khi cần ngay.

SummingMergeTree

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

AggregatingMergeTree

Lưu aggregation state — dùng với MV.

Distributed

Lớp query cluster — gửi task xuống shard, merge kết quả.

ENGINE khi tạo bảng
CREATE TABLE events (
  timestamp  DateTime,
  user_id    UInt64,
  event_type String
)
ENGINE = MergeTree
ORDER BY timestamp;

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: Mỗi lần ghi tạo một part riêng.

  • 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] --> P1[Part 1]
          I1 --> P2[Part 2]
          I1 --> P3[Part 3]
          P1 --> MG[Gộp nền]
          P2 --> MG
          P3 --> MG
          MG --> PF[Part lớn hơn]
                  

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 → Tạo phiên bản dữ liệu mới (mutation)

Xóa → Mutation / lightweight delete

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

Lợi ích

  • Đọc nhanh — không cần khóa

  • 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 — đọc và ghi không xung đột

Đá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 updatePhù hợp sửa ít cột, ít dòng
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 business 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

  • 100.000 requests → 100.000 data parts

  • Metadata overload, merge pressure cực cao

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

Batch insert

  • Gom 100.000 records thành vài batch lớn

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

  • Metadata nhẹ, merge nhanh & í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 buffer layer

App

hiện tại

Buffer

Vector / Kafka / App batch

ClickHouse

few large inserts

Nguyên tắc: ClickHouse thích few large inserts — client batch, async_insert (26.3+), hoặc buffer layer (Vector/Kafka) khi cần durability/retry.

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 buffer bên ngoài

async_insert (ClickHouse 26.3+)

Server-side buffer

Từ 26.3 LTS, async_insert bật mặc định — ClickHouse gom small inserts phía server rồi flush thành batch lớn.

Vẫn khuyến nghị: ≥1.000 rows/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

  • Client batch — app gom 10k rows rồi INSERT

  • async_insert — client gửi lẻ, server buffer

  • Vector / Kafka — queue, retry, routing, transform

Vector/Kafka khi nào?

Không chỉ để batch — cần durable queue, backpressure, filter/transform, tách producer khỏi ClickHouse.

Case study: api_logs nhận log qua Vector batch hoặc async_insert từ agent nhẹ.

Điểm chính: Không còn đúng tuyệt đối “app không INSERT trực tiếp” — chọn client batch, async_insert, hoặc pipeline tùy độ phức tạp.

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 dùng khi query

  • Sắp xếp kết quả trả về

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

ClickHouse (MergeTree)

  • ORDER BY xác định khi tạo bảng

  • Sắp xếp vật lý dữ liệu trên disk

  • Quyết định hiệu năng rất lớn

Ví dụ: tạo bảng với ORDER BY
CREATE TABLE api_logs (
  timestamp   DateTime,
  api         LowCardinality(String),
  endpoint    String,
  status      UInt16,
  duration_ms UInt32
) ENGINE = MergeTree
ORDER BY (api, timestamp);

Case study: api_logs dùng ORDER BY (api, timestamp) — filter theo API trước, range thời gian sau.

Điểm chính: ORDER BY trong ClickHouse = thiết kế schema, không phải thao tác truy vấn. 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 (service, timestamp)

servicetimestampstatus
auth10:00200
auth10:01200
payment10:00200
payment10:01500
user10:00200

Gom theo service, rồi timestamp

Bỏ qua nhóm auth & user

Query
SELECT *
FROM api_logs
WHERE service = 'payment'
  AND timestamp >= now()
- INTERVAL 1 HOUR;

Ý chính: Lọc theo cột đầu ORDER BY → bỏ qua toàn bộ khối dữ liệu không liên quan

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 (service, timestamp)
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 — mỗi mark ~ hàng nghìn dòng
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 PRIMARY KEY thường là prefix của ORDER BY — có thể khác khi cần sort rộng hơn index (granule ~8192 rows)

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[~8192 dòng]
          G2 --> M2[~8192 dòng]
          G3 --> M3[~8192 dòng]
                  

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

ClickHouse vs PostgreSQL

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

  • ClickHouse: Chỉ mục thưa → khối dữ liệu (~8192 dòng/mark)

Tại sao quan trọng?

  • Bỏ qua khối dữ liệu không liên quan — không đọc toàn bộ part

  • Chỉ mục cực nhỏ: vài MB cho tỷ dòng

WHERE timestamp >= '10:30' chỉ đọc các khối dữ liệu từ mark tương ứng trở đi.

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

Drop old data

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

TTL & lifecycle

Expire dữ liệu cũ theo partition

Partition pruning

Filter thời gian → bỏ partition không liên quan

Backup & transfer

Sao chép / chuyển cả partition

DDL & quản lý partition
CREATE TABLE api_logs (
    timestamp DateTime64,
    client_id String,
    api String,
    status_code UInt16
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(timestamp)
ORDER BY (api, timestamp);

ALTER TABLE api_logs
  DROP PARTITION '202601';

Lưu ý: PARTITION BY để quản lý data, không phải index chính — ORDER BY quyết định hiệu suất đọ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 — xem chi tiết PARTITION BY ở slide trước.

PARTITION BYORDER BY
Vai tròChia vùng / lifecycleThứ tự vật lý trong part
Ảnh hưởngTTL, DROP PARTITION, pruning theo keyTốc độ query, skip index
Sai lầmQuá nhiều partition nhỏNhét cột cardinality cao đầu key
api_logs — case study
ENGINE = MergeTree
PARTITION BY toYYYYMM(timestamp)   -- retention 30 ngày
ORDER BY (api, timestamp);         -- dashboard filter API

Quy tắc: ORDER BY → query nhanh hay chậm. PARTITION BY → giữ hay xóa dữ liệu theo thời gian.

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ý

Thói quen PostgreSQL

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

Cách nghĩ ClickHouse

CộtNên dùngB
status_codeUInt162
duration_msUInt324
countryLowCardinality≈1
timestampDateTime648

Đ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 (); dùng Bool thay UInt8 cho true/false.

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

Mục tiêu: Sử dụng LowCardinality để tối ưu storage và query 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, status_code

Ví dụ SQL
CREATE TABLE api_logs (
  country     LowCardinality(String),
  method      LowCardinality(String),
  service     LowCardinality(String),
  status_code UInt16,
  timestamp   DateTime
) ENGINE = MergeTree
ORDER BY (service, timestamp);

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 — cột riêng luôn nhanh hơn.

Query analytics cơ bản

Phần 5

Query analytics cơ bản

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

Dataset ví dụ: api_logs

Cột Kiểu Mô tả
timestampDateTimeThời điểm request
client_idStringClient gọi API
apiStringĐường dẫn API
methodStringGET / POST / …
status_codeUInt16HTTP status
duration_msUInt32Độ trễ (ms)
countryStringQuốc gia

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

PARTITION BY toYYYYMM(timestamp)

ORDER BY (api, timestamp)

Dashboard filter timestamp + GROUP BY api/country — không filter client_id.

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

Query analytics 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.

Query analytics 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;
Top country theo request
SELECT country, count() AS requests
FROM api_logs GROUP BY country
ORDER BY requests DESC;
Top client theo request
SELECT client_id, count() AS requests
FROM api_logs GROUP BY client_id
ORDER BY requests DESC;

GROUP BY + ORDER BY requests DESC để tìm top values.

Query analytics cơ bản

Mục tiêu: Các hàm aggregation cơ bản trong ClickHouse

Aggregation 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.

Query analytics 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 nhanh hơn giá trị này
P99900ms1% request chậm nhất

Dùng P95/P99 cho SLO/SLA thay vì trung bình

Case study: dashboard api_logs — P95 latency và error rate theo từng API.

Query analytics 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() / Month()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.

Query analytics 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 và nhanh hơn.

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

Tỷ lệ lỗi & độ trễ
SELECT
  count() AS total,
  countIf(status_code >= 500) AS errors,
  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.

Query analytics 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 user duy nhất
SELECT uniq(user_id) FROM events;

Điểm chính: uniq() = approximate, memory-efficient. uniqExact() = exact 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 raw → aggregate 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_raw

Dữ liệu gốc — đầ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
15 phút gần nhấtraw hoặc _1m
7 ngày gần nhất_1h
3 năm gần nhất_1d

Khoảng càng rộng → tầng tổng hợp càng cao → quét ít dữ liệu hơn. 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: Materialized View cho dashboard realtime

Materialized View

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

Tạo Materialized View
CREATE MATERIALIZED VIEW api_metrics_1m
ENGINE = SummingMergeTree() ORDER BY (api, minute)
AS SELECT toStartOfMinute(timestamp) AS minute, api,
  count() AS requests, sum(duration_ms) AS total_duration
FROM api_logs GROUP BY minute, api;

Trigger khi INSERT · không tự backfill · ghi vào target table

Điều quan trọng: Bảng thô giữ chi tiết; bảng tổng hợp phục vụ dashboard — ngàn dòng thay vì tỷ dòng.

Case study: api_logsapi_metrics_1m → Grafana (raw giữ 30 ngày, MV giữ 1 năm).

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ụ: request count theo phút, total bytes, revenue

  • Rows 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

Tạo bảng
CREATE TABLE api_metrics_1m
(
  timestamp  DateTime,
  api        LowCardinality(String),
  req_count  UInt64,
  total_ms   UInt64
)
ENGINE = SummingMergeTree((req_count, total_ms))
ORDER BY (api, timestamp);

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

Điểm chính: SummingMergeTree ghi thô → merge nền tự tổng hợp → truy vấn đọc ít dữ liệu hơn.

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 theo từng tầng

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[Sự kiện] -->|avgState| Min[Phút]
          Min -->|merge| Hour[Giờ]
          Hour -->|merge| Day[Ngày]
                  

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

Điểm chính: AggregatingMergeTree lưu trạng thái trung gian — luồng gốc → phút → giờ → ngày.

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 realtime · dashboard & API

Điểm chính: Ghi có thể qua client batch, async_insert (server buffer, mặc định từ 26.3) hoặc Vector/Kafka khi cần queue, retry, routing.

      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 kiến trúc dual-database phổ biến trong hệ thống analytics thực tế

Kiến trúc business analytics

      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 dimension

  • 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 query và schema

ClickHouse Course

Tối ưu query và schema

Mục tiêu: Thay đổi tư duy khi tối ưu query 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 query 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 và datatype tốt — đây là lớp bổ sung. Query 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_client
  client_id
  TYPE bloom_filter(0.01)
  GRANULARITY 4;

Đ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à không cần thay đổi truy vấn.

Tối ưu query và schema

Mục tiêu: Projections — secondary sort order trong cùng một bảng

Projections (lightweight)

Mental model

1 bảng api_logs
       ├─ ORDER BY (api, timestamp)   ← chính
       ├─ projection by timestamp
       └─ projection by client_id

Optimizer tự chọn projection phù hợp query — không duplicate full row như bảng phụ.

Case study: dashboard filter timestamp nhưng có query theo client_id → projection thứ hai.

Thêm projection
ALTER TABLE api_logs
  ADD PROJECTION proj_by_time (
    SELECT * ORDER BY timestamp, api
  );

ALTER TABLE api_logs
  MATERIALIZE PROJECTION proj_by_time;

Điểm chính: Từ 26.1+, projections hoạt động như secondary index — bổ sung khi một ORDER BY không phục vụ hết query patterns.

Tối ưu query và schema

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

PREWHERE — Đọc cột filter trước

Không PREWHERE

Đọc tất cả cột → filter sau → I/O lãng phí

Có PREWHERE

Đọc cột lọc trước → chỉ đọc cột kết quả trên dòng còn lại

Cú pháp PREWHERE
SELECT api, duration_ms
FROM api_logs
PREWHERE status_code = 500
WHERE timestamp >= today();
BướcThao tác
1Đọc status_code, timestamp → lọc dòng
2Đọc api, duration_ms trên dòng còn lại

Điểm chính: PREWHERE giảm I/O đĩa — bộ tối ưu có thể tự thêm khi phù hợp.

Tối ưu query 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

Lệnh EXPLAIN
EXPLAIN indexes = 1
SELECT * FROM api_logs
WHERE service = 'payment' AND timestamp >= today();

-- Kết quả: 12 / 10.000 khối (0,12%) · 48K / 10M dòng

Kế hoạch truy vấn

  • Thứ tự xử lý

  • Filter ở bước nào

Sử dụng chỉ mục

  • Khối dữ liệu đọc / bỏ qua

  • Chỉ mục bỏ qua có hiệu lực?

Luồng xử lý

  • Xử lý song song các part

  • Giới hạn CPU/RAM

Điểm chính: EXPLAIN indexes = 1 kiểm chứng schema bỏ qua dữ liệu tốt — đọc ít = truy vấn nhanh.

Tối ưu query và schema

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

system.query_log

Query ví dụ
SELECT query, query_duration_ms,
 read_rows, read_bytes
FROM system.query_log
WHERE type = 'QueryFinish'
ORDER BY query_duration_ms DESC
LIMIT 20;
CộtÝ nghĩa
query_duration_msThời gian chạy query
read_rowsSố dòng đọc từ đĩa
read_bytesDung lượng đọc
memory_usageRAM sử dụng

Số dòng đọc quan trọng hơn thời gian chạy!

Truy vấn 0,2s đọc 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 đọc, RAM — để tối ưu.

Tối ưu query và schema

Mục tiêu: Sử dụng system.parts để giám sát storage 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 đĩa

bytes_on_disk / compressed · theo dõi hiệu quả nén

Truy vấn giám sát storage
SELECT
    table,
    count() AS parts,
    sum(rows) AS rows,
    formatReadableSize(sum(bytes_on_disk))
FROM system.parts
WHERE active
GROUP BY table;

Ghi chú: Quá nhiều part nhỏ = dấu hiệu cần tăng kích thước mỗi lần ghi hoặc dùng bộ đệm (Vector/Kafka)

Tối ưu query và schema

Mục tiêu:

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

SELECT *

  • Đọc mọi cột, kể cả cột không dùng

  • Phá vỡ ưu thế của lưu theo cột

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

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

  • Nhóm theo cột hàng triệu giá trị khác nhau

  • Kết quả trả về quá lớn, tốn RAM

  • Xem xét tổng hợp trước hoặc dùng ước lượng

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

  • ClickHouse vẫn phải quét và sắp xếp toàn bộ trước

  • Nếu khóa sắp xếp khớp, không cần quét toàn bộ

  • Kiểm tra EXPLAIN để xác nhận

Truy vấn trực tiếp trên bảng thô

  • Quét tỷ dòng bảng thô mỗi lần tải dashboard

  • Dùng bảng tổng hợp trước (MV)

  • Tổng hợp theo phút/giờ phù hợp khoảng truy vấn

Đ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ạnh32–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 clusterChỉ khi đã tối ưu schema & truy vấn

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: Nâng cấp phần cứng trước, mở rộng cluster sau — đọc ít dữ liệu nhất trước khi nghĩ đến 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

  • Mỗi shard = một phần dữ liệu trên node riêng

  • Hash function xác định shard chứa dữ liệu

Tăng capacity

  • Query song song trên shard → tăng throughput

  • Thêm shard = thêm dung lượng + CPU

Ví dụ: cityHash64(user_id) % 3 — 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 availability.

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

High Availability

  • Nếu replica A chết, replica B tiếp tục phục vụ

  • Replication mặc định async / eventually consistent — cần insert_quorum nếu muốn guarantee mạnh hơn

Shard 1

Replica A

data ✗

Lỗi!

Replica B

data ✓

Vẫn hoạt động

Replica B tiếp tục phục vụ khi A gặp sự cố

Điểm chính: Replication ≠ Sharding — Sharding chia dữ liệu để scale, 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ý query xuyên cluster

Distributed Table

Tạo bảng Distributed
CREATE TABLE events
ENGINE = Distributed(
    cluster_3shard,
    default,
    events_local,
    cityHash64(user_id)
);

Distributed engine thường là lớp routing/proxy — dữ liệu thực nằm trên local tables của từng shard

Lưu ý: Distributed table không lưu dữ liệu — chỉ điều phối query tới bảng local trên shard.

      flowchart TB
          C[Client] --> D[events Distributed]
          D --> S1[Shard 1 local]
          D --> S2[Shard 2 local]
          D --> S3[Shard 3 local]
          S1 --> R[Merge results]
          S2 --> R
          S3 --> R
          R --> C
                  

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

Distributed ClickHouse

Mục tiêu: Replicate dữ liệu trong cluster

ReplicatedMergeTree & ClickHouse Keeper

ReplicatedMergeTree

Bọc MergeTree — insert 1 replica, sync sang replica khác.

ClickHouse Keeper

Thay ZooKeeper — metadata replication + leader election.

      flowchart TB
          K[Keeper] -.-> R1[Replica 1]
          K -.-> R2[Replica 2]
          K -.-> R3[Replica 3]
          R1 <-->|sync| R2
          R2 <-->|sync| R3
                  

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 decision guide theo workload.

Tình huống Chọn Ví dụ case study
CRUD, transaction, FKPostgreSQLUsers, API keys, billing
Aggregate tỷ dòng logClickHouseapi_logs P95, error rate
Full-text search phức tạpElasticsearch / OpenSearchLog message search (Phần 9)
Report SQL trên warehouseSnowflake / BigQueryBI cross-team (Phần 9)

Điểm chính: Production thường là polyglot persistence — PG + CH cho api_logs là pattern phổ biến nhất.

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

Mục tiêu: Chọn đúng công cụ cho log/search analytics

ClickHouse vs Elasticsearch/OpenSearch

WorkloadElasticsearchClickHouse
Search / relevance / fuzzy⭐⭐⭐⭐⭐⭐⭐⭐
Exact / token / phrase search⭐⭐⭐⭐⭐⭐⭐⭐⭐ (FTS GA 2026)
Filter + aggregate khối lượng lớn⭐⭐⭐⭐⭐⭐⭐⭐
Nén & chi phí lưu trữ⭐⭐⭐⭐⭐⭐⭐⭐
SQL analytics⭐⭐⭐⭐⭐⭐⭐

Chọn Elasticsearch khi:

Search product — relevance ranking, fuzzy, ecosystem search

Chọn ClickHouse khi:

Observability analytics — filter + aggregate (+ text search cơ bản)

Điểm chính: Log analytics = filter + aggregate → ClickHouse. Search-centric product → Elasticsearch/OpenSearch.

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

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

ClickHouse vs Cloud Data Warehouse

Tiêu chí ClickHouse (OSS / Cloud) BigQuery / Snowflake / Redshift
Vận hành Tự quản lý hạ tầng · linh hoạt tuning Managed service · ít vận hành hơn
Độ trễ Thường tốt cho real-time khi tune đúng Phụ thuộc workload
Elasticity Scale theo cluster thiết kế Scale tài nguyên linh hoạt
Chi phí Hạ tầng có thể dự đoán Pay-per-use · theo dõi usage

Điểm chính: So sánh apples-to-apples: ClickHouse Cloud vs managed DW — hoặc OSS self-hosted vs tự vận hành cluster analytics.

Case study xuyên suốt

Mục tiêu: Mô tả bài toán thực tế — nền tảng monitor API multi-service

Case Study: API Monitoring Platform

Bối cảnh: Nhiều service — mỗi request = 1 dòng log · 300 users · 20 services · 50M logs/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ô

Mô hình: PostgreSQL (OLTP) → Vector/CDC → ClickHouse (OLAP) → Grafana

      flowchart TB
          BE[Backend 20 svc] --> V[Vector batch]
          V --> CH[api_logs_raw]
          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ú
timestampDateTime64Thời điểm request
client_idLowCardinality(String)Client gọi API
apiStringĐường dẫn API
methodLowCardinality(String)GET / POST / …
status_codeUInt16HTTP status
duration_msUInt32Độ trễ (ms)
countryLowCardinality(String)Quốc gia

Cấu hình bảng

ENGINE MergeTree · PARTITION BY toYYYYMM(timestamp)

ORDER BY (api, timestamp) — khớp dashboard: filter timestamp, GROUP BY api/country

LowCardinality / UInt — giảm dung lượng cột lặp lại và số nguyên

Append-only — mỗi request 1 dòng.

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 — top API theo HTTP 5xx

SELECT api, countIf(status_code >= 500) AS errors,
  round(errors / count(), 4) AS error_rate
FROM api_logs WHERE timestamp >= today()
GROUP BY api 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 avg latency

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 (api, timestamp) — khớp các query dashboard bên trên.

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ô

Mục đích:

Điều tra chi tiết, debug, xem log

Khoảng truy vấn:

15 phút gần nhất → theo thời gian thực

Tổng hợp theo phút

Mục đích:

Dashboard theo thời gian thực, cảnh báo, giám sát SLO

Khoảng truy vấn:

1 ngày → 7 ngày gần nhất

Theo giờ / ngày

Mục đích:

Báo cáo dài hạn, phân tích xu hướng, báo cáo quản lý

Khoảng truy vấn:

3 tháng → 3 năm gần nhất

Dùng Materialized View tự động tổng hợp khi ghi dữ liệu

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 — debug

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

6–12 tháng

Dashboard realtime

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

1–3 năm

Report dài hạn, YoY

Nguyên tắc: Raw giữ ngắn, aggregate giữ dài. Dùng PARTITION BY toYYYYMM() để xóa cực nhanh.

TTL — xóa 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

Append hay update?

Event log → append + TTL. CDC → ReplacingMergeTree.

2

Query filter & ORDER BY?

Filter chính → đầu ORDER BY. api_logs: (api, timestamp).

3

Retention & PARTITION BY?

Range thời gian → partition theo ngày/tháng; DROP/TTL partition.

4

Datatype & cardinality?

LowCardinality cho enum; DateTime64; tránh String cho số.

5

MV / projection / TTL?

MV cho dashboard; projection cho query pattern thứ hai; TTL giảm chi phí.

Nguyên tắc: Bắt đầu từ query → filter → ORDER BY / PARTITION BY / datatype. Áp dụng lại cho api_logs từ đầu bài.

Checklist và tổng kết

Mục tiêu: Checklist khi query chậm hoặc cần tối ưu performance

Checklist tối ưu query

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 kết quả lớn → dùng GROUP BY có chọn lọ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 theo cột là nền tảng của mọi tốc độ

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

4. ORDER BY quyết định hiệu năng

5. Ghi theo lô tốt hơn ghi từng dòng

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 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