DataOps nâng cao 13 — Tối ưu chi phí & hiệu năng pipeline
Tin cậy là chưa đủ — pipeline còn phải tiết kiệm
Các bài trước của series tập trung vào việc làm cho pipeline tin cậy: có test, có contract, có observability, có WAP. Nhưng một pipeline tin cậy mà đốt tiền thì vẫn là một pipeline có vấn đề. Trong thực tế, hoá đơn warehouse của một đội dữ liệu ngân hàng rất dễ phình theo thời gian: mỗi lần thêm một mart mới, một dashboard auto-refresh, một mô hình dbt full-refresh chạy hằng đêm, chi phí lại nhích lên một chút — và không ai để ý cho tới khi kế toán hỏi vì sao hoá đơn Snowflake/BigQuery tháng này gấp ba tháng trước.
DataOps hiệu quả phải đồng thời tin cậy VÀ tiết kiệm. Đó chính là chỗ FinOps — kỷ luật quản trị chi phí đám mây — gặp DataOps. Ta không tách FinOps thành một dự án riêng chạy sau, mà đưa tư duy chi phí vào ngay trong vòng đời DataOps: khi thiết kế model, khi lên lịch, khi review PR, khi giám sát sản xuất. Bài này trình bày các đòn bẩy cụ thể để tối ưu chi phí và hiệu năng ở tầng transform/pipeline, kèm cách quan sát chi phí để không bao giờ bị bất ngờ. Nền tảng khái niệm xem FinOps — Tổng quan và FinOps — Chi phí warehouse.
Một nguyên tắc dẫn đường suốt bài: "performance là một tính năng" (performance is a feature) — nó không phải việc dọn dẹp làm sau, mà là một thuộc tính của sản phẩm dữ liệu, cần được thiết kế có chủ đích. Nhưng đi kèm là lời cảnh báo kinh điển: tránh tối ưu sớm (premature optimization). Đừng viết model phức tạp để tiết kiệm vài giây khi chưa biết nó có phải điểm nóng chi phí hay không. Hãy đo trước, tối ưu sau, và tối ưu đúng chỗ đắt nhất.
Đòn bẩy lớn nhất: INCREMENTAL thay full-refresh
Nếu chỉ được chọn một đòn bẩy, hãy chọn incremental. Rất nhiều mô hình transform được viết theo kiểu full-refresh: mỗi lần chạy là xoá sạch bảng đích rồi tính lại toàn bộ lịch sử từ đầu. Với bảng transactions của ngân hàng có hàng trăm triệu dòng tích luỹ nhiều năm, chạy full-refresh hằng đêm nghĩa là quét lại toàn bộ lịch sử mỗi ngày — trong khi 99% dữ liệu đó không hề đổi.
Incremental chỉ xử lý phần dữ liệu mới hoặc thay đổi kể từ lần chạy trước, rồi ghép vào bảng đích. Đây là kỹ thuật tiết kiệm lớn nhất vì chi phí warehouse gần như tỉ lệ thuận với lượng dữ liệu quét. Chuyển một model tổng hợp giao dịch từ full-refresh sang incremental có thể cắt 90-99% khối lượng quét mỗi lần chạy. Chi tiết cách dbt hiện thực điều này (materialized incremental, is_incremental(), chiến lược merge/insert_overwrite, cột unique key) xem bài chuyên sâu dbt — Incremental & Performance và cách dbt được dùng trong series ELT DataOps nâng cao 02 — dbt & ELT.
Ý tưởng cốt lõi rất đơn giản: thay vì tổng hợp trên toàn bộ bảng, ta lọc trước theo mốc thời gian rồi mới tổng hợp. Truy vấn dưới đây minh hoạ tinh thần incremental — chỉ lấy giao dịch trong 1 ngày gần nhất để tính tổng hợp theo tài khoản (trong dbt thật, mốc lọc sẽ lấy động từ max(created_at) của bảng đích qua khối is_incremental()):
-- ▶ Chạy được
SELECT
account_id,
COUNT(*) AS so_giao_dich,
ROUND(SUM(amount)::numeric, 2) AS tong_tien
FROM transactions
WHERE created_at >= NOW() - INTERVAL '1 day'
GROUP BY account_id
ORDER BY tong_tien DESC;
Điểm mấu chốt là mệnh đề WHERE created_at >= ...: nó biến một phép quét toàn bảng thành một phép quét chỉ trên lát dữ liệu mới. Nếu transactions.created_at được đánh index hoặc bảng được partition theo ngày, warehouse chỉ chạm vào đúng phần cần — đó là nơi tiền được tiết kiệm.
Chọn MATERIALIZATION đúng cho từng model
Trong dbt, mỗi model có một materialization — cách nó tồn tại vật lý trong warehouse. Chọn sai materialization là một trong những nguồn lãng phí âm thầm nhất.
| Materialization | Chi phí build | Chi phí đọc | Khi nào dùng |
|---|---|---|---|
| view | Rẻ (chỉ lưu định nghĩa) | Đắt/chậm (tính lại mỗi lần query) | Model nhẹ, ít người đọc, cần dữ liệu luôn tươi |
| table | Đắt (build lại toàn bộ) | Rẻ (đọc bảng vật lý) | Model nặng, nhiều người đọc, chấp nhận build định kỳ |
| incremental | Rẻ dần (chỉ thêm phần mới) | Rẻ | Bảng fact lớn, chỉ thêm/sửa phần mới — cân bằng tốt nhất |
| ephemeral | Không lưu (nhúng thẳng vào model sau qua CTE) | — | Bước trung gian nhỏ, tránh tạo object thừa |
Quy tắc thực dụng: bảng fact khổng lồ như tổng hợp giao dịch → incremental; bảng dimension nhỏ hay staging nhẹ → view cho tươi; logic trung gian chỉ dùng một chỗ → ephemeral để gộp, không tạo bảng rác. Một sai lầm phổ biến ở ngân hàng: để một mart báo cáo nặng ở dạng view, khiến mỗi lần mở dashboard là warehouse tính lại toàn bộ join hàng chục triệu dòng — chuyển nó thành table hoặc incremental cắt được chi phí đọc lặp đi lặp lại.
Giảm DỮ LIỆU QUÉT — đòn bẩy vật lý
Vì chi phí warehouse tỉ lệ với lượng byte quét, mọi kỹ thuật giảm dữ liệu quét đều là tiết kiệm trực tiếp. Bốn kỹ thuật chính:
- Partition & cluster: chia bảng theo cột thời gian (ví dụ
created_attheo ngày/tháng) để query có filter thời gian chỉ đọc đúng partition — partition pruning. Cluster/sort theo cột hay lọc để giảm số block phải đọc. - Chỉ chọn cột cần (column pruning): warehouse cột (columnar) chỉ đọc các cột được nêu.
SELECT *trên bảng rộng đọc mọi cột — kể cả cột không dùng — và tốn tiền cho phần thừa đó. - Predicate pushdown: đẩy điều kiện lọc xuống càng sớm càng tốt để giảm dữ liệu trước khi join/aggregate, thay vì lọc sau khi đã gộp cả núi dữ liệu.
- Aggregate sớm: tổng hợp về mức chi tiết vừa đủ trước khi join với bảng lớn khác.
So sánh chi tiết trên warehouse đám mây xem FinOps — Chi phí warehouse. Truy vấn dưới minh hoạ nguyên tắc chỉ chọn cột cần + tổng hợp thay vì SELECT *: ta không kéo cả bảng transactions với mọi cột về, mà chỉ lấy account_id, amount và gộp ngay:
-- ▶ Chạy được
SELECT
a.currency,
COUNT(*) AS so_giao_dich,
ROUND(AVG(t.amount)::numeric, 2) AS trung_binh,
ROUND(SUM(t.amount)::numeric, 2) AS tong
FROM transactions t
JOIN accounts a ON a.id = t.account_id
WHERE t.created_at >= NOW() - INTERVAL '30 days'
GROUP BY a.currency
ORDER BY tong DESC;
Truy vấn này đọc đúng ba cột cần thiết (account_id, amount, currency) thay vì toàn bộ, lọc thời gian trước khi gộp, và trả về một bảng tổng hợp nhỏ gọn thay vì hàng triệu dòng thô. Trên warehouse cột thật, chênh lệch chi phí giữa cách này và SELECT * không filter có thể là hàng chục lần.
Nhìn thấy chi phí đọc bằng EXPLAIN
Bạn không thể tối ưu thứ không đo được. EXPLAIN cho bạn thấy planner dự định đọc bao nhiêu trước khi thực sự chạy — công cụ đầu tiên để phát hiện một model quét quá nhiều. Chạy EXPLAIN trên truy vấn tổng hợp giao dịch để xem chi phí ước tính và cách planner đọc bảng:
-- ▶ Chạy được
EXPLAIN
SELECT account_id, SUM(amount)
FROM transactions
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUP BY account_id;
Đọc output EXPLAIN, chú ý: có Seq Scan (quét tuần tự toàn bảng) hay Index Scan không; con số rows ước tính; và cost khởi đầu..tổng. Nếu thấy Seq Scan trên bảng lớn dù có filter thời gian, đó là tín hiệu cần index/partition trên created_at. Trên warehouse đám mây, EXPLAIN/plan tương đương sẽ cho biết số byte/partition quét — chính là con số quy đổi thẳng ra tiền. Thói quen chạy EXPLAIN (hoặc xem query profile) trước khi promote một model đắt lên production nên là một bước trong quy trình.
Tránh model/pipeline chạy vô ích
Chi phí rẻ nhất là chi phí không phát sinh. Rất nhiều tiền bị đốt cho các lần chạy không cần thiết:
- Chỉ chạy khi nguồn đổi (data-aware scheduling): thay vì cron mù chạy 4 lần/ngày bất kể có dữ liệu mới hay không, dùng lập lịch nhận biết dữ liệu (Airflow Dataset / dbt source freshness) để job chỉ chạy khi bảng nguồn thật sự có dữ liệu mới. Xem cách orchestration nhận biết trạng thái ở DataOps nâng cao 06 — Observability & DRE.
- Gộp job nhỏ: nhiều micro-job, mỗi cái spin up một warehouse/cluster rồi tắt, tốn overhead khởi động. Gộp thành batch hợp lý giảm số lần "nổ máy".
- Dọn bảng trung gian: xoá các bảng staging/tạm không còn ai đọc — chúng vừa tốn storage vừa có thể vẫn đang được refresh vô ích.
- Lịch hợp lý, đừng refresh dày quá nhu cầu: một dashboard auto-refresh mỗi 5 phút trong khi dữ liệu nguồn chỉ cập nhật cuối ngày là kiểu đốt tiền kinh điển — mỗi lần refresh là một loạt query chạy lại vô nghĩa. Hỏi: người dùng thực sự cần tươi tới đâu?
Slim CI, right-size compute và serverless
Trong CI/CD analytics, một cạm bẫy chi phí là để mỗi PR build lại toàn bộ warehouse dev. Slim CI chỉ build/test những model thay đổi và hạ nguồn của chúng (qua state:modified của dbt so với manifest production), thay vì dbt build toàn bộ — cắt phần lớn chi phí CI. Chi tiết dựng CI/CD cho analytics engineering xem DataOps nâng cao 04 — CI/CD Analytics.
Ở tầng hạ tầng: right-size compute — đừng để warehouse size XL chạy job chỉ cần S; bật auto-suspend để warehouse tắt khi rảnh; tận dụng spot/serverless cho khối lượng chịu được gián đoạn hoặc bùng nổ không đều. Các đòn bẩy compute này thuộc địa hạt FinOps, xem FinOps — Tổng quan.
QUAN SÁT CHI PHÍ trong DataOps
Tối ưu một lần rồi để đó là chưa đủ; chi phí phình lại theo thời gian nếu không giám sát liên tục. Đưa chi phí thành một tín hiệu observability ngang hàng với chất lượng và độ trễ:
- Đo chi phí theo model/pipeline/domain: gắn tag/label cho từng warehouse-run để biết model dbt nào, domain nào (thẻ, khoản vay, tiền gửi) tốn bao nhiêu — thay vì chỉ nhìn một con số tổng mờ mịt.
- Showback: trình bày chi phí về đúng đội/domain sở hữu để họ thấy và tự chịu trách nhiệm (không nhất thiết tính tiền — chargeback — nhưng phải minh bạch).
- Phát hiện query/model đắt bất thường: theo dõi top query theo byte quét; một model đột nhiên đắt gấp 10 lần thường là dấu hiệu regression (join nổ, mất filter, dữ liệu tăng vọt).
- Cảnh báo ngân sách: đặt ngưỡng ngân sách theo ngày/tháng cho từng domain và bắn cảnh báo khi vượt — để bắt sự cố chi phí trong ngày chứ không phải cuối tháng.
Đánh đổi chi phí – độ trễ – độ tươi
Không có bữa trưa miễn phí: chi phí, độ trễ (latency) và độ tươi (freshness) là một tam giác đánh đổi. Muốn dữ liệu tươi hơn (chạy dày hơn) hoặc trả kết quả nhanh hơn (compute lớn hơn) thường tốn tiền hơn. Sai lầm hay gặp là mặc định chọn "tươi nhất, nhanh nhất" cho mọi bảng.
Cách làm đúng là hỏi người dùng thật sự cần gì. Báo cáo đóng sổ cuối ngày cần đúng và kịp SLA, nhưng có cần realtime không? Dashboard giám sát vận hành cần tươi theo phút, nhưng bảng phân tích lịch sử tín dụng thì tươi theo ngày là quá đủ. Phân loại bảng theo cấp độ tươi cần thiết rồi lên lịch tương ứng — đây là nơi tiết kiệm mà không ai bị ảnh hưởng, vì bạn chỉ cắt phần dư thừa so với nhu cầu thật.
Use case thực tế
Bối cảnh. Đội DataOps của một ngân hàng có hệ báo cáo đóng sổ (end-of-day close) chạy trên warehouse đám mây. Hoá đơn warehouse tháng 6 tăng vọt và ban lãnh đạo yêu cầu giảm hoá đơn mà vẫn kịp SLA đóng sổ 06:00 sáng. Điều tra bằng quan sát chi phí (đo theo model) phát hiện:
- Model
mart_txn_daily(tổng hợp giao dịch ngày) đang để full-refresh, quét lại 420 triệu dòng lịch sử mỗi đêm dù chỉ ~1,2 triệu dòng mới/ngày. - Một mart báo cáo nặng để dạng view, và dashboard giám sát bật auto-refresh 5 phút dù nghiệp vụ chỉ xem sau khi đóng sổ.
- CI build lại toàn bộ 180 model dbt cho mỗi PR.
Hành động & kết quả (số minh hoạ).
| Đòn bẩy | Thay đổi | Tác động |
|---|---|---|
| Incremental | mart_txn_daily: full-refresh → incremental theo created_at | Byte quét/đêm giảm ~99% (420tr → ~1,2tr dòng); thời gian chạy 38 phút → 4 phút |
| Materialization | Mart view nặng → table build sau close | Chi phí đọc lặp giảm mạnh; dashboard đọc bảng vật lý |
| Lịch | Auto-refresh 5 phút → refresh 1 lần sau close | Cắt hàng trăm lần query refresh vô nghĩa/ngày |
| Slim CI | dbt build toàn bộ → state:modified+ | Chi phí CI/PR giảm ~80% |
| Right-size | Warehouse close hạ 1 cỡ, bật auto-suspend | Giảm chi phí compute rảnh |
Kết quả: hoá đơn warehouse của hệ báo cáo giảm đáng kể, đồng thời job close kết thúc sớm hơn (nhờ incremental) nên SLA 06:00 an toàn hơn trước — tối ưu chi phí và hiệu năng đi cùng nhau. Sau đó đội gắn cảnh báo ngân sách theo domain để bắt regression sớm và đưa EXPLAIN/query profile vào checklist review PR cho model đắt.
Ghi nhớ
- DataOps tốt = tin cậy VÀ tiết kiệm; đưa tư duy FinOps vào vòng đời DataOps, không làm sau như dự án riêng.
- Đòn bẩy lớn nhất ở tầng transform là incremental thay full-refresh: chỉ xử lý dữ liệu mới → tiết kiệm nhiều nhất.
- Chọn materialization đúng: view rẻ build/đắt đọc, table đắt build/rẻ đọc, incremental cân bằng, ephemeral gộp bước nhỏ.
- Chi phí tỉ lệ với dữ liệu quét: partition/cluster, chỉ chọn cột cần, predicate pushdown, aggregate sớm; dùng
EXPLAINđể thấy chi phí đọc. - Tránh chạy vô ích: data-aware scheduling (chỉ chạy khi nguồn đổi), gộp job, dọn bảng trung gian, đừng auto-refresh dày quá nhu cầu.
- Slim CI để không build lại toàn bộ mỗi PR; right-size compute, auto-suspend, spot/serverless.
- Quan sát chi phí: đo theo model/pipeline/domain, showback, phát hiện query đắt bất thường, cảnh báo ngân sách.
- Cân nhắc tam giác chi phí – độ trễ – độ tươi: hỏi người dùng thật sự cần tươi tới đâu, cắt phần dư.
- "Performance là một tính năng" — thiết kế có chủ đích; nhưng đo trước, tối ưu sau, tránh tối ưu sớm.
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.
Data Engineering là gì, vai trò trong vòng đời dữ liệu, và bức tranh hệ sinh thái công cụ.
Vì sao xử lý phân tán, mô hình Spark (RDD/DataFrame), lazy evaluation, shuffle và tối ưu.
Đả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.