Modeling nâng cao 7 — Activity Schema & Event Modeling
Mô hình tinh thần: một bảng cho mọi câu hỏi
Toàn bộ trường phái dimensional mà ta xây ở Kimball star & snowflake và Dimensional nâng cao dựa trên một giả định: bạn biết trước các câu hỏi nghiệp vụ, nên bạn thiết kế sẵn fact table theo đúng grain của từng quy trình (bán hàng, thanh toán, đăng ký). Mỗi câu hỏi mới thường sinh ra một fact mới, một vòng ETL mới, một lần mô hình lại.
Activity Schema lật ngược giả định đó. Ý tưởng do Narrator (Ahmed Elsamadisi, activityschema.com) đề xuất năm 2020: thay vì hàng chục bảng fact, ta ghi mọi thứ khách hàng làm vào một bảng duy nhất — activity stream — dưới dạng chuỗi sự kiện theo thời gian. Mỗi dòng là một activity (một sự kiện đã xảy ra): khách nào, lúc nào, làm gì, kèm một túi thuộc tính JSON.
Mô hình tinh thần gói gọn trong một câu: thế giới không phải là các bảng trạng thái, mà là một dòng thời gian các sự kiện đã xảy ra. Trạng thái (số dư hiện tại, phân khúc hiện tại) chỉ là kết quả dẫn xuất từ việc phát lại chuỗi sự kiện — cùng tinh thần "the log là nguồn sự thật" ở event time & processing time và CDC event-driven.
Event modeling: fact-per-event
Trước khi nói cấu trúc bảng, cần phân biệt hai cách nghĩ về fact.
| Dimensional cổ điển | Event modeling | |
|---|---|---|
| Đơn vị fact | Một quy trình (sales, payment) | Một sự kiện (một hành động) |
| Grain | Cố định theo bảng | Luôn là "một sự kiện đã xảy ra" |
| Thêm hành vi mới | Thường thêm bảng/cột | Thêm dòng có activity mới |
| Trạng thái | Lưu trực tiếp | Dẫn xuất bằng cách gộp sự kiện |
Fact-per-event nghĩa là mỗi hành vi rời rạc — "mở tài khoản", "đăng nhập app", "chuyển tiền", "bị từ chối khoản vay", "gọi tổng đài" — là một bản ghi bất biến (immutable), append-only, không bao giờ update. Đây chính là tư duy event sourcing áp cho phân tích: bạn không sửa quá khứ, bạn chỉ nối thêm sự kiện. Nhờ vậy grain của activity stream luôn cố định ở mức "một sự kiện", tránh được cái bẫy grain mà mọi mô hình Kimball phải cân nhắc.
Cấu trúc bảng activity stream
Spec Activity Schema (v2.0) quy định một tập cột chuẩn cho mọi activity — đây là điều khiến "một bảng cho tất cả" khả thi:
| Cột | Ý nghĩa |
|---|---|
activity_id | Khoá duy nhất của bản ghi sự kiện |
ts | Thời điểm sự kiện xảy ra (event time) |
customer | Định danh thực thể (khách hàng) — trục để nối các activity |
activity | Tên hành vi, ví dụ opened_account, submitted_loan |
feature_json | Túi thuộc tính riêng của activity đó, dạng JSON |
revenue_impact | Giá trị tiền gắn với sự kiện (nếu có) |
link | URL/tham chiếu tới nguồn |
activity_occurrence | Đây là lần thứ mấy khách này làm activity đó |
activity_repeated_at | ts của lần kế tiếp cùng activity (tiện temporal join) |
Điểm mấu chốt: schema cố định, nội dung linh hoạt. Các thuộc tính đặc thù (số tiền vay, kênh giao dịch, mã lỗi) không nằm ở cột riêng mà gói trong feature_json. Thêm một loại hành vi mới không đổi DDL — chỉ là các dòng mới với activity khác và feature_json khác.
So với star schema, nơi bốn nguồn trên sẽ đổ vào bốn fact table khác nhau (mỗi cái có grain và bộ dimension riêng), ở đây tất cả hội tụ về một dòng thời gian chung trên trục customer + ts.
Temporal join: "relationships between activities"
Nếu chỉ có một bảng phẳng thì trả lời câu hỏi kiểu "trong số khách đăng nhập app, bao nhiêu người nộp hồ sơ vay sau đó?" bằng cách nào? Đây là phần thông minh nhất của Activity Schema: mọi phân tích được diễn đạt như quan hệ thời gian giữa các activity.
Bạn chọn một activity mốc (cohort/primary activity) rồi nối (append) các activity khác vào nó theo một quan hệ thời gian. Các quan hệ chuẩn gồm những họ chính:
- First / Last ever — lần đầu / cuối cùng khách làm activity B (bất kể thời điểm A).
- First before / Last before — activity B ngay trước mốc A.
- First after / Last after — activity B ngay sau mốc A.
- Aggregate (before/after/in between) — gộp nhiều B quanh mốc A: đếm số lần, tổng
revenue_impact, giá trị trung bình... - First/Last in between — B nằm giữa mốc A này và lần lặp kế tiếp của chính A.
Chính vì mọi activity dùng chung cột customer và ts, mọi quan hệ này quy về đúng một khuôn temporal join trên hai trục đó — không cần biết trước sơ đồ quan hệ nào giữa các bảng. Đây là khác biệt bản chất với star schema: quan hệ không được "đóng cứng" bằng khoá ngoại lúc thiết kế, mà được diễn giải khi truy vấn bằng điều kiện thời gian.
Ví dụ SQL: hiện thực một temporal join
Về bản chất "last before" là bài toán as-of join: với mỗi sự kiện mốc, tìm sự kiện B có ts lớn nhất mà vẫn < ts mốc. Trên SQL chuẩn có thể diễn đạt bằng window function:
-- Cohort: mỗi lần khách "submitted_loan"
WITH cohort AS (
SELECT customer, ts AS loan_ts, activity_id
FROM activity_stream
WHERE activity = 'submitted_loan'
),
-- Append LAST BEFORE của activity 'app_login'
login_before AS (
SELECT
c.activity_id,
c.customer,
c.loan_ts,
l.ts AS last_login_ts,
l.feature_json ->> 'device' AS login_device,
ROW_NUMBER() OVER (
PARTITION BY c.activity_id
ORDER BY l.ts DESC -- gần mốc nhất trước
) AS rn
FROM cohort c
JOIN activity_stream l
ON l.customer = c.customer
AND l.activity = 'app_login'
AND l.ts < c.loan_ts -- điều kiện "before"
)
SELECT
customer,
loan_ts,
last_login_ts,
login_device,
EXTRACT(EPOCH FROM (loan_ts - last_login_ts))/60 AS phut_tu_login_toi_vay
FROM login_before
WHERE rn = 1; -- chỉ giữ "last before"
Đổi ORDER BY l.ts DESC thành ASC và < thành > là ta có "first after". Đổi ROW_NUMBER thành COUNT(*)/SUM(revenue_impact) gộp theo cohort là ta có "aggregate". Cùng một khuôn — đó là lý do Narrator có thể sinh SQL này tự động từ vài lựa chọn kéo-thả, thay vì để nhà phân tích tự viết join.
Ưu và nhược
| Khía cạnh | Ưu điểm | Nhược điểm |
|---|---|---|
| Thay đổi yêu cầu | Rất linh hoạt, ít mô hình lại; thêm hành vi = thêm dòng | Kỷ luật đặt tên activity và chuẩn hoá feature_json phải rất nghiêm |
| Grain | Cố định "một sự kiện", không bao giờ sai grain | Mọi thứ dạng "trạng thái" phải dẫn xuất, không lưu sẵn |
| Truy vấn | Mọi câu hỏi quy về một khuôn temporal join | Truy vấn phức tạp — analyst khó tự viết as-of join đúng, thường cần công cụ sinh SQL |
| Hiệu năng | Ít bảng, ít ETL join phía nạp | Hiệu năng nặng: một bảng khổng lồ, temporal self-join tốn kém; phụ thuộc phân vùng theo ts và cluster theo customer |
| Semantics | Rất hợp product analytics, funnel, journey | Không tối ưu cho báo cáo tài chính dạng ma trận (số dư cuối kỳ, sao kê) — nơi star vẫn thắng |
Nói ngắn gọn: Activity Schema đổi chi phí mô hình hoá (con người, trả trước) lấy chi phí truy vấn (máy tính, trả lúc chạy). Với feature_json là JSON, còn có chi phí ép kiểu/parse mỗi lần đọc — cần cân nhắc so với cột đã định kiểu của fact table.
So với star schema — chọn cái nào?
Đây không phải quan hệ thay thế mà là bổ sung, tuỳ loại câu hỏi:
- Star schema (Kimball) thắng ở câu hỏi tổng hợp trên trạng thái đã biết: "doanh số theo chi nhánh theo tháng", "dư nợ cuối kỳ theo nhóm nợ". Grain và dimension ổn định, khối lượng lớn nhưng mẫu truy vấn dự đoán được, tối ưu tốt.
- Activity Schema thắng ở câu hỏi về hành vi và trình tự: "khách làm gì trước khi rời bỏ?", "tỷ lệ chuyển đổi qua từng bước phễu?", "hành trình từ mở tài khoản tới khoản vay đầu tiên mất bao lâu?". Đây là địa hạt product analytics và customer journey, nơi câu hỏi thay đổi liên tục và mối quan hệ thời gian mới là thứ cần mô hình.
Nhiều tổ chức chạy cả hai: activity stream ở tầng khám phá hành vi, rồi vật chất hoá các chỉ số ổn định xuống fact/dimension cho BI và báo cáo pháp lý. Activity stream cũng là nguồn tự nhiên để nuôi Customer 360 — một hồ sơ khách hàng thống nhất chính là kết quả của việc phát lại toàn bộ chuỗi activity của họ.
Use case thực tế
Bối cảnh NCB (số liệu minh hoạ): đội phân tích số muốn hiểu vì sao tỷ lệ hoàn tất hồ sơ vay trên app thấp. Với star schema, câu hỏi này rơi vào vùng xám giữa fact "đăng nhập", fact "hồ sơ vay", fact "giao dịch" — mỗi lần khoan sâu là một lần dựng bảng/join mới, mất vài ngày mỗi vòng.
Chuyển sang một activity_stream gom mọi sự kiện app + core + loan (giả định ~2 tỷ dòng/năm, phân vùng theo ngày ts, cluster theo customer):
- Dựng phễu
app_login → viewed_loan → submitted_loan → loan_approvedchỉ bằng bốn temporal join "first after" liên tiếp, không đổi schema. - Phát hiện (minh hoạ): trong nhóm bị
loan_rejected, 62% cóactivity = call_centertrong 24h sau đó (aggregate after) — tín hiệu ma sát trải nghiệm cần xử lý. - Đo thời gian trung vị từ
opened_account(first ever) tớisubmitted_loan(first after) = 37 ngày — đầu vào cho chiến dịch nurture đúng thời điểm.
Toàn bộ ba phân tích trên dùng chung một bảng, không thêm ETL — thời gian ra insight rút từ "vài ngày" xuống "trong ngày". Đổi lại, phải cấp ngân sách tính toán cho các self-join lớn và giữ kỷ luật danh mục activity.
Ghi nhớ
- Activity Schema (Narrator, activityschema.com): gói toàn bộ hành vi khách hàng vào một bảng activity stream với cột chuẩn
customer,ts,activity,feature_json. - Tư duy nền là event modeling / fact-per-event: mỗi sự kiện là một bản ghi bất biến, append-only; grain luôn cố định ở mức "một sự kiện".
- Trạng thái (số dư, phân khúc) không lưu sẵn mà dẫn xuất bằng cách gộp sự kiện — cùng tinh thần với the-log/event sourcing.
- Phân tích được diễn đạt bằng quan hệ thời gian giữa activity: first/last/aggregate + before/after/in between — quy về một khuôn temporal (as-of) join trên
customer+ts. - Ưu: linh hoạt, ít mô hình lại, thêm hành vi = thêm dòng. Nhược: truy vấn phức tạp và hiệu năng nặng do bảng khổng lồ + self-join.
- Đổi chi phí mô hình hoá (trả trước) lấy chi phí truy vấn (trả lúc chạy); JSON còn thêm chi phí parse.
- Hợp product analytics & customer journey; star schema vẫn thắng ở báo cáo tài chính/pháp lý dạng trạng thái tổng hợp. Thực tế nên kết hợp cả hai.
Nguồn tham khảo
- Narrator — Activity Schema specification (v2.0), activityschema.com (Ahmed Elsamadisi).
- Narrator blog — "The Single Table Data Model" / "Modeling Data with Activity Schema".
- Kimball & Ross, The Data Warehouse Toolkit (Wiley) — chương fact/dimension & grain, để đối chiếu.
- Reis & Housley, Fundamentals of Data Engineering (O'Reilly) — chương Data Modeling & modeling patterns.
- Kleppmann, Designing Data-Intensive Applications (O'Reilly) — event sourcing & derived state.
- Jay Kreps — "The Log: What every software engineer should know about real-time data's unifying abstraction" (blog Confluent/LinkedIn Engineering).
- dbt docs — mô hình hoá dữ liệu theo lớp, docs.getdbt.com (cho phần vật chất hoá xuống fact/dimension).
Bài viết liên quan
So sánh các định dạng dữ liệu (CSV, JSON, XML, Avro, Parquet, ORC) và lý do lưu theo cột nhanh hơn cho phân tích. Bài đi sâu vào row vs columnar storage, nén (Snappy/gzip/zstd), schema evolution, OLTP vs OLAP, object storage và partitioning để tối ưu chi phí lẫn tốc độ truy vấn.
Data Engineering là ngành xây dựng và vận hành hệ thống biến dữ liệu thô thành dữ liệu sạch, tin cậy, sẵn sàng cho phân tích và AI. Bài giới thiệu vai trò Data Engineer trong vòng đời dữ liệu (nguồn → ingestion → storage → transformation → serving), phân biệt với Analyst/Scientist/ML Engineer, bức tranh hệ sinh thái công cụ và bài toán đưa dữ liệu core banking sang kho phân tích.
Stream processing là gì, khác biệt batch vs stream (bounded/unbounded), micro-batch (Spark) vs true streaming, Apache Flink là gì và định vị so với Spark Structured Streaming và Kafka Streams. Kiến trúc runtime JobManager/TaskManager, các tầng API, triết lý streaming-first và bối cảnh phát hiện gian lận ngân hàng.
Vì sao một máy không đủ và cần xử lý phân tán: từ MapReduce, Hadoop/HDFS đến Apache Spark in-memory. Kiến trúc driver–executor–cluster manager, các mức trừu tượng RDD/DataFrame/Dataset, cơ chế lazy evaluation với DAG, và vì sao shuffle (wide dependency) là phần tốn kém nhất. Kèm PySpark, Spark SQL và các kỹ thuật tối ưu (partition, broadcast join, cache, chống skew) cùng khi nào KHÔNG nên dùng Spark.
Cảm nhận của bạn
Bình luận
Chưa có bình luận. Hãy là người đầu tiên chia sẻ!