Modo più efficace per generare un diff
-
29-09-2020 - |
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!
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:
- .
-
Le due tabelle sono unite utilizzando un join esterno,
@diffInput
essendo sul lato esterno per abbinare il proprio join a destra. -
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.
-
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 .
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
.