小明搬家了,去年的業績算台北還是台中?圖解數據中台的緩慢變動維度(SCD)

小明 8 月在台北下單,12 月搬到台中。隔年老闆要 8 月的各城市銷售——這筆錢算台北還是台中?兩個答案都對,差別在維度表有沒有留住版本。用三種留法看數據中台怎麼保存歷史,以及為什麼歷史是唯一一項不能事後補課的功課。

分享
小明搬家了,去年的業績算台北還是台中?圖解數據中台的緩慢變動維度(SCD)

上一篇的最後,我留了一個問題:

咖啡豆改了分類、客戶搬家換了城市,報表到底該看「當時」還是「現在」?線上資料庫系統會怎麼設計?如果沒有這樣的設計,數據中台有沒有辦法解?

這一篇就來回答這三題。而第三題的答案,可能跟你想的不一樣。

答案要從業務問題找,不是從技術選項找

還記得那位在 2026 年 8 月買了兩包咖啡豆的小明嗎?他住台北。

2026 年 12 月,小明搬到台中,客服在系統裡把他的地址改掉了。

2027 年 1 月,老闆說:「把 2026 年 8 月的各城市銷售再調出來給我。」

這筆 1,000 元,該算台北還是台中?

先別急著找答案。兩個答案其實都對,端看這張報表要回答什麼問題:

  • 看「當時」(台北):這筆錢是台北的通路賺到的,要評估台北店的績效、要跟去年的報表對得起來,就得看當時。
  • 看「現在」(台中):想知道「我現在的台中客戶群,累計貢獻了多少」,就得看現在。

所以第一件事不是選技術,是先問清楚這張報表要回答哪一種問題。 選錯了,SQL 寫得再漂亮,答案都是錯的。

而最可怕的情況還不是答錯——是同一張報表,今年重跑跟去年不一樣,而且系統不會給你任何錯誤訊息。去年結算完的數字,今年因為有人改了客戶主檔(就是存客戶基本資料的那張表,一個客戶一列)就悄悄變了。等到有人發現,通常已經是在會議室裡被問「這個數字為什麼跟上次不一樣」的時候。

同一筆訂單兩個答案的時間軸:2026 年 8 月小明在台北下單 1,000 元;12 月客服把客戶資料表的 city 從台北改成台中,欄位只有一格、舊值被覆蓋;隔年 1 月老闆要 8 月的各城市銷售。看「當時」算台北、看「現在」算台中,兩個答案都對——但如果 city 只有一格,永遠只拿得到後者
小明的訂單掛在 2026 年 8 月,但客戶主檔在 12 月被改成台中。同一句 SQL,看的是哪個版本,決定了這 1,000 元落在台北還是台中。

線上系統怎麼做?它只留了一半,而且每張表留的還不一樣

線上交易系統(OLTP)的天性是反映最新狀態customers 表裡的 city 欄位只有一格,客服一改,全世界看到的就都是台中——包括去年的報表。

但它會為一種情況破例。

上一篇談正規化時提過「訂單快照」:發票上的姓名、地址,訂單上的收件人、成交單價,這些欄位在交易成立的當下就會被抄一份,存進交易表裡。因為法規和對帳都不允許你把十年前發票上的名字,自動改成今天的名字。

這確實是一種歷史保存,是大部分線上系統原生就會做的。

但關鍵在於:它只抄了自己需要的那幾欄。

  • 發票要印的姓名、地址 → 會抄
  • 成交當下的單價、折扣 → 會抄
  • 客戶當時是不是金卡會員 → 不會抄
  • 商品當時屬於哪個分類 → 不會抄

為什麼?因為交易系統不需要知道會員等級或商品分類,就能完成一筆交易。它只對「把訂單正確地收下來、把錢正確地收到」負責。

所以答案是:線上系統留了歷史,但只留它自己用得到的那部分。分析要用的歷史,它沒有義務幫你留。

不過現實比這句話更複雜一點。

