前言
最近公司在招聘數(shù)倉開發(fā),筆者負(fù)責(zé)技術(shù)方面的一些問題,緩慢變化維 自然是是不可缺少的環(huán)節(jié)。
但出乎筆者預(yù)料的是,所有的面試者都沒有完整了解 緩慢變化維 的前因后果及處理方式,大都是通過“野路子”碰運(yùn)氣實(shí)現(xiàn)幾種簡單通用的變化方式,甚至有人聲稱緩慢變化維就是拉鏈表。
因此,筆者將基于 kimball 的數(shù)倉理論和自身對(duì)其的理解,對(duì)緩慢變化維進(jìn)行全面且深入的介紹。
什么是緩慢變化維?
要解釋緩慢變化維,必須先解釋什么是維度。
什么是維度?
在數(shù)據(jù)倉庫的DW層中,表根據(jù)用途往往會(huì)分為2個(gè)類型:FACT(事實(shí)表)和 DIM(維度表)。
舉個(gè)例子,如果我們要描述一個(gè)餐飲過程:
? 小明 2020年4月19日下午3點(diǎn)20分 在 海底撈(萬達(dá)廣場) 吃了5道菜,每道菜的單價(jià)是4元,總價(jià)是20元。
那么這個(gè)過程在數(shù)倉中,會(huì)如此劃分:
- fact:餐飲過程,單價(jià)、數(shù)量、總價(jià)
- dim:小明,餐飲時(shí)間,餐飲門店,菜名。
也就是說:吃了多少東西,多少錢——這些屬于fact;在哪里吃、什么時(shí)候吃?這些屬于dim。
下面是簡單的ER圖,方便大家更好的理解。
黃色為事實(shí)表,藍(lán)色的就是維度表。

什么是緩慢變化維?
正如上述所言,我們會(huì)將分析的各種角度,存放在維度表中。但正如每個(gè)人所見,維度里的數(shù)據(jù)是可能發(fā)生變化的——盡管可能跨越極久。
舉2個(gè)例子:
-
客戶的性別變更
可能在第一次登陸中,我們得到的信息是 該客戶性別為男。
但在幾年的客戶再一次使用中,我們又得到該客戶的性別為女。
這就是維度值的一種變化可能
性別一般并不會(huì)改變,所以大概率是其中的一次數(shù)據(jù)有誤。但也有可能是客戶做了變性手術(shù)。
-
雇員的部門更替
假定有一個(gè)雇員叫小楊,他最早是負(fù)責(zé)運(yùn)營的——此時(shí)他的title是"商品運(yùn)營助理";但因?yàn)槟承┰?,他轉(zhuǎn)組成為數(shù)據(jù)組的一員,這時(shí)title就變成了"數(shù)據(jù)分析專員"。
這是緩慢變化維的一種常見可能
上面提到的這些數(shù)據(jù)變化,業(yè)務(wù)系統(tǒng)(CRM、OA等)往往并不會(huì)保留歷史數(shù)據(jù)。但在分析角度,我們是一定要保留這些改變的痕跡。這種隨著時(shí)間可能會(huì)緩慢變化的維度,就是 緩慢變化維、也就是 SCD(Slowly Changing Dimensions)
常見的處理方法
kimball整理的處理方法一共有8種,但往往只有3種被詳細(xì)使用。
類型1 重寫
與業(yè)務(wù)數(shù)據(jù)保持一致,直接update為最新的數(shù)據(jù)。
這種方法主要應(yīng)用于以下兩種情況:
- 數(shù)據(jù)必須正確——例如用戶的身份證號(hào),如需要更新則說明之前錄入錯(cuò)誤。
- 無需考慮歷史變化的維度——例如用戶的頭像url,這種數(shù)據(jù)往往并沒有分析的價(jià)值。因此不做保留。

