Augmentez un compteur pour chaque ligne modifiée

8

J'utilise SQL Server 2008 Standard, qui n'a pas de SEQUENCEfonctionnalité.

Un système externe lit les données de plusieurs tables dédiées de la base de données principale. Le système externe conserve une copie des données et vérifie périodiquement les modifications des données et actualise sa copie.

Pour rendre la synchronisation efficace, je souhaite transférer uniquement les lignes qui ont été mises à jour ou insérées depuis la synchronisation précédente. (Les lignes ne sont jamais supprimées). Pour savoir quelles lignes ont été mises à jour ou insérées depuis la dernière synchronisation, il y a une bigintcolonne RowUpdateCounterdans chaque table.

L'idée est qu'à chaque fois qu'une ligne est insérée ou mise à jour, le nombre dans sa RowUpdateCountercolonne change. Les valeurs qui entrent dans la RowUpdateCountercolonne doivent être tirées d'une séquence de nombres toujours croissante. Les valeurs de la RowUpdateCountercolonne doivent être uniques et chaque nouvelle valeur stockée dans une table doit être supérieure à toute valeur précédente.

Veuillez consulter les scripts qui montrent le comportement souhaité.

Schéma

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

INSÉRER quelques lignes

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

Résultat attendu

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

Les valeurs générées dans RowUpdateCounterpeuvent être différentes, par exemple 5, 3, 7, 9. Ils doivent être uniques et supérieurs à 0, car nous sommes partis d'une table vide.

INSÉRER et METTRE À JOUR certaines lignes

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

Résultat attendu

+----+-------+------------------+
| ID | Value | RowUpdateCounter |
+----+-------+------------------+
|  1 | A     |                1 |
|  2 | B     |                2 |
|  3 | E     |                5 |
|  4 | F     |                6 |
|  5 | G     |                7 |
|  6 | H     |                8 |
+----+-------+------------------+
  • RowUpdateCounterpour les lignes avec ID 1,2doivent rester telles quelles, car ces lignes n'ont pas été modifiées.
  • RowUpdateCounterpour les lignes avec ID 3,4doit changer, car elles ont été mises à jour.
  • RowUpdateCounterpour les lignes avec ID 5,6doit changer, car elles ont été insérées.
  • RowUpdateCounterpour toutes les lignes modifiées doit être supérieur à 4 (le dernier RowUpdateCounterde la séquence).

L'ordre dans lequel les nouvelles valeurs ( 5,6,7,8) sont attribuées aux lignes modifiées n'a pas vraiment d'importance. Les nouvelles valeurs peuvent présenter des lacunes, par exemple 15,26,47,58, mais elles ne devraient jamais diminuer.

Il existe plusieurs tables avec de tels compteurs dans la base de données. Peu importe si tous utilisent la seule séquence globale pour leurs numéros, ou si chaque table a sa propre séquence individuelle.


Je ne veux pas utiliser une colonne avec un tampon datetime au lieu d'un compteur entier, car:

  • L'horloge du serveur peut sauter en avant et en arrière. Surtout quand il est sur une machine virtuelle.

  • Les valeurs renvoyées par les fonctions système comme SYSDATETIMEsont les mêmes pour toutes les lignes affectées. Le processus de synchronisation doit pouvoir lire les modifications des lots. Par exemple, si la taille du lot est de 3 lignes, après l' MERGEétape ci-dessus, le processus de synchronisation ne lira que les lignes E,F,G. Lorsque le processus de synchronisation est exécuté la prochaine fois, il continuera à partir de la ligne H.


La façon dont je le fais maintenant est plutôt moche.

Puisqu'il n'y en a pas SEQUENCEdans SQL Server 2008, j'émule le SEQUENCEpar une table dédiée avec IDENTITYcomme indiqué dans cette réponse . Cela en soi est assez moche et exacerbé par le fait que je dois générer non pas un seul, mais un lot de nombres à la fois.

Ensuite, j'ai un INSTEAD OF UPDATE, INSERTdéclencheur sur chaque table avec RowUpdateCounteret j'y génère les ensembles de nombres requis.

Dans les requêtes INSERT, UPDATEet MERGEj'ai défini RowUpdateCounter0, qui est remplacé par les valeurs correctes dans le déclencheur. Les ???dans les requêtes ci-dessus sont 0.

Cela fonctionne, mais existe-t-il une solution plus simple?

