时态表使用场景

适用于: SQL Server 2016 (13.x) 及以后版本 Azure SQL 数据库Azure SQL 托管实例Microsoft Fabric 中的 SQL 数据库

系统版本控制的时态表适用于需要跟踪数据更改历史记录的场景。 建议在以下用例中考虑使用时态表,可获得巨大的生产力优势。

数据审核

对存储关键信息的表使用时态系统版本控制,可跟踪对这些信息所做的更改和更改发生的时间,以及在任何时间点进行数据取证。

利用时序表在开发周期的早期阶段规划数据审计场景。 你需要时,可以为现有应用或解决方案添加数据审计功能。

下图显示了一个 Employee 表,其数据样本包括当前行版本(标记为蓝色)以及历史行版本(标记为灰色)。

图的右侧部分展示了时间轴上的行版本,以及在时间表上使用不同类型的查询(带有或不带有 SYSTEM_TIME 子句)时所选择的行。

图示显示第一个时序用法场景。

对新表启用系统版本控制,以便进行数据审核

如果发现某些信息需要进行数据审计,请将数据库表创建为系统版本控制的时态表。 以下示例说明了一个假设的 HR 数据库中名为 Employee 的表的场景:

CREATE TABLE Employee
(
    [EmployeeID] INT NOT NULL PRIMARY KEY CLUSTERED,
    [Name] NVARCHAR (100) NOT NULL,
    [Position] VARCHAR (100) NOT NULL,
    [Department] VARCHAR (100) NOT NULL,
    [Address] NVARCHAR (1024) NOT NULL,
    [AnnualSalary] DECIMAL (10, 2) NOT NULL,
    [ValidFrom] DATETIME2 (2) GENERATED ALWAYS AS ROW START,
    [ValidTo] DATETIME2 (2) GENERATED ALWAYS AS ROW END,
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.EmployeeHistory));

创建时序系统版本表的各种选项详见 “创建系统版本时序表”。

对现有表启用系统版本控制,以便进行数据审核

若需在现有数据库中执行数据审核,可使用 ALTER TABLE 扩展非时态表,使之成为系统版本控制型的。 为了避免对应用程序造成破坏性更改,请按照 HIDDEN 中的说明,将周期列添加为

以下示例说明了如何在虚构 HR 数据库中针对现有 Employee 表启用系统版本控制。 它通过两个步骤在 Employee 表中启用系统版本控制。 首先,将新时间段列添加为 HIDDEN。 然后,它会创建默认历史记录表。

ALTER TABLE Employee
ADD
    ValidFrom DATETIME2 (2) GENERATED ALWAYS AS ROW START HIDDEN
        CONSTRAINT DF_ValidFrom DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()),
    ValidTo DATETIME2 (2) GENERATED ALWAYS AS ROW END HIDDEN
        CONSTRAINT DF_ValidTo DEFAULT '9999.12.31 23:59:59.99',
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);

ALTER TABLE Employee
    SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.Employee_History));

Important

源表中的 datetime2 数据类型精度必须与系统版本控制的历史记录表中的精度一致。

执行上一个脚本后,历史表会透明地收集所有数据变更。 在典型的数据审计场景中,你查询在感兴趣的时间段内,针对单个行应用的所有数据变更。 可使用聚集行存储 B 树来创建默认的历史记录表,从而高效地解决这种用例问题。

注释

文档在提到索引时一般使用 B 树这个术语。 在行存储索引中,数据库引擎实现了 B+ 树。 这不适用于列存储索引或内存优化表上的索引。 有关详细信息,请参阅 SQL Server 以及 Azure SQL 索引体系结构和设计指南

执行数据分析

使用上述方法之一启用系统版本控制以后,只需执行一个查询即可进行数据审核。 以下查询将在 Employee 表中搜索具有 EmployeeID = 1000 的记录的行版本,这些记录在 2021 年 1 月 1 日至 2022 年 1 月 1 日期间(包括上边界)内,至少有一部分时间处于活动状态:

SELECT *
FROM Employee FOR SYSTEM_TIME
    BETWEEN '2021-01-01 00:00:00.0000000' AND '2022-01-01 00:00:00.0000000'
WHERE EmployeeID = 1000
ORDER BY ValidFrom;