不是所有線上系統都只存「現在」。在受法規監理的產業裡(醫療、金融、保險、人資),主檔本身就常常帶著「生效日」與「失效日」——因為稽核要求你必須能回答「那一天,這筆資料被記錄成什麼」。這種設計有正式名稱,叫做時態表(temporal table,就是「帶時間版本的表」),SQL 標準裡本來就有。

更麻煩的是:同一個資料庫裡,答案還可能不一樣。

一套跑了幾十年的核心系統——不同年代建的表用了不同策略:有的什麼都沒留、有的做成帶生效日期的版本表。原因不難理解,它們是在不同的法規年代、由不同的人,一層一層蓋上去的。

所以真正的第一步不是選技術,是逐表盤點:這一張表到底留了什麼?這件事沒有捷徑。

這就是為什麼分析端需要自己的一套設計。

數據中台的三種留法

在數據中台裡,「留住歷史」不是一招,而是三種不同的做法,各自對應不同的問題。(事實表、維度表這兩個名詞在上一篇有說明。)

做法一:在事實表裡凍結當下值

最直接的做法——把交易當下的值,複製一份存進事實表自己的欄位裡。

命名上通常會標明「這是當時的值」,例如 name_at_ordercategory_at_orderunit_price_at_order

欄位意思
customer_id這筆訂單是誰買的
city_at_order下單當時他住哪
category_at_order下單當時這個商品屬於什麼分類
amount金額

適合:屬於這筆交易本身、事後不該再變的屬性。

優點:查詢最簡單,連 JOIN 都不用,欄位就在事實表裡。

限制也很明確:你只能凍結當初想到要凍結的那幾欄。三年後老闆問「當時他是不是金卡會員」,而你當初沒抄這一欄——那就補不回來了,因為過去的等級已經被覆蓋掉了。

做法二:讓維度表留住版本(SCD Type 2)

這是「緩慢變動維度」(Slowly Changing Dimension,簡稱 SCD)真正的主場。

教科書把這種「留版本」的做法編為 Type 2——編號的由來之後另寫一篇,這裡先知道它是最常用的一種就夠了。

所謂「緩慢變動」,指的就是客戶、商品、門市這類主檔資料——它們會變,但變得不頻繁。地址一年改一次、商品分類兩年調一次,跟每天幾千筆的訂單完全不同節奏。

做法是:同一個小明,在維度表裡存成兩列。

customer_keycustomer_idnamecity生效日失效日is_current(現在用的)
1001C001小明台北2024-01-012026-12-31
1042C001小明台中2027-01-019999-12-31

(失效日寫成 9999-12-31 是慣例,意思是「目前還沒失效」。有些工具改用空值表示同一件事,但那樣每一句查詢都得多寫一個判斷,漏寫就會少資料。)

注意這裡有兩種「編號」,它們的分工是整篇文章的技術核心:

  • customer_id(C001):小明這個。不管搬幾次家,永遠是 C001。
  • customer_key(1001 / 1042):小明的某一個版本。每搬一次家,就多一個新的 key。

因此在數據中台中,會將維度表轉換成類似事實表存的資訊,多一個欄位 customer_key,不是 customer_id

小明 8 月下單時,系統掛上的是當時有效的版本 1001(台北)。之後他搬到台中、維度表多了一列 1042,但那筆 8 月的訂單仍然指著 1001——它被永遠釘在「台北的小明」身上了

於是同一張維度表,可以回答兩種問題:

-- 看「當時」:事實表本來就指著下單那一刻的版本,直接 JOIN 就對了
SELECT c.city, SUM(f.amount) AS total_sales
FROM fact_sales f
JOIN dim_customer c ON c.customer_key = f.customer_key
WHERE f.date_key BETWEEN 20260801 AND 20260831
GROUP BY c.city;
-- 結果:台北 1,000
-- 看「現在」:先從當時的版本找到「這個人」,再跳到他目前有效的版本
SELECT cur.city, SUM(f.amount) AS total_sales
FROM fact_sales f
JOIN dim_customer c   ON c.customer_key  = f.customer_key   -- 當時的小明
JOIN dim_customer cur ON cur.customer_id = c.customer_id     -- 同一個人
                     AND cur.is_current  = 1                 -- 的現在版本
