Самый эффективный способ создания различий

8

У меня есть таблица на сервере SQL, которая выглядит следующим образом:

Id    |Version  |Name    |date    |fieldA   |fieldB ..|fieldZ
1     |1        |Foo     |20120101|23       |       ..|25334123
2     |2        |Foo     |20120101|23       |NULL   ..|NULL
3     |2        |Bar     |20120303|24       |123......|NULL
4     |2        |Bee     |20120303|34       |-34......|NULL

Я работаю над хранимой процедурой diff, которая принимает входные данные и номер версии. Входные данные имеют столбцы из Имя до поля Z. Ожидается, что большинство столбцов полей будут NULL, т. Е. Каждая строка обычно содержит данные только для первых нескольких полей, остальные - NULL. Имя, дата и версия образуют уникальное ограничение для таблицы.

Мне нужно различить данные, которые вводятся относительно этой таблицы, для данной версии. Каждая строка должна быть диффузной - строка идентифицируется по имени, дате и версии, и любое изменение в любом из значений в столбцах поля должно отображаться в diff.

Обновление: все поля не обязательно должны быть десятичного типа. Некоторые из них могут быть nvarchars. Я бы предпочел, чтобы diff происходил без преобразования типа, хотя вывод diff мог конвертировать все в nvarchar, поскольку он должен использоваться только для отображения.

Предположим, что ввод следующий, и запрашиваемая версия 2,:

Name    |date    |fieldA   |fieldB|..|fieldZ
Foo     |20120101|25       |NULL  |.. |NULL
Foo     |20120102|26       |27    |.. |NULL
Bar     |20120303|24       |126   |.. |NULL
Baz     |20120101|15       |NULL  |.. |NULL

Разница должна быть в следующем формате:

name    |date    |field    |oldValue    |newValue
Foo     |20120101|FieldA   |23          |25
Foo     |20120102|FieldA   |NULL        |26
Foo     |20120102|FieldB   |NULL        |27
Bar     |20120303|FieldB   |123         |126
Baz     |20120101|FieldA   |NULL        |15

Мое решение до сих пор состоит в том, чтобы сначала сгенерировать diff, используя EXCEPT и UNION. Затем преобразуйте diff в желаемый формат вывода, используя JOIN и CROSS APPLY. Хотя это, кажется, работает, мне интересно, есть ли более чистый и эффективный способ сделать это. Количество полей близко к 100, и каждое место в коде, которое имеет ..., на самом деле представляет собой большое количество строк. Ожидается, что и входная таблица, и существующая таблица со временем будут достаточно большими. Я новичок в SQL и все еще пытаюсь научиться настройке производительности.

Вот SQL для этого:

CREATE TABLE #diff
(   [change] [nvarchar](50) NOT NULL,
    [name] [nvarchar](50) NOT NULL,
    [date] [int] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    .....
    [FieldZ] [decimal](38, 10) NULL
)

--Generate the diff in a temporary table
INSERT INTO #diff
SELECT * FROM
(

(
    SELECT
        'old' as change,
        name,
        date,
        FieldA,
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
    EXCEPT
    SELECT 'old' as change,* FROM @diffInput
)
UNION

(
    SELECT 'new' as change, * FROM @diffInput
    EXCEPT
    SELECT
        'new' as change,
        name,
        date,
        FieldA, 
        FieldB,
        ...,
        FieldZ
    FROM 
        myTable mt 
    WHERE 
        version = @version 
        AND mt.name + '_' + CAST(mt.date AS VARCHAR) IN (SELECT name + '_' + CAST(date AS VARCHAR) FROM @diffInput) 
) 
) AS myDiff

SELECT 
d3.name, d3.date, CrossApplied.field, CrossApplied.oldValue, CrossApplied.newValue
FROM
(
    SELECT 
        d2.name, d2.date, 
        d1.FieldA AS oldFieldA, d2.FieldA AS newFieldA, 
        d1.FieldB AS oldFieldB, d2.FieldB AS newFieldB,
        ...
        d1.FieldZ AS oldFieldZ, d2.FieldZ AS newFieldZ,
    FROM #diff AS d1
    RIGHT OUTER JOIN #diff AS d2
    ON 
        d1.name = d2.name
        AND d1.date = d2.date
        AND d1.change = 'old'
    WHERE d2.change = 'new'
) AS d3
CROSS APPLY (VALUES ('FieldA', oldFieldA, newFieldA), 
                ('FieldB', oldFieldB, newFieldB),
                ...
                ('FieldZ', oldFieldZ, newFieldZ))
                CrossApplied (field, oldValue, newValue)
