From System Tables to Operational Analytics
Introduction
Sau khi đã lướt qua sơ bộ những system tables mà Databricks expose, tớ nghĩ chúng ta đã có đủ context để bắt đầu bước vào quá trình dimensional modeling thực sự.
Trong phần này, tớ sẽ thử tiếp cận bài toán theo quy trình 4 bước quen thuộc của Kimball, đồng thời xem thử các concepts truyền thống của dimensional modeling sẽ fit như thế nào với một bài toán thiên nhiều về operational telemetry và runtime analytics như Databricks.
1. Xác định Business Process
1. Thu thập yêu cầu từ stakeholders
Điểm thú vị là trong các bài toán operational analytics kiểu này, “business process” không còn mang nghĩa truyền thống như order fulfillment, payment hay shipment như trong các systems ERP hoặc retail nữa.
Thay vào đó, business process ở đây thường được xác định bằng cách đi ngược từ những operational questions mà platform team thực sự muốn trả lời. Các câu hỏi này có thể có hoặc không giả định trước hệ thống đang xấu. Tuy nhiên mục tiêu chung của model là tạo báo cáo để kiểm chứng: trường hợp nào tốt, bình thường, hoặc cần tối ưu.
Và từ những câu hỏi đó, ta có thể rút ra được các key metrics có thể có:
- startup/setup duration, queue duration, execution/runtime duration.
- overlap count, retry/failure count.
- active/no-query duration, scale event count.
- pool hit/miss count, ready/placed/launching duration.
- CPU, memory, swap, CPU wait.
2. Xác định operational business events
Chỉ chọn những hành động/sự kiện thật sự xảy ra và được hệ thống ghi nhận:
- Job run được trigger và kết thúc.
- Job task run bắt đầu/kết thúc.
- Lakeflow pipeline update được trigger, dùng compute, chạy và kết thúc.
- SQL statement được submit và execute.
- Warehouse start/run/scale/stop.
- Instance trong pool chuyển state: launching, ready, placed, terminated.
- Node được sample utilization theo phút.
3. Đối chiếu với nguồn dữ liệu hiện có
| Candidate process | Source tables | Data readiness |
|---|---|---|
| Job run execution | system.lakeflow.job_run_timeline | Có timeline, duration, status, compute ids |
| Job task execution | system.lakeflow.job_task_run_timeline, system.lakeflow.job_tasks | Có task timeline và task graph metadata |
| Pipeline update execution | system.lakeflow.pipeline_update_timeline, system.lakeflow.pipelines | Có update timeline, result state, trigger, compute và pipeline metadata |
| SQL query execution | system.query.history | Có query duration, wait/capacity time, warehouse id, query source |
| SQL warehouse lifecycle | system.compute.warehouse_events, system.compute.warehouses | Có lifecycle event và config history |
| Instance pool lifecycle | system.compute.instance_events, system.compute.instance_pools | Có instance state event và pool config |
| Cluster/node utilization sampling | system.compute.node_timeline, system.compute.clusters, system.compute.node_types | Có minute-level utilization và cluster/node metadata |
4. Lập bus matrix
| Business process | Fact table | Grain | Natural grain key | Fact type | Key dimensions |
|---|---|---|---|---|---|
| Job run execution | fact_job_run | 1 row per job run | workspace_id + job_id + run_id | Accumulating snapshot | date/time, workspace, job, trigger, run status |
| Job task execution | fact_job_task_run | 1 row per task run | workspace_id + job_id + job_run_id + task_key + task_run_id | Accumulating snapshot | date/time, workspace, job, task, compute resource |
| Pipeline update execution | fact_pipeline_update | 1 row per logical pipeline update | workspace_id + pipeline_id + update_id | Accumulating snapshot | date/time, workspace, pipeline, trigger, update status |
| SQL query execution | fact_query_execution | 1 row per statement | workspace_id + statement_id | Transaction fact | date/time, workspace, warehouse, user, query source |
| Warehouse lifecycle | fact_warehouse_event | 1 row per warehouse event | workspace_id + warehouse_id + event_time + event_type | Transaction fact | date/time, workspace, warehouse, event type |
| Warehouse active state | fact_warehouse_interval | 1 row per warehouse state interval | workspace_id + warehouse_id + interval_start_ts + interval_end_ts | Event interval fact | date/time, workspace, warehouse |
| Instance lifecycle | fact_instance_event | 1 row per instance event | workspace_id + instance_id + event_time + state | Transaction fact | date/time, workspace, instance, pool, cluster, node type |
| Pool instance state | fact_pool_instance_interval | 1 row per instance state interval | workspace_id + instance_id + interval_start_ts + interval_end_ts | Event interval fact | date/time, workspace, pool, instance, cluster, node type |
| Node utilization | fact_node_utilization_minute | 1 row per instance per minute | workspace_id + cluster_id + instance_id + start_time | Periodic snapshot | date/time, workspace, cluster, node type, driver flag |
2. Define Grain
Sau khi xác định được các operational processes chính, bước tiếp theo là xác định grain cho từng fact table.
Trong dimensional modeling, grain thực chất là lời tuyên bố rõ ràng về “một row đại diện cho điều gì”. Đây thường là bước quan trọng nhất của toàn bộ modeling process, vì gần như mọi metric, aggregation hay relationship phía sau đều phụ thuộc vào grain đã chọn.
Với bài toán operational telemetry này, grain phần lớn được quyết định bởi:
- cấp độ mà operational event thực sự xảy ra
- mức granularity cần thiết để trả lời analytical questions
- và mức chi tiết cần giữ lại trước khi aggregate.
Job Run Execution
Phần Job Run khiến tớ khá khó khăn trong việc lựa chọn grain phù hợp. Nguyên nhân là do execution model của Databricks Jobs (và phần lớn workflow orchestrators khác) phức tạp hơn khá nhiều so với accumulating snapshot facts truyền thống trong Kimball.
Về bản chất, mỗi lần workflow được trigger sẽ sinh ra một run_id để đại diện cho logical execution của job. Tuy nhiên trong quá trình chạy, user vẫn có thể retry hoặc repair failed tasks ngay trên chính run_id đó thay vì tạo hoàn toàn một run mới.
Điều này khiến grain “1 row per run_id” bắt đầu trở nên không còn đủ tự nhiên nữa.
- Nếu chọn grain
1 row = 1 run_idthì fact table sẽ khá giống accumulating snapshot fact truyền thống, nơi mỗi row đại diện cho lifecycle tổng thể của một job run. Cách model này phù hợp để phân tích queue duration, startup overhead, total runtime hoặc final status ở cấp tổng thể. Tuy nhiên, vấn đề là grain này không còn thể hiện chính xác retry attempts, repair attempts hoặc execution history chi tiết bên trong cùng một run. Nếu cố nhét retry semantics trực tiếp vào bảng fact bằng các cột cố định thì schema sẽ nhanh chóng trở nên phình to và thiếu tự nhiên. - Ngược lại, nếu chọn grain
1 row = 1 logical execution attemptthì retry/repair semantics sẽ được biểu diễn chính xác hơn. Tuy nhiên lúc nàyrun_idkhông còn unique tuyệt đối nữa, việc query bắt đầu cần deduplicate hoặc xác định execution nào mới là final execution state của workflow.
Vì vậy, giải pháp hợp lý hơn có lẽ là tách thành hai loại facts khác nhau:
- Một accumulating snapshot fact ở grain “1 row per logical job run” để phục vụ các góc nhìn tổng quan ở cấp workflow lifecycle.
- Một transactional/event fact ở grain “1 row per execution attempt” nhằm lưu toàn bộ retry attempts, repair attempts hoặc execution history chi tiết phía dưới logical run đó.
Cách tiếp cận này giúp vừa giữ được analytical simplicity ở cấp high-level lifecycle, vừa không làm mất execution semantics chi tiết của workflow runtime phía dưới.
Tuy nhiên, sau khi nhìn sâu hơn vào Job Task Execution, tớ bắt đầu nhận ra bảng fact thứ hai này có thể sẽ phần nào overlap với vai trò của fact_job_task_run, đặc biệt trong việc biểu diễn retry attempts, repair executions hoặc execution history ở cấp task-level.
Mặc dù góc nhìn phân tích giữa hai bảng vẫn có thể khác nhau - một bên tập trung vào logical run lifecycle, bên còn lại tập trung vào execution attempts chi tiết - nhưng ở thời điểm hiện tại tớ cảm thấy việc bổ sung thêm một fact riêng cho execution attempts có thể khiến model trở nên phức tạp quá sớm so với nhu cầu thực tế.
Vì vậy trước mắt, tớ quyết định chỉ dừng lại ở việc xây dựng accumulating snapshot fact ở cấp logical job run. Nếu sau này phát sinh thêm các bài toán cần phân tích retry/repair semantics sâu hơn, tớ có thể quay lại mở rộng model bằng các attempt-level facts phía dưới.
Và nartual grain key của fact này là workspace_id + job_id + run_id.
Job Task Execution
So với Job Run Execution thì phần Job Task Execution có vẻ “clean” hơn khá nhiều về mặt grain semantics. Mỗi row trong fact_job_task_run đơn giản chỉ đại diện cho một lần execution của task bên trong một job run cụ thể. Retry, rerun hoặc repair semantics lúc này cũng trở nên tự nhiên hơn, vì phần lớn workflow orchestration thực tế cuối cùng đều được phản ánh xuống cấp task execution phía dưới.
Điều này giúp grain “1 row per task run” giữ được execution visibility khá tốt cho các bài toán như dependency bottlenecks, idle gaps giữa tasks, retry/failure patterns, task-level compute usage, critical path analysis,... mà không cần phải introduce thêm attempt-level fact quá sớm như ở cấp Job Run.
Ngoài ra, task-level execution cũng gần với actual runtime behavior hơn so với logical workflow lifecycle phía trên, nên việc model dưới dạng accumulating snapshot fact cũng trở nên tự nhiên hơn khá nhiều.
Và natural grain key của fact này là: workspace_id + job_id + job_run_id + task_key + task_run_id.
Pipeline Update Execution
So với Job Run Execution thì Pipeline Update Execution có vẻ “ổn định” hơn về mặt grain semantics. Từ góc nhìn của system tables, mỗi update_id thường đã đại diện cho một execution lifecycle tương đối hoàn chỉnh của pipeline update, bao gồm trigger, execution state, compute usage, durations cũng như final result state của update đó.
Điều này khiến grain “1 row per logical pipeline update” trở nên tự nhiên hơn khá nhiều cho các bài toán như pipeline reliability analysis, update duration tracking hoặc compute attribution ở cấp pipeline. Do đó, việc model dưới dạng accumulating snapshot fact cũng trở nên phù hợp và ít ambiguity hơn khá nhiều so với Job Run Execution.
Và natural grain key của fact này là: workspace_id + pipeline_id + update_id.
SQL Query Execution
So với Job Run hoặc Job Task Execution thì phần SQL Query Execution có vẻ “straightforward” hơn khá nhiều về mặt grain semantics. Mỗi row trong fact_query_execution đơn giản chỉ đại diện cho một lần execution của SQL statement. Phần lớn query telemetry như execution duration, queue/wait time, read/write IO, spill metrics hoặc scan volume đều phát sinh trực tiếp ở cấp statement execution, nên grain “1 row per statement” gần như trở thành lựa chọn tự nhiên nhất.
Điều này cũng giúp việc phân tích query latency, warehouse contention, BI workload behavior hoặc query performance patterns trở nên trực quan hơn so với aggregate ở session, notebook hoặc warehouse level phía trên. Ngoài ra, query execution thường mang tính transactional và relatively immutable hơn so với workflow orchestration lifecycle, nên việc model dưới dạng transaction fact cũng khá phù hợp và ít ambiguity hơn nhiều so với Job Run Execution.
Và natural grain key của fact này là: workspace_id + statement_id.
SQL Warehouse Lifecycle
SQL Warehouse Lifecycle lại mang tính infrastructure state tracking nhiều hơn là workload execution telemetry thuần túy. Điều này khiến warehouse lifecycle phù hợp hơn với mô hình event fact kết hợp interval fact thay vì accumulating snapshot hoặc transaction fact truyền thống.
- Trong đó,
fact_warehouse_eventsử dụng grain “1 row per warehouse event” để track các lifecycle events như start, stop hoặc scale actions. - Ngược lại,
fact_warehouse_intervallại được model ở grain “1 row per warehouse state interval giữa hai consecutive events” nhằm đo warehouse đã ở trạng thái đó trong bao lâu.
Cách tách này giúp event fact trả lời “warehouse đã thực hiện hành động gì”, còn interval fact trả lời “warehouse đã ở trạng thái đó trong bao lâu”. Điều này khá quan trọng cho các bài toán như warehouse utilization analysis, autoscaling behavior analysis, active/no-query duration hoặc warehouse thrashing detection.
Instance Lifecycle
Tương tự warehouse lifecycle, Instance Pool Lifecycle cũng mang tính infrastructure state tracking nhiều hơn execution telemetry thuần túy.
Mỗi row trong fact_instance_event đại diện cho một instance lifecycle event cụ thể như launching, ready, placed hoặc terminated. Tuy nhiên, chỉ event sequence thôi vẫn chưa đủ cho các bài toán pool effectiveness analysis, vì phần lớn metrics quan trọng của pools lại mang tính duration-based nhiều hơn event-based.
Ví dụ, pool hit/miss analysis cần reconstruct event sequence để hiểu instance đã đi qua lifecycle như thế nào, nhưng idle duration, ready duration hoặc placed duration lại yêu cầu đo khoảng thời gian instance thực sự ở trong từng trạng thái đó.
Vì vậy, ngoài fact_instance_event, tớ cũng tách thêm fact_pool_instance_interval ở grain “1 row per instance state interval giữa hai events liên tiếp” nhằm phục vụ các bài toán aggregate duration analysis phía trên.
Cluster / Node Utilization Sampling
So với các execution hoặc lifecycle facts phía trên thì phần Cluster / Node Utilization lại mang đặc trưng của periodic telemetry sampling rõ rệt hơn nhiều.
Mỗi row trong fact_node_utilization_minute đại diện cho utilization metrics của một instance tại một thời điểm sample cụ thể, thay vì một business event hoặc execution lifecycle hoàn chỉnh.
Source telemetry ban đầu từ system.compute.node_timeline cũng đã được Databricks expose ở minute-level granularity, nên việc giữ nguyên grain “1 row per instance per minute” trở nên khá tự nhiên. Điều này đặc biệt quan trọng cho các bài toán như workload shape analysis, cluster right-sizing, utilization anomaly detection, CPU/memory saturation analysis, idle resource detection,... trước khi aggregate lên cluster, pool hoặc workload level phía trên.
Và natural grain key của fact này là: workspace_id + cluster_id + instance_id + start_time