2012-05-28 5 views
0

У меня есть аудит для записей в таблице. В нескольких столбцах и каждой записи указано изменение 1 или более столбцов.
мне нужно возвращать результат аудита, где возвращение шаблон будет: column (идентификатор, псевдоним или имя), предыдущее значение, новое значение, и т.д.
Проблема заключается в том, что там может быть несколько столбцы с измененными данными для каждой новой записи. В то же время количество проверяемых столбцов равно 5, поэтому можно «жестко указать» имена и изменения проверок.SQL: проверка возврата для записей для каждого столбца

Возможно ли составить такой запрос в сжатом виде, не просто используя UNIONS, и сделать запрос SELECT для каждого столбца и проверить изменения?

Допустим, есть таблица со столбцами:

id, datetime value, int value, varchar value. 

И если у меня есть 2 записей с таким изменением данных как как:

id1, value1, value1, value1 
id1, value2, value1, value2 

Тогда я ожидал таких результатов аудита:

id1, value1 as oldvalue, value2 as newvalue, column2name as columnname 
id1, value1 as oldvalue, value2 as newvalue, column4name as columnname 
+0

Вы также не укажете время изменения (или, по крайней мере, некоторые последовательные значения, указывающие порядок изменений)? –

+0

Еще одной проблемой могут быть типы проверенных столбцов. Если они разные, вам, вероятно, придется преобразовать все их в строки, чтобы они могли выводить их в тех же столбцах (а именно «предыдущее значение» и «новое значение»). –

+0

Да, мне нужно будет вернуть дату проведения аудита, и да, мне придется отдать данные в varchar. – drunkcamel

ответ

2

Если я не пропустил ничего:

WITH ranked AS (
    SELECT 
    ChangeDate, 
    ColPK, 
    Col1, 
    Col2, 
    Col3, 
    Col4, 
    Col5, 
    OverallRank = ROW_NUMBER() OVER (PARTITION BY ColPK  ORDER BY ChangeDate), 
    Col1Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col1 ORDER BY ChangeDate), 
    Col2Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col2 ORDER BY ChangeDate), 
    Col3Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col3 ORDER BY ChangeDate), 
    Col4Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col4 ORDER BY ChangeDate), 
    Col5Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col5 ORDER BY ChangeDate) 
    FROM AuditTable 
) 
, ranked2 AS (
    SELECT 
    ChangeDate, 
    ColPK, 
    Col1, 
    Col2, 
    Col3, 
    Col4, 
    Col5, 
    Col1Group = RANK() OVER (PARTITION BY ColPK, Col1 ORDER BY OverallRank - Col1Rank), 
    Col2Group = RANK() OVER (PARTITION BY ColPK, Col2 ORDER BY OverallRank - Col2Rank), 
    Col3Group = RANK() OVER (PARTITION BY ColPK, Col3 ORDER BY OverallRank - Col3Rank), 
    Col4Group = RANK() OVER (PARTITION BY ColPK, Col4 ORDER BY OverallRank - Col4Rank), 
    Col5Group = RANK() OVER (PARTITION BY ColPK, Col5 ORDER BY OverallRank - Col5Rank), 
    Col1Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col1, OverallRank - Col1Rank ORDER BY ChangeDate), 
    Col2Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col2, OverallRank - Col2Rank ORDER BY ChangeDate), 
    Col3Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col3, OverallRank - Col3Rank ORDER BY ChangeDate), 
    Col4Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col4, OverallRank - Col4Rank ORDER BY ChangeDate), 
    Col5Rank = ROW_NUMBER() OVER (PARTITION BY ColPK, Col5, OverallRank - Col5Rank ORDER BY ChangeDate) 
    FROM ranked 
), 
unpivoted AS (
    SELECT 
    r.ChangeTime, 
    r.ColPK, 
    x.ColName, 
    ColRank = CASE x.Colname 
     WHEN 'Col1' THEN Col1Group 
     WHEN 'Col2' THEN Col2Group 
     WHEN 'Col3' THEN Col3Group 
     WHEN 'Col4' THEN Col4Group 
     WHEN 'Col5' THEN Col5Group 
    END, 
    Value = CASE x.Colname 
     WHEN 'Col1' THEN CONVERT(nvarchar(100), r.Col1) 
     WHEN 'Col2' THEN CONVERT(nvarchar(100), r.Col2) 
     WHEN 'Col3' THEN CONVERT(nvarchar(100), r.Col3) 
     WHEN 'Col4' THEN CONVERT(nvarchar(100), r.Col4) 
     WHEN 'Col5' THEN CONVERT(nvarchar(100), r.Col5) 
    END 
    FROM ranked2 r 
    INNER JOIN (VALUES ('Col1'), ('Col2'), ('Col3'), ('Col4'), ('Col5')) x (ColName) 
     ON x.ColName = 'Col1' AND Col1Rank = 1 
     OR x.ColName = 'Col2' AND Col2Rank = 1 
     OR x.ColName = 'Col3' AND Col3Rank = 1 
     OR x.ColName = 'Col4' AND Col4Rank = 1 
     OR x.ColName = 'Col5' AND Col5Rank = 1 
) 
SELECT 
    new.ChangeTime, 
    new.ColPK, 
    new.ColName, 
    old.Value AS OldValue, 
    new.Value AS NewValue 
