Il modo più efficiente per generare un diff


8

Ho una tabella in SQL Server che assomiglia a questo:

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

Sto lavorando su una procedura memorizzata per diff, che richiede dati di input e un numero di versione. I dati di input hanno colonne da Nome fino a fieldZ. La maggior parte delle colonne dei campi dovrebbe essere NULL, ovvero, ogni riga di solito contiene dati solo per i primi pochi campi, il resto è NULL. Il nome, la data e la versione formano un vincolo univoco sulla tabella.

Ho bisogno di diff i dati che vengono immessi rispetto a questa tabella, per una data versione. Ogni riga deve essere diffusa: una riga è identificata dal nome, dalla data e dalla versione e qualsiasi modifica in uno qualsiasi dei valori nelle colonne del campo dovrà essere mostrata nel diff.

Aggiornamento: non è necessario che tutti i campi siano di tipo decimale. Alcuni di loro potrebbero essere nvarchars. Preferirei che il diff accadesse senza convertire il tipo, anche se l'output diff potrebbe convertire tutto in nvarchar dal momento che deve essere utilizzato solo per scopi di visualizzazione.

Supponiamo che l'input sia il seguente e che la versione richiesta sia 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

Il diff deve essere nel seguente 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

La mia soluzione finora è di generare prima un diff, usando EXCEPT e UNION. Quindi converti il ​​diff nel formato di output desiderato usando JOIN e CROSS APPLY. Anche se questo sembra funzionare, mi chiedo se esiste un modo più pulito ed efficiente per farlo. Il numero di campi è vicino a 100, e ogni posto nel codice che ha un ... è in realtà un gran numero di righe. Sia la tabella di input che la tabella esistente dovrebbero essere abbastanza grandi nel tempo. Sono nuovo di SQL e sto ancora cercando di imparare l'ottimizzazione delle prestazioni.

Ecco l'SQL per esso:

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)  

Grazie!

Risposte:


5

Ecco un altro approccio:

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

Ecco come funziona:

  1. Le due tabelle sono unite usando un join esterno, @diffInputessendo sul lato esterno per abbinare il join destro.

  2. Il risultato del join non è condizionato in modo condizionale mediante CROSS APPLY, dove "condizionalmente" significa che ogni coppia di colonne viene testata singolarmente e restituita solo se le colonne differiscono.

  3. Il modello di ogni condizione di test

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    è equivalente al tuo

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

    solo più conciso. Puoi leggere di più su questo uso di INTERSECT in dettaglio nell'articolo di Paul White Piani di query non documentati: confronti di uguaglianza .

Su una nota diversa, dal momento che stai dicendo,

Sia la tabella di input che la tabella esistente dovrebbero essere abbastanza grandi nel tempo

ti consigliamo di sostituire la variabile di tabella che stai utilizzando per la tabella di input con una tabella temporanea. C'è una risposta molto esauriente di Martin Smith che esplora le differenze tra i due:

In breve, alcune proprietà delle variabili di tabella, come ad esempio l'assenza di statistiche di colonna, potrebbero renderle meno ottimizzate per le query per il tuo scenario rispetto alle tabelle temporanee.


Se il tipo di dati non è lo stesso per i campi AZ, i 2 campi nelle istruzioni selezionate devono essere convertiti in varchar o l'istruzione union non funzionerà.
Andre

5

Modifica per quanto riguarda i campi di diversi tipi, non solo decimal.

Puoi provare a usare il sql_varianttipo. Non l'ho mai usato personalmente, ma potrebbe essere una buona soluzione per il tuo caso. Per provarlo basta sostituire tutto [decimal](38, 10)con sql_variantnello script SQL. La query stessa rimane esattamente com'è, non è necessaria alcuna conversione esplicita per eseguire il confronto. Il risultato finale avrebbe una colonna con valori di diversi tipi al suo interno. Molto probabilmente, alla fine dovresti sapere in qualche modo quale tipo è in quale campo elaborare i risultati nella tua applicazione, ma la query stessa dovrebbe funzionare bene senza conversioni.


A proposito, è una cattiva idea memorizzare le date come int.

Invece di usare EXCEPTe UNIONcalcolare il diff, userei FULL JOIN. Per me, personalmente, è difficile seguire la logica EXCEPTe l' UNIONapproccio.

Vorrei iniziare con la non divulgazione dei dati, piuttosto che per ultimo (usando CROSS APPLY(VALUES)come fai tu). È possibile eliminare l'annullamento dell'immissione, se lo si fa in anticipo, dal lato del chiamante.

Dovresti elencare tutte e 100 le colonne solo in CROSS APPLY(VALUES).

La query finale è piuttosto semplice, quindi la tabella temporanea non è davvero necessaria. Penso che sia più facile da scrivere e mantenere della tua versione. Ecco SQL Fiddle .

Imposta i dati di esempio

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;

Query principale

CTE_Maini dati originali non promossi vengono filtrati in base a quelli forniti Version. CTE_Inputè una tabella di input, che potrebbe essere fornita già in questo formato. La query principale utilizza FULL JOIN, che si aggiunge alle righe dei risultati con Bee. Penso che dovrebbero essere restituiti, ma se non vuoi vederli, puoi filtrarli aggiungendo AND CTE_Input.FieldValue IS NOT NULLo forse usando al LEFT JOINposto di FULL JOIN, non ho esaminato i dettagli lì, perché penso che dovrebbero essere restituiti.

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;

Risultato

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.