Vladimir Baranov
la source
4
Pourriez-vous utiliser la version de ligne / l'horodatage? C'est un champ binaire mais la valeur changera à chaque mise à jour de la ligne
James Z
@JamesZ, j'ai besoin de connaître l'ordre dans lequel les lignes ont été modifiées. Le processus de synchronisation lit le compteur MAX à partir de la copie obsolète de la table, puis il sait récupérer uniquement les lignes dont le compteur est supérieur à cette valeur. Le rowversionne me donnerait pas cette possibilité, si je comprends bien ce qu'il est ... Est - il sûr d'être de plus en plus?
Vladimir Baranov
Merci @MartinSmith, j'ai complètement oublié rowversion. Ça a l'air très tentant. Ma seule préoccupation est que tous les exemples d'utilisation que j'ai vus jusqu'à présent tournent autour de la détection d'une modification d'une seule ligne. J'ai besoin d'un moyen efficace de savoir quel ensemble de lignes a changé depuis un certain moment. D'ailleurs, est-il possible de manquer une mise à jour?
Vladimir Baranov
@MartinSmith time = 0: la dernière valeur de version de ligne est, disons, 122. time = 1: la transaction Amet à jour une ligne, sa version de ligne passe à 123, An'est pas encore validée . time = 2: la transaction Bmet à jour une autre ligne, sa version de ligne passe à 124. time = 3: Bvalide. time = 4: le processus de synchronisation s'exécute et récupère toutes les lignes avec rowversion> 122, ce qui signifie que les lignes sont mises à jour uniquement par B. time = 5: Avalide. Résultat: les modifications apportées par Ane seront jamais détectées par le processus de synchronisation. Ai-je tort? Peut-être qu'une utilisation intelligente de MIN_ACTIVE_ROWVERSIONsera utile?
Vladimir Baranov

Réponses:

5

Vous pouvez utiliser une ROWVERSIONcolonne pour cela.

La documentation indique que

Chaque base de données possède un compteur incrémenté pour chaque opération d'insertion ou de mise à jour effectuée sur une table contenant une colonne de version de ligne dans la base de données.

Les valeurs sont BINARY(8)et vous devez les considérer comme BINARYplutôt que BIGINTcomme après 0x7FFFFFFFFFFFFFFFqu'elles ont 0x80...commencé et qu'elles commencent à fonctionner -9223372036854775808si elles sont traitées comme signées bigint.

Un exemple complet de travail est ci-dessous. Le maintien de l'index sur la ROWVERSIONcolonne coûtera cher si vous avez beaucoup de mises à jour, vous voudrez peut-être tester votre charge de travail avec et sans pour voir si elle en vaut le coût.

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
la source
Je vous remercie. Après avoir lu la documentation, je pense qu'au lieu de cela, @@DBTSil devrait y avoir MIN_ACTIVE_ROWVERSION(), et si vous utilisez la MIN_ACTIVE_ROWVERSION()comparaison <=devrait devenir <et >devenir >=.
Vladimir Baranov
Selon les documents, il existe une différence importante entre @@DBTSet MIN_ACTIVE_ROWVERSION()s'il y a des transactions non engagées actives. Si une application utilise @@DBTSplutôt que MIN_ACTIVE_ROWVERSION, il est possible d'omettre les modifications actives lors de la synchronisation.
Vladimir Baranov
@VladimirBaranov - oui, d'accord, édité.
Martin Smith
-2

Avez-vous essayé d'utiliser l' IDENTITYoption?

Par exemple:

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

  • 1 -> Valeur de départ
  • 2 -> chaque nouvelle ligne est incrémentée de ce

Ceci est similaire à SEQUENCE dans Oracle.

Bibhuti Bhusan Padhi
la source
SQL Server n'a pas d '"option AUTOINCREMENT"
Martin Smith
Oui. Il est pris en charge par Access. Le serveur SQL prend en charge l'option IDENTITY. J'ai mis à jour ma réponse ci-dessus. Merci !!
Bibhuti Bhusan Padhi
4
IDENTITYne fait pas ce qui est requis pour l'incrémentation automatique des mises à jour et des insertions .
Martin Smith
@BibhutiBhusanPadhi, j'ai besoin de savoir quelles lignes ont été mises à jour. Je ne vois pas comment simple IDENTITYpeut aider.
Vladimir Baranov