Modeling nâng cao 7 — Activity Schema & Event Modeling

22 thg 7, 2026 2 lượt xem
#data-engineering
#data-modeling
#activity-schema
#narrator
#customer-journey
#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 & snowflakeDimensional 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ấtactivity 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 timeCDC 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ểnEvent modeling
Đơn vị factMột quy trình (sales, payment)Một sự kiện (một hành động)
GrainCố định theo bảngLuôn là "một sự kiện đã xảy ra"
Thêm hành vi mớiThường thêm bảng/cộtThêm dòng có activity mới
Trạng tháiLưu trực tiếpDẫ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_idKhoá duy nhất của bản ghi sự kiện
tsThờ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
activityTên hành vi, ví dụ opened_account, submitted_loan
feature_jsonTúi thuộc tính riêng của activity đó, dạng JSON
revenue_impactGiá trị tiền gắn với sự kiện (nếu có)
linkURL/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_atts 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 customerts, 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< 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ểmNhược điểm
Thay đổi yêu cầuRất linh hoạt, ít mô hình lại; thêm hành vi = thêm dòngKỷ luật đặt tên activity và chuẩn hoá feature_json phải rất nghiêm
GrainCố định "một sự kiện", không bao giờ sai grainMọi thứ dạng "trạng thái" phải dẫn xuất, không lưu sẵn
Truy vấnMọi câu hỏi quy về một khuôn temporal joinTruy 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ạpHiệ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
SemanticsRất hợp product analytics, funnel, journeyKhô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 analyticscustomer 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_approved chỉ 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_center trong 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ới submitted_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ạphiệ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.

13 thg 7, 2026 10

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.

13 thg 7, 2026 9

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.

13 thg 7, 2026 8

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.

13 thg 7, 2026 7

Cảm nhận của bạn

Bình luận

Bạn cần để viết bình luận.

Chưa có bình luận. Hãy là người đầu tiên chia sẻ!