FOR SYSTEM_TIME BETWEEN...AND 替换为 FOR SYSTEM_TIME ALL 以分析该特定员工的整个数据变更历史记录:

SELECT *
FROM Employee FOR SYSTEM_TIME ALL
WHERE EmployeeID = 1000
ORDER BY ValidFrom;

若要搜索仅在某个时间段内(不计该时间段外)处于活动状态的行版本,请使用 CONTAINED IN。 此查询非常高效,因为只查询历史记录表:

SELECT *
FROM Employee FOR SYSTEM_TIME
    CONTAINED IN ('2021-01-01 00:00:00.0000000', '2022-01-01 00:00:00.0000000')
WHERE EmployeeID = 1000
ORDER BY ValidFrom;

最后,在某些审计场景中,你可能想查看整个表格在过去任何时间点的样子:

SELECT *
FROM Employee FOR SYSTEM_TIME
    AS OF '2021-01-01 00:00:00.0000000';

系统版本控制型时态表在存储时间段列的值时使用 UTC 时区,但你可能会发现在筛选数据和显示结果时,使用本地时区更方便。 以下代码示例展示了如何应用过滤条件,该条件在当地时区指定,然后通过使用以下 AT TIME ZONE方式转换为UTC:

/* Add offset of the local time zone to current time*/
DECLARE @asOf AS DATETIMEOFFSET = GETDATE() AT TIME ZONE 'Pacific Standard Time';

/* Convert AS OF filter to UTC*/
SET @asOf = DATEADD(HOUR, -9, @asOf) AT TIME ZONE 'UTC';

SELECT EmployeeID,
       [Name],
       Position,
       Department,
       [Address],
       [AnnualSalary],
       ValidFrom AT TIME ZONE 'Pacific Standard Time' AS ValidFromPT,
       ValidTo AT TIME ZONE 'Pacific Standard Time' AS ValidToPT
FROM Employee FOR SYSTEM_TIME AS OF @asOf
WHERE EmployeeId = 1000;

在所有其他使用系统版本控制表的场景中,使用 AT TIME ZONE 都很有帮助。

在具有 FOR SYSTEM_TIME 的时间子句中指定的筛选条件是 SARGable

注释

在关系数据库中,术语 SARGable 指的是 Search ARGumentable 谓词,即能够利用索引来加快查询执行速度的谓词。 有关详细信息,请参阅 SQL Server 和 Azure SQL 索引体系结构和设计指南

如果你直接查询历史记录表,请确保筛选条件同样是 SARGable 的,方法是将筛选器指定为 <period column> { < | > | =, ... } date_condition AT TIME ZONE 'UTC' 的形式。

如果将 AT TIME ZONE 应用于期间列,SQL Server 将执行表扫描或索引扫描,这样做的开销可能会很高。 在查询中要避免这种类型的条件:

<period column> AT TIME ZONE '<your time zone>' > {< | > | =, ...} date_condition

有关详细信息,请参阅在系统版本控制的临时表中查询数据

时间点分析(按时间顺序查看)

时间旅行场景不再关注单个记录的变化,而是展示整个数据集随时间的变化。 有时时间旅行包含多个相关的时间表,每个时间表以独立速度变化,你需要对它们进行分析:

  • 历史数据和当前数据中重要指标所指示的趋势
  • 截至过去任意时间点(昨天、一个月前等)的全部数据精确快照
  • 两个所关注时间点(例如,一月前和三月前)之间的差异

许多现实场景都需要时间旅行分析。 为了说明这种使用场景,我们来看带有自动生成历史的在线交易处理(OLTP)。

带有自动生成数据历史记录的 OLTP

在事务处理系统中,可以分析重要度量值在一定时段内的变化情况。 理想情况下,分析历史不应影响 OLTP 应用的性能,因为访问最新数据状态必须以最小延迟和数据锁定完成。 可以使用系统版本控制型时态表以透明方式保留所做更改的完整历史记录,以便以后进行分析。这些历史记录独立于当前数据,将对主 OLTP 工作负荷的影响降到最低。

对于SQL Server和Azure SQL 托管实例中高事务处理工作负载,我们建议使用带有内存优化表的系统版本化时间表,这样你可以以成本效益的方式将当前数据存储在内存中,并将完整的变更历史存储在磁盘上。

