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.
fuente
Respuestas:
PRIMERO
Es probable que no necesita las tres columnas:
old_id
,external_id
,new_id
. Lanew_id
columna, siendo unIDENTITY
, tendrá un nuevo valor generado para cada fila, incluso cuando inserte enexternal_id
. Pero, entreold_id
yexternal_id
, esos son más o menos mutuamente excluyentes: o ya hay unold_id
valor o esa columna, en la concepción actual, solo lo seráNULL
si se usaexternal_id
onew_id
. Como no va a agregar una nueva identificación "externa" a una fila que ya existe (es decir, una que tenga unold_id
valor), y no habrá valores nuevosold_id
, entonces puede haber una columna que se utilice para ambos propósitosEntonces, deshágase de la
external_id
columna y cambie el nombreold_id
para que sea algo asíold_or_external_id
o 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 columnaexternal_id
, incluso si contiene valores "antiguos", si el código de la aplicación ya está escrito para insertarloexternal_id
.Eso reduce la nueva estructura para ser justa:
Ahora solo ha agregado 8 bytes por fila en lugar de 12 bytes (suponiendo que no esté utilizando la
SPARSE
opció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:
old_or_external_id
columna ya tiene valores, o se le dará un nuevo valor desde la aplicación, o se dejará comoNULL
.new_id
siempre tendrá un nuevo valor generado, pero ese valor sólo se utilizará si elold_or_external_id
columna esNULL
.Nunca hay un momento en que necesite valores en ambos
old_or_external_id
ynew_id
. Sí, habrá momentos en que ambas columnas tienen valores debido anew_id
ser unIDENTITY
, pero esosnew_id
valores se ignoran. De nuevo, estos dos campos son mutuamente excluyentes. ¿Y ahora qué?Ahora podemos ver por qué necesitábamos el
external_id
en primer lugar. Teniendo en cuenta que es posible insertar en unaIDENTITY
columna usandoSET 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 lasINSERT
declaraciones / operaciones enSET IDENTITY_INSERT {table_name} ON;
ySET IDENTITY_INSERT {table_name} OFF;
declaraciones. Luego, debe determinar a qué rango inicial restablecer laIDENTITY
columna (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_id
ynew_id
tampoco 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:
Esto agrega 0 bytes a cada fila, en lugar de 8, o incluso 12.
SET IDENTITY_INSERT {table_name} ON;
ySET 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
IDENTITY
valores 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:
Esto agrega 0 bytes a cada fila, en lugar de 8, o incluso 12.
-1
.SET IDENTITY_INSERT {table_name} ON;
ySET 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" ningunaIDENTITY
columna y no tiene riesgo futuro de superposiciones.SEGUNDO, Parte 3
Una última variación de este enfoque sería posiblemente intercambiar las
IDENTITY
columnas 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 hacerloSET IDENTITY_INSERT ON / OFF
.En este enfoque, solo necesita:
Copie la
IDENTITY
columna a una nueva columna que no tenga laIDENTITY
propiedad, pero que tenga unaDEFAULT
Restricción usando la función NEXT VALUE FOR :Esto agrega 0 bytes a cada fila, en lugar de 8, o incluso 12.
SET IDENTITY_INSERT {table_name} ON;
ySET IDENTITY_INSERT {table_name} OFF;
declaraciones.Sin embargo , debido a la exigencia de que el código, ya sea con
SCOPE_IDENTITY()
o@@IDENTITY
aú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!fuente
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?IDENTITY_INSERT ON
para la misma tabla en dos sesiones y estaba insertando en ambas sin problemas.