如何使用 SQL 服务器临时 table 识别更改的值?

How to identify changed values using a SQL Server temporal table?

我有一个 SQL Azure table 并且我已经打开了新的临时 Table 功能(SQL Server 2016 和 SQL Azure 的新增功能v12).此功能创建另一个 table 来跟踪对主要 table 的所有更改(我在问题底部的有关时间 tables 的文档中包含了 link)。您可以使用特殊的查询语言来获取此历史记录。 请注意以下查询中的 FOR SYSTEM_TIME ALL

SELECT 
    ValidFrom
    , ValidTo
    , ShiftId
    , TradeDate
    , StatusID
    , [LastActionDate]
    , [OwnerUserID]
    , [WorkerUserID]
    , [WorkerEmail]
    , [Archived]
FROM [KrisisShifts_ShiftTrade] 
FOR SYSTEM_TIME ALL
WHERE [ShiftID] = 27
ORDER BY ValidTo Desc

结果集如下所示:

ValidFrom                   ValidTo                     ShiftId     TradeDate  StatusID    LastActionDate          OwnerUserID WorkerUserID WorkerEmail                                        Archived
--------------------------- --------------------------- ----------- ---------- ----------- ----------------------- ----------- ------------ -------------------------------------------------- --------
2017-06-21 00:26:44.51      9999-12-31 23:59:59.99      27          2017-01-27 3           2017-01-09 16:23:39.760 45          34           test@hotmail.com                                   1
2017-06-21 00:19:35.57      2017-06-21 00:26:44.51      27          2017-01-27 2           2017-01-09 16:23:39.760 45          34           test@hotmail.com                                   1
2017-06-21 00:19:16.25      2017-06-21 00:19:35.57      27          2017-01-28 3           2017-01-09 16:23:39.760 45          34           test@hotmail.com                                   1

使用 SYSTEM_TIME FOR ALL 临时 Table returns 来自主要 table 的当前记录,这是第一个,其余记录是存储在跟踪 table 中的该记录的先前版本。 (可以看到validFrom和ValidTo这两个列,很明显这个记录的时间就是当前记录)这样的话,保存历史记录的trackingtable就叫做KrisisShifts_ShiftTrade_History

我想要什么:

我想构建一个查询,只突出显示在每个历史点所做的更改。 请注意,第二条记录具有不同的 StatusID,第三条记录具有不同的 TradeDate

我想生成如下结果集(我想我会忽略第一条或当前记录,因为它显然没有被更改):

期望的结果

ShiftId      Column          Value             ValidFrom                   ValidTo
----------  -------------  ------------------- --------------------------- --------------------------
27          StatusId       2                   2017-06-21 00:19:35.57      2017-06-21 00:26:44.51
27          TradeDate      2017-01-28          2017-06-21 00:19:35.57      2017-06-21 00:26:44.51   

我不确定如何完成此操作。或者我愿意接受另一种解决方案。我希望能够快速查看每条记录与原始记录相比的变化。

我试图逆透视结果来比较它们,但我无法让它工作,因为每一行的班次 ID 都相同。我很想在这里展示更多的作品,但我真的被卡住了。

编辑 1:

我已经能够使用 lag() 隔离以下查询中仅一列的更改。我可以将此查询与我要跟踪的每一列的类似查询结合起来,但是,这是很多工作,必须为每个 table 构建。有没有办法动态执行此操作以便自动检测列?

StatusID变化历史查询:(我把记录隔离到一个shiftId为27只是为了测试)

