FinOps 2 — Tối ưu chi phí Data Warehouse

13 thg 7, 2026 2 lượt xem
#data-warehouse
#data-engineering
#snowflake
#bigquery
#finops

Vì sao warehouse thường là hóa đơn lớn nhất

Trong hầu hết team dữ liệu, khi mở bảng chi phí cloud ra thì dòng đắt nhất không phải là storage (lưu trữ) cũng không phải pipeline ingest, mà là data warehouse — nơi analyst, dashboard và job dbt cùng đổ về để query. Ở bài tổng quan FinOps chúng ta đã thấy nguyên tắc: chi phí đi theo mức sử dụng, và ai tạo ra query thì thường không phải người trả hóa đơn. Warehouse là nơi biểu hiện rõ nhất nghịch lý đó: một analyst gõ SELECT * FROM transactions trong 3 giây, nhưng câu đó có thể quét vài terabyte và tốn bằng cả ngày lương.

Điều làm warehouse khó kiểm soát là chi phí ẩn sau độ tiện lợi. Warehouse đám mây được thiết kế để "chạy là ra kết quả", không bắt bạn nghĩ về I/O, về index, về số byte đọc. Chính sự dễ dàng đó khiến chi phí trôi tuột: một dashboard auto-refresh mỗi 5 phút, một JOIN thiếu điều kiện, một bảng trung gian không ai dọn — mỗi thứ nhỏ, cộng lại thành hóa đơn tăng gấp đôi sau mỗi quý.

Muốn tối ưu, trước hết phải hiểu bạn đang bị tính tiền theo cái gì. Có hai mô hình tính phí chủ đạo, và chúng đòi hỏi hai tư duy tối ưu khác nhau.

Mô hình 1 — Tính phí theo dữ liệu quét (BigQuery on-demand)

Với BigQuery ở chế độ on-demand, bạn trả tiền theo số byte mà query đọc (bytes scanned), không phải theo thời gian chạy. Giá tính trên terabyte quét, làm tròn tối thiểu mỗi query. Hệ quả tư duy rất rõ ràng: mọi tối ưu đều quy về một mục tiêu duy nhất — giảm số byte phải đọc. Chi tiết mô hình này được đào sâu ở BigQuery — chi phí & hiệu năng; dưới đây là các đòn bẩy cốt lõi.

Partitioning — chia bảng theo thời gian/khóa

Partition (phân vùng) chia một bảng lớn thành nhiều mảnh theo một cột, thường là ngày. Khi query có điều kiện lọc trên cột partition, engine chỉ đọc đúng các partition liên quan (partition pruning), bỏ qua phần còn lại. Một bảng giao dịch 3 năm, query WHERE ngay = CURRENT_DATE() chỉ chạm 1/1000 dữ liệu.

Nguyên tắc: partition theo cột thường xuất hiện trong WHERE, phổ biến nhất là ngày giao dịch. Đừng partition theo cột có cardinality quá cao (như id khách) — sẽ tạo hàng triệu partition nhỏ, phản tác dụng.

Clustering — sắp xếp trong partition

Clustering sắp xếp vật lý dữ liệu theo một số cột (ví dụ chi_nhanh, loai_giao_dich). Khi lọc theo cột clustered, engine bỏ qua các block không khớp mà không cần đọc chúng. Partition thu hẹp theo ngày, cluster thu hẹp thêm bên trong ngày theo các chiều truy vấn hay dùng.

Chỉ SELECT cột cần — tránh SELECT *

Đây là điểm dễ bị bỏ qua nhất và cũng dễ sửa nhất. BigQuery lưu theo cột (columnar). SELECT * buộc đọc mọi cột; chỉ lấy 3 cột cần có thể giảm byte quét vài lần. Bảng giao dịch có 40 cột nhưng report chỉ cần currency, balanceSELECT * lãng phí ~95% dữ liệu đọc.

Materialized view & kết quả tính sẵn

Materialized view (khung nhìn vật chất hóa) lưu sẵn kết quả một phép tổng hợp và tự cập nhật khi bảng gốc đổi. Query đọc từ view đã tính chỉ quét vài dòng thay vì tính lại triệu dòng. Với báo cáo lặp đi lặp lại (số dư cuối ngày theo chi nhánh), đây là đòn bẩy lớn.

BI Engine / cache & hạn mức chi phí

  • Cache kết quả: query trùng lặp trả về từ cache miễn phí (0 byte tính tiền) nếu bảng gốc chưa đổi. BI Engine là lớp cache trong bộ nhớ cho dashboard, cắt phần lớn chi phí của Looker/Data Studio auto-refresh.
  • Custom quota / hạn mức: đặt hạn mức byte quét theo project hoặc theo user để chặn một query "chạy nhầm" quét cả petabyte. Đây là lưới an toàn — không tối ưu chi phí bình thường nhưng chặn thảm họa.
  • Ước tính trước khi chạy: BigQuery báo ước lượng byte sẽ quét trước khi execute (dry run). Tập cho analyst thói quen nhìn con số đó trước khi bấm run là một thay đổi văn hóa rẻ mà hiệu quả.

