DataOps 3 — Version control & Code review

13 thg 7, 2026 2 lượt xem
#git
#data-engineering
#dataops
#version-control
#code-review

"SQL trôi nổi trên máy cá nhân" — vấn đề gốc

Hãy hình dung một cảnh quen thuộc trong đội dữ liệu ngân hàng. Báo cáo dư nợ cuối tháng do một analyst chạy từ một file bao_cao_du_no_final_v3_moinhat.sql trên Desktop. Người đó nghỉ việc. Ba tháng sau, số liệu lệch, không ai biết logic phân loại nợ nhóm 2–5 đã đổi lúc nào, ai đổi, vì sao. Không có lịch sử, không có bản đúng để so, không thể rollback. Đây không phải chuyện hiếm — nó là hệ quả tất yếu khi code dữ liệu không nằm trong version control.

Bài này thuộc series DataOps, nối tiếp phần Agile & cộng tác. Luận điểm trung tâm: analytics as code — mọi tài sản định nghĩa cách dữ liệu được biến đổi và trình bày đều là code, và code phải sống trong Git. Khi đã ở Git, ba bài sau mới có chỗ đứng: testing & chất lượng chạy trên mỗi thay đổi, CI/CD tự động hoá, và observability quan sát hệ đang chạy.

Analytics as code: cái gì phải vào Git?

Nguyên tắc: nếu nó định nghĩa hành vi của hệ dữ liệu, nó vào version control. Cụ thể:

Loại tài sảnVí dụCó nên vào Git?
Truy vấn & biến đổiSQL báo cáo, dbt model, macroBắt buộc
Định nghĩa pipelineDAG Airflow, job Spark/dbtBắt buộc
Cấu hìnhdbt_project.yml, profiles (không chứa secret), schema, configBắt buộc
Hạ tầngTerraform/IaC (xem CI/CD)Bắt buộc
Notebook.ipynb khám phá, phân tíchCó, với xử lý riêng (mục dưới)
Test & tài liệudbt tests, README, schema.ymlBắt buộc
Dữ liệu thậtbảng vài triệu dòng, PII khách hàngKHÔNG (dùng cách khác)
Secretmật khẩu DB, API keyKHÔNG (secret manager)

Vì sao đặt tất cả code vào Git lại đáng công đến vậy? Sáu lý do thực chiến:

  1. Lịch sử thay đổi (audit trail). git log cho biết ai đổi dòng nào, khi nào, vì sao (qua commit message). Với báo cáo tuân thủ ngân hàng, đây gần như là yêu cầu bắt buộc — thanh tra hỏi "logic tính CAR đổi lúc nào" thì phải trả lời được.
  2. Cộng tác không giẫm chân. Nhiều người sửa cùng project qua branch, merge có kiểm soát thay vì gửi file qua email.
  3. Code review. Mọi thay đổi được người thứ hai đọc trước khi vào production (phần lớn bài này).
  4. Rollback. Deploy hỏng → git revert về commit tốt gần nhất trong vài phút.
  5. Reproducibility. Cùng một commit + cùng input → cùng output. Số liệu tái tạo được, không phụ thuộc "máy của ai".
  6. Nguồn sự thật duy nhất (single source of truth). Repo là bản chính thức; không còn _final, _final_v2, _moinhat.

Ghi chú sandbox: sandbox của Knowledge Base là PostgreSQL read-only, không có Git CLI. Mọi lệnh Git dưới đây là minh hoạ workflow — không phải block "chạy được".

Git cơ bản cho người làm dữ liệu

Nếu đội có bài DevOps về Git thì nên đọc ở đó; ở đây ta gói gọn đủ để dùng. Ba khái niệm cốt lõi:

  • Commit — một ảnh chụp (snapshot) trạng thái code kèm message. Đơn vị của lịch sử.
  • Branch — một nhánh phát triển song song; sửa trên branch không đụng nhánh chính cho tới khi merge.
  • Merge / Pull Request — hợp nhất branch vào nhánh khác; PR là đề nghị merge kèm review.

Vòng đời một thay đổi điển hình:

# minh hoạ — không chạy trên sandbox
git checkout -b feat/bao-cao-du-no-currency   # tạo branch từ main
# ... sửa models/marts/fct_du_no.sql ...
git add models/marts/fct_du_no.sql
git commit -m "feat(marts): thêm chiều currency vào fct_du_no"
git push -u origin feat/bao-cao-du-no-currency
# → mở Pull Request trên GitLab/GitHub, chờ review + CI xanh → merge

Vài quy tắc vệ sinh: commit nhỏ, một mục đích; không commit secret (thêm .env, *.pem vào .gitignore); không commit dữ liệu thật (file CSV vài GB, dump chứa PII). Với đội dữ liệu, .gitignore nên loại target/, dbt_packages/, logs/, *.duckdb, data/*.csv.

Mô hình branching cho analytics

Có hai trường phái. Trunk-based — mọi người làm branch ngắn (vài giờ tới 1–2 ngày) rồi merge nhanh vào main, main luôn ở trạng thái deploy được; hợp với đội có CI mạnh, review nhanh. Feature branch (Git flow rút gọn) — branch sống lâu hơn theo tính năng; dễ quản khi review chậm nhưng dễ trôi xa main gây conflict.

Với analytics, khuyến nghị thực tế là trunk-based + branch tính năng ngắn: branch càng ngắn, review càng dễ, conflict trên file SQL càng ít. Điểm khác biệt của dữ liệu so với app thông thường là branch ánh xạ môi trường:

Ánh xạ điển hình: mỗi dev chạy branch của mình vào schema dev riêng (ví dụ analytics_dev_kienpv) để không đè bảng người khác; main được deploy vào staging cho QA/UAT; và một tag/approve thủ công đẩy lên prod. Cách này khiến "nhánh nào đang chạy ở đâu" luôn rõ ràng — điều cực quan trọng khi một báo cáo sai và bạn cần biết prod đang chạy đúng commit nào.

Code review / Pull Request cho dữ liệu

Đây là trái tim của quy trình. Code review không phải nghi thức hành chính — nó là hàng rào chất lượng trước production.

Vì sao review code dữ liệu đặc biệt quan trọng?

  • Bắt lỗi logic SQL trước khi nó thành số sai. Một LEFT JOIN lẽ ra là INNER, một filter WHERE currency = 'VND' bị quên, một SUM không GROUP BY đúng chiều — những lỗi này không làm code "đỏ" nhưng làm số liệu sai âm thầm. Trong ngân hàng, số sai đi vào báo cáo NHNN hay báo cáo ban điều hành là rủi ro thật.
  • Chuẩn hoá. Đảm bảo cùng một khái niệm ("dư nợ", "khách hàng active") được tính nhất quán giữa các model.
  • Chia sẻ kiến thức (giảm bus factor). Người review học logic nghiệp vụ; kiến thức không bị khoá trong đầu một người.

Review cái gì? Checklist thực dụng:

Khía cạnhCâu hỏi review
Đúng nghiệp vụLogic có khớp định nghĩa nghiệp vụ? Join có nhân đôi (fan-out) không? Xử lý NULL, tỷ giá, ngày cutoff đúng chưa?
Hiệu năngCó full-scan bảng lớn không cần? Có filter đẩy sớm (predicate pushdown)? Incremental hay full-refresh?
Chuẩn đặt tênTheo convention (stg_, fct_, dim_)? Cột rõ nghĩa?
TestCó test khoá chính, not_null, relationships, giá trị hợp lệ? (xem bài 4)
Tài liệuschema.yml/description cập nhật? Cột mới có mô tả?

Nguyên tắc review lành mạnh: PR nhỏ (dưới ~400 dòng thay đổi review kỹ hơn nhiều so với PR khổng lồ), review con người + CI tự động bổ trợ nhau (linter/test bắt lỗi máy, người bắt lỗi logic), và phản hồi vào code, không vào người.

Notebook trong version control

Jupyter notebook là điểm đau kinh điển. File .ipynb thực chất là JSON chứa cả code, output, metadata và số thứ tự cell. Hệ quả:

  • Diff bẩn. Đổi một dòng code làm hàng trăm dòng JSON (output, execution_count) thay đổi → review vô nghĩa, merge conflict liên miên.
  • Rò rỉ dữ liệu. Output có thể chứa dữ liệu khách hàng thật (PII) bị commit lẫn vào repo — vấn đề nghiêm trọng trong ngân hàng.

Cách xử lý:

  1. Clear output trước khi commit. Thủ công, hoặc hook nbstripout tự strip output khi git add.
  2. Jupytext. Lưu song song một bản .py (định dạng percent) cạnh .ipynb; review và version trên file .py sạch, diff đọc được.
  3. Chuyển code production ra module/script. Đây là khuyến nghị mạnh nhất: notebook để khám phá (exploration), còn logic đã ổn định thì tách thành .py module có test, import lại vào notebook nếu cần. Không để pipeline production phụ thuộc vào việc "chạy tay từng cell theo đúng thứ tự".

Versioning dữ liệu & mô hình — khác code

Điểm mấu chốt: code vào Git, dữ liệu thì không. Bạn không commit một bảng vài trăm triệu dòng vào repo. Nhưng nhiều lúc vẫn cần "phiên bản" của dữ liệu — để tái tạo báo cáo hôm qua, để so trước/sau một thay đổi. Các công cụ tương ứng (nhắc để biết, chi tiết ở tài liệu chuyên đề):

  • DVC (Data Version Control). Git-cho-dữ liệu: Git lưu một file con trỏ (hash + đường dẫn), dữ liệu thật nằm trên object store (S3/GCS). Hợp với dataset ML.
  • Snapshot bảng. Chụp trạng thái bảng theo ngày vào một bảng lịch sử — cách "thủ công" nhưng phổ biến.
  • dbt snapshot. Bắt thay đổi kiểu SCD Type 2 cho bảng nguồn thay đổi chậm (xem dbt).
  • Lakehouse time travel. Delta Lake / Iceberg cho phép truy vấn bảng "as of" một version/timestamp cũ — versioning dữ liệu ở tầng storage.
  • Model registry. Với ML, mô hình được version riêng (MLflow, xem MLOps pipelines).

Ý niệm nắm giữ: reproducibility đầy đủ = version(code) + version(dữ liệu) + version(cấu hình). Thiếu một mảnh là không tái tạo được chính xác một con số cũ.

Versioning cấu hình & môi trường

Cấu hình cũng là code. dbt_project.yml, biến môi trường (tên biến, không phải giá trị secret), định nghĩa cluster, file IaC — tất cả vào Git để môi trường tái lập được và mọi thay đổi hạ tầng cũng đi qua review. Nguyên tắc vàng: credential không bao giờ vào Git — dùng env_var(), secret manager (Vault, cloud secrets), inject lúc runtime. Một secret lỡ commit coi như đã lộ, kể cả xoá sau đó (nó còn trong lịch sử).

Quy ước commit & CI trên PR

Commit message có quy ước giúp lịch sử đọc được và tự động sinh changelog. Phổ biến là Conventional Commits:

feat(marts): thêm chiều currency vào fct_du_no
fix(staging): sửa join nhân đôi ở stg_transactions
docs(schema): mô tả cột balance_vnd

Prefix thường dùng: feat, fix, refactor, docs, test, chore. Kèm theo, mỗi PR nên kích hoạt CI tự động: chạy linter (sqlfluff), compile dbt, chạy test trên phần thay đổi — chi tiết ở bài 4 (testing)bài 5 (CI/CD). CI xanh là điều kiện cần để merge; review người là điều kiện đủ.

Ownership & CODEOWNERS

Ai chịu trách nhiệm review vùng nào? File CODEOWNERS (GitHub/GitLab) map đường dẫn → người/đội bắt buộc phải approve:

# minh hoạ CODEOWNERS
/models/marts/finance/     @doi-bao-cao-tai-chinh
/models/marts/risk/        @doi-quan-tri-rui-ro
/dags/                     @doi-data-platform

Với ngân hàng, đây là cơ chế thực thi phân tách trách nhiệm: model báo cáo rủi ro bắt buộc do người của đội rủi ro duyệt; ai đó ngoài đội không thể lặng lẽ đổi logic phân loại nợ rồi merge. Kết hợp với branch protection (cấm push thẳng vào main, bắt buộc N approval + CI xanh), bạn có một hàng rào production chắc chắn.

Ví dụ: một model được version

Đây là loại truy vấn tổng hợp mà bạn sẽ đưa vào Git dưới dạng một model (ví dụ fct_balance_by_currency.sql), review qua PR, rồi deploy. Block dưới đơn giản đủ để chạy trên sandbox PostgreSQL, minh hoạ "một model được version":

-- ▶ Chạy được
SELECT a.currency,
       COUNT(DISTINCT c.id) AS so_khach,
       COUNT(*)             AS so_tk,
       SUM(a.balance)       AS tong_so_du
FROM accounts a
JOIN customers c ON c.id = a.customer_id
GROUP BY a.currency
ORDER BY tong_so_du DESC;

Khi truy vấn này nằm trong Git: đổi từ SUM(balance) sang lọc chỉ tài khoản active sẽ hiện rõ trong diff của PR; người review kiểm tra logic; CI chạy test đảm bảo currency không NULL; merge xong tag release đẩy lên prod. Cùng một commit → cùng một con số. Đó là toàn bộ giá trị của analytics as code cô đọng trong một ví dụ.

Use case thực tế: quản lý phiên bản báo cáo tuân thủ

Bối cảnh. Đội Data của NCB sở hữu ~40 model dbt sinh ra các báo cáo gửi NHNN (phân loại nợ, tỷ lệ an toàn vốn, thanh khoản). Trước đây SQL nằm rải trên máy 5 analyst; mỗi kỳ báo cáo là một lần "ai chạy bản nào" mù mờ, từng có lần số dư nợ nhóm 3 lệch mà không truy được nguyên nhân.

Chuyển sang analytics as code. Các bước và số liệu:

  1. Gom về một repo dbt. 40 model đưa vào GitLab, chia thư mục staging/marts/finance/marts/risk/. .gitignore loại target/, logs/; secret DB chuyển sang Vault.
  2. Branch = môi trường. Mỗi analyst chạy branch vào schema analytics_dev_<user>; main deploy staging cho phòng QLRR đối chiếu; tag release-2026-06 được giám đốc dữ liệu approve mới lên prod.
  3. CODEOWNERS + branch protection. marts/risk/ bắt buộc 1 approval từ đội Quản trị rủi ro; cấm push thẳng main; bắt buộc CI xanh (sqlfluff + dbt test) + tối thiểu 2 approval.
  4. Kết quả sau 1 quý. Mỗi thay đổi logic báo cáo đều có PR ghi rõ ai, khi nào, vì sao → khi thanh tra hỏi logic phân loại nợ đổi lúc nào, tra git log ra ngay. Một lần deploy sai công thức CAR được git revert khôi phục trong ~10 phút thay vì dò tay cả buổi. Số report sai do "chạy nhầm bản SQL cũ" về 0 vì không còn SQL trôi nổi ngoài repo.

Ghi nhớ

  • Analytics as code: mọi tài sản định nghĩa hành vi dữ liệu (SQL, dbt model, pipeline, cấu hình, IaC, test, docs) phải nằm trong Git. Không để "SQL trôi nổi trên máy cá nhân".
  • Sáu lợi ích cốt lõi: lịch sử/audit, cộng tác, review, rollback, reproducibility, single source of truth.
  • Branching cho analytics: ưu tiên trunk-based + branch tính năng ngắn; branch ánh xạ môi trường dev (schema riêng) → staging (main) → prod (tag/approve).
  • Code review/PR là hàng rào chất lượng: bắt lỗi logic SQL (join nhân đôi, filter thiếu) trước khi thành số sai; PR nhỏ, review vào code không vào người.
  • Notebook: clear output (nbstripout), dùng jupytext, và chuyển code production ra module/script có test.
  • Dữ liệu ≠ code: version dữ liệu/mô hình bằng DVC, snapshot, dbt snapshot, lakehouse time travel, model registry — không commit dữ liệu thật hay secret.
  • Conventional Commits + CI trên mỗi PR (linter/compile/test) + CODEOWNERS + branch protection = quy trình review chặt, đặc biệt quan trọng cho báo cáo tuân thủ ngân hàng.
  • Bước tiếp theo: testing & chất lượngCI/CD tự động hoá đứng trên nền tảng Git này.

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