Aumentar um contador para cada linha alterada

8

Estou usando o SQL Server 2008 Standard, que não possui um SEQUENCErecurso.

Um sistema externo lê dados de várias tabelas dedicadas do banco de dados principal. O sistema externo mantém uma cópia dos dados e verifica periodicamente as alterações nos dados e atualiza sua cópia.

Para tornar a sincronização eficiente, desejo transferir apenas linhas que foram atualizadas ou inseridas desde a sincronização anterior. (As linhas nunca são excluídas). Para saber quais linhas foram atualizadas ou inseridas desde a última sincronização, há uma bigintcoluna RowUpdateCounterem cada tabela.

A ideia é que sempre que uma linha for inserida ou atualizada, o número em sua RowUpdateCountercoluna mudará. Os valores que entram na RowUpdateCountercoluna devem ser obtidos de uma sequência cada vez maior de números. Os valores na RowUpdateCountercoluna devem ser exclusivos e cada novo valor armazenado em uma tabela deve ser maior que qualquer valor anterior.

Por favor, veja os scripts que mostram o comportamento desejado.

Esquema

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

INSERIR algumas linhas

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

Resultado esperado

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

Os valores gerados em RowUpdateCounterpodem ser diferentes, digamos 5, 3, 7, 9,. Eles devem ser únicos e maiores que 0, desde que começamos da tabela vazia.

INSERT e UPDATE algumas linhas

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

Resultado esperado

+----+-------+------------------+
| ID | Value | RowUpdateCounter |
+----+-------+------------------+
|  1 | A     |                1 |
|  2 | B     |                2 |
|  3 | E     |                5 |
|  4 | F     |                6 |
|  5 | G     |                7 |
|  6 | H     |                8 |
+----+-------+------------------+
  • RowUpdateCounterpara linhas com ID 1,2deve permanecer como está, porque essas linhas não foram alteradas.
  • RowUpdateCounterpara linhas com ID 3,4deve mudar, porque foram atualizadas.
  • RowUpdateCounterpara linhas com ID 5,6deve mudar, porque foram inseridas.
  • RowUpdateCounterpara todas as linhas alteradas deve ser maior que 4 (a última RowUpdateCounterda sequência).

A ordem na qual novos valores ( 5,6,7,8) são atribuídos a linhas alteradas não importa realmente. Os novos valores podem ter lacunas, por exemplo 15,26,47,58, mas nunca devem diminuir.

Existem várias tabelas com esses contadores no banco de dados. Não importa se todos eles usam a única seqüência global para seus números ou se cada tabela possui sua própria sequência individual.


Não quero usar uma coluna com um carimbo de data e hora em vez de um contador inteiro, porque:

  • O relógio no servidor pode pular para frente e para trás. Especialmente quando está em uma máquina virtual.

  • Os valores retornados pelas funções do sistema SYSDATETIMEsão iguais para todas as linhas afetadas. O processo de sincronização deve poder ler as alterações nos lotes. Por exemplo, se o tamanho do lote for de 3 linhas, após a MERGEetapa acima, o processo de sincronização lerá apenas as linhas E,F,G. Quando o processo de sincronização for executado na próxima vez, continuará da linha H.


O jeito que estou fazendo isso agora é feio.

Como não existe SEQUENCEno SQL Server 2008, eu emulo o SEQUENCEpor uma tabela dedicada, IDENTITYconforme mostrado nesta resposta . Isso por si só é bastante feio e exacerbado pelo fato de eu precisar gerar não apenas um, mas um lote de números de uma só vez.

Então, eu tenho um INSTEAD OF UPDATE, INSERTgatilho em cada tabela com RowUpdateCounteroe gerar conjuntos necessários de números lá.

Nas consultas e INSERT, defino como 0, que é substituído pelos valores corretos no gatilho. As consultas acima são .UPDATEMERGERowUpdateCounter???0

Funciona, mas existe uma solução mais fácil?

