Imported from linhlvit/ubck_atomic_design (
.claude/skills/datamart-lld-design/SKILL.md). Install upstream withnpx skills add linhlvit/ubck_atomic_design --skill datamart-lld-design. Copyright stays with the author.
Skill: Thiết kế LLD Datamart
Đọc file này TRƯỚC KHI bắt đầu Phase 1/2/3 cho bất kỳ module nào.
Tài nguyên đi kèm
- Reference:
reference/phase1_attributes.md— 15 cột CSV, etl_logic_type, ràng buộc key/nullable (Phase 1)reference/phase2_detail_mapping.md— column_role, logic format, xử lý PENDING/Doing (Phase 2)reference/phase3_flat_table.md— data type mapping, ENGINE/PARTITION, pattern CREATE + POPULATE (Phase 3)
- Examples:
examples/etl_logic_correct.md— ví dụ đại diện mỗi etl_logic_typeexamples/etl_logic_wrong.md— pattern sai etl_logicexamples/key_constraints.md— ràng buộc key × bảng type + ví dụ vi phạm
Điều kiện tiên quyết
-
Datamart/hld/DTM_{MODULE}_HLD.mdtồn tại và đã được user duyệt -
Datamart/hld/DTM_{MODULE}_Entities.csvtồn tại và đã được user duyệt (có cộtreuse_status) -
DataModel/Atomic/dm_manifest.yamltồn tại — entry point tra cứu Atomic entities, Nguồn 1 (ưu tiên cao nhất) -
DataModel/working/Atomic/lld/manifest.yamltồn tại — entry point tra cứu Atomic entities draft, Nguồn 2 (chỉ tra khi Nguồn 1 không có entry) -
Datamart/datamart_model.yamltồn tại — registry schema cross-module (có thể rỗng nếu module đầu tiên) -
Datamart/lld/datamart_attributes.csvtồn tại (có thể rỗng nếu module đầu tiên) -
BRD/BA/BA_analyst_{MODULE}.csvtồn tại (cần cho Phase 2) -
DataModel/working/Atomic/lld/classification_schemes.yamltồn tại (cần khi map từ danh mục CV)
❌ TUYỆT ĐỐI CẤM SỬA ATOMIC TỪ SKILL THIẾT KẾ DATAMART: Skill thiết kế Datamart (
datamart-hld-design,datamart-lld-design,datamart-review) chỉ có quyền READ-ONLY đối với thư mụcDataModel/Atomic/vàDataModel/working/Atomic/.
- Tuyệt đối KHÔNG ĐƯỢC tạo file mới, sửa đổi thuộc tính, thêm cột kỹ thuật, hoặc can thiệp vào bất kỳ file YAML nào trong
DataModel/.- Nếu Atomic thiếu bảng, thiếu cột, hoặc thiếu audit field cần thiết cho Datamart:
- Đánh dấu KPI liên quan là PENDING (ghi rõ lý do: "Thiếu nguồn Atomic / Chưa có trong Atomic schema").
- Ghi nhận vào Section 5 Open Issues (
DTM_{MODULE}_HLD.md).- DỪNG lại báo cáo human để Data Modeler thuộc luồng Atomic xử lý độc lập. Tuyệt đối không tự ý sửa Atomic!
QUYẾT ĐỊNH CỨNG: Claude KHÔNG được đoán
source_entityhaysource_attribute. Mọi mapping phải tra cứu trực tiếp từ entity YAML files, theo đúng thứ tự ưu tiên 2 nguồn dưới đây.❌ TUYỆT ĐỐI CẤM dùng
DataModel/working/Atomic_LinhLV/— track cũ đã revert, out of date. Dù cấu trúc thư mục giống hệtDataModel/Atomic/(cùng BCV folder: Arrangement/Common/Event/...), entity ở đây KHÔNG được coi là nguồn hợp lệ dưới bất kỳ hình thức nào — kể cả khi không tìm thấy entity đó ở 2 nguồn hợp lệ bên dưới, vẫn phải kết luận PENDING, không fallback sangAtomic_LinhLV.Quy trình tra cứu Atomic — 2 nguồn theo thứ tự ưu tiên:
Ưu tiên Nguồn Manifest Schema manifest 1 DataModel/Atomic/DataModel/Atomic/dm_manifest.yamldm_manifest— fieldsubfolder/file_name/physical_name/status2 DataModel/working/Atomic/DataModel/working/Atomic/lld/manifest.yamllld_manifest— fieldsource_system/source_table/atomic_entity/lld_file/design_status
- Mở
DataModel/Atomic/dm_manifest.yaml→ tìm entry cóphysical_namekhớpatomic_tableđang cần.
- Có entry (bất kể
status: approvedhaydraft) → dùng Nguồn 1, đọcsubfolder+file_name→ mở fileDataModel/Atomic/{subfolder}/{file_name}.- Lấy
ldm.physical_name=atomic_table,attribute.physical_name=atomic_column.- Không có entry ở Nguồn 1 → mở
DataModel/working/Atomic/lld/manifest.yaml→ tìm entry khớpatomic_entity/source_table→ có entry → mở fileDataModel/working/Atomic/lld/{source_system}/{lld_file}để lấy attribute tương ứng.- Không có entry ở cả 2 nguồn → Atomic PENDING, không suy diễn.
- Trùng ở cả 2 nguồn (entity vừa có ở Nguồn 1 vừa còn bản draft cũ ở Nguồn 2) → luôn dùng Nguồn 1, bỏ qua Nguồn 2.
- Nhiều source table cùng
physical_name/atomic_entity: đọc TẤT CẢ entry cùng tên — không dừng ở entry đầu.src_stm_codevalue: trích từclassification_contextcủa attributeSource System Code— format"Source System Code = 'VALUE'"→ value =VALUE.
QUY TRÌNH (BẮT BUỘC)
Phase 0 (PLAN):
Claude đọc HLD (Section 2) + Entities.csv → trích danh sách TOÀN BỘ nhóm báo cáo
(Nhóm 1 → Nhóm N_max — kể cả nhóm PENDING toàn bộ không có bảng nào cần thiết kế)
→ với mỗi nhóm: liệt kê KPI IDs + bảng cần thiết kế (new/partial) + bảng reuse
— nhóm PENDING toàn bộ ghi "— (PENDING toàn bộ, không có bảng)"
→ dim dùng chung: nhóm đầu tiên dùng → thiết kế; nhóm sau → check datamart_model.yaml
(reuse nếu đủ cột / partial nếu thiếu cột)
→ trình bày plan tổng dạng bảng (xem định dạng bên dưới) — tổng số dòng Plan PHẢI
bằng tổng số nhóm trong HLD Section 2, không chỉ nhóm có bảng READY
→ DỪNG chờ human approve plan → sau khi approve mới bắt đầu Nhóm 1
Loop mỗi nhóm N — Phase 1 (HOÀN THÀNH TOÀN BỘ NHÓM TRƯỚC KHI CHUYỂN PHASE 2):
Phase 1: Đọc reuse_status từ Entities.csv cho các bảng của nhóm N
→ dim dùng chung đã thiết kế ở nhóm trước: check datamart_model.yaml
→ đủ cột → bỏ qua (reuse) | thiếu cột → báo delta, DỪNG chờ approve delta
→ bảng new/partial: đọc datamart_model.yaml lấy baseline (nếu partial)
→ xác định driving table + src_stm_code → sinh từng file
→ SELF-REVIEW 10 TC (TC6/TC7/TC8 quét lại TOÀN BỘ master + các nguồn khác, không chỉ file vừa sinh) → sửa nếu FAIL → trình bày SELF-REVIEW + file
→ DỪNG chờ human duyệt từng file
→ hỏi merge master → DỪNG chờ human xác nhận merge
→ ghi datamart_model.yaml (upsert new / append delta)
→ làm Nhóm N+1 Phase 1 → ... → làm đến hết Nhóm cuối Phase 1
> **GATE CỨNG — chuyển Phase 2:**
> ❌ KHÔNG được bắt đầu Phase 2 khi còn bất kỳ nhóm nào chưa hoàn thành Phase 1.
> Điều kiện bắt buộc: **tất cả nhóm** (Nhóm 1 → Nhóm N_max) đã có file Attributes được human approve và merge vào master.
> Trước khi chuyển Phase 2: liệt kê toàn bộ nhóm + trạng thái Phase 1 → báo cáo human → DỪNG chờ xác nhận.
Phase 2 — Bước 0 (TODO LIST — bắt buộc, chạy 1 lần trước khi vào loop):
Quét lại HLD Section 2 từ đầu (độc lập với Phase 0 Plan) → lập danh sách
TOÀN BỘ nhóm 1 → N_max kèm trạng thái (READY / READY thu hẹp / PENDING toàn bộ)
→ đây là todo list kiểm soát tiến độ Phase 2, KHÔNG lấy lại danh sách từ Phase 0 Plan
(Plan Phase 0 có thể đã lược bỏ nhóm PENDING toàn bộ vì không cần bảng Attributes)
→ trình bày todo list dạng bảng: Nhóm | Trạng thái HLD | Đã có trong Detail Mapping? (chưa/rồi)
→ DỪNG chờ human xác nhận todo list trước khi bắt đầu loop Nhóm 1
Loop mỗi nhóm N — Phase 2 (theo TODO LIST vừa lập, N = 1 → N_max, xử lý cả nhóm PENDING toàn bộ):
Phase 2: Cross-check BA ↔ HLD cho KPI của nhóm N → báo gap nếu có
→ DỪNG chờ human xử lý gap (nếu có)
→ xuất block KPI nhóm N (kể cả nhóm PENDING toàn bộ — xem quy tắc PENDING trong
reference/phase2_detail_mapping.md) → append vào DTM_{MODULE}_Detail_Mapping.csv
theo ĐÚNG THỨ TỰ SỐ NHÓM TĂNG DẦN (không append cuối file nếu nhóm đó có số nhỏ hơn
nhóm đã append trước đó — xem TC6)
→ SELF-REVIEW 4 TC → sửa nếu FAIL → trình bày SELF-REVIEW + block KPI
→ DỪNG chờ human duyệt block KPI nhóm N
→ cập nhật todo list (đánh dấu Nhóm N đã xong) → làm Nhóm N+1 Phase 2 → ... → hết Nhóm cuối
→ Sau khi tất cả nhóm trong todo list đã xử lý:
Chạy SELF-REVIEW module-level TC5 (đối chiếu tổng số nhóm/KPI) + TC6 (thứ tự nhóm)
→ sửa nếu FAIL → DỪNG — báo "Tất cả nhóm Phase 2 hoàn thành, TC5+TC6 PASS."
→ chờ human xác nhận chuyển Phase 3
Phase 3: Sau khi tất cả nhóm đã duyệt → báo số bảng flat
→ DỪNG chờ human xác nhận
→ sinh 2 file SQL → DỪNG chờ human duyệt
Định dạng Plan tổng (Phase 0 output)
## Plan thiết kế LLD — {MODULE}
| Nhóm | Tên nhóm | KPI IDs | Bảng cần thiết kế | Bảng reuse |
|------|----------|---------|-------------------|------------|
| 1 | Tên nhóm 1 | K_X_1, K_X_2 | Fact A (new), Dim B (new) | Calendar Date Dimension |
| 2 | Tên nhóm 2 | K_X_3 | Operational C (new) | Dim B (reuse — đã thiết kế Nhóm 1) |
| ... | ... | ... | ... | ... |
Dim dùng chung:
- Calendar Date Dimension: reuse toàn module (không thiết kế lại)
- Dim B: thiết kế ở Nhóm 1, các nhóm sau check datamart_model.yaml
Tổng: N nhóm | M bảng new | P bảng partial | Q bảng reuse
→ Xác nhận plan để bắt đầu Nhóm 1?
GATE RULE — BẮT BUỘC TUYỆT ĐỐI:
- Claude KHÔNG ĐƯỢC tự bỏ qua bất kỳ GATE nào, dù kết quả có vẻ hiển nhiên hay không có vấn đề gì.
- Tại mỗi GATE: DỪNG hoàn toàn, đặt câu hỏi xác nhận rõ ràng, chờ human trả lời trước khi tiếp tục.
- Human chưa trả lời = chưa được phép tiếp tục. Không được suy diễn "im lặng = đồng ý".
- Không được gộp nhiều GATE vào 1 câu hỏi — mỗi GATE là 1 điểm dừng độc lập.
PHASE 0 — PLAN
Bắt buộc thực hiện trước Phase 1. Mục đích: human nắm toàn bộ scope, approve một lần, Claude chạy từng nhóm.
Bước P1 — Đọc HLD Section 2
Đọc DTM_{MODULE}_HLD.md Section 2 từ đầu đến hết → trích danh sách TOÀN BỘ nhóm báo cáo
(Nhóm 1 → Nhóm N_max), không bỏ sót nhóm nào dù trạng thái gì:
- Tên nhóm (Nhóm 1, Nhóm 2, ..., Nhóm N_max)
- Trạng thái HLD của nhóm: READY / READY (thu hẹp) / PENDING toàn bộ
- KPI IDs thuộc nhóm (bao gồm Chiều + Pending)
❌ Không bỏ sót nhóm PENDING toàn bộ chỉ vì nhóm đó không cần bảng Attributes nào ở Phase 1 — nhóm này vẫn phải xuất hiện trong Plan (cột "Bảng cần thiết kế" ghi "— (PENDING toàn bộ)") để Phase 2 không bỏ sót khi lập todo list.
Bước P2 — Map bảng cho từng nhóm
Đọc DTM_{MODULE}_Entities.csv → với mỗi nhóm:
- Xác định bảng nào phục vụ nhóm đó (từ Source trong HLD Section 2)
- Kiểm tra
reuse_statustừ Entities.csv - Dim dùng chung:
- Conformed dim (Calendar Date Dimension): luôn reuse — không thiết kế lại ở bất kỳ nhóm nào
- Dim nội bộ module: nhóm đầu tiên dùng → gán
new; nhóm sau → gánreuse(checkdatamart_model.yamltrước) - Nếu
datamart_model.yamlcó entry nhưng thiếu cột → gánpartialcho nhóm đó, sẽ báo delta khi đến nhóm đó
- Nhóm PENDING toàn bộ (không có Atomic source hoặc Fact/Dim nào sẵn sàng): vẫn giữ trong Plan với cột "Bảng cần thiết kế" = "— (PENDING toàn bộ, không có bảng)" — KHÔNG loại khỏi Plan
Bước P2b — Chốt physical name chuẩn cho entity dùng chung nhiều bảng (bắt buộc)
Lý do (bài học GSDC 2026-07-16): Khi 1 entity/khái niệm nghiệp vụ (VD: "Public Company") xuất hiện trong tên của ≥ 2 bảng Datamart cùng module (1 Dimension + nhiều Fact), rất dễ mỗi bảng tự nghĩ ra 1 biến thể viết tắt khác nhau (
pblc_coở Dimension,pcở các Fact) — cả hai đều không có trongrule_physical_name_exceptions_datamart.csvvà không nhất quán với nhau. Lỗi này không bị TC nào bắt được cho tới khi đã sinh xong 13 file (Attributes + Detail Mapping + HLD + model + SQL) vì mỗi bảng được review độc lập theo nhóm, không ai đối chiếu ngược các bảng với nhau.
Quy trình:
- Từ danh sách bảng đã map ở Bước P2 (toàn bộ Nhóm), liệt kê mọi entity/khái niệm nghiệp vụ xuất hiện trong tên ≥ 2 bảng khác nhau (kể cả 1 Dimension + N Fact, hoặc N Fact dùng chung 1 khái niệm)
- Với mỗi entity như vậy: áp dụng PHYSICAL NAMING RULE (mục bên dưới) để xác định đúng 1 dạng token duy nhất — full word nếu không có trong exceptions, viết tắt nếu có
grepdatamart_attributes.csvxem entity đó đã từng dùng token nào ở module khác chưa (dim conformed/shared) — nếu có, dùng lại nguyên token đó, không tự nghĩ ra token mới- Ghi bảng "Physical name chuẩn" vào Plan tổng (Bước P3) — liệt kê entity | token chuẩn | áp dụng cho bảng nào — để human duyệt 1 lần cho toàn module, tránh lệch nhau giữa các nhóm thiết kế sau
Bước P3 — Trình bày plan và GATE
Format plan tổng (bắt buộc dùng bảng):
## Plan thiết kế LLD — {MODULE}
| Nhóm | Tên nhóm | KPI IDs | Bảng cần thiết kế | Bảng reuse/bỏ qua |
|------|----------|---------|-------------------|-------------------|
| 1 | <tên> | K_X_1, K_X_2 | Fact A (new), Dim B (new) | Calendar Date Dimension (reuse) |
| 2 | <tên> | K_X_3 | Operational C (new) | Fact A (reuse), Dim B (reuse) |
| ... | | | | |
Ghi chú dim dùng chung:
- <Dim name>: thiết kế ở Nhóm X, các nhóm sau reuse (check datamart_model.yaml)
- Calendar Date Dimension: conformed dim — reuse toàn module
Physical name chuẩn (entity dùng chung ≥ 2 bảng — xem Bước P2b):
| Entity/khái niệm | Token chuẩn | Áp dụng cho bảng |
|---|---|---|
| Public Company | `public_company` (full — không có trong exceptions) | public_company_dim, fct_public_company_risk_score_snpst, fct_public_company_*_score_snpst (5 bảng) |
| ... | ... | ... |
Tổng: [N] nhóm | [M] bảng new | [P] bảng partial | [Q] bảng reuse
→ Xác nhận plan (kể cả bảng Physical name chuẩn) để bắt đầu Nhóm 1?
GATE — bắt buộc dừng: Chờ human approve plan trước khi bắt đầu bất kỳ nhóm nào. ❌ KHÔNG bắt đầu Phase 1 Nhóm 1 khi chưa có xác nhận plan. ❌ Trong Phase 1, khi đặt tên bảng/cột cho entity đã có trong bảng "Physical name chuẩn" — PHẢI dùng đúng token đã duyệt, không tự đổi hay nghĩ ra biến thể khác dù ngắn gọn hơn.
PHASE 1 — ATTRIBUTES CSV
Đọc reference/phase1_attributes.md đầy đủ trước khi bắt đầu.
Input Phase 1:
Datamart/hld/DTM_{MODULE}_Entities.csv— danh sách entity, table_type, reuse_status, source_table đã duyệtDatamart/lld/datamart_attributes.csv— master hiện tại (cần cho partial flow)BRD/BA/BA_analyst_{MODULE}.csv— đối chiếu yêu cầu nghiệp vụ (CHỈ lấy dòng cóTrạng thái mapping ∈ {Done, Doing, Pending}; TUYỆT ĐỐI LOẠI BỎ chỉ tiêu cóTrạng thái mapping = Delete / DELETED / Xóa, cấm sinh thuộc tính/cột cho chỉ tiêu Delete)
Bước 0 — Đọc reuse_status từ Entities.csv:
reuse→ bỏ qua hoàn toàn, không sinh file, ghi note: "Bảng [datamart_table] reuse từ master — không thiết kế mới"new→ thiết kế đầy đủ, sinh filepartial→ đọcDatamart/datamart_model.yaml(entry cóid = DTM-{datamart_table}), lấycolumnshiện có làm baseline + xác địnhmodulesở hữu gốc → so sánh delta → báo cáo human, DỪNG chờ approve trước khi sinh file❌ KHÔNG được sinh file partial khi chưa có xác nhận của human về delta. ❌ KHÔNG tạo file
_delta.csvhay bất kỳ file Attributes nào trong thư mục module đang thiết kế (Datamart/lld/{MODULE}/) khi bảng thuộc sở hữu module khác — cột mới phải sửa TRỰC TIẾP vào file gốcDatamart/lld/{MODULE_SỞ_HỮU}/DTM_{MODULE_SỞ_HỮU}_{datamart_table}_....csv. Xem chi tiếtreference/phase1_attributes.mdmục "Quy trình partial".
Bước 0b — Trình bày danh sách file sẽ sinh cho nhóm N (GATE — chờ human xác nhận trước khi sinh):
Trước khi trình bày: với bảng partial → đọc Datamart/datamart_model.yaml lấy datamart_table physical name đã ghi (không đặt lại tên). Sau khi xác định driving table và src_stm_code cho từng bảng new/partial của nhóm N, trình bày bảng tóm tắt:
Nhóm [N] — [Tên nhóm]: danh sách file sẽ sinh
| STT | Tên file | table_type | Driving Table | src_stm_code |
|-----|----------|------------|---------------|--------------|
| 1 | DTM_NHNCK_scr_prac_conduct_vln_NHNCK_VIOLATIONS.csv | dim | scr_prac_conduct_vln | NHNCK_VIOLATIONS |
| 2 | DTM_NHNCK_fct_prac_exam_rslt.csv | fact | — | — |
| 3 | ... | ... | ... | ... |
Bảng reuse/bỏ qua nhóm này: [danh sách tên bảng + lý do]
→ Xác nhận để tiến hành sinh file nhóm [N]?
GATE — bắt buộc dừng: Chờ human xác nhận danh sách của nhóm trước khi điền 15 cột bất kỳ file nào. ❌ KHÔNG được bắt đầu điền nội dung file khi chưa có xác nhận của human. Nếu driving table hoặc src_stm_code sai → sửa, trình bày lại bảng, chờ xác nhận lại.
Xem quy trình chi tiết trong reference/phase1_attributes.md.
Bước 1 — Xác định Driving Table
Bắt buộc xác định Driving Table trước khi điền etl_logic:
| Loại bảng | Driving Table |
|---|---|
| Fact Event / Snapshot | Atomic entity có grain tương đương |
| Tác nghiệp | Atomic entity của đối tượng chính |
| Dimension | Atomic entity tương ứng |
Ghi rõ Driving Table trong description của row PK/BK.
Driving Table khi Fact không có join key chung (No Driving Table):
Fact dùng pattern CROSS JOIN scalar subquery (mỗi measure aggregate độc lập từ 1 Atomic table) — không có driving table. Không thêm src_stm_code cho loại bảng này.
Bước 1a — Phạm vi cột Dimension (coverage rule — bắt buộc, thiết kế lần đầu VÀ khi partial)
Bài học (module NDTNN, 2026-07-24):
Public Company Dimension(dùng chung GSDC/QLCB/NDTNN) ban đầu chỉ chọn 8 cột đủ dùng cho KPI đang thiết kế lúc đó. Sau đó phải quay lại bổ sung 2 lần (7 cột rồi 11 cột) khi rà soát lại entity Atomic gốc — vì cách chọn "đủ dùng cho KPI hiện tại" bỏ sót nhiều attribute mô tả tĩnh có giá trị dùng chung lâu dài cho các module sau. Với Dimension dùng chung nhiều module, thiếu 1 cột không chỉ vá lại 1 lần — mỗi module mới cần thêm lại phải sửadatamart_attributes.csv+datamart_model.yaml+ mọi Detail Mapping đã tham chiếu, tốn công hơn nhiều so với thiết kế đủ ngay từ đầu.
Quy tắc: Khi thiết kế Dimension mới (new) hoặc mở rộng Dimension đã có (partial), đọc toàn bộ danh sách attribute của Atomic entity driving table (không dừng ở tập cột cần cho KPI hiện tại) và áp dụng bộ lọc theo SCD-nature — bản chất thay đổi chậm/nhanh của chính attribute đó trên Atomic:
| Loại attribute Atomic | Đưa vào Dimension? | Ví dụ |
|---|---|---|
| Đặc điểm/hồ sơ mô tả thực thể (tên, mã, loại hình, ngày đăng ký, tỉnh/thành, cờ cấu trúc sở hữu...) | ✅ Có — kéo dư thừa toàn bộ, kể cả khi KPI hiện tại chưa dùng | pc_nm, enterprise_tp_code, head_office_province_nm, has_parent_company_indicator |
| Khóa/liên kết kỹ thuật thuần (PK/FK surrogate nội bộ Atomic không mang ý nghĩa nghiệp vụ độc lập) | ❌ Không | id kỹ thuật không có business meaning riêng |
| Audit/vận hành thuần (created_by, file_name, upload timestamp, ghi chú xử lý nội bộ) | ❌ Không | attachment_file_nm, specialist_note, company_login_username |
| Gắn với 1 giao dịch/sự kiện cụ thể, đổi theo phiên/ngày (giá, khối lượng, số dư, trạng thái tại 1 thời điểm snapshot) | ❌ Không — thuộc về Fact/Snapshot, không phải Dimension | giá khớp lệnh, khối lượng giao dịch, số dư tài khoản theo ngày |
❌ Không áp dụng ngược lại cho Fact — Fact vẫn giữ nguyên rule "chỉ giữ cột trace được về KPI" (xem CLAUDE.md feedback_fact_no_etl_filter_columns). Coverage rule này CHỈ áp dụng cho Dimension.
Khi partial (Dimension đã tồn tại từ module khác): Trước khi chỉ thêm đúng 1-2 cột module hiện tại cần, chủ động rà lại toàn bộ attribute còn lại của Atomic driving table theo bảng phân loại trên — đề xuất bổ sung 1 lần đầy đủ, tránh để module sau lại phải mở lại cùng bảng.
Bước 1b — Bổ sung src_stm_code cho Dimension và Operational
Mọi bảng dim và operational phải có attribute src_stm_code (thêm cuối danh sách attribute của bảng).
Xác định driving table để lấy src_stm_code: Driving table = bảng chính của grain — bảng có PK/BK trong Attributes, các attribute của nó dùng etl_logic_type = direct. Bảng phụ (etl_logic_type = join_atomic) không dùng để xác định src_stm_code.
| Trường hợp | ETL Logic | etl_logic_type |
|---|---|---|
| Driving table single-source | <driving_table>.src_stm_code WHERE <driving_table>.src_stm_code = '<value>' |
direct |
| Driving table multi-source (nhiều nguồn, lấy 1 chính thức) | <driving_table>.src_stm_code WHERE <driving_table>.src_stm_code = '<value>' |
direct |
| Driving table multi-source (nhiều nguồn, lấy nhiều) | <driving_table>.src_stm_code WHERE <driving_table>.src_stm_code IN ('<val1>','<val2>') |
direct |
| Multi-source tách bộ (xem bên dưới) | <atomic_table>.src_stm_code WHERE <partition_key> = '<value>' |
direct hoặc pending |
Quy tắc WHERE filter bắt buộc (forward-compatibility):
Mọi src_stm_code attribute luôn phải có điều kiện lọc — kể cả khi Atomic driving table hiện chỉ có 1 nguồn. Lý do: nếu sau này Atomic nhận thêm nguồn mới, Datamart ETL không bị ảnh hưởng mà không cần sửa schema.
- Single-source:
WHERE <driving_table>.src_stm_code = '<giá_trị>' - Multi-source lấy 1 chính thức:
WHERE <driving_table>.src_stm_code = '<giá_trị_chính_thức>' - Multi-source lấy nhiều:
WHERE <driving_table>.src_stm_code IN ('<val1>','<val2>',...) - Vẫn dùng
etl_logic_type = directcho tất cả trường hợp trên
Xử lý multi-source — Tách bộ (UNION/Partition pattern): Áp dụng khi bảng Datamart populate từ nhiều nguồn độc lập theo 1 trong 2 trường hợp:
| Trường hợp | Mô tả | Ví dụ |
|---|---|---|
| A — Partition trên 1 Atomic table | 1 Atomic table chứa nhiều nhóm data phân biệt qua 1 partition key, mỗi nhóm có src_stm_code riêng |
cv phân biệt theo scm_code — mỗi scheme có src_stm_code khác nhau |
| B — UNION nhiều Atomic tables | Datamart populate từ N Atomic tables độc lập, mỗi table có schema/grain riêng | Bảng tổng hợp từ insp_case + surveil_nfrc_case + ... |
Cách xử lý tách bộ:
- Tách thành N bộ attribute — 1 bộ per partition value (scheme) hoặc per Atomic table
- Mỗi bộ gồm đầy đủ tất cả attribute của bảng (PK/BK + các cột +
src_stm_code) src_stm_codecủa mỗi bộ map từ Atomic source tương ứng- Nếu Atomic source chưa xác định →
etl_logic_type = pendingtoàn bộ bộ đó - Tên cột
datamart_columnalign với tên cột Atomic source tương ứng
Trường hợp đặc biệt — Conformed Classification Dimension (cl_dim):
- Tách theo
scm_code(scheme) — mỗi scheme = 1 bộ 5 dòng - Tên cột align với Atomic
cv:scm_code,cl_code,cl_nm - Scheme load từ
cv→etl_logic = cv.<col> WHERE cv.scm_code = '<SCHEME>',etl_logic_type = direct - Scheme ETL-generated (không qua
cv) →etl_logic_type = pendingtoàn bộ bộ - Detail Mapping: tên logical dùng
Scheme Code=vàClassification Code=(không phảiScheme=/Code=)
Spec row src_stm_code:
nullable=false | data_domain=Classification Value | data_type=string | key=(trống)
source_entity=<tên Atomic entity của driving table / Atomic table tương ứng bộ>
atomic_table=<driving_table> | source_attribute=Source System Code | atomic_column=src_stm_code
Bước 2 — Tra entity YAML files
Với mỗi attribute cần map:
- Bảng
partial: đọcDatamart/datamart_model.yaml→ lấycolumnshiện có làm baseline — chỉ tra entity YAML Atomic cho delta columns mới - Bảng
newvà delta columns: Tradm_manifest.yaml→ tìm entry theophysical_name→ đọc file YAML tương ứng trongDataModel/Atomic/{subfolder}/ - Lấy
attribute.physical_namexác nhận tênatomic_column - Xác định
etl_logic_typetheo rule trongreference/phase1_attributes.md - Viết
etl_logicđúng format
Bước 3 — Xuất CSV
Header 15 cột:
datamart_entity,datamart_table,datamart_attribute,datamart_column,nullable,data_domain,data_type,key,description,etl_logic,etl_logic_type,source_entity,atomic_table,source_attribute,atomic_column
Export encoding: UTF-8 BOM (utf-8-sig).
Mọi giá trị trong etl_logic và description phải được bao double-quote.
Tên physical — quy tắc bắt buộc: Xem mục PHYSICAL NAMING RULE bên dưới trước khi đặt tên bất kỳ datamart_table hay datamart_column nào.
PHYSICAL NAMING RULE (BẮT BUỘC — đọc trước khi đặt tên cột/bảng)
Nguồn sự thật: system/rules/rule_physical_name_exceptions_datamart.csv — danh sách duy nhất các từ được phép viết tắt.
Quy tắc cốt lõi
Chỉ những từ có trong
rule_physical_name_exceptions_datamart.csvmới được viết tắt. Mọi từ khác phải dùng full word.
⛔ KHÔNG chép bảng exceptions vào file này hay bất kỳ skill/tài liệu nào khác. Danh sách chỉ tồn tại ở đúng 1 nơi là CSV trên. Mọi bản sao nhúng đều sẽ lệch khỏi CSV theo thời gian — đây là lỗi đã xảy ra thật: bản sao cũ trong skill thiếu entry
Calendar → cdr, kèm ví dụ mẫu dạyCalendar Date → calendar_dt, dẫn tới master hiện có 2 tên song song cho cùng 1 logical (calendar_dt_dim_id9 dòng vscdr_dt_dim_id3 dòng).
Bắt buộc đọc CSV trước mỗi lần đặt tên (không nhớ theo trí nhớ, không đọc lại từ ví dụ bên dưới):
cat system/rules/rule_physical_name_exceptions_datamart.csv
Nội dung CSV là cố định, không được tự bổ sung — thêm exception mới phải qua human duyệt và sửa trực tiếp CSV, không sửa trong skill.
Cách áp dụng
- Tách tên logical thành từng token theo dấu cách
- Với mỗi token:
- Nếu từ có trong bảng exceptions → dùng dạng viết tắt tương ứng
- Nếu không → dùng đúng từ đó dạng full word — KHÔNG được thay bằng từ đồng nghĩa hay dạng mở rộng khác
- Nối lại bằng dấu gạch dưới
_
Ràng buộc cốt lõi — bắt buộc: Physical name phải derive trực tiếp từ tên logical, không được thay thế token bằng từ khác dù tương đồng về nghĩa. Quy tắc chỉ cho phép viết tắt các từ trong exceptions, không cho phép mở rộng hay đổi từ.
Ví dụ vi phạm điển hình: logical "Exam Score" → physical
examination_score❌ — chữ "exam" bị mở rộng thành "examination" dù không được phép. Đúng phải làexam_score✅.
Ví dụ đúng:
Certificate Type Code → certificate_tp_code (type → tp; certificate KHÔNG có trong exceptions → full)
Practitioner Code → practitioner_code (không có token nào trong exceptions)
Issue Decision Number → issue_decision_nbr (number → nbr; issue, decision → full)
Organization Name → organization_nm (name → nm; organization → full)
Source System Code → src_stm_code (source → src; system → stm; code → full)
Snapshot Date → snpst_dt (snapshot → snpst; date → dt)
Calendar Date → cdr_dt (calendar → cdr; date → dt — CẢ HAI đều có trong CSV)
Calendar Date Dimension Id → cdr_dt_dim_id (CHỈ dùng làm PK của Dimension cdr_dt_dim; CẤM dùng làm FK trên Fact)
Snapshot Date Dimension Id → snpst_dt_dim_id (FK snapshot date trên Fact Snapshot — role-playing date)
Issue Date Dimension Id → issue_dt_dim_id (FK issue date trên Fact — role-playing date)
Trade Date Dimension Id → trade_dt_dim_id (FK trade date trên Fact — role-playing date)
Practitioner Dimension ID → practitioner_dim_id (dimension → dim; id → id; practitioner → full)
Fact Practitioner Daily Snapshot → fct_practitioner_daily_snpst (fact → fct; snapshot → snpst; practitioner, daily → full — "daily" KHÔNG có trong CSV)
Operational History → opr_hist (operational → opr; history → hist)
Classification Code → cl_code (classification → cl; code → full)
Scheme Code → scm_code (scheme → scm; code → full)
Exam Score → exam_score (exam, score KHÔNG có trong exceptions → full; giữ nguyên "exam", KHÔNG đổi thành "examination")
Exam Start Date → exam_start_dt (date → dt; exam, start → full)
Ví dụ SAI (phổ biến trước đây):
ctf_tp_code ❌ (ctf không phải exception) → certificate_tp_code ✅
prac_code ❌ (prac không phải exception) → practitioner_code ✅
trn_rslt_nm ❌ (trn, rslt không phải exception) → training_result_nm ✅
org_tp_nm ❌ (org không phải exception) → organization_tp_nm ✅
examination_score ❌ (logical là "Exam Score" — "exam" bị mở rộng thành "examination") → exam_score ✅
calendar_dt_dim_id ❌ ("calendar" CÓ trong CSV → phải rút gọn thành cdr) → cdr_dt_dim_id ✅ (chỉ dùng trên cdr_dt_dim)
cdr_dt_dim_id trên Fact table ❌ (làm mất role ngày, vi phạm role-playing) → snpst_dt_dim_id ✅ (Fact Snapshot) hoặc <role>_dt_dim_id ✅
dly / smy / list ❌ (viết tắt của daily/summary/listed — không có trong CSV) → daily / summary / listed ✅
⛔ QUY TẮC BẮT BUỘC: ROLE-PLAYING DATE DIMENSION TRÊN FACT TABLE (BÀI HỌC GSDC 2026-09-08):
Calendar Date Dimension Id(cdr_dt_dim_id) CHỈ là Primary Key của chính bảng Dimensioncdr_dt_dim(table_type = dim).- TUYỆT ĐỐI CẤM dùng
Calendar Date Dimension Id/cdr_dt_dim_idlàm Foreign Key trên bất kỳ Fact table nào!- Mọi FK trỏ tới
cdr_dt_dimtrên Fact table là Role-Playing Dimension Key, PHẢI đặt tên theo đúng vai trò nghiệp vụ (Role):
- Fact Snapshot (
fct_*_snpst): Cột ngày snapshot kỳ bắt buộc làSnapshot Date Dimension Id→snpst_dt_dim_id(snapshot → snpst, date → dt, dimension → dim, id → id).- Các vai trò ngày khác trên Fact:
<Role> Date Dimension Id→<role>_dt_dim_id(ví dụ:Issue Date Dimension Id→issue_dt_dim_id,Trade Date Dimension Id→trade_dt_dim_id,Evaluation Date Dimension Id→evaluation_dt_dim_id,Submission Date Dimension Id→submission_dt_dim_id,Effective Date Dimension Id→effective_dt_dim_id...).- Nguyên nhân gốc rễ gây lỗi: Do tên logical trong HLD/LLD bị ghi generic là "Calendar Date Dimension Id" thay vì "Snapshot Date Dimension Id", dẫn tới thuật toán derive physical name sinh ra
cdr_dt_dim_id. Khi thiết kế Fact, phải đặt tên logical theo vai trò ngày trước khi sinh physical name.
Bài học từ các ví dụ SAI: Cần luôn
catCSV trước khi đặt tên và đối chiếu vai trò nghiệp vụ của cột — bản thân danh sách ví dụ này cũng chỉ là minh hoạ, KHÔNG phải nguồn sự thật.
⚠️ PHÂN ĐỊNH: ROLE-PLAYING DATE FK vs DEGENERATE DATE ATTRIBUTE: Không phải mọi cột ngày trên Fact table đều trỏ sang Date Dimension. Chỉ trục thời gian phân tích chính mới là Role-Playing Date FK:
Tiêu chí Role-Playing Date FK Degenerate Date Attribute Khi nào dùng Trục thời gian phân tích chính: snapshot date, trade date, event date Thuộc tính ngày mô tả nghiệp vụ: ngày ký, ngày sinh, ngày lập biên bản Data Domain Surrogate Dimension KeyDatehoặcTimestampData Type stringdatehoặctimestampKey FKTrống ( "") — CẤMFK,BK,DDetl_logic_type lookup_datedirecthoặcjoin_atomic— CẤMlookup_dateHậu tố tên _dt_dim_id_dt(VD:violation_record_dt,birth_dt) — CẤM_Dimension_IdBài học thực tế (NHNCK):
violation_record_dttừng bị đặt nhầm thànhviolation_record_dt_dim_id(commit 742aede) — đây là Degenerate Date, KHÔNG phải FK.
Lỗi tái diễn — 2 biến thể viết tắt song song cho CÙNG một tên bảng/entity (phát hiện ở GSDC 2026-07-16): Khi module có nhiều bảng Fact/Dim cùng gắn với 1 khái niệm nghiệp vụ (VD: "Public Company"), rất dễ đặt tên bảng đầu tiên theo 1 kiểu viết tắt tự nghĩ ra (
pblc_co_dim) rồi bảng sau lại đặt theo kiểu khác (fct_pc_risk_score_snpst) — cả 2 đều KHÔNG có trong exceptions và KHÔNG nhất quán với nhau. Nguyên nhân sâu xa: dễ nhầm lẫn giữa 2 ngữ cảnh — (a)atomic_table/atomic_columnlà tên Atomic gốc (read-only, VD:pc_evaluation_detail,pc_report_submission,pc_id— những tên này giữ nguyên, không đổi), và (b)datamart_table/datamart_columnlà tên Datamart tự đặt, phải tuân physical naming rule độc lập với cách Atomic đặt tên. Thấy Atomic dùng tiền tốpc_rồi bắt chước đặt tên Datamart cũngpc_/pblc_colà sai — hai tầng đặt tên độc lập nhau. Quy tắc phòng ngừa: Trước khi đặt tên bảng/cột Datamart mới cho 1 entity xuất hiện ở nhiều bảng trong cùng module,greptoàn bộ tên hiện có của entity đó trongdatamart_attributes.csv+ các file Attributes detail đã duyệt trước — đảm bảo dùng lại đúng 1 biến thể duy nhất, không tự nghĩ ra biến thể mới giữa chừng.
Khi đặt tên datamart_column
- Mọi cột trong Attributes CSV phải tuân thủ rule này
- Khi kế thừa từ
atomic_column: nếu Atomic column dùng tên chuẩn (ví dụsp_code,practitioner_position_at_rpt) → giữ nguyên; chỉ đổi nếu Atomic column đang dùng sai convention - Khi đặt tên mới (ETL-derived, computed): áp dụng rule từ đầu
- Không copy tiền tố viết tắt từ tên Atomic sang tên Datamart —
atomic_table/atomic_column(VD:pc_evaluation_detail) là namespace riêng của Atomic, không phải gợi ý cách viết tắt chodatamart_table/datamart_column
Khi review/detect lỗi
Nếu phát hiện datamart_column hoặc datamart_table dùng từ viết tắt không có trong exceptions, HOẶC dùng 2 biến thể viết tắt khác nhau cho cùng 1 entity/khái niệm trong cùng module:
- Tra
system/rules/rule_physical_name_exceptions_datamart.csvxác nhận - Đây là Kịch bản C — Lỗi thiết kế → sửa trực tiếp file Attributes detail +
datamart_attributes.csv+Detail_Mapping.csv(cộtlogic) +HLD.md(tên bảng trong text/mermaid) +datamart_model.yaml(chỉ fieldid/datamart_table/physical_name— KHÔNG dùngyaml.dumpđể ghi lại toàn file vì sẽ phá format/comment gốc, chỉ sửa bằng text-replace theo dòng cụ thể) + tên file Attributes detail (nếu tên file chứa physical_name cũ) + 2 file SQL Phase 3 (nếu đã sinh) - Dùng
grep -rn "tên_cũ" Datamart/để tìm tất cả vị trí trước khi sửa - Tuyệt đối không sửa cột
source_entity/atomic_table/source_attribute/atomic_columntrong Attributes, haysource_atomic_table/source_atomic_columntrongdatamart_model.yaml— đây là tên Atomic gốc (read-only), kể cả khi trùng chuỗi ký tự với tên Datamart bị đổi (VD: đổipc_codeDatamart-side nhưng KHÔNG đổipc_codexuất hiện trongsource_atomic_column: "public_company.pc_code") - Sau khi sửa: chạy lại SELF-REVIEW đầy đủ (Phase 1 — đủ 10 TC + Phase 2 TC5/TC6 module-level) để xác nhận số dòng/cấu trúc không đổi trước và sau khi đổi tên
⛔ QUY TẮC BẮT BUỘC — MỞ RỘNG NGOÀI PHẠM VI ĐẶT TÊN: MỌI sửa
etl_logic(join-key, filter, công thức) vào 1 file Attributes detail đã tồn tại PHẢI đồng bộ ngaydatamart_attributes.csv— không riêng lỗi đặt tên vật lý:
- 5 bước trên viết cho lỗi đặt tên (Kịch bản C — naming) — nhưng nguyên tắc "sửa module file thì phải đồng bộ master" áp dụng cho MỌI loại Kịch bản C, kể cả sửa lỗi logic nghiệp vụ (sai điều kiện JOIN, thiếu filter, sai công thức) không đổi tên bảng/cột nào.
- Bài học thực tế (module GSTT, 2026-09-11): Qua review phát hiện + sửa 2 bug join-key/filter trực tiếp vào
Datamart/lld/GSTT/DTM_GSTT_fct_stock_portfolio_snpst.csv— chỉ sửa file module, KHÔNG đồng bộdatamart_attributes.csv. Vì tên bảng/cột không đổi (chỉ nội dungetl_logicđổi), TC7 (bản cũ) không phát hiện được, và bug fix được báo cáo "hoàn tất" trong khi master registry vẫn chứa logic sai. Chỉ lộ ra khi user hỏi lại ở lượt hội thoại sau.- Quy tắc bắt buộc: Sau BẤT KỲ Edit nào vào
etl_logic/descriptioncủa 1 fileDatamart/lld/{MODULE}/DTM_{MODULE}_*.csvđã tồn tại (không phân biệt sinh mới hay sửa lỗi, không phân biệt lối vào là Phase 1 hay bàn giao từdatamart-review) — ngay lập tức đồng bộ dòng tương ứng trongdatamart_attributes.csvbằng chuỗietl_logicgiống hệt, rồi chạy TC7 Sub-check E (xem mục TC7 bên dưới) để xác nhận khớp — trước khi báo cáo hoàn tất cho human, không đợi tới lần chạy SELF-REVIEW đầy đủ tiếp theo.- Không có ngoại lệ "sửa nhỏ", "chỉ đổi 1 điều kiện AND" — mọi thay đổi nội dung
etl_logic, dù nhỏ, đều phải đồng bộ ngay.
Bước 4 — SELF-REVIEW trước khi trình bày kết quả
Bắt buộc thực hiện sau khi sinh xong mỗi file, trước khi trình bày cho human duyệt. Chạy đủ 10 testcase: TC1, TC2, TC2b, TC3 (gồm sub-check TC3b), TC4 (gồm sub-check A/B/C/D), TC5, TC6, TC7, TC8, TC9 — báo kết quả đủ 10 dòng PASS/FAIL, không gộp, không bỏ mục:
TC1 — Số cột khớp HLD:
- Đếm số attribute trong file CSV vừa sinh (theo
datamart_attributeunique, không đếm multi-source row). - Đối chiếu với bảng entity tương ứng trong
DTM_{MODULE}_HLD.md(Section 3) — số cột trong bảng HLD. - Báo:
✅ TC1 PASS: X cột — khớp HLDhoặc❌ TC1 FAIL: CSV có X cột, HLD có Y cột — Chênh: [danh sách cột thừa/thiếu]. - Nếu FAIL → sửa trước khi trình bày.
TC2 — etl_logic_type hợp lệ:
- Kiểm tra mọi row không phải PK:
etl_logic_typekhông được trống (BK vẫn phải điền). - Sub-check bắt buộc: Với mỗi row có
etl_logic_type ∈ {direct, computed}: kiểm traatomic_tablecủa row đó có phải driving table không. Nếuatomic_tablekhác driving table → phải đổi thànhjoin_atomic. Ngoại lệ:lookup_dim,lookup_date,pivot,pendingkhông áp dụng sub-check này. - Kiểm tra đặc biệt: khi file CSV có nhiều
atomic_tablekhác nhau (ký hiệu/hoặc nhiều row khác nhau) → bảng nào không phải driving phải dùngjoin_atomic. - Báo:
✅ TC2 PASShoặc❌ TC2 FAIL: [danh sách row sai etl_logic_type — ghi rõ atomic_table và driving table]. - Nếu FAIL → sửa trước khi trình bày.
TC2b — key hợp lệ theo loại bảng (bắt buộc, dùng bảng examples/key_constraints.md):
- Xác định loại bảng (
fact/dim/operational) từdatamart_entity/tên file. - Fact: không được có bất kỳ row nào
key = PK. ChỉFK → <Dim>/DD/ (trống) hợp lệ. - Dimension:
key ∈ {PK, BK, (trống)}— cấmFK,DD. Bắt buộc có ít nhất 1PKvà ít nhất 1BK. - Operational:
key ∈ {PK, BK, (trống)}— cấmFK,DD. Bắt buộc có đúng 1PK(thường là_codeđóng vai trò PK, không tạo_idriêng). - Với mọi row
key = BK:etl_logicvàetl_logic_typekhông được trống (BK là business key thật, map từ Atomic — không phải surrogate generated). - Với mọi row
key = PK:descriptionkhông được lẫn chữ "BK" hoặc ngược lại — token trongdescription(nếu có nhắc lại key) phải khớp đúng giá trị cộtkeycủa chính dòng đó. - Sub-check bắt buộc — format
etl_logic/4 cột source của dòngkey = PKtrên Dimension/Operational: Dòng PK phải có ĐÚNG format:etl_logic= trống,etl_logic_type = Generated,source_entity = Generated, và cả 3 cộtatomic_table/source_attribute/atomic_column= trống. Vi phạm khi: (a)etl_logicchứa literal chuỗi"Generated"thay vì để trống (VDetl_logic = Generated, etl_logic_type = direct— sai cả 2:etl_logickhông được có giá trị, vàetl_logic_typephải làGeneratedchứ không phảidirect); (b) bất kỳ cột nào trongatomic_table/source_attribute/atomic_columncó giá trị"Generated"thay vì để trống — chỉ riêngsource_entitymới được ghiGenerated.- Bài học thực tế (rà soát 2026-07-27): 3 file (
foreign_investor_dim,securities_dim,market_index_dim— cả bản gốc lẫn masterdatamart_attributes.csv) đều ghi dòng PK dạng...,Generated,direct,Generated,Generated,Generated,Generated(tổng 6 ô cuối:etl_logic,etl_logic_type, rồi 4 cột source đều dính literalGeneratedhoặcdirectsai) — trong khi các Dimension khác cùng dự án (cl_dim,securities_practitioner_dim,sp_license_certificate_type_dim) đã làm đúng:etl_logictrống,etl_logic_type = Generated, chỉsource_entity = Generated, 3 cột sau trống. Xem ví dụ đúng ởexamples/key_constraints.mddòng "Inspection Team Dimension","PK",...,"","Generated". - Báo:
✅ TC2b-PK-format PASShoặc❌ TC2b-PK-format FAIL: [tên bảng] dòng PK có etl_logic='[giá trị]' (phải trống) hoặc cột source='[giá trị]' (phải trống, trừ source_entity).
- Bài học thực tế (rà soát 2026-07-27): 3 file (
- Báo:
✅ TC2b PASShoặc❌ TC2b FAIL: [danh sách row vi phạm — ghi rõ key hiện tại, loại bảng, và vi phạm cụ thể]. - Nếu FAIL → sửa trước khi trình bày (Dim/Operational thiếu etl_logic cho BK → điền đầy đủ).
- Fact có
key = PK→ XÓA HẲN TOÀN BỘ DÒNG (row) khỏi CSV — không phải chỉ đổi giá trị cộtkeythành trống và giữ nguyên dòng. Bài học thực tế (module GSDC, 2026-07-22): đã từng chỉ đổikey: PK→key: ''mà giữ nguyên dòngfct_..._id, khiến cột surrogate thừa vẫn tồn tại trong Attributes/registry/SQL sau khi báo "đã fix" — human phải tự phát hiện lại. Trước khi xóa, kiểm tra cột đó có được tham chiếu ở nơi khác không (greptrongDetail_Mapping.csvvàHLD.md): nếu KHÔNG có tham chiếu nào → xóa hẳn dòng; nếu có bằng chứng ETL cần cột đó cho merge/upsert kỹ thuật → giữ dòng nhưngkeyđể trống (ngoại lệ hiếm, cần nêu rõ lý do). Khi xóa, đồng bộ đủ 4 nơi: (1) file Attributes detail, (2) masterdatamart_attributes.csv, (3)datamart_model.yaml— xóa cả blockcolumnstương ứng bằng text-replace theo block, KHÔNG dùngyaml.dump, (4) file SQL Phase 3 đã sinh nếu có (01_create_*.sqldòng CREATE,02_populate_*.sqldòng SELECT). - Sub-check Date FK trên Fact table (Role-Playing Date Dimension — bắt buộc):
- Mọi Fact table (
table_type: "fact"hoặc bảng bắt đầu bằngfct_):- CẤM TUYỆT ĐỐI cột có tên
datamart_column = 'cdr_dt_dim_id'hoặcdatamart_attribute = 'Calendar Date Dimension Id'. Cộtcdr_dt_dim_idCHỈ ĐƯỢC PHÉP là PK của chính bảngcdr_dt_dim. - Mọi FK trỏ tới
cdr_dt_dimtrên Fact PHẢI mang tên theo vai trò nghiệp vụ (Role):- Nếu là Fact Snapshot (tên bảng có hậu tố
_snpst): Cột snapshot date bắt buộc làdatamart_attribute: "Snapshot Date Dimension Id",datamart_column: "snpst_dt_dim_id". - Nếu là Fact Transaction/Event/khác: Bắt buộc dùng
<Role> Date Dimension Id→<role>_dt_dim_id(ví dụissue_dt_dim_id,trade_dt_dim_id,submission_dt_dim_id,evaluation_dt_dim_id,effective_dt_dim_id...).
- Nếu là Fact Snapshot (tên bảng có hậu tố
- Nếu phát hiện
cdr_dt_dim_idtrên Fact table → báo FAIL ngay lập tức và yêu cầu đổi tên theo vai trò nghiệp vụ.
- CẤM TUYỆT ĐỐI cột có tên
- Script kiểm tra tự động (bắt buộc dùng Bash tool):
python -c " import csv, sys sys.stdout.reconfigure(encoding='utf-8') files = ['<path_to_attributes_file>'] # file Attributes CSV vừa sinh BANNED = {'cdr_dt_dim_id', 'calendar_dt_dim_id'} BANNED_LOGICAL = {'Calendar Date Dimension Id'} violations = [] for fpath in files: with open(fpath, encoding='utf-8-sig') as f: reader = csv.DictReader(f) for i, row in enumerate(reader, 2): tbl = row.get('datamart_table','') col = row.get('datamart_column','') attr = row.get('datamart_attribute','') # Skip the cdr_dt_dim table itself (PK is valid there) if tbl == 'cdr_dt_dim': continue if tbl.startswith('fct_') or 'fact' in row.get('table_type','').lower(): if col in BANNED or attr in BANNED_LOGICAL: violations.append(f' Row {i}: {tbl}.{col} ({attr})') if violations: print('❌ TC2b-DATE-FK FAIL:') for v in violations: print(v) else: print('✅ TC2b-DATE-FK PASS') "
- Mọi Fact table (
TC3 — Đầy đủ prefix table_name.column_name + thứ tự JOIN đúng:
- Kiểm tra mọi column reference trong
etl_logiccó dạng<table>.<col>. - Ngoại lệ không cần prefix: literal values, SQL functions (
YEAR(...),COUNT(...)), NULL, ETL runtime parameter ({etl_date}). - Sub-check thứ tự JOIN (bắt buộc): Với mọi row có
etl_logic_type ∈ {join_atomic, lookup_dim, lookup_date}màetl_logicchứa từ khóaJOIN: kiểm traetl_logiccó chứa dấu→(hoặc->), và toàn bộ JOIN clause phải nằm TRƯỚC dấu→, cột giá trị nằm SAU. Vi phạm khi: (a) cóJOINnhưng không có→/->, hoặc (b) cột giá trị xuất hiện trước từ khóaJOINđầu tiên trong chuỗi. - Báo:
✅ TC3 PASShoặc❌ TC3 FAIL: [danh sách etl_logic thiếu prefix hoặc sai thứ tự JOIN — ghi rõ loại lỗi]. - Nếu FAIL → sửa trước khi trình bày.
Sub-check TC3b — Cú pháp LOOKUP <dim> ON ... bắt buộc cho mọi lookup FK sang Dimension (bắt buộc):
- Bài học thực tế (module QLCB, 2026-07-23): sub-check thứ tự JOIN ở trên chỉ kích hoạt khi
etl_logicchứa từ khóaJOIN— 4/6 file LLD của QLCB dùng dạng<dim>.<col> WHERE <dim>.<col> = <driving>.<col>(không cóJOINkeyword, chỉ cóWHERE) hoặc multi-hopINNER JOIN ... WHERE <dim>.<col> = ... → <dim>.<col>(JOIN đúng thứ tự trước→, nhưng hop lookup cuối vẫn dùngWHEREthay vìLOOKUP...ON) — cả 2 pattern đều PASS sub-check thứ tự JOIN vì không vi phạm quy tắc "JOIN trước, giá trị sau→", nhưng vẫn sai format chuẩn. Xemexamples/etl_logic_wrong.mdmục SAI 9. - Với mọi row có
etl_logic_type ∈ {lookup_dim, lookup_date}: kiểm tra hop lookup (không JOIN clause phía trước thì là toàn bộetl_logic; có JOIN clause phía trước thì là phần sau dấu→) phải khớp cú phápLOOKUP <dim_table> ON <dim_table>.<col> = <table>.<col>— bắt đầu bằng từ khóaLOOKUP, cóON, không chứaWHEREvalue-first trong chính hop đó. - Vi phạm khi: (a) toàn bộ
etl_logiclà<dim>.<col> WHERE <dim>.<col> = ...(thiếuLOOKUP, giá trị đặt trước điều kiện); (b) có JOIN clause hợp lệ trước→nhưng phần sau→là<dim>.<col>trần hoặc dùngWHEREthay vìLOOKUP <dim> ON .... - Khi phát hiện vi phạm (b), kiểm tra thêm:
etl_logic_typecủa row đó có đang đểjoin_atomicthay vìlookup_dim/lookup_datekhông — nếu hop cuối là lookup dimension thì bắt buộc đổietl_logic_typecho khớp. - Báo:
✅ TC3b PASShoặc❌ TC3b FAIL: [danh sách attribute thiếu LOOKUP...ON — ghi rõ pattern (a) hay (b), và etl_logic_type có cần đổi không]. - Nếu FAIL → sửa trước khi trình bày.
TC4 — Lọc src_stm_code đầy đủ:
Sub-check A — src_stm_code attribute của bảng dim/operational:
- Với mỗi bảng
dimvàoperational: kiểm tra attributesrc_stm_codecóetl_logicchứa điều kiệnWHERE <table>.src_stm_code = '...'hoặcWHERE <table>.src_stm_code IN (...). - Báo fail nếu thiếu:
❌ TC4A FAIL: [tên bảng] thiếu WHERE filter trên src_stm_code.
Sub-check B — JOIN sang bảng Atomic khác driving table:
- Với mọi row có
etl_logic_type ∈ {join_atomic, lookup_dim, lookup_date}: nếu bảng đích (atomic_table) là Atomic entity có cộtsrc_stm_code(xác định bằng cách kiểm traetl_logiccó tham chiếu bảng đó không), thìetl_logicphải chứaAND <atomic_table>.src_stm_code = '<VALUE>'trong điều kiện JOIN. - Ngoại lệ không áp dụng sub-check B:
cv(Classification Value) vàcdr_dt_dim(Calendar Date) — 2 bảng này là conformed/shared, không có phân biệt nguồn. - Lý do (forward-compatibility): khi Atomic table sau này nhận thêm nguồn mới, ETL join không bị nhân bản dữ liệu sai nguồn.
- Báo fail nếu thiếu:
❌ TC4B FAIL: [attribute] — join sang [atomic_table] thiếu AND src_stm_code filter.
Sub-check C — Giá trị src_stm_code đúng format (không chép theo tên file Atomic):
- Giá trị
src_stm_codelà data value (dùng dấu_nối module + table, VDNHNCK_CERTIFICATES) — khác với tên file Atomic YAML (dùng dấu., VDdm_atm_..._-NHNCK.CERTIFICATES.yaml). Đây là 2 thứ khác nhau; nhầm lẫn giữa chúng là lỗi đã từng xảy ra thực tế (2026-07-21, module NHNCK/QLCB/QLKD). - Regex kiểm tra: mọi giá trị trong
src_stm_code = '...'hoặcsrc_stm_code IN (...)không được chứa dấu.giữa 2 khối chữ hoa (pattern lỗi:'[A-Z][A-Z0-9_]*\.[A-Z][A-Z0-9_]*'). - Nếu phát hiện dấu chấm → tra lại
classification_contextcủa attribute "Source System Code" trong Atomic YAML tương ứng (theo quy trình tra cứu Atomic ở đầu skill) để lấy giá trị đúng, KHÔNG tự suy ra bằng cách thay.→_. - Áp dụng luôn cho tên file dim/operational:
DTM_{MODULE}_{mart_table}_{src_stm_code}.csv— phần{src_stm_code}trong tên file cũng phải dùng dấu_, không dùng dấu.. - Báo fail nếu phát hiện:
❌ TC4C FAIL: [file/attribute] — src_stm_code = '[giá trị sai]' dùng dấu chấm, đúng phải là '[giá trị đúng]'.
Sub-check D — Giá trị src_stm_code khớp đúng module nguồn thật của bảng đích (không suy diễn theo tên module đang thiết kế):
- TC4B chỉ kiểm tra "có filter
src_stm_codehay không"; TC4C chỉ kiểm tra "định dạng gạch dưới hay dấu chấm". Cả 2 đều không verify bản thân giá trị filter có đúng thật không — đây là lỗi đã xảy ra thực tế (module NHNCK, 2026-07-21):etl_logicJOIN tớigeographic_area(Atomic entity thuộc phân hệ ECAT, không phải NHNCK) nhưng viếtAND geographic_area.src_stm_code = 'NHNCK_COUNTRIES'— đúng định dạng gạch dưới (qua TC4C), có mặt điều kiện lọc (qua TC4B), nhưng sai hoàn toàn giá trị vì tự suy diễn theo tên module đang thiết kế (NHNCK) thay vì tra cứu module nguồn thật của bảng đích (geographic_arealuôn thuộc ECAT dù được dùng bởi bất kỳ module nào). - Quy tắc: mọi khi
etl_logic_type ∈ {join_atomic, lookup_dim, lookup_date}JOIN sang 1 shared/cross-module Atomic entity (entity dùng chung nhiều module — dấu hiệu nhận biết: entity nằm trong BCV Core ObjectLocation/Involved Party/Common/Classification, hoặc tên entity không có prefix module cụ thể, VDgeographic_area,ip_alternative_identification,ip_postal_address) — không được suy diễn giá trịsrc_stm_codetừ tên module đang thiết kế. Bắt buộc mở đúng file Atomic YAML của bảng đích (grep -rl "entity_physical_name: \"<atomic_table>\"" DataModel/Atomic/**/*.yaml DataModel/working/Atomic/**/*.yaml), đọcclassification_context/etl_derived_valuecủa attribute "Source System Code" trong CHÍNH FILE ĐÓ (không phải file của driving table module đang thiết kế) để lấy giá trị thật. - Ví dụ cụ thể: bảng
geographic_arealuôn có nguồndm_atm_geographic_area-ECAT.COUNTRY.yaml(hoặc.REGION/.PROVINCE_NEW/... tùy cấp hành chính) — giá trịsrc_stm_codeđúng luôn có prefixECAT_, bất kể module Datamart nào (NHNCK, QLCB, GSDC...) đang JOIN tới nó để lấy tên quốc gia/tỉnh/thành. - Cách kiểm tra bằng script: với mỗi giá trị
src_stm_code = 'X'xuất hiện trongetl_logic, tách phần trước dấu_đầu tiên trongX— nếu phần đó KHÔNG khớpsource_systemthật của Atomic YAML chứaatomic_tableđang JOIN (tra bằnggrep "source_system:" <file>.yamlngay dưới attribute Source System Code) → nghi vấn sai, cần xác nhận thủ công bằng cách đọcclassification_context/etl_derived_value. - Báo fail nếu phát hiện:
❌ TC4D FAIL: [attribute] — join sang [atomic_table] dùng src_stm_code = '[giá trị nghi sai]', nhưng Atomic YAML của [atomic_table] ghi source_system=[X] → giá trị đúng phải là '[giá trị đúng theo classification_context]'.
Báo tổng: ✅ TC4 PASS (A, B, C, D đều pass) hoặc liệt kê từng lỗi A/B/C/D.
Nếu FAIL → sửa trước khi trình bày.
TC5 — Cấu trúc CSV hợp lệ (bắt buộc dùng Bash tool):
- Sau khi Write file, chạy lệnh sau bằng Bash tool:
python -c " import csv, sys sys.stdout.reconfigure(encoding='utf-8') # auto-detect delimiter with open('<path_to_file>', encoding='utf-8-sig') as sniff_f: dialect = csv.Sniffer().sniff(sniff_f.read(2048)) with open('<path_to_file>', encoding='utf-8-sig') as f: rows = list(csv.reader(f, delimiter=dialect.delimiter)) bad = [i for i,r in enumerate(rows) if len(r) != 15] print(f'Rows: {len(rows)-1} data rows') print('Bad rows:', bad if bad else 'none') " - Báo:
✅ TC5 PASS: N rows × 15 colshoặc❌ TC5 FAIL: row [i] có X cột — [nội dung row]. - Nếu FAIL → sửa (thường do dấu
"thiếu trong ô trống hoặc dấu phẩy trong etl_logic chưa được quote) → chạy lại TC5 → báo kết quả.
TC6 — Physical name khớp tất định với logical name (bắt buộc dùng Bash tool):
- Mục đích: TC6 bản đầu (chỉ "tự nhìn bằng mắt") đã được kiểm chứng là không phát hiện được gì — khi áp dụng thử thuật toán tất định bên dưới lên
datamart_attributes.csv/datamart_model.yamlhiện có (2026-07-16), phát hiện thực tế 2 lỗi cùng loại đã lọt từ trước: "Practitioner" → viết tắtprac(9 bảng NHNCK) và "Securities Company" → viết tắtsc(4 bảng NHNCK) — cả hai đều không có trong exceptions. TC6 phải tính toán, không đoán. - Thuật toán: với mỗi
datamart_entity(logical name), tính physical name kỳ vọng bằng cách áp dụng đúng PHYSICAL NAMING RULE (tách từng từ theo khoảng trắng/gạch ngang, trarule_physical_name_exceptions_datamart.csv, giữ full word nếu không có exception, nối bằng_) — rồi so vớidatamart_tablethực tế trong toàn bộdatamart_attributes.csv(mọi module, không chỉ module đang thiết kế — bắt cả trường hợp entity conformed/shared bị đặt tên lệch giữa các module). - Chạy script sau trên toàn bộ master sau khi merge (không chỉ file vừa sinh — vì lỗi có thể đã tồn tại từ trước, TC6 phải quét lại toàn bộ mỗi lần có thay đổi):
python -c " import csv, re, sys sys.stdout.reconfigure(encoding='utf-8') # Whitelist ngoại lệ đã xác nhận là quy ước riêng, không phải lỗi — cập nhật khi có ca mới được human duyệt # CẢNH BÁO: KHÔNG thêm entry vào đây chỉ vì "có vẻ là quy ước riêng" — phải xác minh bằng chứng cụ thể # (VD: rule_physical_name_exceptions_datamart.csv, tài liệu quyết định, hoặc human xác nhận trực tiếp). # Bài học 2026-07: 'cls_dim' từng bị thêm vào đây với lý do tự suy diễn "quy ước 3-ký-tự cho bảng conformed" # — không có căn cứ nào cả, chỉ là lỗi gõ tay (thừa chữ 's') trong datamart_model.yaml. Việc thêm exception # đã che giấu lỗi thật khỏi TC6/TC7 trong nhiều tháng dù cả 2 TC vẫn chạy đúng và báo đúng mismatch mỗi lần — # vấn đề nằm ở người đọc kết quả tự nhận định "known exception" mà không truy nguyên gốc. KNOWN_EXCEPTIONS = { 'fct_public_company_nonfinancial_score_snpst', # "Non-Financial" viết liền không gạch dưới ở tên bảng (naive split coi "Non"/"Financial" là 2 từ riêng) — nhưng LƯU Ý: cột non_financial_m_score trong Fact Risk Score Snapshot lại giữ gạch dưới → không nhất quán thật giữa 2 bảng, cân nhắc thống nhất khi rà soát GSDC } exceptions = {} with open('system/rules/rule_physical_name_exceptions_datamart.csv', encoding='utf-8-sig') as f: reader = csv.reader(f) next(reader) for row in reader: if len(row) >= 2: exceptions[row[0].strip().lower()] = row[1].strip().lower() def expected_physical(logical_name): words = re.findall(r\"[A-Za-z0-9']+\", logical_name) # tách cả từ ghép có gạch ngang (Non-Financial -> Non, Financial) tokens = [exceptions.get(w.lower(), w.lower()) for w in words] return '_'.join(tokens) with open('Datamart/lld/datamart_attributes.csv', encoding='utf-8-sig') as f: rows = list(csv.reader(f)) header = rows[0] ent_idx, tbl_idx = header.index('datamart_entity'), header.index('datamart_table') pairs = set((r[ent_idx], r[tbl_idx]) for r in rows[1:]) fails = [] for ent, tbl in sorted(pairs): if tbl in KNOWN_EXCEPTIONS: continue exp = expected_physical(ent) if exp != tbl: fails.append((ent, tbl, exp)) print(f'Tổng entity: {len(pairs)} | Mismatch: {len(fails)}') for ent, tbl, exp in fails: print(f' ❌ {ent!r} actual={tbl!r} expected={exp!r}') " - Lưu ý xử lý kết quả — không phải mọi mismatch đều là lỗi:
- Nếu
actuallà viết tắt tùy tiện không có trong exceptions (VD:prac,sc,pc,pblc_co) → lỗi thật, phải sửa theo Kịch bản C. - Nếu
actualgiữ full word dù từ đó CÓ trong exceptions (VD:historythay vìhistdù "History" có exception) → lỗi thật khác chiều (thiếu áp dụng exception có sẵn), cũng phải sửa. - Nếu mismatch chỉ do thuật toán tách từ ghép ngây thơ (VD: "Non-Financial" tách thành
non_financialnhưng bảng dùng liềnnonfinancial) → kiểm tra xem cách viết liền có nhất quán ở nơi khác cùng module không; nếu có tiền lệ nhất quán → thêm vàoKNOWN_EXCEPTIONS, không phải lỗi. - Nếu là bảng conformed/shared có vẻ theo "quy ước riêng" → KHÔNG tự kết luận là hợp lệ. Bắt buộc tìm bằng chứng cụ thể (exceptions CSV, tài liệu quyết định) trước khi thêm vào
KNOWN_EXCEPTIONS— không suy diễn quy ước không có căn cứ. Nếu không tìm được bằng chứng → mismatch là lỗi thật, sửa theo Kịch bản C.
- Nếu
- Báo:
✅ TC6 PASS: N entity, 0 mismatchhoặc❌ TC6 FAIL: [danh sách entity | actual | expected]— với mỗi FAIL, phân loại rõ là lỗi thật hay cần thêm KNOWN_EXCEPTIONS trước khi kết luận. - Nếu là lỗi thật → sửa theo Kịch bản C (mục "Khi review/detect lỗi" bên dưới) → chạy lại TC6 trên toàn bộ master → báo kết quả.
- Áp dụng cho cả
datamart_column(không chỉdatamart_table): với mỗidatamart_attribute(logical) trong cùng 1datamart_entity, tính expected tương tự và so vớidatamart_columnthực tế.
TC7 — Tên bảng/cột đồng nhất xuyên suốt các nguồn output (bắt buộc dùng Bash tool):
- Mục đích: TC6 chỉ kiểm tra 1 file (
datamart_attributes.csv) có tự nhất quán với chính nó không. TC7 kiểm tra giữa các nguồn khác nhau có cùng dùng 1 tên hay không — bài học NHNCK 2026-07-16:datamart_model.yamlvàdatamart_attributes.csvtồn tại song song 2 tên khác nhau cho cùng 13 entity (scr_pracvssecurities_practitioner) trong nhiều tháng mà không ai phát hiện, vì mỗi file được review độc lập. - Phạm vi tự động hóa — chỉ 4 nguồn có cấu trúc trường rõ ràng, đối chiếu tất định (không đoán):
datamart_attributes.csv(master) — anchor set, nguồn sự thật duy nhấtDatamart/lld/{MODULE}/DTM_{MODULE}_*.csv(Attributes detail) — cùng cấu trúc cột, so trực tiếpDatamart/datamart_model.yaml— sological_name↔datamart_table(entity),columns[].logical_name↔columns[].physical_name(cột)Datamart/lld/DTM_{MODULE}_Detail_Mapping.csv—mart_table/mart_column(logical) phải resolve đúngdatamart_table/datamart_column(physical) trong anchor, và cộtlogicphải chứa đúng chuỗi<physical_table>.<physical_column>tương ứngDatamart/hld/DTM_{MODULE}_Entities.csv—datamart_entity(logical) phải tồn tại trong anchor set
- KHÔNG tự động hóa cho
HLD.md— free-text + mermaid, regex bắt token dễ false positive (node ID, alias biến, tên Atomic lẫn trong công thức). Khi TC7 FAIL ở bất kỳ nguồn nào trong 5 nguồn trên, bước sửa lỗi (Kịch bản C) đã yêu cầugrep -rn "tên_cũ" Datamart/— lệnh này tự nhiên quét luôn HLD.md, nên HLD vẫn được rà soát nhưng qua cơ chế sửa lỗi thủ công, không qua TC7 tự động. - Chạy script sau (xây anchor set 1 lần, đối chiếu cả 4 nguồn):
python -c " import csv, yaml, re, glob, sys sys.stdout.reconfigure(encoding='utf-8') with open('Datamart/lld/datamart_attributes.csv', encoding='utf-8-sig') as f: rows = list(csv.reader(f)) header = rows[0] idx = {h: i for i, h in enumerate(header)} entity_map, column_map = {}, {} for r in rows[1:]: ent, tbl, attr, col = r[idx['datamart_entity']], r[idx['datamart_table']], r[idx['datamart_attribute']], r[idx['datamart_column']] entity_map[ent] = tbl column_map[(ent, attr)] = col fails = [] # Nguồn 2: Attributes detail — thay '{MODULE}' bằng module đang xử lý for fp in glob.glob('Datamart/lld/{MODULE}/DTM_{MODULE}_*.csv'): with open(fp, encoding='utf-8-sig') as f: rows
Truncated - read the full file at https://github.com/linhlvit/ubck_atomic_design/blob/9503e0ce0b62dca00750362c0690901043087d9c/.claude/skills/datamart-lld-design/SKILL.md.