SELECT 'SHIFT STATUS'  as ColumnName, t1.RecVersion, t1.ShiftID, t1.ValidFrom, t1.ValidTo, t1.StatusId
, (SELECT [Title] FROM [dbo].[KrisisShifts_Status] WHERE [dbo].[KrisisShifts_Status].[StatusID] = t1.StatusId) AS RecStatus
FROM
    (SELECT TOP 100 PERCENT 
        ROW_NUMBER() OVER(PARTITION BY ShiftId ORDER BY ValidTo ASC) AS RecVersion -- reverse sorting the ValidTo date gives "version count" to column changes
        , t2.ValidTo
        , t2.ValidFrom
        , t2.ShiftID
        , t2.StatusId
        , LAG(StatusId,1,0) OVER (ORDER BY ValidTo DESC) AS PrevStatusId
    FROM [KrisisShifts_ShiftTrade] 
    FOR SYSTEM_TIME ALL AS t2

    ORDER BY t2.ValidTo Desc
    ) AS t1
WHERE
    (t1.StatusId <> t1.PrevStatusId)
    AND
    SHIFTID = 27
ORDER BY t1.ValidTo DESC

查询结果:

ColumnName   RecVersion           ShiftID     ValidFrom                   ValidTo                     StatusId    RecStatus
------------ -------------------- ----------- --------------------------- --------------------------- ----------- --------------------------------------------------
SHIFT STATUS 3                    27          2017-06-21 00:26:44.51      2017-06-25 14:09:32.37      3           Confirmed
SHIFT STATUS 2                    27          2017-06-21 00:19:35.57      2017-06-21 00:26:44.51      2           Reserved
SHIFT STATUS 1                    27          2017-06-21 00:19:16.25      2017-06-21 00:19:35.57      3           Confirmed

结束编辑 1:

问题:

谁能帮我从时态 table 结果集中每个 shiftId 的前一条记录中分离列中更改的数据?

提前致谢

编辑 # 2:

以下是我想要从此 table 中 "watch for changes" 的所有列的列表:

[贸易日期] [状态ID] [最后行动日期] [允许的等级 ID] [所有者用户 ID] [所有者邮箱] [所有者位置 ID] [OwnerRankID] [所有者雇员 ID] [工人用户 ID] [工作邮箱] [WorkerLocationID] [WorkerRankID] [WorkerPlatoonID] [工人雇员 ID] [IsPartialShift] [细节] [LastModifiedByUserID] [存档] [更新日期]

结束编辑 2:

新标签注意事项:

我为临时 table 创建了一个新标签,因为没有。如果有更多声誉的人想将其添加到标签的详细信息,下面有它们的描述。

MS Docs on Temporal Tables

您有多少存储空间space?

上次我做这样的事情时,我们在单独的更改日志中为每个更改的列插入了新行 table。我们使用客户端逻辑来完成它,但您可以使用触发器获得相同的效果。

这会占用大量空间并减慢您的写入速度,但它确实可以让您快速读取更改日志。

P.S。我们没有通用的解决方案,所以我们只为需要 UI 支持的 table 解决了这个问题。其他一切都使用伪时间 tables。 (旧版本 SQL 服务器。)

方法

建议使用一个存储过程,该过程使用游标遍历行并在临时 table 中构建结果。 (由于这里有可管理的列数,我建议手动比较每个列值,而不是尝试动态比较,因为后者会更复杂。)

演示

Rextester 演示:http://rextester.com/EEELN72555

存储过程SQL