Mô hình 2 — Tính phí theo thời gian compute (Snowflake)

Snowflake tính tiền theo thời gian warehouse (compute cluster) chạy, quy ra credit theo giây (tối thiểu 60 giây khi resume). Cùng một warehouse chạy 10 phút hay chạy query nặng hay nhẹ trong 10 phút đó, chi phí xấp xỉ như nhau — cái bị tính là thời gian bật, không phải byte đọc. Tư duy tối ưu vì thế khác hẳn: giảm thời gian warehouse phải bật, và bật đúng kích cỡ. Chi tiết ở Snowflake — quản lý chi phí.

Chọn size phù hợp (right-sizing)

Warehouse Snowflake có size XS, S, M, L... mỗi bậc gấp đôi số credit/giờ. Size lớn hơn chạy nhanh hơn cho query nặng, nhưng nếu query nhẹ thì bạn trả gấp đôi để hoàn thành sớm vài giây — lỗ. Nguyên tắc: bắt đầu nhỏ, chỉ tăng size khi query thực sự bị nghẽn compute, không tăng vì "cho chắc".

Auto-suspend ngắn

Warehouse để bật mà không query nào chạy vẫn tính tiền. Auto-suspend tự tắt warehouse sau N giây rảnh. Đặt ngưỡng ngắn (ví dụ 60 giây cho warehouse phục vụ dashboard rời rạc) cắt phần "bật không mà chạy". Đánh đổi: resume mất vài giây và mất cache cục bộ — nên với warehouse query liên tục thì để ngưỡng dài hơn.

Tách warehouse theo workload

Đừng để job ETL nặng ban đêm và dashboard analyst ban ngày dùng chung một warehouse. Tách ra: một warehouse cho ETL (size lớn, chạy theo lịch), một cho BI (size nhỏ, auto-suspend ngắn), một cho ad-hoc. Lợi ích kép: tính được chi phí theo từng nhóm (chargeback/showback) và mỗi warehouse tune riêng đúng đặc thù workload.

Resource monitor

Resource monitor đặt trần credit theo tháng/tuần cho một warehouse, và có thể tự tắt khi chạm ngưỡng. Đây là phanh khẩn cấp: một job vòng lặp vô tận không thể tự nó đốt hết ngân sách quý.

Vẫn phải giảm dữ liệu quét

Dù tính theo thời gian, giảm dữ liệu đọc vẫn giúp query chạy nhanh hơn → warehouse bật ngắn hơn → rẻ hơn. Snowflake dùng micro-partition pruning: dữ liệu tự chia micro-partition, và query lọc theo cột có tính clustering tốt sẽ bỏ qua micro-partition không liên quan. Nguyên tắc "chọn cột cần, lọc sớm, tránh full scan" vẫn đúng — chỉ khác cơ chế tính tiền.

Nguyên nhân chi phí phổ biến (cả hai mô hình)

Nguyên nhânBiểu hiệnCách xử lý
Query quét toàn bảngThiếu WHERE trên cột partitionÉp lọc theo ngày; partition/cluster
SELECT *Lấy 40 cột, dùng 3Chỉ chọn cột cần
JOIN nổKết quả nhân bản do khóa join saiKiểm tra cardinality, dùng khóa đúng
Dashboard auto-refresh dàyCùng query chạy mỗi vài phútGiãn tần suất, dùng cache/BI Engine
Full-refresh thay incrementalJob dbt build lại toàn bộ mỗi giờChuyển incremental
Bảng trung gian không dọnStaging/temp tích tụ, bị query lạiDọn định kỳ, đặt TTL

Nguyên tắc GIẢM DỮ LIỆU QUÉT — minh họa chạy được trên Postgres

Warehouse của bạn có thể là BigQuery hay Snowflake, nhưng nguyên tắc lõi thì phổ quát: đọc ít dữ liệu hơn thì rẻ hơn (dù đơn vị tính là byte quét hay giây compute). Sandbox của Knowledge Base là PostgreSQL, nhưng ba nguyên tắc dưới đây minh họa đúng tư duy đó và chạy được trên sandbox. Đây là minh họa nguyên lý, không phải cú pháp BigQuery/Snowflake.

(a) EXPLAIN — nhìn thấy engine chỉ đọc cái cần

Trước khi tối ưu, phải thấy engine đang đọc gì. EXPLAIN cho biết kế hoạch thực thi: chỉ chiếu ra các cột cần và lọc theo điều kiện. Đây chính là "dry run" — thói quen nên áp cho mọi query đắt trước khi chạy thật trên warehouse.

