Database
 sql >> Base de Dados >  >> RDS >> Database

Schema Switch-A-Roo:Parte 2


Em agosto, escrevi um post sobre minha metodologia de troca de esquema para T-SQL terça-feira. A abordagem essencialmente permite que você carregue lentamente uma cópia de uma tabela (digamos, uma tabela de pesquisa de algum tipo) em segundo plano para minimizar a interferência com os usuários:uma vez que a tabela em segundo plano esteja atualizada, tudo o que é necessário para fornecer os dados atualizados para os usuários é uma interrupção longa o suficiente para confirmar uma alteração de metadados.

Nesse post, mencionei duas advertências que a metodologia que defendi ao longo dos anos não atende atualmente:restrições de chave estrangeira e estatísticas . Há uma série de outros recursos que também podem interferir nessa técnica. Um que surgiu em uma conversa recentemente:gatilhos . E há outros:colunas de identidade , restrições de chave primária , restrições padrão , verificar restrições , restrições que fazem referência a UDFs , índices , visualizações (incluindo visualizações indexadas , que requerem SCHEMABINDING ) e partições . Não vou lidar com tudo isso hoje, mas pensei em testar alguns para ver exatamente o que acontece.

Confesso que minha solução original era basicamente um instantâneo de um pobre, sem todos os aborrecimentos, banco de dados inteiro e requisitos de licenciamento de soluções como replicação, espelhamento e grupos de disponibilidade. Essas eram cópias somente leitura de tabelas de produção que estavam sendo "espelhadas" usando T-SQL e a técnica de troca de esquema. Portanto, eles não precisavam de nenhuma dessas teclas, restrições, gatilhos e outros recursos sofisticados. Mas vejo que a técnica pode ser útil em mais cenários e, nesses cenários, alguns dos fatores acima podem entrar em jogo.

Então, vamos configurar um par simples de tabelas que tenham várias dessas propriedades, realizar uma troca de esquema e ver o que ocorre. :-)

Primeiro, os esquemas:
CREATE SCHEMA prep;
GO
CREATE SCHEMA live;
GO
CREATE SCHEMA holder;
GO

Agora, a tabela no live esquema, incluindo um gatilho e uma 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

Agora, repetimos a mesma coisa para a cópia da tabela em prep . Também precisamos de uma segunda cópia do gatilho, porque não podemos criar um gatilho no prep esquema que referencia uma tabela em live , ou vice-versa. Vamos definir propositalmente a identidade para uma semente mais alta e um valor padrão diferente para int_column (para nos ajudar a acompanhar melhor com qual cópia da tabela estamos realmente lidando após várias trocas de esquema):
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

Agora, vamos inserir algumas linhas em cada tabela e observar a saída:
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;

Resultados:
id int_column udf_column computed_column
1
1 20 2
2
1 20 2

Resultados do live.t1
id int_column udf_column computed_column
1000
2 20 3
1001
2 20 3

Resultados de prep.t1

E no painel de mensagens:
live.trig
live.trig
prep.trig
prep.trig
Agora, vamos realizar uma simples troca de esquema:
 -- 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 repita o exercício:
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;

Os resultados nas tabelas parecem bons:
id int_column udf_column computed_column
1
1 20 2
2
1 20 2
3
1 20 2
4
1 20 2

Resultados do 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

Resultados de prep.t1

Mas o painel de mensagens lista a saída do gatilho na ordem errada:
prep.trig
prep.trig
live.trig
live.trig
Então, vamos nos aprofundar em todos os metadados. Aqui está uma consulta que inspecionará rapidamente todas as colunas de identidade, gatilhos, chaves primárias, padrão e restrições de verificação para essas tabelas, concentrando-se no esquema do objeto associado, no nome e na definição (e na semente / último valor para colunas de identidade):
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');

Os resultados indicam uma grande confusão de metadados:
tipo esquema nome definição
Verificar preparação ck_live ([int_column]>(0))
Verificar ao vivo ck_prep ([int_column]>(1))
Padrão preparação df_live1 ((1))
Padrão preparação df_live2 ([dbo].[udf]())
Padrão ao vivo df_prep1 ((2))
Padrão ao vivo df_prep2 ([dbo].[udf]())
Acionador preparação trig_live CREATE TRIGGER live.trig_live ON live.t1 FOR INSERT AS BEGIN PRINT 'live.trig'; END
Acionador ao vivo trig_prep CREATE TRIGGER prep.trig_prep ON prep.t1 FOR INSERT AS BEGIN PRINT 'prep.trig'; END
Identidade preparação semente =1 last_value =4
Identidade ao vivo semente =1000 last_value =1003
Chave primária preparação pk_live
Chave primária ao vivo pk_prep

