Database
 sql >> Database >  >> RDS >> Database

Schema Switch-A-Roo:Parte 2

Ad agosto ho scritto un post sulla mia metodologia di scambio di schemi per T-SQL Tuesday. L'approccio consente essenzialmente di caricare in modo lento una copia di una tabella (ad esempio, una tabella di ricerca di qualche tipo) in background per ridurre al minimo le interferenze con gli utenti:una volta che la tabella in background è aggiornata, tutto ciò che è necessario per fornire i dati aggiornati per gli utenti è un'interruzione sufficientemente lunga per eseguire una modifica dei metadati.

In quel post, ho menzionato due avvertimenti che la metodologia che ho sostenuto nel corso degli anni non soddisfa attualmente:vincoli chiave estranei e statistiche . Ci sono una miriade di altre caratteristiche che possono interferire con questa tecnica pure. Uno che è emerso di recente in una conversazione:trigger . E ce ne sono altri:colonne di identità , vincoli chiave primaria , vincoli predefiniti , controlla i vincoli , vincoli che fanno riferimento a UDF , indici , viste (incluse viste indicizzate , che richiedono SCHEMABINDING ), e partizioni . Oggi non affronterò tutti questi problemi, ma ho pensato di provarne alcuni per vedere esattamente cosa succede.

Confesserò che la mia soluzione originale era fondamentalmente l'istantanea di un povero, senza tutti i problemi, l'intero database e i requisiti di licenza di soluzioni come la replica, il mirroring e i gruppi di disponibilità. Si trattava di copie di sola lettura di tabelle di produzione che venivano "rispecchiate" utilizzando T-SQL e la tecnica di scambio di schemi. Quindi non avevano bisogno di nessuna di queste chiavi fantasiose, vincoli, trigger e altre funzionalità. Ma vedo che la tecnica può essere utile in più scenari e in questi scenari possono entrare in gioco alcuni dei fattori di cui sopra.

Quindi impostiamo una semplice coppia di tabelle che hanno diverse di queste proprietà, eseguiamo uno scambio di schemi e vediamo cosa si interrompe. :-)

Innanzitutto, gli schemi:

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

Ora, la tabella nel live schema, inclusi un trigger e un 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

Ora, ripetiamo la stessa cosa per la copia della tabella in prep . Abbiamo anche bisogno di una seconda copia del trigger, perché non possiamo creare un trigger in prep schema che fa riferimento a una tabella in live , o vice versa. Imposteremo di proposito l'identità su un seme più alto e un valore predefinito diverso per int_column (per aiutarci a tenere traccia di quale copia della tabella abbiamo davvero a che fare dopo più scambi di schemi):

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

Ora inseriamo un paio di righe in ogni tabella e osserviamo l'output:

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;

Risultati:

id colonna_int udf_colonna colonna_calcolata
1

1 20 2
2

1 20 2

Risultati di live.t1

id colonna_int udf_colonna colonna_calcolata
1000

2 20 3
1001

2 20 3

Risultati di prep.t1

E nel riquadro dei messaggi:

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

Ora, eseguiamo un semplice scambio di schemi:

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

E poi ripeti l'esercizio:

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;

I risultati nelle tabelle sembrano ok:

id colonna_int udf_colonna colonna_calcolata
1

1 20 2
2

1 20 2
3

1 20 2
4

1 20 2

Risultati di live.t1

id colonna_int udf_colonna colonna_calcolata
1000

2 20 3
1001

2 20 3
1002

2 20 3
1003

2 20 3

Risultati di prep.t1

Ma il riquadro dei messaggi elenca l'output del trigger nell'ordine sbagliato:

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

Quindi, analizziamo tutti i metadati. Ecco una query che ispezionerà rapidamente tutte le colonne di identità, i trigger, le chiavi primarie, i valori predefiniti e verificherà i vincoli per queste tabelle, concentrandosi sullo schema dell'oggetto associato, sul nome e sulla definizione (e sul valore seme/ultimo per colonne identità):

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

I risultati indicano un bel pasticcio di metadati:

digitare schema definizione
Controlla preparazione ck_live ([colonna_int]>(0))
Controlla in diretta ck_prep ([colonna_int]>(1))
Predefinito preparazione df_live1 ((1))
Predefinito preparazione df_live2 ([dbo].[udf]())
Predefinito in diretta df_prep1 ((2))
Predefinito in diretta df_prep2 ([dbo].[udf]())
Trigger preparazione trig_live CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END
Trigger in diretta trig_prep CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Identità preparazione seme =1 ultimo_valore =4
Identità in diretta seme =1000 ultimo_valore =1003
Chiave primaria preparazione pk_live
Chiave primaria in diretta pk_prep

