【问题标题】:How to identify changed values using a SQL Server temporal table?如何使用 SQL Server 时态表识别更改的值?
【发布时间】:2017-11-28 00:41:01
【问题描述】:

我有一个 SQL Azure 表,并且我打开了新的临时表功能(SQL Server 2016 和 SQL Azure v12 的新功能)。此功能创建另一个表来跟踪对主表的所有更改(我在问题的底部包含了指向有关时态表的文档的链接)。您可以使用特殊查询语言来获取此历史记录。 请注意以下查询中的 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临时表从主表返回当前记录,这是第一个,其余记录是存储在跟踪表中的该记录的先前版本。 (可以看到validFrom和ValidTo列,很明显时间记录就是当前记录)在这种情况下,保存历史记录的跟踪表称为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() 隔离以下查询中仅一列的更改。我可以将这个查询与我想要跟踪的每一列的类似查询联合起来,但是,这是很多工作并且必须为每个表构建。有没有办法动态执行此操作,以便自动检测列?

StatusID 更改历史查询:(我将记录隔离到 27 的 shiftId 仅用于测试)

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:

问题:

有人可以帮我将时态表结果集中每个 shiftId 的列中更改的数据与之前的记录隔离吗?

提前致谢

编辑#2:

以下是我想从这个表中“观察变化”的所有列的列表:

[交易日期] [状态ID] [最后行动日期] [AllowedRankID] [所有者用户 ID] [所有者电子邮件] [所有者位置 ID] [所有者等级 ID] [所有者雇员 ID] [工人用户 ID] [工人电子邮件] [工人位置 ID] [工人等级 ID] [工人排号] [工人雇员ID] [IsPartialShift] [细节] [LastModifiedByUserID] [存档] [更新日期]

结束编辑 2:

关于新标签的说明:

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

MS Docs on Temporal Tables

【问题讨论】:

  • shiftID 似乎永远不会改变,你确定你说的不是 statusID?
  • 我认为您只想使用 Lag() 并根据您声明的输出请求比较值。
  • ShiftId 是表的主键
  • 除非你想弄乱游标(请不要),我建议你做这个客户端。
  • 关系型 DBMS(例如 SQL Server)假定架构稳定。这意味着您必须在查询中显式写入列名。编写适用于任何表的通用代码的唯一方法是使用来自INFORMATION_SCHEMA 系统视图(或此类视图的非标准 SQL Server 特定版本)的元数据动态构建查询文本,然后执行它,比如说,sp_executesql

标签: sql sql-server tsql azure-sql-database temporal-tables


【解决方案1】:

关于@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 的查询。

这是我对马丁富有洞察力的开端的增强。 它仅通过使用 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 中,并使用 LEAD 函数和 '9999-12-31 23:59:59.99' 作为“默认”参数进行查询。

编辑:我刚刚意识到我的解决方案和 Martin 的解决方案不能正确处理删除主表行然后稍后重新插入的情况。下面的测试数据代表了一个案例,其中(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-23 00: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)

【讨论】:

  • 感谢为已经回答的问题增加价值!我得试试这个
【解决方案2】:

你有多少存储空间?

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

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

附:我们没有通用的解决方案,所以我们只针对需要 UI 支持的一张表进行了处理。其他一切都使用伪时态表。 (旧版本的 SQL Server。)

【讨论】:

  • 你好乔纳森,感谢您的回复,我希望在这里避免触发,因为新的临时表已经处理了记录的每个版本的跟踪更改(并且速度非常快)。我在这里想要的是一种查询历史版本的方法(通过提供的主键),并且只返回每个历史记录中更改的值。
  • 我也想要那个,但我不知道如何在不弄乱光标的情况下做到这一点。
【解决方案3】:

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

为所有 dml 类型 (i,u,d) 创建包含时间戳和 dml 类型列(row_id、s__dml_dt、s__dml_type + 源表中的所有列)的表图像。

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

现在插入/删除/更新后,您可以检查所有历史值。如果您想要透视结果,您可以使用 dbo.KrisisShifts_ShiftTrade_logtable 的透视轻松创建视图。

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

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

【讨论】:

  • 感谢您的反馈,但是问题是如何查询时态表,而不是是否使用它们。那将是另一个讨论,但由于它们直接内置于 SQL 服务器中,因此它们与任何触发器一样快,可以在任何给定时间捕获记录状态,并为时间点恢复记录提供简单的方法。此外,“更容易和更短”是相对的。我第一次在不到几分钟的时间内在我的数据库上设置了临时表。我只是想要一种不同的方式来查询表的结果。你知道不使用时态表的其他原因吗?
  • 当然 - 触发器对用户更友好,可以暂时禁用,您可以制作自己的架构,对以前版本的 sql server 友好,您可以仅过滤和记录选定的内容。使用触发器,您可以在几分钟内轻松记录数千个表而没有开销(CDC 每行添加至少 34 个字节),您可以在更改后检查引用完整性,添加业务逻辑等。但是对于一个表,这并不重要..
  • 很好,我要考虑的事情,但是,这个项目目前致力于临时表,不需要考虑以前版本的 SQL。感谢您的反馈
【解决方案4】:

您也可以使用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,经过一些测试,这最终满足了我的需求
【解决方案5】:

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

有没有办法动态执行此操作,以便检测列 自动?

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;

【讨论】:

  • 谢谢马丁!你的查询确实有效,你是对的,它没有很好的性能。它在 SQL Azure 上运行需要一分钟以上,我在其中提供了具有 20 个“更改”或临时记录的特定记录的 Shift Id。如果我要对列进行硬编码,我将如何做到这一点,性能改进可能值得为每个表构建它。
  • @MartinSmith,你是对的。我瞥见了关于for xml 的想法,但没有理会它并写了一个关于使用INFORMATION_SCHEMA 的通用声明。感谢您展示此方法。
【解决方案6】:

-- 非常有趣的问题。

--想想你想要的结果——“值”列应该包含不同类型的值(int、decimal、date、binary、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)

这只是一个想法,我希望它会有所帮助

【讨论】:

    【解决方案7】:

    方法

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

    演示

    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;
    

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

    【讨论】:

      猜你喜欢
      • 2017-05-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-29
      • 2023-03-28
      • 1970-01-01
      • 2015-12-02
      • 2011-04-14
      相关资源
      最近更新 更多