Aumenta un contatore per ogni riga modificata


8

Sto usando SQL Server 2008 Standard, che non ha una SEQUENCEfunzionalità.

Un sistema esterno legge i dati da diverse tabelle dedicate del database principale. Il sistema esterno conserva una copia dei dati e verifica periodicamente la presenza di modifiche ai dati e ne aggiorna la copia.

Per rendere efficiente la sincronizzazione, voglio trasferire solo le righe che sono state aggiornate o inserite dalla sincronizzazione precedente. (Le righe non vengono mai eliminate). Per sapere quali righe sono state aggiornate o inserite dall'ultima sincronizzazione, c'è una bigintcolonna RowUpdateCounterin ogni tabella.

L'idea è che ogni volta che una riga viene inserita o aggiornata, il numero nella sua RowUpdateCountercolonna cambierebbe. I valori che vanno nella RowUpdateCountercolonna dovrebbero essere presi da una sequenza di numeri sempre crescente. I valori nella RowUpdateCountercolonna dovrebbero essere univoci e ogni nuovo valore archiviato in una tabella dovrebbe essere maggiore di qualsiasi valore precedente.

Si prega di consultare gli script che mostrano il comportamento desiderato.

Schema

CREATE TABLE [dbo].[Test](
    [ID] [int] NOT NULL,
    [Value] [varchar](50) NOT NULL,
    [RowUpdateCounter] [bigint] NOT NULL,
CONSTRAINT [PK_Test] PRIMARY KEY CLUSTERED
(
    [ID] ASC
))
GO

CREATE UNIQUE NONCLUSTERED INDEX [IX_RowUpdateCounter] ON [dbo].[Test]
(
    [RowUpdateCounter] ASC
)
GO

INSERIRE alcune righe

INSERT INTO [dbo].[Test]
    ([ID]
    ,[Value]
    ,[RowUpdateCounter])
VALUES
(1, 'A', ???),
(2, 'B', ???),
(3, 'C', ???),
(4, 'D', ???);

Risultato atteso

+----+-------+------------------+
| ID | Value | RowUpdateCounter |
+----+-------+------------------+
|  1 | A     |                1 |
|  2 | B     |                2 |
|  3 | C     |                3 |
|  4 | D     |                4 |
+----+-------+------------------+

I valori generati in RowUpdateCounterpossono essere diverse, dicono 5, 3, 7, 9. Dovrebbero essere univoci e dovrebbero essere maggiori di 0, poiché siamo partiti da una tabella vuota.

INSERISCI e AGGIORNA alcune righe

DECLARE @NewValues TABLE (ID int NOT NULL, Value varchar(50));
INSERT INTO @NewValues (ID, Value) VALUES
(3, 'E'),
(4, 'F'),
(5, 'G'),
(6, 'H');

MERGE INTO dbo.Test WITH (HOLDLOCK) AS Dst
USING
(
    SELECT ID, Value
    FROM @NewValues
)
AS Src ON Dst.ID = Src.ID
WHEN MATCHED THEN
UPDATE SET
     Dst.Value            = Src.Value
    ,Dst.RowUpdateCounter = ???
WHEN NOT MATCHED BY TARGET THEN
INSERT
    (ID
    ,Value
    ,RowUpdateCounter)
VALUES
    (Src.ID
    ,Src.Value
    ,???)
;

Risultato atteso