FROM unpivoted new 
    LEFT JOIN unpivoted old 
    ON new.ColPK = old.ColPK 
    AND new.ColName = old.ColName 
    AND new.ColRank = old.ColRank + 1 

В принципе, идея состоит в том, чтобы ранжировать смежные группы одинаковых значений и выбирать первые вхождения каждого значения. Это делается для каждого столбца, значения которого проверяются, а столбцы не распознаются в процессе. После этого набор без разделителей строк соединяется с собой, то есть для каждого имени PK и столбца каждая строка сопоставляется с ее предшественником (на основе ранжирования), чтобы получить старое значение в той же строке для конечного набора результатов.

0

Вот простой запрос, который дает одинаковые желаемые результаты, и его намного легче модифицировать, чтобы разместить другое количество столбцов или изменять имена столбцов, поскольку единственными отличиями являются столбцы PK + одна строка в не-PK-столбцом в CROSS APPLY. Мне пришлось добавить столбец ChangeDate - без него нет способа узнать порядок строк, вставленных в таблицу аудита.

WITH ColValues AS (
    SELECT 
     Grp = Row_Number() OVER (
     PARTITION BY H.OrderID, U.ColName ORDER BY H.ChangeDate ASC, X.Which 
    )/2, 
     H.OrderID, 
     H.ChangeDate, 
     U.*, 
     X.Which 
    FROM 
     dbo.OrderHistory H 
     CROSS APPLY (VALUES 
     ('DeliveryDate', Convert(varchar(1000), DeliveryDate, 121)), 
     ('Quantity', Convert(varchar(1000), Quantity)), 
     ('SpecialNotes', Convert(varchar(1000), SpecialNotes)) 
    ) U (ColName, Value) 
     CROSS JOIN (VALUES (1), (2)) X (Which) 
) 
SELECT 
    V.OrderID, 
    V.ColName, 
    DateChanged = Max(V.ChangeDate), 
    OldValue = Max(F.Value), 
    NewValue = Max(T.Value) 
FROM 
    ColValues V 
    OUTER APPLY (SELECT V.ColName, V.Value WHERE V.Which = 2) F 
    OUTER APPLY (SELECT V.ColName, V.Value WHERE V.Which = 1) T 
GROUP BY 
    V.OrderID, 
    V.ColName, 
    V.Grp 
HAVING 
    Count(*) = 2 
    AND EXISTS (
     SELECT Max(F.Value) 
     EXCEPT SELECT Max(T.Value) 
    ) 
; 

See a live demo of this query at SQL Fiddle.

В SQL 2012 это было бы лучше с аналитической функцией LEAD или LAG. CROSS JOIN и Row_Number в моем запросе имитируют это путем дублирования каждой строки и назначения этих дублированных строк парами в свои собственные группы (где каждая группа имеет две строки, представляющие смежные строки истории аудита). Затем путем стратегического использования агрегатов мы можем иметь дело с группированными парами, чтобы выбирать и сравнивать их значения.

Кроме того, я изначально написал запрос с UNPIVOT, но, увы, он не сохраняет NULL - серьезный надзор со стороны Microsoft, на мой взгляд. Разработчикам было бы легко добавить условие удаления NULL, если это необходимо, но способ, которым он равен UNPIVOT, вообще не может быть использован при желании сохранить NULL. По иронии судьбы, полученный код более компактен и на 2 строки короче, используя CROSS APPLY для UNPIVOT - теперь преобразование и разворачивание происходят за один шаг вместо 2.

Мои данные выборки:

ChangeDate    OrderID DeliveryDate   Quantity SpecialNotes 
----------------------- ------- ----------------------- -------- ---------------------------------------------------- 
2013-03-01 11:28:00.000 1  2013-04-01 00:00:00.000 25  NULL 
2013-03-01 11:56:00.000 1  2013-04-01 00:00:00.000 30  NULL 
2013-03-05 10:18:00.000 1  2013-04-02 00:00:00.000 30  Customer called to ask for delivery date adjustment. 
2013-03-01 11:37:00.000 2  2013-03-05 00:00:00.000 17  NULL 

Полученный набор строк:

OrderID ColName  DateChanged    OldValue    NewValue 
------- ------------ ----------------------- ----------------------- --------------------------------------------------- 
1  DeliveryDate 2013-03-05 10:18:00.000 2013-04-01 00:00:00.000 2013-04-02 00:00:00.000 
1  Quantity  2013-03-01 11:56:00.000 25      30 
1  SpecialNotes 2013-03-05 10:18:00.000 NULL     Customer called to ask for delivery date adjustment. 

Примечание: поскольку мой запрос имеет только одну функция ранжирования и нет JOIN с, это будет работать очень хорошо даже в очень большие таблицы - на порядок лучше, возможно, чем решения, которые используют JOIN, где нет индекса поддержки. Было бы лучше, если бы таблица аудита имела кластерный индекс на PK, ChangeDate.

Смежные вопросы