Database
 sql >> Base de données >  >> RDS >> Database

Schéma Switch-A-Roo :Partie 2

En août, j'ai écrit un article sur ma méthodologie d'échange de schéma pour T-SQL mardi. L'approche vous permet essentiellement de charger paresseusement une copie d'une table (par exemple, une table de recherche quelconque) en arrière-plan pour minimiser les interférences avec les utilisateurs :une fois que la table d'arrière-plan est à jour, tout ce qui est nécessaire pour fournir les données mises à jour aux utilisateurs est une interruption suffisamment longue pour valider une modification de métadonnées.

Dans cet article, j'ai mentionné deux mises en garde que la méthodologie que j'ai défendue au fil des ans ne répond pas actuellement aux :contraintes de clé étrangère et des statistiques . De nombreuses autres fonctionnalités peuvent également interférer avec cette technique. L'un d'entre eux a récemment fait l'objet d'une conversation :déclencheurs . Et il y en a d'autres :colonnes d'identité , contraintes de clé primaire , contraintes par défaut , vérifier les contraintes , contraintes faisant référence aux FDU , index , vues (y compris les vues indexées , qui nécessitent SCHEMABINDING ) et partitions . Je ne vais pas traiter de tout cela aujourd'hui, mais j'ai pensé en tester quelques-uns pour voir exactement ce qui se passe.

J'avoue que ma solution d'origine était essentiellement un instantané du pauvre, sans tous les tracas, la base de données complète et les exigences de licence de solutions telles que la réplication, la mise en miroir et les groupes de disponibilité. Il s'agissait de copies en lecture seule de tables de production qui étaient "mises en miroir" à l'aide de T-SQL et de la technique d'échange de schéma. Ils n'avaient donc besoin d'aucune de ces clés, contraintes, déclencheurs et autres fonctionnalités fantaisistes. Mais je vois que la technique peut être utile dans plus de scénarios, et dans ces scénarios, certains des facteurs ci-dessus peuvent entrer en jeu.

Configurons donc une simple paire de tables qui ont plusieurs de ces propriétés, effectuons un échange de schéma et voyons ce qui casse. :-)

Tout d'abord, les schémas :

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

Maintenant, la table dans le live schéma, incluant un déclencheur et une 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

Maintenant, nous répétons la même chose pour la copie de la table dans prep . Nous avons également besoin d'une deuxième copie du déclencheur, car nous ne pouvons pas créer de déclencheur dans le prep schéma qui référence une table dans live , ou vice versa. Nous définirons délibérément l'identité sur une graine plus élevée et une valeur par défaut différente pour int_column (pour nous aider à mieux savoir à quelle copie de la table nous avons vraiment affaire après plusieurs échanges de schéma) :

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

Insérons maintenant quelques lignes dans chaque tableau et observons le résultat :

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;

Résultats :

identifiant int_column udf_column colonne_calculée
1

1 20 2
2

1 20 2

Résultats de live.t1

identifiant int_column udf_column colonne_calculée
1 000

2 20 3
1001

2 20 3

Résultats de prep.t1

Et dans le volet des messages :

en direct.trig
en direct.trig
prép.trig
prép.trig

Effectuons maintenant un échange de schéma 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;

Et puis répétez l'exercice :

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;

Les résultats dans les tableaux semblent corrects :

identifiant int_column udf_column colonne_calculée
1

1 20 2
2

1 20 2
3

1 20 2
4

1 20 2

Résultats de live.t1

identifiant int_column udf_column colonne_calculée
1 000

2 20 3
1001

2 20 3
1002

2 20 3
1003

2 20 3

Résultats de prep.t1

Mais le volet des messages répertorie la sortie du déclencheur dans le mauvais ordre :

prép.trig
prép.trig
live.trig
live.trig