-- ▶ Chạy được
EXPLAIN SELECT currency, SUM(balance) FROM accounts WHERE currency = 'VND' GROUP BY currency;

Trên warehouse tính phí theo byte, kế hoạch tương đương sẽ báo ước lượng byte quét — con số quyết định hóa đơn. Nhìn plan trước khi run là cách rẻ nhất để tránh một query đắt bất ngờ.

(b) Chỉ chọn cột cần, không lấy toàn bộ dòng

Đây là bản dịch trực tiếp của nguyên tắc "tránh SELECT *". Thay vì kéo mọi cột của accounts, ta chỉ lấy đúng hai cột cần cho báo cáo số dư theo loại tiền tệ. Trên kho columnar (BigQuery/Snowflake), khác biệt này trực tiếp cắt số byte đọc.

-- ▶ Chạy được
SELECT currency, SUM(balance) AS tong_so_du FROM accounts GROUP BY currency ORDER BY tong_so_du DESC;

So với SELECT * FROM accounts, câu trên chỉ đụng hai cột currencybalance — đó chính là phần "chỉ đọc cột cần" mà columnar warehouse thưởng công bằng hóa đơn nhỏ hơn.

(c) Tổng hợp có điều kiện lọc — thu hẹp trước khi tính

Kết hợp cả hai: lọc sớm (WHERE) để giảm dòng phải đọc, rồi chỉ tổng hợp cột cần. Trên warehouse, WHERE trên cột partition/clustered là thứ kích hoạt pruning — bỏ qua phần lớn dữ liệu.

-- ▶ Chạy được
SELECT account_id, ROUND(SUM(amount)::numeric, 2) AS tong_giao_dich, COUNT(*) AS so_lenh
FROM transactions
WHERE kind = 'debit' AND created_at >= DATE '2024-01-01'
GROUP BY account_id
ORDER BY tong_giao_dich DESC;

Điều kiện created_at >= ... chính là mô phỏng partition pruning theo ngày: engine warehouse thật sẽ chỉ đọc các partition trong khoảng đó thay vì toàn bộ lịch sử. kind = 'debit' mô phỏng lọc theo cột clustered. Ta cũng chỉ tổng hợp amount chứ không kéo mọi cột — đúng nguyên tắc (b).

Lưu ý: ba block trên chạy trên Postgres để minh họa tư duy. Cú pháp partition/cluster/materialized view của BigQuery và warehouse sizing của Snowflake là phương ngữ riêng, không chạy trên sandbox này.

Tối ưu ở tầng mô hình dữ liệu & lịch chạy

Tối ưu từng query là chữa triệu chứng; tối ưu ở tầng thiết kế mới trị gốc.

  • Mô hình dữ liệu tốt: bảng được partition/cluster đúng, tránh bảng rộng vô tội vạ, tách thô/tinh hợp lý. Xem OBT & metrics hiện đại cho cân nhắc giữa one-big-table và star schema — ảnh hưởng trực tiếp tới byte quét mỗi query.
  • Incremental thay full-refresh: đây là đòn bẩy đơn lẻ lớn nhất cho pipeline lặp lại. Thay vì build lại toàn bộ bảng tỷ dòng mỗi giờ, chỉ xử lý phần mới. Chi tiết ở dbt — incremental & hiệu năng. Một job từ full-refresh sang incremental thường cắt 80–95% dữ liệu quét/thời gian compute.
  • Dọn bảng tạm & staging: đặt TTL/lịch dọn cho bảng trung gian; đừng để chúng vừa tốn storage vừa bị query nhầm.
  • Lịch chạy hợp lý: dashboard auto-refresh mỗi 5 phút cho báo cáo mà nghiệp vụ chỉ xem 2 lần/ngày là lãng phí thuần túy. Giãn tần suất theo nhu cầu thật; job ETL không cần chạy mỗi giờ nếu dữ liệu nguồn cập nhật mỗi ngày.

Phân tích chi phí — tìm query đắt

Không đo thì không tối ưu được. Cả hai nền tảng đều lộ lịch sử query để soi:

  • BigQuery: INFORMATION_SCHEMA.JOBS chứa từng job kèm total_bytes_billed, user, thời điểm. Sắp xếp giảm dần theo byte billed để tìm top query đắt và ai chạy chúng.
  • Snowflake: schema ACCOUNT_USAGE (view QUERY_HISTORY, WAREHOUSE_METERING_HISTORY) cho biết credit tiêu theo warehouse, theo query, theo user. INFORMATION_SCHEMA.QUERY_HISTORY cho dữ liệu gần thời gian thực hơn.

