小明搬家了,去年的業績算台北還是台中?圖解數據中台的緩慢變動維度(SCD)
小明 8 月在台北下單,12 月搬到台中。隔年老闆要 8 月的各城市銷售——這筆錢算台北還是台中?兩個答案都對,差別在維度表有沒有留住版本。用三種留法看數據中台怎麼保存歷史,以及為什麼歷史是唯一一項不能事後補課的功課。
上一篇的最後,我留了一個問題:
咖啡豆改了分類、客戶搬家換了城市,報表到底該看「當時」還是「現在」?線上資料庫系統會怎麼設計?如果沒有這樣的設計,數據中台有沒有辦法解?
這一篇就來回答這三題。而第三題的答案,可能跟你想的不一樣。
答案要從業務問題找,不是從技術選項找
還記得那位在 2026 年 8 月買了兩包咖啡豆的小明嗎?他住台北。
2026 年 12 月,小明搬到台中,客服在系統裡把他的地址改掉了。
2027 年 1 月,老闆說:「把 2026 年 8 月的各城市銷售再調出來給我。」
這筆 1,000 元,該算台北還是台中?
先別急著找答案。兩個答案其實都對,端看這張報表要回答什麼問題:
- 看「當時」(台北):這筆錢是台北的通路賺到的,要評估台北店的績效、要跟去年的報表對得起來,就得看當時。
- 看「現在」(台中):想知道「我現在的台中客戶群,累計貢獻了多少」,就得看現在。
所以第一件事不是選技術,是先問清楚這張報表要回答哪一種問題。 選錯了,SQL 寫得再漂亮,答案都是錯的。
而最可怕的情況還不是答錯——是同一張報表,今年重跑跟去年不一樣,而且系統不會給你任何錯誤訊息。去年結算完的數字,今年因為有人改了客戶主檔(就是存客戶基本資料的那張表,一個客戶一列)就悄悄變了。等到有人發現,通常已經是在會議室裡被問「這個數字為什麼跟上次不一樣」的時候。

線上系統怎麼做?它只留了一半,而且每張表留的還不一樣
線上交易系統(OLTP)的天性是反映最新狀態。customers 表裡的 city 欄位只有一格,客服一改,全世界看到的就都是台中——包括去年的報表。
但它會為一種情況破例。
上一篇談正規化時提過「訂單快照」:發票上的姓名、地址,訂單上的收件人、成交單價,這些欄位在交易成立的當下就會被抄一份,存進交易表裡。因為法規和對帳都不允許你把十年前發票上的名字,自動改成今天的名字。
這確實是一種歷史保存,是大部分線上系統原生就會做的。
但關鍵在於:它只抄了自己需要的那幾欄。
- 發票要印的姓名、地址 → 會抄
- 成交當下的單價、折扣 → 會抄
- 客戶當時是不是金卡會員 → 不會抄
- 商品當時屬於哪個分類 → 不會抄
為什麼?因為交易系統不需要知道會員等級或商品分類,就能完成一筆交易。它只對「把訂單正確地收下來、把錢正確地收到」負責。
所以答案是:線上系統留了歷史,但只留它自己用得到的那部分。分析要用的歷史,它沒有義務幫你留。
不過現實比這句話更複雜一點。
不是所有線上系統都只存「現在」。在受法規監理的產業裡(醫療、金融、保險、人資),主檔本身就常常帶著「生效日」與「失效日」——因為稽核要求你必須能回答「那一天,這筆資料被記錄成什麼」。這種設計有正式名稱,叫做時態表(temporal table,就是「帶時間版本的表」),SQL 標準裡本來就有。
更麻煩的是:同一個資料庫裡,答案還可能不一樣。
一套跑了幾十年的核心系統——不同年代建的表用了不同策略:有的什麼都沒留、有的做成帶生效日期的版本表。原因不難理解,它們是在不同的法規年代、由不同的人,一層一層蓋上去的。
所以真正的第一步不是選技術,是逐表盤點:這一張表到底留了什麼?這件事沒有捷徑。
這就是為什麼分析端需要自己的一套設計。
數據中台的三種留法
在數據中台裡,「留住歷史」不是一招,而是三種不同的做法,各自對應不同的問題。(事實表、維度表這兩個名詞在上一篇有說明。)
做法一:在事實表裡凍結當下值
最直接的做法——把交易當下的值,複製一份存進事實表自己的欄位裡。
命名上通常會標明「這是當時的值」,例如 name_at_order、category_at_order、unit_price_at_order。
| 欄位 | 意思 |
|---|---|
customer_id | 這筆訂單是誰買的 |
city_at_order | 下單當時他住哪 |
category_at_order | 下單當時這個商品屬於什麼分類 |
amount | 金額 |
適合:屬於這筆交易本身、事後不該再變的屬性。
優點:查詢最簡單,連 JOIN 都不用,欄位就在事實表裡。
限制也很明確:你只能凍結當初想到要凍結的那幾欄。三年後老闆問「當時他是不是金卡會員」,而你當初沒抄這一欄——那就補不回來了,因為過去的等級已經被覆蓋掉了。
做法二:讓維度表留住版本(SCD Type 2)
這是「緩慢變動維度」(Slowly Changing Dimension,簡稱 SCD)真正的主場。
教科書把這種「留版本」的做法編為 Type 2——編號的由來之後另寫一篇,這裡先知道它是最常用的一種就夠了。
所謂「緩慢變動」,指的就是客戶、商品、門市這類主檔資料——它們會變,但變得不頻繁。地址一年改一次、商品分類兩年調一次,跟每天幾千筆的訂單完全不同節奏。
做法是:同一個小明,在維度表裡存成兩列。
| customer_key | customer_id | name | city | 生效日 | 失效日 | is_current(現在用的) |
|---|---|---|---|---|---|---|
| 1001 | C001 | 小明 | 台北 | 2024-01-01 | 2026-12-31 | 否 |
| 1042 | C001 | 小明 | 台中 | 2027-01-01 | 9999-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,讓人根本沒機會接錯。