這種處理方式的優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):
- 簡化ETL——直接update即可。
- 節(jié)省存儲(chǔ)空間——其他存儲(chǔ)方法都占用更多空間。
- 缺點(diǎn):
- 無法保留歷史痕跡——萬一有天想分析呢?
類型2 增加新行
更新歷史數(shù)據(jù)時(shí)間戳,新增新行記錄新值。
這種方法主要用于 僅需要保存歷史數(shù)據(jù) 的業(yè)務(wù)場景
具體的ETL則如下:
自然鍵即指有業(yè)務(wù)意義的唯一ID,例如用戶ID、身份證號(hào)等。代理鍵則可以簡單理解為該表的自增ID值
-
自然鍵第一次出現(xiàn)時(shí)。
新增一行數(shù)據(jù),created為業(yè)務(wù)系統(tǒng)的創(chuàng)建時(shí)間,updated為9999-12-31
數(shù)倉的規(guī)范不允許數(shù)據(jù)存在NULL值的情況,因此用9999-12-31代替

-
類型2的維度發(fā)生變化時(shí)
將自然鍵當(dāng)前記錄的updated由9999-12-31刷為最新時(shí)間
新增一行記錄,記錄最新的數(shù)據(jù),created為最新時(shí)間,updated為 9999-12-31

這樣一來,因?yàn)槭聦?shí)表存儲(chǔ)的是維度表的代理鍵而非自然鍵,因此在歷史數(shù)據(jù)的查詢中會(huì)以歷史的維度值進(jìn)行計(jì)算。同時(shí)在維度值更新后的相關(guān)數(shù)據(jù)自然使用的是新的代理鍵。完美的解決了大部分緩慢變化維情況。
類型3 增加當(dāng)前值屬性
在大部分的維度模式中,很多的源數(shù)據(jù)變化將產(chǎn)生類型1和類型2變化。有時(shí)兩種技術(shù)都不能滿足需求——當(dāng)需要分析所有 伴隨著新值或舊值的變化前后 記錄的事實(shí)時(shí),需要采用類型3變化。
很多人都難以理解類型3的重要性,因此筆者舉一個(gè)例子——一個(gè)無法用類型1和類型2處理的例子:
假定一家公司的銷售是按照銷售區(qū)域進(jìn)行分組:
image.png
突然有一天,領(lǐng)導(dǎo)靈機(jī)一動(dòng),決定 精細(xì)化銷售,將東部、南部、北部重新劃分為東南、東北部
image.png
但由于發(fā)送的過于倉促,因此銷售人員是立刻使用了新的部門劃分;但同樣希望保留舊的名稱——至少要暫時(shí)保留,用以比較今年和去年的業(yè)績。即:
- 擁有使用 新區(qū)域 分析所有事實(shí)的能力,無論變化前還是變化后
- 擁有使用 舊區(qū)域 分析所有事實(shí)的能力,無論變化前還是變化后
第一個(gè)需求——新區(qū)域分析——允許立即采用新的分組,所有歷史訂單都能分為東南、東北等新類別;
第二個(gè)需求——舊區(qū)域分析——允許公司采用舊分組,所有的訂單可以根據(jù)舊值分組——就好像一切都沒發(fā)生過變化。
這時(shí),就會(huì)發(fā)生一些問題:先前的技術(shù)不適合——無論是類型1還是類型2,都不能同時(shí)滿足這兩個(gè)需求;
- 類型1可以滿足第一種需求,使用新值寫舊值。但顯然它無法實(shí)現(xiàn)第二個(gè)需求;
- 類型2則更糟,它不能滿足任意一個(gè)需求——舊的事實(shí)和舊的維度相連;而新的維度值和以后的事實(shí)相連。毫無疑問,它既不能分析舊數(shù)據(jù)、也不能分析新數(shù)據(jù)。
此時(shí)引入 類型3 處理方法:新增字段同時(shí)儲(chǔ)存新舊值。

