Erhöhen Sie einen Zähler für jede geänderte Zeile

8

Ich verwende SQL Server 2008 Standard, der keine SEQUENCEFunktion hat.

Ein externes System liest Daten aus mehreren dedizierten Tabellen der Hauptdatenbank. Das externe System speichert eine Kopie der Daten und überprüft diese regelmäßig auf Änderungen der Daten und aktualisiert ihre Kopie.

Um die Synchronisierung effizient zu gestalten, möchte ich nur Zeilen übertragen, die seit der vorherigen Synchronisierung aktualisiert oder eingefügt wurden. (Die Zeilen werden nie gelöscht). Um zu wissen, welche Zeilen seit der letzten Synchronisierung aktualisiert oder eingefügt wurden, befindet sich in jeder Tabelle eine bigintSpalte RowUpdateCounter.

Die Idee ist, dass sich die Nummer in ihrer RowUpdateCounterSpalte jedes Mal ändert , wenn eine Zeile eingefügt oder aktualisiert wird . Die Werte, die in die RowUpdateCounterSpalte eingegeben werden, sollten einer ständig wachsenden Folge von Zahlen entnommen werden. Die Werte in der RowUpdateCounterSpalte sollten eindeutig sein und jeder neue in einer Tabelle gespeicherte Wert sollte größer sein als jeder vorherige Wert.

Bitte beachten Sie die Skripte, die das gewünschte Verhalten zeigen.

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

Fügen Sie einige Zeilen ein

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

Erwartetes Ergebnis

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

Die generierten Werte in RowUpdateCounterkönnen beispielsweise unterschiedlich sein 5, 3, 7, 9. Sie sollten eindeutig sein und größer als 0 sein, da wir von einer leeren Tabelle ausgegangen sind.

Einige Zeilen einfügen und aktualisieren

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

Erwartetes Ergebnis

+----+-------+------------------+
| ID | Value | RowUpdateCounter |
+----+-------+------------------+
|  1 | A     |                1 |
|  2 | B     |                2 |
|  3 | E     |                5 |
|  4 | F     |                6 |
|  5 | G     |                7 |
|  6 | H     |                8 |
+----+-------+------------------+
  • RowUpdateCounterfür Zeilen mit ID 1,2sollte unverändert bleiben, da diese Zeilen nicht geändert wurden.
  • RowUpdateCounterfür Zeilen mit ID 3,4sollte sich ändern, da sie aktualisiert wurden.
  • RowUpdateCounterfür Zeilen mit ID 5,6sollte sich ändern, da sie eingefügt wurden.
  • RowUpdateCounterfür alle geänderten Zeilen sollte größer als 4 sein (die letzte RowUpdateCounteraus der Sequenz).

Die Reihenfolge, in der neuen Werten ( 5,6,7,8) geänderten Zeilen zugewiesen werden, spielt keine Rolle. Die neuen Werte können z. B. Lücken aufweisen, 15,26,47,58sollten jedoch niemals abnehmen.

Es gibt mehrere Tabellen mit solchen Zählern in der Datenbank. Es spielt keine Rolle, ob alle die einzelne globale Sequenz für ihre Nummern verwenden oder ob jede Tabelle ihre eigene individuelle Sequenz hat.


Ich möchte keine Spalte mit einem Datums- / Uhrzeitstempel anstelle eines Ganzzahlzählers verwenden, weil:

  • Die Uhr auf dem Server kann sowohl vorwärts als auch rückwärts springen. Besonders wenn es sich um eine virtuelle Maschine handelt.

  • Die von Systemfunktionen zurückgegebenen Werte SYSDATETIMEsind für alle betroffenen Zeilen gleich. Der Synchronisierungsprozess sollte in der Lage sein, Änderungen in Stapeln zu lesen. Wenn die Stapelgröße beispielsweise 3 Zeilen MERGEbeträgt, liest der Synchronisierungsprozess nach dem obigen Schritt nur Zeilen E,F,G. Wenn der Synchronisierungsprozess das nächste Mal ausgeführt wird, wird er ab Zeile fortgesetzt H.


Die Art, wie ich es jetzt mache, ist ziemlich hässlich.

Da es SEQUENCEin SQL Server 2008 keine gibt , emuliere ich die SEQUENCEdurch eine dedizierte Tabelle mit IDENTITYwie in dieser Antwort gezeigt . Dies an sich ist ziemlich hässlich und wird durch die Tatsache verschärft, dass ich nicht eine einzige, sondern eine Reihe von Zahlen gleichzeitig generieren muss.

Dann habe ich INSTEAD OF UPDATE, INSERTauf jeder Tabelle einen Trigger mit dem RowUpdateCounterund generiere dort die erforderlichen Zahlenmengen.

In den INSERT, UPDATEund MERGEAbfragen setze ich RowUpdateCounterauf 0, die durch die richtigen Werte im Trigger ersetzt wird. Die ???in den obigen Abfragen sind 0.

Es funktioniert, aber gibt es eine einfachere Lösung?