做法三:快照事實表——沒有事件發生的東西怎麼辦?
前面兩種做法有一個共同前提:有一筆交易可以掛。訂單成立了,我們才有地方存「當時的城市」。
但有些數字,沒有任何事件發生,它自己也在變:
- 今天倉庫裡還剩幾包咖啡豆
- 這個月底有多少人在職
- 現在有多少應收帳款還沒收回來
- 今天有多少病人住在院內
這些是狀態,不是事件。沒有一筆交易可以掛上去,所以只剩一個辦法:定期拍照。
這就是快照事實表(snapshot fact table)——每天(或每月底)把當下的狀態整批切一刀存下來:
| snapshot_date | product_id | 庫存數量 |
|---|---|---|
| 2026-08-15 | P001 | 120 |
| 2026-08-16 | P001 | 118 |
| 2026-08-17 | P001 | 118 |
判斷要不要用快照,有一個很簡單的準則:
問題問的是「這段期間累積了多少」,還是「那個時間點剩下多少」?
- 累積了多少(賣了幾包、收了多少錢)→ 一般事實表加總就好
- 那個時間點剩多少(還剩幾包、有幾人在職)→ 只能靠快照
因為「剩下多少」是無法用加總還原的。你把每天的進貨與出貨都加起來,得到的是流量,不是餘額——除非你有一個起點,而那個起點本身就是一張快照。
三種做法怎麼選
先給一條判準,它能解掉最常見的猶豫:這個值是「這件事的屬性」,還是「某個人、某個東西在某個時刻的屬性」?
- 成交單價、折扣、當時的稅率——是這筆交易的屬性。同一件商品,同一天賣給兩個人可以是不同價錢。這個值天生就屬於這筆交易,放進事實表不是重複,是它本來就該在那裡。
- 城市、會員等級、商品分類——是某個人、某個東西在某個時刻的屬性。它屬於小明,只是會隨時間變。這種交給維度表留版本,用編號釘住。
| 做法 | 解決什麼問題 | 查詢成本 | 沒做的話能補嗎 |
|---|---|---|---|
| 事實表凍結欄位 | 屬於這筆交易本身、不該再變的屬性 | 最低(不用 JOIN) | 不能——當初沒抄的欄位,過去的值已經沒了 |
| SCD Type 2 維度 | 主檔會變,而且「當時」與「現在」都要能查 | 中(多一次 JOIN) | 不能——只能從開始留版本的那天往後算 |
| 快照事實表 | 沒有事件、只有狀態的數字(庫存、在職、餘額) | 中(資料量大,但查詢單純) | 不能——沒拍到的那天,就是永遠沒拍到 |
你可能已經注意到最後一欄的答案完全一樣。沒做的話能補嗎?不能。

