Requête détaillant les différences entre les lignes pour une grande quantité de données


15

J'ai un certain nombre de grandes tables, chacune avec> 300 colonnes. L'application que j'utilise crée des "archives" des lignes modifiées en faisant une copie de la ligne actuelle dans une table secondaire.

Prenons un exemple trivial:

CREATE TABLE dbo.bigtable
(
  UpdateDate datetime,
  PK varchar(12) PRIMARY KEY,
  col1 varchar(100),
  col2 int,
  col3 varchar(20),
  .
  .
  .
  colN datetime
);

Table d'archive:

CREATE TABLE dbo.bigtable_archive
(
  UpdateDate datetime,
  PK varchar(12) NOT NULL,
  col1 varchar(100),
  col2 int,
  col3 varchar(20),
  .
  .
  .
  colN datetime
);

Avant l'exécution de toute mise à jour dbo.bigtable, une copie de la ligne est créée dansdbo.bigtable_archive , puis dbo.bigtable.UpdateDatemise à jour avec la date actuelle.

Par conséquent, UNIONle fait de regrouper les deux tables et de les regrouper par PKcrée une chronologie des modifications, lorsqueUpdateDate .

Je souhaite créer un rapport détaillant les différences entre les lignes, triées par UpdateDate, regroupées par PK, au format suivant:

PK,   UpdateDate,  ColumnName,  Old Value,   New Value

