【问题标题】:How can I write a query to extract individual changes from snapshots of data?如何编写查询以从数据快照中提取单个更改?
【发布时间】:2011-12-06 04:36:36
【问题描述】:

我需要创建一个进程,该进程将从表中提取更改,其中每一行都是另一个表中一行的快照。现实世界的问题涉及到很多表,很多字段,但是作为一个简单的例子,假设我有以下快照数据:

Sequence    DateTaken      ID       Field1    Field2
--------    -----------    ----     ------    ------
       1    '2011-01-01'      1     'Red'          2
       2    '2011-01-01'      2     'Blue'        10
       3    '2011-02-01'      1     'Green'        2
       4    '2011-03-01'      1     'Green'        3
       5    '2011-03-01'      2     'Purple'       2
       6    '2011-04-01'      1     'Yellow'       2

SequenceDateTaken 字段与快照表本身直接相关。 ID 字段是源表的主键,Field1Field2 是同一(源)表中的其他字段。

我可以通过这样的查询获得部分解决方案:

WITH Snapshots (Sequence, DateTaken, ID, Field1, Field2, _Index)
AS
(
    SELECT Sequence, DateTaken, ID, Field1, Field2, ROW_NUMBER() OVER (ORDER BY ID, Sequence) _Index
    FROM #Snapshots
)
SELECT
      c.DateTaken, c.ID
    , c.Field1 Field1_Current, p.Field1 Field1_Previous, CASE WHEN c.Field1 = p.Field1 THEN 0 ELSE 1 END Field1_Changed
    , c.Field2 Field2_Current, p.Field2 Field2_Previous, CASE WHEN c.Field2 = p.Field2 THEN 0 ELSE 1 END Field2_Changed
FROM Snapshots c
JOIN Snapshots p ON p.ID = c.ID AND (p._Index + 1) = c._Index
ORDER BY c.Sequence DESC

上面的查询将识别从一个快照到下一个快照的变化,但它仍然不是我需要的形式。输出中的每一行都可能包含多个更改。在一天结束时,我需要每次更改一行来标识更改的字段及其先前/当前值。实际未更改的字段将需要从最终输出中排除。所以如果上面的查询输出是这样的:

DateTaken   ID  Field1_Current  Field1_Previous  Field1_Changed  Field2_Current  Field2_Previous  Field2_Changed
----------  --  --------------  ---------------  --------------  --------------  ---------------  --------------
2011-04-01  1   Yellow          Green            1               2               3                1
2011-02-01  1   Green           Red              1               2               2                0

我需要把它变成这样的东西:

DateTaken   ID  Field    Previous   Current
----------  --  -------  --------   ---------
2011-04-01  1   Field1   Green      Yellow
2011-04-01  1   Field2   3          2
2011-02-01  1   Field1   Red        Green

我认为我可以通过 UNPIVOT 实现这一目标,但我无法做到这一点。我认为任何涉及游标或类似的解决方案都是绝对的最后手段。

非常感谢您的建议。

【问题讨论】:

标签: sql sql-server sql-server-2005


【解决方案1】:

这是一个使用 UNPIVOT 的工作示例。这是基于我对问题Better way to Partially UNPIVOT in Pairs in SQL的回答@

这有一些不错的功能。

  1. 添加其他字段很容易。只需将值添加到 SELECT 和 UNPIVOT 子句。您不必添加额外的 UNION 子句

  2. 无论添加多少字段,where 子句 WHERE curr.value <> prev.value 都不会改变。

  3. 性能出奇的快。

  4. 如果您需要,它可以移植到当前版本的 Oracle


SQL

Declare @Snapshots as table(
Sequence int,
DateTaken      datetime,
[id] int,
field1 varchar(20),
field2 int)