WHERE f.date_key BETWEEN 20260801 AND 20260831
GROUP BY cur.city;
-- 結果:台中 1,000

同一份事實表、同一段期間,差別只在 JOIN 到哪一個版本

這就是讓維度表留住版本的真正價值:它不是「幫你決定要看當時還是現在」,而是讓你兩個都能答

咖啡豆改分類也是同一件事——dim_product 留兩個版本,去年的報表照樣算在「食品」,今年的算在「飲品沖泡」,兩邊都不會錯。

順帶提醒一個最容易踩的坑:查歷史要用 customer_key 接,不要用 customer_id。用 customer_id 去接維度表,一筆訂單會對到小明的每一個版本——搬過五次家,1,000 元就被算成六次,而且報表照樣跑得出來,系統不會給你任何錯誤訊息。防守的方法不是叫大家小心,是在交付層包一層已經寫好 JOIN 的 view,讓人根本沒機會接錯。

SCD Type 2 維度表:同一個小明存成兩列,customer_key 1001 對應台北(2024-01-01 至 2026-12-31),1042 對應台中(2027-01-01 起)。事實表的訂單掛在版本 1001 上,所以看「當時」一步就到台北;看「現在」則先從版本跳回 customer_id C001,再跳到目前有效的版本 1042 得到台中
customer_id 是人、customer_key 是版本;訂單掛在版本上,所以歷史被釘住了。要看現在,只要從版本跳回人、再跳到目前有效的版本。

做法三:快照事實表——沒有事件發生的東西怎麼辦?

前面兩種做法有一個共同前提:有一筆交易可以掛。訂單成立了,我們才有地方存「當時的城市」。

但有些數字,沒有任何事件發生,它自己也在變:

  • 今天倉庫裡還剩幾包咖啡豆
  • 這個月底有多少人在職
  • 現在有多少應收帳款還沒收回來
  • 今天有多少病人住在院內

這些是狀態,不是事件。沒有一筆交易可以掛上去,所以只剩一個辦法:定期拍照

這就是快照事實表(snapshot fact table)——每天(或每月底)把當下的狀態整批切一刀存下來:

snapshot_dateproduct_id庫存數量
2026-08-15P001120
2026-08-16P001118
2026-08-17P001118

判斷要不要用快照,有一個很簡單的準則:

問題問的是「這段期間累積了多少」,還是「那個時間點剩下多少」?

  • 累積了多少(賣了幾包、收了多少錢)→ 一般事實表加總就好
  • 那個時間點剩多少(還剩幾包、有幾人在職)→ 只能靠快照

因為「剩下多少」是無法用加總還原的。你把每天的進貨與出貨都加起來,得到的是流量,不是餘額——除非你有一個起點,而那個起點本身就是一張快照。

三種做法怎麼選

先給一條判準,它能解掉最常見的猶豫:這個值是「這件事的屬性」,還是「某個人、某個東西在某個時刻的屬性」?

  • 成交單價、折扣、當時的稅率——是這筆交易的屬性。同一件商品,同一天賣給兩個人可以是不同價錢。這個值天生就屬於這筆交易,放進事實表不是重複,是它本來就該在那裡。
  • 城市、會員等級、商品分類——是某個人、某個東西在某個時刻的屬性。它屬於小明,只是會隨時間變。這種交給維度表留版本,用編號釘住。
做法解決什麼問題查詢成本沒做的話能補嗎
事實表凍結欄位屬於這筆交易本身、不該再變的屬性最低(不用 JOIN)不能——當初沒抄的欄位,過去的值已經沒了
SCD Type 2 維度主檔會變,而且「當時」與「現在」都要能查中(多一次 JOIN)不能——只能從開始留版本的那天往後算
快照事實表沒有事件、只有狀態的數字(庫存、在職、餘額)中(資料量大,但查詢單純)不能——沒拍到的那天,就是永遠沒拍到

你可能已經注意到最後一欄的答案完全一樣。沒做的話能補嗎?不能。