Metadados pato-pato-ganso

Os problemas com as colunas e restrições de identidade não parecem ser um grande problema. Mesmo que os objetos *pareçam* apontar para os objetos errados de acordo com as visualizações do catálogo, a funcionalidade – pelo menos para inserções básicas – funciona como você poderia esperar se nunca tivesse examinado os metadados.

O grande problema é com o trigger – esquecendo por um momento o quão trivial eu fiz este exemplo, no mundo real, ele provavelmente referencia a tabela base por esquema e nome. Nesse caso, quando está preso à mesa errada, as coisas podem dar... bem, erradas. Vamos voltar:
BEGIN TRANSACTION;
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
COMMIT TRANSACTION;

(Você pode executar a consulta de metadados novamente para se convencer de que tudo voltou ao normal.)

Agora vamos alterar o gatilho *somente* no live versão para realmente fazer algo útil (bem, "útil" no contexto deste experimento):
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

Agora vamos inserir uma linha:
INSERT live.t1 DEFAULT VALUES;

Resultados:
id    msg
----  ----------
5     live.trig

Em seguida, execute a troca novamente:
BEGIN TRANSACTION;
  ALTER SCHEMA holder TRANSFER prep.t1;
  ALTER SCHEMA prep   TRANSFER live.t1;
  ALTER SCHEMA live   TRANSFER holder.t1;
COMMIT TRANSACTION;

E insira outra linha:
INSERT live.t1 DEFAULT VALUES;

Resultados (no painel de mensagens):
prep.trig

Uh-oh. Se realizarmos essa troca de esquema uma vez por hora, então por 12 horas de cada dia, o gatilho não está fazendo o que esperamos, pois está associado à cópia errada da tabela! Agora vamos alterar a versão "prep" do gatilho:
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

Resultado:
Msg 208, Nível 16, Estado 6, Procedimento trig_prep, Linha 1
Nome de objeto inválido 'prep.trig_prep'.
Bem, isso definitivamente não é bom. Como estamos na fase de troca de metadados, não existe tal objeto; os acionadores agora são live.trig_prep e prep.trig_live . Confuso ainda? Eu também. Então vamos tentar isso:
EXEC sp_helptext 'live.trig_prep';

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

Bem, isso não é engraçado? Como altero esse gatilho quando seus metadados não são refletidos adequadamente em sua própria definição? Vamos tentar isso:
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

Resultados:
Msg 2103, Nível 15, Estado 1, Procedimento trig_prep, Linha 1
Não é possível alterar o gatilho 'live.trig_prep' porque seu esquema é diferente do esquema da tabela ou exibição de destino.
Isso também não é bom, obviamente. Parece que não há realmente uma boa maneira de resolver esse cenário que não envolva a troca dos objetos de volta para seus esquemas originais. Eu poderia alterar este gatilho para ser contra 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

Mas agora eu tenho dois gatilhos que dizem, no corpo do texto, que eles operam em live.t1 , mas apenas este realmente executa. Sim, minha cabeça está girando (assim como a de Michael J. Swart (@MJSwart) neste post). E observe que, para limpar essa bagunça, depois de trocar os esquemas novamente, posso descartar os gatilhos com seus nomes originais:
DROP TRIGGER live.trig_live;
DROP TRIGGER prep.trig_prep;

Se eu tentar DROP TRIGGER live.trig_prep; , por exemplo, recebo um erro de objeto não encontrado.

Resoluções?


Uma solução alternativa para o problema do gatilho é gerar dinamicamente o CREATE TRIGGER código e solte e recrie o gatilho, como parte da troca. Primeiro, vamos colocar um gatilho de volta na tabela *current* em live (você pode decidir em seu cenário se você ainda precisa de um gatilho no prep versão da tabela em tudo):
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

Agora, um exemplo rápido de como nossa nova troca de esquema funcionaria (e você pode ter que ajustar isso para lidar com cada gatilho, se tiver vários gatilhos, e repeti-lo para o esquema no prep versão, se você precisar manter um gatilho lá também. Tome cuidado especial para que o código abaixo, por brevidade, assuma que há apenas *um* gatilho em 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;

Outra solução (menos desejável) seria executar toda a operação de troca de esquema duas vezes, incluindo quaisquer operações que ocorram no prep versão da tabela. O que anula amplamente o objetivo da troca de esquema em primeiro lugar:reduzir o tempo que os usuários não podem acessar a(s) tabela(s) e trazer a eles os dados atualizados com o mínimo de interrupção.