雙時態歷史

通常需要存取某些屬性的歷史值。但有時此歷史記錄本身需要根據追溯更新進行修改。雙時態歷史記錄將時間視為兩個面向:實際歷史記錄了在完美資訊傳輸下應有的歷史記錄,而記錄歷史記錄則擷取了我們對歷史記錄的認知如何變更。

2021 年 4 月 7 日



當我們思考某些屬性(例如您的地址或薪資)如何隨著時間變更時,我們通常會將其視為線性變更順序。但令人驚訝的是,它通常會變得更加複雜,可能會讓電腦化記錄感到混淆。

我可以透過一個簡單的範例來說明這一切

因此,當我們被問及莎莉在 2 月 25 日的薪資為何時,我們應該如何回答?在某種意義上,我們應該回答 6500 美元,因為我們現在知道那是費率。但我們通常無法忽略在 2 月 25 日我們認為薪資是 6000 美元,畢竟那是我們執行薪資的時候。我們列印了一張支票,寄給她,她兌現了它。這些都是根據她的薪資金額發生的。如果稅務機關詢問我們她在 2 月 25 日的薪資,這一點就變得重要了。

兩個面向

我發現我可以透過將時間視為兩個面向來理解這大部分的糾葛,因此稱為「雙時態」。一個面向是莎莉薪資的實際歷史,我將透過在每個月 25 日取樣來說明,因為那是執行薪資的時間。

日期薪資
1 月 25 日6000
2 月 25 日6500
3 月 25 日6500

當我們詢問莎莉在 2 月 25 日的薪資記錄是什麼時,第二個面向就出現了。在 2 月 25 日,我們尚未收到人力資源部的信件,因此我們認為她的薪資始終為 6000 美元。實際記錄與我們的記錄之間存在差異。我們可以透過在表格中新增日期來顯示此差異

記錄日期實際日期薪資
1 月 25 日1 月 25 日6000
2 月 25 日1 月 25 日6000
3 月 25 日1 月 25 日6000
2 月 25 日2 月 25 日6000
3 月 25 日2 月 25 日6500
3 月 25 日3 月 25 日6500

我使用術語實際記錄來表示兩個面向。您也可能會聽到人們使用術語有效(對於實際)和交易(對於記錄)。[1]

我透過說出類似「在 3 月 25 日,我們認為莎莉在 2 月 25 日的薪資為 6500 美元」之類的話來閱讀此表格的列。使用這種思考方式,我可以查看莎莉實際記錄的先前表格,並更精確地說出這是 3 月 25 日已知的(記錄的)莎莉實際記錄。

在程式設計術語中,如果我想知道莎莉的薪資,而且我沒有任何記錄,那麼我可以透過類似 sally.salary 的方式取得。若要新增(實際)記錄支援,我需要使用 sally.salaryAt('2021-02-25')。在雙時態世界中,我需要另一個參數 sally.salaryAt('2021-02-25', '2021-03-25')

視覺化此問題的另一種方式是繪製一個圖表,其中 x 軸為實際時間,y 軸為記錄時間。我根據薪資等級對區域進行陰影處理。(圖表的形狀為三角形,因為我們不嘗試記錄未來值。[2])

使用此圖表,我可以製作一個表格,說明實際記錄如何隨著 25 日的每次薪資執行而改變。我們看到 2 月 25 日的薪資執行是在莎莉尚未加薪時,但當 3 月 25 日的薪資執行時,加薪已為人所知。

變更追溯變更

現在考慮人力資源部的另一則訊息

  • 4 月 5 日:抱歉,我們之前的電子郵件中有一個錯字。莎莉在 2 月 15 日的加薪為 6400 美元。造成不便,敬請見諒。

這類變更會讓天使哭泣。但是當我們從雙時態記錄的角度思考時,理解起來並不困難。以下是包含此新資訊的圖表。

用於薪資的橫線,代表記錄時間點的實際歷史。4 月 25 日,我們知道莎莉的薪水在 2 月 15 日從 6000 美元增加到 6400 美元。從這個角度來看,我們永遠不會看到莎莉 6500 美元的薪水,因為這從未發生過。

查看圖表,垂直線代表什麼意思?

這代表我們在特定日期對數值的了解。表格指出 2 月 25 日的記錄薪水,因為我們的了解會隨著時間而改變。

使用雙時態

