DE

[Tech Blog #32 | Data Engineering] Giải Mã Columnar Database: Tại Sao ClickHouse & DuckDB Quét 1 Tỷ Dòng Trong 1 Giây?

0 views
[Tech Blog #32 | Data Engineering] Giải Mã Columnar Database: Tại Sao ClickHouse & DuckDB Quét 1 Tỷ Dòng Trong 1 Giây?

1. Đặt vấn đề: Khi PostgreSQL "thở dốc" trước hàng trăm triệu dòng

Hãy tưởng tượng bạn đang quản lý cơ sở dữ liệu cho một chuỗi bán lẻ hoặc hệ thống sàn thương mại điện tử:

  • Bảng nhật ký giao dịch (transactions) đã tích lũy lên tới 200 triệu dòng.
  • Giám đốc kinh doanh yêu cầu một biểu đồ phân tích:
    SELECT store_id, SUM(amount), AVG(discount) FROM transactions GROUP BY store_id;

Khi bạn chạy câu lệnh này trên PostgreSQL hay MySQL:

  • Quạt tản nhiệt máy chủ kêu rú lên, CPU chạm đỉnh 100%, ổ cứng đọc liên tục không ngừng nghỉ.
  • Màn hình xoay vòng mất 3 đến 5 phút (thậm chí báo lỗi Timeout hoặc Out of Memory).

Thế nhưng, khi nạp cùng 200 triệu dòng đó sang một Cơ sở dữ liệu dạng cột (Columnar Database) như ClickHouse hoặc DuckDB, câu truy vấn tương tự trả về kết quả chỉ trong vỏn vẹn 0.2 giây!

Bí mật đằng sau tốc độ "nhanh như chớp" này không phải do máy chủ có phần cứng thần thánh, mà nằm ở sự thay đổi mang tính cách mạng trong cách tổ chức dữ liệu dưới ổ đĩa.


2. Giải phẫu kiến trúc: Row-oriented (Hàng) vs. Column-oriented (Cột)

Sự khác biệt cốt lõi nhất giữa hai trường phái cơ sở dữ liệu:

BẢNG DỮ LIỆU LOGIC:
ID | Khách Hàng | Tuổi | Doanh Thu
---+------------+------+----------
 1 | Alice      | 25   | 500,000  
 2 | Bob        | 30   | 200,000  
 3 | Charlie    | 22   | 800,000  

========================================================================
1. LƯU TRỮ DẠNG HÀNG (Row-oriented - PostgreSQL, MySQL):
Ổ đĩa: [1, Alice, 25, 500000] [2, Bob, 30, 200000] [3, Charlie, 22, 800000]
-> Muốn tính SUM(Doanh Thu), máy tính BẮT BUỘC phải đọc qua cả ID, Khách Hàng, Tuổi!

========================================================================
2. LƯU TRỮ DẠNG CỘT (Column-oriented - ClickHouse, DuckDB):
File 1 (ID)        : [1, 2, 3]
File 2 (Khách Hàng): [Alice, Bob, Charlie]
File 3 (Tuổi)      : [25, 30, 22]
File 4 (Doanh Thu) : [500000, 200000, 800000]
-> Muốn tính SUM(Doanh Thu), máy tính CHỈ ĐỌC đúng File 4, bỏ qua 100% các file còn lại!
  • Row-oriented (Lưu theo dòng - OLTP): Cực kỳ tối ưu cho việc chèn (INSERT), cập nhật (UPDATE) hoặc lấy thông tin trọn vẹn của 1 đơn hàng cụ thể (WHERE id = 101).
  • Column-oriented (Lưu theo cột - OLAP): Cực kỳ tối ưu cho việc tính toán tổng hợp (SUM, AVG, COUNT, GROUP BY) trên hàng chục triệu dòng vì chỉ cần nạp đúng các cột liên quan vào RAM (Column Pruning).

3. Ba "vũ khí bí mật" giúp ClickHouse đạt tốc độ ánh sáng

Ngoài việc lưu trữ theo cột (tương tự như nguyên lý file Parquet ở [Tech Blog #24]), ClickHouse còn trang bị 3 cơ chế phần cứng tối tân:

  1. Xử lý vector hóa (Vectorized Query Execution / SIMD):
    Thay vì dùng vòng lặp for để cộng từng con số một, ClickHouse tận dụng tập lệnh phần cứng SIMD (Single Instruction, Multiple Data) của CPU hiện đại để cộng một mảng gồm hàng nghìn con số cùng lúc chỉ trong một chu kỳ xung nhịp CPU.
  2. Nén dữ liệu đỉnh cao (LZ4 / ZSTD Compression):
    Vì toàn bộ giá trị trong cùng một cột có cùng kiểu dữ liệu (ví dụ: cột store_id toàn là các số nguyên lặp lại), thuật toán nén có thể ép dung lượng xuống 5–10 lần. Dữ liệu nén nhỏ →\rightarrow Ổ cứng đọc siêu nhanh.
  3. Chỉ mục thưa (Sparse Primary Index):
    PostgreSQL dùng B-Tree index dày đặc (mỗi dòng dữ liệu có 1 con trỏ index, tốn hàng chục GB RAM).
    ClickHouse chỉ lưu 1 điểm đánh dấu cho mỗi khối 8.192 dòng dữ liệu (gọi là 1 Granule). Chỉ mục thưa này siêu nhẹ (chỉ vài chục MB), có thể nằm trọn vẹn 100% trong RAM máy chủ để tìm kiếm cực nhanh.

4. Bảng so sánh: PostgreSQL (Row) vs. ClickHouse (Column)

Tiêu chíPostgreSQL (Row-oriented - OLTP)ClickHouse (Column-oriented - OLAP)
Mục đích chínhGiao dịch thời gian thực (Web/App, E-commerce).Phân tích dữ liệu lớn, Báo cáo BI, Logs/Metrics.
Tốc độ INSERT từng dòng🚀 Rất nhanh (Vài mili-giây cho mỗi transaction).🐢 Kém (Bắt buộc phải nạp theo lô lớn - Batch Insert ≥10.000\ge 10.000 dòng).
Thao tác UPDATE / DELETERất linh hoạt, hỗ trợ ACID trọn vẹn.🔴 Rất tốn kém tài nguyên (Được thiết kế cho dữ liệu chỉ ghi thêm - Append-only).
Tốc độ tính toán AggregationChậm khi vượt quá vài triệu dòng.🚀 Siêu thanh (Quét hàng trăm triệu dòng trong < 1s).
Tỷ lệ nén dữ liệuKém (1x - 2x).🟢 Xuất sắc (5x - 10x nhờ nén theo cột).

5. Thực hành thực chiến: Khởi tạo bảng MergeTree trong ClickHouse & Đo lường tốc độ

Engine phổ biến và mạnh mẽ nhất của ClickHouse là họ động cơ MergeTree. Dưới đây là cú pháp SQL chuẩn DDL tạo bảng phân tích và ví dụ truy vấn:

-- =============================================================================
-- BƯỚC 1: TẠO BẢNG PHÂN TÍCH GIAO DỊCH CHUẨN CÚ PHÁP CLICKHOUSE
-- =============================================================================
CREATE TABLE default.fact_retail_transactions
(
    transaction_id UInt64,                    -- Kiểu số nguyên không dấu 64-bit
    store_id UInt16,                          -- Mã cửa hàng (1 - 65535)
    customer_id UInt32,
    transaction_date Date,                    -- Kiểu ngày (YYYY-MM-DD)
    payment_method LowCardinality(String),   -- Chuỗi ít biến động (Tự động nén thành từ điển số)
    amount Decimal64(2),                      -- Tiền giao dịch (độ chính xác cao)
    discount_amount Decimal64(2)
)
ENGINE = MergeTree()
-- 1. Phân vùng dữ liệu theo tháng: Giúp khi truy vấn theo tháng nào chỉ quét thư mục tháng đó
PARTITION BY toYYYYMM(transaction_date)
-- 2. Khóa sắp xếp chính (Sparse Primary Index): Bắt buộc sắp xếp dữ liệu vật lý dưới đĩa
ORDER BY (store_id, transaction_date, transaction_id)
-- 3. Cài đặt kích thước Granule mặc định (8.192 dòng cho mỗi mốc index)
SETTINGS index_granularity = 8192;

-- =============================================================================
-- BƯỚC 2: TRUY VẤN TỔNG HỢP SIÊU TỐC TRÊN HÀNG TRĂM TRIỆU DÒNG
-- =============================================================================
-- Yêu cầu: Tính tổng doanh thu và số lượng đơn hàng theo từng cửa hàng trong tháng 09/2026

SELECT
    store_id,
    payment_method,
    COUNT(transaction_id) AS total_orders,
    SUM(amount) AS total_revenue_vnd,
    ROUND(AVG(amount), 0) AS avg_order_value
FROM
    default.fact_retail_transactions
WHERE
    -- Nhờ có PARTITION BY toYYYYMM, ClickHouse bỏ qua toàn bộ dữ liệu của các năm trước!
    transaction_date >= '2026-09-01' AND transaction_date <= '2026-09-30'
GROUP BY
    store_id,
    payment_method
ORDER BY
    total_revenue_vnd DESC
LIMIT 10;

Script Python kết nối ClickHouse đo thời gian phản hồi:

import time
import clickhouse_connect

# Kết nối vào ClickHouse (có thể chạy qua Docker đã học ở bài 16)
client = clickhouse_connect.get_client(
    host="localhost", 
    port=8123, 
    username="default", 
    password=""
)

# Chạy thử nghiệm truy vấn phân tích
start_time = time.time()
result = client.query("""
    SELECT store_id, SUM(amount) 
    FROM default.fact_retail_transactions 
    GROUP BY store_id 
    ORDER BY SUM(amount) DESC 
    LIMIT 5
""")
elapsed = time.time() - start_time

print(f"✅ Truy vấn hoàn tất trong: {elapsed:.4f} giây!")
print(f"Tổng số dòng quét: {result.summary.get('read_rows', 'N/A')} dòng.")
print(f"Dung lượng dữ liệu đọc từ đĩa: {result.summary.get('read_bytes', 'N/A')} bytes.")

6. Câu hỏi phỏng vấn thực tế (Interview Q&A)

Câu 1 (Mức độ: Entry / Junior)

Câu hỏi: "Tại sao trong các cơ sở dữ liệu dạng cột như ClickHouse, việc thực hiện lệnh UPDATE hoặc DELETE từng dòng đơn lẻ lại bị coi là điều cấm kỵ (Anti-pattern)?"

Gợi ý trả lời:
"Bởi vì trong Columnar Database, các giá trị của một dòng dữ liệu không nằm liền kề nhau mà bị phân tán thành từng file riêng biệt theo từng cột dưới ổ cứng (và được nén rất chặt thành các Part file bất biến - Immutable Data Parts).
Khi chúng ta muốn sửa hoặc xóa duy nhất 1 dòng, hệ thống không thể sửa trực tiếp tại chỗ (in-place update) như PostgreSQL, mà nó buộc phải giải nén toàn bộ khối dữ liệu của tất cả các cột, sửa dòng đó, rồi nén và ghi đè lại một file mới hoàn toàn (Heavy Mutation).
Nếu chạy hàng nghìn lệnh UPDATE nhỏ lẻ, máy chủ ClickHouse sẽ bị nghẽn I/O và sập vì quá tải gộp file (Too many parts). Vì vậy, ClickHouse chỉ nên dùng cho dữ liệu ghi thêm (Append-only); nếu cần cập nhật, người ta dùng cơ chế thay thế theo lô như ReplacingMergeTree hoặc cập nhật định kỳ cả phân vùng."

Câu 2 (Mức độ: Junior / Mid-level)

Câu hỏi: "Trong kiến trúc dữ liệu doanh nghiệp hiện đại, khi nào bạn nên kết hợp cả PostgreSQL và ClickHouse thay vì chỉ dùng một trong hai?"

Gợi ý trả lời:
*"Đây là mô hình kiến trúc chuẩn mực kết hợp OLTP (PostgreSQL) và OLAP (ClickHouse):

  • PostgreSQL đảm nhận vai trò làm Cơ sở dữ liệu giao dịch chính của ứng dụng web/app: Phục vụ khách hàng đăng ký, đặt hàng, thanh toán, đảm bảo tính toàn vẹn dữ liệu tuyệt đối (ACID) với các thao tác CRUD diễn ra tức thì.
  • ClickHouse đóng vai trò là Kho dữ liệu phân tích (Analytics Warehouse): Nhận dữ liệu đồng bộ từ PostgreSQL thông qua các công nghệ CDC (Change Data Capture như Debezium/Kafka) hoặc Batch ETL. ClickHouse sẽ phục vụ các báo cáo BI Dashboard cho ban giám đốc, các truy vấn phức tạp của Data Analyst và theo dõi log hệ thống.
    Sự kết hợp này giúp giải phóng hoàn toàn gánh nặng phân tích cho PostgreSQL, bảo đảm hệ thống bán hàng của web không bao giờ bị chậm hay nghẽn do người làm dữ liệu chạy báo cáo."*

7. Key Takeaways (Ghi nhớ nhanh)

  1. Row cho Giao dịch (OLTP), Column cho Phân tích (OLAP): Cần ghi từng dòng nhanh dùng Postgres; cần tính tổng hàng trăm triệu dòng dùng ClickHouse.
  2. Column Pruning cắt giảm 90% tải I/O: Chỉ đọc đúng các cột có trong câu lệnh SELECT và WHERE, bỏ qua toàn bộ phần còn lại.
  3. MergeTree là trái tim của ClickHouse: Luôn thiết kế khóa sắp xếp ORDER BY dựa trên các cột thường xuyên được dùng để lọc và gom nhóm.
  4. Không chạy UPDATE/DELETE lẻ tẻ trong ClickHouse: Luôn nạp dữ liệu theo lô lớn (Batch ≥10.000\ge 10.000 dòng) và ưu tiên dữ liệu dạng chỉ ghi thêm (Append-only).
0Claps