BigQuery 4 — Partitioning & clustering
BigQuery 4 — Partitioning & clustering
Trong bài Storage & tables chúng ta đã thấy BigQuery lưu dữ liệu ở định dạng cột (columnar) và tách rời hoàn toàn khỏi lớp tính toán. Bài này trả lời câu hỏi thực tế nhất khi bảng của bạn lớn dần: làm sao để một truy vấn không phải đọc toàn bộ bảng? Câu trả lời có hai tầng — partitioning (phân vùng) và clustering — và hiểu đúng chúng là ranh giới giữa một hoá đơn BigQuery vài chục đô một tháng và một hoá đơn vài nghìn đô.
Vì sao chuyện này đáng tiền — theo nghĩa đen
Với mô hình on-demand (mặc định), BigQuery tính tiền theo số byte mà truy vấn quét, không phải theo số dòng trả về, cũng không theo thời gian chạy. Một câu SELECT COUNT(*) FROM t WHERE d = '2026-06-30' nếu buộc phải đọc cả bảng 2 TB thì bạn trả tiền cho 2 TB — dù kết quả chỉ là một con số.
Vì lưu trữ theo cột, BigQuery đã có sẵn một mức tối ưu: nó chỉ đọc những cột bạn nhắc tới trong truy vấn. Đây là lý do SELECT * là một thói quen đắt đỏ (chi tiết ở bài Chi phí & hiệu năng). Nhưng columnar chỉ giảm chiều ngang (số cột). Partitioning và clustering giảm chiều dọc (số dòng phải đọc trong mỗi cột). Ghép cả hai lại: truy vấn chỉ đọc đúng vài cột, trong đúng vài phần dữ liệu.
Nguyên tắc vàng của cả bài gói gọn một câu:
Byte quét ít đi = tiền ít đi VÀ nhanh hơn. Đây là trường hợp hiếm hoi mà tối ưu chi phí và tối ưu tốc độ trùng khít nhau.
Partitioning — chia bảng thành các mảnh có tên
Partition là một mảnh vật lý của bảng, được xác định bởi giá trị của một cột (hoặc thời điểm nạp dữ liệu). Toàn bộ dữ liệu của một partition được lưu tách biệt với các partition khác. Khi truy vấn lọc theo cột phân vùng, BigQuery chỉ mở những mảnh liên quan và bỏ qua phần còn lại.
Điểm cốt lõi phải nhớ: một bảng chỉ được phân vùng theo đúng một tiêu chí (một cột time-unit, hoặc ingestion time, hoặc một cột integer). Không có "partition theo 2 cột" như một số hệ khác. Muốn cắt nhỏ thêm, bạn dùng clustering (phần sau).
BigQuery hỗ trợ ba kiểu phân vùng:
1. Time-unit column — phân vùng theo cột thời gian
Phổ biến nhất. Bạn chọn một cột DATE, TIMESTAMP hoặc DATETIME làm khoá phân vùng, với độ mịn (granularity):
- Cột
DATE: theo ngày / tháng / năm. - Cột
TIMESTAMP/DATETIME: theo giờ / ngày / tháng / năm.
Ranh giới partition được tính theo UTC. Đây là chi tiết dễ gây lỗi: nếu nghiệp vụ của bạn theo giờ Việt Nam (UTC+7), một giao dịch lúc 06:00 sáng ngày 30/6 giờ VN thực ra rơi vào partition 2026-06-29 theo UTC. Hãy thống nhất múi giờ từ đầu.
-- (minh hoạ — BigQuery)
CREATE TABLE sales.events
(
event_time TIMESTAMP,
customer_id INT64,
amount NUMERIC,
country STRING
)
PARTITION BY DATE(event_time) -- phân vùng theo ngày (rút DATE từ TIMESTAMP)
OPTIONS (
require_partition_filter = TRUE,
partition_expiration_days = 400
);
2. Ingestion-time — phân vùng theo thời điểm nạp
Nếu dữ liệu không có sẵn cột thời gian nghiệp vụ đáng tin, bạn có thể để BigQuery gán mỗi dòng vào partition dựa trên thời điểm nó được nạp. Khi đó bảng có hai pseudocolumn (cột giả, không tốn lưu trữ):
_PARTITIONTIME—TIMESTAMPđầu ngày UTC của partition._PARTITIONDATE—DATEtương ứng.
-- (minh hoạ — BigQuery)
CREATE TABLE logs.raw_events (payload STRING)
PARTITION BY _PARTITIONDATE
OPTIONS (require_partition_filter = TRUE);
-- Truy vấn phải lọc trên pseudocolumn
SELECT COUNT(*)
FROM logs.raw_events
WHERE _PARTITIONDATE BETWEEN '2026-06-01' AND '2026-06-30';
Ưu điểm: đơn giản, không cần chuẩn bị cột. Nhược điểm: "ngày nạp" không phải lúc nào cũng bằng "ngày phát sinh sự kiện" — nếu bạn nạp bù dữ liệu cũ (backfill), tất cả rơi vào partition của hôm nạp.
3. Integer range — phân vùng theo dải số nguyên
Dùng khi khoá phân vùng tự nhiên là một số nguyên (ví dụ customer_id, region_id). Bạn khai báo giá trị đầu, giá trị cuối và bước nhảy:
-- (minh hoạ — BigQuery)
CREATE TABLE crm.accounts (account_id INT64, name STRING)
PARTITION BY RANGE_BUCKET(account_id, GENERATE_ARRAY(0, 100000, 1000));
-- 100 partition, mỗi cái gom 1000 account_id
Giá trị nằm ngoài dải rơi vào partition đặc biệt __UNPARTITIONED__; giá trị NULL rơi vào __NULL__.
Ba "van an toàn" cần biết
- Giới hạn số partition. Một bảng có trần 10.000 partition. Con số này quyết định chọn granularity: phân vùng theo giờ chỉ đủ cho ~416 ngày dữ liệu (10.000 / 24), nên với bảng lịch sử dài hạn hãy dùng theo ngày (đủ ~27 năm) hoặc theo tháng.
require_partition_filter = TRUE. Bật lên thì mọi truy vấn bắt buộc phải có điều kiện lọc trên cột phân vùng, nếu không BigQuery từ chối chạy. Đây là "phanh tay" chống việc lỡ tay quét cả bảng tỷ dòng — cực kỳ nên bật cho bảng lớn.- Partition expiration. Đặt
partition_expiration_daysđể BigQuery tự xoá partition quá hạn. Đây là cách rẻ và sạch nhất để giữ lịch sử lăn (rolling window) — ví dụ chỉ giữ 400 ngày gần nhất — thay vì viết jobDELETEđịnh kỳ.
Partition pruning — cơ chế thực sự tiết kiệm tiền
Partition pruning là hành vi BigQuery đọc điều kiện WHERE trên cột phân vùng và chỉ mở đúng các partition khớp. Đây mới là nơi tiền được cắt. Partitioning mà không có pruning thì vô nghĩa.
-- (minh hoạ — BigQuery)
-- Bảng events partition theo DATE(event_time), tổng 2 TB, mỗi ngày ~5 GB
SELECT customer_id, SUM(amount) AS total
FROM sales.events
WHERE DATE(event_time) BETWEEN '2026-06-01' AND '2026-06-30' -- prune: chỉ 30 partition
GROUP BY customer_id;
-- Quét ~150 GB thay vì 2 TB → rẻ hơn ~13 lần
Điều quyết định pruning có xảy ra hay không là: biểu thức lọc phải để BigQuery nhìn thấy giá trị partition tại thời điểm lập kế hoạch. Có vài cái bẫy khiến pruning biến mất âm thầm (truy vấn vẫn đúng, chỉ là quét sạch bảng và bạn phải trả tiền):
- Bọc hàm không "cắt được" lên cột partition.
WHERE DATE(event_time) = '2026-06-30'thì pruning tốt (vì bảng vốn partition theoDATE(event_time)). NhưngWHERE CAST(event_time AS STRING) LIKE '2026-06-30%'biến cột thành một biểu thức mà bộ lập kế hoạch không map được về partition → mất pruning. - So sánh với giá trị không xác định lúc lập kế hoạch. Nếu điều kiện lọc partition đến từ một subquery (
WHERE d = (SELECT MAX(d) FROM ...)), BigQuery thường không prune được vì chưa biết giá trị khi lập plan. Hãy tính giá trị ra trước rồi truyền vào như một tham số/hằng. ORbắc cầu sang cột khác.WHERE d = '2026-06-30' OR country = 'VN'buộc phải xét mọi dòng để kiểm điều kiện thứ hai → mất pruning.
Mẹo kiểm tra: trước khi chạy, dùng dry run hoặc nhìn ước tính "This query will process X bytes" trên console. Nếu X gần bằng kích thước cả bảng dù bạn đã lọc theo ngày → pruning đang hỏng, sửa WHERE trước khi bấm chạy.
Clustering — sắp xếp dữ liệu bên trong
Clustering sắp xếp vật lý dữ liệu trong bảng theo giá trị của tối đa 4 cột. Sau khi sắp xếp, các dòng có giá trị gần nhau nằm chung trong các storage block. Khi truy vấn lọc/gộp/join theo cột cluster, BigQuery làm block pruning: chỉ đọc các block có thể chứa giá trị cần, bỏ qua các block khác.
Điểm khác biệt bản chất so với partitioning:
| Partitioning | Clustering | |
|---|---|---|
| Đơn vị cắt | Partition (mảnh có tên) | Storage block (bên trong dữ liệu) |
| Số nhóm | Có trần (10.000) | Không giới hạn, tự động |
| Chi phí biết trước? | Có — dry run báo byte chính xác | Không — chỉ biết byte thực tế sau khi chạy |
| Quản lý | Bạn khai báo khoá + granularity | BigQuery tự sắp xếp, tự re-cluster ngầm |
| Số cột | 1 tiêu chí | tối đa 4 cột, theo thứ tự |
Thứ tự cột cluster là quan trọng — quy tắc prefix
Clustering hoạt động như một khoá sắp xếp phức hợp (composite sort key). Vì thế truy vấn được lợi khi lọc theo cột cluster từ trái sang, theo tiền tố (prefix). Với bảng CLUSTER BY country, status, event_date:
- Lọc theo
country→ tốt. - Lọc theo
countryvàstatus→ rất tốt. - Lọc chỉ theo
status(bỏ quacountry) → không được block pruning hiệu quả, vì dữ liệu chưa được gom theostatustrước.
Quy tắc thực hành: cột được lọc thường xuyên nhất / có tính chọn lọc cao nhất để lên đầu.
Vài lưu ý định lượng từ tài liệu Google: clustering chỉ thật sự phát huy khi bảng đủ lớn (từ khoảng 64 MB trở lên, dưới ngưỡng đó cả bảng đã là một khối nhỏ nên chẳng có gì để prune). Ngược lại, partitioning phát huy tốt nhất khi mỗi partition đủ "dày" — trung bình từ vài GB trở lên; nếu partition quá nhỏ và vụn thì overhead metadata lấn át lợi ích.
Phối hợp cả hai — mẫu tối ưu cho bảng sự kiện lớn
Đây là kết hợp "kinh điển" và là câu trả lời cho hầu hết bảng lớn trong thực tế:
Partition theo thời gian (ngày) + Cluster theo cột lọc/join thường gặp (ví dụ
customer_id).
Cơ chế xếp tầng: BigQuery partition trước → truy vấn lọc theo ngày chỉ chạm vào vài partition. Bên trong mỗi partition, dữ liệu lại được cluster theo customer_id → nếu truy vấn còn lọc thêm theo khách hàng, block pruning cắt tiếp bên trong. Hai lớp giảm byte chồng lên nhau.
-- (minh hoạ — BigQuery)
CREATE TABLE sales.events
(
event_time TIMESTAMP,
customer_id INT64,
amount NUMERIC,
country STRING
)
PARTITION BY DATE(event_time) -- lớp 1: cắt theo ngày
CLUSTER BY customer_id, country -- lớp 2: sắp xếp trong partition
OPTIONS (
require_partition_filter = TRUE,
partition_expiration_days = 730
);
-- Truy vấn tận dụng CẢ hai lớp
SELECT SUM(amount) AS total
FROM sales.events
WHERE DATE(event_time) BETWEEN '2026-03-01' AND '2026-03-31' -- partition pruning
AND customer_id = 42; -- block pruning
Use case thực tế — bảng giao dịch tỷ dòng
Hãy đặt vào một bài toán ngân hàng/thanh toán: bảng transactions 1,5 tỷ dòng, ~2 TB, tăng thêm ~5 GB mỗi ngày. Các truy vấn phổ biến: (a) báo cáo theo khoảng ngày, (b) tra sao kê của một tài khoản trong một khoảng ngày.
Nếu để bảng "phẳng" (không partition, không cluster): mỗi truy vấn quét gần 2 TB. Với đơn giá on-demand ~5 USD/TB (kiểm tra bảng giá hiện hành theo region của bạn), một truy vấn ~10 USD. 200 truy vấn/ngày của đội phân tích ≈ 2.000 USD/ngày — chưa làm gì đã cháy ngân sách.
Thiết kế đúng: PARTITION BY DATE(txn_time) + CLUSTER BY account_id + require_partition_filter = TRUE.
- Truy vấn (a) — báo cáo 1 tháng: pruning xuống ~30 partition ≈ 150 GB. Từ 10 USD còn ~0,75 USD.
- Truy vấn (b) — sao kê 1 tài khoản trong 1 tháng: partition pruning về 30 ngày, rồi block pruning theo
account_idcắt tiếp trong mỗi ngày — thực tế thường chỉ đọc vài trăm MB. Chi phí gần như không đáng kể. require_partition_filterchặn đứng những truy vấn "quênWHERE" — thủ phạm số một của hoá đơn sốc.partition_expiration_daysgiữ cửa sổ lịch sử theo quy định (ví dụ 7 năm với dữ liệu tài chính, hoặc ngắn hơn cho log) mà không cần job dọn dẹp thủ công.
Cùng một dữ liệu, cùng một câu hỏi nghiệp vụ — chỉ khác ở DDL — chi phí chênh nhau hơn một bậc độ lớn.
So với partition của Oracle/Postgres
Nếu bạn đến từ Postgres partitioning & sharding hoặc từ Oracle, hãy chú ý mấy khác biệt về cơ chế:
- BigQuery tự quản. Không có
CREATE TABLE ... PARTITION OFcho từng partition con như declarative partitioning của Postgres, cũng không có "local index" / "global index" như Oracle. Bạn khai báo khoá phân vùng một lần, BigQuery tạo và xoá partition tự động. - Không có index B-tree. Postgres/Oracle tăng tốc bằng chỉ mục B-tree; BigQuery không có index kiểu đó cho truy vấn phân tích thông thường. Vai trò "cắt bớt dữ liệu đọc" được đảm nhiệm bởi partitioning + clustering + lưu trữ cột. Đây là thay đổi tư duy lớn: đừng đi tìm
CREATE INDEX, hãy thiết kế khoá phân vùng và cột cluster. - Clustering ≠ list/hash partition.
CLUSTER BYcủa BigQuery gần với "khoá sắp xếp" hơn là "partition theo list/hash" của Postgres — nó không tạo các nhóm rời rạc có tên, mà sắp xếp liên tục và để BigQuery tự re-cluster ngầm.
Điểm chung về tư tưởng thì vẫn y hệt: đọc ít dữ liệu hơn thì nhanh hơn. Chỉ là công cụ để đạt điều đó khác nhau.
Ghi nhớ
- Chi phí = byte quét. Mọi kỹ thuật trong bài đều nhằm giảm byte đọc; giảm byte đồng thời giảm tiền và tăng tốc.
- Bảng lớn → luôn partition theo thời gian. Chọn granularity sao cho không vượt trần 10.000 partition (thường là theo ngày).
- Cluster theo cột hay lọc/join nhất, đặt cột chọn lọc cao lên đầu (quy tắc prefix, tối đa 4 cột).
- Mẫu vàng: partition theo ngày + cluster theo
customer_id/account_idcho bảng sự kiện/giao dịch. - Bật
require_partition_filter = TRUEcho bảng lớn — phanh tay chống quét toàn bảng. - Coi chừng mất pruning: đừng bọc hàm không-cắt-được lên cột partition, tránh lọc partition bằng subquery, cẩn thận với
OR. - Dry run trước khi chạy để xác nhận số byte — nếu gần bằng cả bảng thì
WHEREđang sai. - Đừng
SELECT *— columnar chỉ giúp khi bạn chọn đúng cột (xem Chi phí & hiệu năng).
Đọc tiếp
- BigQuery 2 — Storage & tables: nền tảng lưu trữ cột mà partitioning/clustering đứng trên.
- BigQuery 3 — SQL features: cú pháp truy vấn để khai thác bảng đã tối ưu.
- BigQuery 5 — Chi phí & hiệu năng: dry run, ước lượng byte, slot và các đòn tối ưu chi phí khác.
- Tối ưu 8 — Phân vùng, sharding & mở rộng quy mô: partitioning ở Postgres để đối chiếu cơ chế.
Bài viết liên quan
Cách index hoạt động (B-Tree), đọc EXPLAIN, seq scan vs index scan và mẫu tối ưu truy vấn.
Khoá, ràng buộc, quan hệ và chuẩn hoá 1NF/2NF/3NF — thiết kế lược đồ đúng từ đầu.
Kết nhiều bảng đúng cách: các loại JOIN, bẫy thường gặp và phép hợp tập.
Truy vấn lồng, CTE (WITH), CTE đệ quy và hàm cửa sổ — vũ khí cho phân tích nâng cao.