如果發(fā)生第二次變化,當(dāng)前的current會(huì)被更新到previous中,新的變化值則會(huì)寫入current。
類型3 不保存事實(shí)的歷史內(nèi)容
需要注意的是,類型3 的改變往往并不是一個(gè)僅此一次的過程——它能發(fā)生1次就有可能發(fā)生2次甚至更多次。類型3 變化只保護(hù)變化屬性的一個(gè)舊版本,一旦發(fā)生第二次變化,第一次變化前的值就要被廢棄了。如果想要用變化3 來實(shí)現(xiàn)更多的版本,那只能增加更多的列來實(shí)現(xiàn)(例如dpt_2018,dpt_2019)——這無疑是非常愚蠢的。因此,除非特定需要,應(yīng)盡量避免使用類型3的變化。
其他類型
剩下的5種類型基本都不被采用,但值得一提。
類型0 不做調(diào)整
這里的數(shù)據(jù)定義與類型1類似,但不同點(diǎn)在于 類型0絕不允許ETL對(duì)該維度進(jìn)行更新——你真要改的話就手動(dòng)改表吧。
例如數(shù)倉中的代理鍵
類型4 微型維度
當(dāng)變化頻率加快時(shí)候,并且維度表包含幾百萬行的維度表。如果對(duì)變化的跟蹤采用可靠的SCD2技術(shù)對(duì)瀏覽和查詢性能具有負(fù)面影響——太多行且無必要。采用新的獨(dú)立的維度表消除頻繁分析或者頻繁變化的屬性,這一維度技術(shù)叫做微型維度。
例如employee的 年齡、薪資、稅收金額
年齡每年一變,薪資、稅收金額也經(jīng)常改變
這里要注意:
-
這些“易變化”的值并非存儲(chǔ)其準(zhǔn)確的值,而是其范圍值;
例如年齡,我們不會(huì)存“23",而是會(huì)存"20-30"。收入我們不會(huì)存”999“,而是”0-1000“。如果存準(zhǔn)確值的話,數(shù)據(jù)量會(huì)過于高。從分析角度,我們往往也只需要一個(gè)模糊的范圍即可。
如果需要記錄準(zhǔn)確值,可以考慮使用 無事實(shí)的事實(shí)表 單獨(dú)記錄。
-
微型維度沒有自然鍵,只有值的笛卡爾積組合。
你不會(huì)在表中看到 user_id 這種自然鍵,因此類型4中微型維度只能在事實(shí)表中出現(xiàn)。
如果想把維度表和相關(guān)的微型維度連起來,那就是類型5了。
常見的微型維度表結(jié)構(gòu):

可以看到,該微型維度是由 年齡和薪資的笛卡爾積組合構(gòu)成。
你可以在建立時(shí)就將所有可能組合都預(yù)計(jì)算存入——缺點(diǎn)是表一開始就較大,但優(yōu)點(diǎn)是省去了ETL的功夫
也可以出現(xiàn)一個(gè)存一個(gè),維度表的稀疏性表明了實(shí)際數(shù)據(jù)量并不會(huì)那么多。
常見的維度表、微型維度、事實(shí)表組合:查看餐飲時(shí)雇員的職位和年齡。

可以看到,微型維度表與維度表通過事實(shí)表相連,并不直接連接。
類型5 類型1+微型維度
類型5,即是將類型4與類型1組合起來的方法合并。
該技術(shù)的特點(diǎn)是增加當(dāng)前微型維度主鍵作為主維度的一個(gè)屬性。該屬性在主維度中以類型1進(jìn)行變化更新——從而避免主維度表行的爆炸增長

這樣一來:
- 可以從主維度表獲取到其對(duì)應(yīng)的微型維度數(shù)據(jù)——雖然只有最新的。
- 可以從相關(guān)事實(shí)表中獲取微型維度歷史變更的信息。
類型6 類型1+類型2+類型3
類型6,即是將類型1、2、3的聯(lián)合使用。
主要解決的業(yè)務(wù)場景是:
- 該維度列變化頻次較高,但即使這樣也希望歷史業(yè)務(wù)能以最新的值來分析。
- 無法確定每次該維度的變更時(shí)間。
- 希望保留歷史數(shù)據(jù)方便追溯
下圖為例:

