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.trigen 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.trigpré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 1Nom 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 1Impossible 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.