三種留法的決策樹:先問有沒有一筆事件可以掛,沒有就用快照事實表;有的話再問這個值屬於這件事本身(用事實表凍結欄位)還是某個人、某個東西在某個時刻(用 SCD Type 2 維度)。三者共同的限制是都只能從開始留的那一天往後算
凍結欄位解「這筆交易本身的屬性」、SCD Type 2 解「主檔會變但兩邊都要查」、快照事實表解「沒有事件的狀態」;三者共同的限制是都只能從今天開始留。

最誠實的一節:來源系統沒留歷史,中台救得回來嗎?

這是上一篇留下的第三個問題,也是最多人誤會的地方。

我先給結論:

救不回來。中台不能無中生有。

如果你的線上系統只存「現在」,那麼在你開始接資料之前,所有的變動都已經消失了。小明去年住哪、咖啡豆前年屬於哪個分類——那些值曾經存在於某一格欄位裡,但已經被新的值覆蓋掉,沒有留下任何痕跡。中台再厲害,也只能讀到「現在的那一格」。

中台能做的,是從你開始接的那一天起,每天比對、每天留版本。

也就是說:歷史是從今天開始長出來的,不是從過去撈回來的。

第一,建中台的這一項效益,往往要等一年後才看得到。你今天開始留版本,明年這個時候才會有一整年的歷史可以回溯。這也是為什麼「歷史管理」這個效益很難在專案簡報上打動人——它在提案當下完全看不出價值。

當然,也有例外,這裡誠實說明:如果來源系統剛好有異動記錄表(audit log)、有保留每日的完整備份、或應用程式有寫變更 log,那確實可以回頭重建一部分歷史。但這通常代價不低——要一天一天地還原比對,而且往往不完整(log 可能只記了部分欄位、備份可能只保留 30 天)。它是搶救方案,不是常態設計。

還有第四種例外,而且是最乾淨的一種:如果來源系統本身就留了版本(也就是前面說的時態表),那歷史一直都在,不需要搶救。但要小心,通常不會是所有的表都這樣——新舊夾雜的系統裡,不同維度的歷史深度並不一樣,而這件事從查詢結果上完全看不出來。這個坑值得單獨談,下一篇再說。

所以如果你正在評估要不要建數據中台,這一項的效益不要寫「報表會更快」——快,加機器就能解決。真正該寫的是:明年這個時候,你能不能回答「我們去年做的那件事,到底有沒有效」。

以電商銷售舉例,所有的成效評估都需要 before 和 after:改了商品分類、調了會員制度、換了做法,想知道有沒有用,你需要「改之前長什麼樣」——而那只存在於歷史裡。沒有留,這一類問題不是算得慢,是永遠沒有答案。在受監理的產業,這件事還有更硬的版本:稽核與評鑑會要你「說明這個數字當時是怎麼算的」,沒有歷史,連舉證都做不到。

而這是唯一一項加錢也買不回來的:報表慢可以加機器、規則亂可以加人整理,去年的資料沒有留,多少預算都變不出來

回到最開始的問題

小明搬家了,去年的業績算台北還是台中?

答案是:兩個都要能算。

「看當時還是看現在」從來就不該是二選一的取捨——那是被系統的限制逼出來的假選擇。當維度表留住了版本,這個問題就從「你只能選一個」變成「你想看哪一個」。

這也是這個系列四篇文章的同一條主線:從正規化到星形架構、從 OLTP 到 OLAP,再到今天的緩慢變動維度——數據中台的每一個設計,都是在把「資料管理者的方便」轉譯成「資料取用者的直覺」。

說到底,SCD 做的事就是用空間換追溯力:多存幾列版本,換來的是這個——有一天兩張報表對不起來,你查得出到底是誰、在什麼時候、改了什麼。查得出原因,數字才敢拿來做決定。

而歷史,是這條路上唯一一項你不能事後補課的功課。

最後也想問問你:你們公司的客戶主檔改了地址、商品換了分類之後,去年的報表數字會跟著變嗎? 如果你不確定——那可能就是最值得回去查一下的事。歡迎留言或回信告訴我。


延伸閱讀

如果你也走在自己的路上,歡迎訂閱《小穗步電子報》——我會把每一次的覺察與練習,寫成信寄給你。→ 訂閱小穗步電子報

Read more