A maneira mais eficiente de gerar um diff

8

Eu tenho uma tabela no SQL Server que se parece com isso:

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

Estou trabalhando em um procedimento armazenado para diff, que leva dados de entrada e um número de versão. Os dados de entrada possuem colunas do Nome até o campoZ. Espera-se que a maioria das colunas de campo seja NULL, ou seja, cada linha geralmente possui dados apenas para os primeiros campos, o restante é NULL. O nome, a data e a versão formam uma restrição exclusiva na tabela.

Eu preciso diferenciar os dados que são inseridos em relação a esta tabela, para uma determinada versão. Cada linha precisa ser diferenciada - uma linha é identificada pelo nome, data e versão, e qualquer alteração em qualquer um dos valores nas colunas de campo precisará ser exibida na comparação.

Atualização: todos os campos não precisam ser do tipo decimal. Alguns deles podem ser nvarchars. Eu preferiria que o diff acontecesse sem converter o tipo, embora a saída do diff possa converter tudo em nvarchar, pois deve ser usada apenas para exibição proposta.

Suponha que a entrada seja a seguinte e a versão solicitada seja 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

O diff precisa estar no seguinte formato:

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

Minha solução até agora é primeiro gerar um diff, usando EXCEPT e UNION. Em seguida, converta o diff para o formato de saída desejado usando um JOIN e CROSS APPLY. Embora isso pareça estar funcionando, estou me perguntando se existe uma maneira mais limpa e eficiente de fazer isso. O número de campos é próximo de 100 e cada local no código que possui um ... é na verdade um grande número de linhas. Espera-se que a tabela de entrada e a tabela existente sejam bastante grandes ao longo do tempo. Eu sou novo no SQL e ainda estou tentando aprender o ajuste de desempenho.

Aqui está o SQL para isso:

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)  

Obrigado!

ame
fonte

Respostas:

5

Aqui está outra abordagem:

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)
;

É assim que funciona:

  1. As duas tabelas são unidas usando uma junção externa, @diffInputestando do lado externo para corresponder à sua junção direita.

  2. O resultado da junção é condicionalmente não dinâmico usando CROSS APPLY, onde "condicionalmente" significa que cada par de colunas é testado individualmente e retornado apenas se as colunas diferirem.

  3. O padrão de cada condição de teste

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    é equivalente ao seu

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

    apenas mais conciso. Você pode ler mais sobre esse uso do INTERSECT em detalhes no artigo de Paul White, Planos de consulta não documentados: Comparações de igualdade .

Em uma nota diferente, já que você está dizendo,

Espera-se que a tabela de entrada e a tabela existente sejam bastante grandes ao longo do tempo

convém substituir a variável de tabela que você está usando para a tabela de entrada por uma tabela temporária. Há uma resposta muito abrangente de Martin Smith que explora as diferenças entre os dois:

Em resumo, certas propriedades das variáveis ​​da tabela, como, por exemplo, ausência de estatísticas da coluna, podem torná-las menos amigáveis ​​ao otimizador de consultas para o seu cenário do que as tabelas temporárias.

Andriy M
fonte
Se o tipo de dados não for o mesmo para os campos AZ, os 2 campos nas instruções de seleção precisarão ser convertidos em varchar ou a instrução de união não funcionará.
21317 Andre Andre
5

Edite os campos com tipos diferentes, não apenas decimal.

Você pode tentar usar o sql_varianttipo. Eu nunca o usei pessoalmente, mas pode ser uma boa solução para o seu caso. Para tentar, basta substituir todos [decimal](38, 10)por sql_variantno script SQL. A consulta em si permanece exatamente como está, nenhuma conversão explícita é necessária para realizar a comparação. O resultado final teria uma coluna com valores de tipos diferentes. Provavelmente, eventualmente, você precisará saber de alguma forma qual tipo está em qual campo processar os resultados em seu aplicativo, mas a consulta em si deve funcionar bem sem conversões.


A propósito, é uma má idéia armazenar datas como int.

Em vez de usar EXCEPTe UNIONcalcular o diff, eu usaria FULL JOIN. Para mim, pessoalmente, é difícil seguir a lógica por trás EXCEPTe a UNIONabordagem.

Eu começaria desviando os dados, em vez de fazê-lo por último (usando CROSS APPLY(VALUES)como você). Você pode se livrar da não articulação da entrada, se você fizer isso com antecedência, no lado do chamador.

Você precisaria listar todas as 100 colunas apenas em CROSS APPLY(VALUES).

A consulta final é bem simples, portanto, a tabela temporária não é realmente necessária. Eu acho que é mais fácil escrever e manter do que a sua versão. Aqui está o SQL Fiddle .

Configurar dados de amostra

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;

Consulta principal

CTE_Mainsão dados originais não dinâmicos filtrados para o dado Version. CTE_Inputé uma tabela de entrada, que já pode ser fornecida nesse formato. Utilizações de consulta principal FULL JOIN, adicionadas às linhas de resultado com Bee. Eu acho que eles devem ser devolvidos, mas se você não quiser vê-los, é possível filtrá-los adicionando AND CTE_Input.FieldValue IS NOT NULLou talvez usando em LEFT JOINvez de FULL JOIN, não procurei detalhes lá, porque acho que eles deveriam ser devolvidos.

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;

Resultado

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
Vladimir Baranov
fonte