WHERE 
    crossApplied.oldValue != crossApplied.newValue 
    OR (crossApplied.oldValue IS NULL AND crossApplied.newValue IS NOT NULL) 
    OR (crossApplied.oldValue IS NOT NULL AND crossApplied.newValue IS NULL)  

Спасибо!

AME
источник

Ответы:

5

Вот еще один подход:

SELECT
  di.name,
  di.date,
  x.field,
  x.oldValue,
  x.newValue
FROM
  @diffInput AS di
  LEFT JOIN dbo.myTable AS mt ON
    mt.version = @version
    AND mt.name = di.name
    AND mt.date = di.date
  CROSS APPLY
  (
    SELECT
      'fieldA',
      mt.fieldA,
      di.fieldA
    WHERE
      NOT EXISTS (SELECT mt.fieldA INTERSECT SELECT di.fieldA)

    UNION ALL

    SELECT
      'fieldB',
      mt.fieldB,
      di.fieldB
    WHERE
      NOT EXISTS (SELECT mt.fieldB INTERSECT SELECT di.fieldB)

    UNION ALL

    SELECT
      'fieldC',
      mt.fieldC,
      di.fieldC
    WHERE
      NOT EXISTS (SELECT mt.fieldC INTERSECT SELECT di.fieldC)

    UNION ALL

    ...
  ) AS x (field, oldValue, newValue)
;

Вот как это работает:

  1. Две таблицы объединяются с использованием внешнего соединения, @diffInputнаходящегося на внешней стороне, чтобы соответствовать вашему правому соединению.

  2. Результат объединения условно не разворачивается с помощью CROSS APPLY, где «условно» означает, что каждая пара столбцов проверяется индивидуально и возвращается только в том случае, если столбцы различаются.

  3. Шаблон каждого условия испытаний

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    эквивалентно вашему

    oldValue != newValue
    OR (oldValue IS NULL AND newValue IS NOT NULL)
    OR (oldValue IS NOT NULL AND newValue IS NULL)

    только более лаконично. Подробнее об этом использовании INTERSECT вы можете прочитать в статье Пола Уайта « Недокументированные планы запросов: сравнения равенства» .

С другой стороны, так как вы говорите,

Ожидается, что и входная таблица, и существующая таблица со временем будут достаточно большими

Возможно, вы захотите заменить переменную таблицы, которую вы используете для входной таблицы, временной таблицей. Мартин Смит предлагает исчерпывающий ответ, в котором рассматриваются различия между ними:

Короче говоря, некоторые свойства табличных переменных, такие как, например, отсутствие статистики столбцов, могут сделать их менее оптимизированными для запросов в вашем сценарии, чем временные таблицы.

Андрей М
источник
Если тип данных не совпадает для полей AZ, 2 поля в операторах выбора необходимо преобразовать в varchar, иначе оператор объединения не будет работать.
Андре
5

Редактировать поля, имеющие разные типы, а не только decimal.

Вы можете попробовать использовать sql_variantтип. Я никогда не использовал это лично, но это может быть хорошим решением для вашего случая. Чтобы попробовать это просто заменить все [decimal](38, 10)с sql_variantв сценарии SQL. Сам запрос остается в точности таким, какой он есть, для сравнения не требуется явного преобразования. Конечный результат будет иметь столбец со значениями разных типов. Скорее всего, в конечном итоге вам нужно будет каким-то образом узнать, какой тип находится в каком поле для обработки результатов в вашем приложении, но сам запрос должен работать без преобразований.


Кстати, это плохая идея хранить даты как int.

Вместо использования EXCEPTи UNIONдля вычисления различий, я бы использовал FULL JOIN. Лично мне сложно следовать логике EXCEPTи UNIONподходу.

Я бы начал с удаления данных, а не делал это последним (используя, CROSS APPLY(VALUES)как вы). Вы можете избавиться от отмены ввода, если вы делаете это заранее, на стороне вызывающего абонента.

Вы должны были бы перечислить все 100 столбцов только в CROSS APPLY(VALUES).

