【问题标题】:Query Temporal Table and Combine Rows查询时态表并合并行
【发布时间】:2020-07-10 00:23:59
【问题描述】:

假设我有一个名为 ProductDetails 的临时表,使用以下查询返回一些历史数据。

SELECT * FROM ProductDetails
FOR system_time
BETWEEN '1900-01-01 00:00:00' AND '9999-12-31 00:00:00'
WHERE ProductID = 8


ID    ProductID(FK)    Attribute    Value    SysStartTime           SysEndTime
--    -------------    ---------    -----    -------------------    ----------
1     8                Size         S        2020-07-06 05:00:00    9999-12-31 23:59:59
2     8                Color        Blue     2020-07-06 05:00:01    2020-07-09 11:11:11
2     8                Color        Green    2020-07-09 11:11:11    9999-12-31 23:59:59

这意味着当 ID = 8 的产品在 2020 年 7 月 6 日 05:00:00 创建时,添加了 2 个属性,然后将其中一个记录编辑为从“蓝色”更改为“绿色”。请注意,保存时第二行的 SysStartTime 相差 1 秒。

现在我需要编写一个查询以获得以下结果。基本上,它是发生变化时不同时间快照中的属性值。时间已到分钟。

Start Time          End Time            Attributes Values
----------------    ----------------    -----------------
2020-07-06 05:00    2020-07-09 11:11    Size = S, Color = Blue
2020-07-09 11:11    NULL                Size = S, Color = Green

我怎样才能做到这一点?每个产品可能有不同的属性,但查询一次只针对一种产品。

【问题讨论】:

  • 能否提供更多的样本数据,±20 行?
  • 这是一种产品的真实数据。我们可以为这个样本解决它吗?

标签: sql-server tsql temporal-tables


【解决方案1】:

以下是一种在一个查询中格式化数据的解决方案。对于 4 行的小型数据集(我在您的示例中添加了一行),性能不是问题,但我猜这对于数百万条记录来说不会很快。

此处提供的解决方案以common table expressions (CTE) 的形式生成不同的数据集,并使用其他 StackOverflow 对remove the secondsconcatenate the row values 的回答中的一些技术。最后加上cross apply

可以按照与连续 CTE 的/连接对应的步骤来描述该方法:

  1. 为每个产品创建一组属性。
  2. 为每个产品创建一组周期开始时刻(省略秒数)。
  3. 将每个产品的属性与每个时期相结合,并寻找合适的值。
  4. 使用一些 XML 函数来格式化单行中的属性值。
  5. 使用cross apply 获取期末。

完整解决方案:

-- sample data
declare @data table
(
    ID              int,
    ProductId       int,
    Attribute       nvarchar(10),
    Value           nvarchar(10),
    SysStartTime    datetime2(0),
    SysEndTime      datetime2(0)
);

insert into @data (ID, ProductId, Attribute, Value, SysStartTime, SysEndTime) values
(1, 8, 'Size', 'S', '2020-07-06 05:00:00', '9999-12-31 23:59:59'),
(2, 8, 'Color', 'Blue', '2020-07-06 05:00:01', '2020-07-09 11:11:11'),
(2, 8, 'Color', 'Green', '2020-07-09 11:11:11', '9999-12-31 23:59:59'),
(2, 8, 'Weight', 'Light', '2020-07-10 10:11:12', '9999-12-31 23:59:59'); -- additional data to have extra attribute not available from start

-- solution
with prodAttrib as -- attributes per product
(
    select d.ProductId, d.Attribute
    from @data d
    group by d.ProductId, d.Attribute
),
prodPeriod as -- periods per product
(
    select  d.ProductId,
            dateadd(minute, datediff(minute, 0, d.SysStartTime), 0) as 'SysStartTimeNS' -- start time No Seconds
    from @data d
    group by ProductId, dateadd(minute, datediff(minute, 0, d.SysStartTime), 0)
),
prodResult as -- attribute value per period per product
(
    select  pp.ProductId,
            convert(nvarchar(16), pp.SysStartTimeNS, 120) as 'FromDateTime',
            convert(nvarchar(16), coalesce(pe.SysEndTime, '9999-12-31 23:59:59'), 120) as 'ToDateTime',
            pa.Attribute,
            av.Value
    from prodPeriod pp
    join prodAttrib pa
        on  pa.ProductId = pp.ProductId
    outer apply (   select top 1 d.Value
                    from @data d
                    where d.ProductId = pp.ProductId
                      and d.Attribute = pa.Attribute
                      and dateadd(minute, datediff(minute, 0, d.SysStartTime), 0) <= pp.SysStartTimeNS
                    order by d.SysStartTime desc ) av -- attribute values per product
    outer apply (   select top 1 dateadd(second, -1, d.SysStartTime) as 'SysEndTime'
                    from @data d
                    where d.ProductId = pp.ProductId
                      and dateadd(minute, datediff(minute, 0, d.SysStartTime), 0) > pp.SysStartTimeNS
                    order by d.SysStartTime ) pe -- period end
),
prodResultFormat as -- concatenate attribute values per period
(
    select  pp.ProductId,
            convert(nvarchar(16), pp.SysStartTimeNS, 120) as 'FromDateTime',
            (
                select pr.Attribute + ' = ' + coalesce(pr.Value,'') + ', ' as [text()]
                from prodResult pr
                where pr.ProductId = pp.ProductId
                  and pr.FromDateTime = convert(nvarchar(16), pp.SysStartTimeNS, 120)
                order by pr.Attribute
                for xml path('')
            ) as 'Attributes'
    from prodPeriod pp
)
select  prf.ProductId,
        prf.FromDateTime,
        x.ToDateTime,
        left(prf.Attributes, len(prf.Attributes)-1) as 'Attributes'
from prodResultFormat prf
cross apply (   select top 1 pr.ToDateTime
                from prodResult pr
                where pr.ProductId = prf.ProductId
                  and pr.FromDateTime = prf.FromDateTime ) x
order by prf.ProductId, prf.FromDateTime;

扩展示例数据的结果:

ProductId    FromDateTime      ToDateTime        Attributes
-----------  ----------------  ----------------  ----------------------------------------
8            2020-07-06 05:00  2020-07-09 11:11  Color = Blue, Size = S, Weight = 
8            2020-07-09 11:11  2020-07-10 10:11  Color = Green, Size = S, Weight = 
8            2020-07-10 10:11  9999-12-31 23:59  Color = Green, Size = S, Weight = Light

附:如果您真的需要NULL 值,请将x.EndDateTime 替换为case when x.ToDateTime = '9999-12-31 23:59' then NULL else x.ToDateTime end as 'ToDateTime'

【讨论】:

  • 感谢您的回答。第二行的 ToDateTime 应该是 2020-07-10 10:10,否则答案不正确。我希望看到查询临时表的功能,例如 FOR SYSTEM_TIME AS OF 等。我当前的解决方案涉及创建动态 SQL(在每个 AS OF 结果中合并 STRING_AGGed 属性快照)。
  • @Delphi.Boy 在 prodResult CTE 中使用第二个 OUTER APPLY 修复了最终结果。没有考虑 AS OF(需要在我的测试环境中设置临时表),但也许您可以将您的解决方案合并到我的解决方案中。前 2 个 CTE 提供 ProductId、Attribute 和 SysStartTimeNS,它们可以用作 CTE prodResult 中带有 AS OF 的新查询的 WHERE 子句参数。之后几乎不需要更改。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-07-15
  • 1970-01-01
  • 1970-01-01
  • 2015-01-31
  • 1970-01-01
  • 2013-11-27
相关资源
最近更新 更多