Le moyen le plus efficace pour générer un diff


8

J'ai une table dans SQL Server qui ressemble à ceci:

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

Je travaille sur une procédure stockée pour différencier, qui prend les données d'entrée et un numéro de version. Les données d'entrée ont des colonnes du champ Nom uptilZ. La plupart des colonnes de champ sont censées être NULL, c'est-à-dire que chaque ligne contient généralement des données uniquement pour les premiers champs, les autres sont NULL. Le nom, la date et la version forment une contrainte unique sur la table.

J'ai besoin de différencier les données entrées par rapport à ce tableau, pour une version donnée. Chaque ligne doit être différenciée - une ligne est identifiée par le nom, la date et la version, et tout changement dans l'une des valeurs dans les colonnes de champ devra apparaître dans le diff.

Mise à jour: tous les champs n'ont pas besoin d'être de type décimal. Certains d'entre eux peuvent être des nvarchars. Je préférerais que le diff se produise sans convertir le type, bien que la sortie diff puisse tout convertir en nvarchar car elle ne doit être utilisée qu'à des fins d'affichage.

Supposons que l'entrée soit la suivante et que la version demandée soit 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

Le diff doit être au format suivant:

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

Ma solution jusqu'à présent est de générer d'abord un diff, en utilisant EXCEPT et UNION. Convertissez ensuite le diff au format de sortie souhaité à l'aide de JOIN et CROSS APPLY. Bien que cela semble fonctionner, je me demande s'il existe un moyen plus propre et plus efficace de le faire. Le nombre de champs est proche de 100, et chaque place dans le code qui a un ... est en fait un grand nombre de lignes. La table d'entrée et la table existante devraient être assez importantes au fil du temps. Je suis nouveau dans SQL et j'essaie toujours d'apprendre le réglage des performances.

Voici le SQL pour cela:

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)  

Je vous remercie!

Réponses:


5

Voici une autre approche:

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

Voilà comment cela fonctionne:

  1. Les deux tables sont jointes à l'aide d'une jointure externe, se @diffInputtrouvant du côté externe pour correspondre à votre jointure droite.

  2. Le résultat de la jointure est conditionnellement non pivoté à l'aide de CROSS APPLY, où "conditionnellement" signifie que chaque paire de colonnes est testée individuellement et renvoyée uniquement si les colonnes diffèrent.

  3. Le modèle de chaque condition de test

    NOT EXISTS (SELECT oldValue INTERSECT SELECT newValue)

    est équivalent à votre

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

    seulement plus concis. Vous pouvez en savoir plus sur cette utilisation d'INTERSECT en détail dans l'article de Paul White, Plans de requête non documentée: comparaisons d'égalité .

Sur une note différente, puisque vous dites,

La table d'entrée et la table existante devraient être assez grandes au fil du temps

vous pouvez envisager de remplacer la variable de table que vous utilisez pour la table d'entrée par une table temporaire. Il y a une réponse très complète de Martin Smith qui explore les différences entre les deux:

En bref, certaines propriétés des variables de table, comme par exemple l'absence de statistiques sur les colonnes, peuvent les rendre moins optimistes pour les requêtes pour votre scénario que les tables temporaires.


Si le type de données n'est pas le même pour les champs AZ, les 2 champs des instructions select doivent être convertis en varchar ou l'instruction union ne fonctionnera pas.
Andre

5

Modifier les champs ayant différents types, pas seulement decimal.

Vous pouvez essayer d'utiliser le sql_varianttype. Je ne l'ai jamais utilisé personnellement, mais cela peut être une bonne solution pour votre cas. Pour l'essayer, remplacez tout simplement [decimal](38, 10)par sql_variantdans le script SQL. La requête elle-même reste exactement telle qu'elle est, aucune conversion explicite n'est nécessaire pour effectuer la comparaison. Le résultat final aurait une colonne avec des valeurs de différents types. Très probablement, vous devrez éventuellement savoir quel type se trouve dans quel champ pour traiter les résultats dans votre application, mais la requête elle-même devrait fonctionner correctement sans conversions.


Soit dit en passant, c'est une mauvaise idée de stocker les dates en tant que int.

Au lieu d'utiliser EXCEPTet UNIONde calculer le diff, j'utiliserais FULL JOIN. Pour moi, personnellement, il est difficile de suivre la logique EXCEPTet l' UNIONapproche.

Je commencerais par annuler le pivot des données, plutôt que de le faire en dernier (en utilisant CROSS APPLY(VALUES)comme vous le faites). Vous pouvez vous débarrasser de la suppression de pivot de l'entrée, si vous le faites à l'avance, du côté de l'appelant.

Vous devez répertorier les 100 colonnes uniquement dans CROSS APPLY(VALUES).

La requête finale est assez simple, donc la table temporaire n'est pas vraiment nécessaire. Je pense qu'il est plus facile d'écrire et de maintenir que votre version. Voici SQL Fiddle .

Configurer des exemples de données

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;

Requête principale

CTE_Mainest des données originales non pivotées filtrées en fonction des données Version. CTE_Inputest une table d'entrée, qui pourrait déjà être fournie dans ce format. La requête principale utilise FULL JOIN, ce qui ajoute aux lignes de résultat avec Bee. Je pense qu'ils devraient être retournés, mais si vous ne voulez pas les voir, vous pouvez les filtrer en ajoutant AND CTE_Input.FieldValue IS NOT NULLou peut-être en utilisant à la LEFT JOINplace de FULL JOIN, je n'ai pas examiné les détails, car je pense qu'ils devraient être retournés.

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;

Résultat

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.