다음을 통해 공유


SQL Server에서 마지막 페이지 삽입 PAGELATCH_EX 경합 해결

원래 제품 버전: SQL Server
원래 KB 번호: 4460004

이 문서에서는 SQL Server에서 마지막 페이지 삽입 PAGELATCH_EX 경합을 해결하는 방법을 소개합니다.

증상

다음 시나리오를 고려하세요.

  • Getdate() 함수를 통해 삽입되는 Id 열 또는 DateTime 열과 같은 순차적 값을 포함하는 열이 있습니다.

  • 순차 열이 선행 열로 포함된 클러스터형 인덱스가 있습니다.

    참고 항목

    가장 일반적인 시나리오는 ID 열의 클러스터형 기본 키입니다. 비클러스터형 인덱스에 대해서는 이 문제를 자주 관찰할 수 없습니다.

  • 애플리케이션은 테이블에 대해 INSERT 또는 UPDATE 작업을 자주 수행합니다.

  • 시스템에 많은 CPU가 있습니다. 일반적으로 서버에는 16개 이상의 CPU가 있습니다. 이 하드웨어 구성을 사용하면 여러 세션에서 동일한 테이블에 대해 INSERT 작업을 동시에 수행할 수 있습니다.

이 경우 애플리케이션의 성능이 저하 될 수 있습니다. 대기 유형을 sys.dm_exec_requests검사할 때 PAGELATCH_EX 대기 유형 및 이 대기 유형에서 대기 중인 많은 세션에 대한 대기를 관찰합니다.

시스템에서 다음 진단 쿼리를 실행하는 경우 또 다른 문제가 발생합니다.

session_id 50 및 wait_type = 'pagelatch_ex'에서 sys.dm_exec_requests session_id, wait_type > , wait_time wait_resource 선택합니다.

이 경우 다음과 유사한 결과를 얻을 수 있습니다.

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

여러 세션이 모두 다음 패턴과 유사한 동일한 리소스를 기다리고 있음을 알 수 있습니다.

database_id = 5, file_id = 1, 데이터베이스 page_id = 4144

참고 항목

database_id 사용자 데이터베이스여야 합니다(ID 번호가 5보다 크거나 같음). database_id 2면 대신 TEMPDB의 파일, 추적 플래그 및 업데이트에서 설명하는 문제가 발생할 수 있습니다.

원인

PAGELATCH (데이터 또는 인덱스 페이지의 래치)는 스레드 동기화 메커니즘입니다. 버퍼 캐시에 있는 데이터베이스 페이지에 대한 단기 물리적 액세스를 동기화하는 데 사용됩니다.

PAGELATCHPAGEIOLATCH와 다릅니다. 후자는 디스크에서 읽거나 디스크에 쓸 때 페이지에 대한 물리적 액세스를 동기화하는 데 사용됩니다.

페이지 래치는 물리적 페이지 보호를 보장하기 때문에 모든 시스템에서 일반적입니다. 클러스터형 인덱스는 선행 키 열로 데이터를 정렬합니다. 따라서 순차 열에 인덱스를 만들 때 모든 새 데이터 삽입은 해당 페이지가 채워질 때까지 인덱스 끝의 동일한 페이지에서 발생합니다. 그러나 부하가 높은 상태에서 동시 INSERT 작업으로 인해 B-트리의 마지막 페이지에서 경합이 발생할 수 있습니다. 이 경합은 클러스터형 및 비클러스터형 인덱스에 발생할 수 있습니다. 그 이유는 비클러스터형 인덱스가 리프 수준 페이지를 선행 키로 정렬하기 때문입니다. 이 문제를 마지막 페이지 삽입 경합이라고도 합니다.

자세한 내용은 SQL Server에서 래치 경합 진단 및 해결을 참조 하세요.

해결

다음 두 가지 옵션 중 하나를 선택하여 문제를 해결할 수 있습니다.

옵션 1: Azure Data Studio를 통해 Notebook에서 직접 단계 실행

참고 항목

이 Notebook을 열기 전에 Azure Data Studio가 로컬 컴퓨터에 설치되어 있는지 확인합니다. 설치하려면 Azure Data Studio를 설치하는 방법을 알아봅니다.

옵션 2: 수동으로 단계 수행

이 경합을 해결하기 위해 전체 전략은 모든 동시 INSERT 작업이 동일한 데이터베이스 페이지에 액세스하지 못하도록 하는 것입니다. 대신 각 INSERT 작업을 다른 페이지에 액세스하고 동시성을 높입니다. 따라서 순차 열 이외의 열로 데이터를 구성하는 다음 메서드 중에서 이 목표를 달성합니다.

1. PAGELATCH_EX 경합을 확인하고 경합 리소스를 식별합니다.

