Snowflake 5 — Hiệu năng: micro-partition, clustering, cache

13 thg 7, 2026 2 lượt xem
#clustering
#data-engineering
#snowflake
#performance
#query-profile

Vì sao cần hiểu "bên trong" Snowflake

Snowflake che giấu gần như toàn bộ hạ tầng: không có index để tạo, không có VACUUM, không có tablespace để chia. Điều đó dễ khiến người dùng nghĩ rằng "cứ query là chạy". Nhưng khi bảng đạt hàng tỷ dòng, cùng một câu lệnh có thể quét 40 GB hoặc chỉ quét 200 MB — chênh lệch 200 lần về thời gian và chi phí credit. Sự khác biệt nằm ở việc storage engine có cắt tỉa (prune) được phần lớn dữ liệu không liên quan hay không.

Bài này đi vào cơ chế lưu trữ vật lý (micro-partition), cách metadata giúp bỏ qua dữ liệu, khi nào phải can thiệp bằng clustering key, ba tầng cache, và cách đọc Query Profile để biết tiền credit của bạn đang chảy đi đâu. Nếu chưa nắm kiến trúc chung, xem lại Snowflake — Tổng quanVirtual Warehouses.

Micro-partition — đơn vị lưu trữ

Snowflake không lưu bảng thành một khối phẳng. Khi dữ liệu được nạp, engine tự động chia bảng thành các micro-partition: mỗi cái chứa khoảng 50–500 MB dữ liệu chưa nén, tương đương ~16 MB sau khi nén theo cột. Với một bảng lớn, số micro-partition có thể lên tới hàng trăm nghìn hoặc hàng triệu.

Đặc điểm cốt lõi:

  • Lưu theo cột (columnar) trong từng micro-partition. Đọc 3 cột trong bảng 200 cột thì engine chỉ giải nén 3 cột đó.
  • Bất biến (immutable). Không sửa tại chỗ. Một UPDATE/DELETE sẽ ghi ra micro-partition mới và đánh dấu cái cũ hết hiệu lực — đây chính là nền tảng cho Time Travel (xem Time Travel & Sharing).
  • Metadata cho mỗi micro-partition: với từng cột, Snowflake lưu giá trị min/max, số distinct value, số NULL, và các thông tin khác. Metadata này nằm trong lớp cloud services, tách khỏi dữ liệu.

Chính bộ metadata min/max là "index tự nhiên" thay cho B-tree index. Người dùng không tạo, không bảo trì — engine tự sinh khi ghi.

Partition pruning — cắt tỉa dựa trên metadata

Khi bạn viết WHERE txn_date = '2026-06-01', engine không mở toàn bộ micro-partition. Nó đọc metadata min/max của cột txn_date từng micro-partition và loại bỏ ngay những cái có khoảng [min, max] không chứa ngày cần tìm. Chỉ những micro-partition có thể chứa dữ liệu mới được đưa vào scan. Đây là static pruning.

Ví dụ minh hoạ (cú pháp Snowflake — KHÔNG chạy trong sandbox):

-- Minh hoạ (Snowflake), KHÔNG đánh dấu chạy được
SELECT customer_id, SUM(amount)
FROM fact_transactions
WHERE txn_date BETWEEN '2026-06-01' AND '2026-06-07'
GROUP BY customer_id;

Nếu dữ liệu được nạp theo thứ tự thời gian, các giao dịch một tuần nằm gọn trong vài trăm micro-partition thay vì hàng trăm nghìn. Pruning càng hiệu quả khi cột lọc tương quan với thứ tự ghi vật lý.

Ngoài static pruning còn có dynamic pruning: trong join, giá trị từ bảng nhỏ (build side) được dùng để cắt tỉa micro-partition của bảng lớn (probe side) ngay trong lúc chạy — rất hữu ích cho mô hình star schema. Bạn xác nhận pruning có hoạt động không bằng chỉ số Partitions scanned / Partitions total trong Query Profile: tỷ lệ càng nhỏ càng tốt.

Điều quan trọng cần nhớ: pruning chỉ hoạt động trên predicate đơn giản áp trực tiếp lên cột. Bọc cột trong hàm sẽ vô hiệu hoá nó, vì min/max của col không suy ra được min/max của f(col):

-- Minh hoạ (Snowflake): hai cách viết, hiệu quả pruning khác nhau
-- XẤU: hàm bọc cột -> không prune được theo txn_date
WHERE TO_CHAR(txn_date, 'YYYY-MM') = '2026-06'
-- TỐT: predicate trực tiếp -> prune tốt
WHERE txn_date >= '2026-06-01' AND txn_date < '2026-07-01'

