DataOps nâng cao 19 — Semantic Layer & nhất quán chỉ số
Bài toán "mỗi báo cáo một con số"
Cảnh quen thuộc trong mọi ngân hàng: cuộc họp giao ban, khối tín dụng đưa ra dư nợ cuối tháng là 48.200 tỷ, khối kế hoạch báo 47.900 tỷ, còn dashboard tự làm của một chi nhánh hiện 48.500 tỷ. Ba con số, ba đúng theo cách của người tạo, và ba mươi phút họp trôi qua để tranh cãi con nào đúng thay vì bàn xem dư nợ tăng hay giảm có ý nghĩa gì.
Nguyên nhân gần như luôn giống nhau: cùng một chỉ tiêu được định nghĩa và tính lại ở nhiều nơi khác nhau. Người thì lấy SUM(balance) trên bảng khoản vay tại ngày cuối tháng; người lọc thêm điều kiện trạng thái ACTIVE; người khác quên trừ nợ đã xử lý rủi ro ngoại bảng; một dashboard tính bình quân theo ngày còn dashboard kia lấy số dư cuối kỳ. Logic nằm rải rác trong SQL của mỗi báo cáo, trong công thức của mỗi file Excel, trong LookML của một model, trong DAX của một Power BI khác. Không nơi nào sai cú pháp, nhưng không nơi nào đồng ý với nơi nào.
Hệ quả không chỉ là số lệch. Nó là mất niềm tin: khi lãnh đạo thấy hai con số cho cùng một tên gọi, họ ngừng tin cả hệ thống báo cáo và quay về hỏi trực tiếp một chuyên viên "cho anh con số thật". Đó là dấu chấm hết cho self-service BI.
Bài này giải bài toán ở góc kỹ thuật / DataOps: cách xây và vận hành một metrics layer ngay trong pipeline dữ liệu. Góc quản trị chỉ số & thiết kế nghiệp vụ metric đã bàn ở OSBI 6 — Semantic & metrics; ở đây ta tập trung vào việc định nghĩa chỉ số as-code, đưa nó qua CI/test/review, và phục vụ mọi consumer từ một nguồn duy nhất.
Semantic layer / metrics layer là gì
Semantic layer (tầng ngữ nghĩa) là một lớp nằm giữa kho dữ liệu vật lý (bảng, cột, khoá) và người/công cụ tiêu thụ. Nó ánh xạ ngôn ngữ kỹ thuật (accounts.balance, join key, filter) sang ngôn ngữ nghiệp vụ (dư nợ, CASA, số khách hoạt động) và giữ định nghĩa đó ở một nơi duy nhất.
Metrics layer là phần cốt lõi nhất của semantic layer: nơi khai báo chỉ số (metric — công thức tổng hợp) và chiều (dimension — cách cắt lát: theo thời gian, chi nhánh, loại tiền, phân khúc). Nguyên tắc trung tâm: Define Once, Query Anywhere — định nghĩa "dư nợ" đúng một lần, mọi dashboard, API, notebook đều tiêu thụ cùng một định nghĩa đó thay vì mỗi bên tự viết lại.
Điểm khác biệt với một view SQL thông thường: semantic layer không vật chất hoá sẵn mọi tổ hợp metric × dimension × time-grain (số tổ hợp là tích Descartes, bùng nổ ngay). Thay vào đó nó lưu định nghĩa nguyên tử (measure + join graph + dimension) và sinh SQL đúng lúc truy vấn cho tổ hợp mà consumer yêu cầu. Đây là khác biệt cốt lõi giữa "một đống view" và "một metrics engine".
Headless BI: tách định nghĩa khỏi công cụ
Cách làm cũ nhốt định nghĩa metric bên trong công cụ BI: LookML sống trong Looker, dataset sống trong Power BI. Hệ quả: notebook của data scientist và API của app không dùng được định nghĩa đó — họ phải chép tay lại, và thế là lệch.
Headless BI đảo ngược: semantic layer đứng độc lập, không thuộc công cụ BI nào, và phục vụ nhiều consumer qua interface chung — thường là một SQL endpoint (consumer gõ SQL "ảo" theo tên metric/dimension, layer dịch sang SQL vật lý) và/hoặc REST/GraphQL API. "Headless" nghĩa là có bộ não định nghĩa nhưng không gắn cứng vào một cái đầu (giao diện) cụ thể. Nhờ vậy Power BI, Superset, một React app, và một notebook Python đều lấy đúng cùng con "dư nợ" từ cùng một nguồn.
Các cách tiếp cận & công cụ
Ba trường phái phổ biến hiện nay:
| Công cụ | Định nghĩa ở đâu | Interface consumer | Ghi chú |
|---|---|---|---|
| dbt Semantic Layer (MetricFlow) | YAML trong dbt project, cạnh model | JDBC/SQL, GraphQL, tích hợp BI | Metric sống chung repo với transform → cùng CI/lineage |
| Cube | .yml/.js (data models) | REST, GraphQL, SQL API | Mạnh về caching (pre-aggregation), embed vào app |
| LookML (Looker) | .lkml trong Looker | Chủ yếu qua Looker (+ SQL Runner, API) | Chín muồi nhưng gắn với Looker → kém "headless" |
Với một đội đã dùng dbt cho ELT (xem DataOps nâng cao 2 — dbt & ELT), dbt Semantic Layer / MetricFlow là lựa chọn tự nhiên nhất: metric nằm ngay cạnh model, thừa hưởng cùng CI, cùng lineage, cùng quy trình review. Ba khái niệm chính của MetricFlow:
- Semantic model: gắn với một bảng vật lý (thường là một mart), khai báo
entities(khoá để join, ví dụcustomer,account),dimensions(chiều cắt lát), vàmeasures(đại lượng tổng hợp nguyên tử nhưsum(balance)). - Measure: một phép tổng hợp trên một cột — viên gạch nhỏ nhất.
- Metric: chỉ số nghiệp vụ, xây từ một hay nhiều measure (simple, ratio, derived, cumulative...).
Ví dụ định nghĩa (MetricFlow YAML — minh hoạ, không phải cấu hình chạy được):
# semantic model gắn với mart dư nợ
semantic_models:
- name: loan
model: ref('fact_loan')
entities:
- name: loan_id
type: primary
- name: customer # để join sang dim_customer
type: foreign
expr: customer_id
dimensions:
- name: snapshot_date
type: time
type_params: { time_granularity: day }
- name: currency
type: categorical
measures:
- name: outstanding_balance # đại lượng nguyên tử
agg: sum
expr: balance
agg_time_dimension: snapshot_date
metrics:
- name: du_no # "dư nợ" — định nghĩa MỘT LẦN
label: "Dư nợ tín dụng"
type: simple
type_params:
measure: outstanding_balance
filter: "{{ Dimension('loan__status') }} = 'ACTIVE'"
- name: npl_ratio # tỷ lệ nợ xấu = derived
label: "Tỷ lệ NPL"
type: ratio
type_params:
numerator: du_no_nhom_3_5
denominator: du_no
Với Cube, cùng ý tưởng nhưng cú pháp khác (minh hoạ, không chạy được):
cubes:
- name: deposits
sql_table: marts.fact_deposit
joins:
- name: customers
sql: "{CUBE}.customer_id = {customers}.id"
relationship: many_to_one
measures:
- name: casa_balance
type: sum
sql: balance
filters: [{ sql: "{CUBE}.product_type IN ('CA','SA')" }]
dimensions:
- name: currency
type: string
sql: currency
- name: month
type: time
sql: snapshot_date
Điểm chung của cả ba: khai báo measure/dimension/join một lần; công cụ tự ghép SQL cho bất kỳ tổ hợp nào consumer hỏi.
Metric as code: nối vào DataOps
Đây là chỗ semantic layer trở thành một phần của DataOps thay vì một tính năng BI đơn lẻ. Định nghĩa metric là code, nên nó được đối xử như mọi artefact code khác:
- Versioned: định nghĩa nằm trong Git; mọi thay đổi có commit, có tác giả, có thời điểm. Muốn biết "dư nợ" từng được định nghĩa thế nào 6 tháng trước — mở lịch sử git.
- Review qua PR: đổi công thức "dư nợ" (ví dụ thêm/bớt điều kiện lọc) phải qua pull request, có owner nghiệp vụ duyệt. Không ai sửa lén định nghĩa chỉ tiêu trong một dashboard cá nhân nữa. Đây là governance thực thi bằng quy trình code — liên quan DataOps nâng cao 4 — CI/CD analytics.
- Test trong CI: mỗi PR chạy CI để (a) validate cú pháp định nghĩa, (b) parse được thành SQL, (c) chạy metric test đối soát giá trị (xem dưới).
- Lineage: vì metric build trên model dbt, lineage kéo dài từ nguồn → staging → mart → metric → dashboard. Truy vết một con số về tận bảng gốc là được — cực kỳ giá trị cho giải trình báo cáo NHNN.
So sánh trực diện với cách cũ ở Dữ liệu — OBT & metrics hiện đại và BI — Metrics & KPI: thay vì "mỗi báo cáo một công thức", ta có một định nghĩa đi qua quy trình như code sản phẩm.
Chiều, join tự động, time grain, aggregation
Sức mạnh thật của metrics layer nằm ở chỗ nó tự lo phần khó của SQL:
- Join tự động theo entity graph: consumer chỉ hỏi "dư nợ theo
citycủa khách hàng"; layer biếtfact_loan.customer_id → dim_customer.id, tự viết JOIN đúng khoá. Consumer không cần biết sơ đồ join. - Time grain (độ hạt thời gian): cùng metric "số khách hoạt động", hỏi theo
day/month/quarterđều được, layer tựdate_truncvà group đúng. Không phải viết 3 query. - Aggregation nhất quán: measure khai
agg: sum/count_distinctmột lần; mọi tổ hợp dimension đều tổng hợp đúng kiểu. Tránh lỗi kinh điển: cộng trung bình của trung bình. - Fan-out / chống đếm trùng: khi join 1-nhiều, đếm distinct entity đúng thay vì phồng số dòng — engine xử lý thay ta.
Caching & hiệu năng
Sinh SQL đúng lúc truy vấn linh hoạt nhưng có thể chậm nếu mỗi lần đều quét bảng fact lớn. Ba lớp giảm tải:
- Pre-aggregation (Cube gọi vậy): vật chất hoá trước một số tổ hợp metric × dimension × grain hay dùng (ví dụ "dư nợ theo chi nhánh theo ngày") thành bảng rollup nhỏ; query trúng rollup thì nhanh, không trúng thì rơi về bảng gốc.
- Result cache: cache kết quả query lặp lại trong một cửa sổ thời gian (dashboard giờ cao điểm).
- Đẩy tính toán xuống warehouse: layer chỉ sinh SQL, còn tính nặng để warehouse (Snowflake/BigQuery/Postgres) làm — nơi có compute đàn hồi.
Đánh đổi caching: nhanh hơn nhưng dữ liệu có độ trễ; phải cân giữa freshness và tốc độ cho từng metric.
Metric test: đối soát để số không đổi bất ngờ
Đây là điểm DataOps-nhất và thường bị bỏ. Ngoài test cấu trúc (dbt test cho model — xem dbt 1 — Tổng quan), metrics layer cần metric test theo dõi giá trị chỉ số qua thời gian:
- Snapshot & so sánh: lưu giá trị metric mỗi kỳ; nếu "dư nợ" hôm nay lệch > X% so với hôm qua mà không có lý do nghiệp vụ → cảnh báo. Bắt được lỗi pipeline làm nhân đôi/mất dòng.
- Đối soát chéo:
du_notừ metrics layer phải khớp con số hạch toán kế toán trong ngưỡng cho phép; lệch quá → chặn deploy. - Regression khi đổi định nghĩa: PR sửa công thức phải in ra "trước/sau" trên dữ liệu mẫu để reviewer thấy tác động, tránh sửa vô tình đổi cả lịch sử báo cáo.
SQL mà semantic layer sinh ra
Để thấy layer thật ra làm gì, đây là loại SQL nó sinh cho một metric. Hai ví dụ dưới chạy được trực tiếp trên sandbox Postgres (mô phỏng metric bằng đúng schema demo).
Metric "số dư bình quân theo loại tiền" — như một measure avg cắt theo dimension currency, kèm bối cảnh khách hàng:
-- ▶ Chạy được
WITH acc AS (
SELECT a.id, a.currency, a.balance, c.city
FROM accounts a
JOIN customers c ON c.id = a.customer_id
)
SELECT
currency,
COUNT(*) AS so_tai_khoan,
ROUND(AVG(balance)::numeric, 2) AS so_du_binh_quan,
ROUND(SUM(balance)::numeric, 2) AS tong_so_du
FROM acc
GROUP BY currency
ORDER BY tong_so_du DESC;
Metric "số khách hoạt động theo tháng" — active = có phát sinh giao dịch trong tháng; đây chính là kiểu SQL (time grain = month, count_distinct entity customer, chống đếm trùng qua nhiều tài khoản/giao dịch) mà một metrics engine sinh ra:
-- ▶ Chạy được
SELECT
date_trunc('month', t.created_at)::date AS thang,
COUNT(DISTINCT c.id) AS so_kh_hoat_dong,
COUNT(DISTINCT a.id) AS so_tk_giao_dich,
ROUND(
COUNT(DISTINCT a.id)::numeric
/ NULLIF(COUNT(DISTINCT c.id), 0), 2) AS tk_tren_kh
FROM transactions t
JOIN accounts a ON a.id = t.account_id
JOIN customers c ON c.id = a.customer_id
GROUP BY date_trunc('month', t.created_at)
ORDER BY thang;
Điểm cần thấy: consumer chỉ khai "cho tôi active_customers theo month"; toàn bộ join transactions → accounts → customers, date_trunc, COUNT(DISTINCT), và chống đếm trùng do layer lo — không consumer nào tự viết lại, nên mọi báo cáo ra cùng một con số.
Tránh trùng lặp logic giữa dbt marts & BI
Một câu hỏi thực tế: logic nên nằm ở mart (dbt) hay ở metric (semantic layer)? Ranh giới nên là:
- Mart: làm sạch, chuẩn hoá, join thành fact/dim ổn định, tính các cột hạt mịn (ví dụ số dư từng khoản vay). Đây là "dữ liệu".
- Metric: phép tổng hợp và tỷ lệ phụ thuộc dimension/time-grain do người dùng chọn lúc truy vấn (dư nợ theo chi nhánh theo quý, NPL ratio, NIM). Đây là "cách đo".
Lỗi thường gặp: nhét mọi tổ hợp tổng hợp vào mart (bùng nổ số bảng) hoặc tính lại logic sạch/join trong metric (trùng dbt). Nguyên tắc: fact hạt mịn ở dbt, aggregation & ratio ở metric layer, không ai lặp lại ai.
Đánh đổi & khi nào cần
Semantic layer thêm một lớp — thêm công cụ, thêm khái niệm, thêm chỗ phải học và vận hành. Nó đáng khi:
- Nhiều consumer (BI + API + notebook + báo cáo tuân thủ) cùng dùng một tập chỉ số cốt lõi.
- Chỉ số có định nghĩa tinh tế, dễ tính sai và hậu quả lệch là nghiêm trọng (báo cáo NHNN, chỉ tiêu quản trị).
- Đã có tranh cãi "con số nào đúng" tái diễn.
Chưa cần vội khi đội nhỏ, một công cụ BI duy nhất, chỉ vài metric đơn giản — khi đó một lớp view chuẩn trong dbt có thể đủ, thêm semantic layer chỉ là chi phí thừa. Như nền tảng self-service, hãy đưa metrics layer vào khi quy mô làm sự lệch trở thành vấn đề thật, đừng làm big-bang từ đầu.
Use case thực tế
Bối cảnh. NCB có bốn nguồn con số "dư nợ": khối tín dụng (SQL tay trên core), khối kế hoạch (file Excel), một dashboard Power BI của phòng số, và báo cáo NHNN từ đội tuân thủ. Giao ban tháng nào cũng mất ~30 phút đối soát; ba lần trong nửa năm, số gửi NHNN phải đính chính vì lệch định nghĩa nợ ngoại bảng.
Giải pháp — metrics layer as-code (dbt Semantic Layer).
- Chốt định nghĩa nghiệp vụ với owner (khối tín dụng):
du_no=SUM(outstanding_balance)các khoảnstatus='ACTIVE', trừ nợ đã XLRR ngoại bảng;npl_ratio= dư nợ nhóm 3-5 / tổng dư nợ;casa_balance;active_customers. - Khai measure/metric trong YAML cạnh mart
fact_loan/fact_deposit; commit vào repo dbt, review qua PR có owner tín dụng duyệt. - CI chạy: validate parse → build SQL → metric test đối soát
du_novới số hạch toán kế toán (ngưỡng lệch < 0,1%) → cảnh báo nếu biến động ngày > 5% không lý do. - Bật headless: Power BI, Superset, đội tuân thủ và notebook rủi ro cùng truy vấn qua SQL endpoint của layer.
- Pre-aggregation "dư nợ theo chi nhánh theo ngày" cho dashboard giờ cao điểm.
Kết quả sau một quý.
| Chỉ số | Trước | Sau |
|---|---|---|
| Số "phiên bản" dư nợ trong tổ chức | 4 | 1 |
| Thời gian đối soát mỗi giao ban | ~30 phút | ~0 |
| Đính chính báo cáo NHNN | 3 lần/6 tháng | 0 |
| Sửa định nghĩa metric | ad-hoc, không dấu vết | 100% qua PR, có lineage |
Mấu chốt: con số về một nguồn không nhờ họp nhiều hơn, mà nhờ định nghĩa sống một chỗ, as-code, qua CI/test — và mọi công cụ buộc phải tiêu thụ chính nó.
Ghi nhớ
- Gốc rễ "mỗi báo cáo một con số" là định nghĩa chỉ tiêu bị tính lại rải rác trong SQL/Excel/LookML/DAX; hệ quả nặng nhất là mất niềm tin vào toàn hệ thống báo cáo.
- Semantic/metrics layer: định nghĩa metric & dimension một lần, một nơi (Define Once, Query Anywhere); lưu định nghĩa nguyên tử và sinh SQL lúc truy vấn, không vật chất hoá mọi tổ hợp.
- Headless BI: layer độc lập với công cụ BI, phục vụ nhiều consumer qua SQL endpoint / REST / GraphQL → BI, API, notebook, báo cáo NHNN cùng một con số.
- Công cụ: dbt Semantic Layer/MetricFlow (semantic model, measure, dimension, entity — cùng repo/CI/lineage với dbt), Cube (mạnh caching, embed app), LookML (chín nhưng gắn Looker).
- Metric as code: versioned trong Git, review qua PR có owner, test trong CI, lineage kéo tới tận bảng gốc — nối thẳng vào DataOps.
- Layer tự lo phần khó của SQL: join theo entity graph, time grain, aggregation nhất quán, chống đếm trùng (fan-out).
- Metric test đối soát giá trị qua thời gian & chéo với hạch toán; chặn số đổi bất ngờ hay regression khi sửa định nghĩa.
- Ranh giới dbt vs metric: fact hạt mịn ở mart; aggregation & ratio phụ thuộc dimension/time-grain ở metric layer — không lặp logic.
- Đánh đổi: thêm một lớp; chỉ đáng khi nhiều consumer, metric dễ tính sai và hậu quả lệch nghiêm trọng — đừng thêm quá 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.