Você provavelmente já ouviu muitas vezes antes que o SQL Server fornece uma garantia para propriedades de transação ACID. Este artigo se concentra na parte D, que obviamente significa durabilidade. Mais especificamente, este artigo concentra-se em um aspecto da arquitetura de log do SQL Server que reforça a durabilidade da transação — liberações de buffer de log. Eu falo sobre a função que o buffer de log serve, as condições que forçam o SQL Server a liberar o buffer de log para o disco, o que você pode fazer para otimizar o desempenho da transação, bem como tecnologias relacionadas recentemente adicionadas, como durabilidade atrasada e memória de classe de armazenamento não volátil.
Limpezas de buffer de log
A parte D nas propriedades da transação ACID significa durabilidade. No nível lógico, isso significa que quando um aplicativo envia ao SQL Server uma instrução para confirmar uma transação (explicitamente ou com uma transação de confirmação automática), o SQL Server normalmente retorna o controle ao chamador somente depois de garantir que a transação seja durável. Em outras palavras, uma vez que o chamador recupere o controle após confirmar uma transação, ele pode confiar que, mesmo que um momento depois o servidor sofra uma falha de energia, as alterações da transação chegaram ao banco de dados. Contanto que o servidor seja reiniciado com sucesso e os arquivos do banco de dados não tenham sido corrompidos, você descobrirá que todas as alterações de transação foram aplicadas.
A maneira como o SQL Server reforça a durabilidade da transação, em parte, é garantindo que todas as alterações da transação sejam gravadas no log de transações do banco de dados no disco antes de devolver o controle ao chamador. No caso de uma falha de energia após a confirmação de uma transação, você sabe que todas essas alterações foram pelo menos gravadas no log de transações em disco. Esse é o caso mesmo que as páginas de dados relacionadas tenham sido modificadas apenas no cache de dados (o pool de buffers), mas ainda não liberadas para os arquivos de dados no disco. Quando você reinicia o SQL Server, durante a fase de refazer do processo de recuperação, o SQL Server usa as informações registradas no log para reproduzir as alterações que foram aplicadas após o último ponto de verificação e que não chegaram aos arquivos de dados. Há um pouco mais na história dependendo do modelo de recuperação que você está usando e se as operações em massa foram aplicadas após o último ponto de verificação, mas para os propósitos de nossa discussão, basta focar na parte que envolve o endurecimento das alterações no Log de transações.
A parte complicada na arquitetura de log do SQL Server é que as gravações de log são sequenciais. Se o SQL Server não tivesse usado algum tipo de buffer de log para aliviar as gravações de log em disco, os sistemas de gravação intensiva - especialmente aqueles que envolvem muitas transações pequenas - rapidamente se deparariam com terríveis gargalos de desempenho relacionados à gravação de log.
Para aliviar o impacto negativo no desempenho de gravações de log sequenciais frequentes em disco, o SQL Server usa um buffer de log na memória. As gravações de log são feitas primeiro no buffer de log e determinadas condições fazem com que o SQL Server libere ou proteja o buffer de log no disco. A unidade reforçada (também conhecida como bloco de log) pode variar de um tamanho mínimo de setor (512 bytes) a um máximo de 60 KB. A seguir estão as condições que acionam uma liberação do buffer de log (ignore as partes que aparecem entre colchetes por enquanto):
- O SQL Server recebe uma solicitação de confirmação de uma transação [totalmente durável] que altera dados [em um banco de dados diferente de tempdb]
- O buffer de log fica cheio, atingindo sua capacidade de 60 KB
- O SQL Server precisa proteger páginas de dados sujas, por exemplo, durante um processo de ponto de verificação, e os registros de log que representam as alterações nessas páginas ainda não foram protegidos (registro antecipado de gravação , ou WAL em resumo)
- Você solicita manualmente uma liberação de buffer de log executando o procedimento sys.sp_flush_log
- O SQL Server grava um novo valor de recuperação relacionado ao cache de sequência [em um banco de dados diferente de tempdb]
As primeiras quatro condições devem ser bem claras, se você ignorar por enquanto as informações entre colchetes. O último talvez ainda não esteja claro, mas explicarei em detalhes mais adiante no artigo.
O tempo que o SQL Server espera por uma operação de E/S manipulando uma liberação de buffer de log para ser concluído é refletido pelo tipo de espera WRITELOG.
Então, por que essa informação é tão interessante e o que fazemos com ela? Compreender as condições que acionam as liberações de buffer de log pode ajudá-lo a descobrir por que certas cargas de trabalho enfrentam gargalos relacionados. Além disso, em alguns casos, existem ações que você pode tomar para reduzir ou eliminar esses gargalos. Abordarei vários exemplos, como uma transação grande versus muitas transações pequenas, transações totalmente duráveis versus transações duráveis atrasadas, banco de dados do usuário versus tempdb e cache de objetos de sequência.
Uma transação grande versus muitas transações pequenas
Como mencionado, uma das condições que aciona uma liberação do buffer de log é quando você confirma uma transação para garantir a durabilidade da transação. Isso significa que as cargas de trabalho que envolvem muitas transações pequenas, como cargas de trabalho OLTP, podem enfrentar gargalos relacionados à gravação de log.
Mesmo que esse não seja o caso, se você tiver uma única sessão enviando muitas pequenas alterações, uma maneira simples e eficaz de otimizar o trabalho é aplicar as alterações em uma única grande transação em vez de várias pequenas transações.
Considere o seguinte exemplo simplificado (faça o download do PerformanceV3 aqui):
SET NOCOUNT ON; USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Desativado; -- default DROP TABLE SE EXISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i +=1;END;
Esse código executa 1.000.000 pequenas transações que alteram dados em um banco de dados de usuário. Esse trabalho acionará pelo menos 1.000.000 liberações de buffer de log. Você pode obter alguns adicionais devido ao preenchimento do buffer de log. Você pode usar o seguinte modelo de teste para contar o número de liberações de buffer de log e medir o tempo que o trabalho levou para ser concluído:
-- Modelo de teste -- ... Preparação vai aqui ... -- Contagem de fluxos de log e medição de tempoDECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats beforeSET @logflushes =( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/s' AND instance_name =@db ); SET @starttime =SYSDATETIME(); -- ... O trabalho real vai aqui ... -- Stats afterSET @duration =DATEDIFF(second, @starttime, SYSDATETIME());SET @logflushes =( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/sec ' AND instance_name =@db ) - @logflushes; SELECT @duration AS duração em segundos, @logflushes AS logflushes;
Mesmo que o nome do contador de desempenho seja Log Flushes/s, na verdade, ele continua acumulando o número de liberações de buffer de log até agora. Assim, o código subtrai a contagem antes do trabalho da contagem depois do trabalho para descobrir a contagem de liberações de log geradas pelo trabalho. Esse código também mede o tempo em segundos que o trabalho levou para ser concluído. Mesmo que eu não faça isso aqui, você poderia, se quisesse, descobrir da mesma forma o número de registros de log e o tamanho gravado no log pelo trabalho consultando os estados antes e depois do trabalho do fn_dblog função.
Para o nosso exemplo acima, segue a parte que você precisa colocar na seção de preparação do modelo de teste:
-- PreparaçãoSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Desativado; DROP TABLE SE EXISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'PerformanceV3'; DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT;
E a seguir está a parte que você precisa colocar na seção de trabalho real:
-- Trabalho realDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i +=1;END;
Ao todo, você obtém o seguinte código:
-- Exemplo de teste com muitas transações pequenas e totalmente duráveis no banco de dados do usuário-- ... Preparação vai aqui ... -- PreparaçãoSET NOCOUNT ON;USE PerformanceV3; ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Desativado; DROP TABLE SE EXISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'PerformanceV3'; DECLARE @logflushes AS INT, @starttime AS DATETIME2, @duration AS INT; -- Stats beforeSET @logflushes =( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/s' AND instance_name =@db ); SET @starttime =SYSDATETIME(); -- ... O trabalho real vai aqui ... -- Trabalho realDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i +=1;END; -- Stats afterSET @duration =DATEDIFF(second, @starttime, SYSDATETIME()); SET @logflushes =( SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name ='Log Flushes/s' AND instance_name =@db ) - @logflushes; SELECT @duration AS duração em segundos, @logflushes AS logflushes;
Esse código levou 193 segundos para ser concluído no meu sistema e acionou 1.000.036 liberações de buffer de log. Isso é muito lento, mas pode ser explicado devido ao grande número de liberações de log.
Em cargas de trabalho OLTP típicas, diferentes sessões enviam pequenas alterações em diferentes pequenas transações simultaneamente, portanto, não é como se você realmente tivesse a opção de encapsular muitas pequenas alterações em uma única grande transação. No entanto, se sua situação é que todas as pequenas alterações são enviadas da mesma sessão, uma maneira simples de otimizar o trabalho é encapsulá-lo em uma única transação. Isso lhe dará dois benefícios principais. Uma é que seu trabalho gravará menos registros de log. Com 1.000.000 de transações pequenas, cada transação na verdade grava três registros de log:um para iniciar a transação, um para a alteração e um para confirmar a transação. Então, você está vendo cerca de 3.000.000 de registros de log de transações versus um pouco mais de 1.000.000 quando executados como uma grande transação. Mas o mais importante, com uma grande transação, a maioria dos log flushes são acionados apenas quando o buffer de log fica cheio, além de mais um log flush no final da transação quando ela é confirmada. A diferença de desempenho pode ser bastante significativa. Para testar o trabalho em uma grande transação, use o código a seguir na parte de trabalho real do modelo de teste:
-- Trabalho realBEGIN TRAN; DECLARE @i AS INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1; FIM; COMMIT TRAN;
No meu sistema, esse trabalho foi concluído em 7 segundos e acionou 1.758 liberações de log. Aqui está uma comparação entre as duas opções:
#transactions log libera a duração em segundos -------------- ------------ -------------- ------1000000 1000036 1931 1758 7
Mas, novamente, em cargas de trabalho OLTP típicas, você realmente não tem a opção de substituir muitas pequenas transações enviadas de diferentes sessões por uma grande transação enviada da mesma sessão.
Transações totalmente duráveis versus duráveis atrasadas
A partir do SQL Server 2014, você pode usar um recurso chamado durabilidade atrasada que permite melhorar o desempenho de cargas de trabalho com muitas transações pequenas, mesmo se enviadas por sessões diferentes, sacrificando a garantia normal de durabilidade total. Ao confirmar uma transação durável atrasada, o SQL Server reconhece a confirmação assim que o registro de log de confirmação é gravado no buffer de log, sem acionar uma liberação de buffer de log. O buffer de log é liberado devido a qualquer uma das outras condições mencionadas, como quando ele é preenchido, mas não quando uma transação durável atrasada é confirmada.
Antes de usar esse recurso, você precisa pensar com muito cuidado se é apropriado para você. Em termos de desempenho, seu impacto é significativo apenas em cargas de trabalho com muitas transações pequenas. Se para começar sua carga de trabalho envolve principalmente grandes transações, você provavelmente não verá nenhuma vantagem de desempenho. Mais importante, você precisa perceber o potencial de perda de dados. Digamos que o aplicativo confirme uma transação durável atrasada. Um registro de confirmação é gravado no buffer de log e imediatamente reconhecido (controle devolvido ao chamador). Se o SQL Server passar por uma falha de energia antes que o buffer de log seja liberado, após a reinicialização, o processo de recuperação desfaz todas as alterações feitas pela transação, mesmo que o aplicativo pense que ela foi confirmada.
Então, quando é bom usar esse recurso? Um caso óbvio é quando a perda de dados não é um problema, como este exemplo de Melissa Connors da SentryOne. Outra é quando, após uma reinicialização, você tem meios de identificar quais alterações não chegaram ao banco de dados e pode reproduzi-las. Se sua situação não se enquadra em uma dessas duas categorias, não use esse recurso apesar da tentação.
Para trabalhar com transações duráveis atrasadas, você precisa definir uma opção de banco de dados chamada DELAYED_DURABILITY. Esta opção pode ser definida para um dos três valores:
- Desativado (padrão):todas as transações no banco de dados são totalmente duráveis e, portanto, cada confirmação aciona uma liberação de buffer de log
- Forçado :todas as transações no banco de dados são atrasadas duráveis e, portanto, as confirmações não acionam uma liberação do buffer de log
- Permitido :a menos que mencionado de outra forma, as transações são totalmente duráveis e confirmá-las aciona uma liberação do buffer de log; no entanto, se você usar a opção DELAYED_DURABILITY =ON em uma instrução COMMIT TRAN ou em um bloco atômico (de um proc compilado nativamente), essa transação específica será atrasada durável e, portanto, confirmá-la não acionará uma liberação de buffer de log
Como teste, use o seguinte código na seção de preparação do nosso modelo de teste (observe que a opção de banco de dados está definida como Forçada):
-- PreparaçãoSET NOCOUNT ON;USE PerformanceV3; -- http://tsql.solidq.com/SampleDatabases/PerformanceV3.zip ALTER DATABASE PerformanceV3 SET DELAYED_DURABILITY =Forçado; DROP TABLE SE EXISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'PerformanceV3';
E use o seguinte código na seção de trabalho real (aviso, 1.000.000 pequenas transações):
-- Trabalho realDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i +=1;END;
Como alternativa, você pode usar o modo Permitido no nível do banco de dados e, em seguida, no comando COMMIT TRAN, adicionar WITH (DELAYED_DURABILITY =ON).
No meu sistema, o trabalho levou 22 segundos para ser concluído e acionou 95.407 liberações de log. Isso é mais do que executar o trabalho como uma grande transação (7 segundos), pois mais registros de log são gerados (lembre-se, por transação, um para iniciar a transação, um para a alteração e um para confirmar a transação); no entanto, é muito mais rápido do que os 193 segundos que levou o trabalho para ser concluído usando 1.000.000 de transações totalmente duráveis, já que o número de liberações de log caiu de mais de 1.000.000 para menos de 100.000. Além disso, com durabilidade atrasada, você obteria o ganho de desempenho mesmo se as transações fossem enviadas de sessões diferentes, onde não é uma opção usar uma grande transação.
Para demonstrar que não há benefício em usar durabilidade atrasada ao fazer o trabalho como grandes transações, mantenha o mesmo código na parte de preparação do último teste e use o seguinte código na parte de trabalho real:
-- Trabalho realBEGIN TRAN; DECLARE @i AS INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;END; COMMIT TRAN;
Eu tenho 8 segundos de tempo de execução (em comparação com 7 para uma grande transação totalmente durável) e 1.759 log flushes (em comparação com 1.758). Os números são essencialmente os mesmos, mas com a transação durável atrasada, você corre o risco de perda de dados.
Aqui está um resumo dos números de desempenho para todos os quatro testes:
durabilidade #transactions log libera duração em segundos-------- ------------------ ------ ------ --------------------completo 1000000 1000036 193completo 1 1758 7atrasado 1000000 95407 22atrasado 1 1759 8
Memória da classe de armazenamento
O recurso de durabilidade atrasada pode melhorar significativamente o desempenho de cargas de trabalho no estilo OLTP que envolvem um grande número de pequenas transações de atualização que exigem alta frequência e baixa latência. O problema é que você está arriscando a perda de dados. E se você não puder permitir nenhuma perda de dados, mas ainda quiser ganhos de desempenho semelhantes a durabilidade atrasada, em que o buffer de log não seja liberado para cada confirmação, e sim quando estiver cheio? Todos nós gostamos de comer o bolo e comê-lo também, certo?
Você pode conseguir isso no SQL Server 2016 SP1 ou posterior usando memória de classe de armazenamento, também conhecida como armazenamento não volátil NVDIMM-N. Esse hardware é essencialmente um módulo de memória que oferece desempenho de nível de memória, mas as informações são mantidas e, portanto, não são perdidas quando a energia acaba. A adição no SQL Server 2016 SP1 permite configurar o buffer de log como persistente em tal hardware. Para fazer isso, você configura o SCM como um volume no Windows e o formata como um volume do Modo de Acesso Direto (DAX). Em seguida, você adiciona um arquivo de log ao banco de dados usando o comando ALTER DATABASE
Para obter mais detalhes sobre esse recurso, incluindo números de desempenho, consulte Aceleração de latência de confirmação de transação usando memória de classe de armazenamento no Windows Server 2016/SQL Server 2016 SP1 por Kevin Farlee.
Curiosamente, o SQL Server 2019 aprimora o suporte para memória de classe de armazenamento além do cenário de cache de log persistente. Ele suporta a colocação de arquivos de dados, arquivos de log e arquivos de ponto de verificação OLTP na memória em tal hardware. Tudo o que você precisa fazer é expô-lo como um volume no nível do sistema operacional e formatá-lo como uma unidade DAX. O SQL Server 2019 reconhece automaticamente essa tecnologia e funciona de forma iluminada modo, acessando diretamente o dispositivo, ignorando a pilha de armazenamento do sistema operacional. Bem vindo ao futuro!
Banco de dados do usuário versus tempdb
É claro que o banco de dados tempdb é criado do zero como uma nova cópia do banco de dados modelo toda vez que você reinicia o SQL Server. Como tal, nunca há necessidade de recuperar nenhum dado que você grava no tempdb, seja em tabelas temporárias, variáveis de tabela ou tabelas de usuário. Tudo se foi após a reinicialização. Sabendo disso, o SQL Server pode relaxar muitos dos requisitos relacionados ao log. Por exemplo, independentemente de você ativar ou não a opção de durabilidade atrasada, os eventos de confirmação não acionam uma liberação de buffer de log. Além disso, a quantidade de informações que precisam ser registradas é reduzida, pois o SQL Server precisa apenas de informações suficientes para dar suporte à reversão de transações ou ao desfazer o trabalho, se necessário, mas não ao repasse de transações ou ao refazer o trabalho. Como resultado, os registros de log de transações que representam alterações em um objeto em tempdb tendem a ser menores em comparação com quando a mesma alteração é aplicada a um objeto em um banco de dados de usuário.
Para demonstrar isso, você executará os mesmos testes executados anteriormente no PerformanceV3, só que desta vez no tempdb. Começaremos com o teste de muitas transações pequenas quando a opção de banco de dados DELAYED_DURABILITY estiver definida como Desativada (padrão). Use o seguinte código na seção de preparação do modelo de teste:
-- PreparaçãoSET NOCOUNT ON;USE tempdb; ALTER DATABASE tempdb SET DELAYED_DURABILITY =Desativado; DROP TABLE SE EXISTE dbo.T1; CREATE TABLE dbo.T1(col1 INT NOT NULL); DECLARE @db AS sysname =N'tempdb';
Use o seguinte código na seção de trabalho real:
-- Trabalho realDECLARE @i AS INT =1; WHILE @i <=1000000BEGIN BEGIN TRAN INSERT INTO dbo.T1(col1) VALUES(@i); COMMIT TRAN; SET @i +=1;END;
Esse trabalho gerou 5.095 liberações de log e levou 19 segundos para ser concluído. Isso é comparado a mais de um milhão de liberações de log e 193 segundos em um banco de dados de usuário com durabilidade total. Isso é ainda melhor do que com durabilidade atrasada em um banco de dados do usuário (95.407 liberações de log e 22 segundos) devido ao tamanho reduzido dos registros de log.
Para testar uma transação grande, deixe a seção de preparação inalterada e use o seguinte código na seção de trabalho real:
-- Trabalho realBEGIN TRAN; DECLARE @i AS INT =1; WHILE @i <=1000000BEGIN INSERT INTO dbo.T1(col1) VALUES(@i); SET @i +=1;END; COMMIT TRAN;
Eu tenho 1.228 log flushes e 9 segundos de tempo de execução. Isso é comparado a 1.758 liberações de log e 7 segundos de tempo de execução no banco de dados do usuário. O tempo de execução é semelhante, até um pouco mais rápido no banco de dados do usuário, mas pode haver pequenas variações entre os testes. Os tamanhos dos registros de log no tempdb são reduzidos e, portanto, você obtém menos liberações de log em comparação com o banco de dados do usuário.
Você também pode tentar executar os testes com a opção DELAYED_DURABILITY definida como Forced, mas isso não terá impacto no tempdb, pois, como mencionado, de qualquer maneira os eventos de commit não acionam uma liberação de log no tempdb.
Aqui estão as medidas de desempenho para todos os testes, tanto no banco de dados do usuário quanto no tempdb:
durabilidade do banco de dados #transactions log libera duração em segundos-------------- ------------------- ----- --------- ------------ --------------------DesempenhoV3 completo 1000000 1000036 193DesempenhoV3 completo 1 1758 7DesempenhoV3 atrasado 1000000 95407 22PerformanceV3 atrasado 1 1759 8tempdb cheio 1000000 5095 19tempdb cheio 1 1228 9tempdb atrasado 1000000 5091 18tempdb atrasado 1 1226 9
Cache de objeto de sequência
Talvez um caso surpreendente que acione liberações de buffer de log esteja relacionado à opção de cache de objeto de sequência. Considere como exemplo a seguinte definição de sequência:
CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- o tamanho padrão do cache é 50;
Toda vez que você precisar de um novo valor de sequência, use a função NEXT VALUE FOR, assim:
SELECIONE O PRÓXIMO VALOR PARA dbo.Seq1;
A propriedade CACHE é um recurso de desempenho. Sem ele, toda vez que um novo valor de sequência fosse solicitado, o SQL Server teria que gravar o valor atual no disco para fins de recuperação. De fato, esse é o comportamento que você obtém ao usar o modo NO CACHE. Em vez disso, quando a opção é definida como um valor maior que zero, o SQL Server grava um valor de recuperação no disco apenas uma vez a cada número de solicitações de tamanho de cache. O SQL Server mantém dois membros na memória, dimensionados como o tipo de sequência, um mantendo o valor atual e outro mantendo o número de valores restantes antes que a próxima gravação em disco do valor de recuperação seja necessária. Em caso de falha de energia, na reinicialização, o SQL Server define o valor da sequência atual para o valor de recuperação.
Isso é provavelmente muito mais fácil de explicar com um exemplo. Considere a definição de sequência acima com a opção CACHE definida como 50 (padrão). Você solicita um novo valor de sequência pela primeira vez executando a instrução SELECT acima. O SQL Server define os membros mencionados acima com os seguintes valores:
Valor de recuperação no disco:50, valor atual na memória:1, valores restantes na memória:49, você obtém:1
Mais 49 solicitações não vão tocar no disco, mas apenas atualizar os membros da memória. Após 50 solicitações no total, os membros são definidos com os seguintes valores:
Valor de recuperação no disco:50, valor atual na memória:50, valores restantes na memória:0, você obtém:50
Faça outra solicitação para um novo valor de sequência e isso acionará uma gravação em disco do valor de recuperação 100. Os membros são definidos com os seguintes valores:
Valor de recuperação no disco:100, valor atual na memória:51, valores restantes na memória:49, você obtém:51
Se neste ponto o sistema sofrer uma falha de energia, após a reinicialização, o valor da sequência atual é definido como 100 (o valor recuperado do disco). A próxima solicitação de um valor de sequência produz 101 (gravando o valor de recuperação 150 no disco). Você perdeu todos os valores no intervalo de 52 a 100. O máximo que você pode perder devido a um encerramento incorreto do processo do SQL Server, como no caso de uma falha de energia, são tantos valores quanto o tamanho do cache. A compensação é clara; quanto maior o tamanho do cache, menos gravações em disco do valor de recuperação e, portanto, melhor o desempenho. Ao mesmo tempo, maior a diferença que pode ser gerada entre dois valores de sequência em caso de falha de energia.
Tudo isso é bastante simples, e talvez você esteja muito familiarizado com o funcionamento. O que pode ser surpreendente é que toda vez que o SQL Server grava um novo valor de recuperação no disco (a cada 50 solicitações em nosso exemplo), ele também protege o buffer de log. Esse não é o caso da propriedade de coluna de identidade, embora o SQL Server use internamente o mesmo recurso de cache para identidade como faz para o objeto de sequência, ele simplesmente não permite que você controle seu tamanho. Está ativado por padrão com tamanho 10000 para BIGINT e NUMERIC, 1000 para INT, 100 para SMALLINT e 10 para TINYINT. Se desejar, você pode desativá-lo com o sinalizador de rastreamento 272 ou a opção de configuração com escopo IDENTITY_CACHE (2017+). O motivo pelo qual o SQL Server não precisa liberar o buffer de log ao gravar o valor de recuperação relacionado ao cache de identidade no disco é que um novo valor de identidade só pode ser criado ao inserir uma linha em uma tabela. Em caso de falha de energia, uma linha inserida em uma tabela por uma transação que não foi confirmada será retirada da tabela como parte do processo de recuperação do banco de dados quando o sistema for reiniciado. Portanto, mesmo que após a reinicialização o SQL Server gere o mesmo valor de identidade como aquele criado na transação que não foi confirmada, não há chance de duplicatas, pois a linha foi retirada da tabela. Se a transação fosse confirmada, isso teria acionado uma liberação de log, que também persistiria a gravação de um valor de recuperação relacionado ao cache. Portanto, a Microsoft não se sentiu obrigada a liberar o buffer de log toda vez que ocorre uma gravação de disco relacionada ao cache de identidade do valor de recuperação.
Com o objeto de sequência a situação é diferente. Um aplicativo pode solicitar um novo valor de sequência e não armazená-lo no banco de dados. Em caso de falha de energia após a criação de um novo valor de sequência em uma transação que não foi confirmada, após a reinicialização, não há como o SQL Server informar a aplicação para não confiar nesse valor. Portanto, para evitar a criação de um novo valor de sequência após a reinicialização que seja igual a um valor de sequência gerado anteriormente, o SQL Server força uma liberação de log sempre que um novo valor de recuperação relacionado ao cache de sequência é gravado no disco. Uma exceção a essa regra é quando o objeto de sequência é criado em tempdb, é claro que não há necessidade de tais liberações de log, pois de qualquer forma, após uma reinicialização do sistema, o tempdb é criado novamente.
Um impacto negativo no desempenho das descargas de log frequentes é especialmente perceptível ao usar um tamanho de cache de sequência muito pequeno e em uma transação gerando muitos valores de sequência, por exemplo, ao inserir muitas linhas em uma tabela. Sem a sequência, essa transação fortaleceria principalmente o buffer de log quando ele ficasse cheio, além de mais uma vez quando a transação fosse confirmada. Mas com a sequência, você obtém uma liberação de log sempre que ocorre uma gravação em disco de um valor de recuperação. É por isso que você deseja evitar o uso de um tamanho de cache pequeno - sem falar no modo NO CACHE.
Para demonstrar isso, use o seguinte código na seção de preparação do nosso modelo de teste:
-- PreparaçãoSET NOCOUNT ON;USE PerformanceV3; -- tente PerformanceV3, tempdb ALTER DATABASE PerformanceV3 -- tente PerformanceV3, tempdb SET DELAYED_DURABILITY =Desativado; -- tente desabilitado, forçado DROP TABLE IF EXISTS dbo.T1; DROP SEQUENCE SE EXISTE dbo.Seq1; CREATE SEQUENCE dbo.Seq1 AS BIGINT MINVALUE 1 CACHE 50; -- tente NO CACHE, CACHE 50, CACHE 10000 DECLARE @db AS sysname =N'PerformanceV3'; -- tente PerformanceV3, tempdb
E o seguinte código na seção de trabalho real:
-- Real workSELECT -- n -- para testar sem seq NEXT VALUE FOR dbo.Seq1 AS n -- para testar sequenceINTO dbo.T1FROM PerformanceV3.dbo.GetNums(1, 1000000) AS N;
Esse código usa uma transação para gravar 1.000.000 de linhas em uma tabela usando a instrução SELECT INTO, gerando tantos valores de sequência quanto o número de linhas inseridas.
Conforme instruído nos comentários, execute o teste com NO CACHE, CACHE 50 e CACHE 10000, tanto no PerformanceV3 quanto no tempdb, e tente as transações totalmente duráveis e as duráveis atrasadas.
Aqui estão os números de desempenho que obtive no meu sistema:
o log de cache de durabilidade do banco de dados libera a duração em segundos-------------- ------------------- ------ --- ------------ --------------------PerformanceV3 full NO CACHE 1000047 171PerformanceV3 full 50 20008 4PerformanceV3 full 10000 339 <1tempdb completo NO CACHE 96 4tempdb completo 50 74 1tempdb completo 10000 8 <1PerformanceV3 atrasado NO CACHE 1000045 166PerformanceV3 atrasado 50 20011 4PerformanceV3 atrasado 10000 334 <1tempdb atrasado NO CACHE 91 4tempdb atrasado 50 74 1tempdb atrasado 10000
Existem algumas coisas interessantes a serem observadas.
Com NO CACHE, você obtém uma liberação de log para cada valor de sequência gerado. Portanto, é altamente recomendável evitá-lo.
Com um tamanho de cache de sequência pequeno, você ainda obtém muitas liberações de log. Talvez a situação não seja tão ruim quanto com NO CACHE, mas observe que a carga de trabalho levou 4 segundos para ser concluída com o tamanho de cache padrão de 50 em comparação com menos de um segundo com o tamanho de 10.000. Eu pessoalmente uso 10.000 como meu valor preferido.
No tempdb, você não obtém liberações de log quando um valor de recuperação relacionado ao cache de sequência é gravado no disco, mas o valor de recuperação ainda é gravado no disco a cada número de solicitações do tamanho do cache. Isso talvez seja surpreendente, pois esse valor nunca precisaria ser recuperado. Therefore, even when using a sequence object in tempdb, I’d still recommend using a large cache size.
Also notice that delayed durability doesn’t prevent the need for log flushes every time the sequence cache-related recovery value is written to disk.
Conclusão
This article focused on log buffer flushes. Understanding this aspect of SQL Server’s logging architecture is important especially in order to be able to optimize OLTP-style workloads that require high frequency and low latency. Workloads using In-Memory OLTP included, of course. You have more options with newer features like delayed durability and persisted log buffer with storage class memory. Make sure you’re very careful with the former, though, since it does incur potential for data loss unlike the latter.
Be careful not to use the sequence object with a small cache size, not to speak of the NO CACHE mode. I find the default size 50 too small and prefer to use 10,000. I’ve heard people expressing concerns that with a cache size 10000, after multiple power failures they might lose all the values in the type. However, even with a four-byte INT type, using only the positive range, 10,000 fits 214,748 times. If your system experience that many power failures, you have a completely different problem to worry about. Therefore, I feel very comfortable with a cache size of 10,000.