Vladimir Baranov
fonte
4
Você poderia usar a versão da linha / carimbo de data / hora? É um campo binário, mas o valor vai mudar cada vez que a linha é atualizada
James Z
@ JamesZ, preciso saber a ordem em que as linhas foram alteradas. O processo de sincronização lê o contador MAX a partir da cópia desatualizada da tabela e, em seguida, ele sabe buscar apenas as linhas que possuem contador além desse valor. A rowversionnão iria me dar essa possibilidade, se eu entendi corretamente o que é ... É garantida a ser cada vez maior?
Vladimir Baranov
Obrigado @MartinSmith, eu esqueci completamente rowversion. Parece muito tentador. Minha única preocupação é que todos os exemplos de uso que eu vi até agora giram em torno de detectar se uma única linha foi alterada. Preciso de uma maneira eficiente de saber que conjunto de linhas mudou desde um determinado momento. Além disso, é possível perder uma atualização?
Vladimir Baranov
@MartinSmith time = 0: o último valor da versão de linha é, por exemplo, 122. time = 1: A transação Aatualiza uma linha, sua versão de linha é alterada para 123, Aainda não foi confirmada. time = 2: a transação Batualiza outra linha, sua versão de linha muda para 124. time = 3: Bconfirma. time = 4: o processo de sincronização é executado e busca todas as linhas com versão> 122, o que significa que as linhas são atualizadas apenas por B. time = 5: Aconfirma. Resultado: as alterações de Anunca serão captadas pelo processo de sincronização. Estou errado? Talvez algum uso inteligente de MIN_ACTIVE_ROWVERSIONajude?
Vladimir Baranov

Respostas:

5

Você pode usar uma ROWVERSIONcoluna para isso.

A documentação afirma que

Cada banco de dados possui um contador incrementado para cada operação de inserção ou atualização executada em uma tabela que contém uma coluna de versão de linha no banco de dados.

Os valores são BINARY(8)e você deve considerá-los como BINARYantes BIGINTe depois de 0x7FFFFFFFFFFFFFFFprosseguir 0x80...e começar a trabalhar se -9223372036854775808for tratado como assinado bigint.

Um exemplo completo está abaixo. Manter o índice na ROWVERSIONcoluna será caro se você tiver muitas atualizações, portanto, você pode testar sua carga de trabalho com e sem para ver se vale o custo.

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
fonte
Obrigado. Depois de ler os documentos, acho que, em vez de @@DBTS, deveria haver MIN_ACTIVE_ROWVERSION(), e se o uso da MIN_ACTIVE_ROWVERSION()comparação <=deveria se tornar <e >se tornar >=.
Vladimir Baranov
De acordo com os documentos, há uma diferença material entre @@DBTSe MIN_ACTIVE_ROWVERSION()se há transações não confirmadas ativas. Se um aplicativo usar em @@DBTSvez de MIN_ACTIVE_ROWVERSION, é possível perder as alterações ativas quando a sincronização ocorrer.
Vladimir Baranov
@VladimirBaranov - sim, concordou, editado.
Martin Smith
-2

Você já tentou usar a IDENTITYopção?

Por exemplo:

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

Onde

  • 1 -> Valor inicial
  • 2 -> cada nova linha é incrementada por este

Isso é semelhante ao SEQUENCE no Oracle.

Bibhuti Bhusan Padhi
fonte
SQL Server não tem nenhuma "opção AUTOINCREMENT"
Martin Smith
sim. É suportado pelo Access. O servidor SQL suporta a opção IDENTITY. Eu atualizei minha resposta acima. Obrigado !!
Bibhuti Bhusan Padhi
4
IDENTITYnão faz o necessário para incrementar automaticamente as atualizações e as inserções .
Martin Smith
@BibhutiBhusanPadhi, preciso saber quais linhas foram atualizadas. Não vejo como simples IDENTITYpode ajudar.
Vladimir Baranov