Natural clustering và clustering depth

Thứ tự vật lý của dữ liệu quyết định pruning tốt tới đâu. Khi nạp dữ liệu theo dòng thời gian (batch nạp mỗi ngày), bảng có natural clustering theo cột thời gian — các micro-partition không (hoặc ít) chồng lấn khoảng min/max của nhau.

Snowflake đo mức độ này bằng clustering depth: với một cột (hoặc bộ cột), độ sâu là số micro-partition trung bình chồng lấn nhau tại một điểm giá tr. Depth thấp (gần 1) nghĩa là dữ liệu sắp xếp gọn, mỗi giá trị tập trung ở vài partition → prune tốt. Depth cao nghĩa là mỗi giá trị rải khắp bảng → phải scan nhiều.

-- Minh hoạ (Snowflake): xem chất lượng clustering
SELECT SYSTEM$CLUSTERING_INFORMATION('fact_transactions', '(txn_date)');
-- Trả về JSON: average_depth, total_partition_count, histogram phân bố depth...

Diễn giải kết quả:

average_depthÝ nghĩa
~1–2Rất tốt, prune gần tối ưu
3–8Bình thường, đa số truy vấn vẫn ổn
> 20Kém — cột này gần như không giúp prune

Trước khi nghĩ tới clustering key, hãy tận dụng natural clustering: nạp dữ liệu theo thứ tự tự nhiên (thời gian), tránh nạp xen kẽ lộn xộn.

Clustering key — khi nào cần, chi phí ra sao

Clustering key là chỉ thị nói với Snowflake: hãy giữ bảng được sắp xếp vật lý theo (các) cột này. Sau khi định nghĩa, Snowflake tự chạy automatic reclustering ở nền để duy trì thứ tự khi dữ liệu mới được ghi vào làm depth tăng.

-- Minh hoạ (Snowflake)
ALTER TABLE fact_transactions CLUSTER BY (txn_date, branch_id);

Khi nào nên dùng:

  • Bảng rất lớn (thường từ hàng trăm GB / nhiều TB trở lên). Bảng nhỏ prune tốt sẵn.
  • Truy vấn thường xuyên lọc/join theo một cột không phải cột nạp tự nhiên (ví dụ lọc theo branch_id trong khi dữ liệu nạp theo thời gian).
  • Query Profile cho thấy Partitions scanned rất cao so với total dù đã có predicate.

Khi nào KHÔNG nên:

  • Bảng đã natural clustering tốt theo đúng cột hay lọc.
  • Bảng ghi/cập nhật liên tục với thứ tự ngẫu nhiên → reclustering sẽ chạy suốt và đốt credit.
  • Chọn cột cardinality quá cao (như transaction_id unique): mỗi giá trị một partition, key vô dụng mà chi phí duy trì lớn.

Chi phí: automatic reclustering tiêu tốn compute (tính bằng credit, ghi hoá đơn riêng dòng "Automatic Clustering" trong billing). Mỗi lần dữ liệu thay đổi làm depth xấu đi, Snowflake ghi lại (rewrite) các micro-partition liên quan — vừa tốn compute vừa tốn storage tạm. Quy tắc thực chiến: chọn key theo cột trong WHERE/JOIN phổ biến nhất, cardinality thấp–trung bình, thứ tự cột từ thô đến mịn (ví dụ (txn_date, branch_id) chứ không phải (branch_id, txn_date) nếu lọc theo ngày là chính). Bật, đo bằng SYSTEM$CLUSTERING_INFORMATION sau vài ngày, tắt nếu không cải thiện. Xem thêm cân đối chi phí ở Cost Management.

Ba tầng cache

Snowflake tăng tốc bằng ba lớp cache độc lập. Hiểu rõ giúp bạn giải thích vì sao một query lúc chạy 8 giây lúc chạy 0,1 giây.

1. Result cache (cloud services layer). Lưu kết quả của truy vấn trong ~24 giờ (gia hạn mỗi lần dùng, tối đa 31 ngày). Truy vấn sau nếu giống hệt về text (kể cả khoảng trắng có thể ảnh hưởng), dữ liệu nguồn chưa đổi, và người dùng có quyền — sẽ nhận kết quả tức thì, không khởi động warehouse, không tốn credit compute. Bị vô hiệu nếu bảng đổi dữ liệu, hoặc query dùng hàm không tất định như CURRENT_TIMESTAMP(). Đây là lý do dashboard mở lại rất nhanh nếu truy vấn không đổi.

