Community程式設計與開發github.com

ZrobDev/data-modeling

用於設計或審查分析模型(星狀綱要、資料超市、事實/維度表)、處理 Join 後的指標重複計算或膨脹、選擇代理鍵或自然鍵以及緩慢變更維度類型、決定原始/暫存/中間/資料超市分層、命名與模型所需測試,並針對 PostgreSQL OLTP 綱要進行審查(正規化、外鍵索引、多租戶鍵)。

data-modeling 是什麼?

這個 Data Modeling 技能是一個結構化的指南,用於設計與審查分析型資料模型,支援 PostgreSQL、BigQuery、Snowflake 等資料庫,並以 dbt 作為轉換工具,DuckDB 用於本地原型測試。它提供了一套從商業問題定義、資料粒度聲明、事實與維度選擇到星狀綱要設計的完整流程。此技能特別強調避免常見的錯誤,如 Join 後的 Fan-out 導致計量重複計算,以及如何透過正確的鍵設計(自然鍵與代理鍵)與緩慢變更維度(SCD Type 1/2)來處理歷史資料。它還包含了詳細的命名慣例、測試清單,以及針對 PostgreSQL OLTP 綱要的審查建議,讓開發人員能建立更正確、可維護的資料模型。

相容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ZrobDev/data-engineering-skills/tree/HEAD/skills/data-modeling

Installed? Explore more 程式設計與開發 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜歡的 AI 中提問

開啟一個已預先載入此 Agent Skill 的新對話。

說明文件

資料建模

目標平台:PostgreSQL、BigQuery、Snowflake。轉換工具:dbt。本地原型測試:DuckDB 2.0。 關於 dbt 的機制(模型配置、測試 YAML、快照、語意層、v2 遷移),請使用此 repo 的 de-vendors 技能引入的官方 dbt Labs 技能。本技能決定「要建模什麼」,而非 dbt 如何拼寫它。

流程

  1. 說明商業問題與消費者(儀表板、ML 特徵、財務結算)。
  2. 在定義任何欄位之前,先將粒度寫成一句話:「每個訂單行每天一行」。如果這句話需要「或」字,則代表需要分成兩個資料表。
  3. 選擇事實類型(如下),然後列出在該粒度下為真(true)的維度。
  4. 命名粒度鍵(自然鍵欄位或它們的雜湊值)。它需要具有 unique + not_null 約束。
  5. 在 DuckDB 上針對樣本資料建立原型,執行 Fan-out 檢查,然後再移植。
  6. 在宣告完成之前,加入「需要測試什麼」中列出的測試。

粒度

  • 先定義粒度,再定義欄位。每個度量(measure)和每個外鍵(foreign key)在該粒度下必須是單值的;不屬於該粒度的欄位應放在另一個資料表中,因為它會默默地使資料行數倍增。
  • 維持原始資料來源支援的最低粒度。彙總(aggregates)可以從明細(detail)推導出來,但明細無法從彙總推導出來。將彙總表建立為獨立的資料超市。
  • 切勿在一個資料表中混合不同的粒度(例如在訂單行資料列上重複訂單標頭總和)。對訂單行資料列加總標頭金額會導致重複計算;應分別為標頭和明細建立事實表。

事實 vs 維度

  • 事實:一個事件或測量值;應該是數值型、盡可能可加(additive),並以維度鍵加上退化維度(degenerate dimension,如訂單號碼)作為鍵值。
  • 維度:用於篩選和分組的描述性上下文。寬且反正規化的維度是可以接受的。
  • 對每個度量進行分類:可加(additive,可跨所有維度加總)、半可加(semi-additive,如餘額:不能跨時間加總)、不可加(non-additive,如比率:儲存分子和分母,在彙總後再進行除法)。
  • 事實類型:
    • 交易事實(Transaction):每個事件一行。預設選擇。
    • 定期快照(Periodic snapshot):每個實體每個期間一行(如每日餘額)。當狀態很重要且事件稀疏或延遲送達時使用。
    • 累積快照(Accumulating snapshot):每個流程實例一行,每個里程碑有一個日期欄位,並在原地更新。用於具有已知生命週期的流程(例如已下單、已出貨、已送達)。預期會有更新;確保載入作業是等冪的(idempotent)。
    • 無事實事實(Factless fact):沒有度量的事件或覆蓋範圍(如出席率、資格)。計算資料列數。

綱要形狀

  • 預設使用星狀綱要(Star Schema):一個事實表,加上平坦的維度表。僅當子維度(sub-dimension)是共享的且體積龐大,或由另一個團隊維護時,才考慮使用雪花綱要(Snowflake,即正規化的維度表);額外的 Join 會損害可讀性和 BI 工具的相容性。
  • 一致維度(Conformed dimensions):一個 dim_customer、dim_date 被所有事實表共享,具有相同的鍵和屬性意義。只建立一次;如果有第二個規則不同的客戶表,這是一個缺陷,因為跨事實的報表會不一致。
  • 角色扮演維度(Role-playing dimensions,例如下單日期、出貨日期)透過不同的外鍵欄位重複使用同一個 dim_date。
  • 使用垃圾/小型維度(Junk/mini dimensions)來處理低基數的標誌欄位;不要在事實表上保留 8 個布林欄位。
  • 未知成員(Unknown member):在每個維度表中保留一列(鍵值為 -1 或 'unknown' 的雜湊值),以便延遲送達的事實資料能保留其資料列。與維度表進行 Inner Join 會默默地丟棄事實資料;應使用 Left Join 並透過 coalesce 來使用未知鍵。