对于历史记录表,建议你使用聚集列存储索引,原因如下:

  • 聚集列存储索引可提高查询性能,这有益于典型的趋势分析。

  • 在 OLTP 工作负荷很重的情况下,如果历史记录表使用聚集列存储索引,则内存优化表的数据刷新任务的执行效果最佳。

  • 聚集列存储索引的压缩效果很好,尤其是在并非所有列的更改都同时发生的情况下。

使用内存中的 OLTP 的时序表减少了将整个数据集保存在内存中的需求,并能轻松区分热数据和冷数据。

现实中此类情况的示例包括库存管理、货币贸易等。

下图展示了用于库存管理的简化数据模型:

关系图显示用于库存管理的简化数据模型。

以下代码示例创建一个 ProductInventory 内存内系统版本化的时序表,历史表上有一个集群列存储索引(取代默认创建的行存储索引):

注释

请确保你的数据库允许创建内存优化表。 请参阅 创建内存优化表和本机编译的存储过程

USE TemporalProductInventory;
GO

BEGIN

    --If the table is system-versioned, set SYSTEM_VERSIONING to OFF first
    IF ((SELECT temporal_type
        FROM SYS.TABLES
        WHERE object_id = OBJECT_ID('dbo.ProductInventory', 'U')) = 2)
    BEGIN
        ALTER TABLE [dbo].[ProductInventory]
            SET (SYSTEM_VERSIONING = OFF);
    END

    DROP TABLE IF EXISTS [dbo].[ProductInventory];
    DROP TABLE IF EXISTS [dbo].[ProductInventoryHistory];

END
GO

CREATE TABLE [dbo].[ProductInventory]
(
    ProductId INT NOT NULL,
    LocationID INT NOT NULL,
    Quantity INT NOT NULL CHECK (Quantity >= 0),
    ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
    ValidTo DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
    --Primary key definition
    CONSTRAINT PK_ProductInventory PRIMARY KEY NONCLUSTERED (ProductId, LocationId),
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (
    MEMORY_OPTIMIZED = ON,
    SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = [dbo].[ProductInventoryHistory],
        DATA_CONSISTENCY_CHECK = ON
    )
);

CREATE CLUSTERED COLUMNSTORE INDEX IX_ProductInventoryHistory
    ON [ProductInventoryHistory] WITH (DROP_EXISTING = ON);

对于上述模型来说,下面是库存维护过程的具体内容:

CREATE PROCEDURE [dbo].[spUpdateInventory] (
    @productId INT,
    @locationId INT,
    @quantityIncrement INT
)
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC
WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'English')
    UPDATE dbo.ProductInventory
        SET Quantity = Quantity + @quantityIncrement
    WHERE ProductId = @productId
        AND LocationId = @locationId;

    -- If zero rows were updated then this is an insert
    -- of the new product for a given location
    IF @@rowcount = 0
    BEGIN
        IF @quantityIncrement < 0
        BEGIN
            SET @quantityIncrement = 0;
        END

        INSERT INTO [dbo].[ProductInventory]
        (
            [ProductId],
            [LocationID],
            [Quantity]
        )
        VALUES (
            @productId,
            @locationId,
            @quantityIncrement
        );
    END
END;

spUpdateInventory 存储过程可以将新产品插入库存中,也可以更新特定位置的产品数量。 业务逻辑简单,专注于通过表格更新递增/递减 Quantity 字段来始终保持最新状态的准确性,而系统版本化表则透明地为数据添加历史维度,如下图所示。

图中显示了时态使用情况,以及内存中的当前使用情况和聚集列存储中的历史使用情况。

现在,你可以高效地查询本地编译模块的最新状态:

CREATE PROCEDURE [dbo].[spQueryInventoryLatestState]
WITH NATIVE_COMPILATION, SCHEMABINDING
AS
BEGIN ATOMIC
WITH (TRANSACTION ISOLATION LEVEL = SNAPSHOT, LANGUAGE = N'English')
    SELECT ProductId,
           LocationID,
           Quantity,
           ValidFrom
    FROM dbo.ProductInventory
    ORDER BY ProductId, LocationId;
END;
GO

EXECUTE [dbo].[spQueryInventoryLatestState];

使用 FOR SYSTEM_TIME ALL 子句可轻松进行一定时段内的数据更改分析,如以下示例所示:

DROP VIEW IF EXISTS vw_GetProductInventoryHistory;
GO