2. Local disk (warehouse) cache. Mỗi virtual warehouse có SSD cục bộ, giữ lại các micro-partition đã đọc gần đây. Truy vấn kế tiếp trên cùng warehouse chạm dữ liệu đó sẽ đọc từ SSD thay vì tải lại từ cloud storage. Cache này mất khi warehouse suspend rồi resume. Đó là lý do warehouse "vừa ngủ dậy" chạy chậm hơn một chút (cold cache). Auto-suspend quá ngắn sẽ liên tục xoá cache; xem đánh đổi ở Virtual Warehouses.

3. Remote storage. Không phải cache mà là nguồn gốc: micro-partition nằm trên object store (S3/GCS/Azure Blob). Khi cả result cache và local cache đều miss, dữ liệu được kéo từ đây — chậm nhất và là chi phí I/O thực.

Đọc Query Profile để tìm nút thắt

Query Profile (trong Snowsight, tab History → chọn query) là công cụ chẩn đoán quan trọng nhất. Đây là những dấu hiệu cần soi:

  • Bytes scanned / Percentage scanned from cache. Bytes lớn = prune kém. Nếu % from cache cao thì đang tận dụng local cache tốt.
  • Partitions scanned vs total. Tỷ lệ cao (ví dụ 95% partition bị quét dù có WHERE) = pruning không ăn → cân nhắc viết lại predicate hoặc clustering.
  • Spilling. Xuất hiện "Bytes spilled to local storage" và tệ hơn là "Bytes spilled to remote storage". Nghĩa là bộ nhớ warehouse không đủ, phải tràn ra đĩa/cloud. Đây là sát thủ hiệu năng. Cách xử lý: tăng size warehouse (nhiều RAM hơn), giảm dữ liệu trung gian, hoặc bỏ bớt cột/ORDER BY không cần.
  • Exploding join (join bùng nổ). Node join có số dòng output lớn hơn nhiều tổng input → thường do thiếu điều kiện join (vô tình tạo tích Descartes) hoặc join trên khoá không unique gây nhân bản dòng. Dấu hiệu: một join node chiếm phần lớn thời gian và số "Rows" tăng vọt. Sửa bằng cách kiểm tra điều kiện join và mức độ trùng của khoá.
  • Most expensive nodes. Query Profile xếp hạng % thời gian theo node (TableScan, Join, Aggregate, Sort). Tối ưu node đắt nhất trước.

Quy trình chẩn đoán thực dụng: nhìn node đắt nhất → nếu là TableScan thì soi pruning; nếu là Join thì soi exploding/ spilling; nếu là Sort/Aggregate thì soi spilling và cân nhắc tăng warehouse.

Search Optimization Service

Clustering giúp truy vấn quét dải (range) theo cột đã cluster. Nhưng với truy vấn điểm (point lookup) trên cột có cardinality rất cao — kiểu WHERE account_no = '0123456789' hay tra một mã giao dịch — clustering không thực tế. Search Optimization Service (SOS) sinh ra một cấu trúc tìm kiếm phụ (search access path) giúp point lookup nhắm thẳng đúng micro-partition.

-- Minh hoạ (Snowflake)
ALTER TABLE fact_transactions ADD SEARCH OPTIMIZATION ON EQUALITY(account_no);

Phù hợp cho: bảng lớn, truy vấn selective (trả rất ít dòng), lọc bằng =/IN, tìm chuỗi con, hoặc điều kiện trên VARIANT/geo. Không phù hợp cho truy vấn quét diện rộng (analytics tổng hợp). SOS tốn storage cho cấu trúc phụ và tốn compute để duy trì khi dữ liệu đổi — nên bật có chọn lọc và đo lại.

Materialized View

Materialized view (MV) lưu sẵn kết quả của một truy vấn (thường là aggregate/projection trên bảng lớn) và Snowflake tự động cập nhật khi bảng nền đổi. Truy vấn chạm đúng pattern có thể được engine tự "route" sang MV mà không cần viết lại.

Giới hạn thực tế cần nhớ: MV của Snowflake chỉ dựng trên một bảng (không join), không hỗ trợ nhiều loại hàm/GROUP BY phức tạp tuỳ phiên bản, và tốn credit để tự bảo trì giống clustering. Dùng khi có một phép tổng hợp nặng lặp lại nhiều lần trên dữ liệu ít thay đổi. Nếu cần join nhiều bảng, cân nhắc bảng tổng hợp dựng bằng pipeline (ví dụ dbt incremental) thay cho MV.

