Interroga dettagliatamente le differenze tra le righe per una grande quantità di dati


15

Ho un numero di tabelle di grandi dimensioni, ognuna con> 300 colonne. L'applicazione che sto usando crea "archivi" di righe modificate facendo una copia della riga corrente in una tabella secondaria.

Considera un esempio banale:

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

Tavolo d'archivio:

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

Prima di eseguire qualsiasi aggiornamento dbo.bigtable, viene creata una copia della riga dbo.bigtable_archive, quindi dbo.bigtable.UpdateDateviene aggiornata con la data corrente.

Pertanto, UNIONunendo le due tabelle e raggruppandole per PKcrea una sequenza temporale delle modifiche, quando ordinate per UpdateDate.

Desidero creare un rapporto che descriva in dettaglio le differenze tra le righe, ordinate per UpdateDate, raggruppate per PK, nel seguente formato:

PK,   UpdateDate,  ColumnName,  Old Value,   New Value

Old Valuee New Valuepossono essere le colonne pertinenti espresse in un VARCHAR(MAX)(non ci sono TEXToBYTE colonne coinvolte), poiché non ho bisogno di fare alcuna post-elaborazione dei valori stessi.

Al momento non riesco a pensare a un modo sano di farlo per una grande quantità di colonne, senza ricorrere a generare le query a livello di codice - potrei doverlo fare.

Aperto a molte idee, quindi aggiungerò una taglia alla domanda dopo 2 giorni.

Risposte:


15

Questo non sembrerà carino, soprattutto considerando le oltre 300 colonne e l'indisponibilità di LAG, né è probabile che funzioni eccessivamente bene, ma proprio come qualcosa per cominciare, proverei il seguente approccio:

  • UNION i due tavoli.
  • Per ogni PK nel set combinato, ottieni la sua "incarnazione" precedente dalla tabella degli archivi (l'implementazione di seguito usa OUTER APPLY+ TOP (1)come un uomo poveroLAG ).
  • varchar(max)Trasmetti ciascuna colonna di dati e annullale in coppie, ovvero il valore corrente e precedente (CROSS APPLY (VALUES ...) funziona bene per questa operazione).
  • Infine, filtra i risultati in base alla differenza tra i valori di ciascuna coppia.

Il Transact-SQL di quanto sopra come lo vedo io:

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

Se si annullano i pivot dei dati in una tabella temporanea

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

Si potrebbe abbinare le righe per trovare il valore vecchi e nuovi con un auto join su PK, ColumnNamee Version = Version + 1.

La parte non così carina sta, ovviamente, eseguendo il perno delle tue 300 colonne nella tabella temporanea dalle due tabelle di base.

XML per il salvataggio per rendere le cose meno imbarazzanti.

È possibile annullare la rotazione dei dati con XML senza dover sapere quali colonne effettive ci sono nella tabella che verranno annullate. I nomi delle colonne devono essere validi come nomi di elementi in XML o falliranno.

L'idea è di creare un XML per ogni riga con tutti i valori per quella riga.

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è lì per creare elementi per colonne con NULL.

L'XML può quindi essere distrutto utilizzando nodes('*') per ottenere una riga per ogni colonna e utilizzare local-name(.)per ottenere il nome dell'elemento e text()ottenere il valore.

  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)

Soluzione completa di seguito. Si noti che Versionè invertito. 0 = Ultima versione.

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

Ti suggerirei un altro approccio.

Sebbene non sia possibile modificare l'applicazione corrente, è possibile che sia possibile modificare il comportamento del database.

Se possibile, aggiungerei due TRIGGER alle tabelle correnti.

Un INSTEAD OF INSERT su dbo.bigtable_archive che aggiunge il nuovo record solo se non esiste attualmente.

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

E un trigger AFTER INSERT su bigtable che fa esattamente lo stesso lavoro, ma utilizzando i dati di 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, ho creato un piccolo esempio qui con questi valori iniziali:

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

Data aggiornamento | PK | col1 | col2 | Col3
: ------------------ | : - | : --- | ---: | : ---
01/01/2017 00:00:00 | ABC | C1 | 1 | C1  

Ora dovresti inserire in bigtable_archive tutti i record in sospeso da bigtable.

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

Ora, la prossima volta che l'applicazione tenta di inserire un record nella tabella bigtable_archive, i trigger rileveranno se esiste e l'inserimento verrà evitato.

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