CREATE VIEW vw_GetProductInventoryHistory AS
    SELECT ProductId,
           LocationId,
           Quantity,
           ValidFrom,
           ValidTo
    FROM [dbo].[ProductInventory] FOR SYSTEM_TIME ALL;
GO

SELECT *
FROM vw_GetProductInventoryHistory
WHERE ProductId = 2;

下图显示了一个产品的数据历史记录,可以将上述视图导入 Power Query、Power BI 或类似的商业智能工具来轻松呈现该记录:

关系图显示一种产品的数据历史记录。

在这种情况下,你可以使用时间表进行其他类型的时间旅行分析,比如重建过去任一时间点的库存 AS OF 状态,或比较属于不同时间点的快照。

对于这种使用场景,你还可以将 ProductNumberOfEmployee 表扩展为时态表,以便后续分析 UnitPriceLocation 的变更历史。

ALTER TABLE Product
ADD ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN
        CONSTRAINT DF_ValidFrom DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()),
    ValidTo DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN
        CONSTRAINT DF_ValidTo DEFAULT '9999.12.31 23:59:59.99',
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);

ALTER TABLE Product
    SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.ProductHistory));

ALTER TABLE [Location]
ADD
    ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN
        CONSTRAINT DFValidFrom DEFAULT DATEADD(SECOND, -1, SYSUTCDATETIME()),
    ValidTo DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN
        CONSTRAINT DFValidTo DEFAULT '9999.12.31 23:59:59.99',
    PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);

ALTER TABLE [Location]
    SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.LocationHistory));

由于数据模型现在涉及多个时序表,分析的最佳实践 AS OF 是创建一个视图,从相关表中提取必要数据并应用于 FOR SYSTEM_TIME AS OF 视图,因为这大大简化了重建整个数据模型状态的过程:

DROP VIEW IF EXISTS vw_ProductInventoryDetails;
GO

CREATE VIEW vw_ProductInventoryDetails
AS
SELECT PrInv.ProductId,
       PrInv.LocationId,
       p.ProductName,
       l.LocationName,
       PrInv.Quantity,
       p.UnitPrice,
       l.NumberOfEmployees,
       p.ValidFrom AS ProductStartTime,
       p.ValidTo AS ProductEndTime,
       l.ValidFrom AS LocationStartTime,
       l.ValidTo AS LocationEndTime,
       PrInv.ValidFrom AS InventoryStartTime,
       PrInv.ValidTo AS InventoryEndTime
FROM dbo.ProductInventory AS PrInv
    INNER JOIN dbo.Product AS p
        ON PrInv.ProductId = p.ProductID
    INNER JOIN dbo.Location AS l
        ON PrInv.LocationId = l.LocationID;
GO

SELECT *
FROM vw_ProductInventoryDetails
    FOR SYSTEM_TIME AS OF '2022-01-01';

以下屏幕截图显示了为 SELECT 查询生成的执行计划。 这表明,在处理时态关系时,不管过程怎么复杂,都可以通过数据库引擎得到妥善处理:

图中显示了“SELECT”查询生成的执行计划,其中说明了在处理时态关系时,不管过程怎么复杂,都可以通过 SQL Server 数据库引擎得到妥善处理。

请使用以下代码比较两个时间点(一天前和一个月前)的产品库存状态:

DECLARE @dayAgo AS DATETIME2 = DATEADD(DAY, -1, SYSUTCDATETIME());
DECLARE @monthAgo AS DATETIME2 = DATEADD(MONTH, -1, SYSUTCDATETIME());

SELECT inventoryDayAgo.ProductId,
       inventoryDayAgo.ProductName,
       inventoryDayAgo.LocationName,
       inventoryDayAgo.Quantity AS QuantityDayAgo,
       inventoryMonthAgo.Quantity AS QuantityMonthAgo,
       inventoryDayAgo.UnitPrice AS UnitPriceDayAgo,
       inventoryMonthAgo.UnitPrice AS UnitPriceMonthAgo
FROM vw_ProductInventoryDetails FOR SYSTEM_TIME AS OF @dayAgo AS inventoryDayAgo
     INNER JOIN vw_ProductInventoryDetails FOR SYSTEM_TIME AS OF @monthAgo AS inventoryMonthAgo
         ON inventoryDayAgo.ProductId = inventoryMonthAgo.ProductId
        AND inventoryDayAgo.LocationId = inventoryMonthAgo.LocationID;

