In augustus schreef ik een bericht over mijn schema-swap-methodologie voor T-SQL Tuesday. De aanpak stelt u in wezen in staat om een kopie van een tabel (bijvoorbeeld een soort opzoektabel) op de achtergrond te lui laden om interferentie met gebruikers te minimaliseren:zodra de achtergrondtabel up-to-date is, is alles wat nodig is om de bijgewerkte gegevens te leveren voor gebruikers is een onderbreking die lang genoeg is om een wijziging van de metagegevens door te voeren.
In dat bericht noemde ik twee kanttekeningen die de methodologie waar ik in de loop der jaren voor heb gepleit momenteel niet voorziet in:buitenlandse belangrijke beperkingen en statistieken . Er zijn tal van andere functies die deze techniek ook kunnen verstoren. Een die onlangs ter sprake kwam:triggers . En er zijn andere:identiteitskolommen , primaire sleutelbeperkingen , standaardbeperkingen , controleer beperkingen , beperkingen die verwijzen naar UDF's , indexen , weergaven (inclusief geïndexeerde weergaven , waarvoor SCHEMABINDING
. vereist is ), en partities . Ik ga deze vandaag niet allemaal behandelen, maar ik dacht dat ik er een paar zou testen om te zien wat er precies gebeurt.
Ik moet bekennen dat mijn oorspronkelijke oplossing in feite een momentopname was van een arme man, zonder al het gedoe, de volledige database en licentievereisten van oplossingen zoals replicatie, spiegeling en beschikbaarheidsgroepen. Dit waren alleen-lezen kopieën van tabellen uit de productie die werden "gespiegeld" met behulp van T-SQL en de schemawisseltechniek. Dus ze hadden geen van deze mooie toetsen, beperkingen, triggers en andere functies nodig. Maar ik zie wel dat de techniek in meer scenario's nuttig kan zijn, en in die scenario's kunnen enkele van de bovenstaande factoren een rol spelen.
Laten we dus een eenvoudig paar tabellen opzetten die meerdere van deze eigenschappen hebben, een schemawissel uitvoeren en kijken wat er kapot gaat. :-)
Eerst de schema's:
CREATE SCHEMA prep; GO CREATE SCHEMA live; GO CREATE SCHEMA holder; GO
Nu, de tabel in de live
schema, inclusief een trigger en een 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
Nu herhalen we hetzelfde voor de kopie van de tabel in prep
. We hebben ook een tweede kopie van de trigger nodig, omdat we geen trigger kunnen maken in de prep
schema dat verwijst naar een tabel in live
, of vice versa. We stellen de identiteit met opzet in op een hogere seed en een andere standaardwaarde voor int_column
(om ons te helpen beter bij te houden met welk exemplaar van de tabel we echt te maken hebben na meerdere schemawisselingen):
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
Laten we nu een paar rijen in elke tabel invoegen en de uitvoer bekijken:
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;
Resultaten:
id | int_column | udf_column | computed_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
Resultaten van live.t1
id | int_column | udf_column | computed_column |
---|---|---|---|
1000 | 2 | 20 | 3 |
1001 | 2 | 20 | 3 |
Resultaten van prep.t1
En in het berichtenvenster:
live.triglive.trig
prep.trig
prep.trig
Laten we nu een eenvoudige schemawisseling uitvoeren:
-- 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;
En herhaal dan de oefening:
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;
De resultaten in de tabellen lijken in orde:
id | int_column | udf_column | computed_column |
---|---|---|---|
1 | 1 | 20 | 2 |
2 | 1 | 20 | 2 |
3 | 1 | 20 | 2 |
4 | 1 | 20 | 2 |
Resultaten van 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 |
Resultaten van prep.t1
Maar het berichtenvenster geeft de trigger-output in de verkeerde volgorde weer:
prep.trigprep.trig
live.trig
live.trig
Laten we dus ingaan op alle metadata. Hier is een query die snel alle identiteitskolommen, triggers, primaire sleutels, standaard- en controlebeperkingen voor deze tabellen inspecteert, waarbij de nadruk ligt op het schema van het bijbehorende object, de naam en de definitie (en de seed / laatste waarde voor identiteitskolommen):
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');
Resultaten duiden op een behoorlijke metadata-rommel:
typ | schema | naam | definitie |
---|---|---|---|
Controleer | voorbereiding | ck_live | ([int_column]>(0)) |
Controleer | live | ck_prep | ([int_column]>(1)) |
Standaard | voorbereiding | df_live1 | ((1)) |
Standaard | voorbereiding | df_live2 | ([dbo].[udf]()) |
Standaard | live | df_prep1 | ((2)) |
Standaard | live | df_prep2 | ([dbo].[udf]()) |
Trigger | voorbereiding | trig_live | CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END |
Trigger | live | trig_prep | CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END |
Identiteit | voorbereiding | zaad =1 | last_value =4 |
Identiteit | live | zaad =1000 | last_value =1003 |
Primaire sleutel | voorbereiding | pk_live | |
Primaire sleutel | live | pk_prep |
Metagegevens duck-duck-goose
De problemen met de identiteitskolommen en -beperkingen lijken geen groot probleem te zijn. Hoewel de objecten *lijken* te verwijzen naar de verkeerde objecten volgens de catalogusweergaven, werkt de functionaliteit – in ieder geval voor basisinvoegingen – zoals je zou verwachten als je nooit naar de metadata had gekeken.
Het grote probleem zit in de trigger - even vergeten hoe triviaal ik dit voorbeeld heb gemaakt, in de echte wereld verwijst het waarschijnlijk naar de basistabel op schema en naam. In dat geval, wanneer het aan de verkeerde tafel is bevestigd, kan het mis gaan ... nou ja, fout. Laten we terugschakelen:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
(Je kunt de metadata-query opnieuw uitvoeren om jezelf ervan te overtuigen dat alles weer normaal is.)
Laten we nu de trigger veranderen *alleen* op de live
versie om daadwerkelijk iets nuttigs te doen (nou ja, "nuttig" in de context van dit experiment):
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
Laten we nu een rij invoegen:
INSERT live.t1 DEFAULT VALUES;
Resultaten:
id msg ---- ---------- 5 live.trig
Voer de ruil vervolgens opnieuw uit:
BEGIN TRANSACTION; ALTER SCHEMA holder TRANSFER prep.t1; ALTER SCHEMA prep TRANSFER live.t1; ALTER SCHEMA live TRANSFER holder.t1; COMMIT TRANSACTION;
En voeg nog een rij in:
INSERT live.t1 DEFAULT VALUES;
Resultaten (in het berichtenvenster):
prep.trig
Oh Oh. Als we deze schemawisseling één keer per uur uitvoeren, dan doet de trigger gedurende 12 uur per dag niet wat we ervan verwachten, omdat deze is gekoppeld aan de verkeerde kopie van de tabel! Laten we nu de "prep"-versie van de trigger wijzigen:
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
Resultaat:
Msg 208, niveau 16, status 6, procedure trig_prep, regel 1Ongeldige objectnaam 'prep.trig_prep'.
Nou, dat is zeker niet goed. Aangezien we ons in de metadata-is-swapped-fase bevinden, is er geen dergelijk object; de triggers zijn nu live.trig_prep
en prep.trig_live
. Nog in de war? Ik ook. Dus laten we dit proberen:
EXEC sp_helptext 'live.trig_prep';
Resultaten:
CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Nou, is dat niet grappig? Hoe wijzig ik deze trigger als de metadata niet eens goed wordt weerspiegeld in de eigen definitie? Laten we dit proberen:
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
Resultaten:
Msg 2103, Level 15, State 1, Procedure trig_prep, Line 1Kan trigger 'live.trig_prep' niet wijzigen omdat het schema verschilt van het schema van de doeltabel of -view.
Dit is natuurlijk ook niet goed. Het lijkt erop dat er niet echt een goede manier is om dit scenario op te lossen waarbij de objecten niet worden teruggezet naar hun oorspronkelijke schema's. Ik zou deze trigger kunnen wijzigen om tegen live.t1
te zijn :
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
Maar nu heb ik twee triggers die in hun hoofdtekst zeggen dat ze werken tegen live.t1
, maar alleen deze wordt daadwerkelijk uitgevoerd. Ja, mijn hoofd tolt (en dat gold ook voor Michael J. Swart's (@MJSwart) in deze blogpost). En merk op dat, om deze rotzooi op te ruimen, nadat ik de schema's weer terug heb gewisseld, ik de triggers met hun originele namen kan laten vallen:
DROP TRIGGER live.trig_live; DROP TRIGGER prep.trig_prep;
Als ik DROP TRIGGER live.trig_prep;
. probeer , ik krijg bijvoorbeeld de foutmelding 'object niet gevonden'.
Oplossingen?
Een tijdelijke oplossing voor het triggerprobleem is het dynamisch genereren van de CREATE TRIGGER
code, en laat de trigger vallen en maak deze opnieuw, als onderdeel van de swap. Laten we eerst een trigger terugzetten op de *huidige* tabel in live
(u kunt in uw scenario beslissen of u zelfs een trigger nodig heeft op de prep
versie van de tabel helemaal):
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
Nu, een snel voorbeeld van hoe onze nieuwe schema-swap zou werken (en het kan zijn dat je dit moet aanpassen om met elke trigger om te gaan, als je meerdere triggers hebt, en het herhaalt voor het schema op de prep
versie, als u daar ook een trigger moet behouden. Let er vooral op dat de onderstaande code, voor de beknoptheid, ervan uitgaat dat er slechts *één* trigger is op 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;
Een andere (minder wenselijke) oplossing zou zijn om de hele schemawisselbewerking twee keer uit te voeren, inclusief alle bewerkingen die plaatsvinden tegen de prep
versie van de tafel. Wat in de eerste plaats het doel van de schemawisseling grotendeels tenietdoet:het verkorten van de tijd dat gebruikers geen toegang hebben tot de tabel(len) en het brengen van de bijgewerkte gegevens met minimale onderbreking.