sql >> Base de Datos >  >> RDS >> Database

Esquema Switch-A-Roo:Parte 2

En agosto, escribí una publicación sobre mi metodología de intercambio de esquemas para T-SQL Tuesday. El enfoque esencialmente le permite cargar de forma diferida una copia de una tabla (por ejemplo, una tabla de búsqueda de algún tipo) en segundo plano para minimizar la interferencia con los usuarios:una vez que la tabla de fondo está actualizada, todo lo que se requiere para entregar los datos actualizados para los usuarios es una interrupción lo suficientemente larga como para confirmar un cambio de metadatos.

En esa publicación, mencioné dos advertencias que la metodología que he defendido a lo largo de los años no satisface actualmente:restricciones de clave externa y estadísticas . Hay una serie de otras características que también pueden interferir con esta técnica. Uno que surgió en una conversación recientemente:desencadenantes . Y hay otras:columnas de identidad , restricciones de clave principal , restricciones por defecto , verificar restricciones , restricciones que hacen referencia a UDF , índices , vistas (incluyendo vistas indexadas , que requieren SCHEMABINDING ), y particiones . No me ocuparé de todos estos hoy, pero pensé en probar algunos para ver qué sucede exactamente.

Confieso que mi solución original era básicamente la instantánea de un hombre pobre, sin todas las molestias, la base de datos completa y los requisitos de licencia de soluciones como la replicación, la duplicación y los grupos de disponibilidad. Se trataba de copias de solo lectura de tablas de producción que se "reflejaban" mediante T-SQL y la técnica de intercambio de esquemas. Por lo tanto, no necesitaban ninguna de estas elegantes claves, restricciones, disparadores y otras características. Pero veo que la técnica puede ser útil en más escenarios, y en esos escenarios pueden entrar en juego algunos de los factores anteriores.

Entonces, configuremos un par de tablas simples que tengan varias de estas propiedades, realicemos un intercambio de esquema y veamos qué falla. :-)

Primero, los esquemas:

CREATE SCHEMA prep;
GO
CREATE SCHEMA live;
GO
CREATE SCHEMA holder;
GO

Ahora, la tabla en el live esquema, incluidos un activador y una UDF:

CREATE FUNCTION dbo.udf()
RETURNS INT 
AS
BEGIN
  RETURN (SELECT 20);
END
GO
 
CREATE TABLE live.t1
(
  id INT IDENTITY(1,1),
  int_column INT NOT NULL DEFAULT 1,
  udf_column INT NOT NULL DEFAULT dbo.udf(),
  computed_column AS CONVERT(INT, int_column + 1),
  CONSTRAINT pk_live PRIMARY KEY(id),
  CONSTRAINT ck_live CHECK (int_column > 0)
);
GO
 
CREATE TRIGGER live.trig_live
ON live.t1
FOR INSERT
AS
BEGIN
  PRINT 'live.trig';
END
GO

Ahora, repetimos lo mismo para la copia de la tabla en prep . También necesitamos una segunda copia del disparador, porque no podemos crear un disparador en la prep esquema que hace referencia a una tabla en live , o viceversa. Estableceremos deliberadamente la identidad en una semilla más alta y un valor predeterminado diferente para int_column (para ayudarnos a realizar un mejor seguimiento de la copia de la tabla con la que realmente estamos tratando después de varios intercambios de esquema):

CREATE TABLE prep.t1
(
  id INT IDENTITY(1000,1),
  int_column INT NOT NULL DEFAULT 2,
  udf_column INT NOT NULL DEFAULT dbo.udf(),
  computed_column AS CONVERT(INT, int_column + 1),
  CONSTRAINT pk_prep PRIMARY KEY(id),
  CONSTRAINT ck_prep CHECK (int_column > 1)
);
GO
 
CREATE TRIGGER prep.trig_prep
ON prep.t1
FOR INSERT
AS
BEGIN
  PRINT 'prep.trig';
END
GO

Ahora, insertemos un par de filas en cada tabla y observemos el resultado:

SET NOCOUNT ON;
 
INSERT live.t1 DEFAULT VALUES;
INSERT live.t1 DEFAULT VALUES;
 
INSERT prep.t1 DEFAULT VALUES;
INSERT prep.t1 DEFAULT VALUES;
 
SELECT * FROM live.t1;
SELECT * FROM prep.t1;

Resultados:

id int_column columna_udf columna_calculada
1

1 20 2
2

1 20 2

Resultados de live.t1

id int_column columna_udf columna_calculada
1000

2 20 3
1001

2 20 3

Resultados de prep.t1

Y en el panel de mensajes:

live.trig
live.trig
prep.trig
prep.trig

Ahora, realicemos un intercambio de esquema simple:

 -- assume that you do background loading of prep.t1 here
 
BEGIN TRANSACTION;
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
COMMIT TRANSACTION;

Y luego repite el ejercicio:

SET NOCOUNT ON;
 
INSERT live.t1 DEFAULT VALUES;
INSERT live.t1 DEFAULT VALUES;
 