INSERT INTO @Snapshots VALUES 

      (1,    '2011-01-01',      1,     'Red',          2),
      (2,    '2011-01-01',      2,     'Blue',        10),
      (3,    '2011-02-01',      1,     'Green',        2),
      (4,    '2011-03-01',      1,     'Green' ,       3),
      (5,    '2011-03-01',      2,     'Purple',       2),
      (6,    '2011-04-01',      1,     'Yellow',       2)

;WITH Snapshots (Sequence, DateTaken, ID, Field1, Field2, _Index)
AS
(
    SELECT Sequence, DateTaken, ID, Field1, Field2, ROW_NUMBER() OVER (ORDER BY ID, Sequence) _Index
    FROM @Snapshots
)
,  data as(
SELECT
     c._Index
    , c.DateTaken
    ,  c.ID
    , cast(c.Field1  as varchar(max)) Field1
    , cast(p.Field1  as varchar(max))Field1_Previous
    , cast(c.Field2   as varchar(max))Field2
    , cast(p.Field2  as varchar(max)) Field2_Previous 


FROM Snapshots c
JOIN Snapshots p ON p.ID = c.ID AND (p._Index + 1) = c._Index
)


, fieldsToRows 
     AS (SELECT DateTaken, 
                id,
                _Index,
                value,
                field

         FROM   data p UNPIVOT (value FOR field IN (field1, field1_previous, 
                                                        field2, field2_previous) ) 
                AS unpvt
        ) 
SELECT 
    curr.DateTaken,
    curr.ID,
    curr.field,
    prev.value previous,
    curr.value 'current'

FROM 
        fieldsToRows curr 
        INNER  JOIN  fieldsToRows prev
        ON curr.ID = prev.id
            AND curr._Index = prev._Index 
            AND curr.field + '_Previous' = prev.field
WHERE 
    curr.value <> prev.value

输出

DateTaken               ID          field     previous current
----------------------- ----------- --------- -------- -------
2011-02-01 00:00:00.000 1           Field1    Red      Green
2011-03-01 00:00:00.000 1           Field2    2        3
2011-04-01 00:00:00.000 1           Field1    Green    Yellow
2011-04-01 00:00:00.000 1           Field2    3        2
2011-03-01 00:00:00.000 2           Field1    Blue     Purple
2011-03-01 00:00:00.000 2           Field2    10       2

【讨论】:

    【解决方案2】:
    WITH Snapshots (Sequence, DateTaken, ID, Field, FieldValue, _Index) AS
    (
        SELECT
            Sequence,
            DateTaken,
            ID,
            'Field1' AS Field
            CAST(Field1 AS VARCHAR(100)) AS FieldValue,  -- Find an appropriate length
            ROW_NUMBER() OVER (ORDER BY ID, Sequence)
        FROM
            #Snapshots
        UNION ALL
        SELECT
            Sequence,
            DateTaken,
            ID,
            'Field2' AS Field
            CAST(Field2 AS VARCHAR(100)) AS FieldValue,  -- Find an appropriate length
            ROW_NUMBER() OVER (ORDER BY ID, Sequence)
        FROM
            #Snapshots
    )
    SELECT
        S1.DateTaken,
        S1.ID,
        S1.Field,
        S1.FieldValue AS Previous,
        S2.FieldValue As New   -- Not necessarily "Current"
    FROM
        Snapshots S1
    INNER JOIN Snapshots S2 ON
        S2.ID = S1.ID AND
        S2.Field = S1.Field AND
        S2._Index = S1._Index + 1 AND
        S2.FieldValue <> S1.FieldValue    -- Might need to handle NULL values
    

    【讨论】:

    • 这是一个非常好的解决方案,汤姆。感谢您花时间帮助我。我将接受的答案提供给康拉德,因为我认为他的版本将更好地扩展到每个表中有许多字段的现实世界场景。
    猜你喜欢
    • 2019-05-06
    • 1970-01-01
    • 2016-12-10
    • 2018-11-14
    • 1970-01-01
    • 2021-01-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多