鍵

  • 自然鍵(Natural key):來源系統的商業識別碼。始終將其保留為一個屬性。
  • 代理鍵(Surrogate key):當自然鍵是複合鍵、可能變更、來自多個來源,或者維度是 SCD2(一個實體有多個版本)時,加入代理鍵。在 dbt 風格的資料倉儲中,相對於序號(sequences),偏好使用自然鍵(加上 SCD2 的 valid_from)的確定性雜湊(deterministic hash),這樣重新執行和平行建置才能保持一致。
  • 不要將代理鍵作為識別碼暴露給商業使用者;它們會在重新建置時變更。

緩慢變更維度

  • 類型 1(覆蓋):當歷史記錄沒有分析價值,或者變更只是更正時使用。
  • 類型 2(每個版本新的一行,包含 valid_from / valid_to / is_current):當必須根據事件時間的屬性值來報告事實時使用(例如購買時的客戶等級)。
  • 當來源不保留歷史記錄,且您只需要從現在開始捕捉變更時,快照(Snapshots)就足夠了;它們是捕捉機制,而 SCD2 維度仍然是建立在其上的模型。如果來源已經有歷史記錄或事件日誌,則應從中推導有效範圍,而不是進行快照。
  • 將事實連接到 SCD2 的條件:鍵值加上 event_ts >= valid_from AND event_ts < valid_to(半開放區間,避免重疊)。對每個自然鍵測試是否有重疊或間隙的區間。
  • 類型 3 或按屬性類型 2:僅當一個單一的「先前值」欄位就能回答問題時才使用;否則跳過。

Fan-out 與重複計算

原因:連線粒度不匹配的資料表,導致「一」的那一方實際上是「多」。症狀:總和大於原始來源,count(*) 在 Left Join 後增加,平均值改變。

  • 偵測:在每次 Join 前後比較 count(*) 和 count(distinct grain_key);在連接到維度表後,兩者必須相等。在維度端測試 Join 鍵的唯一性(select key from dim group by 1 having count(*) > 1)。
  • 對帳:資料超市中某個度量的總和,在相同篩選條件下應等於原始來源的總和。
  • 修復方法:在 Join 之前,先將多的一方彙總到事實的粒度,或者透過具有分配權重的橋接表(bridge table)進行 Join;切勿使用 distinct 來隱藏結果。
  • 多對多關係(客戶到帳戶,產品到類別):使用帶有權重因子的橋接表,或者選擇一個主要的並加以說明。
  • 這些檢查的 SQL:請參閱 references/fan-out-checks.md。

分層

  • raw/staging:每個來源資料表對應一個 staging 模型;進行重新命名、型別轉換、去重,不做 Join,不包含商業邏輯。保持 raw 層不變。
  • intermediate:進行 Join、重塑結構、以及被兩個以上資料超市共用的邏輯。不暴露給 BI。
  • marts:事實表和維度表,每個資料超市對應一個商業流程,有文件、測試、合約(contract)。只有 marts 是公開的。
  • 依賴關係是單向的;marts 不能讀取 raw 層,staging 層也不能讀取其他 staging 層。
  • 將 staging 模型實體化為檢視(view),marts 實體化為資料表(對於大型且粒度鍵穩定的表,使用增量(incremental)模式)。

命名

  • stg_<source>__<entity>、int_<entity>_<verb>、fct_<process>、dim_<entity>;使用蛇形命名法(snake_case);單數或複數保持一致(選擇一種)。
  • 鍵:<entity>_id(自然鍵)和 <entity>_key(代理鍵);時間戳記:<event>_at(使用 UTC);日期:<event>_date;布林值:is_ / has_;金額需附帶單位或貨幣(amount_usd)。

需要測試什麼

  • 粒度鍵:每個模型上都必須有 unique + not_null 約束;對於複合粒度,測試其組合。
  • 事實表上的每個外鍵(FK):not_null(或未知成員)以及連接到維度表的關係測試。
  • 狀態/類型欄位的接受值測試;新值會失敗並發出警示,而不是被錯誤分類。
  • 每個資料超市與其原始來源之間的資料列數和總和對帳,並說明允許的誤差範圍(財務相關為零誤差)。
  • SCD2:沒有重疊的區間,每個自然鍵恰好有一行當前的記錄。
  • 來源表的時效性(Freshness)測試。在樣本資料上斷言(break)每個測試一次,以證明它能夠失敗。

OLTP 綱要審查(PostgreSQL)

  • 交易表正規化到 3NF;僅在經過衡量的讀取路徑下進行反正規化。每個資料表都必須有主鍵。
  • 為外鍵欄位建立索引:PostgreSQL 不會自動為外鍵建立索引,未建立索引的外鍵會使父表刪除操作和 Join 操作掃描子表。
  • 使用 timestamptz、numeric 處理貨幣、text 取代任意長度的 varchar;透過 NOT NULL、CHECK、UNIQUE 和外鍵約束來強制執行規則,而不是僅依賴應用程式程式碼。
  • 對於大量插入的資料表,偏好使用 bigint identity 或 UUIDv7 鍵,而非隨機的 UUIDv4(索引局部性考量)。
  • 多租戶:將 tenant_id 放在每個租戶擁有的資料表上,使其成為複合鍵和索引的前導欄位,並將其包含在外鍵中(複合外鍵),以便無法進行跨租戶引用,並配合列級安全性(RLS)。預設使用含 tenant_id 的共享綱要;僅在需要嚴格隔離或每個租戶獨立還原的需求下,才使用每個租戶獨立綱要或資料庫的方案。
  • 軟刪除:使用部分唯一索引(where deleted_at is null),以便唯一性約束仍然有效。

陷阱與觀察到的失敗模式:references/gotchas.md

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

相關技能