Aumentar un contador para cada fila cambiada

8

Estoy usando SQL Server 2008 Standard, que no tiene una SEQUENCEfunción.

Un sistema externo lee datos de varias tablas dedicadas de la base de datos principal. El sistema externo mantiene una copia de los datos y verifica periódicamente los cambios en los datos y actualiza su copia.

Para que la sincronización sea eficiente, quiero transferir solo las filas que se actualizaron o insertaron desde la sincronización anterior. (Las filas nunca se eliminan). Para saber qué filas se actualizaron o insertaron desde la última sincronización, hay una bigintcolumna RowUpdateCounteren cada tabla.

La idea es que cada vez que se inserte o actualice una fila, el número en su RowUpdateCountercolumna cambiaría. Los valores que entran en la RowUpdateCountercolumna deben tomarse de una secuencia de números cada vez mayor. Los valores en la RowUpdateCountercolumna deben ser únicos y cada nuevo valor almacenado en una tabla debe ser mayor que cualquier valor anterior.

Consulte los scripts que muestran el comportamiento deseado.

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

INSERTAR algunas filas

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 |
+----+-------+------------------+

Los valores generados en RowUpdateCounterpueden ser diferentes, por ejemplo, 5, 3, 7, 9. Deben ser únicos y deben ser mayores que 0, ya que comenzamos desde una tabla vacía.

INSERTAR y ACTUALIZAR algunas filas

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 las filas con ID 1,2debe permanecer como está, porque estas filas no se modificaron.
  • RowUpdateCounterpara las filas con ID 3,4deberían cambiar, porque se actualizaron.
  • RowUpdateCounterpara las filas con ID 5,6deberían cambiar, porque se insertaron.
  • RowUpdateCounterpara todas las filas modificadas debe ser mayor que 4 (el último RowUpdateCounterde la secuencia).

El orden en que 5,6,7,8se asignan los nuevos valores ( ) a las filas modificadas realmente no importa. Los nuevos valores pueden tener huecos, por ejemplo 15,26,47,58, pero nunca deberían disminuir.

Hay varias tablas con dichos contadores en la base de datos. No importa si todos usan la secuencia global única para sus números, o si cada tabla tiene su propia secuencia individual.


No quiero usar una columna con un sello de fecha y hora en lugar de un contador entero, porque:

  • El reloj en el servidor puede saltar hacia adelante y hacia atrás. Especialmente cuando está en una máquina virtual.

  • Los valores devueltos por las funciones del sistema SYSDATETIMEson iguales para todas las filas afectadas. El proceso de sincronización debería poder leer los cambios en lotes. Por ejemplo, si el tamaño del lote es de 3 filas, luego del MERGEpaso anterior, el proceso de sincronización solo leería filas E,F,G. Cuando se ejecute el proceso de sincronización la próxima vez, continuará desde la fila H.


La forma en que lo hago ahora es bastante fea.

Como no existe SEQUENCEen SQL Server 2008, emulo SEQUENCEuna tabla dedicada con la IDENTITYque se muestra en esta respuesta . Esto en sí mismo es bastante feo y se ve exacerbado por el hecho de que necesito generar no uno solo, sino un lote de números a la vez.

Luego, tengo un INSTEAD OF UPDATE, INSERTactivador en cada tabla con RowUpdateCountery genero los conjuntos de números necesarios allí.

En las consultas INSERT, UPDATEy MERGEconfiguro RowUpdateCountera 0, que se reemplaza por los valores correctos en el disparador. El ???en las consultas anteriores son 0.

Funciona, pero ¿hay una solución más fácil?

Vladimir Baranov
fuente
44
¿Podría usar la versión de fila / marca de tiempo? Es un campo binario, pero el valor cambiará cada vez que se actualice la fila
James Z
@JamesZ, necesito saber el orden en que se cambiaron las filas. El proceso de sincronización lee el contador MAX de la copia desactualizada de la tabla y luego sabe buscar solo las filas que tienen un contador mayor que ese valor. El rowversionno me daría esta posibilidad, si entiendo correctamente lo que es ... ¿Se garantiza que aumentará?
Vladimir Baranov
Gracias @ MartinSmith, me olvidé por completo rowversion. Se ve muy tentador. Mi única preocupación es que todos los ejemplos de su uso que he visto hasta ahora giran en torno a detectar si una sola fila cambió. Necesito una forma eficiente de saber qué conjunto de filas cambió desde un momento determinado. Además, ¿es posible perder una actualización?
Vladimir Baranov
@MartinSmith time = 0: el valor de la última versión de la fila es, digamos, 122. time = 1: la transacción Aactualiza una fila, su versión de la fila cambia a 123, Aaún no se ha confirmado. time = 2: la transacción Bactualiza otra fila, su versión de fila cambia a 124. time = 3: Bcommits. time = 4: el proceso de sincronización se ejecuta y recupera todas las filas con rowversion> 122, lo que significa que las filas se actualizan solo por B. tiempo = 5: Aconfirmaciones. Resultado: los cambios por Anunca serán recogidos por el proceso de sincronización. ¿Me equivoco? Tal vez algún uso inteligente de MIN_ACTIVE_ROWVERSIONayudará?
Vladimir Baranov

Respuestas:

5

Puedes usar una ROWVERSIONcolumna para esto.

La documentación indica que

Cada base de datos tiene un contador que se incrementa para cada operación de inserción o actualización que se realiza en una tabla que contiene una columna de versión de fila dentro de la base de datos.

Los valores son BINARY(8)y debe considerarlos como BINARYalgo más que BIGINTdespués de 0x7FFFFFFFFFFFFFFFque continúe 0x80...y comience a funcionar -9223372036854775808si se trata como un signo bigint.

Un ejemplo completo trabajado está por debajo. Mantener el índice en la ROWVERSIONcolumna será costoso si tiene muchas actualizaciones, por lo que es posible que desee probar su carga de trabajo con y sin ver si vale la pena el 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] 
Martin Smith
fuente
Gracias. Después de leer los documentos, creo que en lugar de @@DBTSdebería haber MIN_ACTIVE_ROWVERSION(), y si se usa la MIN_ACTIVE_ROWVERSION()comparación, <=debería convertirse <y >convertirse >=.
Vladimir Baranov
Según los documentos, existe una diferencia importante entre @@DBTSy MIN_ACTIVE_ROWVERSION()si hay transacciones activas no comprometidas. Si una aplicación usa en @@DBTSlugar de MIN_ACTIVE_ROWVERSION, es posible perder los cambios que están activos cuando se produce la sincronización.
Vladimir Baranov
@VladimirBaranov - sí, de acuerdo, editado.
Martin Smith
-2

¿Has intentado usar la IDENTITYopción?

Por ejemplo:

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

dónde

  • 1 -> Valor inicial
  • 2 -> cada nueva fila se incrementa por esto

Esto es similar a SECUENCIA en Oracle.

Bibhuti Bhusan Padhi
fuente
SQL Server no tiene ninguna "opción de AUTOINCREMENTO"
Martin Smith
si. Es compatible con Access. El servidor SQL admite la opción IDENTIDAD. He actualizado mi respuesta arriba. Gracias !!
Bibhuti Bhusan Padhi
44
IDENTITYno hace lo que se requiere con respecto al incremento automático tanto en las actualizaciones como en las inserciones .
Martin Smith
@BibhutiBhusanPadhi, necesito saber qué filas se han actualizado. No veo cuán simple IDENTITYpuede ayudar.
Vladimir Baranov