Metadati papera-anatra-oca

I problemi con le colonne e i vincoli di identità non sembrano essere un grosso problema. Anche se gli oggetti *sembrano* puntare agli oggetti sbagliati in base alle viste del catalogo, la funzionalità, almeno per gli inserti di base, funziona come ci si potrebbe aspettare se non avessi mai guardato i metadati.

Il grosso problema è con il trigger:dimenticando per un momento quanto banale ho reso questo esempio, nel mondo reale, probabilmente fa riferimento alla tabella di base per schema e nome. In tal caso, quando è attaccato al tavolo sbagliato, le cose possono andare... beh, male. Torniamo indietro:

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

(Puoi eseguire nuovamente la query sui metadati per convincerti che tutto è tornato alla normalità.)

Ora cambiamo il trigger *solo* su live versione per fare effettivamente qualcosa di utile (beh, "utile" nel contesto di questo esperimento):

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

Ora inseriamo una riga:

INSERT live.t1 DEFAULT VALUES;

Risultati:

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

Quindi esegui di nuovo lo scambio:

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

E inserisci un'altra riga:

INSERT live.t1 DEFAULT VALUES;

Risultati (nel riquadro dei messaggi):

prep.trig

Uh Oh. Se eseguiamo questo scambio di schemi una volta all'ora, quindi per 12 ore al giorno, il trigger non sta facendo ciò che ci aspettiamo che faccia, poiché è associato alla copia sbagliata della tabella! Ora modifichiamo la versione "preparativa" del trigger:

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

Risultato:

Msg 208, livello 16, stato 6, procedura trig_prep, riga 1
Nome oggetto 'prep.trig_prep' non valido.

Beh, non va assolutamente bene. Poiché siamo nella fase di scambio dei metadati, non esiste un tale oggetto; i trigger ora sono live.trig_prep e prep.trig_live . Confuso ancora? Anche io. Quindi proviamo questo:

EXEC sp_helptext 'live.trig_prep';

Risultati:

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

Beh, non è divertente? Come posso modificare questo trigger quando i suoi metadati non si riflettono nemmeno correttamente nella sua stessa definizione? Proviamo questo:

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

Risultati:

Msg 2103, livello 15, stato 1, procedura trig_prep, riga 1
Impossibile modificare il trigger 'live.trig_prep' perché il relativo schema è diverso dallo schema della tabella o della vista di destinazione.

Anche questo non va bene, ovviamente. Sembra che non ci sia davvero un buon modo per risolvere questo scenario che non implichi il ritorno degli oggetti ai loro schemi originali. Potrei modificare questo trigger in modo che sia contro 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

Ma ora ho due trigger che dicono, nel loro corpo del testo, che operano contro live.t1 , ma solo questo viene effettivamente eseguito. Sì, mi gira la testa (e anche quello di Michael J. Swart (@MJSwart) in questo post sul blog). E nota che, per ripulire questo pasticcio, dopo aver scambiato nuovamente gli schemi, posso rilasciare i trigger con i loro nomi originali:

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

Se provo DROP TRIGGER live.trig_prep; , ad esempio, ottengo un errore di oggetto non trovato.

Risoluzioni?

Una soluzione alternativa per il problema del trigger consiste nel generare dinamicamente il CREATE TRIGGER codice e rilascia e ricrea il trigger, come parte dello scambio. Per prima cosa, reinseriamo un trigger nella tabella *current* in live (puoi decidere nel tuo scenario se hai anche bisogno di un trigger sulla prep versione della tabella):

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

Ora, un rapido esempio di come funzionerebbe il nostro nuovo scambio di schemi (e potrebbe essere necessario modificarlo per gestire ciascun trigger, se si dispone di più trigger, e ripeterlo per lo schema su prep versione, se è necessario mantenere un trigger anche lì. Prestare particolare attenzione al fatto che il codice seguente, per brevità, presuppone che vi sia un solo *un* trigger su 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;

Un'altra soluzione (meno desiderabile) sarebbe quella di eseguire l'intera operazione di scambio dello schema due volte, incluse tutte le operazioni che si verificano contro la prep versione della tabella. Il che vanifica in gran parte lo scopo dello scambio di schemi in primo luogo:ridurre il tempo in cui gli utenti non possono accedere alle tabelle e portare loro i dati aggiornati con interruzioni minime.