ETL步驟:
- 記錄需要如此處理的列,分別創(chuàng)建previous和current兩列。
- 新數(shù)據(jù)插入時(shí),current = previous.
- 該列有新值時(shí),
- 歷史previous使用類型2方法處理——舊數(shù)據(jù)只處理updated,新行存儲(chǔ)新值。
- 當(dāng)前current使用類型1方法處理——所有該自然鍵的值都刷成最新值。
類型7 雙類型1+類型2
在上面的類型6有一個(gè)缺點(diǎn)——需要額外增加一個(gè)current列。
如果有大量的維度都需要如此處理的話就會(huì)有問題:假定該維度表有150列,如果我們都用類型6處理,則會(huì)變成300列——這無疑是令人無法接受的。
類型7就是 解決以上困難的。有很多種實(shí)現(xiàn)方法,下面會(huì)介紹2種比較常見的
-
雙重外鍵——應(yīng)用于類型1&類型2的維度表
事實(shí)表對(duì)于該維度表存儲(chǔ)2個(gè)外鍵,如下圖
image.png
這么一來,如果想要了解雇員在用餐發(fā)生瞬間歷史的狀態(tài),關(guān)聯(lián)至左邊的維度表即可得知;如果想要以雇員最新的狀態(tài)進(jìn)行分析。則直接取右邊的表即可。
最新視圖獲取:右邊表可以通過視圖展示(比如只取updated == '9999-12-31'的數(shù)據(jù)),也可以生成一個(gè)實(shí)際表來存儲(chǔ)。
視圖主鍵ID:對(duì)應(yīng)的主鍵current_employee_id則有多種取法,筆者建議使用超自然鍵,如系統(tǒng)無超自然鍵的話也可以用普通的自然鍵來替代。
超自然鍵:更為持續(xù)的自然鍵;一般自然鍵是由OLTP系統(tǒng)生成,但他們有可能發(fā)生改變——例如員工離職又入職,他的userid自然會(huì)變化。因此,完整的OLAP系統(tǒng)會(huì)自己生成一個(gè)和實(shí)際事務(wù)對(duì)應(yīng)的自然鍵——即超自然鍵。
-
單外鍵
也可以更節(jié)省一些——事實(shí)表連外鍵都不需要增加。
image.png
這種處理方法的主要難點(diǎn)在于右表——不再只需一個(gè)自然鍵。
下圖是左表(實(shí)際維度表)的變化——標(biāo)準(zhǔn)的類型1&類型2

下圖是右表(最新視圖)的變化

可以看到,在右表的所有相關(guān)字段中,維度列無論是類型1、2都更新成了最新的值。
雙重外鍵的處理方法需要事實(shí)表多一個(gè)值,單重外鍵則需要ETL制造出一個(gè)存儲(chǔ)最新值的維度表(邏輯較復(fù)雜,且查詢會(huì)較慢)。具體采用哪種方法需要視具體業(yè)務(wù)場景
總結(jié)
對(duì)以上類型做一個(gè)總結(jié)
| SCD類型 | 維度表行動(dòng) | 對(duì)事實(shí)分析的影響 |
|---|---|---|
| 類型0 | 屬性值無變化 | 事實(shí)與原始值相關(guān)聯(lián) |
| 類型1 | 重寫屬性值 | 事實(shí)與當(dāng)前值相關(guān)聯(lián) |
| 類型2 | 為新屬性值增加新行 | 事實(shí)與發(fā)生時(shí)的有效值關(guān)聯(lián) |
| 類型3 | 增加新列來存儲(chǔ)當(dāng)前和原先值 | 事實(shí)與當(dāng)前和先前值關(guān)聯(lián) |
| 類型4 | 增加對(duì)快速變化維的微型維度 | 事實(shí)與有效的變化范圍關(guān)聯(lián) |
| 類型5 | 增加類型4和主維度表的類型1外鍵 | 事實(shí)與有效的變化范圍關(guān)聯(lián) |
| 類型6 | 用類型3方法新增列,歷史值用類型2處理,當(dāng)前值用類型1處理 | 事實(shí)與有效的變化范圍、當(dāng)前值關(guān)聯(lián) |
| 類型7 | 增加儲(chǔ)存最新值的維度表或視圖 | 事實(shí)與有效的變化范圍、當(dāng)前值關(guān)聯(lián) |
其他雜談
緩慢變化維的處理,可以說是數(shù)倉的最基本能力要求之一。類型1、2、3是基本;類型4、5、6、7則是拓展。



