Um requisito comum no ETL e em vários cenários de relatórios é carregar silenciosamente uma tabela de preparo do SQL Server em segundo plano, para que os usuários que consultam os dados não sejam afetados pelas gravações e vice-versa. O truque é como e quando você aponta os usuários para a nova versão atualizada dos dados.
Exemplo simplificado de uma tabela de preparo:analogia de mercado de um agricultor
Então, o que é uma tabela de preparo no SQL? Uma mesa de preparação pode ser mais facilmente compreendida usando um exemplo do mundo real:digamos que você tenha uma mesa cheia de vegetais que está vendendo no mercado local. À medida que seus vegetais são vendidos e você traz um novo estoque:
- Quando você traz uma carga de legumes novos, levará 20 minutos para limpar a mesa e substituir o estoque restante pelo produto mais novo.
- Você não quer que os clientes fiquem sentados e esperem 20 minutos para que a troca aconteça, já que a maioria vai buscar seus vegetais em outro lugar.
Agora, e se você tivesse uma segunda mesa vazia onde você carregasse os novos vegetais, e enquanto você faz isso, os clientes ainda podem comprar os vegetais mais velhos da primeira mesa? (Vamos fingir que não é porque os vegetais mais velhos ficaram ruins ou são menos desejáveis.)
Atualizando tabelas no SQL Server
Existem vários métodos para recarregar tabelas inteiras enquanto elas estão sendo consultadas ativamente; duas décadas atrás, aproveitei desenfreadamente o
sp_rename
— Eu jogava um jogo de shell com uma cópia de sombra vazia da tabela, recarregando alegremente a cópia de sombra e apenas realizando a renomeação dentro de uma transação. No SQL Server 2005, comecei a usar esquemas para manter cópias de sombra de tabelas que simplesmente transferi usando a mesma técnica de jogo de shell, sobre a qual escrevi nestes dois posts:
- Trick Shots:Schema Switch-a-Roo
- Schema Switch-a-Roo, Parte 2
A única vantagem de transferir objetos entre esquemas sobre renomeá-los é que não há mensagens de aviso sobre a renomeação de um objeto – o que nem é um problema, por si só, exceto que as mensagens de aviso preenchem os logs do histórico do agente muito mais rápido.
Ambas as abordagens ainda exigem um bloqueio de modificação de esquema (Sch-M), portanto, devem aguardar que quaisquer transações existentes liberem seus próprios bloqueios. Uma vez que adquirem seu bloqueio Sch-M, eles bloqueiam todas as consultas subsequentes que exigem bloqueios de estabilidade de esquema (Sch-S)... que é quase todas as consultas. Ele pode rapidamente se tornar um pesadelo da cadeia de bloqueio, pois qualquer nova consulta que precise de Sch-S precisa entrar em uma fila atrás do Sch-M. (E não, você não pode contornar isso usando RCSI ou
NOLOCK
em todos os lugares, pois mesmo essas consultas ainda exigem Sch-S. Você não pode adquirir Sch-S com um Sch-M instalado, pois eles são incompatíveis - Michael J. Swart fala sobre isso aqui.) Kendra Little realmente abriu meus olhos sobre os perigos da transferência de esquema em seu post, “Staging Data:Locking Danger with ALTER SCHEMA TRANSFER”. Lá ela mostra por que a transferência de esquema pode ser pior do que renomear. Mais tarde, ela detalhou uma terceira e muito menos impactante maneira de trocar tabelas, que agora uso exclusivamente:comutação de partição. Esse método permite que o switch aguarde em uma prioridade mais baixa, o que nem é uma opção com as técnicas de renomeação ou transferência de esquema. Joe Sack entrou em detalhes sobre esse aprimoramento adicionado no SQL Server 2014:“Exploring Low Priority Lock Wait Options in SQL Server 2014 CTP1.”
Exemplo de alternância de partição do SQL Server
Vejamos um exemplo básico, seguindo a essência completa de Kendra aqui. Primeiro, vamos criar dois novos bancos de dados:
CREATE DATABASE NewWay; CREATE DATABASE OldWay; GO
No novo banco de dados, criaremos uma tabela para manter nosso inventário de vegetais e duas cópias da tabela para nosso jogo de shell:
USE NewWay; GO CREATE TABLE dbo.Vegetables_NewWay ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO -- we need to create two extra copies of the table. CREATE TABLE dbo.Vegetables_NewWay_prev ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO CREATE TABLE dbo.Vegetables_NewWay_hold ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO
Criamos um procedimento que carrega a cópia de teste da tabela e, em seguida, usa uma transação para desativar a cópia atual.
CREATE PROCEDURE dbo.DoTheVeggieSwap_NewWay AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE dbo.Vegetables_NewWay_prev; INSERT dbo.Vegetables_NewWay_prev SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, LEFT(m.definition, 500) FROM sys.dm_exec_sessions AS s CROSS JOIN model.sys.all_objects AS o INNER JOIN model.sys.all_sql_modules AS m ON o.[object_id] = m.[object_id]; -- need to take Sch-M locks here: BEGIN TRANSACTION; ALTER TABLE dbo.Vegetables_NewWay SWITCH TO dbo.Vegetables_NewWay_hold WITH (WAIT_AT_LOW_PRIORITY (MAX_DURATION = 1 MINUTES, ABORT_AFTER_WAIT = BLOCKERS)); ALTER TABLE dbo.Vegetables_NewWay_prev SWITCH TO dbo.Vegetables_NewWay; COMMIT TRANSACTION; -- and now users will query the new data in dbo -- can switch the old copy back and truncate it -- without interfering with other queries ALTER TABLE dbo.Vegetables_NewWay_hold SWITCH TO dbo.Vegetables_NewWay_prev; TRUNCATE TABLE dbo.Vegetables_NewWay_prev; END GO
A beleza de
WAIT_AT_LOW_PRIORITY
é que você pode controlar completamente o comportamento com o ABORT_AFTER_WAIT
opção:ABORT_AFTER_WAIT configuração | Descrição/sintomas |
---|---|
EU | Isso significa que o switch irá desistir após n minutos. Para a sessão que está tentando realizar a troca, isso aparecerá como a mensagem de erro: O tempo limite da solicitação de bloqueio foi excedido. |
BLOQUEADORES | Isso determina que o switch aguardará até n minutos e, em seguida, forçar-se para a frente da linha matando todos os bloqueadores à sua frente . Sessões que tentam interagir com a tabela que é atingida pela operação do switch verão alguma combinação destas mensagens de erro: Sua sessão foi desconectada devido a uma operação DDL de alta prioridade. Não é possível continuar a execução porque a sessão está no estado de interrupção. Um erro severo ocorreu no comando atual. Os resultados, se existirem, deveriam ser descartados. |
NENHUM | Isso diz que o switch aguardará alegremente até chegar a sua vez, independentemente de MAX_DURATION . Este é o mesmo comportamento que você obteria com renomeação, transferência de esquema ou alternância de partição sem WAIT_AT_LOW_PRIORITY . |
Os
BLOCKERS
opção não é a maneira mais amigável de lidar com as coisas, já que você já está dizendo que está tudo bem por meio dessa operação de teste/troca para os usuários verem dados um pouco desatualizados. Eu provavelmente preferiria usar SELF
e faça a operação tentar novamente nos casos em que não conseguiu obter os bloqueios necessários no tempo alocado. No entanto, eu acompanharia com que frequência ele falha, especialmente falhas consecutivas, porque você quer garantir que os dados nunca fiquem muito obsoletos. Em comparação com a antiga maneira de alternar entre esquemas
Veja como eu teria lidado com a mudança antes:
USE OldWay; GO -- create two schemas and two copies of the table CREATE SCHEMA prev AUTHORIZATION dbo; GO CREATE SCHEMA hold AUTHORIZATION dbo; GO CREATE TABLE dbo.Vegetables_OldWay ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO CREATE TABLE prev.Vegetables_OldWay ( VegetableID int, Name sysname, WhenPicked datetime, BackStory nvarchar(max) ); GO CREATE PROCEDURE dbo.DoTheVeggieSwap_OldWay AS BEGIN SET NOCOUNT ON; TRUNCATE TABLE prev.Vegetables_OldWay; INSERT prev.Vegetables_OldWay SELECT TOP (1000000) s.session_id, o.name, s.last_successful_logon, LEFT(m.definition, 500) FROM sys.dm_exec_sessions AS s CROSS JOIN model.sys.all_objects AS o INNER JOIN model.sys.all_sql_modules AS m ON o.[object_id] = m.[object_id]; -- need to take Sch-M locks here: BEGIN TRANSACTION; ALTER SCHEMA hold TRANSFER dbo.Vegetables_OldWay; ALTER SCHEMA dbo TRANSFER prev.Vegetables_OldWay; COMMIT TRANSACTION; -- and now users will query the new data in dbo -- can transfer the old copy back and truncate it without -- interfering with other queries: ALTER SCHEMA prev TRANSFER hold.Vegetables_OldWay; TRUNCATE TABLE prev.Vegetables_OldWay; END GO
Executei testes de simultaneidade usando duas janelas do SQLQueryStress de Erik Ejlskov Jensen:uma para repetir uma chamada para o procedimento a cada minuto e a outra para executar 16 threads assim, milhares de vezes:
BEGIN TRANSACTION; UPDATE TOP (1) dbo.<table> SET name += 'x'; SELECT TOP (10) name FROM dbo.<table> ORDER BY NEWID(); WAITFOR DELAY '00:00:02'; COMMIT TRANSACTION;
Você pode olhar para a saída de SQLQueryStress, ou sys.dm_exec_query_stats, ou Query Store, e você verá algo como os seguintes resultados (mas eu recomendo usar uma ferramenta de monitoramento de desempenho do SQL Server de qualidade se você for sério sobre otimizar proativamente ambientes de banco de dados):
Duração e taxas de erro | Transferência de esquema | ABORT_AFTER_WAIT: SELF | ABORT_AFTER_WAIT: BLOQUEADORES |
---|---|---|---|
Duração média – transferência/troca | 96,4 segundos | 68,4 segundos | 20,8 segundos |
Duração média – DML | 18,7 segundos | 2,7 segundos | 2,9 segundos |
Exceções – Transferência/Troca | 0 | 0,5/minuto | 0 |
Exceções – DML | 0 | 0 | 25,5/minuto |
Observe que as durações e as contagens de exceção serão altamente dependentes das especificações do seu servidor e do que mais está acontecendo em seu ambiente. Observe também que, embora não haja exceções para os testes de transferência de esquema ao usar SQLQueryStress, você pode atingir alguns tempos limite mais rígidos, dependendo do aplicativo consumidor. E foi muito mais lento em média, porque o bloqueio se acumulou muito mais agressivamente. Ninguém nunca quer exceções, mas quando há uma troca como essa, você pode preferir algumas exceções aqui e ali (dependendo da frequência da operação de atualização) do que todos esperando mais o tempo todo.
Mudança de partição versus transferência de renomear/esquema para atualizar tabelas do SQL Server
A alternância de partição permite que você escolha qual parte do seu processo arca com o custo da simultaneidade. Você pode dar preferência ao processo de comutação, para que os dados sejam atualizados de forma mais confiável, mas isso significa que algumas de suas consultas falharão. Por outro lado, você pode priorizar as consultas, ao custo de um processo de atualização mais lento (e uma falha ocasional). O principal objetivo é a alternância de partição do SQL Server é um método superior para atualizar as tabelas do SQL Server em comparação com as técnicas de transferência de renomeação/esquema anteriores em quase todos os pontos, e você pode usar uma lógica de repetição mais robusta ou experimentar tolerâncias de duração para chegar ao ponto ideal para sua carga de trabalho.