Още през август написах публикация за моята методология за размяна на схеми за T-SQL във вторник. Подходът по същество ви позволява да заредите мързеливо копие на таблица (да речем, някаква таблица за търсене) във фонов режим, за да сведете до минимум смущенията с потребителите:след като фоновата таблица е актуална, всичко, което е необходимо за доставяне на актуализираните данни за потребителите е прекъсване, достатъчно дълго, за да се извърши промяна на метаданните.
В тази публикация споменах две предупреждения, че методологията, която поддържах през годините, понастоящем не се грижи за:ограничения на външния ключ и статистика . Има множество други функции, които също могат да попречат на тази техника. Един, който се появи в разговор наскоро:задействания . Има и други:графи за идентичност , ограничения на първичния ключ , ограничения по подразбиране , проверете ограниченията , ограничения, които препращат UDF , индекси , прегледи (включително индексирани изгледи , които изискват SCHEMABINDING
) и раздели . Няма да се занимавам с всички тези днес, но реших да тествам няколко, за да видя какво точно ще се случи.
Ще призная, че първоначалното ми решение беше основно моментна снимка на бедния човек, без всички проблеми, цялата база данни и лицензионни изисквания на решения като репликация, огледално копиране и групи за наличност. Това бяха копия само за четене на таблици от производството, които бяха "огледални" с помощта на T-SQL и техниката за размяна на схеми. Така че те не се нуждаеха от тези фантастични клавиши, ограничения, тригери и други функции. Но виждам, че техниката може да бъде полезна в повече сценарии и в тези сценарии някои от горните фактори могат да влязат в игра.
Така че нека настроим обикновена двойка таблици, които имат няколко от тези свойства, да извършим размяна на схемата и да видим какво се поврежда. :-)
Първо, схемите:
CREATE SCHEMA prep; GO CREATE SCHEMA live; GO CREATE SCHEMA holder; GO
Сега таблицата в live
схема, включително тригер и 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
Сега повтаряме същото нещо за копието на таблицата в prep
. Нуждаем се и от второ копие на тригера, защото не можем да създадем тригер в prep
схема, която препраща към таблица в live
, или обратно. Нарочно ще зададем идентичността на по-високо начало и различна стойност по подразбиране за int_column
(за да ни помогне да следим по-добре с кое копие на таблицата наистина работим след множество размяна на схеми):
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
Сега нека вмъкнем няколко реда във всяка таблица и да наблюдаваме изхода:
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;
Резултати:
id | int_column | udf_column | изчислена_колона |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
Резултати от live.t1
id | int_column | udf_column | изчислена_колона |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
Резултати от prep.t1
И в панела за съобщения:
live.triglive.trig
prep.trig
prep.trig
Сега, нека извършим проста размяна на схема:
-- 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;
И след това повторете упражнението:
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;
Резултатите в таблиците изглеждат наред:
id | int_column | udf_column | изчислена_колона |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
3 | 1 | 20 | 2 |
4 | 1 | 20 | 2 |
Резултати от live.t1
id | int_column | udf_column | изчислена_колона |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
1002 | 2 | 20 | 3 |
1003 | 2 | 20 | 3 |
Резултати от prep.t1
Но панелът за съобщения изброява изхода на тригера в грешен ред:
prep.trigprep.trig
live.trig
live.trig
И така, нека се поразровим във всички метаданни. Ето заявка, която бързо ще инспектира всички колони за идентичност, тригери, първични ключове, ограничения по подразбиране и проверка за тези таблици, като се фокусира върху схемата на свързания обект, името и дефиницията (и началната/последната стойност за колони за идентичност):
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');
Резултатите показват доста бъркотия с метаданни:
тип | схема | име | определение |
---|---|---|---|
Проверка | подготовка | ck_live | ([int_column]>(0)) |
Проверка | на живо | ck_prep | ([int_column]>(1)) |
По подразбиране | подготовка | df_live1 | ((1)) |
По подразбиране | подготовка | df_live2 | ([dbo].[udf]()) |
По подразбиране | на живо | df_prep1 | ((2)) |
По подразбиране | на живо | df_prep2 | ([dbo].[udf]()) |
Задействане | подготовка | trig_live | CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END |
Задействане | на живо | trig_prep | CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END |
Идентичност | подготовка | семена =1 | последна_стойност =4 |
Идентичност | на живо | семена =1000 | последна_стойност =1003 |
Първичен ключ | подготовка | pk_live | |
Първичен ключ | на живо | pk_prep |
Метаданни duck-duck-goose
Проблемите с колоните за идентичност и ограниченията не изглежда да са голям проблем. Въпреки че обектите *изглежда* сочат към грешни обекти според изгледите на каталога, функционалността – поне за основни вмъквания – работи както бихте очаквали, ако никога не сте гледали метаданните.
Големият проблем е с тригера – забравяйки за момент колко тривиален направих този пример, в реалния свят той вероятно препраща към базовата таблица по схема и име. В този случай, когато е прикрепен към грешната маса, нещата могат да тръгнат... добре, наред. Нека се върнем назад:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
(Можете да изпълните отново заявката за метаданни, за да се убедите, че всичко е нормално.)
Сега нека променим тригера *само* на live
версия, за да направите нещо полезно (е, "полезно" в контекста на този експеримент):
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
Сега нека вмъкнем ред:
INSERT live.t1 DEFAULT VALUES;
Резултати:
id msg ---- ---------- 5 live.trig
След това извършете размяната отново:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
И вмъкнете още един ред:
INSERT live.t1 DEFAULT VALUES;
Резултати (в панела за съобщения):
prep.trig
О-о. Ако извършим тази размяна на схема веднъж на час, тогава за 12 часа всеки ден, тригерът не прави това, което очакваме да направи, тъй като е свързан с грешно копие на таблицата! Сега нека променим "подготвителната" версия на тригера:
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
Резултат:
Съобщение 208, ниво 16, състояние 6, процедура trig_prep, ред 1Невалидно име на обект 'prep.trig_prep'.
Е, това определено не е добре. Тъй като сме във фазата на размяната на метаданни, няма такъв обект; тригерите вече са live.trig_prep
и prep.trig_live
. Още ли сте объркани? Аз също. Така че нека опитаме това:
EXEC sp_helptext 'live.trig_prep';
Резултати:
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Е, не е ли смешно? Как да променя този тригер, когато неговите метаданни дори не са правилно отразени в собствената му дефиниция? Нека опитаме това:
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
Резултати:
Msg 2103, ниво 15, състояние 1, процедура trig_prep, ред 1Не може да променя тригера 'live.trig_prep', защото неговата схема е различна от схемата на целевата таблица или изглед.
Това също не е добре, очевидно. Изглежда, че всъщност няма добър начин за разрешаване на този сценарий, който не включва размяна на обектите обратно към оригиналните им схеми. Бих могъл да променя този тригер да бъде срещу 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
Но сега имам два тригера, които казват в основния си текст, че работят срещу live.t1
, но само този действително се изпълнява. Да, главата ми се върти (както и тази на Майкъл Дж. Суорт (@MJSwart) в тази публикация в блога). И имайте предвид, че за да изчистя тази бъркотия, след като разменя схемите отново, мога да пусна тригерите с оригиналните им имена:
DROP TRIGGER live.trig_live; DROP TRIGGER prep.trig_prep;
Ако опитам DROP TRIGGER live.trig_prep;
, например получавам грешка за обект не е намерен.
Резолюции?
Заобиколно решение за проблема с тригера е динамично генериране на CREATE TRIGGER
код и пуснете и създайте отново тригера, като част от размяната. Първо, нека върнем тригер обратно в *текущата* таблица в live
(можете да решите във вашия сценарий дали дори имате нужда от задействане на prep
версия на таблицата изобщо):
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
Сега, един бърз пример за това как би работила нашата нова размяна на схема (и може да се наложи да коригирате това, за да се справите с всеки тригер, ако имате няколко задействания, и да го повторите за схемата в prep
версия, ако трябва да поддържате и тригер там. Обърнете специално внимание, че кодът по-долу, за краткост, предполага, че има само *един* тригер на 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;
Друго (по-малко желателно) заобиколно решение би било да се изпълни цялата операция за размяна на схема два пъти, включително каквито и операции да възникнат срещу prep
версия на таблицата. Което до голяма степен побеждава целта на размяната на схемата на първо място:намаляване на времето, през което потребителите нямат достъп до таблицата(ите) и им предоставяне на актуализираните данни с минимално прекъсване.