异常情况检测

异常检测,或称 异常值检测,是指识别不符合预期模式或与数据集中其他数据项不一致的数据项。 你可以使用系统版本化的时间表,通过时间查询快速定位特定模式,检测周期性或不规则的异常。 什么算是异常,取决于你收集的数据类型和你的业务逻辑。

以下示例显示了在销售数字中检测“峰值”所需的简化逻辑。 假定你要处理一个临时表,该表收集所购产品的历史记录:

CREATE TABLE [dbo].[Product]
(
    [ProdID] INT NOT NULL PRIMARY KEY CLUSTERED,
    [ProductName] VARCHAR (100) NOT NULL,
    [DailySales] INT NOT NULL,
    [ValidFrom] DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL,
    PERIOD FOR SYSTEM_TIME ([ValidFrom], [ValidTo])
)
WITH (
    SYSTEM_VERSIONING = ON (
        HISTORY_TABLE = [dbo].[ProductHistory],
        DATA_CONSISTENCY_CHECK = ON
    )
);

下图显示了一定时段内的购买项目:

显示购买情况随时间变化的图表。

假设在正常情况下,购买的产品数量波动较小,则以下查询可识别孤立离群点:即与其相邻样本相比差异显著(达到 2 倍),而周围样本之间的差异并不显著(小于 20%)的样本:

WITH CTE (ProdId, PrevValue, CurrentValue, NextValue, ValidFrom, ValidTo)
AS (SELECT ProdId,
           LAG(DailySales, 1, 1) OVER (PARTITION BY ProdId ORDER BY ValidFrom) AS PrevValue,
           DailySales,
           LEAD(DailySales, 1, 1) OVER (PARTITION BY ProdId ORDER BY ValidFrom) AS NextValue,
           ValidFrom,
           ValidTo
    FROM Product FOR SYSTEM_TIME ALL)
SELECT ProdId,
       PrevValue,
       CurrentValue,
       NextValue,
       ValidFrom,
       ValidTo,
       ABS(PrevValue - NextValue) / CONVERT (FLOAT, (CASE WHEN NextValue > PrevValue THEN PrevValue ELSE NextValue END)) AS PrevToNextDiff,
       ABS(CurrentValue - PrevValue) / CONVERT (FLOAT, (CASE WHEN CurrentValue > PrevValue THEN PrevValue ELSE CurrentValue END)) AS CurrentToPrevDiff,
       ABS(CurrentValue - NextValue) / CONVERT (FLOAT, (CASE WHEN CurrentValue > NextValue THEN NextValue ELSE CurrentValue END)) AS CurrentToNextDiff
FROM CTE
WHERE ABS(PrevValue - NextValue) / (CASE WHEN NextValue > PrevValue THEN PrevValue ELSE NextValue END) < 0.2
      AND ABS(CurrentValue - PrevValue) / (CASE WHEN CurrentValue > PrevValue THEN PrevValue ELSE CurrentValue END) > 2
      AND ABS(CurrentValue - NextValue) / (CASE WHEN CurrentValue > NextValue THEN NextValue ELSE CurrentValue END) > 2;

注释

此示例已特意简化。 在生产方案中,你可能使用高级统计方法来识别不遵循常见模式的样本。

渐变维度

数据仓库中的维度通常包含相对静态的与以下实体相关的数据:地理位置、客户或产品。 不过,某些方案会要求你也跟踪数据在维度表中的更改。 鉴于维度的变动发生频率较低,且发生在不可预测且超出事实表的常规更新计划之外,这类维度表被称为缓慢变化维度(SCD)。

根据变化历史的保存方式,有几类缓慢变化的维度:

维度类型 详细信息
类型 0 不保留历史记录。 维度属性反映原始值。
类型 1 维度属性反映最新值(覆盖以前的值)
类型 2 维度成员的每个版本在表中通常以单独的一行表示,并配有表示有效期的列
类型 3 在同一行中使用额外列保留所选属性的有限历史记录
类型 4 在单独的表中保留历史记录,而原始维度表则保留最新(当前)的维度成员版本

选择 SCD 策略时,由 ETL(提取-转换-加载)层确保维度表的准确性,通常这需要编写更复杂的代码并完成额外的维护工作。

