Der effizienteste Weg, um ein Diff zu erzeugen

8

Ich habe eine Tabelle in SQL Server, die so aussieht:

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

Ich arbeite an einer gespeicherten Prozedur zum Diff, die Eingabedaten und eine Versionsnummer verwendet. Die Eingabedaten enthalten Spalten aus Name bis FeldZ. Es wird erwartet, dass die meisten Feldspalten NULL sind, dh jede Zeile enthält normalerweise nur Daten für die ersten Felder, der Rest ist NULL. Der Name, das Datum und die Version bilden eine eindeutige Einschränkung für die Tabelle.

Ich muss die Daten, die in Bezug auf diese Tabelle eingegeben werden, für eine bestimmte Version unterscheiden. Jede Zeile muss unterschiedlich sein - eine Zeile wird durch den Namen, das Datum und die Version identifiziert, und jede Änderung der Werte in den Feldspalten muss im Unterschied angezeigt werden.

Update: Alle Felder müssen nicht dezimal sein. Einige von ihnen können nvarchars sein. Ich würde es vorziehen, wenn das Diff ohne Konvertierung des Typs erfolgt, obwohl die Diff-Ausgabe alles in nvarchar konvertieren könnte, da es nur für die Anzeige verwendet werden soll.

Angenommen, die Eingabe ist die folgende und die angeforderte Version ist 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

Der Diff muss das folgende Format haben:

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

Meine bisherige Lösung besteht darin, zunächst mit EXCEPT und UNION ein Diff zu generieren. Konvertieren Sie dann das Diff mit JOIN und CROSS APPLY in das gewünschte Ausgabeformat. Obwohl dies zu funktionieren scheint, frage ich mich, ob es einen saubereren und effizienteren Weg gibt, dies zu tun. Die Anzahl der Felder liegt nahe bei 100, und jede Stelle im Code, die ein ... enthält, besteht tatsächlich aus einer großen Anzahl von Zeilen. Es wird erwartet, dass sowohl die Eingabetabelle als auch die vorhandene Tabelle im Laufe der Zeit ziemlich groß sind. Ich bin neu in SQL und versuche immer noch, die Leistungsoptimierung zu lernen.

Hier ist die SQL dafür:

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)  

Vielen Dank!

ein Ich
quelle

Antworten:

5

Hier ist ein anderer Ansatz:

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

So funktioniert es:

  1. Die beiden Tabellen werden mithilfe eines äußeren Joins verbunden, @diffInputder sich auf der Außenseite befindet und Ihrem rechten Join entspricht.

  2. Das Ergebnis des Joins wird mit CROSS APPLY bedingt nicht gedreht, wobei "bedingt" bedeutet, dass jedes Spaltenpaar einzeln getestet und nur zurückgegeben wird, wenn sich die Spalten unterscheiden.

  3. Das Muster jeder Testbedingung

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    ist gleichbedeutend mit Ihrem

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

    nur prägnanter. Weitere Informationen zu dieser Verwendung von INTERSECT finden Sie ausführlich in Paul Whites Artikel Undokumentierte Abfragepläne : Gleichheitsvergleiche .

In einem anderen Sinne, da Sie sagen:

Es wird erwartet, dass sowohl die Eingabetabelle als auch die vorhandene Tabelle im Laufe der Zeit ziemlich groß sind

Möglicherweise möchten Sie die Tabellenvariable, die Sie für die Eingabetabelle verwenden, durch eine temporäre Tabelle ersetzen. Es gibt eine sehr umfassende Antwort von Martin Smith, die die Unterschiede zwischen den beiden untersucht:

Kurz gesagt, bestimmte Eigenschaften von Tabellenvariablen, z. B. das Fehlen von Spaltenstatistiken, können dazu führen, dass sie für Ihr Szenario weniger abfrageoptimiererfreundlich sind als temporäre Tabellen.

Andriy M.
quelle
Wenn der Datentyp für die Felder AZ nicht identisch ist, müssen die beiden Felder in den select-Anweisungen in varchar konvertiert werden, da sonst die union-Anweisung nicht funktioniert.
Andre
5

Bearbeiten Sie nicht nur Felder mit unterschiedlichen Typen decimal.

Sie können versuchen, sql_varianttype zu verwenden . Ich habe es nie persönlich benutzt, aber es kann eine gute Lösung für Ihren Fall sein. Um es zu versuchen, ersetzen Sie einfach alle [decimal](38, 10)durch sql_variantim SQL-Skript. Die Abfrage selbst bleibt genau so, wie sie ist. Für den Vergleich ist keine explizite Konvertierung erforderlich. Das Endergebnis würde eine Spalte mit Werten unterschiedlichen Typs enthalten. Höchstwahrscheinlich müssten Sie irgendwann wissen, welcher Typ sich in welchem ​​Feld befindet, um die Ergebnisse in Ihrer Anwendung zu verarbeiten, aber die Abfrage selbst sollte ohne Konvertierungen problemlos funktionieren.


Übrigens ist es eine schlechte Idee, Daten als zu speichern int.

Anstatt den Diff zu verwenden EXCEPTund UNIONzu berechnen, würde ich verwenden FULL JOIN. Für mich persönlich ist es schwierig, der Logik dahinter zu folgen EXCEPTund mich zu UNIONnähern.

Ich würde damit beginnen, die Daten zu entfernen, anstatt sie zuletzt zu tun ( CROSS APPLY(VALUES)wie Sie es tun). Sie können das Aufheben der Eingabe auf der Anruferseite beseitigen, wenn Sie dies im Voraus tun.

Sie müssten alle 100 Spalten nur in auflisten CROSS APPLY(VALUES).

Die endgültige Abfrage ist ziemlich einfach, so dass eine temporäre Tabelle nicht wirklich benötigt wird. Ich denke, es ist einfacher zu schreiben und zu warten als Ihre Version. Hier ist SQL Fiddle .

Richten Sie Beispieldaten ein

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;

Hauptabfrage

CTE_MainEs werden nicht gedrehte Originaldaten auf die angegebenen gefiltert Version. CTE_Inputist eine Eingabetabelle, die bereits in diesem Format bereitgestellt werden könnte. Hauptabfrage verwendet FULL JOIN, die zu Ergebniszeilen mit hinzufügt Bee. Ich denke, sie sollten zurückgegeben werden, aber wenn Sie sie nicht sehen möchten, können Sie sie herausfiltern, indem Sie sie hinzufügen AND CTE_Input.FieldValue IS NOT NULLoder verwenden. LEFT JOINStattdessen habe FULL JOINich dort keine Details untersucht, da ich denke, dass sie zurückgegeben werden sollten.

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;

Ergebnis

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
quelle