Quy trình thực chiến: mỗi tuần lấy top 20 query đắt nhất, xem chúng là gì (dashboard? ETL? ad-hoc?), và 80% chi phí thường nằm ở một nhúm nhỏ. Tối ưu đúng nhúm đó cho ROI cao nhất — nguyên tắc Pareto rất đúng với warehouse.

Reservation/commit vs on-demand

Khi mức dùng đã ổn định và đủ lớn, chuyển từ on-demand sang cam kết dung lượng thường rẻ hơn đáng kể:

  • BigQuery: mua slot reservation (cam kết capacity tính toán theo tháng/năm) thay vì trả theo byte. Có lợi khi tổng byte quét/tháng vượt điểm hòa vốn và tải đều đặn.
  • Snowflake: mua capacity commitment (cam kết chi tiêu credit theo hợp đồng) để hưởng đơn giá thấp hơn on-demand.

Nguyên tắc: on-demand cho tải bất định/thử nghiệm, cam kết cho tải ổn định đã đo được. Đừng cam kết trước khi có vài tháng dữ liệu sử dụng — cam kết dư cũng là lãng phí. Cam kết là bước sau khi đã tối ưu query, không phải thay thế cho tối ưu.

Use case thực tế

Bối cảnh (NCB): Phòng phân tích rủi ro chạy một warehouse Snowflake và một project BigQuery cho báo cáo. Hóa đơn warehouse tăng từ ~90 triệu lên ~180 triệu VND/tháng sau hai quý, không rõ vì sao. FinOps được yêu cầu cắt về mức cũ mà không giảm năng lực báo cáo.

Điều tra: Lấy ACCOUNT_USAGE.QUERY_HISTORY (Snowflake) và INFORMATION_SCHEMA.JOBS (BigQuery), sắp xếp theo chi phí. Phát hiện:

  1. Một dashboard rủi ro tín dụng auto-refresh mỗi 5 phút, mỗi lần query SELECT * trên bảng giao dịch 900 triệu dòng, không lọc ngày. Chiếm ~35% byte quét BigQuery.
  2. Một job dbt build lại full-refresh bảng số dư cuối ngày mỗi giờ, quét toàn bộ lịch sử 3 năm. Chiếm ~40% thời gian compute Snowflake.
  3. Warehouse ETL để auto-suspend 30 phút — bật rảnh phần lớn buổi tối.

Hành động & kết quả:

ViệcTrướcSauTiết kiệm
Dashboard: bỏ SELECT *, thêm partition ngày, refresh 30 phút + BI Engine cache~63 tr~8 tr~55 tr
Job dbt số dư: full-refresh → incremental~72 tr~9 tr~63 tr
Warehouse ETL: auto-suspend 30' → 60'', tách BI/ETL~18 tr~7 tr~11 tr
Custom quota + resource monitor (chặn query nhầm)lưới an toàn

Tổng hóa đơn về ~93 triệu/tháng, dưới mức cũ, năng lực báo cáo giữ nguyên (dashboard 30 phút hoàn toàn đủ cho nghiệp vụ). Sau khi tải ổn định 3 tháng, mua slot reservation BigQuery cắt thêm ~15%. Bài học: 75% mức tăng nằm ở hai query, đúng nguyên tắc Pareto — soi query history là bước đầu tiên, không phải bước cuối.

Ghi nhớ

  • Hiểu bạn bị tính tiền theo gì trước khi tối ưu: BigQuery on-demand tính theo byte quét; Snowflake tính theo thời gian compute — hai tư duy tối ưu khác nhau.
  • Mô hình theo byte quét (BigQuery): partitioning + clustering để pruning, chỉ SELECT cột cần (tránh SELECT *), materialized view, cache/BI Engine, custom quota, dry-run ước tính trước khi chạy.
  • Mô hình theo compute (Snowflake): right-sizing warehouse, auto-suspend ngắn, tách warehouse theo workload, resource monitor; giảm dữ liệu quét vẫn giúp query nhanh hơn → bật ngắn hơn (micro-partition pruning).
  • Nguyên tắc lõi phổ quát: đọc ít dữ liệu hơn = rẻ hơn. Lọc sớm (WHERE trên cột partition), chọn cột cần, tránh JOIN nổ và full scan.
  • Tối ưu tầng thiết kế trị gốc: mô hình dữ liệu tốt, incremental thay full-refresh (đòn bẩy lớn nhất), dọn bảng tạm, lịch chạy đúng nhu cầu.
  • Đo rồi mới tối ưu: soi INFORMATION_SCHEMA.JOBS (BQ) / ACCOUNT_USAGE.QUERY_HISTORY (Snowflake) tìm top query đắt — 80% chi phí nằm ở một nhúm nhỏ.
  • Reservation/commit rẻ hơn on-demand khi tải đã ổn định và đo được — là bước sau tối ưu, không thay thế tối ưu.

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