Последний запрос довольно прост, поэтому временная таблица на самом деле не нужна. Я думаю, что это легче написать и поддерживать, чем ваша версия. Вот SQL Fiddle .

Настройте пример данных

DECLARE @TMain TABLE (
    [ID] [int] NOT NULL,
    [Version] [int] NOT NULL,
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TMain ([ID],[Version],[Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
(1,1,'Foo','20120101',23,23  ,25334123),
(2,2,'Foo','20120101',23,NULL,NULL),
(3,2,'Bar','20120303',24,123 ,NULL),
(4,2,'Bee','20120303',34,-34 ,NULL);

DECLARE @TInput TABLE (
    [Name] [nvarchar](50) NOT NULL,
    [dt] [date] NOT NULL,
    [FieldA] [decimal](38, 10) NULL,
    [FieldB] [decimal](38, 10) NULL,
    [FieldZ] [decimal](38, 10) NULL
);

INSERT INTO @TInput ([Name],[dt],[FieldA],[FieldB],[FieldZ]) VALUES
('Foo','20120101',25,NULL,NULL),
('Foo','20120102',26,27  ,NULL),
('Bar','20120303',24,126 ,NULL),
('Baz','20120101',15,NULL,NULL);

DECLARE @VarVersion int = 2;

Основной запрос

CTE_Mainнепивотированные исходные данные отфильтрованы по заданным Version. CTE_Inputявляется входной таблицей, которая может быть предоставлена ​​уже в этом формате. Основной запрос использует FULL JOIN, который добавляет к строке результатов с Bee. Я думаю, что они должны быть возвращены, но если вы не хотите их видеть, вы можете отфильтровать их, добавив AND CTE_Input.FieldValue IS NOT NULLили, возможно, используя LEFT JOINвместо этого FULL JOIN, я не стал вдаваться в подробности, потому что я думаю, что они должны быть возвращены.

WITH
CTE_Main
AS
(
    SELECT
        Main.ID
        ,Main.Version
        ,Main.Name
        ,Main.dt
        ,FieldName
        ,FieldValue
    FROM
        @TMain AS Main
        CROSS APPLY
        (
            VALUES
                ('FieldA', Main.FieldA),
                ('FieldB', Main.FieldB),
                ('FieldZ', Main.FieldZ)
        ) AS CA(FieldName, FieldValue)
    WHERE
        Main.Version = @VarVersion
)
,CTE_Input
AS
(
    SELECT
        Input.Name
        ,Input.dt
        ,FieldName
        ,FieldValue
    FROM
        @TInput AS Input
        CROSS APPLY
        (
            VALUES
                ('FieldA', Input.FieldA),
                ('FieldB', Input.FieldB),
                ('FieldZ', Input.FieldZ)
        ) AS CA(FieldName, FieldValue)
)

SELECT
    ISNULL(CTE_Main.Name, CTE_Input.Name) AS FullName
    ,ISNULL(CTE_Main.dt, CTE_Input.dt) AS FullDate
    ,ISNULL(CTE_Main.FieldName, CTE_Input.FieldName) AS FullFieldName
    ,CTE_Main.FieldValue AS OldValue
    ,CTE_Input.FieldValue AS NewValue
FROM
    CTE_Main
    FULL JOIN CTE_Input ON 
        CTE_Input.Name = CTE_Main.Name
        AND CTE_Input.dt = CTE_Main.dt
        AND CTE_Input.FieldName = CTE_Main.FieldName
WHERE
    (CTE_Main.FieldValue <> CTE_Input.FieldValue)
    OR (CTE_Main.FieldValue IS NULL AND CTE_Input.FieldValue IS NOT NULL)
    OR (CTE_Main.FieldValue IS NOT NULL AND CTE_Input.FieldValue IS NULL)
--ORDER BY FullName, FullDate, FullFieldName;

Результат

FullName    FullDate    FullFieldName   OldValue        NewValue
Foo         2012-01-01  FieldA          23.0000000000   25.0000000000
Foo         2012-01-02  FieldA          NULL            26.0000000000
Foo         2012-01-02  FieldB          NULL            27.0000000000
Bar         2012-03-03  FieldB          123.0000000000  126.0000000000
Baz         2012-01-01  FieldA          NULL            15.0000000000
Bee         2012-03-03  FieldB          -34.0000000000  NULL
Bee         2012-03-03  FieldA          34.0000000000   NULL
Владимир Баранов
источник