當我們必須處理追溯變更時,雙時態歷史是建構歷史的實用方式。然而,我們不常看到它被使用,部分原因是許多人不知道這項技術,但也是因為我們通常可以在沒有它的情況下解決問題。

避免它的方法之一是不支援追溯變更。如果您的保險公司表示,任何變更在收到您的信件時生效,那麼這是一種強制實際時間與記錄時間相符的方式。

當動作基於追溯變更的過去狀態時,追溯變更會造成問題,例如根據現在已更新的薪資水準發出的薪資支票。如果我們只是記錄歷史,那麼我們不必擔心它會追溯變更,我們基本上會忽略記錄歷史,只記錄實際歷史。即使我們有固定動作,我們也可能會這樣做,如果動作的記錄方式是記錄任何必要的輸入資料。因此,莎莉的薪資可以記錄她在發出支票時的薪水,這對於稽核目的來說就足夠了。在這種情況下,我們只能使用她的薪水的實際歷史。記錄歷史會埋藏在她的薪資單中。

如果在動作發生之前進行任何追溯變更,我們也可能只使用實際歷史。如果我們在 2 月 24 日得知莎莉的薪水變更,我們可以在薪資動作依賴不正確數字之前調整她的記錄。

如果我們能避免使用雙時態歷史,那通常比較好,因為它會使系統複雜化很多。但是,當我們必須處理實際歷史和記錄歷史之間的差異時,通常是因為追溯更新,那麼我們就需要咬緊牙關。其中最困難的部分之一是教育使用者雙時態歷史如何運作。大多數人不會認為歷史記錄會改變,更不用說記錄和實際歷史的兩個面向。

僅附加歷史記錄

在一個簡單的世界中,歷史是只追加的。如果通訊是完美且即時的,那麼所有感興趣的參與者都會立即得知所有新資訊。然後,我們可以將歷史視為隨著世界上發生新事件而新增的內容。

雙時態歷史是一種接受通訊既不完美也不即時的途徑。實際歷史不再是只追加的,我們會回頭進行追溯變更。然而,記錄歷史本身只追加的。我們不會改變我們在 2 月 25 日對莎莉薪資的認知。我們只會追加我們後來獲得的知識。透過在實際歷史上分層只追加的記錄歷史,我們允許修改實際歷史,同時建立其修改的可靠歷史。

追溯變更的後果

雙時態歷史是一種機制,允許我們追蹤值如何變更,而且能夠詢問 sally.salaryAt(actualDate, recordDate) 會非常有幫助。但是,追溯變更不僅僅是調整歷史記錄。正如專家所說:「人們假設時間是因果關係的嚴格進展,但實際上從非線性、非主觀的觀點來看,它更像是一團搖擺不定的時光漩渦。」[3]如果我們在應該支付莎莉 6400 美元時支付了她 6000 美元,那麼我們需要糾正它。至少這表示在後續的薪水中獲得更多,但它也可能導致其他後果。也許更高的薪資表示她應該在一個月前跨過某個重要的門檻,也許有稅務影響。

雙時態歷史本身不足以找出這些依賴效應是什麼,這需要一組額外的機制,這超出了此模式的範圍。一項措施是建立一個平行模型,它擷取了世界在正確薪資下的狀態,並使用它來找出補償變更。[4]雙時態歷史對於這類措施可能是個有用的元素,但只能解開那團大漩渦的一部分。

記錄時間觀點

我上面用於記錄時間的範例使用日期來擷取我們對實際歷史的變更理解。但是,我們擷取記錄歷史的方式可能比這更複雜。

為了讓上述內容更容易理解,我在薪資日期上取樣了歷史紀錄。但更好的歷史紀錄呈現方式是使用日期範圍,以下是 2021 年的表格

紀錄日期實際日期薪資
1 月 1 日 - 3 月 14 日1 月 1 日 - 12 月 31 日6000
3 月 15 日 - 4 月 4 日1 月 1 日 - 2 月 14 日6000
3 月 15 日 - 4 月 4 日2 月 15 日 - 12 月 31 日6500
4 月 5 日 - 12 月 31 日1 月 1 日 - 2 月 14 日6000
4 月 5 日 - 12 月 31 日2 月 15 日 - 12 月 31 日6400

我們可以將莎莉的薪資視為兩個金鑰的組合記錄,實際金鑰(日期範圍)和紀錄金鑰(也是日期範圍)。但我們的紀錄金鑰概念可能比這更複雜。

