Database
 sql >> база данни >  >> RDS >> Database

Схема Switch-A-Roo:Част 2

Още през август написах публикация за моята методология за размяна на схеми за 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.trig
live.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.trig
prep.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 версия на таблицата. Което до голяма степен побеждава целта на размяната на схемата на първо място:намаляване на времето, през което потребителите нямат достъп до таблицата(ите) и им предоставяне на актуализираните данни с минимално прекъсване.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Изтеглете копие на вашата база данни

  2. Обединения на вложени цикли и спулове за производителност

  3. Пазете се от подвеждащи данни от SET STATISTICS IO

  4. Trace Flag 2389 и новият оценител на мощността

  5. Как да получите вчерашна дата в T-SQL