你可以使用系统版本化的时序表,大幅降低代码的复杂度,因为数据历史会自动被保存。 由于是使用两个表来实现的,时态表最接近于类型 4 SCD。 但是,由于时态查询只允许你引用当前表,因此也可以考虑在计划使用类型 2 SCD 的环境中使用时态表。

要将常规维度转换为SCD,你可以创建一个新的维度,或者修改现有维度,使其成为系统版本的时序表。 如果你现有的维度表包含历史数据,创建一个单独的表,并将历史数据移到那里,同时保留当前(实际)维度版本在原维度表中。 然后,使用 ALTER TABLE 语法将维度表转换为带有预定义历史表的系统版本控制的时态表。

以下示例说明了该过程,并假设 DimLocation 维量表中已有 ValidFromValidTo 作为 datetime2 不可空的列,ETL 进程填充这些列:

  • 封闭 行版本移入新的历史表:

    SELECT *
    INTO DimLocationHistory
    FROM DimLocation
    WHERE ValidTo < '9999-12-31 23:59:59.99';
    GO
    
  • 创建集群列存储索引,这是数据仓库场景中的一个不错选择:

    CREATE CLUSTERED COLUMNSTORE INDEX IX_DimLocationHistory
        ON DimLocationHistory;
    
  • 从中删除之前的版本 DimLocation,该表成为时序系统版本控制配置中的当前表:

    DELETE FROM DimLocation
    WHERE ValidTo < '9999-12-31 23:59:59.99';
    
  • 添加周期定义:

    ALTER TABLE DimLocation
        ADD PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
    
  • 启用系统版本控制,并将历史记录表绑定到 DimLocation

    ALTER TABLE DimLocation
        SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.DimLocationHistory));
    

在创建SCD后,在数据仓库加载过程中维护SCD时,不需要额外代码。

下图展示了如何在涉及两个SCD(DimLocationDimProduct)和一个事实表的基本场景中使用时间表。

图中显示了如何在涉及 2 个 SCD(DimLocation 和 DimProduct)和 1 个事实数据表的简单方案中使用时态表。

要在报表中使用先前的 SCD,你需要相应地调整查询方式。 例如,你可能需要计算过去六个月的总销售额和人均销售产品数。 这两个指标都需要将事实表中的数据与维度表中的数据进行关联,而维度表中对分析很重要的属性可能已经发生了变化(DimLocation.NumOfCustomersDimProduct.UnitPrice)。

以下查询对所需指标进行了正确的计算:

DECLARE @now AS DATETIME2 = SYSUTCDATETIME();
DECLARE @sixMonthsAgo AS DATETIME2;

SET @sixMonthsAgo = DATEADD(month, -12, SYSUTCDATETIME());

SELECT DimProduct_History.ProductId,
       DimLocation_History.LocationId,
       SUM(f.Quantity * DimProduct_History.UnitPrice) AS TotalAmount,
       AVG(f.Quantity / DimLocation_History.NumOfCustomers) AS AverageProductsPerCapita
FROM FactProductSales AS f
     /* find corresponding record in SCD history in last 6 months, based on matching fact */
     INNER JOIN DimLocation FOR SYSTEM_TIME BETWEEN @sixMonthsAgo AND @now AS DimLocation_History
         ON DimLocation_History.LocationId = f.LocationId
        AND f.FactDate BETWEEN DimLocation_History.ValidFrom AND DimLocation_History.ValidTo
     /* find corresponding record in SCD history in last 6 months, based on matching fact */
     INNER JOIN DimProduct FOR SYSTEM_TIME BETWEEN @sixMonthsAgo AND @now AS DimProduct_History
         ON DimProduct_History.ProductId = f.ProductId
        AND f.FactDate BETWEEN DimProduct_History.ValidFrom AND DimProduct_History.ValidTo
WHERE f.FactDate BETWEEN @sixMonthsAgo AND @now
GROUP BY DimProduct_History.ProductId, DimLocation_History.LocationId;

Considerations

如果基于数据库事务时间计算的有效期适合你的业务逻辑,使用系统版本化的时序表用于SCD是可以接受的。 如果加载数据延迟较大,交易时间可能无法接受。