CREATE PROCEDURE GetChanges(@RequestedShiftID INT)
AS
BEGIN
    DECLARE @ValidFrom DATETIME, @ValidTo DATETIME, @TradeDate DATETIME; 
    DECLARE @PrevTradeDate DATETIME, @LastActionDate DATETIME;
    DECLARE @PrevLastActionDate DATETIME;
    DECLARE @ShiftId INT, @StatusID INT, @PrevStatusID INT, @OwnerUserID INT;
    DECLARE @PrevOwnerUserID INT, @WorkerUserID INT, @PrevWorkerUserID INT;
    DECLARE @Archived INT, @PrevArchived INT;
    DECLARE @WorkerEmail VARCHAR(MAX), @PrevWorkerEmail VARCHAR(MAX);

    CREATE TABLE #Results (Id INT NOT NULL IDENTITY (1,1) PRIMARY KEY, ShiftId INT,
                           [Column] VARCHAR(255), Value VARCHAR(MAX), 
                           ValidFrom DATETIME, ValidTo DATETIME);

    DECLARE cur CURSOR FOR
    SELECT 
        ValidFrom
        , ValidTo
        , ShiftId
        , TradeDate
        , StatusID
        , [LastActionDate]
        , [OwnerUserID]
        , [WorkerUserID]
        , [WorkerEmail]
        , [Archived]
    FROM [KrisisShifts_ShiftTrade]
    FOR SYSTEM_TIME ALL
    WHERE [ShiftID] = @RequestedShiftID
    ORDER BY ValidTo Desc;

    OPEN cur;
    FETCH NEXT FROM cur INTO
        @ValidFrom
        , @ValidTo
        , @ShiftId
        , @TradeDate
        , @StatusID
        , @LastActionDate
        , @OwnerUserID
        , @WorkerUserID
        , @WorkerEmail
        , @Archived;

    WHILE @@FETCH_STATUS = 0
    BEGIN
       SET @PrevTradeDate = @TradeDate;
       SET @PrevStatusID = @StatusID;
       SET @PrevLastActionDate = @LastActionDate;
       SET @PrevOwnerUserID = @OwnerUserID;
       SET @PrevWorkerUserID = @WorkerUserID;
       SET @PrevWorkerEmail = @WorkerEmail;
       SET @PrevArchived = @Archived;

       FETCH NEXT FROM cur INTO
            @ValidFrom
            , @ValidTo
            , @ShiftId
            , @TradeDate
            , @StatusID
            , @LastActionDate
            , @OwnerUserID
            , @WorkerUserID
            , @WorkerEmail
            , @Archived;

       IF @TradeDate <> @PrevTradeDate
           INSERT INTO #Results (ShiftId, [Column], Value, ValidFrom, ValidTo)
           VALUES (@ShiftId, 'TradeDate', @TradeDate, @ValidFrom, @ValidTo);
       IF @StatusID <> @PrevStatusID
           INSERT INTO #Results (ShiftId, [Column], Value, ValidFrom, ValidTo)
           VALUES (@ShiftId, 'StatusID', @StatusID, @ValidFrom, @ValidTo);
       IF @LastActionDate <> @PrevLastActionDate
           INSERT INTO #Results (ShiftId, [Column], Value, ValidFrom, ValidTo)
           VALUES (@ShiftId, 'LastActionDate', @LastActionDate, @ValidFrom, @ValidTo);
       IF @OwnerUserID <> @PrevOwnerUserID
           INSERT INTO #Results (ShiftId, [Column], Value, ValidFrom, ValidTo)
           VALUES (@ShiftId, 'OwnerUserID', @OwnerUserID, @ValidFrom, @ValidTo);
       IF @WorkerUserID <> @PrevWorkerUserID
           INSERT INTO #Results (ShiftId, [Column], Value, ValidFrom, ValidTo)
           VALUES (@ShiftId, 'WorkerUserID', @WorkerUserID, @ValidFrom, @ValidTo);
       IF @WorkerEmail <> @PrevWorkerEmail
           INSERT INTO #Results (ShiftId, [Column], Value, ValidFrom, ValidTo)
           VALUES (@ShiftId, 'WorkerEmail', @WorkerEmail, @ValidFrom, @ValidTo);
       IF @Archived <> @PrevArchived
           INSERT INTO #Results (ShiftId, [Column], Value, ValidFrom, ValidTo)
           VALUES (@ShiftId, 'WorkerEmail', @WorkerEmail, @ValidFrom, @ValidTo);
    END   

    CLOSE cur;
    DEALLOCATE cur;

    SELECT ShiftId, [Column], Value, ValidFrom, ValidTo
    FROM #Results
    ORDER BY Id
END;

注意:以上仅包括问题示例中的列。在最近的编辑中可以更改的列列表比这更宽,但当然可以以相同的方式添加其他列。

-- 很有意思的问题。