Anti-pattern thường gặp

  • SELECT * khi chỉ cần vài cột. Phá lợi thế columnar, buộc giải nén mọi cột, tăng bytes scanned và dễ gây spilling. Luôn liệt kê cột cần.
  • Cluster bừa. Bật clustering key trên bảng nhỏ, hoặc trên cột cardinality cực cao, hoặc trên bảng ghi ngẫu nhiên liên tục → đốt credit reclustering mà không cải thiện prune.
  • Bọc cột lọc trong hàm (TO_CHAR, CAST, UPPER...) → mất pruning.
  • Warehouse quá nhỏ cho query nặng → spilling to remote. Đôi khi warehouse lớn hơn chạy nhanh hơn và rẻ hơn vì xong nhanh, ít tràn đĩa.
  • ORDER BY cuối cùng không cần thiết trên tập lớn → sort tốn kém, dễ spill.
  • Dựa vào result cache nhưng dùng hàm không tất định (CURRENT_TIMESTAMP) khiến cache không bao giờ hit.

Use case thực tế

Bối cảnh (NCB): Bảng fact_transactions ~4 TB, ~6 tỷ dòng, nạp mỗi ngày theo txn_date. Đội báo cáo phàn nàn dashboard "Giao dịch theo chi nhánh trong ngày" (lọc txn_date = hôm nay AND branch_id = ?) chạy 25–40 giây, warehouse size M.

Chẩn đoán qua Query Profile:

  • Most expensive node: TableScan, chiếm 78% thời gian.
  • Partitions scanned = 190.000 / total 210.000 (quét ~90%!). Pruning theo txn_date ổn nhưng branch_id rải khắp mọi micro-partition.
  • Không có spilling. Vậy nút thắt là thiếu prune theo branch_id, không phải thiếu RAM.

Hành động:

  1. Kiểm tra clustering: SYSTEM$CLUSTERING_INFORMATION('fact_transactions', '(txn_date, branch_id)') → average_depth theo branch_id khoảng 45 (rất cao).
  2. Đặt clustering key (txn_date, branch_id) (thô→mịn: vẫn giữ prune theo ngày, thêm prune theo chi nhánh trong ngày).
  3. Sau 3 ngày reclustering, depth theo branch_id giảm còn ~4.

Kết quả đo lại:

Chỉ sốTrướcSau
Partitions scanned190.000 (~90%)~2.400 (~1,1%)
Thời gian truy vấn25–40 s0,9–1,5 s
Bytes scanned~28 GB~380 MB

Chi phí reclustering thêm ~2 credit/ngày — không đáng kể so với việc dashboard được ~200 lượt mở/ngày giờ đây gần như tức thì (và phần lớn lượt sau còn hit result cache, 0 credit). Đây là ví dụ điển hình: đo pruning trước, chỉ cluster khi có bằng chứng, rồi đo lại.

Ghi nhớ

  • Micro-partition ~16 MB nén cột, bất biến, mỗi cái kèm metadata min/max — đóng vai trò "index tự nhiên".
  • Pruning dựa min/max chỉ hoạt động với predicate trực tiếp trên cột; bọc cột trong hàm là mất prune.
  • Dữ liệu nạp theo thứ tự (thường là thời gian) cho natural clustering tốt; đo bằng clustering depth (SYSTEM$CLUSTERING_INFORMATION), depth thấp = prune tốt.
  • Clustering key chỉ cần cho bảng rất lớn lọc theo cột không tự nhiên; chọn cardinality thấp–trung bình, thứ tự thô→mịn; automatic reclustering tốn credit — luôn đo trước/sau.
  • Ba tầng cache: result cache (kết quả, ~24h, 0 credit), local disk cache (SSD warehouse, mất khi suspend), remote storage (nguồn gốc, chậm nhất).
  • Trong Query Profile soi: partitions scanned/total, bytes scanned, spilling (đặc biệt to remote → tăng warehouse), exploding join (thiếu điều kiện join / khoá không unique).
  • Search Optimization cho point lookup cardinality cao; Materialized View cho aggregate lặp lại trên một bảng — cả hai đều tốn credit bảo trì.
  • Anti-pattern: SELECT *, cluster bừa, bọc cột lọc trong hàm, warehouse quá nhỏ gây spill.

Bài viết liên quan

CSV/JSON/Parquet/Avro, lưu trữ theo hàng vs cột, nén, và OLTP vs OLAP.

13 thg 7, 2026 8

Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.

13 thg 7, 2026 5

Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.

13 thg 7, 2026 5

Đảm bảo chất lượng & minh bạch dữ liệu bằng dbt: data tests dựng sẵn (unique, not_null, relationships, accepted_values), singular test, unit test, và tài liệu tự sinh kèm lineage graph.

13 thg 7, 2026 5