Resolva a contenção de PAGELATCH_EX de inserção da última página no SQL Server
Versão original do produto: SQL Server
Número original do KB: 4460004
Este artigo apresenta como resolver a contenção de inserção PAGELATCH_EX
de última página no SQL Server.
Sintomas
Considere os seguintes cenário:
Você tem uma coluna que inclui valores sequenciais, como uma coluna Identity ou uma coluna DateTime, inserida por meio da função Getdate().
Você tem um índice clusterizado que tem a coluna sequencial como uma coluna principal.
Observação
O cenário mais comum é uma chave primária clusterizada em uma coluna Identidade. Com menos frequência, esse problema pode ser observado para índices não clusterizados.
Seu aplicativo faz operações INSERT ou UPDATE frequentes na tabela.
Você tem muitas CPUs no sistema. Normalmente, o servidor tem 16 CPUs ou mais. Essa configuração de hardware permite que várias sessões façam as operações INSERT na mesma tabela simultaneamente.
Nessa situação, você pode experimentar uma diminuição no desempenho do seu aplicativo. Ao examinar os tipos de espera no sys.dm_exec_requests
, você observa esperas no tipo de espera PAGELATCH_EX e muitas sessões que estão aguardando esse tipo de espera.
Outro problema ocorrerá se você executar a seguinte consulta de diagnóstico em seu sistema:
Selecione session_id, wait_type, wait_time wait_resource de sys.dm_exec_requests
onde session_id > 50 e wait_type = 'pagelatch_ex'
Nessa situação, você pode obter resultados semelhantes aos seguintes.
session_id | wait_type | wait_time | wait_resource |
---|---|---|---|
60 | PAGELATCH_EX | 100 | 5:1:4144 |
75 | PAGELATCH_EX | 123 | 5:1:4144 |
79 | PAGELATCH_EX | 401 | 5:1:4144 |
80 | PAGELATCH_EX | 253 | 5:1:4144 |
81 | PAGELATCH_EX | 312 | 5:1:4144 |
82 | PAGELATCH_EX | 355 | 5:1:4144 |
84 | PAGELATCH_EX | 312 | 5:1:4144 |
85 | PAGELATCH_EX | 338 | 5:1:4144 |
87 | PAGELATCH_EX | 405 | 5:1:4144 |
88 | PAGELATCH_EX | 111 | 5:1:4144 |
90 | PAGELATCH_EX | 38 | 5:1:4144 |
92 | PAGELATCH_EX | 115 | 5:1:4144 |
94 | PAGELATCH_EX | 49 | 5:1:4144 |
101 | PAGELATCH_EX | 301 | 5:1:4144 |
102 | PAGELATCH_EX | 45 | 5:1:4144 |
103 | PAGELATCH_EX | 515 | 5:1:4144 |
105 | PAGELATCH_EX | 39 | 5:1:4144 |
Você observa que várias sessões estão aguardando o mesmo recurso que se assemelha ao seguinte padrão:
database_id = 5, file_id = 1, banco de dados page_id = 4144
Observação
O database_id deve ser um banco de dados de usuários (o número de ID é maior ou igual a 5). Se o database_id for 2, você pode, em vez disso, estar enfrentando o problema discutido em Arquivos, sinalizadores de rastreamento e atualizações em TEMPDB.
Causa
PAGELATCH (trava em uma página de dados ou índice) é um mecanismo de sincronização de threads. Ele é usado para sincronizar o acesso físico de curto prazo às páginas do banco de dados localizadas no cache do buffer.
PAGELATCH é distinto de um PAGEIOLATCH. Este último é usado para sincronizar o acesso físico às páginas quando elas são lidas ou gravadas em disco.
As travas de página são comuns em todos os sistemas porque garantem a proteção física da página. Um índice clusterizado ordena os dados pela coluna de chave à esquerda. Por esse motivo, quando você cria o índice em uma coluna sequencial, todas as novas inserções de dados ocorrem na mesma página no final do índice até que essa página seja preenchida. Mas, com carga alta, as operações INSERT simultâneas podem levar à contenção na última página da árvore B. Essa contenção pode ocorrer em índices clusterizados e não clusterizados. Isso ocorre porque os índices não clusterizados ordenam as páginas em nível de folha pela chave à esquerda. Esse problema também é conhecido como contenção de inserção de última página.
Para obter mais informações, consulte Diagnosticando e resolvendo a contenção de trava no SQL Server.
Solução
Você pode escolher uma das duas opções a seguir para resolver o problema.
Opção 1: executar as etapas diretamente em um notebook usando o Azure Data Studio
Observação
Antes de tentar abrir este notebook, verifique se o Azure Data Studio está instalado em seu computador local. Para instalá-lo, acesse Saiba como instalar o Azure Data Studio.
Opção 2: siga as etapas manualmente
Para resolver essa contenção, a estratégia geral é impedir que todas as operações INSERT simultâneas acessem a mesma página do banco de dados. Em vez disso, faça com que cada operação INSERT acesse uma página diferente e aumente a simultaneidade. Portanto, qualquer um dos métodos a seguir que organizam os dados por uma coluna diferente da coluna sequencial atinge esse objetivo.
1. Confirme a contenção em PAGELATCH_EX e identifique o recurso de contenção
Esse script T-SQL ajuda você a descobrir se há PAGELATCH_EX
esperas no sistema com várias sessões (5 ou mais) com tempo de espera significativo (10 ms ou mais). Ele também ajuda você a descobrir em qual objeto e índice a contenção está usando sys.dm_exec_requests e DBCC PAGE ou sys.fn_PageResCracker e sys.dm_db_page_info (somente SQL Server 2019).
SET NOCOUNT ON
DECLARE @dbname SYSNAME, @dbid INT, @objectid INT, @indexid INT, @indexname SYSNAME, @sql VARCHAR(8000), @manul_identification VARCHAR(8000)
IF (CONVERT(INT, SERVERPROPERTY('ProductMajorVersion')) >= 15)
BEGIN
DROP TABLE IF EXISTS #PageLatchEXContention
SELECT DB_NAME(page_info.database_id) DbName, r.db_id DbId, page_info.[object_id] ObjectId, page_info.index_id IndexId
INTO #PageLatchEXContention
FROM sys.dm_exec_requests AS er
CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS st
CROSS APPLY sys.fn_PageResCracker (er.page_resource) AS r
CROSS APPLY sys.dm_db_page_info(r.[db_id], r.[file_id], r.page_id, 'DETAILED') AS page_info
WHERE er.wait_type = 'PAGELATCH_EX' AND page_info.database_id not in (db_id('master'),db_id('msdb'), db_id('model'), db_id('tempdb'))
GROUP BY DB_NAME(page_info.database_id), r.db_id, page_info.[object_id], page_info.index_id
HAVING COUNT(er.session_id) > 5 AND Max (er.wait_time) > 10
SELECT * FROM #PageLatchEXContention
IF EXISTS (SELECT 1 FROM #PageLatchEXContention)
BEGIN
DECLARE optimize_for_seq_key_cursor CURSOR FOR
SELECT DbName, DbId, ObjectId, IndexId FROM #PageLatchEXContention
OPEN optimize_for_seq_key_cursor
FETCH NEXT FROM optimize_for_seq_key_cursor into @dbname, @dbid, @objectid , @indexid
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT 'Consider using below statement to enable OPTIMIZE_FOR_SEQUENTIAL_KEY for the indexes in the "' + @dbname + '" database' AS Recommendation
SELECT @sql = 'select ''use ' + @dbname + '; ALTER INDEX '' + i.name + '' ON ' + OBJECT_NAME(@objectid, @dbid) + ' SET (OPTIMIZE_FOR_SEQUENTIAL_KEY = ON )'' AS Corrective_Action from #PageLatchEXContention pl JOIN ' + @dbname+'.sys.indexes i ON pl.ObjectID = i.object_id WHERE object_id = ' + CONVERT(VARCHAR, @objectid) + ' AND index_id = ' + CONVERT(VARCHAR, @indexid)
EXECUTE (@sql)
FETCH NEXT FROM optimize_for_seq_key_cursor INTO @dbname, @dbid, @objectid , @indexid
END
CLOSE optimize_for_seq_key_cursor
DEALLOCATE optimize_for_seq_key_cursor
END
ELSE
SELECT 'No PAGELATCH_EX contention found on user databases on in SQL Server at this time'
END
ELSE
BEGIN
IF OBJECT_ID('tempdb..#PageLatchEXContentionLegacy') IS NOT NULL
DROP TABLE #PageLatchEXContentionLegacy
SELECT 'dbcc traceon (3604); dbcc page(' + replace(wait_resource,':',',') + ',3); dbcc traceoff (3604)' TSQL_Command
INTO #PageLatchEXContentionLegacy
FROM sys.dm_exec_requests er
WHERE er.wait_type = 'PAGELATCH_EX' AND er.database_id NOT IN (db_id('master'),db_id('msdb'), db_id('model'), db_id('tempdb'))
GROUP BY wait_resource
HAVING COUNT(er.session_id) > 5 AND Max (er.wait_time) > 10
SELECT * FROM #PageLatchEXContentionLegacy
IF EXISTS(SELECT 1 FROM #PageLatchEXContentionLegacy)
BEGIN
SELECT 'On SQL Server 2017 or lower versions, you can manually identify the object where contention is occurring using DBCC PAGE locate the m_objId = ??. Then SELECT OBJECT_NAME(object_id_identified) and locate indexes with sequential values in this object' AS Recommendation
DECLARE get_command CURSOR FOR
SELECT TSQL_Command from #PageLatchEXContentionLegacy
OPEN get_command
FETCH NEXT FROM get_command into @sql
WHILE @@FETCH_STATUS = 0
BEGIN
SELECT @sql AS Step1_Run_This_Command_To_Find_Object
SELECT 'select OBJECT_NAME(object_id_identified)' AS Step2_Find_Object_Name_From_ID
FETCH NEXT FROM get_command INTO @sql
END
CLOSE get_command
DEALLOCATE get_command
SELECT 'Follow https://learn.microsoft.com/troubleshoot/sql/performance/resolve-pagelatch-ex-contention for resolution recommendations that fits your environment best' Step3_Apply_KB_article
END
ELSE
SELECT 'No PAGELATCH_EX contention found on user databases on in SQL Server at this time'
END
2. Escolha um método para resolver o problema
Você pode usar um dos seguintes métodos para resolver o problema. Escolha o que melhor se adapta às suas circunstâncias.
Método 1: Usar OPTIMIZE_FOR_SEQUENTIAL_KEY opção de índice (somente SQL Server 2019)
No SQL Server 2019, foi adicionada uma nova opção de índice (OPTIMIZE_FOR_SEQUENTIAL_KEY
) que pode ajudar a resolver esse problema sem usar nenhum dos métodos a seguir. Consulte Bastidores no OPTIMIZE_FOR_SEQUENTIAL_KEY para obter mais informações.
Método 2: Mover a chave primária da coluna de identidade
Transforme a coluna que contém valores sequenciais em um índice não clusterizado e mova o índice clusterizado para outra coluna. Por exemplo, para uma chave primária em uma coluna de identidade, remova a chave primária clusterizada e recrie-a como uma chave primária não clusterizada. Este método é o mais fácil de seguir e atinge diretamente o objetivo.
Por exemplo, suponha que você tenha a tabela a seguir que foi definida usando uma chave primária clusterizada em uma coluna Identidade.
USE testdb;
CREATE TABLE Customers
( CustomerID BIGINT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
CustomerLastName VARCHAR (32) NOT NULL,
CustomerFirstName VARCHAR(32) NOT NULL );
Para alterar esse design, você pode remover o índice de chave primária e redefini-lo.
USE testdb;
ALTER TABLE Customers
DROP CONSTRAINT PK__Customer__A4AE64B98819CFF6;
ALTER TABLE Customers
ADD CONSTRAINT pk_Cust1
PRIMARY KEY NONCLUSTERED (CustomerID)
Método 3: Transformar a chave principal em uma coluna não sequencial
Reordene a definição de índice clusterizado de forma que a coluna à esquerda não seja a coluna sequencial. Esse método requer que o índice clusterizado seja um índice composto. Por exemplo, em uma tabela de clientes, você pode fazer com que uma coluna CustomerLastName seja a coluna principal, seguida pela CustomerID. Recomendamos que você teste completamente esse método para garantir que ele atenda aos requisitos de desempenho.
USE testdb;
ALTER TABLE Customers
ADD CONSTRAINT pk_Cust1
PRIMARY KEY CLUSTERED (CustomerLastName, CustomerID)
Método 4: Adicionar um valor não sequencial como chave principal
Adicione um valor de hash não sequencial como a chave de índice principal. Essa técnica também ajuda a espalhar as inserções. Um valor de hash é gerado como um módulo que corresponde ao número de CPUs no sistema. Por exemplo, em um sistema de 16 CPUs, você pode usar um módulo de 16. Esse método distribui as operações INSERT uniformemente em várias páginas do banco de dados.
USE testdb;
CREATE TABLE Customers
( CustomerID BIGINT IDENTITY(1,1) NOT NULL,
CustomerLastName VARCHAR (32) NOT NULL,
CustomerFirstName VARCHAR(32) NOT NULL );
ALTER TABLE Customers
ADD [HashValue] AS (CONVERT([TINYINT], abs([CustomerID])%16)) PERSISTED NOT NULL;
ALTER TABLE Customers
ADD CONSTRAINT pk_table1
PRIMARY KEY CLUSTERED (HashValue, CustomerID);
Método 5: Usar um GUID como chave principal
Use um GUID como a coluna de chave à esquerda de um índice para garantir a distribuição uniforme de inserções.
Observação
Embora atinja a meta, não recomendamos esse método porque ele apresenta vários desafios, incluindo uma chave de índice grande, divisões de página frequentes, baixa densidade de páginas e assim por diante.
Método 6: Usar particionamento de tabela e uma coluna computada com um valor de hash
Use o particionamento de tabela e uma coluna computada que tenha um valor de hash para distribuir as operações INSERT. Como esse método usa particionamento de tabela, ele só pode ser usado em edições Enterprise do SQL Server.
Observação
Você pode usar tabelas particionadas no SQL Server 2016 SP1 Standard Edition. Para obter mais informações, consulte a descrição de "Particionamento de tabela e índice" no artigo Edições e recursos com suporte do SQL Server 2016.
Veja a seguir um exemplo em um sistema que tem 16 CPUs.
USE testdb;
CREATE TABLE Customers
( CustomerID BIGINT IDENTITY(1,1) NOT NULL,
CustomerLastName VARCHAR (32) NOT NULL,
CustomerFirstName VARCHAR(32) NOT NULL );
ALTER TABLE Customers
ADD [HashID] AS CONVERT(TINYINT, ABS(CustomerID % 16)) PERSISTED NOT NULL;
CREATE PARTITION FUNCTION pf_hash (TINYINT) AS RANGE LEFT FOR VALUES (0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15) ;
CREATE PARTITION SCHEME ps_hash AS PARTITION pf_hash ALL TO ([PRIMARY]);
CREATE UNIQUE CLUSTERED INDEX CIX_Hash
ON Customers (CustomerID, HashID) ON ps_hash(HashID);
Método 7: Alternar para OLTP na memória
Como alternativa, use o OLTP na memória, especialmente se a contenção de trava for alta. Essa tecnologia elimina a contenção geral da trava. No entanto, você precisa reprojetar e migrar as tabelas específicas, onde a contenção de trava de página é observada, para uma tabela com otimização de memória. Você pode usar o Supervisor de Otimização de Memória e o Relatório de Análise de Desempenho de Transação para determinar se a migração é possível e qual seria o esforço para fazer a migração. Para obter mais informações sobre como o OLTP in-memory elimina a contenção de trava, baixe e examine o documento em OLTP in-memory - Common Workload Patterns and Migration Considerations.