INSERT prep.t1 DEFAULT VALUES;
INSERT prep.t1 DEFAULT VALUES;
 
SELECT * FROM live.t1;
SELECT * FROM prep.t1;

Los resultados en las tablas parecen correctos:

id int_column columna_udf columna_calculada
1

1 20 2
2

1 20 2
3

1 20 2
4

1 20 2

Resultados de live.t1

id int_column columna_udf columna_calculada
1000

2 20 3
1001

2 20 3
1002

2 20 3
1003

2 20 3

Resultados de prep.t1

Pero el panel de mensajes enumera la salida del disparador en el orden incorrecto:

prep.trig
prep.trig
live.trig
live.trig

Entonces, profundicemos en todos los metadatos. Aquí hay una consulta que inspeccionará rápidamente todas las columnas de identidad, disparadores, claves primarias, restricciones predeterminadas y de verificación para estas tablas, enfocándose en el esquema del objeto asociado, el nombre y la definición (y la semilla/último valor para columnas de identidad):

SELECT 
  [type] = 'Check', 
  [schema] = OBJECT_SCHEMA_NAME(parent_object_id), 
  name, 
  [definition]
FROM sys.check_constraints
WHERE OBJECT_SCHEMA_NAME(parent_object_id) IN (N'live',N'prep')
UNION ALL
SELECT 
  [type] = 'Default', 
  [schema] = OBJECT_SCHEMA_NAME(parent_object_id), 
  name, 
  [definition]
FROM sys.default_constraints
WHERE OBJECT_SCHEMA_NAME(parent_object_id) IN (N'live',N'prep')
UNION ALL
SELECT 
  [type] = 'Trigger',
  [schema] = OBJECT_SCHEMA_NAME(parent_id), 
  name, 
  [definition] = OBJECT_DEFINITION([object_id])
FROM sys.triggers
WHERE OBJECT_SCHEMA_NAME(parent_id) IN (N'live',N'prep')
UNION ALL
SELECT 
  [type] = 'Identity',
  [schema] = OBJECT_SCHEMA_NAME([object_id]),
  name = 'seed = ' + CONVERT(VARCHAR(12), seed_value), 
  [definition] = 'last_value = ' + CONVERT(VARCHAR(12), last_value)
FROM sys.identity_columns
WHERE OBJECT_SCHEMA_NAME([object_id]) IN (N'live',N'prep')
UNION ALL
SELECT
  [type] = 'Primary Key',
  [schema] = OBJECT_SCHEMA_NAME([parent_object_id]),
  name,
  [definition] = ''
FROM sys.key_constraints
WHERE OBJECT_SCHEMA_NAME([object_id]) IN (N'live',N'prep');

Los resultados indican un gran lío de metadatos:

tipo esquema nombre definición
Comprobar preparar ck_live ([int_column]>(0))
Comprobar en vivo ck_prep ([int_column]>(1))
Predeterminado preparar df_live1 ((1))
Predeterminado preparar df_live2 ([dbo].[udf]())
Predeterminado en vivo df_prep1 ((2))
Predeterminado en vivo df_prep2 ([dbo].[udf]())
Disparador preparar trig_live CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END
Disparador en vivo trig_prep CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Identidad preparar semilla =1 último_valor =4
Identidad en vivo semilla =1000 último_valor =1003
Clave principal preparar pk_live
Clave principal en vivo pk_prep

Metadatos pato-pato-ganso

Los problemas con las columnas de identidad y las restricciones no parecen ser un gran problema. Aunque los objetos *parecen* apuntar a los objetos incorrectos según las vistas del catálogo, la funcionalidad, al menos para las inserciones básicas, funciona como cabría esperar si nunca hubiera consultado los metadatos.

El gran problema es con el disparador:olvidando por un momento lo trivial que hice este ejemplo, en el mundo real, probablemente hace referencia a la tabla base por esquema y nombre. En cuyo caso, cuando se adjunta a la mesa equivocada, las cosas pueden salir... bueno, mal. Volvamos a cambiar:

BEGIN TRANSACTION;
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
COMMIT TRANSACTION;

(Puede volver a ejecutar la consulta de metadatos para convencerse de que todo ha vuelto a la normalidad).

Ahora cambiemos el activador *solo* en el live versión para hacer algo realmente útil (bueno, "útil" en el contexto de este experimento):

ALTER TRIGGER live.trig_live
ON live.t1
FOR INSERT
AS
BEGIN
  SELECT i.id, msg = 'live.trig'
    FROM inserted AS i 
    INNER JOIN live.t1 AS t 
    ON i.id = t.id;
END
GO

Ahora insertemos una fila:

INSERT live.t1 DEFAULT VALUES;

Resultados:

id    msg
----  ----------
5     live.trig

Luego realice el intercambio nuevamente:

BEGIN TRANSACTION;
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
COMMIT TRANSACTION;

E inserte otra fila:

INSERT live.t1 DEFAULT VALUES;

Resultados (en el panel de mensajes):

