Cambio de clave primaria de IDENTIDAD a persistente Columna calculada usando COALESCE


10

En un intento de desacoplar una aplicación de nuestra base de datos monolítica, hemos tratado de cambiar las columnas INT IDENTITY de varias tablas para que sean una columna computada PERSISTADA que use COALESCE. Básicamente, necesitamos que la aplicación desacoplada tenga la capacidad de actualizar la base de datos para datos comunes compartidos en muchas aplicaciones y al mismo tiempo permitir que las aplicaciones existentes creen datos en estas tablas sin la necesidad de modificar el código o el procedimiento.

Entonces, esencialmente, nos hemos movido de una definición de columna de;

PkId INT IDENTITY(1,1) PRIMARY KEY

a;

PkId AS AS COALESCE(old_id, external_id, new_id) PERSISTED NOT NULL,
old_id INT NULL, -- Values here are from existing records of PkId before table change
external_id INT NULL,
new_id INT IDENTITY(2000000,1) NOT NULL

En todos los casos, el PkId también es una CLAVE PRIMARIA y, en todos los casos, excepto uno, está CLUSTERADO. Todas las tablas tienen las mismas claves e índices externos que antes. En esencia, el nuevo formato permite que el PkId sea suministrado por la aplicación desacoplada (como external_id), pero también permite que el PkId sea el valor de la columna IDENTITY, lo que permite el código existente que se basa en la columna IDENTITY mediante el uso de SCOPE_IDENTITY y @@ IDENTITY trabajar como solía hacerlo.

El problema que hemos tenido es que nos hemos encontrado con un par de consultas que solían ejecutarse en un momento aceptable para explotar por completo. Los planes de consulta generados utilizados por estas consultas no se parecen en nada a lo que solían ser antes.

Dado que la nueva columna es una CLAVE PRIMARIA, el mismo tipo de datos que antes, y PERSISTED, habría esperado que las consultas y los planes de consulta se comportaran igual que antes. ¿Debería el PkId INT COMPUTADO PERSISTADO esencialmente comportarse de la misma manera que una definición INT explícita en términos de cómo SQL Server producirá el plan de ejecución? ¿Hay otros problemas probables con este enfoque que pueda ver?

Se suponía que el propósito de este cambio nos permitiría cambiar la definición de la tabla sin la necesidad de modificar los procedimientos y el código existentes. Dados estos problemas, no creo que podamos seguir con este enfoque.


Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Paul White 9

Respuestas:


4

PRIMERO

Es probable que no necesita las tres columnas: old_id, external_id, new_id. La new_idcolumna, siendo un IDENTITY, tendrá un nuevo valor generado para cada fila, incluso cuando inserte en external_id. Pero, entre old_idy external_id, esos son más o menos mutuamente excluyentes: o ya hay un old_idvalor o esa columna, en la concepción actual, solo lo será NULLsi se usa external_ido new_id. Como no va a agregar una nueva identificación "externa" a una fila que ya existe (es decir, una que tenga un old_idvalor), y no habrá valores nuevos old_id, entonces puede haber una columna que se utilice para ambos propósitos

Entonces, deshágase de la external_idcolumna y cambie el nombre old_idpara que sea algo así old_or_external_ido lo que sea. Esto no debería requerir ningún cambio real en nada, pero reduce algunas de las complicaciones. A lo sumo, es posible que deba llamar a la columna external_id, incluso si contiene valores "antiguos", si el código de la aplicación ya está escrito para insertarlo external_id.

Eso reduce la nueva estructura para ser justa:

PkId AS AS COALESCE(old_or_external_id, new_id, -1) PERSISTED NOT NULL,
old_or_external_id INT NULL, -- values from existing record OR passed in from app
new_id INT IDENTITY(2000000, 1) NOT NULL

Ahora solo ha agregado 8 bytes por fila en lugar de 12 bytes (suponiendo que no esté utilizando la SPARSEopción o la compresión de datos). Y no necesitaba cambiar ningún código, T-SQL o código de aplicación.

SEGUNDO

