sql >> Databasteknik >  >> RDS >> Database

Schema Switch-A-Roo:Del 2

Tillbaka i augusti skrev jag ett inlägg om min metodik för schemabyte för T-SQL tisdag. Tillvägagångssättet låter dig i huvudsak ladda en kopia av en tabell (säg en uppslagstabell av något slag) i bakgrunden för att minimera störningar med användare:när bakgrundstabellen är uppdaterad, allt som krävs för att leverera uppdaterad data för användare är ett avbrott som är tillräckligt länge för att begå en metadataändring.

I det inlägget nämnde jag två varningar som den metod som jag har kämpat för genom åren för närvarande inte tillgodoser:främmande nyckelbegränsningar och statistik . Det finns en mängd andra funktioner som också kan störa denna teknik. En som nyligen dök upp i konversation:triggers . Och det finns andra:identitetskolumner , primära nyckelbegränsningar , standardbegränsningar , kontrollera begränsningar , begränsningar som refererar till UDF:er , index , visningar (inklusive indexerade vyer , som kräver SCHEMABINDING ), och partitioner . Jag tänker inte ta itu med alla dessa idag, men jag tänkte testa några för att se exakt vad som händer.

Jag ska erkänna att min ursprungliga lösning i grunden var en fattigmans ögonblicksbild, utan alla krångel, hela databasen och licenskrav för lösningar som replikering, spegling och tillgänglighetsgrupper. Dessa var skrivskyddade kopior av tabeller från produktion som "speglades" med T-SQL och schema swap-tekniken. Så de behövde inte någon av dessa snygga nycklar, begränsningar, utlösare och andra funktioner. Men jag ser att tekniken kan vara användbar i fler scenarier, och i dessa scenarier kan några av ovanstående faktorer spela in.

Så låt oss sätta upp ett enkelt par tabeller som har flera av dessa egenskaper, utföra ett schemabyte och se vad som går sönder. :-)

Först, scheman:

CREATE SCHEMA prep;
GO
CREATE SCHEMA live;
GO
CREATE SCHEMA holder;
GO

Nu, tabellen i live schema, inklusive en utlösare och en 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 upprepar vi samma sak för kopian av tabellen i prep . Vi behöver också en andra kopia av triggern, eftersom vi inte kan skapa en trigger i prep schema som refererar till en tabell i live , eller tvärtom. Vi kommer avsiktligt att ställa in identiteten till ett högre frö och ett annat standardvärde för int_column (för att hjälpa oss att bättre hålla reda på vilken kopia av tabellen vi egentligen har att göra med efter flera schemabyten):

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

Låt oss nu infoga ett par rader i varje tabell och observera resultatet:

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;

Resultat:

id int_column udf_column beräknad_kolumn
1

1 20 2
2

1 20 2

Resultat från live.t1

id int_column udf_column beräknad_kolumn
1000

2 20 3
1001

2 20 3

Resultat från prep.t1

Och i meddelanderutan:

live.trig
live.trig
prep.trig
prep.trig

Låt oss nu utföra ett enkelt schemabyte:

 -- 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;

Och upprepa sedan övningen:

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 i tabellerna verkar okej:

id int_column udf_column beräknad_kolumn
1

1 20 2
2

1 20 2
3

1 20 2
4

1 20 2

Resultat från live.t1

id int_column udf_column beräknad_kolumn
1000

2 20 3
1001

2 20 3
1002

2 20 3
1003

2 20 3

Resultat från prep.t1

Men meddelanderutan listar triggerutgången i fel ordning:

prep.trig
prep.trig
live.trig
live.trig

Så låt oss gräva i all metadata. Här är en fråga som snabbt kommer att inspektera alla identitetskolumner, utlösare, primärnycklar, standard- och kontrollbegränsningar för dessa tabeller, med fokus på schemat för det associerade objektet, namnet och definitionen (och frö-/sistavärdet för identitetskolumner):

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 tyder på en hel röra med metadata:

typ schema namn definition
Kontrollera förberedelser ck_live ([int_column]>(0))
Kontrollera live ck_prep ([int_column]>(1))
Standard förberedelser df_live1 ((1))
Standard förberedelser df_live2 ([dbo].[udf]())
Standard live df_prep1 ((2))
Standard live df_prep2 ([dbo].[udf]())
Trigger förberedelser 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
Identitet förberedelser frö =1 sista_värde =4
Identitet live frö =1000 sista_värde =1003
Primärnyckel förberedelser pk_live
Primärnyckel live pk_prep

Metadata duck-duck-goose

Problemen med identitetskolumner och begränsningar verkar inte vara ett stort problem. Även om objekten *verkar* peka på fel objekt enligt katalogvyerna, fungerar funktionaliteten – åtminstone för grundläggande insättningar – som du kan förvänta dig om du aldrig hade tittat på metadata.

