BigQuery 5 — Chi phí & tối ưu hiệu năng
BigQuery 5 — Chi phí & tối ưu hiệu năng
Trong bài Partition & cluster chúng ta đã đặt ra một nguyên tắc vàng: byte quét ít đi = tiền ít đi VÀ nhanh hơn. Bài này mở rộng nguyên tắc đó thành một khung tư duy đầy đủ về chi phí và hiệu năng trên BigQuery — hai thứ mà trên các hệ RDBMS truyền thống thường tách rời (bạn trả tiền server cố định, tối ưu chỉ để chạy nhanh hơn), còn trên BigQuery lại quấn chặt vào nhau vì bạn trả tiền theo tài nguyên thực sự tiêu thụ.
Lưu ý về môi trường: sandbox của Knowledge Base chạy PostgreSQL, nên các đoạn
sql/bashdưới đây là minh hoạ — BigQuery, không chạy được trong sandbox. Chúng minh hoạ cú pháp và ý tưởng, hãy đối chiếu tài liệu chính thức khi triển khai thật.
Hai mô hình giá tính toán
Điều đầu tiên cần phân biệt: trên BigQuery, lưu trữ (storage) và tính toán (compute) được tính tiền tách rời. Bạn luôn trả một khoản cho storage (theo GB-tháng, có active vs long-term như đã nói ở bài Storage & tables); phần còn lại — phần thường lớn hơn và biến động mạnh — là compute, và BigQuery cho bạn chọn giữa hai mô hình.
1. On-demand — trả theo byte được quét
Đây là mô hình mặc định. Nguyên tắc đơn giản đến mức tàn nhẫn:
Bạn trả tiền theo số byte mà truy vấn đọc (quét) — không phải số dòng trả về, không phải thời gian chạy, không phải số truy vấn.
Một câu SELECT COUNT(*) FROM t WHERE d = '2026-06-30' mà buộc phải đọc cả bảng 2 TB thì bạn trả tiền cho 2 TB dữ liệu quét, dù kết quả chỉ là một con số. Một vài chi tiết dễ quên nhưng rất tốn tiền:
- Mức tối thiểu 10 MiB mỗi bảng được tham chiếu cho mỗi truy vấn. Quét một bảng bé xíu vẫn bị làm tròn lên — nên gộp nhiều truy vấn nhỏ vào một chỗ thay vì bắn hàng nghìn truy vấn tí hon.
- Byte quét được tính theo kiểu dữ liệu cột (ví dụ
INT648 byte,TIMESTAMP8 byte...), nhân với số dòng đọc — không phải theo dung lượng nén trên đĩa.
On-demand hợp khi khối lượng thưa, nhỏ, khó dự đoán: chỉ trả khi thực sự chạy.
2. Capacity / Editions — mua slots
Ở mô hình này, bạn không trả theo byte quét nữa mà mua năng lực tính toán dưới dạng slots (đơn vị CPU/RAM/IO ảo). Bạn tạo một reservation (khối slots dành riêng) theo edition (Standard / Enterprise / Enterprise Plus). Có ba cách trả:
- Autoscaling / theo giây: reservation tự co giãn số slot theo tải, tính tiền theo mức dùng — hợp lý cho tải lên xuống.
- Cam kết theo giờ / theo năm (commitments): trả trước để có giá thấp hơn, hợp lý khi tải ổn định và lớn.
Ưu điểm lớn nhất của capacity: chi phí compute cố định, dự đoán được — không "cháy túi" vì một câu SELECT * lỡ tay. Nhược điểm: mua thừa slot không dùng thì vẫn trả.
Khi nào chọn cái nào
Sơ đồ nêu một điểm tinh tế: đòn bẩy tối ưu khác nhau tuỳ mô hình. Với on-demand, bạn giảm tiền bằng cách giảm byte quét. Với capacity, byte quét không còn tính tiền trực tiếp, nên bạn tối ưu slot-time (giảm tổng công lao động của các slot: bớt shuffle, bớt skew, bớt tính toán lặp). May mắn là hầu hết kỹ thuật ở dưới có lợi cho cả hai — quét ít byte hơn thường cũng tốn ít slot-time hơn.
Giảm byte quét — chìa khoá tiết kiệm on-demand
Không dùng SELECT *
Vì BigQuery lưu trữ theo cột (columnar), nó chỉ đọc những cột bạn nhắc tới. SELECT * phá huỷ lợi thế này: nó buộc đọc mọi cột, kể cả những cột STRING/JSON khổng lồ mà bạn không cần.
-- (minh hoạ — BigQuery)
-- ĐẮT: đọc toàn bộ cột của bảng, kể cả raw_payload JSON nặng
SELECT *
FROM `proj.ds.events`
WHERE event_date = '2026-06-30';
-- RẺ: chỉ đọc đúng 3 cột cần thiết
SELECT user_id, event_type, event_ts
FROM `proj.ds.events`
WHERE event_date = '2026-06-30';
Nếu thật sự cần "gần hết" cột, dùng SELECT * EXCEPT(raw_payload, debug_blob) để loại các cột nặng. Và nếu chỉ muốn xem thử dữ liệu, đừng chạy SELECT — dùng tab Preview trên Console hoặc bq head, những cách này miễn phí vì không quét byte nào.
Partition pruning + clustering
Đây là chủ đề của bài Partition & cluster, nhưng nhắc lại trong khung chi phí: khi truy vấn lọc theo cột phân vùng, BigQuery bỏ qua các partition không liên quan (partition pruning); clustering sắp xếp dữ liệu trong partition để chỉ đọc đúng block. Để phòng lỗi "quên lọc dẫn tới quét cả bảng", hãy bật:
-- (minh hoạ — BigQuery)
CREATE TABLE `proj.ds.events`
PARTITION BY event_date
CLUSTER BY user_id
OPTIONS (require_partition_filter = TRUE); -- bắt buộc phải có WHERE trên event_date
Từ đó, một câu SELECT ... FROM events thiếu điều kiện phân vùng sẽ bị từ chối thay vì âm thầm quét cả bảng và "đốt" hoá đơn.
Xem trước chi phí: dry run
Trước khi chạy, hãy ước tính byte mà truy vấn sẽ quét. Đây là thói quen rẻ nhất và hiệu quả nhất để tránh tai nạn:
# (minh hoạ — BigQuery) — dry run: chỉ ước tính byte, KHÔNG chạy, KHÔNG tính tiền
bq query --use_legacy_sql=false --dry_run '
SELECT user_id, event_type
FROM `proj.ds.events`
WHERE event_date = "2026-06-30"
'
# -> "Query successfully validated. Assuming the tables are not modified,
# running this query will process 3489234 bytes of data."
Trong client API, đặt dryRun = true; trong bq, dùng cờ --dry_run. Kết hợp với maximum_bytes_billed để đặt trần cứng — nếu truy vấn ước tính quét vượt ngưỡng, nó fail ngay thay vì chạy và tính tiền:
-- (minh hoạ — BigQuery) — đặt trần trong session/query
SET @@maximum_bytes_billed = 50000000000; -- ~50 GB; vượt là truy vấn bị chặn
Tránh quét lại: materialized views và bảng tổng hợp
Nếu cùng một phép tổng hợp nặng bị chạy đi chạy lại (ví dụ dashboard cập nhật mỗi 5 phút), quét lại bảng gốc mỗi lần là lãng phí thuần tuý. Materialized view (MV) giải quyết việc này:
- MV precompute kết quả và tự cập nhật tăng dần (incremental refresh) trong nền khi bảng gốc thay đổi — bạn không phải làm gì.
- BigQuery có automatic rewrite / smart tuning: nếu một truy vấn (dù viết trên bảng gốc) có phần nào giải được bằng MV, optimizer tự động chuyển hướng sang đọc MV để rẻ và nhanh hơn — người viết truy vấn không cần biết MV tồn tại.
-- (minh hoạ — BigQuery)
CREATE MATERIALIZED VIEW `proj.ds.daily_active_users`
PARTITION BY event_date
AS
SELECT
event_date,
COUNT(DISTINCT user_id) AS dau,
COUNT(*) AS event_cnt
FROM `proj.ds.events`
GROUP BY event_date;
-- Sau này, ngay cả câu này (viết trên bảng gốc) cũng có thể được
-- optimizer tự động rewrite để đọc từ MV -> quét ít byte hơn nhiều:
SELECT event_date, COUNT(DISTINCT user_id) AS dau
FROM `proj.ds.events`
WHERE event_date BETWEEN '2026-06-01' AND '2026-06-30'
GROUP BY event_date;
Lưu ý giới hạn của MV: không lồng MV trong MV, không dùng trên external table / logical view / snapshot, cú pháp và hàm tổng hợp bị hạn chế, và không sửa được định nghĩa sau khi tạo (phải drop rồi tạo lại). Khi MV không đủ (join phức tạp, logic đặc thù), lựa chọn thay thế là bảng tổng hợp (summary table) do một job lịch chạy hằng ngày ghi ra — kém "tự động" hơn nhưng linh hoạt hơn.
LIMIT KHÔNG giảm byte quét
Đây là hiểu lầm phổ biến nhất, đáng nêu riêng:
SELECT ... FROM huge_table LIMIT 10trên bảng không phân vùng / không cluster vẫn quét toàn bộ bảng.LIMITchỉ cắt bớt kết quả trả về, nó không làm BigQuery đọc ít dữ liệu hơn — nên không làm giảm tiền on-demand.
Muốn chỉ đọc một phần dữ liệu, phải lọc theo cột phân vùng / cột cluster (để pruning thực sự bỏ qua block), chứ không phải LIMIT. Đây chính là lý do bài Partition & cluster quan trọng đến vậy.
Hiệu năng: đọc query execution plan
Sau khi truy vấn chạy, BigQuery cung cấp execution plan (tab "Execution details" trên Console, hoặc trường queryPlan trong job). Plan được tổ chức thành stages (giai đoạn chạy song song bằng slots), mỗi stage có các steps. Vài chỉ số quan trọng để đọc:
| Chỉ số | Ý nghĩa | Cảnh báo khi... |
|---|---|---|
Slot time (totalSlotMs) | Tổng công lao động của mọi slot | Rất cao so với thời gian thực → nhiều việc song song, có thể tối ưu |
Bytes shuffled (shuffleOutputBytes) | Dữ liệu chuyển giữa các stage | Lớn → join/aggregate tốn kém; nên lọc sớm, cluster theo khoá join |
Spilled (shuffleOutputBytesSpilled) | Shuffle tràn ra đĩa vì thiếu RAM | > 0 → thiếu tài nguyên, stage bị chậm |
Wait ratio (waitRatioAvg/Max) | Thời gian slot chờ được cấp phát | Cao → tranh chấp tài nguyên / thiếu slot |
Compute ratio (computeRatioMax vs Avg) | Phân bổ công giữa các worker | Max ≫ Avg → data skew: khoá join/nhóm lệch |
| Records in/out | Số dòng vào/ra mỗi stage | Chênh lớn = lọc/tổng hợp mạnh (thường tốt) |
Từ các chỉ số này, những "mùi" hiệu năng thường gặp và cách xử lý:
- Data skew (
computeRatioMax≫computeRatioAvg): một vài worker gánh phần lớn dữ liệu vì khoá join/GROUP BY phân bố lệch (ví dụ mộtuser_id"rác" chiếm 40% số dòng). Cân nhắc tách trường hợp lệch ra xử lý riêng, hoặc salt khoá. - Shuffle khổng lồ: thường do join hai bảng lớn chưa lọc. Lọc sớm (đẩy
WHERExuống trước join), cluster theo cột join, hoặc dựng MV cho phần aggregate lặp. - Cross join bùng nổ: một
JOINthiếu điều kiện (hoặcCROSS JOINvô tình) tạo tích Descartes — số dòng nổ theo cấp số nhân. Luôn kiểm trarecords outcủa stage join.
Về thứ tự JOIN và broadcast: BigQuery tự tối ưu (chọn broadcast bảng nhỏ, sắp xếp lại join), nên bạn ít khi cần can thiệp thủ công như trên Oracle. Nhưng nguyên tắc "lọc trước, join sau" luôn giúp: dữ liệu vào join càng ít, shuffle càng nhỏ.
Cuối cùng, approximate functions rẻ hơn hẳn khi bạn chấp nhận sai số nhỏ:
-- (minh hoạ — BigQuery)
-- Chính xác nhưng tốn slot-time (phải giữ mọi giá trị distinct):
SELECT COUNT(DISTINCT user_id) FROM `proj.ds.events`;
-- Xấp xỉ (sai số ~vài %), nhanh & rẻ hơn nhiều trên tập lớn:
SELECT APPROX_COUNT_DISTINCT(user_id) FROM `proj.ds.events`;
Ngoài APPROX_COUNT_DISTINCT, còn có APPROX_QUANTILES, APPROX_TOP_COUNT... — rất hợp cho dashboard nơi con số "gần đúng" là đủ.
BI Engine — tăng tốc dashboard
Với dashboard (đặc biệt là Looker và Looker Studio), BI Engine là bộ nhớ đệm in-memory thông minh: nó tự động nạp các phần dữ liệu hay dùng nhất vào RAM, giúp truy vấn trả về gần như tức thì thay vì quét lại storage mỗi lần.
- Bạn mua BI Engine reservation (dung lượng RAM theo project/region).
- Có thể chỉ định preferred tables để ưu tiên nạp các bảng/partition quan trọng.
- Hoạt động trong suốt: dashboard đang chạy trên BigQuery tự động hưởng lợi, không cần đổi truy vấn.
Kết hợp rất tốt với MV: MV thu nhỏ dữ liệu (precompute aggregate), BI Engine giữ phần thu nhỏ đó trong RAM → dashboard vừa rẻ vừa nhanh.
Caching — kết quả truy vấn miễn phí
BigQuery cache kết quả truy vấn và phục vụ lại miễn phí (không tính byte quét) nếu:
- Truy vấn giống hệt (cùng chuỗi SQL, cùng tham số), và
- Bảng nguồn chưa thay đổi kể từ lần chạy trước, và
- Truy vấn không có hàm không tất định (
CURRENT_TIMESTAMP(),RAND()...), không đọc dữ liệu streaming đang thay đổi.
Cached result mặc định bật; kết quả cache tồn tại khoảng 24 giờ. Đây là lý do lần chạy thứ hai của một dashboard tĩnh thường "0 byte, 0 đồng". Đừng phá cache vô ý bằng cách chèn NOW() vào mọi truy vấn.
Giám sát & kiểm soát chi phí
Không đo thì không quản được. BigQuery phơi bày lịch sử job qua INFORMATION_SCHEMA.JOBS — nguồn sự thật để biết ai chạy truy vấn nào quét bao nhiêu byte:
-- (minh hoạ — BigQuery) — top người dùng theo byte quét trong 30 ngày
SELECT
user_email,
COUNT(*) AS job_cnt,
SUM(total_bytes_billed) / POW(1024, 4) AS tib_billed,
SUM(total_slot_ms) / 1000 / 3600 AS slot_hours
FROM `region-us`.INFORMATION_SCHEMA.JOBS
WHERE creation_time >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 30 DAY)
AND job_type = 'QUERY'
AND statement_type != 'SCRIPT'
GROUP BY user_email
ORDER BY tib_billed DESC
LIMIT 20;
Các đòn bẩy kiểm soát khác:
- Custom cost controls / quotas: giới hạn số byte xử lý mỗi ngày theo project hoặc theo user — chặn tai nạn "một câu query đốt cả ngân sách".
maximum_bytes_billed: trần cứng ở mức truy vấn/session (đã nói ở trên).- Phân bổ chi phí theo label: gắn label (
--label team:analytics) vào job/dataset, rồi nhóm chi phí theo label trong billing export để biết đội nào tốn tiền vào việc gì — nền tảng của chargeback nội bộ. - Budget alerts ở mức billing account để nhận cảnh báo khi vượt ngưỡng.
Use case thực tế: giảm hoá đơn cho một dashboard
Bối cảnh: một dashboard "Hoạt động người dùng theo ngày" trên Looker Studio, chạy trên bảng events 50 TB không phân vùng, làm mới mỗi 10 phút. Hoá đơn on-demand tăng vọt vì mỗi lần refresh quét gần cả bảng. Chuỗi tối ưu:
- Bỏ
SELECT *: truy vấn nguồn của dashboard chỉ cầnevent_date, user_id, event_type. Riêng việc chỉ đọc 3 cột thay vì ~40 cột (trong đó có cột JSON payload nặng) đã cắt phần lớn byte quét. - Partition theo
event_date+ cluster theouser_id, bậtrequire_partition_filter. Dashboard chỉ xem 30 ngày gần nhất → pruning bỏ qua toàn bộ dữ liệu cũ, quét vài chục GB thay vì 50 TB. - Materialized view
daily_active_users: precomputeCOUNT(DISTINCT user_id)theo ngày. Optimizer tự rewrite truy vấn dashboard sang MV → mỗi refresh chỉ đọc kết quả precompute nhỏ xíu. - BI Engine reservation với preferred table là MV: giữ dữ liệu trong RAM → dashboard trả về gần tức thì, và các refresh liên tiếp phần lớn hit cache 0 byte.
- Giám sát bằng
INFORMATION_SCHEMA.JOBSvới labeldashboard:user-activityđể xác nhận byte quét đã giảm và theo dõi tiếp.
Kết quả điển hình: byte quét mỗi refresh giảm nhiều bậc độ lớn (từ TB xuống MB), độ trễ dashboard giảm mạnh, và chi phí compute từ "biến động đáng sợ" thành "nhỏ và ổn định". Nếu tải phân tích tổng thể của tổ chức lớn và đều, đây cũng là lúc cân nhắc chuyển từ on-demand sang capacity/editions để cố định luôn chi phí compute.
Ghi nhớ
- Storage và compute tính tiền tách rời. Compute có hai mô hình: on-demand (theo byte quét) và capacity/slots (mua năng lực). Thưa/nhỏ → on-demand; lớn/ổn định → capacity.
- On-demand: đòn bẩy là byte quét. Không
SELECT *, lọc theo cột phân vùng/cluster, dựng MV/bảng tổng hợp cho phép tính lặp. LIMITKHÔNG giảm byte quét trên bảng chưa phân vùng — hiểu lầm tốn tiền phổ biến nhất.- Luôn dry run để ước tính byte trước khi chạy; đặt
maximum_bytes_billedlàm trần cứng; xem trước dữ liệu bằng Preview/bq head(miễn phí). - Materialized view tự cập nhật tăng dần và được optimizer tự rewrite; BI Engine giữ dữ liệu nóng trong RAM cho dashboard; kết quả cache phục vụ lại miễn phí khi truy vấn giống hệt và bảng chưa đổi.
- Đọc query plan: soi slot time, bytes shuffled, wait ratio, và
computeRatioMaxvsAvg(skew). Lọc sớm, tránh cross join, dùngAPPROX_*khi chấp nhận sai số. - Giám sát bằng
INFORMATION_SCHEMA.JOBS, đặt quotas/custom cost controls, gắn label để phân bổ chi phí. - Tránh phụ thuộc vào con số giá tuyệt đối trong đầu — luôn kiểm tra bảng giá hiện hành của BigQuery vì đơn giá và cấu trúc edition thay đổi theo thời gian.
Đọc tiếp
- BigQuery 4 — Partition & cluster: nền tảng của việc giảm byte quét.
- BigQuery 3 — SQL features: các hàm và cấu trúc SQL trên BigQuery.
- BigQuery 2 — Storage & tables: mô hình lưu trữ cột và cách tính storage.
- BigQuery 7 — Bảo mật & ops: label, quota, quản trị vận hành.
- Looker — Tổng quan: tầng BI đứng trên BigQuery, nơi BI Engine phát huy tác dụng.
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.