一個明顯的案例是不同的代理商可能有不同的紀錄歷史。這顯然是莎莉的案例,人事部門傳送訊息到薪資部門需要時間,因此這兩者對實際歷史修改的紀錄時間會有所不同。

部門紀錄日期實際日期薪資
人事1 月 1 日 - 2 月 14 日1 月 1 日 - 12 月 31 日6000
人事2 月 15 日 - 12 月 31 日1 月 1 日 - 2 月 14 日6000
人事2 月 15 日 - 12 月 31 日2 月 15 日 - 12 月 31 日6400
薪資1 月 1 日 - 3 月 14 日1 月 1 日 - 12 月 31 日6000
薪資3 月 15 日 - 4 月 4 日1 月 1 日 - 2 月 14 日6000
薪資3 月 15 日 - 4 月 4 日2 月 15 日 - 12 月 31 日6500
薪資4 月 5 日 - 12 月 31 日1 月 1 日 - 2 月 14 日6000
薪資4 月 5 日 - 12 月 31 日2 月 15 日 - 12 月 31 日6400

任何可以記錄歷史的內容都會有自己的紀錄時間戳記,表示其得知資訊的時間。根據該資料,我們可以說企業會選擇某個代理商作為記錄特定類型資料的定義代理商。但代理商會跨越權限界線 - 無論公司規模多大,這都不會改變其處理的稅務機關的記錄日期。對於因不同代理商在不同時間得知相同事實而造成的問題,需要投入大量精力來解決。

我們可以將部門和紀錄日期範圍的概念合併成一個觀點的概念,進而概括此處發生的情況。因此我們會說類似「根據人事部門在 2 月 25 日的觀點,莎莉的薪資為 6400 美元」。在表格形式中,我們可能會這樣視覺化。

觀點實際日期薪資
人事,1 月 1 日 - 2 月 14 日1 月 1 日 - 12 月 31 日6000
人事,2 月 15 日 - 12 月 31 日1 月 1 日 - 2 月 14 日6000
人事,2 月 15 日 - 12 月 31 日2 月 15 日 - 12 月 31 日6400
薪資,1 月 1 日 - 3 月 14 日1 月 1 日 - 12 月 31 日6000
薪資,3 月 15 日 - 4 月 4 日1 月 1 日 - 2 月 14 日6000
薪資,3 月 15 日 - 4 月 4 日2 月 15 日 - 12 月 31 日6500
薪資,4 月 5 日 - 12 月 31 日1 月 1 日 - 2 月 14 日6000
薪資,4 月 5 日 - 12 月 31 日2 月 15 日 - 12 月 31 日6400

將這些內容濃縮成單一觀點概念有什麼好處?它讓我們可以思考其他觀點是什麼。一個範例是考慮替代觀點。我們可以建立一個觀點,移除個人加薪(例如莎莉在 2 月 15 日的加薪),並在 3 月 1 日給所有員工 10% 的加薪。這將導致莎莉薪資的新的紀錄時間維度。

觀點實際日期薪資
真實世界1 月 1 日 - 2 月 14 日6000
真實世界2 月 15 日 - 12 月 31 日6400
全球加薪1 月 1 日 - 2 月 28 日6000
全球加薪3 月 1 日 - 12 月 31 日6600

這個紀錄時間概念的概括表示,我們可以使用本質上相同的機制來推理追溯變更和替代歷史,將多個觀點分層疊加在實際歷史上。

在歷史上疊加許多觀點維度並非廣泛有用的做法,即使與雙時間歷史相比也是如此。但我發現這是一種思考此類情況的有用方式:推理替代情境,無論是在歷史上或未來。

儲存和處理雙時態歷史記錄

將歷史資料加入資料中會增加複雜性。在雙時態的世界中,我需要兩個日期參數才能存取莎莉的薪資 - sally.salaryAt('2021-02-25', '2021-03-25')。如果我們將記錄時間的預設值視為今日,則可以簡化存取,然後處理僅需要目前記錄時間的資料即可忽略雙時態的複雜性。

然而,簡化存取並不一定會簡化儲存。如果任何客戶端需要雙時態資料,我們必須以某種方式儲存它。雖然有些資料庫內建支援某種程度的時間性,但它們相對小眾。明智的做法是,當涉及到長期資料時,人們往往會特別小心小眾技術。

有鑑於此,通常最好的方法是提出我們自己的方案。有兩種廣泛的方法。