이 T-SQL 스크립트는 대기 시간이 10ms 이상인 여러 세션(5개 이상)이 있는 시스템에서 대기가 있는지 PAGELATCH_EX 를 검색하는 데 도움이 됩니다. 또한 sys.dm_exec_requests 및 DBCC PAGE 또는 sys.fn_PageResCracker 및 sys.dm_db_page_info 사용하여 경합이 있는 개체를 검색하고 인덱싱하는 데 도움이 됩니다(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. 문제를 해결하는 방법 선택

다음 방법 중 하나를 사용하여 문제를 해결할 수 있습니다. 상황에 가장 적합한 것을 선택합니다.

방법 1: OPTIMIZE_FOR_SEQUENTIAL_KEY 인덱스 옵션 사용(SQL Server 2019에만 해당)

SQL Server 2019에서는 다음 방법 중 하나도 사용하지 않고 이 문제를 해결하는 데 도움이 되는 새 인덱스 옵션(OPTIMIZE_FOR_SEQUENTIAL_KEY)이 추가되었습니다. 자세한 내용은 OPTIMIZE_FOR_SEQUENTIAL_KEY 백그라운드를 참조하세요.

방법 2: ID 열에서 기본 키 이동

순차 값이 포함된 열을 비클러스터형 인덱스로 만들고 클러스터형 인덱스를 다른 열로 이동합니다. 예를 들어 ID 열의 기본 키에 대해 클러스터된 기본 키를 제거한 다음 비클러스터형 기본 키로 다시 만듭니다. 이 메서드는 가장 쉬운 팔로우이며 목표를 직접 달성합니다.

예를 들어 ID 열에 클러스터형 기본 키를 사용하여 정의된 다음 테이블이 있다고 가정합니다.

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 );

이 디자인을 변경하려면 기본 키 인덱스 제거 및 다시 정의할 수 있습니다.

USE testdb;

ALTER TABLE Customers
DROP CONSTRAINT PK__Customer__A4AE64B98819CFF6;

ALTER TABLE Customers
ADD CONSTRAINT pk_Cust1
PRIMARY KEY NONCLUSTERED (CustomerID)
방법 3: 선행 키를 비순차 열로 만들기

선행 열이 순차 열이 아닌 방식으로 클러스터형 인덱스 정의의 순서를 다시 지정합니다. 이 메서드를 사용하려면 클러스터형 인덱스가 복합 인덱스입니다. 예를 들어 고객 테이블에서 CustomerLastName 열을 선행 열로, 그 뒤에 CustomerID를 지정할 수 있습니다. 이 메서드를 철저히 테스트하여 성능 요구 사항을 충족하는지 확인하는 것이 좋습니다.

USE testdb;

ALTER TABLE Customers
ADD CONSTRAINT pk_Cust1
PRIMARY KEY CLUSTERED (CustomerLastName, CustomerID)
메서드 4: 순차적이지 않은 값을 선행 키로 추가

순차적이지 않은 해시 값을 선행 인덱스 키로 추가합니다. 이 기술은 삽입을 분산하는 데도 도움이 됩니다. 해시 값은 시스템의 CPU 수와 일치하는 모듈로로 생성됩니다. 예를 들어 16-CPU 시스템에서는 16의 모듈로를 사용할 수 있습니다. 이 메서드는 여러 데이터베이스 페이지에 대해 INSERT 작업을 균일하게 분산합니다.

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);
방법 5: GUID를 선행 키로 사용

GUID를 인덱스의 선행 키 열로 사용하여 삽입의 균일한 분포를 보장합니다.

참고 항목

목표를 달성하지만 큰 인덱스 키, 빈번한 페이지 분할, 낮은 페이지 밀도 등을 비롯한 여러 가지 과제를 제시하기 때문에 이 방법은 권장하지 않습니다.

방법 6: 해시 값과 함께 테이블 분할 및 계산 열 사용

테이블 분할 및 해시 값이 있는 계산 열을 사용하여 INSERT 작업을 분산합니다. 이 메서드는 테이블 분할을 사용하므로 SQL Server의 Enterprise 버전에서만 사용할 수 있습니다.

참고 항목

SQL Server 2016 SP1 Standard Edition에서 분할된 테이블을 사용할 수 있습니다. 자세한 내용은 SQL Server 2016 버전 및 지원되는 기능 문서의 "테이블 및 인덱스 분할"에 대한 설명을 참조하세요.

다음은 CPU가 16개인 시스템의 예입니다.

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);
방법 7: 메모리 내 OLTP로 전환

또는 특히 래치 경합이 높은 경우 메모리 내 OLTP를 사용합니다. 이 기술은 래치 경합을 전반적으로 제거합니다. 그러나 페이지 래치 경합이 관찰되는 특정 테이블을 메모리 최적화 테이블로 다시 디자인하고 마이그레이션해야 합니다. 메모리 최적화 관리자트랜잭션 성능 분석 보고서를 사용하여 마이그레이션이 가능한지 여부와 마이그레이션을 수행할 작업을 결정할 수 있습니다. 메모리 내 OLTP가 래치 경합을 제거하는 방법에 대한 자세한 내용은 메모리 내 OLTP - 일반적인 워크로드 패턴 및 마이그레이션 고려 사항에서 문서를 다운로드하여 검토합니다.

참조

PAGELATCH_EX 대기 및 무거운 삽입