Continuando por este camino de simplificación, veamos lo que nos queda:

  • La old_or_external_idcolumna ya tiene valores, o se le dará un nuevo valor desde la aplicación, o se dejará como NULL.
  • El new_idsiempre tendrá un nuevo valor generado, pero ese valor sólo se utilizará si el old_or_external_idcolumna es NULL.

Nunca hay un momento en que necesite valores en ambos old_or_external_idy new_id. Sí, habrá momentos en que ambas columnas tienen valores debido a new_idser un IDENTITY, pero esos new_idvalores se ignoran. De nuevo, estos dos campos son mutuamente excluyentes. ¿Y ahora qué?

Ahora podemos ver por qué necesitábamos el external_iden primer lugar. Teniendo en cuenta que es posible insertar en una IDENTITYcolumna usando SET IDENTITY_INSERT {table_name} ON;, podría salirse con la suya sin hacer ningún cambio de esquema y solo modificar el código de su aplicación para envolver las INSERTdeclaraciones / operaciones en SET IDENTITY_INSERT {table_name} ON;y SET IDENTITY_INSERT {table_name} OFF;declaraciones. Luego, debe determinar a qué rango inicial restablecer la IDENTITYcolumna (para los valores recién generados), ya que deberá estar muy por encima de los valores que el código de la aplicación insertará, ya que la inserción de un valor más alto hará que el siguiente valor generado automáticamente ser mayor que el valor MAX actual. Pero siempre puede insertar un valor que esté por debajo del valor IDENT_CURRENT .

La combinación de las columnas old_or_external_idy new_idtampoco aumenta las posibilidades de encontrarse con una situación de valor superpuesto entre los valores generados automáticamente y los valores generados por la aplicación, ya que la intención de tener las columnas 2, o incluso 3, es combinarlas en un valor de clave primaria, y esos son siempre valores únicos.

En este enfoque, solo necesita:

  • Deje las tablas como:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Esto agrega 0 bytes a cada fila, en lugar de 8, o incluso 12.

  • Determine el rango inicial para los valores generados por la aplicación. Estos serán mayores que el valor MAX actual en cada tabla, pero menores que lo que se convertirá en el valor mínimo para los valores autogenerados.
  • Determine en qué valor debe comenzar el rango autogenerado. Debe haber mucho espacio entre el valor MAX actual y mucho espacio para crecer, sabiendo que en el límite superior está un poco más de 2.14 mil millones. Luego puede establecer este nuevo valor mínimo inicial a través de DBCC CHECKIDENT .
  • Envuelva el código de la aplicación INSERT en SET IDENTITY_INSERT {table_name} ON;y SET IDENTITY_INSERT {table_name} OFF;declaraciones.

SEGUNDO, Parte B

Una variación en el enfoque anotado directamente arriba sería hacer que los valores de inserción del código de la aplicación comiencen con -1 y desciendan desde allí. Esto deja a los IDENTITYvalores por ser los únicos que van hacia arriba . El beneficio aquí es que no solo no complica el esquema, sino que tampoco tiene que preocuparse por encontrarse con ID superpuestos (si los valores generados por la aplicación se encuentran en el nuevo rango generado automáticamente). Esta es solo una opción si aún no está utilizando valores de ID negativos (y parece bastante raro que las personas usen valores negativos en columnas generadas automáticamente, por lo que esta debería ser una posibilidad en la mayoría de las situaciones).

En este enfoque, solo necesita:

  • Deje las tablas como:

    PkId INT IDENTITY(1,1) PRIMARY KEY

    Esto agrega 0 bytes a cada fila, en lugar de 8, o incluso 12.

  • El rango inicial para los valores generados por la aplicación será -1.
  • Envuelva el código de la aplicación INSERT en SET IDENTITY_INSERT {table_name} ON;y SET IDENTITY_INSERT {table_name} OFF;declaraciones.

Aquí aún debe hacer lo siguiente IDENTITY_INSERT, pero: no agrega ninguna columna nueva, no necesita "volver a colocar" ninguna IDENTITYcolumna y no tiene riesgo futuro de superposiciones.

SEGUNDO, Parte 3