Old Valueet New Valuepeuvent être les colonnes pertinentes transtypées en VARCHAR(MAX)(il n'y a pas TEXTouBYTE colonnes impliquées), car je n'ai pas besoin d'effectuer de post-traitement des valeurs elles-mêmes.

Pour le moment, je ne peux pas penser à une manière sensée de le faire pour une grande quantité de colonnes, sans recourir à la génération des requêtes par programme - je devrais peut-être le faire.

Ouvert à beaucoup d'idées, je vais donc ajouter une prime à la question après 2 jours.

Réponses:


15

Cela ne va pas sembler joli, surtout compte tenu des plus de 300 colonnes et de l'indisponibilité de LAG, et il est peu probable qu'il fonctionne extrêmement bien, mais tout comme quelque chose pour commencer, j'essaierais l'approche suivante:

  • UNION les deux tableaux.
  • Pour chaque PK dans l'ensemble combiné, obtenez sa précédente "incarnation" dans la table d'archive (l'implémentation ci-dessous utilise OUTER APPLY+TOP (1) comme un pauvre LAG).
  • Convertissez chaque colonne de données varchar(max)et débloquez-les par paires, c'est-à-dire la valeur actuelle et la valeur précédente (CROSS APPLY (VALUES ...) fonctionne bien pour cette opération).
  • Enfin, filtrez les résultats en fonction de la différence entre les valeurs de chaque paire.

Le Transact-SQL de ce qui précède tel que je le vois:

WITH
  Combined AS
  (
    SELECT * FROM dbo.bigtable
    UNION ALL
    SELECT * FROM dbo.bigtable_archive
  ) AS derived,
  OldAndNew AS
  (
    SELECT
      this.*,
      OldCol1 = last.Col1,
      OldCol2 = last.Col2,
      ...
    FROM
      Combined AS this
      OUTER APPLY
      (
        SELECT TOP (1)
          *
        FROM
          dbo.bigtable_archive
        WHERE
          PK = this.PK
          AND UpdateDate < this.UpdateDate
        ORDER BY
          UpdateDate DESC
      ) AS last
  )
SELECT
  t.PK,
  t.UpdateDate,
  x.ColumnName,
  x.OldValue,
  x.NewValue
FROM
  OldAndNew AS t
  CROSS APPLY
  (
    VALUES
    ('Col1', CAST(t.OldCol1 AS varchar(max), CAST(t.Col1 AS varchar(max))),
    ('Col2', CAST(t.OldCol2 AS varchar(max), CAST(t.Col2 AS varchar(max))),
    ...
  ) AS x (ColumnName, OldValue, NewValue)
WHERE
  NOT EXISTS (SELECT x.OldValue INTERSECT x.NewValue)
ORDER BY
  t.PK,
  t.UpdateDate,
  x.ColumnName
;

13

Si vous décochez les données dans une table temporaire

create table #T
(
  PK varchar(12) not null,
  UpdateDate datetime not null,
  ColumnName nvarchar(128) not null,
  Value varchar(max),
  Version int not null
);

Vous pouvez faire correspondre les lignes pour trouver une nouvelle et une ancienne valeur avec une auto-jointure activée PK,ColumnName et Version = Version + 1.

La partie pas si jolie est, bien sûr, de faire pivoter vos 300 colonnes dans la table temporaire à partir des deux tables de base.

XML à la rescousse pour rendre les choses moins gênantes.

Il est possible d'annuler le pivotement des données avec XML sans avoir à connaître les colonnes réelles de la table qui ne seront pas pivotées. Les noms de colonne doivent être valides en tant que noms d'élément en XML sinon cela échouera.

L'idée est de créer un XML pour chaque ligne ayant toutes les valeurs de cette ligne.

select bt.PK,
       bt.UpdateDate,
       (select bt.* for xml path(''), elements xsinil, type) as X
from dbo.bigtable as bt;
<UpdateDate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2001-01-03T00:00:00</UpdateDate>
<PK xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">PK1</PK>
<col1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">c1_1_3</col1>
<col2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">3</col2>
<col3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:nil="true" />
<colN xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">2001-01-03T00:00:00</colN>

elements xsinil est là pour créer des éléments pour les colonnes avec NULL .

Le XML peut ensuite être déchiqueté en utilisant nodes('*') pour obtenir une ligne pour chaque colonne et utiliser local-name(.)pour obtenir le nom de l'élément et text()obtenir la valeur.

  select C1.PK,
         C1.UpdateDate,
         T.X.value('local-name(.)', 'nvarchar(128)') as ColumnName,
         T.X.value('text()[1]', 'varchar(max)') as Value
  from C1
    cross apply C1.X.nodes('row/*') as T(X)

Solution complète ci-dessous. Notez que Versionc'est inversé. 0 = Dernière version.

create table #X
(
  PK varchar(12) not null,
  UpdateDate datetime not null,
  Version int not null,
  RowData xml not null
);

create table #T
(
  PK varchar(12) not null,
  UpdateDate datetime not null,
  ColumnName nvarchar(128) not null,
  Value varchar(max),
  Version int not null
);


insert into #X(PK, UpdateDate, Version, RowData)
select bt.PK,
       bt.UpdateDate,
       0,
       (select bt.* for xml path(''), elements xsinil, type)
from dbo.bigtable as bt
union all
select bt.PK,
       bt.UpdateDate,
       row_number() over(partition by bt.PK order by bt.UpdateDate desc),
       (select bt.* for xml path(''), elements xsinil, type)
from dbo.bigtable_archive as bt;

with C as 
(
  select X.PK,
         X.UpdateDate,
         X.Version,
         T.C.value('local-name(.)', 'nvarchar(128)') as ColumnName,
         T.C.value('text()[1]', 'varchar(max)') as Value
  from #X as X
    cross apply X.RowData.nodes('*') as T(C)
)
insert into #T (PK, UpdateDate, ColumnName, Value, Version)
select C.PK,
       C.UpdateDate,
       C.ColumnName,
       C.Value,
       C.Version
from C 
where C.ColumnName not in (N'PK', N'UpdateDate');

/*
option (querytraceon 8649);

The above query might need some trick to go parallel.
For the testdata I had on my machine exection time is 16 seconds vs 2 seconds
https://sqlkiwi.blogspot.com/2011/12/forcing-a-parallel-query-execution-plan.html
http://dataeducation.com/next-level-parallel-plan-forcing-an-alternative-to-8649/

*/

select New.PK,
       New.UpdateDate,
       New.ColumnName,
       Old.Value as OldValue,
       New.Value as NewValue
from #T as New
  left outer join #T as Old
    on Old.PK = New.PK and
       Old.ColumnName = New.ColumnName and
       Old.Version = New.Version + 1;

6

Je vous suggère une autre approche.

Bien que vous ne puissiez pas modifier l'application actuelle, il se peut que vous puissiez modifier le comportement de la base de données.

Si possible, j'ajouterais deux TRIGGERS aux tables actuelles.

Un INSTEAD OF INSERT sur dbo.bigtable_archive qui ajoute le nouvel enregistrement uniquement s'il n'existe pas actuellement.

CREATE TRIGGER dbo.IoI_BTA
ON dbo.bigtable_archive
INSTEAD OF INSERT
AS
BEGIN
    IF NOT EXISTs(SELECT 1 
                  FROM dbo.bigtable_archive bta
                  INNER JOIN inserted i
                  ON  bta.PK = i.PK
                  AND bta.UpdateDate = i.UpdateDate)
    BEGIN
        INSERT INTO dbo.bigtable_archive
        SELECT * FROM inserted;
    END
END

Et un déclencheur AFTER INSERT sur bigtable qui fait exactement le même travail, mais en utilisant les données de bigtable.

CREATE TRIGGER dbo.IoI_BT
ON dbo.bigtable
AFTER INSERT
AS
BEGIN
    IF NOT EXISTS(SELECT 1 
                  FROM dbo.bigtable_archive bta
                  INNER JOIN inserted i
                  ON  bta.PK = i.PK
                  AND bta.UpdateDate = i.UpdateDate)
    BEGIN
        INSERT INTO dbo.bigtable_archive
        SELECT * FROM inserted;
    END
END

Ok, j'ai mis en place un petit exemple ici avec ces valeurs initiales:

SELECT * FROM bigtable;
SELECT * FROM bigtable_archive;
UpdateDate | PK | col1 | col2 | col3
: ------------------ | : - | : --- | ---: | : ---
02/01/2017 00:00:00 | ABC | C3 | 1 | C1  

UpdateDate | PK | col1 | col2 | col3
: ------------------ | : - | : --- | ---: | : ---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  

Vous devez maintenant insérer dans bigtable_archive tous les enregistrements en attente de bigtable.

INSERT INTO bigtable_archive
SELECT *
FROM   bigtable
WHERE  UpdateDate >= '20170102';
SELECT * FROM bigtable_archive;
GO
UpdateDate | PK | col1 | col2 | col3
: ------------------ | : - | : --- | ---: | : ---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  
02/01/2017 00:00:00 | ABC | C3 | 1 | C1  

Maintenant, la prochaine fois que l'application tentera d'insérer un enregistrement sur la table bigtable_archive, les déclencheurs détecteront s'il existe et l'insertion sera évitée.

INSERT INTO dbo.bigtable_archive VALUES('20170102', 'ABC', 'C3', 1, 'C1');
GO
SELECT * FROM bigtable_archive;
GO
UpdateDate | PK | col1 | col2 | col3
: ------------------ | : - | : --- | ---: | : ---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  
02/01/2017 00:00:00 | ABC | C3 | 1 | C1  

De toute évidence, vous pouvez maintenant obtenir la chronologie des modifications en interrogeant uniquement la table d'archives. Et l'application ne réalisera jamais qu'un déclencheur fait tranquillement le travail sous les couvertures.

dbfiddle ici


4

Proposition de travail, avec quelques exemples de données, peut être trouvée @ rextester: bigtable unpivot


L'essentiel de l'opération:

1 - Utilisez syscolumns et for xml pour générer dynamiquement nos listes de colonnes pour l'opération de pivotement; toutes les valeurs seront converties en varchar (max), w / NULLs étant convertis en la chaîne 'NULL' (ceci résout le problème avec unpivot sautant les valeurs NULL)

2 - Générez une requête dynamique pour annuler le pivotement des données dans la table temporaire #columns

  • Pourquoi une table temporaire vs CTE (via avec clause)? préoccupé par un problème de performances potentiel pour un grand volume de données et une auto-jointure CTE sans index / schéma de hachage utilisable; une table temporaire permet de créer un index qui devrait améliorer les performances de l'auto-jointure [voir auto-jointure CTE lente ]
  • Les données sont écrites dans #colonnes dans l'ordre PK + ColName + UpdateDate, ce qui nous permet de stocker les valeurs PK / Colname dans des lignes adjacentes; une colonne d'identité ( rid ) nous permet d'auto-joindre ces lignes consécutives via rid = rid + 1

3 - Effectuez une auto-jointure de la table #temp pour générer la sortie souhaitée

Couper-coller du rextester ...

Créez des exemples de données et notre table #columns:

CREATE TABLE dbo.bigtable
(UpdateDate datetime      not null
,PK         varchar(12)   not null
,col1       varchar(100)      null
,col2       int               null
,col3       varchar(20)       null
,col4       datetime          null
,col5       char(20)          null
,PRIMARY KEY (PK)
);

CREATE TABLE dbo.bigtable_archive
(UpdateDate datetime      not null
,PK         varchar(12)   not null
,col1       varchar(100)      null
,col2       int               null
,col3       varchar(20)       null
,col4       datetime          null
,col5       char(20)          null
,PRIMARY KEY (PK, UpdateDate)
);

insert into dbo.bigtable         values ('20170512', 'ABC', NULL, 6, 'C1', '20161223', 'closed')

insert into dbo.bigtable_archive values ('20170427', 'ABC', NULL, 6, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170315', 'ABC', NULL, 5, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170212', 'ABC', 'C1', 1, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170109', 'ABC', 'C1', 1, 'C1', '20160513', 'open')

insert into dbo.bigtable         values ('20170526', 'XYZ', 'sue', 23, 'C1', '20161223', 're-open')

insert into dbo.bigtable_archive values ('20170401', 'XYZ', 'max', 12, 'C1', '20160825', 'cancel')
insert into dbo.bigtable_archive values ('20170307', 'XYZ', 'bob', 12, 'C1', '20160825', 'cancel')
insert into dbo.bigtable_archive values ('20170223', 'XYZ', 'bob', 12, 'C1', '20160820', 'open')
insert into dbo.bigtable_archive values ('20170214', 'XYZ', 'bob', 12, 'C1', '20160513', 'open')
;

create table #columns
(rid        int           identity(1,1)
,PK         varchar(12)   not null
,UpdateDate datetime      not null
,ColName    varchar(128)  not null
,ColValue   varchar(max)      null
,PRIMARY KEY (rid, PK, UpdateDate, ColName)
);

Les tripes de la solution:

declare @columns_max varchar(max),
        @columns_raw varchar(max),
        @cmd         varchar(max)

select  @columns_max = stuff((select ',isnull(convert(varchar(max),'+name+'),''NULL'') as '+name
                from    syscolumns
                where   id   = object_id('dbo.bigtable')
                and     name not in ('PK','UpdateDate')
                order by name
                for xml path(''))
            ,1,1,''),
        @columns_raw = stuff((select ','+name
                from    syscolumns
                where   id   = object_id('dbo.bigtable')
                and     name not in ('PK','UpdateDate')
                order by name
                for xml path(''))
            ,1,1,'')


select @cmd = '
insert #columns (PK, UpdateDate, ColName, ColValue)
select PK,UpdateDate,ColName,ColValue
from
(select PK,UpdateDate,'+@columns_max+' from bigtable
 union all
 select PK,UpdateDate,'+@columns_max+' from bigtable_archive
) p
unpivot
  (ColValue for ColName in ('+@columns_raw+')
) as unpvt
order by PK, ColName, UpdateDate'

--select @cmd

execute(@cmd)

--select * from #columns order by rid
;

select  c2.PK, c2.UpdateDate, c2.ColName as ColumnName, c1.ColValue as 'Old Value', c2.ColValue as 'New Value'
from    #columns c1,
        #columns c2
where   c2.rid                       = c1.rid + 1
and     c2.PK                        = c1.PK
and     c2.ColName                   = c1.ColName
and     isnull(c2.ColValue,'xxx')   != isnull(c1.ColValue,'xxx')
order by c2.UpdateDate, c2.PK, c2.ColName
;

Et les résultats:

entrez la description de l'image ici

Remarque: les excuses ... n'ont pas pu trouver un moyen facile de couper-coller la sortie du rextester dans un bloc de code. Je suis ouvert aux suggestions.


Problèmes / préoccupations potentiels:

1 - la conversion des données en un varchar générique (max) peut entraîner une perte de précision des données, ce qui peut signifier que nous manquons certains changements de données; considérons les paires datetime et float suivantes qui, lorsqu'elles sont converties / transtypées en 'varchar (max)' générique, perdent leur précision (c'est-à-dire que les valeurs converties sont les mêmes):

original value       varchar(max)
-------------------  -------------------
06/10/2017 10:27:15  Jun 10 2017 10:27AM
06/10/2017 10:27:18  Jun 10 2017 10:27AM

    234.23844444                 234.238
    234.23855555                 234.238

    29333488.888            2.93335e+007
    29333499.999            2.93335e+007

Bien que la précision des données puisse être maintenue, elle nécessiterait un peu plus de codage (par exemple, un cast basé sur les types de données de la colonne source); pour l'instant, j'ai choisi de m'en tenir au varchar générique (max) selon la recommandation de l'OP (et en supposant que l'OP connaît suffisamment les données pour savoir que nous ne rencontrerons aucun problème de perte de précision des données).

2 - pour de très grands ensembles de données, nous courons le risque de faire sauter certaines ressources du serveur, qu'il s'agisse d'espace tempdb et / ou de cache / mémoire; le principal problème provient de l'explosion des données qui se produit pendant un pivot (par exemple, nous passons de 1 ligne et 302 éléments de données à 300 lignes et 1 200 à 1 500 éléments de données, dont 300 copies des colonnes PK et UpdateDate, 300 noms de colonne)


1

Cette approche utilise une requête dynamique pour générer un sql pour obtenir les modifications. Le SP prend un nom de table et de schéma et donne la sortie que vous désirez.

Les hypothèses sont que les colonnes PK et UpdateDate sont présentes dans toutes les tables. Et toutes les tables d'archives ont le format originalTableName + "_archive" ..

NB: je n'ai pas vérifié les performances.

NB: étant donné que cela utilise SQL dynamique, je dois ajouter une mise en garde concernant l'injection de sécurité / SQL. Restreignez l'accès au SP et ajoutez d'autres validations pour empêcher l'injection SQL.

    CREATE proc getTableChanges
    @schemaname  varchar(255),
    @tableName varchar(255)
    as

    declare @strg nvarchar(max), @colNameStrg nvarchar(max)='', @oldValueString nvarchar(max)='', @newValueString nvarchar(max)=''

    set @strg = '
    with cte as (

    SELECT  * , ROW_NUMBER() OVER(partition by PK ORDER BY UpdateDate) as RowNbr
    FROM    (

        SELECT  *
        FROM    [' + @schemaname + '].[' + @tableName + ']

        UNION

        SELECT  *
        FROM    [' + @schemaname + '].[' + @tableName + '_archive]

        ) a

    )
    '


    SET @strg = @strg + '

    SELECT  a.pk, a.updateDate, 
    CASE '

    DECLARE @colName varchar(255)
    DECLARE cur CURSOR FOR
        SELECT  COLUMN_NAME
        FROM    INFORMATION_SCHEMA.COLUMNS
        WHERE TABLE_SCHEMA = @schemaname
        AND TABLE_NAME = @tableName
        AND COLUMN_NAME NOT IN ('PK', 'Updatedate')

    OPEN cur
    FETCH NEXT FROM cur INTO @colName 

    WHILE @@FETCH_STATUS = 0
    BEGIN

        SET @colNameStrg  = @colNameStrg  + ' when a.' + @colName + ' <> b.' + @colName + ' then ''' + @colName + ''' '
        SET @oldValueString = @oldValueString + ' when a.' + @colName + ' <> b.' + @colName + ' then cast(a.' + @colName + ' as varchar(max))'
        SET @newValueString = @newValueString + ' when a.' + @colName + ' <> b.' + @colName + ' then cast(b.' + @colName + ' as varchar(max))'


    FETCH NEXT FROM cur INTO @colName 
    END

    CLOSE cur
    DEALLOCATE cur


    SET @colNameStrg = @colNameStrg  + '    END as ColumnChanges '
    SET @oldValueString = 'CASE ' + @oldValueString + ' END as OldValue'
    SET @newValueString = 'CASE ' + @newValueString + ' END as NewValue'

    SET @strg = @strg + @colNameStrg + ',' + @oldValueString + ',' + @newValueString

    SET @strg = @strg + '
        FROM    cte a join cte b on a.PK = b.PK and a.RowNbr + 1 = b.RowNbr 
        ORDER BY  a.pk, a.UpdateDate
    '

    print @strg

    execute sp_executesql @strg


    go

Exemple d'appel:

exec getTableChanges 'dbo', 'bigTable'

Si je ne me trompe pas, cela n'attrape pas plusieurs modifications apportées à la même ligne, n'est-ce pas?
Mikael Eriksson

c'est vrai .. plusieurs colonnes mises à jour en même temps ne seront pas capturées. seule la première colonne avec un changement sera capturée.
Dharmendar Kumar 'DK'

1

J'utilise AdventureWorks2012`, Production.ProductCostHistory et Production.ProductListPriceHistory dans mon exemple. Ce n'est peut-être pas un exemple parfait de table d'historique, "mais le script est capable de rassembler la sortie souhaitée et la sortie correcte".

     DECLARE @sql NVARCHAR(MAX)
    ,@columns NVARCHAR(Max)
    ,@table VARCHAR(200) = 'ProductCostHistory'
    ,@Schema VARCHAR(200) = 'Production'
    ,@Archivecolumns NVARCHAR(Max)
    ,@ColForUnpivot NVARCHAR(Max)
    ,@ArchiveColForUnpivot NVARCHAR(Max)
    ,@PKCol VARCHAR(200) = 'ProductID'
    ,@UpdatedCol VARCHAR(200) = 'modifiedDate'
    ,@Histtable VARCHAR(200) = 'ProductListPriceHistory'
SELECT @columns = STUFF((
            SELECT ',CAST(p.' + QUOTENAME(column_name) + ' AS VARCHAR(MAX)) AS ' + QUOTENAME(column_name)
            FROM information_schema.columns
            WHERE table_name = @table
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')
    ,@Archivecolumns = STUFF((
            SELECT ',CAST(p1.' + QUOTENAME(column_name) + ' AS VARCHAR(MAX)) AS ' + QUOTENAME('A_' + column_name)
            FROM information_schema.columns
            WHERE table_name = @Histtable
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')
    ,@ColForUnpivot = STUFF((
            SELECT ',' + QUOTENAME(column_name)
            FROM information_schema.columns
            WHERE table_name = @table
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')
    ,@ArchiveColForUnpivot = STUFF((
            SELECT ',' + QUOTENAME('A_' + column_name)
            FROM information_schema.columns
            WHERE table_name = @Histtable
                AND column_name NOT IN (
                    @PKCol
                    ,@UpdatedCol
                    )
            ORDER BY ORDINAL_POSITION
            FOR XML PATH('')
            ), 1, 1, '')

--SELECT @columns   ,@Archivecolumns    ,@ColForUnpivot
SET @sql = N' 
    SELECT ' + @PKCol + ', ColumnName,
            OldValue,NewValue,' + @UpdatedCol + '
    FROM    (  
    SELECT p.' + @PKCol + '
        ,p.' + @UpdatedCol + '
        ,' + @columns + '
        ,' + @Archivecolumns + '
    FROM ' + @Schema + '.' + @table + ' p
    left JOIN ' + @Schema + '.' + @Histtable + ' p1 ON p.' + @PKCol + ' = p1.' + @PKCol + '

  ) t
    UNPIVOT (
        OldValue
        FOR ColumnName in (' + @ColForUnpivot + ')
    ) up

     UNPIVOT (
        NewValue
        FOR ColumnName1 in (' + @ArchiveColForUnpivot + ')
    ) up1

--print @sql
EXEC (@sql)

Ici, dans la requête Select interne, considérez p comme table principale et p1 comme table d'historique. Dans unpivot, il est important de le convertir en même type.

Vous pouvez prendre n'importe quel autre nom de table avec moins de nom de colonne pour comprendre mon script.Toute explication doit ensuite me cingler.

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.