Det stora problemet är med triggern – glömmer för ett ögonblick hur trivialt jag gjorde det här exemplet, i den verkliga världen refererar det förmodligen till bastabellen efter schema och namn. I så fall, när den är fäst vid fel bord, kan saker gå... ja, fel. Låt oss byta tillbaka:

BEGIN TRANSACTION;
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
COMMIT TRANSACTION;

(Du kan köra metadatafrågan igen för att övertyga dig själv om att allt är tillbaka till det normala.)

Låt oss nu ändra utlösaren *endast* på live version för att faktiskt göra något användbart (nåja, "användbart" i sammanhanget av detta 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

Låt oss nu infoga en rad:

INSERT live.t1 DEFAULT VALUES;

Resultat:

id    msg
----  ----------
5     live.trig

Utför sedan bytet igen:

BEGIN TRANSACTION;
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
COMMIT TRANSACTION;

Och infoga ytterligare en rad:

INSERT live.t1 DEFAULT VALUES;

Resultat (i meddelanderutan):

prep.trig

Hoppsan. Om vi ​​utför detta schemaväxling en gång i timmen, så gör triggern under 12 timmar av varje dag inte vad vi förväntar oss att den ska göra, eftersom den är associerad med fel kopia av tabellen! Låt oss nu ändra "prep"-versionen av triggern:

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

Resultat:

Meddelande 208, nivå 16, tillstånd 6, procedur trig_prep, rad 1
Ogiltigt objektnamn 'prep.trig_prep'.

Tja, det är definitivt inte bra. Eftersom vi är i fasen metadata-bytes, finns det inget sådant objekt; triggarna är nu live.trig_prep och prep.trig_live . Förvirrad ännu? Jag med. Så låt oss prova detta:

EXEC sp_helptext 'live.trig_prep';

Resultat:

CREATE TRIGGER prep.trig_prep
ON prep.t1
FOR INSERT
AS
BEGIN
  PRINT 'prep.trig';
END

Tja, är inte det roligt? Hur ändrar jag denna utlösare när dess metadata inte ens återspeglas korrekt i dess egen definition? Låt oss prova detta:

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

Resultat:

Msg 2103, Nivå 15, Tillstånd 1, Procedur trig_prep, Rad 1
Kan inte ändra triggern 'live.trig_prep' eftersom dess schema skiljer sig från schemat för måltabellen eller målvyn.

Det här är inte bra heller, uppenbarligen. Det verkar inte finnas ett riktigt bra sätt att lösa detta scenario som inte innebär att objekten byts tillbaka till deras ursprungliga scheman. Jag skulle kunna ändra denna utlösare så att den är mot 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

Men nu har jag två triggers som säger, i deras brödtext, att de verkar mot live.t1 , men bara den här körs faktiskt. Ja, mitt huvud snurrar (och det var Michael J. Swarts (@MJSwart) också i det här blogginlägget). Och observera att, för att rensa upp den här röran, efter att ha bytt tillbaka scheman igen, kan jag släppa triggers med deras ursprungliga namn:

DROP TRIGGER live.trig_live;
DROP TRIGGER prep.trig_prep;

Om jag försöker DROP TRIGGER live.trig_prep; , till exempel får jag ett felmeddelande om att objekt inte hittades.

Lösningar?

En lösning för triggerproblemet är att dynamiskt generera CREATE TRIGGER kod, och släpp och återskapa utlösaren, som en del av bytet. Låt oss först sätta tillbaka en trigger på den *aktuella* tabellen i live (du kan bestämma i ditt scenario om du ens behöver en trigger på prep version av tabellen överhuvudtaget):

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, ett snabbt exempel på hur vårt nya schemabyte skulle fungera (och du kanske måste justera detta för att hantera varje utlösare, om du har flera utlösare, och upprepa det för schemat i prep version, om du behöver behålla en trigger även där. Var särskilt noga med att koden nedan, för korthets skull, antar att det bara finns *en* trigger på 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;

En annan (mindre önskvärd) lösning skulle vara att utföra hela schemaväxlingsoperationen två gånger, inklusive alla operationer som inträffar mot prep version av tabellen. Vilket till stor del motverkar syftet med schemabytet i första hand:att minska den tid som användare inte kan komma åt tabellen/tabellerna och ge dem uppdaterad data med minimalt avbrott.


  1. Grundläggande om tabelluttryck, del 6 – Rekursiva CTE:er

  2. Har Oracle en motsvarighet till SQL Servers tabellvariabler?

  3. Hur skapar man ett index på datumdelen av DATETIME-fältet i MySql

  4. Hur man byter ut en ny linje i Oracle