-- 考虑您想要的结果 - "Value" 列应包含不同类型的值(整数、十进制、日期、二进制、varchar 等)。因此,您需要将值转换为 varchar,或使用 sqlvariant,或二进制。然后在某些时候您将需要识别值的类型并对不同的行进行不同的处理

-- 要获取值,您可以尝试使用 UNPIVOT:

SELECT someRowID, ValidTo, ValidFrom, col, val
FROM 
    (SELECT someRowID, ValidTo, ValidFrom /*, ... */,
            [TradeDate], [StatusID], [LastActionDate], [AllowedRankID], [OwnerUserID], [OwnerEmail], [OwnerLocationID], [OwnerRankID], [OwnerEmployeeID], [WorkerUserID], [WorkerEmail], [WorkerLocationID], [WorkerRankID], [WorkerPlatoonID], [WorkerEmployeeID], [IsPartialShift], [Detail], [LastModifiedByUserID], [Archived], [UpdatedDate]
     FROM ... ) AS p
UNPIVOT    
    (val FOR col IN ([TradeDate], [StatusID], [LastActionDate], [AllowedRankID], [OwnerUserID], [OwnerEmail], [OwnerLocationID], [OwnerRankID], [OwnerEmployeeID], [WorkerUserID], [WorkerEmail], [WorkerLocationID], [WorkerRankID], [WorkerPlatoonID], [WorkerEmployeeID], [IsPartialShift], [Detail], [LastModifiedByUserID], [Archived], [UpdatedDate])
) AS unpvt

然后类似 UNPIVOT 以前的值

... 并将结果合并为

SELECT ...
FROM prevVals
INNER JOIN vals 
        ON vals.someRowID = prevVals.someRowID 
       AND vals.col = prevVals.col
WHERE vals.val <> prevVals.val        -- yes, I know here can be a problem (NULLs, types)

这只是一个想法,希望对您有所帮助

这肯定不是性能最好的方式,但符合要求

Is there a way to do this dynamically so it detects the columns automatically?

Demo

WITH k
     AS (SELECT *,
                ROW_NUMBER() OVER (PARTITION BY ShiftId ORDER BY ValidFrom) AS _RN
         FROM   KrisisShifts_ShiftTrade
        /*FOR SYSTEM_TIME ALL*/
        ),
     T
     AS (SELECT k.*,
                _colname = n.n.value('local-name(.)[1]', 'sysname'),
                _colvalue = n.n.value('text()[1]', 'nvarchar(4000)')
         FROM   k
                CROSS apply (SELECT (SELECT k.*
                                     FOR xml path('row'), elements xsinil, type)) ca(x)
                CROSS APPLY x.nodes('/row/*[not(self::_RN or self::ValidFrom or self::ValidTo)]') n(n))
SELECT T.ShiftId,
       T._colname  AS [Column],
       T._colvalue AS value,
       t.ValidFrom,
       T.ValidTo
FROM   T T
       INNER JOIN T Tnext
         ON Tnext._RN = T._RN + 1
            AND T.ShiftId = Tnext.ShiftId
            AND T._colname = Tnext._colname
WHERE  EXISTS(SELECT T._colvalue
              EXCEPT
              SELECT Tnext._colvalue)
ORDER  BY ShiftId,
          [Column],
          ValidFrom;

尽量不要使用时间 Table 功能:)。尝试使用触发器来检查更改 - 它更容易、更短。

使用时间戳和 dml 类型列创建 table 的图像(row_id、s__dml_dt、s__dml_type + 来自源 table 的所有列)对于所有 dml 类型 (i,u,d)。

create trigger dbo.KrisisShifts_ShiftTrade on dbo.KrisisShifts_ShiftTrade
after insert as
begin
     insert into dbo.KrisisShifts_ShiftTrade_logtable
     select getdate() s__dml_dt, 'i' s__dml_type, * from inserted
     -- for udpate select getdate() s__dml_dt, 'i' s__dml_type, * from inserted
     -- for delete select getdate() s__dml_dt, 'd' s__dml_type, * from deleted
end

