Domanda

Ho una tabella in SQL Server che sembra 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 su diff, che prende i dati di input e un numero di versione. I dati di input hanno colonne dal nome Uptil Fieldz. La maggior parte delle colonne del campo dovrebbe essere nullo, cioè ogni riga di solito ha dati per solo i primi campi, il resto è nullo. Il nome, la data e la versione formano un vincolo unico sul tavolo.

Ho bisogno di differire i dati che vengono immessi rispetto a questa tabella, per una determinata 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à mostrare nel diff.

Aggiornamento: tutti i campi non devono essere di tipo decimale. Alcuni di loro potrebbero essere nvarcharch. Preferirei che il Diff accada senza convertire il tipo, sebbene l'output diff potesse convertire tutto su nvarchar poiché deve essere utilizzato solo per il display intenzionato.

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

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 è quella di generare prima un diff, usando tranne e unione. Quindi convertire il diff del formato di output desiderato utilizzando un join e incrociati. Anche se questo sembra funzionare, mi chiedo se c'è un modo più pulito e più efficiente per farlo. Il numero di campi è vicino a un 100, e ogni posto nel codice che ha un ... è in realtà un gran numero di linee. Sia la tabella di input che la tabella esistente dovrebbero essere abbastanza grandi nel tempo. Sono nuovo a SQL e sto ancora cercando di imparare la sintonizzazione delle prestazioni.

Ecco il SQL per questo:

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!

È stato utile?

Soluzione

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 utilizzando un join esterno, @diffInput essendo sul lato esterno per abbinare il proprio join a destra.

  2. Il risultato del join è dichiarato condizionatamente inserito con la croce, dove "condizionalmente" significa che ogni coppia di colonne viene testata singolarmente e restituita solo se le colonne differiscono.

  3. Lo schema 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 intersetto in dettaglio nell'articolo di Paul White piani di query non documentati: confronti di uguaglianza .

  4. Su una nota diversa, dal momento che stai dicendo,

    .

    Sia la tabella di ingresso che la tabella esistente dovrebbero essere piuttosto grandi nel tempo

    Potrebbe voler considerare di sostituire la variabile della tabella che si sta utilizzando per la tabella di input con una tabella temporanea. C'è una risposta molto completa di Martin Smith che esplora le differenze tra i due:

    In breve, alcune proprietà delle variabili da tavolo, come ad es. Assenza di statistiche di colonne, può renderle meno query Optimiser-Friendly per il tuo scenario rispetto alle tabelle temporanee.

Altri suggerimenti

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

Puoi provare a utilizzare sql_variant Tipo. Non l'ho mai usato personalmente, ma potrebbe essere una buona soluzione per il tuo caso. Per provarlo, sostituire tutto [decimal](38, 10) con sql_variant nello script SQL. La query stessa rimane esattamente così com'è, non è necessaria alcuna conversione esplicita per eseguire il confronto. Il risultato finale avrebbe una colonna con valori di diversi tipi in esso. Molto probabilmente, alla fine dovresti sapere in qualche modo quale tipo è in quale campo per 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 utilizzare EXCEPT e UNION per calcolare il diff, utilizzerei FULL JOIN. Per me, personalmente, è difficile seguire la logica dietro EXCEPT e l'approccio UNION.

Inizierei con unpivoting i dati, piuttosto che farlo durare (usando CROSS APPLY(VALUES) come fai). Puoi sbarazzarti di unpivoting dell'ingresso, se lo fai in anticipo, sul lato chiamante.

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

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

Imposta 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_Main è unpitoto da dati originali filtrati al Version indicato. CTE_Input è una tabella di immissione, che potrebbe essere fornita già in questo formato. La query principale utilizza FULL JOIN, che aggiunge di conseguire righe con Bee. Penso che dovrebbero essere restituiti, ma se non vuoi vederli, puoi filtrarli aggiungendo generacolitagcode o forse utilizzando AND CTE_Input.FieldValue IS NOT NULL invece di LEFT JOIN, non ho esaminato 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
.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a dba.stackexchange
scroll top