Alors, creusons dans toutes les métadonnées. Voici une requête qui inspectera rapidement toutes les colonnes d'identité, les déclencheurs, les clés primaires, les contraintes par défaut et de vérification pour ces tables, en se concentrant sur le schéma de l'objet associé, le nom et la définition (et la graine/dernière valeur pour colonnes d'identité) :

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');

Les résultats indiquent un sacré gâchis de métadonnées :

type schéma nom définition
Vérifier préparation ck_live ([int_column]>(0))
Vérifier en direct ck_prep ([int_column]>(1))
Par défaut préparation df_live1 ((1))
Par défaut préparation df_live2 ([dbo].[udf]())
Par défaut en direct df_prep1 ((2))
Par défaut en direct df_prep2 ([dbo].[udf]())
Déclencheur préparation trig_live CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END
Déclencheur en direct trig_prep CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Identité préparation graine =1 last_value =4
Identité en direct graine =1000 last_value =1003
Clé primaire préparation pk_live
Clé primaire en direct pk_prep

Métadonnées canard-canard-oie

Les problèmes avec les colonnes d'identité et les contraintes ne semblent pas être un gros problème. Même si les objets *semblent* pointer vers les mauvais objets selon les vues du catalogue, la fonctionnalité - au moins pour les insertions de base - fonctionne comme on pourrait s'y attendre si vous n'aviez jamais regardé les métadonnées.

Le gros problème est avec le déclencheur - oubliant un instant à quel point j'ai fait cet exemple trivial, dans le monde réel, il fait probablement référence à la table de base par schéma et nom. Dans ce cas, lorsqu'il est attaché à la mauvaise table, les choses peuvent aller… eh bien, mal. Revenons en arrière :

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

(Vous pouvez réexécuter la requête de métadonnées pour vous convaincre que tout est revenu à la normale.)

Changeons maintenant le déclencheur *uniquement* sur le live version pour faire quelque chose d'utile (enfin, "utile" dans le contexte de cette expérience) :

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

Insérons maintenant une ligne :

INSERT live.t1 DEFAULT VALUES;

Résultats :

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

Effectuez ensuite à nouveau l'échange :

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

Et insérez une autre ligne :

INSERT live.t1 DEFAULT VALUES;

Résultats (dans le volet des messages) :

prep.trig

Oh-oh. Si nous effectuons cet échange de schéma une fois par heure, puis pendant 12 heures par jour, le déclencheur ne fait pas ce que nous attendons de lui, car il est associé à la mauvaise copie de la table ! Modifions maintenant la version "prep" du déclencheur :

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

Résultat :

Msg 208, Niveau 16, État 6, Procédure trig_prep, Ligne 1
Nom d'objet non valide 'prep.trig_prep'.

Eh bien, ce n'est certainement pas bon. Puisque nous sommes dans la phase d'échange de métadonnées, un tel objet n'existe pas ; les déclencheurs sont maintenant live.trig_prep et prep.trig_live . Confus encore? Moi aussi. Alors essayons ceci :

EXEC sp_helptext 'live.trig_prep';

Résultats :

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

Eh bien, n'est-ce pas drôle? Comment modifier ce déclencheur lorsque ses métadonnées ne sont même pas correctement reflétées dans sa propre définition ? Essayons ceci :

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

Résultats :

Msg 2103, Niveau 15, État 1, Procédure trig_prep, Ligne 1
Impossible de modifier le déclencheur 'live.trig_prep' car son schéma est différent du schéma de la table ou de la vue cible.

Ce n'est pas bon non plus, évidemment. Il semble qu'il n'y ait pas vraiment de bonne façon de résoudre ce scénario qui n'implique pas de remettre les objets dans leurs schémas d'origine. Je pourrais modifier ce déclencheur pour qu'il soit contre 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

Mais maintenant, j'ai deux déclencheurs qui disent, dans leur corps de texte, qu'ils fonctionnent avec live.t1 , mais seul celui-ci s'exécute réellement. Oui, ma tête tourne (tout comme celle de Michael J. Swart (@MJSwart) dans cet article de blog). Et notez que, pour nettoyer ce gâchis, après avoir à nouveau échangé les schémas, je peux supprimer les déclencheurs avec leurs noms d'origine :

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

Si j'essaie DROP TRIGGER live.trig_prep; , par exemple, j'obtiens une erreur objet introuvable.

Des résolutions ?

Une solution de contournement pour le problème de déclencheur consiste à générer dynamiquement le CREATE TRIGGER code, puis supprimez et recréez le déclencheur, dans le cadre du swap. Tout d'abord, remettons un déclencheur sur la table *current* dans live (vous pouvez décider dans votre scénario si vous avez même besoin d'un déclencheur sur le prep version du tableau):

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

Maintenant, un exemple rapide de la façon dont notre nouvel échange de schéma fonctionnerait (et vous devrez peut-être l'ajuster pour gérer chaque déclencheur, si vous avez plusieurs déclencheurs, et le répéter pour le schéma sur le prep version, si vous avez besoin de maintenir un déclencheur là aussi. Veillez tout particulièrement à ce que le code ci-dessous, par souci de brièveté, suppose qu'il n'y a qu'un seul déclencheur sur 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;

Une autre solution de contournement (moins souhaitable) serait d'effectuer l'intégralité de l'opération d'échange de schéma deux fois, y compris les opérations qui se produisent sur le prep version du tableau. Ce qui va largement à l'encontre de l'objectif de l'échange de schéma en premier lieu :réduire le temps pendant lequel les utilisateurs ne peuvent pas accéder aux tables et leur fournir les données mises à jour avec une interruption minimale.