现在 insert/delete/update 之后,您可以查看所有历史值。如果您想要透视结果,您可以轻松地为 dbo.KrisisShifts_ShiftTrade_logtable.

创建带有透视的视图

用于在数据库中记录所有 table 的脚本(它将创建带有前缀 r_ 的 table)。

declare @table sysname
declare @nl varchar(2)
declare @create_table int
declare @cmd varchar(max)
declare @trgname sysname
declare c_tables cursor for
    select table_name,
             case
                when exists (
                  select 2
                     from information_schema.tables
                    where table_name = 'r_'+ot.table_name
                  ) then 0
                else 1
             end create_table
      from information_schema.tables ot 
     where table_type = 'BASE TABLE'
        and table_name not like 'r[_]%'
        --and table_name like @tblfilter

open c_tables
fetch next from c_tables into @table,@create_table
while @@fetch_status=0
begin
   -- logovaci tabulka
    if @create_table=1
    begin
        set @cmd = 'create table r_'+@table+'(s__row_id int not null identity(1,1),s__dml_dt datetime not null,s__dml_type char(1) not null'
        select @cmd = @cmd + char(13)+char(10)+','+column_name+' '+data_type+isnull('('+case when character_maximum_length<0 then 'max' else cast(character_maximum_length as varchar) end+')','')+' null' from information_schema.columns where table_name=@table order by ordinal_position
        set @cmd = @cmd + ')'
        exec(@cmd)

        exec('create index i_s__dml_dt on r_'+@table+' (s__dml_dt)')
    end

    -- delete trigger
    set @trgname = 'trg_'+@table+'_dl_del'
    if object_id(@trgname) is not null exec('drop trigger '+@trgname)
    exec('
        create trigger '+@trgname+' on '+@table+' after delete as
        begin
          insert into r_'+@table+' select getdate(),''d'',t.* from deleted t
        end
    ')
    -- insert trigger
    set @trgname = 'trg_'+@table+'_dl_ins'
    if object_id(@trgname) is not null exec('drop trigger '+@trgname)
    exec('
        create trigger '+@trgname+' on '+@table+' after insert as
        begin
          insert into r_'+@table+' select getdate(),''i'',t.* from inserted t
        end
    ')
    -- update trigger
    set @trgname = 'trg_'+@table+'_dl_upd'
    if object_id(@trgname) is not null exec('drop trigger '+@trgname)
    exec('
        create trigger '+@trgname+' on '+@table+' after update as
        begin
          insert into r_'+@table+' select getdate(),''u'',t.* from deleted t
        end
    ')


    fetch next from c_tables into @table,@create_table
end
close c_tables
deallocate c_tables

你也可以用CROSS APPLYUNPIVOT

应该注意 ValidFromValidTo 指的是行版本本身的有效性,不一定是列值。我相信这就是您的要求,但这可能会造成混淆。

Demo

WITH T
     AS (SELECT ValidFrom,
                ValidTo,
                ShiftId,
                TradeDate,
                StatusID,
                LastActionDate,
                OwnerUserID,
                WorkerUserID,
                WorkerEmail,
                Archived,
                nextTradeDate = LEAD(TradeDate) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextStatusID = LEAD(StatusID) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextLastActionDate = LEAD(LastActionDate) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextOwnerUserID = LEAD(OwnerUserID) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextWorkerUserID = LEAD(WorkerUserID) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextWorkerEmail = LEAD(WorkerEmail) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextArchived = LEAD(Archived) OVER (PARTITION BY ShiftId ORDER BY ValidFrom)
         FROM   KrisisShifts_ShiftTrade)
SELECT ShiftId,
       Colname AS [Column],
       value,
       ValidFrom,
       ValidTo
FROM   T
       CROSS APPLY ( VALUES 
                    ('TradeDate', CAST(TradeDate AS NVARCHAR(4000)), CAST(nextTradeDate AS NVARCHAR(4000))),
                    ('StatusID', CAST(StatusID AS NVARCHAR(4000)), CAST(nextStatusID AS NVARCHAR(4000))),
                    ('LastActionDate', CAST(LastActionDate AS NVARCHAR(4000)), CAST(nextLastActionDate AS NVARCHAR(4000))),
                    ('OwnerUserID', CAST(OwnerUserID AS NVARCHAR(4000)), CAST(nextOwnerUserID AS NVARCHAR(4000))),
                    ('WorkerUserID', CAST(WorkerUserID AS NVARCHAR(4000)), CAST(nextWorkerUserID AS NVARCHAR(4000))),
                    ('WorkerEmail', CAST(WorkerEmail AS NVARCHAR(4000)), CAST(nextWorkerEmail AS NVARCHAR(4000))),
                    ('Archived', CAST(Archived AS NVARCHAR(4000)), CAST(nextArchived AS NVARCHAR(4000)))
                   ) CA(Colname, value, nextvalue)
WHERE  EXISTS(SELECT value
              EXCEPT
              SELECT nextvalue)
       AND ValidTo <> '9999-12-31 23:59:59'
ORDER  BY ShiftId,
          [Column],
          ValidFrom;

如果您确实想要列级别的有效性,您可以使用 (Demo)

WITH T1 AS
(
SELECT *, 
       ROW_NUMBER() OVER (PARTITION BY ShiftId, colname ORDER BY ValidFrom)  
       - ROW_NUMBER() OVER (PARTITION BY ShiftId, colname, Colvalue ORDER BY ValidFrom)  AS Grp,
       IIF(DENSE_RANK() OVER (PARTITION BY ShiftId, colname ORDER BY Colvalue) + 
        DENSE_RANK() OVER (PARTITION BY ShiftId, colname ORDER BY Colvalue DESC) = 2, 0,1) AS HasChanges
FROM KrisisShifts_ShiftTrade
       CROSS APPLY ( VALUES 
                    ('TradeDate', CAST(TradeDate AS NVARCHAR(4000))),
                    ('StatusID', CAST(StatusID AS NVARCHAR(4000))),
                    ('LastActionDate', CAST(LastActionDate AS NVARCHAR(4000))),
                    ('OwnerUserID', CAST(OwnerUserID AS NVARCHAR(4000))),
                    ('WorkerUserID', CAST(WorkerUserID AS NVARCHAR(4000))),
                    ('WorkerEmail', CAST(WorkerEmail AS NVARCHAR(4000))),
                    ('Archived', CAST(Archived AS NVARCHAR(4000)))
                   ) CA(Colname, Colvalue)
)
SELECT  ShiftId, colname, Colvalue, MIN(ValidFrom) AS ValidFrom, MAX(ValidTo) AS ValidTo
FROM T1
WHERE HasChanges = 1
GROUP BY ShiftId, colname, Colvalue, Grp
ORDER  BY ShiftId,
          colname,
          ValidFrom;

关于@Martin Smith“WITH T”解决方案(2017 年 7 月 1 日在 19:31 回答),没有足够的测试数据。 我们可以在 2017-06-21 00:22:22(在 (StatusID = 2) 的现有范围中间)修改测试数据以更新 OwnerUserID(从 55 到 45):

VALUES
('2017-06-21 00:26:44', '9999-12-31 23:59:59', 27, '2017-01-27', 3, '2017-01-09 16:23:39.760',45, 34, 'test@hotmail.com', 1),
('2017-06-21 00:22:22', '2017-06-21 00:26:44', 27, '2017-01-27', 2, '2017-01-09 16:23:39.760',45, 34, 'test@hotmail.com', 1),
('2017-06-21 00:19:35', '2017-06-21 00:22:22', 27, '2017-01-27', 2, '2017-01-09 16:23:39.760',55, 34, 'test@hotmail.com', 1),
('2017-06-21 00:19:16', '2017-06-21 00:19:35', 27, '2017-01-28', 3, '2017-01-09 16:23:39.760',55, 34, 'test@hotmail.com', 1)

则结果为:

ShiftId     Column         value       ValidFrom                   ValidTo
----------- -------------- ----------- --------------------------- ---------------------------
27          OwnerUserID    55          2017-06-21 00:19:35.0000000 2017-06-21 00:22:22.0000000
27          StatusID       3           2017-06-21 00:19:16.0000000 2017-06-21 00:19:35.0000000
27          StatusID       2           2017-06-21 00:22:22.0000000 2017-06-21 00:26:44.0000000
27          TradeDate      2017-01-28  2017-06-21 00:19:16.0000000 2017-06-21 00:19:35.0000000

结果显示 (StatusID = 2) 的范围不正确。 ValidFrom 日期应为 2017-06-21 00:19:35。 错误来自从与 ValidTo 相同的行中提取 ValidFrom 的查询。

这是我对 Martin 富有洞察力的开始的改进。 它仅通过使用 ValidFrom 来工作。它报告每个值的开始时间。 我们真的不需要显示 ValidTo,因为它只是下一行的 ValidFrom。

USE tempdb
;
DROP TABLE IF EXISTS KrisisShifts_ShiftTrade
;
CREATE TABLE KrisisShifts_ShiftTrade
  (
     [ValidFrom]      DATETIME2,
     [ValidTo]        DATETIME2,
     [ShiftId]        INT,
     [TradeDate]      DATE,
     [StatusID]       INT,
     [LastActionDate] DATETIME2,
     [OwnerUserID]    INT,
     [WorkerUserID]   INT,
     [WorkerEmail]    VARCHAR(16),
     [Archived]       INT
  ); 

INSERT INTO KrisisShifts_ShiftTrade
    ([ValidFrom], [ValidTo], [ShiftId], [TradeDate], [StatusID], [LastActionDate], [OwnerUserID],[WorkerUserID],[WorkerEmail], [Archived])
VALUES
    ('2017-06-21 00:26:44', '9999-12-31 23:59:59', 27, '2017-01-27', 3, '2017-01-09 16:23:39.760',45, 34, 'test@hotmail.com', 1),
    ('2017-06-21 00:22:22', '2017-06-21 00:26:44', 27, '2017-01-27', 2, '2017-01-09 16:23:39.760',45, 34, 'test@hotmail.com', 1),
    ('2017-06-21 00:19:35', '2017-06-21 00:22:22', 27, '2017-01-27', 2, '2017-01-09 16:23:39.760',55, 34, 'test@hotmail.com', 1),
    ('2017-06-21 00:19:16', '2017-06-21 00:19:35', 27, '2017-01-28', 3, '2017-01-09 16:23:39.760',55, 34, 'test@hotmail.com', 1)
;

WITH T
     AS (SELECT ValidFrom,
                ShiftId,
                TradeDate,
                StatusID,
                LastActionDate,
                OwnerUserID,
                WorkerUserID,
                WorkerEmail,
                Archived,
                nextTradeDate = LAG(TradeDate) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextStatusID = LAG(StatusID) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextLastActionDate = LAG(LastActionDate) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextOwnerUserID = LAG(OwnerUserID) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextWorkerUserID = LAG(WorkerUserID) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextWorkerEmail = LAG(WorkerEmail) OVER (PARTITION BY ShiftId ORDER BY ValidFrom),
                nextArchived = LAG(Archived) OVER (PARTITION BY ShiftId ORDER BY ValidFrom)
         FROM   KrisisShifts_ShiftTrade)
SELECT ShiftId,
       Colname AS [Column],
       value,
       ValidFrom
FROM   T
       CROSS APPLY ( VALUES 
                    ('TradeDate', CAST(TradeDate AS NVARCHAR(4000)), CAST(nextTradeDate AS NVARCHAR(4000))),
                    ('StatusID', CAST(StatusID AS NVARCHAR(4000)), CAST(nextStatusID AS NVARCHAR(4000))),
                    ('LastActionDate', CAST(LastActionDate AS NVARCHAR(4000)), CAST(nextLastActionDate AS NVARCHAR(4000))),
                    ('OwnerUserID', CAST(OwnerUserID AS NVARCHAR(4000)), CAST(nextOwnerUserID AS NVARCHAR(4000))),
                    ('WorkerUserID', CAST(WorkerUserID AS NVARCHAR(4000)), CAST(nextWorkerUserID AS NVARCHAR(4000))),
                    ('WorkerEmail', CAST(WorkerEmail AS NVARCHAR(4000)), CAST(nextWorkerEmail AS NVARCHAR(4000))),
                    ('Archived', CAST(Archived AS NVARCHAR(4000)), CAST(nextArchived AS NVARCHAR(4000)))
                   ) CA(Colname, value, nextvalue)
WHERE  EXISTS(SELECT value
              EXCEPT
              SELECT nextvalue)
ORDER  BY ShiftId,
          [Column],
          ValidFrom 
;

这确实包括初始值和当前值(无论好坏)。 每列都有一行显示相同的初始 ValidFrom - 2017-06-21 00:19:16, 每列的最后一行显示当前值。

ShiftId     Column         value                ValidFrom
----------- -------------- -------------------- -------------------
27          Archived       1                    2017-06-21 00:19:16
27          LastActionDate 2017-01-09 16:23:39  2017-06-21 00:19:16
27          OwnerUserID    55                   2017-06-21 00:19:16
27          OwnerUserID    45                   2017-06-21 00:22:22
27          StatusID       3                    2017-06-21 00:19:16
27          StatusID       2                    2017-06-21 00:19:35
27          StatusID       3                    2017-06-21 00:26:44
27          TradeDate      2017-01-28           2017-06-21 00:19:16
27          TradeDate      2017-01-27           2017-06-21 00:19:35
27          WorkerEmail    test@hotmail.com     2017-06-21 00:19:16
27          WorkerUserID   34                   2017-06-21 00:19:16

但重要的是,它确实正确地显示了 (StatusID = 2) 从 2017-06-21 00:19:35 开始并在 2017-06-21 00:26:44 被 (StatusID = 3) 取代]. 如果您确实需要同时查看 ValidFrom 和 ValidTo 列,您可以将上面的最终查询包装在 CTE 中,并使用带有 '9999-12-31 23:59:59.99' 的 LEAD 函数查询“默认”参数.

Edit:我刚刚意识到我的解决方案和 Martin 的解决方案没有正确处理主 table 行被删除然后稍后重新插入的情况。下面的测试数据表示 (ShiftId = 27) 在 2017-07-22 00:26:55 被删除并稍后在 2017-08-23 00:26:59 重新插入的情况。因此,(StatusID = 3) 在 2017-07-22 00:26:55 和 2017-08-23 00:26:59 之间不存在。一个合适的解决方案需要一个 ValidFrom 和一个 ValidTo 列,这样我们就可以为每一列有一行 ValidTo = 2017-07-22 00:26:55 与同一列的另一行匹配 ValidFrom = 2017- 08-2300:26:59这样我们就可以看到数据不存在的范围

VALUES
    ('2017-08-23 00:26:59', '9999-12-31 23:59:59', 27, '2017-01-27', 3, '2017-01-09 16:23:39.760',45, 34, 'test@hotmail.com', 1),
    ('2017-06-21 00:26:44', '2017-07-22 00:26:55', 27, '2017-01-27', 3, '2017-01-09 16:23:39.760',45, 34, 'test@hotmail.com', 1),
    ('2017-06-21 00:22:22', '2017-06-21 00:26:44', 27, '2017-01-27', 2, '2017-01-09 16:23:39.760',45, 34, 'test@hotmail.com', 1),
    ('2017-06-21 00:19:35', '2017-06-21 00:22:22', 27, '2017-01-27', 2, '2017-01-09 16:23:39.760',55, 34, 'test@hotmail.com', 1),
    ('2017-06-21 00:19:16', '2017-06-21 00:19:35', 27, '2017-01-28', 3, '2017-01-09 16:23:39.760',55, 34, 'test@hotmail.com', 1)