Vladimir Baranov
quelle
4
Könnten Sie die Zeilenversion / den Zeitstempel verwenden? Es ist ein binäres Feld, aber der Wert ändert sich jedes Mal, wenn die Zeile aktualisiert wird
James Z
@ JamesZ, ich muss die Reihenfolge kennen, in der die Zeilen geändert wurden. Der Synchronisierungsprozess liest den MAX-Zähler aus der veralteten Kopie der Tabelle und kann dann nur Zeilen abrufen, deren Zähler diesen Wert überschreitet. Das rowversionwürde mir diese Möglichkeit nicht geben, wenn ich richtig verstehe, was es ist ... Wird es garantiert immer größer?
Vladimir Baranov
Vielen Dank an MartinSmith, ich habe es komplett vergessen rowversion. Es sieht sehr verlockend aus. Meine einzige Sorge ist, dass sich alle Beispiele für die Verwendung, die ich bisher gesehen habe, darauf konzentrieren, festzustellen, ob sich eine einzelne Zeile geändert hat. Ich brauche eine effiziente Art und Weise zu wissen , was setzte seit einem bestimmten Zeitpunkt geändert von Zeilen. Ist es außerdem möglich, ein Update zu verpassen?
Vladimir Baranov
@MartinSmith time = 0: Der letzte Zeilenversionswert ist beispielsweise 122. time = 1: Die Transaktion Aaktualisiert eine Zeile, ihre Zeilenversion ändert sich auf 123, Aist noch nicht festgeschrieben. time = 2: Die Transaktion Baktualisiert eine weitere Zeile, ihre Zeilenversion ändert sich in 124. time = 3: BCommits. Zeit = 4: Der Synchronisierungsprozess wird ausgeführt und ruft alle Zeilen mit einer Zeilenversion> 122 ab. Dies bedeutet, dass die Zeilen nur von aktualisiert werden B. Zeit = 5: ACommits. Ergebnis: Änderungen von Awerden vom Synchronisierungsprozess niemals erfasst. Liege ich falsch? Vielleicht MIN_ACTIVE_ROWVERSIONhilft ein kluger Einsatz ?
Vladimir Baranov

Antworten:

5

Sie können hierfür eine ROWVERSIONSpalte verwenden.

Die Dokumentation besagt, dass

Jede Datenbank verfügt über einen Zähler, der für jede Einfüge- oder Aktualisierungsoperation erhöht wird, die für eine Tabelle ausgeführt wird, die eine Zeilenversionsspalte in der Datenbank enthält.

Die Werte sind BINARY(8)und Sie sollten sie als betrachten , BINARYanstatt BIGINTwie nach 0x7FFFFFFFFFFFFFFFes auf geht 0x80...und beginnt Aufarbeitung von , -9223372036854775808wenn sie als unterzeichnet behandelt bigint.

Ein vollständig ausgearbeitetes Beispiel finden Sie unten. Das Verwalten des Index für die ROWVERSIONSpalte ist teuer, wenn Sie viele Updates haben. Daher möchten Sie möglicherweise Ihre Arbeitslast mit und ohne testen, um festzustellen, ob sich die Kosten lohnen.

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] 
Martin Smith
quelle
Vielen Dank. Nach dem Lesen der Dokumente denke ich, dass es statt @@DBTSgeben sollte MIN_ACTIVE_ROWVERSION(), und wenn MIN_ACTIVE_ROWVERSION()Vergleich <=verwendet werden sollte <und >werden >=.
Vladimir Baranov
Laut den Dokumenten gibt es einen wesentlichen Unterschied zwischen @@DBTSund MIN_ACTIVE_ROWVERSION()wenn es aktive nicht festgeschriebene Transaktionen gibt. Wenn eine Anwendung @@DBTSeher als verwendet MIN_ACTIVE_ROWVERSION, können Änderungen übersehen werden, die bei der Synchronisierung aktiv sind.
Vladimir Baranov
@VladimirBaranov - ja, zugestimmt, bearbeitet.
Martin Smith
-2

Haben Sie versucht, die IDENTITYOption zu verwenden?

Zum Beispiel:

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

wo

  • 1 -> Startwert
  • 2 -> jede neue Zeile wird dadurch erhöht

Dies ähnelt SEQUENCE in Oracle.

Bibhuti Bhusan Padhi
quelle
SQL Server hat keine "AUTOINCREMENT-Option"
Martin Smith
Ja. Es wird von Access unterstützt. SQL Server unterstützt die IDENTITY-Option. Ich habe meine Antwort oben aktualisiert. Vielen Dank !!
Bibhuti Bhusan Padhi
4
IDENTITYtut nicht das, was für das automatische Inkrementieren von Updates und Einfügungen erforderlich ist .
Martin Smith
@BibhutiBhusanPadhi, ich muss wissen, welche Zeilen aktualisiert wurden. Ich sehe nicht, wie einfach IDENTITYhelfen kann.
Vladimir Baranov