Ovviamente ora puoi ottenere la cronologia delle modifiche eseguendo una query solo sulla tabella di archivio. E l'applicazione non si accorgerà mai che un triggger sta facendo tranquillamente il lavoro sotto le coperte.

dbfiddle qui


4

La proposta di lavoro, con alcuni dati di esempio, è reperibile @ rextester: bigtable unpivot


L'essenza dell'operazione:

1 - Usa syscolumns e per xml per generare dinamicamente i nostri elenchi di colonne per l'operazione di non pivot; tutti i valori verranno convertiti in varchar (max), mentre i N / N vengono convertiti nella stringa "NULL" (risolve il problema con i valori NULL saltati da univot)

2 - Genera una query dinamica per annullare la rotazione dei dati nella tabella temp #columns

  • Perché una tabella temporanea vs CTE (tramite con clausola)? preoccupato per un potenziale problema di prestazioni per un grande volume di dati e un self-join CTE senza schema di hashing / indice utilizzabile; una tabella temporanea consente la creazione di un indice che dovrebbe migliorare le prestazioni sul self-join [vedi self-join CTE lento ]
  • I dati vengono scritti in colonne # in ordine PK + ColName + UpdateDate, consentendoci di memorizzare i valori PK / Colname in righe adiacenti; una colonna di identità ( rid ) ci consente di unire noi stessi queste righe consecutive tramite rid = rid + 1

3 - Eseguire un self join della tabella #temp per generare l'output desiderato

Taglia e incolla da rextester ...

Crea alcuni dati di esempio e la nostra tabella #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)
);

Il coraggio della soluzione:

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
;

E i risultati:

inserisci qui la descrizione dell'immagine

Nota: scuse ... non sono riuscito a capire un modo semplice per tagliare e incollare l'output del rextester in un blocco di codice. Sono aperto ai suggerimenti.


Potenziali problemi / preoccupazioni:

1 - la conversione dei dati in un varchar generico (max) può portare a una perdita di precisione dei dati che a sua volta può significare che mancano alcune modifiche ai dati; considerare le seguenti coppie datetime e float che, quando convertite / cast nel generico 'varchar (max)', perdono la loro precisione (cioè i valori convertiti sono gli stessi):

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

Sebbene la precisione dei dati possa essere mantenuta, richiederebbe un po 'più di codifica (ad esempio, casting basato sui tipi di dati della colonna di origine); per ora ho scelto di attenermi al generico varchar (max) secondo le raccomandazioni del PO (e suppongo che il PO conosca i dati abbastanza bene da sapere che non avremo problemi di perdita di precisione dei dati).

2 - per insiemi di dati veramente grandi corriamo il rischio di esaurire alcune risorse del server, che si tratti di spazio tempdb e / o cache / memoria; il problema principale deriva dall'esplosione di dati che si verifica durante un univoco (ad esempio, passiamo da 1 riga e 302 parti di dati a 300 righe e 1200-1500 parti di dati, tra cui 300 copie delle colonne PK e UpdateDate, 300 nomi di colonne)


1

Questo approccio utilizza la query dinamica per generare un sql per ottenere le modifiche. L'SP prende una tabella e il nome dello schema e fornisce l'output desiderato.

I presupposti sono che le colonne PK e UpdateDate siano presenti in tutte le tabelle. E tutte le tabelle di archivio hanno il formato originalTableName + "_archive" ..

NB: Non l'ho verificato per le prestazioni.

NB: poiché questo utilizza sql dinamico, dovrei aggiungere un avvertimento sulla sicurezza / sql injection. Limitare l'accesso a SP e aggiungere altre convalide per impedire l'iniezione 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

Chiamata di esempio:

exec getTableChanges 'dbo', 'bigTable'

Se non sbaglio, questo non rileva più modifiche apportate alla stessa riga, giusto?
Mikael Eriksson,

esatto .. non verranno acquisite più colonne aggiornate contemporaneamente. verrà acquisita solo la prima colonna con una modifica.
Dharmendar Kumar "DK",

1

Sto usando AdventureWorks2012`, Production.ProductCostHistory e Production.ProductListPriceHistory nel mio esempio. Potrebbe non essere un esempio di tabella cronologica perfetta, "ma lo script è in grado di mettere insieme l'output desiderato e l'output corretto".

     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)

Qui nella query di selezione interna considera p come tabella principale e p1 come tabella cronologica. In un pivot è importante convertirlo nello stesso tipo.

Puoi prendere qualsiasi altro nome di tabella con meno nomi di colonna per capire il mio script. Qualsiasi spiegazione deve quindi eseguire il ping.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.