prep.trig

UH oh. Si realizamos este intercambio de esquema una vez por hora, entonces durante 12 horas de cada día, el disparador no está haciendo lo que esperamos que haga, ya que está asociado con la copia incorrecta de la tabla. Ahora modifiquemos la versión "prep" del activador:

ALTER TRIGGER prep.trig_prep
ON prep.t1
FOR INSERT
AS
BEGIN
  SELECT i.id, msg = 'prep.trig'
    FROM inserted AS i 
	INNER JOIN prep.t1 AS t 
	ON i.id = t.id;
END
GO

Resultado:

Mensaje 208, Nivel 16, Estado 6, Procedimiento trig_prep, Línea 1
Nombre de objeto no válido 'prep.trig_prep'.

Bueno, eso definitivamente no es bueno. Dado que estamos en la fase de intercambio de metadatos, no existe tal objeto; los activadores ahora son live.trig_prep y prep.trig_live . ¿Confundido todavía? Yo también. Así que probemos esto:

EXEC sp_helptext 'live.trig_prep';

Resultados:

CREATE TRIGGER prep.trig_prep
ON prep.t1
FOR INSERT
AS
BEGIN
  PRINT 'prep.trig';
END

Bueno, ¿no es divertido? ¿Cómo modifico este activador cuando sus metadatos ni siquiera se reflejan correctamente en su propia definición? Intentemos esto:

ALTER TRIGGER live.trig_prep
ON prep.t1
FOR INSERT
AS
BEGIN
  SELECT i.id, msg = 'prep.trig'
    FROM inserted AS i 
    INNER JOIN prep.t1 AS t 
    ON i.id = t.id;
END
GO

Resultados:

Mensaje 2103, nivel 15, estado 1, procedimiento trig_prep, línea 1
No se puede modificar el activador 'live.trig_prep' porque su esquema es diferente del esquema de la tabla o vista de destino.

Esto tampoco es bueno, obviamente. Parece que no hay realmente una buena manera de resolver este escenario que no implique cambiar los objetos a sus esquemas originales. Podría modificar este disparador para estar en contra de live.t1 :

ALTER TRIGGER live.trig_prep
ON live.t1
FOR INSERT
AS
BEGIN
  SELECT i.id, msg = 'live.trig'
    FROM inserted AS i 
    INNER JOIN live.t1 AS t 
    ON i.id = t.id;
END
GO

Pero ahora tengo dos disparadores que dicen, en su cuerpo de texto, que operan contra live.t1 , pero solo este se ejecuta realmente. Sí, mi cabeza da vueltas (y también la de Michael J. Swart (@MJSwart) en esta publicación de blog). Y tenga en cuenta que, para limpiar este lío, después de volver a intercambiar los esquemas, puedo soltar los activadores con sus nombres originales:

DROP TRIGGER live.trig_live;
DROP TRIGGER prep.trig_prep;

Si pruebo DROP TRIGGER live.trig_prep; , por ejemplo, aparece un error de objeto no encontrado.

¿Resoluciones?

Una solución para el problema del disparador es generar dinámicamente el CREATE TRIGGER código, y suelte y vuelva a crear el activador, como parte del intercambio. Primero, volvamos a poner un activador en la tabla *actual* en live (puede decidir en su escenario si incluso necesita un disparador en la prep versión de la tabla en absoluto):

CREATE TRIGGER live.trig_live
ON live.t1
FOR INSERT
AS
BEGIN
  SELECT i.id, msg = 'live.trig'
    FROM inserted AS i 
    INNER JOIN live.t1 AS t 
    ON i.id = t.id;
END
GO

Ahora, un ejemplo rápido de cómo funcionaría nuestro nuevo intercambio de esquema (y es posible que deba ajustar esto para tratar con cada activador, si tiene varios activadores, y repetirlo para el esquema en la prep versión, si necesita mantener un disparador allí también. Tenga especial cuidado de que el siguiente código, por brevedad, asuma que solo hay *un* disparador en live.t1 .

BEGIN TRANSACTION;
  DECLARE 
    @sql1 NVARCHAR(MAX),
    @sql2 NVARCHAR(MAX);
 
  SELECT 
    @sql1 = N'DROP TRIGGER live.' + QUOTENAME(name) + ';',
    @sql2 = OBJECT_DEFINITION([object_id])
  FROM sys.triggers
  WHERE [parent_id] = OBJECT_ID(N'live.t1');
 
  EXEC sp_executesql @sql1; -- drop the trigger before the transfer
 
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
 
  EXEC sp_executesql @sql2; -- re-create it after the transfer
COMMIT TRANSACTION;

Otra solución alternativa (menos deseable) sería realizar la operación de intercambio de esquema completa dos veces, incluidas las operaciones que ocurran contra la prep versión de la tabla. Lo que anula en gran medida el propósito del intercambio de esquema en primer lugar:reducir el tiempo que los usuarios no pueden acceder a las tablas y brindarles los datos actualizados con una interrupción mínima.