作為SQL Server 2016(CTP3.x)的另一個新特性,Temporal Table(歷史表)記錄了表歷史上任何時間點所有的數據改動。Temporal Table其實早在ANSI SQL 2011就提出了,而SAP HANA, DB2和Oracle早已在它們的產品中加入/實現了這一特性。所以說微軟其實是落后了幾個競爭對手。既然在CTP3.0中加入了,相信RTM也肯定有這個特性。
Temporal Table(歷史表)有何作用?
1)審計數據改動,為報表和數據分析提供支持,洞察記錄的變化趨勢
2)實現了ETL中的Slowly Changing Dimension的類型2(保留所有數據的舊版本)
3)一旦發生誤操作的情況下可以及時進行數據恢復
Temporal Table(歷史表)和CDC的區別
以前微軟為ETL提供了CDC功能來記錄數據改動。Temporal Table同樣是用於記錄數據改動,但是它倆不一樣。第一點,Temporal Table不像CDC是基於事務日志,它是作為事務的一部分被提交的。第二點,CDC是每次對新的表記錄最新版本拷貝一份到另外一張表,而Temporal Table是把舊版本的記錄轉移到另外一張表,只有把Temporal Table和當前表的記錄合並才可以構成表的整個歷史版本(記錄沒有被刪除的情況下)。
Temporal Table(歷史表)的條件?
1)必須有主鍵;2)兩個記錄有效時間范圍字段必須為not null;3)歷史表必須是和主表在結構上一模一樣,包括字段名字和數據類型;
Temporal Table(歷史表)如何實現?
Temporal Table其實對一對數據庫表進行數據版本化(System-versioning)。一張是主表,一張是主表的歷史記錄表。Temporal Table的條件之一是添加兩個類型為datetime2的字段來標示記錄的有效時間范圍 -- SysStartTime和SysEndTime。這兩個字段是有系統自動更新的,可以選擇在建表的時候對字段加入HIDDEN提示把字段隱藏,這樣就避免在SELECT * FROM或者INSERT INTO的時候出現兩個字段在列表里面。當插入(insert)發生時,事務開始的時間作為主表的SysStartTime,SysEndTime則被更新為9999-12-31,歷史表不會有任何變化。當更新(update)發生時,歷史記錄表中的SysEndTime被更新為事務開始的時間,主表的SysStartTime則被更新為事務開始的時間,SysEndTime則被更新為9999-12-31。當刪除(delete)發生時,歷史記錄表中的SysEndTime被更新為事務開始的時間。
查詢Temporal Table(歷史表)的記錄
SQL Server對T-SQL提供了幾個新的子句用於查詢Temporal Table中的記錄,即在正常的T-SQL查詢語句后面添加新的子句:
FOR SYSTEM_TIME ALL, AS OF, BETWEEN...AND, FROM...TO, CONTAINED IN
這里如果SysStartTime和SysEndTime相等時不會返回記錄的。
AS OF <date_time> 等於SysStartTime<= date_time AND SysEndTime> date_time
FROM <start_date_time> TO <end_date_time> 等於SysStartTime< end_date_time ANDSysEndTime> start_date_time
BETWEEN <start_date_time> AND <end_date_time> 等於SysStartTime<= end_date_time ANDSysEndTime> start_date_time
CONTAINED IN(<start_date_time> ,<end_date_time>) 等於SysStartTime>= start_date_time ANDSysEndTime<= end_date_time
ALL 等於沒有任何篩選條件
下圖是來自MSDN的一張圖,我覺得用於描述Temporal Table(歷史表)的工作流程非常確切
Temporal Table(歷史表)的最佳實踐?
1)如果是做數據分析,比如統計一個平均值或者總數這樣的分析,在History表的主鍵上建聚集列存儲索引(clustered columnstore index);
2)如果是做審計,對數據行有效時間范圍字段建聚集索引,然后對主鍵也建立索引;
創建Temporal Table(歷史表)
History table是不會出現在SQL Server Management Studio的Object Explorer窗口的,但是你可以通過sys.tables找出來。
Temporal Table(歷史表)可以有三種方法創建:1)你完全不關心名字,讓SQL Server幫你創建包括幫你自動生成表名;2)你指定表名然后讓SQL Server根據表名為你生成表結構;3)你事先創建好表;
由SQL Server自動創建History表
CREATE TABLE dbo.TemporalTableTEST1 ( ID INT PRIMARY KEY CLUSTERED , SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL , SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL , PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime) ) WITH ( SYSTEM_VERSIONING = ON);
自己指定表名字和自己事先創建好表都是一樣的語法。如果表已經存在,表的結構會被檢查。檢出出問題命令失敗。
CREATE TABLE dbo.TemporalTableTEST2 ( ID INT PRIMARY KEY CLUSTERED , SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL , SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL , PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime) ) WITH ( SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TemporalTableTEST2_History));
因為事先創建好的表可能已經存在數據行,建議添加好DATA_CONSISTENCY_CHECK來同時檢查表中的數據行。建議在事先創建好表的情況下添加DATA_CONSISTENCY_CHECK選項
CREATE TABLE dbo.TemporalTableTEST3 ( ID INT PRIMARY KEY CLUSTERED , SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL , SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL , PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime) ) WITH ( SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TemporalTableTEST3_History, DATA_CONSISTENCY_CHECK = ON));
如果是對一張現有表進行轉換,要分兩種情況:一種是表是空表,一種是表里面已經存在數據行。下面是對一張空表轉換成Temporal Table的例子
--DROP TABLE dbo.TemporalTableTEST5 CREATE TABLE dbo.TemporalTableTEST5 ( ID INT PRIMARY KEY CLUSTERED ) GO SELECT * FROM dbo.TemporalTableTEST5 GO ALTER TABLE dbo.TemporalTableTEST5 ADD SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START CONSTRAINT DF_TemporalTableTEST5_SysStart DEFAULT SYSUTCDATETIME() , SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END CONSTRAINT DF_TemporalTableTEST5_SysEnd DEFAULT CONVERT(datetime2 (0), '9999-12-31 23:59:59.9999999'), PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime) GO ALTER TABLE dbo.TemporalTableTEST5 SET ( SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TemporalTableTEST5_History, DATA_CONSISTENCY_CHECK = ON)); GO
如果表中有數據,需要分開來完成這個轉換過程。
--DROP TABLE dbo.TemporalTableTEST4 CREATE TABLE dbo.TemporalTableTEST4 ( ID INT PRIMARY KEY CLUSTERED ) GO INSERT INTO dbo.TemporalTableTEST4(ID) VALUES(1),(2),(3) GO --INSERT INTO dbo.TemporalTableTEST4(ID) --VALUES(4),(5),(6) --GO ALTER TABLE dbo.TemporalTableTEST4 ADD SysStartTime DATETIME2 NOT NULL CONSTRAINT DF_TemporalTableTEST4_SysStart DEFAULT SYSUTCDATETIME() , SysEndTime DATETIME2 NOT NULL CONSTRAINT DF_TemporalTableTEST4_SysEnd DEFAULT CONVERT(DATETIME2, '9999-12-31 23:59:59.9999999') GO SELECT * FROM dbo.TemporalTableTEST4 GO --UPDATE dbo.TemporalTableTEST4 SET SysEndTime = '9999-12-31 23:59:59.9999999' --GO --ALTER TABLE dbo.TemporalTableTEST4 --ALTER COLUMN SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START --ALTER TABLE dbo.TemporalTableTEST4 --ALTER COLUMN SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END ALTER TABLE dbo.TemporalTableTEST4 ADD PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime); GO ALTER TABLE dbo.TemporalTableTEST4 SET ( SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TemporalTableTEST4_History, DATA_CONSISTENCY_CHECK = ON)); GO
如果你直接用第一種辦法就會收到一個錯誤提示
Msg 13575, Level 16, State 0, Line 53 ADD PERIOD FOR SYSTEM_TIME failed because table 'JerryDB.dbo.TemporalTableTEST4' contains records where end of period is not equal to MAX datetime.
為了證明SQL Server只要求主表和歷史表的字段結構和約束一致,不要求分區和壓縮選項一致,這里做一個實驗
CREATE PARTITION FUNCTION myPF (int) AS RANGE LEFT FOR VALUES (1, 100, 1000); GO CREATE PARTITION SCHEME myPS1 AS PARTITION myPF TO ( [primary], [primary], [primary], [primary] ); --DROP TABLE dbo.TemporalTableTEST7_History CREATE TABLE dbo.TemporalTableTEST7_History ( ID INT NOT NULL , SysStartTime DATETIME2 NOT NULL , SysEndTime DATETIME2 NOT NULL ) ON myPS1(ID) WITH (DATA_COMPRESSION = PAGE ) CREATE TABLE dbo.TemporalTableTEST7 ( ID INT PRIMARY KEY CLUSTERED , SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL , SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL , PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime) ) WITH ( SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.TemporalTableTEST7_History));
上面是可行的。
總結一下:
總之記住幾個點
1)PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)和SYSTEM_VERSIONING都是Temporal Table的特性,但是它倆在某些方面是不互相依賴。SYSTEM_VERSIONING是起到啟動歷史表的作用,PERIOD FOR SYSTEM_TIME是為標示表記錄有效時間范圍而存在,屬於表結構屬性的范疇。
2)PERIOD FOR SYSTEM_TIME隱式的將連個SYSTEM TIME的字段轉換成AS ROW STARTS和AS ROW ENDS
3)SYSTEM_VERSIONING是不會阻止你去更新主鍵的,所以一旦你更新了主鍵,將會導致主表和歷史表的記錄錯亂;
4)雖然PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)和SYSTEM_VERSIONING在某些方面不互相依賴,但是要更新SYSTEMTIME字段,需要SET SYSTEM_VERSIONING = OFF
對PARTITION SWITCH的支持
條件是stage表需要有SYSTEMTIME PERIOD,但是不要求表必須是SYSTEM_VERSIONING。這里對主表和歷史表的限制是
主表:在SYSTEM_VERSIONING=ON的情況下SWITCH OUT是不允許的,我們都知道PARTITION SWITCH僅是元數據(metadata)的變動,這樣History表是捕捉不到分區內數據的,所以行不通;SWITCH IN在SYSTEM_VERSIONING=OFF的情況下是可以進行的,畢竟這樣也不會對History表有什么影響,因為SWITCH IN相當於INSERT行為,INSERT對於History表沒有影響。
歷史表:SWITCH OUT可以在SYSTEM_VERSIONING=ON的情況下進行;SWITCH IN在SYSTEM_VERSIONING=ON的情況下則不行,因為這本身就違反了History表的數據驗證流程;
注:
這里所說的Data Consistency檢查只是檢查是否SysStartTime<=SysEndTime
對Temporal Table的主表的結構改動
這一步SQL Server倒是做得挺好的,就是不需要你改完主表還要去改歷史表。比如你添加一個字段和一個默認約束到主表,歷史表也自動應用到同樣的改動。
ALTER TABLE dbo.TemporalTableTEST4 ADD Name NVARCHAR(100) NOT NULL CONSTRAINT DK_TemporalTableTEST4_Name DEFAULT 'Jerry' GO
select * from dbo.TemporalTableTEST4
select * from dbo.TemporalTableTEST4_History
結果
某些情況下我們可以不停用SYSTEM_VERSIONING的情況下照樣完成了對主表的結構改動,比如添加一個正常的字段(非compted等),但是如果添加諸如identity或者computed字段則需要停用system_versioning。否則
Msg 13724, Level 16, State 1, Line 135 System-versioned table schema modification failed because adding computed column while system-versioning is ON is not supported.
但是有一點是例外,就是如果你要刪除主表的一個字段,除了要刪除主表字段上創建的約束外還要刪除歷史表上對應字段的約束,否則
Msg 5074, Level 16, State 1, Line 142 The object 'DF__TemporalTa__dttm__02FC7413' is dependent on column 'dttm'. Msg 4922, Level 16, State 9, Line 142 ALTER TABLE DROP COLUMN dttm failed because one or more objects access this column.
查詢數據
上面講了FOR SYSTEM_TIME的五個子句 AS OF | FROM...TO | BETWEEN...AND | CONTAINED IN (<START>,<END>) | ALL
理解這幾個其實很容易,完全無需去記住他們所應用的WHERE表達式。
ALL = 全部嘛,這個不用講
AS OF = AS OF的英文意思是自...開始,那就是某個時間點有效(包括這個時間點)的行
FROM... TO = 時間區間內有效的行,但是不包含開閉的時間點,即不包含上限
BETWEEN...AND = 時間區間內有效的行,包含開的時間點,即包含下限
CONTAINED IN (<START>,<END>) = CONTAINED的意思是包含,也就是說記錄有效區間處在我們指定的時間區間這個容器內
拿上面的TemporalTableTEST5來demo。先准備好數據。
ALTER TABLE dbo.TemporalTableTEST5 ADD float_col FLOAT GO INSERT INTO dbo.TemporalTableTEST5(ID, float_col) VALUES(1,100),(2,200),(3,300) GO UPDATE dbo.TemporalTableTEST5 SET float_col = float_col + 50 WHERE ID = 1 UPDATE dbo.TemporalTableTEST5 SET float_col = float_col + 50 WHERE ID = 1 UPDATE dbo.TemporalTableTEST5 SET float_col = float_col + 50 WHERE ID = 1
查詢全部的歷史數據
SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME ALL
ID float_col SysStartTime SysEndTime 1 250 2016-02-21 09:27:52.0379197 9999-12-31 23:59:59.9999999 2 200 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 3 300 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 1 100 2016-02-21 09:27:38.6363057 2016-02-21 09:27:40.5162446 1 150 2016-02-21 09:27:40.5162446 2016-02-21 09:27:46.4312559 1 200 2016-02-21 09:27:46.4312559 2016-02-21 09:27:52.0379197
AS OF
SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME AS OF '2016-02-21 09:27:38.6363057';
ID float_col SysStartTime SysEndTime 2 200 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 3 300 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 1 100 2016-02-21 09:27:38.6363057 2016-02-21 09:27:40.5162446
FROM...TO
SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME FROM '2016-02-21' TO '2016-02-21 09:27:38.6363057' SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME FROM '2016-02-21 09:27:40.5162446' TO '2016-02-22'
第一個沒有記錄返回
第二個返回了
ID float_col SysStartTime SysEndTime 1 250 2016-02-21 09:27:52.0379197 9999-12-31 23:59:59.9999999 2 200 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 3 300 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 1 150 2016-02-21 09:27:40.5162446 2016-02-21 09:27:46.4312559 1 200 2016-02-21 09:27:46.4312559 2016-02-21 09:27:52.0379197
把FROM...TO的例子替換成BETWEEN...AND
SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME BETWEEN '2016-02-21' AND '2016-02-21 09:27:38.6363057' SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME BETWEEN '2016-02-21 09:27:40.5162446' AND '2016-02-22'
第一個返回了
ID float_col SysStartTime SysEndTime 2 200 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 3 300 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 1 100 2016-02-21 09:27:38.6363057 2016-02-21 09:27:40.5162446
第二個返回了
ID float_col SysStartTime SysEndTime 1 250 2016-02-21 09:27:52.0379197 9999-12-31 23:59:59.9999999 2 200 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 3 300 2016-02-21 09:27:38.6363057 9999-12-31 23:59:59.9999999 1 150 2016-02-21 09:27:40.5162446 2016-02-21 09:27:46.4312559 1 200 2016-02-21 09:27:46.4312559 2016-02-21 09:27:52.0379197
還是上面的例子改寫,變成CONTAINED IN
SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME CONTAINED IN ('2016-02-21', '2016-02-21 09:27:38.6363057') SELECT ID, float_col, [SysStartTime],[SysEndTime] FROM [dbo].TemporalTableTEST5 FOR SYSTEM_TIME CONTAINED IN ('2016-02-21 09:27:40.5162446', '2016-02-22')
結果就是第一個沒有返回任何結果
第二個返回了
ID float_col SysStartTime SysEndTime 1 150 2016-02-21 09:27:40.5162446 2016-02-21 09:27:46.4312559 1 200 2016-02-21 09:27:46.4312559 2016-02-21 09:27:52.0379197
這個語法同樣適用於UPDATE
講了這么多,Temparal Table還是有需要方面可講的,比如它對In-memory OLTP Optimized Table的支持啦,比如安全的考慮啦。真要將估計很多。姑且到這。今后有機會再深究下。最后,在這里思考下到底這個東西在現實生產環境中可以怎么好好利用或者結合其他的特性一起發揮它的最大價值呢?
1)首先我覺得基於上面講到的可以作為數據誤操作的數據復原。Temparal Table 結合SQL Server Audit。Temparal Table實現記錄歷史記錄改動,而SQL Server Audit提供了對用戶行為的審計。兩者通過時間來關聯。這樣我們就是當初這條舊的歷史版本記錄是被誰改動或者刪除的。然后對SQL Server Audit加載的目標表創建聚集索引到時間行以及以時間字段創建分區表。再創建非聚集索引到Object字段,再結合PAGE COMPRESSION壓縮SQL Server Audit加載的目標表的數據行。
2)報表分析這個案例我覺得視情況而定,要看到底是為了查看某條或者若干記錄過去的變化趨勢,還是查看數據分組后的平均變化情況或者是一些總量之類的東西。前者我覺得對Temparal Table的歷史表應用聚集索引配合PAGE COMPRESSION,后者對Temparal Table的歷史表創建Clustered Columnstore Index,加上以分區表技術(時間字段選擇end time)。
參考:
Temporal Tables
Getting Started with System-Versioned Temporal Tables
Temporal Table System Consistency Checks
Partitioning with Temporal Tables
Temporal Table Considerations and Limitations
Manage Retention of Data in History Tables in System-Versioned Temporal Tables
System-Versioned Temporal Tables with Memory-Optimized Tables
Temporal Table Metadata Views and Functions
Manage Retention of Historical Data in System-Versioned Temporal Tables