sql >> Database >  >> RDS >> Database

Schema Switch-A-Roo:deel 2

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.trig
live.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.trig
prep.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 1
Ongeldige 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 1
Kan 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.


  1. SQL Server SP - Passparameter voor IN-arraylijst?

  2. Wat vertegenwoordigt een dubbele in sql-server?

  3. Hoe een datum in Duits formaat weer te geven in SQL Server (T-SQL)

  4. Hoe controleer je of IDENTITY_INSERT is ingesteld op AAN of UIT in SQL Server?