Una última variación de este enfoque sería posiblemente intercambiar las IDENTITYcolumnas y, en su lugar, usar Secuencias . La razón para adoptar este enfoque es poder tener los valores de inserción del código de la aplicación que son: positivo, superior al rango generado automáticamente (no inferior) y sin necesidad de hacerlo SET IDENTITY_INSERT ON / OFF.

En este enfoque, solo necesita:

  • Crear secuencias usando CREAR SECUENCIA
  • Copie la IDENTITYcolumna a una nueva columna que no tenga la IDENTITYpropiedad, pero que tenga una DEFAULTRestricción usando la función NEXT VALUE FOR :

    PkId INT PRIMARY KEY CONSTRAINT [DF_TableName_NextID] DEFAULT (NEXT VALUE FOR...)

    Esto agrega 0 bytes a cada fila, en lugar de 8, o incluso 12.

  • El rango inicial para los valores generados por la aplicación estará muy por encima de lo que cree que se acercarán los valores generados automáticamente.
  • Envuelva el código de la aplicación INSERT en SET IDENTITY_INSERT {table_name} ON;y SET IDENTITY_INSERT {table_name} OFF;declaraciones.

Sin embargo , debido a la exigencia de que el código, ya sea con SCOPE_IDENTITY()o @@IDENTITYaún funciona correctamente, el cambio a secuencias no es actualmente una opción ya que parece que no existe un equivalente de esas funciones para las secuencias :-(. Triste!


Muchas gracias por su respuesta. Planteas algunos puntos que se discutieron aquí internamente. Desafortunadamente, algunos de estos no funcionarán para nosotros por un par de razones. Nuestra base de datos es bastante antigua y algo frágil y se ejecuta bajo el modo de compatibilidad 2005, por lo que las SECUENCIAS están fuera. El envío de datos de nuestra aplicación se realiza a través de una herramienta de carga de datos que obtiene nuevos registros de las colas de los agentes de servicio y los envía a través de múltiples hilos. IDENTITY_INSERT solo se puede usar para una tabla por sesión, y el pensamiento actual es que nuestra arquitectura no puede satisfacer eso sin un cambio significativo. Estoy probando tu sugerencia de puño ahora.
Mr Moose

@MrMoose Sí, actualicé mi respuesta para incluir más información sobre las secuencias al final. No funcionaría en tu situación de todos modos. Y me preguntaba sobre posibles problemas de concurrencia IDENTITY_INSERT, pero no lo he probado. No estoy seguro de que la opción # 1 vaya a resolver su problema general, fue solo una observación para reducir la complejidad innecesaria. Aún así, si tiene varios subprocesos que insertan nuevas ID "externas", ¿cómo garantiza que sean únicos?
Solomon Rutzky

@MrMoose En realidad, con respecto a " IDENTITY_INSERT solo se puede usar para una tabla por sesión ", ¿cuál es exactamente el problema aquí? 1) solo puede insertar en una tabla a la vez, por lo que lo apaga para la Tabla A antes de insertar en la Tabla B, y 2) Acabo de probar y, contrariamente a lo que pensaba, no hay problemas de concurrencia: pude tengo IDENTITY_INSERT ONpara la misma tabla en dos sesiones y estaba insertando en ambas sin problemas.
Solomon Rutzky

1
Como sugirió, el cambio 1 hizo poca diferencia. La identificación que usaremos se asignará fuera de la base de datos actual y se usará para relacionar registros. Es muy posible que mi comprensión de las sesiones no sea del todo correcta, por lo que IDENTITY_INSERT podría funcionar. Sin embargo, me llevará un poco de tiempo investigar eso, por lo que no podré informar por un tiempo. Gracias de nuevo por el aporte. Es muy apreciado.
Mr Moose

1
Creo que su sugerencia de usar IDENTITY_INSERT (con un alto valor inicial para las aplicaciones existentes) funcionará bien. Aaron Bertrand proporcionó una respuesta aquí con un buen pequeño ejemplo sobre cómo probarlo con concurrencia. Modificamos nuestra herramienta de carga de datos para poder manejar tablas que necesitan especificar valores de identidad y realizaremos algunas pruebas adicionales en las próximas semanas.
Mr Moose
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.