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.triglive.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.trigprep.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 1Nome 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 1Impossibile 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.