+----+-------+------------------+
| ID | Value | RowUpdateCounter |
+----+-------+------------------+
|  1 | A     |                1 |
|  2 | B     |                2 |
|  3 | E     |                5 |
|  4 | F     |                6 |
|  5 | G     |                7 |
|  6 | H     |                8 |
+----+-------+------------------+
  • RowUpdateCounterper le righe con ID 1,2dovrebbe rimanere così com'è, poiché queste righe non sono state modificate.
  • RowUpdateCounterper le righe con ID 3,4dovrebbe cambiare, perché sono state aggiornate.
  • RowUpdateCounterper le righe con ID 5,6dovrebbe cambiare, perché sono state inserite.
  • RowUpdateCounterper tutte le righe modificate dovrebbe essere maggiore di 4 (l'ultimo RowUpdateCounterdalla sequenza).

L'ordine in cui i nuovi valori ( 5,6,7,8) vengono assegnati alle righe modificate non ha importanza. I nuovi valori possono presentare lacune, ad esempio 15,26,47,58, ma non dovrebbero mai ridursi.

Esistono diverse tabelle con tali contatori nel database. Non importa se tutti usano la singola sequenza globale per i loro numeri o ogni tabella ha la sua sequenza individuale.


Non voglio usare una colonna con un timbro datetime invece di un contatore intero, perché:

  • L'orologio sul server può saltare sia in avanti che all'indietro. Soprattutto quando si trova su una macchina virtuale.

  • I valori restituiti da funzioni di sistema come SYSDATETIMEsono gli stessi per tutte le righe interessate. Il processo di sincronizzazione dovrebbe essere in grado di leggere le modifiche in batch. Ad esempio, se la dimensione del batch è di 3 righe, dopo il MERGEpassaggio sopra il processo di sincronizzazione leggerà solo le righe E,F,G. La prossima volta che il processo di sincronizzazione verrà eseguito dalla riga H.


Il modo in cui lo sto facendo ora è piuttosto brutto.

Dato che non è presente SEQUENCEin SQL Server 2008, emulo il SEQUENCEda una tabella dedicata con IDENTITYcome mostrato in questa risposta . Questo di per sé è piuttosto brutto ed esacerbato dal fatto che ho bisogno di generare non un singolo, ma un gruppo di numeri contemporaneamente.

Quindi, ho un INSTEAD OF UPDATE, INSERTtrigger su ogni tabella con RowUpdateCountere generare lì i set di numeri richiesti.

In INSERT, UPDATEe le MERGEquery ho impostato RowUpdateCountersu 0, che viene sostituito dai valori corretti nel trigger. Le ???domande sopra sono 0.

Funziona, ma esiste una soluzione più semplice?


4
Potresti usare la versione di riga / data / ora? È un campo binario ma il valore cambia ogni volta che la riga viene aggiornata
James Z,

@JamesZ, ho bisogno di sapere l'ordine in cui sono state cambiate le righe. Il processo di sincronizzazione legge il contatore MAX dalla copia obsoleta della tabella e quindi sa recuperare solo le righe che hanno un contatore maggiore di quel valore. Il rowversionnon mi avrebbe dato questa possibilità, se ho capito bene quello che è ... E 'garantito per essere sempre più?
Vladimir Baranov,


Grazie @MartinSmith, mi sono completamente dimenticato rowversion. Sembra molto allettante. La mia unica preoccupazione è che tutti gli esempi del suo uso che ho visto finora ruotino attorno al rilevamento di una singola riga modificata. Ho bisogno di un modo efficace per sapere quale serie di righe è cambiata da un certo momento. Inoltre, è possibile perdere un aggiornamento?
Vladimir Baranov,

@MartinSmith time = 0: l'ultimo valore di rowversion è, per esempio, 122. time = 1: la transazione Aaggiorna una riga, la suaversione di riga cambia in 123, Anon è ancora impegnata. time = 2: la transazione Baggiorna un'altra riga, la sua rowversion cambia in 124. time = 3: Bcommit. time = 4: il processo di sincronizzazione viene eseguito e recupera tutte le righe con rowversion> 122, il che significa che le righe vengono aggiornate solo da B. time = 5: Acommit. Risultato: le modifiche di Anon verranno mai rilevate dal processo di sincronizzazione. Ho sbagliato? Forse un uso intelligente di MIN_ACTIVE_ROWVERSIONaiuterà?
Vladimir Baranov,

Risposte:


5

È possibile utilizzare una ROWVERSIONcolonna per questo.

La documentazione afferma che

Ogni database ha un contatore che viene incrementato per ogni operazione di inserimento o aggiornamento eseguita su una tabella che contiene una colonna rowversion all'interno del database.

I valori sono BINARY(8)e dovresti considerarli come BINARYpiuttosto che BIGINTdopo 0x7FFFFFFFFFFFFFFFche vanno avanti 0x80...e iniziano a funzionare -9223372036854775808se trattati come firmati bigint.

Di seguito è riportato un esempio completo. Mantenere l'indice sulla ROWVERSIONcolonna sarà costoso se si dispone di molti aggiornamenti, quindi si consiglia di testare il carico di lavoro con e senza per vedere se vale il costo.

CREATE TABLE [dbo].[Test]
  (
     [ID]               [INT] NOT NULL CONSTRAINT [PK_Test] PRIMARY KEY,
     [Value]            [VARCHAR](50) NOT NULL,
     [RowUpdateCounter] [ROWVERSION] NOT NULL UNIQUE NONCLUSTERED
  )

INSERT INTO [dbo].[Test]
            ([ID],
             [Value])
VALUES     (1,'Foo'),
            (2,'Bar'),
            (3,'Baz');

DECLARE @RowVersion_LastSynch ROWVERSION = MIN_ACTIVE_ROWVERSION();

UPDATE [dbo].[Test]
SET    [Value] = 'X'
WHERE  [ID] = 2;

DECLARE @RowVersion_ThisSynch ROWVERSION = MIN_ACTIVE_ROWVERSION();

SELECT *
FROM   [dbo].[Test]
WHERE  [RowUpdateCounter] >= @RowVersion_LastSynch
       AND RowUpdateCounter < @RowVersion_ThisSynch;

/*TODO: Store @RowVersion_ThisSynch somewhere*/

DROP TABLE [dbo].[Test] 

Grazie. Dopo aver letto i documenti, penso che invece @@DBTSdovrebbe esserci MIN_ACTIVE_ROWVERSION(), e se usare il MIN_ACTIVE_ROWVERSION()confronto <=dovrebbe diventare <e >diventare >=.
Vladimir Baranov,

Secondo i documenti c'è una differenza sostanziale tra @@DBTSe MIN_ACTIVE_ROWVERSION()se ci sono transazioni attive non impegnate. Se un'applicazione utilizza @@DBTSpiuttosto che MIN_ACTIVE_ROWVERSION, è possibile perdere le modifiche attive al momento della sincronizzazione.
Vladimir Baranov,

@VladimirBaranov - sì, d'accordo, modificato.
Martin Smith,

-2

Hai provato a usare l' IDENTITYopzione?

Per esempio:

[RowUpdateCounter] [bigint] NOT NULL IDENTITY(1,2)

dove

  • 1 -> Valore iniziale
  • 2 -> ogni nuova riga è incrementata di questo

Questo è simile a SEQUENCE in Oracle.


SQL Server non ha alcuna "opzione AUTOINCREMENT"
Martin Smith,

sì. È supportato da Access. Il server SQL supporta l'opzione IDENTITY. Ho aggiornato la mia risposta sopra. Grazie !!
Bibhuti Bhusan Padhi,

4
IDENTITYnon fa ciò che è necessario per quanto riguarda l'incremento automatico sia degli aggiornamenti che degli inserti .
Martin Smith,

@BibhutiBhusanPadhi, devo sapere quali righe sono state aggiornate. Non vedo quanto semplice IDENTITYpossa aiutare.
Vladimir Baranov,
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.