V srpnu jsem napsal příspěvek o své metodice swapování schémat pro T-SQL Tuesday. Tento přístup v podstatě umožňuje líné načítání kopie tabulky (řekněme nějaké vyhledávací tabulky) na pozadí, aby se minimalizovaly interference s uživateli:jakmile je tabulka na pozadí aktuální, vše, co je potřeba k dodání aktualizovaných dat. pro uživatele je přerušení dostatečně dlouhé na provedení změny metadat.
V tomto příspěvku jsem zmínil dvě upozornění, že metodika, kterou jsem v průběhu let prosazoval, v současnosti nevyhovuje:zahraniční klíčová omezení a statistiky . Existuje řada dalších funkcí, které mohou narušovat tuto techniku. Jeden, který se nedávno objevil v konverzaci:spouštěče . A jsou tu další:sloupce identity , omezení primárního klíče , výchozí omezení , zkontrolujte omezení , omezení, která odkazují na UDF , indexy , zobrazení (včetně indexovaných zobrazení , které vyžadují SCHEMABINDING
) a oddíly . Nebudu se dnes zabývat všemi těmito, ale řekl jsem si, že jich pár otestuji, abych přesně viděl, co se stane.
Přiznám se, že mé původní řešení bylo v podstatě snímek chudáka, bez všech potíží, celé databáze a licenčních požadavků řešení, jako je replikace, zrcadlení a skupiny dostupnosti. Jednalo se o kopie tabulek pouze pro čtení z produkce, které byly „zrcadleny“ pomocí T-SQL a techniky swapování schémat. Takže nepotřebovali žádné z těchto ozdobných klíčů, omezení, spouštěčů a dalších funkcí. Ale vidím, že tato technika může být užitečná ve více scénářích a v těchto scénářích mohou hrát roli některé z výše uvedených faktorů.
Pojďme tedy nastavit jednoduchý pár tabulek, které mají několik těchto vlastností, proveďte výměnu schématu a uvidíme, co se pokazí. :-)
Nejprve schémata:
CREATE SCHEMA prep; GO CREATE SCHEMA live; GO CREATE SCHEMA holder; GO
Nyní tabulka v live
schéma, včetně spouštěče a 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
Nyní totéž zopakujeme pro kopii tabulky v prep
. Potřebujeme také druhou kopii spouštěče, protože spouštěč nemůžeme vytvořit v prep
schéma, které odkazuje na tabulku v live
, nebo naopak. Záměrně nastavíme identitu na vyšší počáteční hodnotu a jinou výchozí hodnotu pro int_column
(abychom mohli lépe sledovat, se kterou kopií tabulky skutečně máme co do činění po několika výměnách schémat):
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
Nyní vložíme do každé tabulky několik řádků a sledujeme výstup:
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;
Výsledky:
id | int_column | udf_column | computed_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
Výsledky z live.t1
id | int_column | udf_column | computed_column |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
Výsledky z prep.t1
A v podokně zpráv:
live.triglive.trig
prep.trig
prep.trig
Nyní provedeme jednoduchou výměnu schématu:
-- 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;
A pak opakujte cvičení:
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;
Výsledky v tabulkách se zdají být v pořádku:
id | int_column | udf_column | computed_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
3 | 1 | 20 | 2 |
4 | 1 | 20 | 2 |
Výsledky z live.t1
id | int_column | udf_column | computed_column |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
1002 | 2 | 20 | 3 |
1003 | 2 | 20 | 3 |
Výsledky z prep.t1
Ale podokno zpráv uvádí spouštěcí výstup ve špatném pořadí:
prep.trigprep.trig
live.trig
live.trig
Pojďme se tedy ponořit do všech metadat. Zde je dotaz, který rychle zkontroluje všechny sloupce identity, spouštěče, primární klíče, výchozí a zkontroluje omezení pro tyto tabulky, se zaměřením na schéma přidruženého objektu, název a definici (a počáteční / poslední hodnotu pro sloupce identity):
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');
Výsledky naznačují docela nepořádek v metadatech:
type | schéma | jméno | definice |
---|---|---|---|
Zkontrolujte | příprava | ck_live | ([int_column]>(0)) |
Zkontrolujte | živě | ck_prep | ([int_column]>(1)) |
Výchozí | příprava | df_live1 | ((1)) |
Výchozí | příprava | df_live2 | ([dbo].[udf]()) |
Výchozí | živě | df_prep1 | ((2)) |
Výchozí | živě | df_prep2 | ([dbo].[udf]()) |
Spouštěč | příprava | trig_live | CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END |
Spouštěč | živě | trig_prep | CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END |
Identita | příprava | semeno =1 | poslední_hodnota =4 |
Identita | živě | semeno =1000 | poslední_hodnota =1003 |
Primární klíč | příprava | pk_live | |
Primární klíč | živě | pk_prep |
Metadata kachna-kachna-husa
Problémy se sloupci identity a omezeními se nezdají být velkým problémem. I když se zdá, že objekty podle zobrazení katalogu ukazují na nesprávné objekty, funkce – alespoň pro základní vložky – funguje tak, jak byste očekávali, kdybyste se na metadata nikdy nedívali.
Velký problém je se spouštěčem – když jsem na chvíli zapomněl, jak triviální jsem tento příklad udělal, v reálném světě pravděpodobně odkazuje na základní tabulku podle schématu a názvu. V takovém případě, když je připevněna ke špatnému stolu, věci se mohou pokazit... no, špatně. Vraťme se zpět:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
(Můžete znovu spustit dotaz na metadata, abyste se přesvědčili, že je vše zpět k normálu.)
Nyní změňme spouštěč *pouze* na live
verze, aby skutečně udělal něco užitečného (dobře, „užitečného“ v kontextu tohoto experimentu):
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
Nyní vložíme řádek:
INSERT live.t1 DEFAULT VALUES;
Výsledky:
id msg ---- ---------- 5 live.trig
Poté proveďte výměnu znovu:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
A vložte další řádek:
INSERT live.t1 DEFAULT VALUES;
Výsledky (v podokně zpráv):
prep.trig
A jé. Pokud provádíme tuto výměnu schématu jednou za hodinu, pak po dobu 12 hodin z každého dne spouštěč nedělá to, co očekáváme, protože je spojen se špatnou kopií tabulky! Nyní změňme "prep" verzi spouštěče:
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
Výsledek:
Zpráva 208, úroveň 16, stav 6, procedura trig_prep, řádek 1Neplatný název objektu 'prep.trig_prep'.
No, to rozhodně není dobré. Protože jsme ve fázi metadata-is-swap, žádný takový objekt neexistuje; spouštěče jsou nyní live.trig_prep
a prep.trig_live
. Ještě zmatený? Já také. Takže zkusíme toto:
EXEC sp_helptext 'live.trig_prep';
Výsledky:
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
No není to vtipné? Jak změním tento spouštěč, když se jeho metadata ani správně neodrážejí v jeho vlastní definici? Zkusme toto:
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
Výsledky:
Msg 2103, Level 15, State 1, Procedure trig_prep, Line 1Nelze změnit trigger 'live.trig_prep', protože jeho schéma se liší od schématu cílové tabulky nebo pohledu.
To také není dobré, očividně. Zdá se, že ve skutečnosti neexistuje dobrý způsob, jak vyřešit tento scénář, který nezahrnuje výměnu objektů zpět do jejich původních schémat. Tento spouštěč bych mohl upravit tak, aby byl proti 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
Ale teď mám dva spouštěče, které ve svém hlavním textu říkají, že fungují proti live.t1
, ale pouze tento skutečně provede. Ano, točí se mi hlava (a stejně tak i Michael J. Swart (@MJSwart) v tomto příspěvku na blogu). A všimněte si, že za účelem vyčištění tohoto nepořádku mohu po opětovném prohození schémat zahodit spouštěče s jejich původními názvy:
DROP TRIGGER live.trig_live; DROP TRIGGER prep.trig_prep;
Pokud zkusím DROP TRIGGER live.trig_prep;
, například se mi zobrazí chyba nenalezen objekt.
Rozlišení?
Řešením problému se spouštěčem je dynamické generování CREATE TRIGGER
kód a v rámci swapu zahoďte a znovu vytvořte spouštěč. Nejprve vložme spouštěč zpět do *aktuální* tabulky v live
(ve svém scénáři se můžete rozhodnout, zda vůbec potřebujete spouštěč na prep
verze tabulky vůbec):
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
Nyní rychlý příklad toho, jak by fungovala naše nová výměna schématu (a možná budete muset upravit, aby se zabývalo každým spouštěčem, pokud máte více spouštěčů, a zopakovat to pro schéma v prep
verzi, pokud tam také potřebujete udržovat spoušť. Věnujte zvláštní pozornost tomu, že níže uvedený kód pro stručnost předpokládá, že na live.t1
je pouze *jeden* spouštěč .
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;
Dalším (méně žádoucím) řešením by bylo provést celou operaci výměny schématu dvakrát, včetně všech operací, které se vyskytnou proti prep
verze tabulky. Což v první řadě maří účel výměny schémat:zkrácení doby, po kterou uživatelé nemohou přistupovat k tabulkám a přináší jim aktualizovaná data s minimálním přerušením.