最誠實的一節:來源系統沒留歷史,中台救得回來嗎?
這是上一篇留下的第三個問題,也是最多人誤會的地方。
我先給結論:
救不回來。中台不能無中生有。
如果你的線上系統只存「現在」,那麼在你開始接資料之前,所有的變動都已經消失了。小明去年住哪、咖啡豆前年屬於哪個分類——那些值曾經存在於某一格欄位裡,但已經被新的值覆蓋掉,沒有留下任何痕跡。中台再厲害,也只能讀到「現在的那一格」。
中台能做的,是從你開始接的那一天起,每天比對、每天留版本。
也就是說:歷史是從今天開始長出來的,不是從過去撈回來的。
第一,建中台的這一項效益,往往要等一年後才看得到。你今天開始留版本,明年這個時候才會有一整年的歷史可以回溯。這也是為什麼「歷史管理」這個效益很難在專案簡報上打動人——它在提案當下完全看不出價值。
當然,也有例外,這裡誠實說明:如果來源系統剛好有異動記錄表(audit log)、有保留每日的完整備份、或應用程式有寫變更 log,那確實可以回頭重建一部分歷史。但這通常代價不低——要一天一天地還原比對,而且往往不完整(log 可能只記了部分欄位、備份可能只保留 30 天)。它是搶救方案,不是常態設計。
還有第四種例外,而且是最乾淨的一種:如果來源系統本身就留了版本(也就是前面說的時態表),那歷史一直都在,不需要搶救。但要小心,通常不會是所有的表都這樣——新舊夾雜的系統裡,不同維度的歷史深度並不一樣,而這件事從查詢結果上完全看不出來。這個坑值得單獨談,下一篇再說。
所以如果你正在評估要不要建數據中台,這一項的效益不要寫「報表會更快」——快,加機器就能解決。真正該寫的是:明年這個時候,你能不能回答「我們去年做的那件事,到底有沒有效」。
以電商銷售舉例,所有的成效評估都需要 before 和 after:改了商品分類、調了會員制度、換了做法,想知道有沒有用,你需要「改之前長什麼樣」——而那只存在於歷史裡。沒有留,這一類問題不是算得慢,是永遠沒有答案。在受監理的產業,這件事還有更硬的版本:稽核與評鑑會要你「說明這個數字當時是怎麼算的」,沒有歷史,連舉證都做不到。
而這是唯一一項加錢也買不回來的:報表慢可以加機器、規則亂可以加人整理,去年的資料沒有留,多少預算都變不出來。
回到最開始的問題
小明搬家了,去年的業績算台北還是台中?
答案是:兩個都要能算。
「看當時還是看現在」從來就不該是二選一的取捨——那是被系統的限制逼出來的假選擇。當維度表留住了版本,這個問題就從「你只能選一個」變成「你想看哪一個」。
這也是這個系列四篇文章的同一條主線:從正規化到星形架構、從 OLTP 到 OLAP,再到今天的緩慢變動維度——數據中台的每一個設計,都是在把「資料管理者的方便」轉譯成「資料取用者的直覺」。
說到底,SCD 做的事就是用空間換追溯力:多存幾列版本,換來的是這個——有一天兩張報表對不起來,你查得出到底是誰、在什麼時候、改了什麼。查得出原因,數字才敢拿來做決定。
而歷史,是這條路上唯一一項你不能事後補課的功課。
最後也想問問你:你們公司的客戶主檔改了地址、商品換了分類之後,去年的報表數字會跟著變嗎? 如果你不確定——那可能就是最值得回去查一下的事。歡迎留言或回信告訴我。
延伸閱讀
如果你也走在自己的路上,歡迎訂閱《小穗步電子報》——我會把每一次的覺察與練習,寫成信寄給你。→ 訂閱小穗步電子報