第一個是使用雙時態資料結構:將必要的日期資訊編碼到用於儲存資料的資料結構中。這可以使用巢狀日期範圍物件或關聯式表格中的一對開始/結束日期來運作。

記錄開始記錄結束實際開始實際結束薪資
1 月 1 日3 月 14 日1 月 1 日12 月 31 日6000
3 月 15 日4 月 4 日1 月 1 日2 月 14 日6000
3 月 15 日4 月 4 日2 月 15 日12 月 31 日6500
4 月 5 日12 月 31 日1 月 1 日2 月 14 日6000
4 月 5 日12 月 31 日2 月 15 日12 月 31 日6400

這允許存取所有雙時態歷史記錄,但更新和查詢很麻煩 - 儘管透過建立一個函式庫來處理雙時態資訊的存取,可以讓這變得更容易。

另一種方法是使用 事件溯源。在此,我們不會將莎莉薪資的狀態儲存在主要儲存區中,而是將所有變更儲存為事件。此類事件可能如下所示

記錄日期實際日期動作
1 月 1 日1 月 1 日sally.salary6000
3 月 15 日2 月 15 日sally.salary6500
4 月 5 日2 月 15 日sally.salary6400

請注意,如果事件需要支援雙時態歷史記錄,則它們本身需要是雙時態的。這表示每個事件都需要一個實際日期(或時間)來表示事件在世界上發生的時間,以及一個記錄日期(或時間)來表示我們得知該事件的時間。

在概念上,儲存事件較為直接,但需要更多處理才能回答查詢。然而,透過建立應用程式狀態的快照,可以快取許多處理。因此,如果這個資料的大部分使用者只要求目前的實際歷史,那麼我們可以建立一個僅支援實際歷史的資料結構,從事件中填入資料,並在有新事件流入時保持最新狀態。想要二時間資料的使用者可以建立一個更複雜的結構,並從相同的事件中填入資料,但他們的複雜度不會讓想要較簡單模型的人感到困難。(而且如果有些人想要在不同的記錄日期查看實際歷史,他們可以使用幾乎所有相同的程式碼來處理目前的實際歷史。)


進一步閱讀

我在 1980 和 90 年代使用各種軟體系統時遇到了二時間歷史的問題。我開始寫下我觀察到的模式,但在其他寫作專案接手之前,我從未完成初稿。其中有關於二時間歷史的討論,我撰寫這篇文章來強調這個概念,並希望更清楚地解釋它。

在那段時間,Richard Snodgrass 寫了一本書:在 SQL 中開發時間導向資料庫應用程式。它詳細說明如何在 SQL 系統中處理這種類型的問題,其方法影響了SQL:2011 標準。

我從時間旅行:一個用於會變化的值的模式語言中取得觀點的概念

腳註

1: 實際/記錄相對於有效/交易

有效時間和交易時間的術語來自Snodgrass,也用於SQL:2011 標準。當我最早在 2000 年代初開始舉辦有關時間建模的工作坊時,我使用了這些術語,但人們發現它們令人困惑。因此,我們開始使用實際/記錄。由於有效/交易尚未廣泛使用,我將遵循這個教訓,並在此處使用實際/記錄。

2: 二時間未來

在歷史中,實際時間永遠在記錄時間之前或同時。但二時間性的概念可以應用於未來。如果在 5 月 5 日被告知 Sally 將在 5 月 12 日獲得另一筆加薪,那麼我可以用 5 月 5 日的記錄時間和 5 月 12 日的實際時間來記錄這筆加薪。

3: 如果您不認識這句話,您應該將眨眼加入您的追蹤清單。有史以來拍攝最棒的時間旅行故事之一。

4: 我在 2000 年代中期關於 平行模型 的早期寫作中開始探討這個主題。當時我沒有繼續走那條路,我不確定我未來是否會或何時會重新審視那條路。

致謝

Alexandre Klaser、Dave Elliman、Joshua Taylor、Martha Rohte、Mauro Vilasi、Pavlo Kerestey、Pramod Sadalge、Rebecca Parsons、Saager Mhatre 和 Wolf Schlegel 在我們的內部郵件清單上對本文進行了有益的討論。

Heikki Heinonen 提醒我注意觀點表中的一些錯誤。

重大修訂

2021 年 4 月 7 日:發布

2021 年 3 月 17 日:送交內部審查

2021 年 3 月 2 日:開始起草