默认情况下,系统版本控制时态表不允许在加载后更改历史记录数据(将 SYSTEM_VERSIONING 设置为 OFF 后可以修改历史记录)。 在频繁更改历史数据的情况下,这可能导致功能受限。

只要列发生更改,系统版本控制型时态表就会生成行版本。 如果你想在某一列变化时抑制新版本,就需要把这个限制纳入ETL逻辑。

如果你预计SCD表中有大量历史行,可以考虑使用聚类列存储索引作为历史表的主要存储选项。 使用列存储索引可减少历史记录表占用情况,并加快分析查询的速度。

修复行级数据损坏

你可以利用系统版本控制时态表中的历史数据,快速将单个行还原到之前捕获的任意状态。 如果能够找到受影响的行,并且/或者知道在何时进行了不需要的数据更改,则时态表的这种属性会很有用。 借助此知识,无需处理备份,即可高效执行修复。

此方法有多种优点:

  • 你可以精确控制修复范围。 不受影响的记录需保持最新状态,这通常是一项很关键的要求。

  • 操作很高效,数据库会保持联机状态,以便所有工作负荷使用数据。

  • 修复操作本身也有版本。 你有维修作业的审计记录,如果需要,可以分析后续情况。

你可以相对轻松地自动化修复操作。 以下代码示例展示了一个存储过程,该存储过程对用于数据审计场景的表 Employee 执行数据修复。

DROP PROCEDURE IF EXISTS sp_RepairEmployeeRecord;
GO

CREATE PROCEDURE sp_RepairEmployeeRecord (
    @EmployeeID INT,
    @versionNumber INT = 1
)
AS
WITH History
AS (
    /* Order historical rows by their age in DESC order*/
    SELECT ROW_NUMBER() OVER (PARTITION BY EmployeeID
        ORDER BY [ValidTo] DESC) AS RN,
        *
    FROM Employee FOR SYSTEM_TIME ALL
    WHERE YEAR(ValidTo) < 9999
          AND Employee.EmployeeID = @EmployeeID)

/* Update current row using N-th row version from history
(default is 1, that is, the last version) */
UPDATE Employee
    SET [Position] = h.[Position],
        [Department] = h.Department,
        [Address] = h.[Address],
        AnnualSalary = h.AnnualSalary
FROM Employee AS e
     INNER JOIN History AS h
         ON e.EmployeeID = h.EmployeeID
        AND RN = @versionNumber
WHERE e.EmployeeID = @EmployeeID;

此存储过程采用 @EmployeeID@versionNumber 作为输入参数。 它默认会将行状态恢复到历史@versionNumber = 1记录中的最后版本()。

下图显示了程序调用前后该行的状态。 红色矩形标记当前错误的行版本,绿色矩形标记历史中正确的版本。

屏幕截图,其中显示了过程调用前后的行状态。

EXECUTE sp_RepairEmployeeRecord
    @EmployeeID = 1,
    @versionNumber = 1;

屏幕截图显示已更正的行。

可以将该修复存储过程定义为接受具体的时间戳而非行版本。 此操作会将该行还原到在所提供时间点(即 AS OF 时间点)处于活动状态的版本。

DROP PROCEDURE IF EXISTS sp_RepairEmployeeRecordAsOf;
GO

CREATE PROCEDURE sp_RepairEmployeeRecordAsOf (
    @EmployeeID INT,
    @asOf DATETIME2
)
AS
/* Update current row to the state that was actual AS OF provided date*/
UPDATE Employee
    SET [Position] = History.[Position],
        [Department] = History.Department,
        [Address] = History.[Address],
        AnnualSalary = History.AnnualSalary
FROM Employee AS e
     INNER JOIN Employee FOR SYSTEM_TIME AS OF @asOf AS History
         ON e.EmployeeID = History.EmployeeID
WHERE e.EmployeeID = @EmployeeID;

对于同一数据样本,下图显示的是带有时间条件的修复方案。 高亮显示参数 @asOf 、历史中在给定时间点实际存在的所选行,以及修复操作后当前表格中新行版本:

显示带有时间条件的修复方案的屏幕截图。

在数据仓库和报表系统中自动加载数据时,可能会进行数据更正。 如果新近更新的值不正确,那么在很多情况下,从历史记录中恢复先前版本就是足够有效的缓解措施。 下图显示了自动执行此过程的方法:

关系图显示如何自动执行过程。