728x90

SQL Server 데이터베이스의 트랜잭션 로그가 예기치 않게 커지는 것을 막는 방법

기술 자료 ID : 873235
마지막 검토 : 2006년 2월 17일 금요일
수정 : 2.0

소개

이 문서에서는 트랜잭션 로그가 허용할 수 없는 한계까지 커질 경우에 수행해야 할 작업을 설명합니다. 트랜잭션 로그가 커지면 Microsoft SQL Server 데이터베이스를 사용할 수 없게 됩니다. 이 문서에서는 또한 트랜잭션 로그 크기가 예기치 않게 커지는 것을 막기 위해 사용할 수 있는 옵션에 대해서도 설명합니다.

위로 가기

추가 정보

SQL Server 2000 및 SQL Server 2005에서 각 데이터베이스에는 데이터 파일과 트랜잭션 로그 파일이 하나 이상 포함되어 있습니다. SQL Server는 데이터를 데이터 파일에 물리적으로 저장합니다. 트랜잭션 로그 파일은 SQL Server 데이터베이스에 수행한 모든 수정 작업의 세부 정보와 각 수정 작업을 수행한 트랜잭션의 세부 정보를 저장합니다. 트랜잭션 무결성은 SQL Server의 근본적이고 본질적인 특성이므로 SQL Server에서는 트랜잭션의 세부 정보를 로깅하는 기능을 해제할 수 없습니다.

트랜잭션 로그 파일은 논리적으로 가상 로그 파일이라고 하는 보다 작은 세그먼트로 나뉘어집니다. SQL Server 2000에서는 필요한 경우 트랜잭션 로그 파일 크기가 커지도록 구성할 수 있습니다. 트랜잭션 로그 크기가 커지도록 사용자가 제어하거나, 사용 가능한 모든 디스크 공간을 사용하도록 구성할 수도 있습니다. 트랜잭션 로그 파일을 자르거나 늘리는 등 SQL Server에서 트랜잭션 로그 파일 크기를 수정하는 모든 작업은 가상 로그 파일 단위로 수행됩니다.

트랜잭션 로그 파일이 자동으로 커지도록 옵션을 설정한 상태에서 SQL Server 데이터베이스의 해당 트랜잭션 로그 파일이 채워지면 트랜잭션 로그 파일은 가상 로그 파일 단위로 커집니다. 트랜잭션 로그 파일 크기가 아주 커져서 디스크 공간이 부족해질 수도 있습니다. 트랜잭션 로그 파일이 사용 가능한 디스크 공간을 모두 사용하여 더 이상 커질 수 없어지면 데이터베이스에서 데이터를 더 이상 수정할 수 없습니다. 또한 트랜잭션 로그 크기 증가에 따른 공간 부족으로 인해 SQL Server가 데이터베이스를 주의 대상으로 표시할 수 있습니다.

트랜잭션 로그 파일 크기가 예기치 않게 커질 수 있는 시나리오에 대한 자세한 내용은 Microsoft 기술 자료의 다음 문서를 참조하십시오.
317375 (http://support.microsoft.com/kb/317375/) INF: SQL Server에서 트랜잭션 로그가 예기치 않게 커지거나 가득 찬다

위로 가기

트랜잭션 로그 크기 줄이기

트랜잭션 로그 크기가 허용할 수 없는 한계까지 커지는 상황에서 복구하려면 트랜잭션 로그 크기를 줄여야 합니다. 이렇게 하려면 트랜잭션 로그에서 비활성 트랜잭션을 잘라내고 트랜잭션 로그 파일을 축소해야 합니다.

참고 트랜잭션 로그는 데이터베이스의 트랜잭션 무결성을 유지하는 데 매우 중요합니다. 따라서 데이터베이스와 트랜잭션 로그의 백업 복사본을 만든 후에도 트랜잭션 로그 파일을 삭제하지 않아야 합니다.

트랜잭션 로그에서 비활성 트랜잭션 잘라내기

트랜잭션 로그 크기가 허용할 수 없는 한계까지 커지면 즉시 트랜잭션 로그 파일을 백업해야 합니다. 트랜잭션 로그 파일의 백업 복사본을 만드는 동안 SQL Server는 트랜잭션 로그의 비활성 부분을 자동으로 잘라냅니다. 트랜잭션 로그 파일의 비활성 부분에는 완료된 트랜잭션도 포함되므로 복구 프로세스 동안 SQL Server는 더 이상 트랜잭션 로그 파일을 사용하지 않습니다. 또한 트랜잭션 로그 크기가 계속 커져서 추가 공간을 사용하도록 허용하는 대신 이렇게 잘라낸 트랜잭션 로그의 비활성 공간을 재사용합니다.

트랜잭션 로그의 백업 복사본을 만들 때 고려해야 할 문제와 트랜잭션 로그 백업을 복원할 때 고려해야 할 문제에 대한 자세한 내용은 SQL Server 온라인 설명서의 다음 항목을 참조하십시오.
트랜잭션 로그 백업
트랜잭션 로그 백업 및 복원
또한 Truncate 메서드를 사용하여 트랜잭션 로그 파일에서 비활성 트랜잭션을 삭제해도 됩니다. 트랜잭션 로그를 자르는 방법에 대한 자세한 내용은 SQL Server 온라인 설명서의 "트랜잭션 로그 잘라내기" 항목을 참조하십시오.

중요 수동으로 트랜잭션 로그 파일을 자른 후에는 트랜잭션 로그 백업을 만들기 전에 전체 데이터베이스 백업을 만들어야 합니다.

트랜잭션 로그 파일을 자를 때 발생할 수 있는 문제에 대한 자세한 내용은 Microsoft 기술 자료의 다음 문서를 참조하십시오.
62866 (http://support.microsoft.com/kb/62866/) INFO: SQL 트랜잭션 로그가 잘리지 않는 이유

트랜잭션 로그 파일 축소

백업 작업이나 Truncate 메서드는 로그 파일 크기를 줄이지 않습니다. 트랜잭션 로그 파일의 크기를 줄이려면 트랜잭션 로그 파일을 축소해야 합니다. 요청한 크기로 트랜잭션 로그 파일을 축소하고 사용하지 않는 페이지를 제거하려면 DBCC SHRINKFILE 연산을 사용해야 합니다. DBCC SHRINKFILE Transact-SQL 문은 로그 파일 내부의 비활성 부분만 축소할 수 있습니다.

참고 DBCC SHRINKFILE Transact-SQL 문은 단독으로 로그를 자르고 로그 파일 내부에서 사용된 공간을 축소하지 못합니다.

트랜잭션 로그 파일을 축소하는 방법에 대한 자세한 내용은 SQL Server 설명서의 다음 항목을 참조하십시오.
트랜잭션 로그 축소
DBCC SHRINKFILE
SQL Server 2000에서 트랜잭션 로그 파일을 축소하는 방법에 대한 자세한 내용은 Microsoft 기술 자료의 다음 문서를 참조하십시오.
272318 (http://support.microsoft.com/kb/272318/) INF: SQL Server 2000에서 DBCC SHRINKFILE을 사용하여 트랜잭션 로그를 축소하는 방법
트랜잭션 로그 파일을 축소할 때 발생할 수 있는 문제에 대한 자세한 내용은 Microsoft 기술 자료의 다음 문서를 참조하십시오.
814574 (http://support.microsoft.com/kb/814574/) PRB: 오류 메시지: "... 로그 파일을 축소할 수 없습니다."가 트랜잭션 로그 파일을 축소할 때 발생한다
324432 (http://support.microsoft.com/kb/324432/) PRB: DBCC SHRINKFILE 및 SHRINKDATABASE 명령이 부족하게 채워진 Text, Ntext 또는 Image 열 때문에 작동하지 않을 수 있다

위로 가기

트랜잭션 로그 파일이 예기치 않게 커지는 것을 막기

트랜잭션 로그 파일이 예기치 않게 커지는 것을 막으려면 다음 방법 중 하나를 사용하십시오.
트랜잭션 로그 파일이 자동으로 커지지 않도록 트랜잭션 로그 파일 크기를 큰 값으로 설정합니다.
최적의 메모리 크기를 철저하게 계산한 후 백분율 대신 메모리 단위를 사용하여 트랜잭션 로그 파일의 자동 증가 옵션을 구성합니다.

자동 증가 옵션을 구성할 때 고려해야 할 문제에 대한 자세한 내용은 Microsoft 기술 자료의 다음 문서를 참조하십시오.
315512 (http://support.microsoft.com/kb/315512/) INF: SQL Server에서 자동 증가와 자동 축소 구성을 위한 고려 사항
복구 모델을 변경합니다. 재해나 데이터 손상이 발생한 경우에는 데이터베이스의 트랜잭션 무결성과 데이터 일관성이 유지되도록 데이터베이스를 복구해야 합니다. 데이터베이스에 있는 데이터의 중요도에 따라 아래의 복구 모델 중 하나를 사용하여 데이터 백업 방법과 어떤 것이 데이터 손실에 노출되는지 확인할 수 있습니다.
단순 복구 모델
전체 복구 모델
대량 로그 복구 모델
단순 복구 모델을 사용하면 데이터베이스의 최신 백업으로 데이터베이스를 복구할 수 있습니다. 전체 복구 모델이나 대량 로그 복구 모델을 사용하면 트랜잭션 로그 파일 백업으로 데이터베이스를 복원하여 실패가 발생한 시점으로 데이터베이스를 복구할 수 있습니다.

기본적으로 SQL Server 2000 및 SQL Server 2005에서는 SQL Server 데이터베이스에 대한 복구 모델이 전체 복구 모델로 설정되어 있습니다. 전체 복구 모델에서는 트랜잭션 로그 파일 크기가 데이터베이스 크기와 불균형적으로 커지는 것을 막기 위해 트랜잭션 로그를 정기적으로 백업합니다. 그러나 트랜잭션 로그를 정기적으로 백업하지 않으면 트랜잭션 로그 파일 크기가 디스크를 채울 정도로 커져서 SQL Server 데이터베이스에서 데이터를 수정하지 못할 수 있습니다.

재해 복구 작업 동안 트랜잭션 로그 파일을 사용하지 않으려면 전체 복구 모델에서 단순 복구 모델로 변경할 수 있습니다.
정기적으로 트랜잭션 로그 파일을 백업하여 트랜잭션 로그에서 비활성 트랜잭션을 삭제합니다.
트랜잭션을 작게 설계합니다.
커밋되지 않은 트랜잭션이 무기한 계속 실행되지 않도록 합니다.
매일 실행되도록 통계 업데이트 옵션을 예약합니다.
프로덕션 환경에서 작업 부하 성능을 향상시키기 위해 인덱스 조각을 모으려면 DBCC DBREINDEX Transact-SQL 문 대신 DBCC INDEXDEFRAG Transact-SQL 문을 사용합니다. SQL Server 데이터베이스가 전체 복구 모드에 있을 때 DBCC DBREINDEX 문을 실행하면 트랜잭션 로그가 상당히 증가할 수 있습니다. 또한 DBCC INDEXDEGRAG 문은 DBCC DBREINDEX 문과 달리 잠금을 장시간 유지하지 않습니다.

SQL Server 2000에서 인덱스 조각을 모으는 방법에 대한 자세한 내용은 다음 Microsoft 웹 사이트를 참조하십시오.
http://www.microsoft.com/technet/prodtechnol/sql/2000/maintain/ss2kidbp.mspx (http://www.microsoft.com/technet/prodtechnol/sql/2000/maintain/ss2kidbp.mspx)(영문)
데이터베이스 유지 관리 계획의 일부 작업으로 DBCC DBREINDEX 문을 실행해야 할 경우에는 이 작업을 여러 개의 작업으로 나누어야 합니다. 또한 각 작업 실행 사이에 자주 트랜잭션 로그를 백업해야 합니다.

위로 가기

참조

복구 프로세스 동안 추가 디스크 공간 요구 문제를 해결하는 방법에 대한 자세한 내용은 SQL Server 온라인 설명서의 "디스크 공간 부족" 항목을 참조하십시오. 트랜잭션 로그 아키텍처에 대한 자세한 내용은 SQL Server 온라인 설명서의 다음 항목을 참조하십시오.
트랜잭션 로그 아키텍처
트랜잭션 로그 논리 아키텍처
트랜잭션 로그 물리 아키텍처
SQL Server 2000의 복구 모델에 대한 자세한 내용은 SQL Server 온라인 설명서의 다음 항목을 참조하십시오.
복구 모델 선택
단순 복구
전체 복구
대량 로그 복구
복구 모델 전환





Microsoft 제품 관련 기술 전문가들과 온라인으로 정보를 교환하시려면 Microsoft 뉴스 그룹 (http://support.microsoft.com/newsgroups/default.aspx)에 참여하시기 바랍니다.

위로 가기


본 문서의 정보는 다음의 제품에 적용됩니다.
Microsoft SQL Server 2000 Standard Edition
Microsoft SQL Server 2005 Standard Edition
Microsoft SQL Server 2005 Developer Edition
Microsoft SQL 2005 Server Enterprise
Microsoft SQL Server 2005 Express Edition
Microsoft SQL 2005 Server Workgroup
728x90

느린 쿼리


느리거나 오래 실행되는 쿼리는 리소스를 과도하게 소비할 수 있으며 쿼리 차단으로 이어집니다.

과도한 리소스 소비는 CPU 리소스에만 국한되지 않고, I/O 저장소 대역폭과 메모리 대역폭 또한 포함될 수 있습니다. SQL Server 쿼리가 WHERE절을 통해서 결과 집합을 제한함으로써 테이블 전체 스캔을 피하도록 설계되었더라도, 해당 쿼리를 지원하는 적절한 인덱스가 없는 경우에 예상한대로 수행되지 않게됩니다. 또한, 사용자 입력에 따라 응용 프로그램에서 동적으로 WHERE절이 구성되는 경우, 기존 인덱스가 모든 경우의 조건을 다룰 수가 없게 됩니다. Transact-SQL 문장에 의한 과도한 CPU, I/O, 그리고 메모리 소비는 본 문서 전반부에서 다루어졌습니다.

인덱스 부재와 함께 사용되지 않는 인덱스도 존재할 수 있습니다. 모든 인덱스는 유지 관리 대상이므로, 쿼리 성능에는 영향을 미치지 않더라도, DML 쿼리에는 영향을 줍니다.

논리적 읽기를 위한 대기 상태 혹은 쿼리를 차단하고 있는 시스템 리소스에 대한 대기 상태로 인해 쿼리가 느리게 실행될 수 있습니다. 차단의 원인은 서투른 응용 프로그램 설계, 잘못된 쿼리 계획, 유용한 인덱스 부재, 그리고 작업 부하를 고려해서 적절히 구성되지 못한 SQL Server 인스턴스와 같은 것들이 될 수 있습니다.

이번 절은 느린 쿼리의 두 가지 원인, 차단과 인덱스 문제에 촛점을 맞춥니다.


차단


차단은 주로 논리적 읽기에 대한 차단입니다. 이는 특정 리소스에 X 잠금 획득을 위한 대기나 래치 같은 저 수준 동기화 개체로 인한 대기와 같습니다.

논리적 읽기 대기는 이미 잠긴 리소스에 호환되지 않은 다른 잠금을 요청할 때 발생합니다. 이는 Transact-SQL 문장을 실행할 때 트랜잭션 격리 수준에 따라 데이터 일관성을 제공하기 위해서 필요하지만, 최종 사용자에게는 SQL Server가 느리게 실행된다는 인식을 줍니다. 쿼리가 차단된다는 것이 시스템에 어떤 리소스를 소비하는 것은 아니므로, 시간은 오래 걸리지만 리소스 소비는 작은 쿼리를 찾을 수 있습니다. 동시성 제어 및 차단에 대한 상세 정보는 SQL Server 온라인 설명서를 참조하십시오.

저 수준의 동기화 개체에 대한 대기는 시스템이 작업 부하를 처리할 수 있도록 구성되지 못한 결과입니다.

차단/대기에 대한 공통적인 시나리오는:

ㆍ 차단 주범 식별

ㆍ 오래 걸리는 차단 식별

ㆍ 개체 별 차단

ㆍ 페이지 래치 문제

ㆍ 차단으로 인한 전반적인 성능 평가를 위해 SQL Server 대기 사용

요청을 처리하는데 필요한 시스템 리소스(혹은 잠금)가 현재 이용할 수 없는 상태이면 SQL Server 세션은 대기 상태에 놓입니다. 다른 말로 하자면, 리소스가 미해결 요청에 대한 큐를 가지는 것입니다. DMV는 리소스 대기 중인 세션에 대한 정보를 제공합니다.

SQL Server 2005는 보다 상세하고 일관된 대기 정보를 제공합니다, SQL Server 2000은 76개의 대기 유형을 지원한 반면 SQL Server 2005는 대략 125 개 이상의 대기 유형을 보고해 줍니다. DMV는 이러한 정보들을 두 가지 범위로 나누어 제공합니다. SQL Server 전체에 누적된 대기를 위한 sys.dm_os_wait_statistics[역주17]와, 세션 단위로 분류된 대기를 위한 세션 범위의 sys.dm_os_waiting_tasks입니다. 다음의 DMV는 특정 리소스에 대기 중인 작업의 대기 큐에 대한 상세 정보를 제공합니다. 예를 들어, 다음 쿼리를 실행해서 차단된 세션 56에 대한 상세 정보를 찾을 수 있습니다.

select * from sys.dm_os_waiting_tasks where session_id=56

waiting_task_address session_id exec_context_id wait_duration_ms
wait_type
resource_address blocking_task_address blocking_session_id
blocking_exec_context_id resource_description
-------------------- ---------- --------------- -------------------- -----
------------------------------------------------------- ------------------
--------------------- ------------------- ------------------------ -------
--------------------------------------------------------------------------
--------------------------------------------------------------------------
--------------------------------------------------------------------------
---------------------------
0x022A8898 56 0 1103500
LCK_M_S 0x03696820
0x022A8D48 53 NULL
ridlock fileid=1 pageid=143 dbid=9 id=lock3667d00 mode=X
associatedObjectId=72057594038321152

이 결과에서 세션 53이 세션 56에 의해서 특정 잠금 대기로 1103500 밀리초 동안 차단되고 있음을 보여줍니다.

잠금이 허가된 혹은 잠금을 대기 중인 세션을 찾기 위해서, sys.dm_tran_locks DMV를 사용할 수 있습니다. 각 행은 현재 활성 요청을 나타내는 것으로, 잠금 관리자를 통해 잠금을 허가 받았거나 혹은 그 요청에 의해 차단된 상태로 허가를 기다리고 있는 요청입니다. 일반적인 잠금에 대해서는, 허가된 요청은 해당 리소스에 잠금이 요청자에게 허가되었음을 나타냅니다. 대기 중인 요청은 아직 허가받지 못했음을 나타냅니다. 예를 들어, 다음 쿼리는 세션 56이 리소스 1:143:3에 대해서 세션 53에 의한 X 모드가 있으므로 차단되었음을 보여줍니다.

select
request_session_id as spid,
resource_type as rt,
resource_database_id as rdb,
(case resource_type
WHEN 'OBJECT' then object_name(resource_associated_entity_id)
WHEN 'DATABASE' then ' '
ELSE (select object_name(object_id)
from sys.partitions
where hobt_id=resource_associated_entity_id)
END) as objname,
resource_description as rd,
request_mode as rm,
request_status as rs
from sys.dm_tran_locks

다음은 출력 결과입니다.

spid rt rdb objname rd rm
rs
--------------------------------------------------------------------------
---
56 DATABASE 9
S GRANT
53 DATABASE 9 S GRANT
56 PAGE 9 t_lock 1:143 IS GRANT
53 PAGE 9 t_lock 1:143 IX GRANT
53 PAGE 9 t_lock 1:153 IX GRANT
56 OBJECT 9 t_lock IS GRANT
53 OBJECT 9 t_lock IX GRANT
53 KEY 9 t_lock (a400c34cb X GRANT
53 RID 9 t_lock 1:143:3 X GRANT
56 RID 9 t_lock 1:143:3 S WAIT

사실 저장 프로시저 sp_block에서 보여지는대로 위 두 DMV를 조인할 수 있습니다. 그림 1은 차단된 세션과 그 세션을 차단하는 세션 목록을 보여줍니다. 소스 코드는 부록 B에서 찾을 수 있습니다. 필요하면 저장 프로시저를 수행해서 원하는 속성을 결과 집합에 추가하거나 제거할 수 있습니다. @spid 옵션 매개변수는 특정 spid를 차단하는 세션과 잠금 요청에 대한 상세 정보를 제공합니다.

그림 1: sp_block 결과

SQL Server 2000에서는, 다음 문장을 사용해서 차단된 spid에 대한 정보를 알 수 있습니다.

select * from master..sysprocesses where blocked <> 0

저장 프로시저 sp_lock으로 관련된 잠금 정보를 볼 수 있습니다.


오래 걸리는 차단 식별


이전에 언급한대로, SQL Server에서 차단은 일반적인 것이며 트랜잭션의 일관성을 유지하기위해 필요한 논리적 잠금의 결과입니다. 그러나, 잠금에 대한 대기가 한계를 초과하면 응답 시간에 영향을 미칩니다. 오래 걸리는 차단을 식별하기 위해 BlockedProcessThreshold 구성 매개 변수를 사용하면 서버 범위의 차단 임계값을 구성할 수 있습니다. 임계값은 초 단위 시간을 정의합니다. 차단이 임계값을 초과하면 SQL Trace에서 추적할 수 있도록 이벤트가 발생합니다.

예를 들어, 다음과 같이 SQL Server Management Studio에서 차단 프로세스 임계값을 200초로 구성할 수 있습니다:

1.
Execute Sp_configure ‘blocked process threshold’, 200

2.
Reconfigure with override

차단 프로세스 임계값을 구성하고 나서, SQL Trace나 프로파일러를 가지고 추적 이벤트를 캡처합니다.

3.
SQL Trace를 사용한다면, event_id = 137로 sp_trace_setevent를 설정합니다.

4.
SQL Server 프로파일러를 사용한다면, (Errors and Warnings 개체 밑에) Blocked Process Report 이벤트 클래스를 선택합니다. 그림 2를 참조하십시오.

그림 2: 오래 걸리는 차단과 교착 상태 추적


참고 이것은 가벼운 추적 작업에 해당합니다. (1) 차단이 임계값을 초과하거나 (2) 교착상태가 발생할 때만 이벤트가 캡처됩니다. 차단 잠금에 대해 매 200초 간격으로 추적 이벤트가 발생합니다. 이는 600초 동안 차단된 단일 잠금의 경우 3번의 추적 이벤트가 발생하는 것입니다. 그림 3을 참조하십시오.

그림 3: 차단 보고 > 차단 임계값

추적 이벤트에는 차단하고 있는 프로세스와 차단 당하는 프로세스 양쪽의 전체 SQL 문장을 포함합니다. 위 경우 “Update Customers” 문장이 “Select from Customers” 문장을 차단하고 있습니다.

상대적으로, SQL Server 2000에서는 차단 시나리오를 검사하기 위해 Sysprocesses 시스템 테이블을 조회하고 결과를 처리하는 별도의 코드를 필요로 합니다.

sys.dm_db_index_operational_stats를 사용한 개체 별 차단
SQL Server 2005의 새로운 DMV인 sys.dm_db_index_operational_stats는 차단을 포함해서 총체적인 인덱스 사용 통계 정보를 제공합니다. 차단으로 표현하자면, 테이블, 인덱스, 분할 별 상세한 잠금 통계 정보를 제공합니다. 예를 들어 주어진 인덱스나 테이블에 대한 액세스, 잠금(row_lock_count), 차단(row_lock_wait_count), 그리고 대기(row_lock_wait_in_ms) 이력 정보를 포함합니다.

이 DMV를 통해서 얻을 수 있는 정보들은 다음과 같습니다:

ㆍ 잠금 보유 누적 개수, 예) 행 혹은 페이지.

ㆍ 차단 혹은 대기 누적 개수, 예) 행, 페이지.

ㆍ 차단 혹은 대기 누적 시간, 예) 행, 페이지.

ㆍ 페이지 래치 대기 누적 개수.

ㆍ page_latch_wait 대기 누적 시간: 순차 키 입력과 같은 경우의 특정 페이지에 대한 경합과 관련됩니다. 그런 경우, 마지막 페이지에 동시에 여러 프로세스가 쓰기 작업을 수행하기 위해 배타적 페이지 래치를 요구하게 되므로 마지막 페이지에 핫 스팟(hot spot)이 일어나게 됩니다. 이것이 Pagelatch 대기로 나타나게 됩니다.

ㆍ page_io_latch_wait 대기 누적 시간: I/O 래치는 사용자가 요청한 페이지가 버퍼 풀에 존재하지 않을 때 발생합니다. 느린 I/O 하위 시스템, 혹은 용량을 초과한 I/O 하위 시스템은 높은 PageIOlatch를 경험할 수 있으며 이것이 실제 I/O 문제를 나타냅니다. 이러한 문제는 캐시 플러시나 인덱스 부재 등에 의해서도 일어날 수 있습니다.

ㆍ 페이지 래치 대기 시간.[역주18]

차단 관련 정보 이외에도, 인덱스 액세스에 대한 추가 정보들이 제공됩니다.

ㆍ 액세스 유형, 예) 범위, 단일 조회.

ㆍ 리프 수준에서의 insert, update, delete.

ㆍ 리프 수준 상위 수준에서의 insert, update, delete. 리프 상위의 동작은 인덱스 유지 관리 작업니다. 상위 수준에는 각 리프 페이지의 첫 행이 입력됩니다. 리프에 새로운 페이지가 할당되면, 해당 페이지의 첫 행이 상위 페이지에 입력됩니다.

ㆍ 리프 수준에서의 페이지 병합은 행 삭제로 인해 할당이 해제된 빈 페이지를 나타냅니다.

ㆍ 인덱스 유지 관리. 리프 수준 상위에서의 페이지 병합은 리프에서 행 삭제로 인해 중간 수준 페이지가 빈 상태로 남겨지는, 할당 해제된 빈 페이지입니다. 리프 페이지의 첫 행은 상위 수준에 입력되어 있습니다. 리프 수준에서 충분한 수의 행이 삭제되면, 리프 페이지에 대한 첫 행을 입력으로 포함하고 있던 중간 수준의 인덱스 페이지들은 빈 페이지가 됩니다. 이것은 리프 상위 수준에서의 병합을 유발합니다.

이 정보는 SQL Server 인스턴스가 시작되면서부터 누적됩니다. 이 정보는 인스턴스가 재시작하면 사라지며, 또한 재설정하는 방법도 없습니다. 이 DMV에 의해서 반환되는 데이터는 해당 힙이나 인덱스를 나타내는 메타 데이터 캐시가 사용 가능한 경우에만 존재합니다. 힙이나 인덱스를 메타 데이터 캐시로 가져갈 때마다 각 열 값은 0으로 설정됩니다. 통계 정보는 메타 데이터 캐시에서 캐시 개체가 제거될 때까지 누적됩니다. 따라서, 이후 분석을 위해서는 주기적으로 데이터를 수집해서 저장해 두어야 합니다.

부록 B 인덱스 작업 데이터를 수집하는데 사용할 수 있는 저장 프로시저들을 제공합니다. 원하는 주기로 해당 데이터를 분석할 수 있습니다. 다음은 부록 B에 소개한 저장 프로시저를 사용하는 단계입니다.

1.
init_index_operational_stats 를 사용해서 indexstats를 초기화합니다.

2.
insert_indexstats를 사용해서 기준 상태를 캡처합니다.

3.
작업 부하를 발생시킵니다.

4.
insert_indexstats를 사용해서 인덱스 통계 정보에 대한 최종 정보를 캡처합니다.

5.
수집된 인덱스 통계 정보를 분석하기 위해서, 저장 프로시저 get_indexstats를 실행합니다. 평균 잠금 개수(인덱스와 분할에 대한 Row_lock_count), 차단, 그리고 인덱스 별 대기 정보를 생성합니다. 높은 값의 blocking % 혹은 높은 평균 대기는 인덱스 전략 문제나 쿼리 문제를 나타냅니다.

다음은 저장 프로시저를 사용해서 얻을 수 있는 정보들의 몇 가지 예제[역주19] 를 보여줍니다.

ㆍ 인덱스 사용량을 정렬 기준으로 모든 데이터베이스의 상위 5개 인덱스 출력.

exec get_indexstats
@dbid=-1,
@top='top 5',
@columns='index, usage',
@order='index usage'


ㆍ 잠금 승격을 시도한 적이 있는 상위 5개 인덱스 잠금 승격 출력.

exec get_indexstats
@dbid=-1,
@top='top 5',
@order='index lock promotions',
@threshold='[index lock promotion attempts] > 0'


ㆍ 평균 잠금 대기 시간이 2ms를 초과하는 상위 5개 단일 조회 작업에 대해 wait, scan, singleton열을 포함해서 출력.

exec get_indexstats
@dbid=5,
@top='top 5',
@columns='wait,scan,singleton',
@order='singleton lookups',
@threshold='[avg row lock wait ms] > 2'


ㆍ 모든 데이터베이스에서 행 잠금 대기가 1을 초과하는 상위 10개에 대해 avg, wait 열을 포함해서 대기 시간을 기준으로 출력.

exec get_indexstats
@dbid=-1,
@top='top 10 ',
@columns='wait,row',
@order='row lock wait ms',
@threshold='[row lock waits] > 1'


ㆍ 평균 행 잠금 대기 시간을 정렬 기준으로 상위 5개 행 출력.

exec get_indexstats
@dbid=-1,
@top='top 5',
@order='avg row lock wait ms'


ㆍ 평균 페이지 래치 대기 시간을 정렬 기준으로 상위 5개 행 출력.

exec get_indexstats
@dbid=-1,
@top='top 5',
@order='avg page latch wait ms'


ㆍ 평균 페이지 I/O 래치 시간을 정렬 기준으로 상위 3%의 인덱스 통계 출력.

exec get_indexstats
@dbid=-1,
@top='top 3 percent',
@order='avg pageio latch wait ms',
@threshold='[pageio latch waits] > 0'


ㆍ db=5에서 block%를 기준으로 0.1을 초과하는 상위 10위 정보 출력.

exec get_indexstats
@dbid=-1,
@top='top 10',
@order='block %',
@threshold='[block %] > 0.1'

그림 4는 차단 분석 보고 예제를 보여줍니다.

그림 4: 차단 분석 보고


SQL Server 2000에서는 개체나 인덱스 사용량에 대한 통계 정보를 제공하지 않습니다.

차단으로 인한 전반적인 성능 평가를 위해 SQL 대기 사용
SQL Server 2000은 76개의 대기 유형을 보고합니다. SQL Server 2005는 응용 프로그램 성능 추적을 위해서100개 이상의 추가 대기 유형을 제공합니다. 사용자 연결이 대기 중인 동안, SQL Server는 대기 시간을 누적합니다. 예를 들어, 응용 프로그램이 I/O, 잠금, 혹은 메모리 같은 리소스를 요청하면 해당 리소스를 사용 가능할 때까지 기다릴 수 있습니다. 작업 부하가 발생했을 때 해당하는 성능 프로필을 얻을 수 있도록 이러한 대기 정보들이 모든 연결에 걸쳐 요약되고 분류됩니다. 따라서, SQL 대기 유형은 응용 프로그램 작업 부하로부터 발생하는 사용자(혹은 쓰레드) 대기 정보를 식별하고 분류합니다.

다음 쿼리는 SQL Server에 상위 10위 대기 정보을 보여줍니다. 이 대기 정보는 누적되는 정보이지만DBCC SQLPERF ([sys.dm_os_wait_stats], clear)를 사용해서 재설정할 수가 있습니다.

select top 10 *
from sys.dm_os_wait_stats
order by wait_time_ms desc

다음은 출력 결과입니다. 주목할 만한 몇 가지 핵심 요점은:

ㆍ 지연 기록기(lazy writer)와 같이 백그라운드 쓰레드로 인한 몇 가지 대기는 정상적입니다.

ㆍ 몇 가지 세션은 SH 잠금을 얻기 위해 장 시간 대기했습니다.

ㆍ signal wait은 작업자(worker)가 리소스에 액세스를 허가 받았을 때부터 CPU 예약을 획득한 시점까지의 대기 시간입니다. 오랜 signal wait은 높은 CPU 경합을 암시합니다.

wait_type waiting_tasks_count wait_time_ms max_wait_time_ms
signal_wait_time_ms
------------------ -------------------- -------------------- -------------
------- -------
LAZYWRITER_SLEEP 415088 415048437 1812
156
SQLTRACE_BUFFER_FLUSH 103762 415044000 4000
0
LCK_M_S 6 25016812 23240921
0
WRITELOG 7413 86843 187
406
LOGMGR_RESERVE_APPEND 82 82000 1000
0
SLEEP_BPOOL_FLUSH 4948 28687 31
15
LCK_M_X 1 20000 20000
0
PAGEIOLATCH_SH 871 11718 140
15
PAGEIOLATCH_UP 755 9484 187
0
IO_COMPLETION 636 7031 203
0

대기 정보를 분석하기 위해서, 정기적으로 데이터를 수집해야 합니다. 부록 B에서 두 가지 저장 프로시저를 제공합니다.

ㆍ Track_waitstats. 원하는 샘플 숫자와 샘플 간격으로 데이터를 수집할 수 있습니다. 다음은 호출 예제입니다.

exec dbo.track_waitstats @num_samples=6
,@delay_interval=30
,@delay_type='s'
,@truncate_history='y'
,@clear_waitstats='y'


ㆍ Get_waitstats. 이전 단계에서 수집된 데이터를 분석합니다. 다음은 호출 예제입니다.

exec [dbo].[get_waitstats_2005]

ㆍ spid가 실행 중입니다. 그 다음 현재 사용할 수 없는 리소스를 필요로 합니다. 리소스를 사용할 수 없으므로, 시간 T0에 리소스 대기 목록으로 이동합니다.

ㆍ Signal은 리소스가 사용 가능함을 나타냅니다, 따라서 spid는 시간 T1 에 실행 가능 큐로 이동합니다.

ㆍ spid는 cpu 가 실행 가능 큐에 도착한 순서대로 작업을 하고 T2에 실행 상태가 될 때까지 기다립니다.

이 저장 프로시저를 사용해서 리소스 대기와 신호(signal) 대기를 분석하거나 자원 경합을 격리시킬 수 있습니다.

그림5 보고서 예제

그림 5: 대기에 대한 통계 정보 분석 리포트


그림 5의 대기 분석 리포트 예제는 성능 문제가 차단(LCK_M_S)과 메모리 할당(RESOURCE_SEMAPHORE)에 있음을 알려줍니다. 특히 전체 대기 중 메모리 요구로 인한 문제가 43%인 반면, 55%가 공유 잠금입니다. 개체 별 차단 분석은 경합의 핵심 요점을 식별합니다.


인덱스 사용량 모니터링


쿼리 성능의 또 다른 관점은 DML 쿼리, 데이터를 삭제, 추가, 변경하는 쿼리와 관련됩니다. 테이블에 더 많은 인덱스를 정의하면 데이터를 수정하는데 더 많은 리소스를 필요로 합니다. 트랜잭션에서 점유하는 잠금과 결합해서, 오랜 시간 동안의 변경 작업은 동시성을 방해하게 됩니다. 따라서, 시간이 지난 뒤에 응용 프로그램에 의해서 사용되는 인덱스가 무엇인지 알아내는 것이 매우 중요합니다. 그리고 나서, 사용되지 않는 인덱스로 인해 데이터베이스 스키마가 불필요하게 크지 않은지를 알아내야 합니다.

SQL Server 2005는 sys.dm_db_index_usage_stats 동적 관리 뷰를 새로 제공합니다. 이 뷰는 어떤 인덱스가 사용되고 있는지, 해당 인덱스가 사용자 쿼리에 의해서 사용되는지 시스템 작업에 의해서만 사용되는지를 알려줍니다. 쿼리를 실행할 때마다, 쿼리에서 사용한 쿼리 계획에 따라서 이 뷰내의 열 값이 증가됩니다. 데이터는 SQL Server 실행 동안 수집됩니다. 이 DMV의 데이터는 메모리에서만 유지되며 영구히 보존되지는 않습니다. 따라서 SQL Server 인스턴스가 중지되면, 데이터는 손실됩니다. 정기적으로 데이터를 수집하고 저장해서 이후 분석을 위해 사용합니다.

인덱스에 대한 작업은 사용자 유형과 시스템 유형으로 분류됩니다. 사용자 유형은 SELECT와 INSERT/UPDATE/DELETE 작업입니다. 시스템 유형은 DBCC나 DDL 명령 혹은 update statistics와 같은 명령입니다. 각 문장의 범주별로 열이 구별됩니다:

ㆍ 인덱스에 대한 검색 (user_seeks나 system_seeks)

ㆍ 인덱스에 대한 조회 작업 (user_lookups 나 system_lookups)

ㆍ 인덱스에 대한 스캔 작업 (user_scans 나 system_scans)

ㆍ 인덱스에 대한 변경 작업 (user_updates 나 system_updates)

인덱스 액세스 유형별로, 마지막 액세스 시간 또한 알 수 있습니다.

인덱스 자체는 세 가지 칼럼, database_id, object_id, index_id를 통해서 알 수 있습니다. index_id = 0은 힙 테이블을 나타내며, index_id = 1은 클러스터형 인덱스를, index_id > 1인 경우는 비클러스터형 인덱스를 나타냅니다.

SQL Server 2005에서 검색, 스캔, 그리고 조회 동작에 대한 규칙과 정의는 다음과 같습니다.

ㆍ 검색: 데이터 액세스를 위해서 B-트리 구조가 사용된 카운트를 나타냅니다. 한 행을 찾기 위해서 인덱스 각 수준에서 적은 페이지를 읽거나 혹은 기가바이트 데이터나 수 백만 행을 읽기위해 인덱스 페이지 반을 읽었더라도 문제가 되지 않습니다. 따라서 인덱스를 사용한 것으로 해당 범주에 누적됩니다.

ㆍ 스캔: B-트리 인덱스 중 하나를 사용하지 않고 테이블의 데이터 계층을 사용해서 스캔을 한 카운트를 나타냅니다. 테이블에 인덱스가 없는 경우가 이에 해당합니다. 인덱스가 존재하지만 쿼리에 의해서 사용되지 않은 경우에도 해당합니다.

ㆍ 조회: 비클러스터형 인덱스가 사용된 경우를 ‘검색”으로 정의한다면, 추가로 클러스터형 인덱스(혹은 힙)를 사용해서 데이터를 조회한 경우를 나타냅니다. 이는 SQL Server 2000에서 책갈피 조회(bookmark lookup) 시나리오를 나타냅니다. 이는 테이블 액세스에 사용된 비클러스터형 인덱스가 쿼리 select 목록에 열과 where 절에 정의된 열을 모두 포함하지 못하는 시나리오를 나타냅니다. SQL Server는 비클러스터형 인덱스에 대해 user_seeks 열 값을 증가시키고 클러스터형 인덱스 항목의 user_lookups 열에 더합니다. 이 카운트는 테이블에 비클러스형 인덱스가 많은 경우 매우 높은 값을 가질 수 있습니다. 클러스터형 인덱스에 user_seeks 값이 매우 높고, 더불어 user_lookups 값도 높으며 특정 비클러스터형 인덱스의 user_seeks 값 또한 매우 높다면, 높은 값을 가진 비클러스터형 인덱스를 클러스터형 인덱스로 바꾸어 만들면 쿼리 성능을 더 향상시킬 수도 있습니다.

다음 DMV 쿼리로 모든 데이터베이스의 모든 개체에 대해 인덱스 사용량에 대한 유용한 정보를 알 수 있습니다.

select object_id, index_id, user_seeks, user_scans, user_lookups
from sys.dm_db_index_usage_stats
order by object_id, index_id

다음 결과는 특정 테이블 정보를 보여줍니다.

object_id index_id user_seeks user_scans user_lookups
------------ ------------- -------------- -------------- -----------------
521690298 1 0 251 123
521690298 2 123 0 0

이 경우, 인덱스를 사용하지 않고 테이블의 데이터 계층을 직접 액세스한 쿼리가 251번 실행되었습니다. 첫 번째 비클러스터형 인덱스를 사용해서 테이블을 액세스한 쿼리가 123번 실행되었습니다. 이는 쿼리의 select 열이나 WHERE 절에 지정된 열이 비클러스터형 인덱스에 모두 존재하지 않으므로 클러스터형 인덱스를 123번 조회한 것입니다.

가장 관심있게 살펴볼 범주는 ‘사용자 유형 문장’ 입니다. ‘시스템 범주’내에 나타나는 사용량은 기존 인덱스에 결과로 보여질 수 있습니다. 인덱스가 존재하지 않으면, 통계 정보를 변경하거나 일관성을 검사할 필요가 없기 때문입니다. 따라서, 임시(ad hoc) 문장이나 사용자 응용 프로그램에 의한 사용량을 나타내는 4개의 칼럼에 집중해서 분석합니다.

SQL Server가 시작된 이후에 특정 테이블에서 사용되지 않은 인덱스에 대한 정보를 얻기 위해서, 해당 테이블이 존재하는 데이터베이스에서 다음 쿼리를 실행합니다.

select i.name
from sys.indexes i
where i.object_id=object_id('<table_name>') and
i.index_id NOT IN (select s.index_id
from sys.dm_db_index_usage_stats s
where s.object_id=i.object_id and
i.index_id=s.index_id and
database_id = <dbid>[역주20] )

사용되지 않은 모든 인덱스 정보는 다음 쿼리로 알 수 있습니다:

select object_name(object_id), i.name
from sys.indexes i
where i.index_id NOT IN (select s.index_id
from sys.dm_db_index_usage_stats s
where s.object_id=i.object_id and
i.index_id=s.index_id and
database_id = <dbid> )
order by object_name(object_id) asc

이 경우, 테이블 이름과 인덱스 이름은 테이블 이름에 따라서 정렬됩니다.

실제로 이 동적 관리 뷰의 목적은 오랫 동안 인덱스의 사용량을 관찰하는 것입니다. 뷰나 쿼리를 수행한 결과를 매일 혹은 특정 주기로 저장하고 시간이 지나면서 각각 비교를 하는 것입니다. 분기별 보고나 회계연도 보고와 같이 한 달 혹은 특정 기간 동안 사용되지 않는 인덱스를 알아낸다면, 데이터베이스에서 해당 인덱스를 삭제할 수 있습니다.

728x90

차단 스크립트(Blocking Scripts)


부록은 본 문서에서 참조한 저장 프로시저의 소스 목록입니다. 필요에 따라 수정해서 사용하십시오.

sp_block

create proc dbo.sp_block (@spid bigint=NULL)
as
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- This proc reports blocks
-- 1. optional parameter @spid
--

select
t1.resource_type,
'database'=db_name(resource_database_id),
'blk object' = t1.resource_associated_entity_id,
t1.request_mode,
t1.request_session_id,
t2.blocking_session_id
from
sys.dm_tran_locks as t1,
sys.dm_os_waiting_tasks as t2
where
t1.lock_owner_address = t2.resource_address and
t1.request_session_id = isnull(@spid,t1.request_session_id)

인덱스 사용 정보 분석

아래 저장 프로시저들은 인덱스 사용량 분석에 사용할 수 있습니다.

get_indexstats

create proc dbo.get_indexstats
(@dbid smallint=-1
,@top varchar(100)=NULL
,@columns varchar(500)=NULL
,@order varchar(100)='lock waits'
,@threshold varchar(500)=NULL)
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- This proc analyzes index statistics including accesses, overhead,
-- locks, blocks, and waits
--
-- Instructions: Order of execution is as follows:
-- (1) truncate indexstats with init_indexstats
-- (2) take initial index snapshot using insert_indexstats
-- (3) Run workload
-- (4) take final index snapshot using insert_indexstats
-- (5) analyze with get_indexstats

-- @dbid limits analysis to a database
-- @top allows you to specify TOP n
-- @columns is used to specify what columns from
-- sys.dm_db_index_operational_stats will be included in
-- the report
-- For example, @columns='scans,lookups,waits' will include
-- columns
-- containing these keywords
-- @order used to order results
-- @threshold used to add a threshold,
-- example: @threshold='[block %] > 5' only include if
-- blocking is over 5%
--
------ definition of some computed columns returned
-- [blk %] = percentage of locks that cause blocks e.g. blk% = 100 * lock
-- waits / locks
-- [index usage] = range_scan_count + singleton_lookup_count +
-- leaf_insert_count
-- [nonleaf index overhead]=nonleaf_insert_count + nonleaf_delete_count +
-- nonleaf_update_count
-- [avg row lock wait ms]=row_lock_wait_in_ms/row_lock_wait_count
-- [avg page lock wait ms]=page_lock_wait_in_ms/page_lock_wait_count
-- [avg page latch wait ms]=page_latch_wait_in_ms/page_latch_wait_count
-- [avg pageio latch wait
-- ms]=page_io_latch_wait_in_ms/page_io_latch_wait_count
--------------------------------------------------------------------------
-------------------------
--- Case 1 - only one snapshot of sys.dm_db_operational_index_stats was
-- stored in
--- indexstats. This is an error - return errormsg to user
--- Case 2 - beginning snapshot taken, however some objects were not
-- referenced
--- at the time of the beginning snapshot. Thus, they will
-- not be in the initial
--- snapshot of sys.dm_db_operational_index_stats, use 0 for
-- starting values.
--- Print INFO msg for informational purposes.
--- Case 3 - beginning and ending snapshots, beginning values for all
-- objects and indexes
--- this should be the normal case, especially if SQL Server
-- is up a long time
--------------------------------------------------------------------------
-------------------------
set nocount on
declare @orderby varchar(100), @where_dbid_is varchar(100), @temp
varchar(500), @threshold_temptab varchar(500)
declare @cmd varchar(max),@col_stmt varchar(500),@addcol varchar(500)
declare @begintime datetime, @endtime datetime, @duration datetime,
@mincount int, @maxcount int

select @begintime = min(now), @endtime = max(now) from indexstats

if @begintime = @endtime
begin
print 'Error: indexstats contains only 1 snapshot of
sys.dm_db_index_operational_stats'
print 'Order of execution is as follows: '
print ' (1) truncate indexstats with init_indexstats'
print ' (2) take initial index snapshot using insert_indexstats'
print ' (3) Run workload'
print ' (4) take final index snapshot using insert_indexstats'
print ' (5) analyze with get_indexstats'
return -99
end

select @mincount = count(*) from indexstats where now = @begintime
select @maxcount = count(*) from indexstats where now = @endtime

if @mincount < @maxcount
begin
print 'InfoMsg1: sys.dm_db_index_operational_stats only contains
entries for objects referenced since last SQL re-cycle'
print 'InfoMsg2: Any newly referenced objects and indexes captured
in the ending snapshot will use 0 as a beginning value'
end

select @top = case
when @top is NULL then ''
else lower(@top)
end,
@where_dbid_is = case (@dbid)
when -1 then ''
else ' and i1.database_id = ' + cast(@dbid as varchar(10))
end,
--- thresholding requires a temp table
@threshold_temptab = case
when @threshold is NULL then ''
else ' select * from #t where ' + @threshold
end
--- thresholding requires temp table, add 'into #t' to select statement
select @temp = case (@threshold_temptab)
when '' then ''
else ' into #t '
end
select @orderby=case(@order)
when 'leaf inserts' then 'order by [' + @order + ']'
when 'leaf deletes' then 'order by [' + @order + ']'
when 'leaf updates' then 'order by [' + @order + ']'
when 'nonleaf inserts' then 'order by [' + @order + ']'
when 'nonleaf deletes' then 'order by [' + @order + ']'
when 'nonleaf updates' then 'order by [' + @order + ']'
when 'nonleaf index overhead' then 'order by [' + @order + ']'
when 'leaf allocations' then 'order by [' + @order + ']'
when 'nonleaf allocations' then 'order by [' + @order + ']'
when 'allocations' then 'order by [' + @order + ']'
when 'leaf page merges' then 'order by [' + @order + ']'
when 'nonleaf page merges' then 'order by [' + @order + ']'
when 'range scans' then 'order by [' + @order + ']'
when 'singleton lookups' then 'order by [' + @order + ']'
when 'index usage' then 'order by [' + @order + ']'
when 'row locks' then 'order by [' + @order + ']'
when 'row lock waits' then 'order by [' + @order + ']'
when 'block %' then 'order by [' + @order + ']'
when 'row lock wait ms' then 'order by [' + @order + ']'
when 'avg row lock wait ms' then 'order by [' + @order + ']'
when 'page locks' then 'order by [' + @order + ']'
when 'page lock waits' then 'order by [' + @order + ']'
when 'page lock wait ms' then 'order by [' + @order + ']'
when 'avg page lock wait ms' then 'order by [' + @order + ']'
when 'index lock promotion attempts' then 'order by [' + @order + ']'
when 'index lock promotions' then 'order by [' + @order + ']'
when 'page latch waits' then 'order by [' + @order + ']'
when 'page latch wait ms' then 'order by [' + @order + ']'
when 'pageio latch waits' then 'order by [' + @order + ']'
when 'pageio latch wait ms' then 'order by [' + @order + ']'
else ''
end

if @orderby <> '' select @orderby = @orderby + ' desc'
select
'start time'=@begintime,
'end time'=@endtime,
'duration (hh:mm:ss:ms)'=convert(varchar(50),
@endtime-@begintime,14),
'Report'=case (@dbid)
when -1 then 'all databases'
else db_name(@dbid)
end +
case
when @top = '' then ''
when @top is NULL then ''
when @top = 'none' then ''
else ', ' + @top
end +
case
when @columns = '' then ''
when @columns is NULL then ''
when @columns = 'none' then ''
else ', include only columns containing ' + @columns
end +
case(@orderby)
when '' then ''
when NULL then ''
when 'none' then ''
else ', ' + @orderby
end +
case
when @threshold = '' then ''
when @threshold is NULL then ''
when @threshold = 'none' then ''
else ', threshold on ' + @threshold
end

select @cmd = ' select i2.database_id, i2.object_id, i2.index_id,
i2.partition_number '
select @cmd = @cmd +' , begintime=case min(i1.now) when max(i2.now) then
NULL else min(i1.now) end '
select @cmd = @cmd +' , endtime=max(i2.now) '
select @cmd = @cmd +' into #i '
select @cmd = @cmd +' from indexstats i2 '
select @cmd = @cmd +' full outer join '
select @cmd = @cmd +' indexstats i1 '
select @cmd = @cmd +' on i1.database_id = i2.database_id '
select @cmd = @cmd +' and i1.object_id = i2.object_id '
select @cmd = @cmd +' and i1.index_id = i2.index_id '
select @cmd = @cmd +' and i1.partition_number = i2.partition_number '
select @cmd = @cmd +' where i1.now >= ''' +
convert(varchar(100),@begintime, 109) + ''''
select @cmd = @cmd +' and i2.now = ''' + convert(varchar(100),@endtime,
109) + ''''
select @cmd = @cmd + ' ' + @where_dbid_is + ' '
select @cmd = @cmd + ' group by i2.database_id, i2.object_id, i2.index_id,
i2.partition_number '
select @cmd = @cmd + ' select ' + @top + ' i.database_id,
db_name=db_name(i.database_id),
object=isnull(object_name(i.object_id),i.object_id), indid=i.index_id,
part_no=i.partition_number '

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf inserts]=i2.leaf_insert_count -
isnull(i1.leaf_insert_count,0)'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,@col_stmt=' ,
[leaf deletes]=i2.leaf_delete_count ?
isnull(i1.leaf_delete_count,0)'

select @cmd = @cmd + @addcol

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf updates]=i2.leaf_update_count ?
isnull(i1.leaf_update_count,0)'

select @cmd = @cmd + @addcol

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf inserts]=i2.nonleaf_insert_count ?
isnull(i1.nonleaf_insert_count,0)'

select @cmd = @cmd + @addcol

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf deletes]=i2.nonleaf_delete_count ?
isnull(i1.nonleaf_delete_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf updates]=i2.nonleaf_update_count ?
isnull(i1.nonleaf_update_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf index overhead]=(i2.nonleaf_insert_count ?
isnull(i1.nonleaf_insert_count,0)) + (i2.nonleaf_delete_count ?
isnull(i1.nonleaf_delete_count,0)) + (i2.nonleaf_update_count ?
isnull(i1.nonleaf_update_count,0))'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf allocations]=i2.leaf_allocation_count ?
isnull(i1.leaf_allocation_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf allocations]=i2.nonleaf_allocation_count ?
isnull(i1.nonleaf_allocation_count,0)'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[allocations]=(i2.leaf_allocation_count ?
isnull(i1.leaf_allocation_count,0)) + (i2.nonleaf_allocation_count ?
isnull(i1.nonleaf_allocation_count,0))'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf page merges]=i2.leaf_page_merge_count ?
isnull(i1.leaf_page_merge_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf page merges]=i2.nonleaf_page_merge_count ?
isnull(i1.nonleaf_page_merge_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[range scans]=i2.range_scan_count ?
isnull(i1.range_scan_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing= @columns,
@col_stmt=' ,[singleton lookups]=i2.singleton_lookup_count ?
isnull(i1.singleton_lookup_count,0)'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[index usage]=(i2.range_scan_count ?
isnull(i1.range_scan_count,0)) + (i2.singleton_lookup_count ?
isnull(i1.singleton_lookup_count,0)) + (i2.leaf_insert_count ?
isnull(i1.leaf_insert_count,0))'
select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[row locks]=i2.row_lock_count ?
isnull(i1.row_lock_count,0)'
select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[row lock waits]=i2.row_lock_wait_count ?
isnull(i1.row_lock_wait_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[block %]=cast (100.0 * (i2.row_lock_wait_count ?
isnull(i1.row_lock_wait_count,0)) / (1 + i2.row_lock_count ?
isnull(i1.row_lock_count,0)) as numeric(5,2))'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[row lock wait ms]=i2.row_lock_wait_in_ms ?
isnull(i1.row_lock_wait_in_ms,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg row lock wait ms]=cast ((1.0*(i2.row_lock_wait_in_ms
- isnull(i1.row_lock_wait_in_ms,0)))/(1 + i2.row_lock_wait_count -
isnull(i1.row_lock_wait_count,0)) as numeric(20,1))'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page locks]=i2.page_lock_count ?
isnull(i1.page_lock_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page lock waits]=i2.page_lock_wait_count ?
isnull(i1.page_lock_wait_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page lock wait ms]=i2.page_lock_wait_in_ms ?
isnull(i1.page_lock_wait_in_ms,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg page lock wait ms]=cast
((1.0*(i2.page_lock_wait_in_ms - isnull(i1.page_lock_wait_in_ms,0)))/(1 +
i2.page_lock_wait_count - isnull(i1.page_lock_wait_count,0)) as
numeric(20,1))'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[index lock promotion
attempts]=i2.index_lock_promotion_attempt_count ?
isnull(i1.index_lock_promotion_attempt_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[index lock promotions]=i2.index_lock_promotion_count ?
isnull(i1.index_lock_promotion_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page latch waits]=i2.page_latch_wait_count ?
isnull(i1.page_latch_wait_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page latch wait ms]=i2.page_latch_wait_in_ms ?
isnull(i1.page_latch_wait_in_ms,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg page latch wait ms]=cast
((1.0*(i2.page_latch_wait_in_ms - isnull(i1.page_latch_wait_in_ms,0)))/(1
+ i2.page_latch_wait_count - isnull(i1.page_latch_wait_count,0)) as
numeric(20,1))'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[pageio latch waits]=i2.page_io_latch_wait_count ?
isnull(i1.page_latch_wait_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[pageio latch wait ms]=i2.page_io_latch_wait_in_ms ?
isnull(i1.page_latch_wait_in_ms,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg pageio latch wait ms]=cast
((1.0*(i2.page_io_latch_wait_in_ms ?
isnull(i1.page_io_latch_wait_in_ms,0)))/(1 + i2.page_io_latch_wait_count ?
isnull(i1.page_io_latch_wait_count,0)) as numeric(20,1))'

select @cmd = @cmd +@addcol
select @cmd = @cmd + @temp
select @cmd = @cmd + ' from #i i '
select @cmd = @cmd + ' left join indexstats i1 on i.begintime = i1.now and
i.database_id = i1.database_id and i.object_id = i1.object_id and
i.index_id = i1.index_id and i.partition_number = i1.partition_number '

select @cmd = @cmd + ' left join indexstats i2 on i.endtime = i2.now and
i.database_id = i2.database_id and i.object_id = i2.object_id and
i.index_id = i2.index_id and i.partition_number = i2.partition_number '
select @cmd = @cmd + ' ' + @orderby + ' '
select @cmd = @cmd + @threshold_temptab
exec ( @cmd )
go

insert_indexstats

create proc insert_indexstats (@dbid smallint=NULL,
@objid int=NULL,
@indid int=NULL,
@partitionid int=NULL)
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
confers no rights.
-- Use of included script samples are subject to the terms specified at
http://www.microsoft.com/info/cpyright.htm
-- This stored procedure stores a snapshot of
sys.dm_db_index_operational_stats into the table indexstas
-- for later analysis by the stored procedure get_indexstats. Please note
that the indexstats table has an additional
-- column to store the timestamp when the snapshot is taken
--
-- T. Davidson
-- snapshot sys.dm_db_index_operational_stats
--
declare @now datetime
select @now = getdate()
insert into indexstats
(database_id
,object_id
,index_id
,partition_number
,leaf_insert_count
,leaf_delete_count
,leaf_update_count
,leaf_ghost_count
,nonleaf_insert_count
,nonleaf_delete_count
,nonleaf_update_count
,leaf_allocation_count
,nonleaf_allocation_count
,leaf_page_merge_count
,nonleaf_page_merge_count
,range_scan_count
,singleton_lookup_count
,forwarded_fetch_count
,lob_fetch_in_pages
,lob_fetch_in_bytes
,lob_orphan_create_count
,lob_orphan_insert_count
,row_overflow_fetch_in_pages
,row_overflow_fetch_in_bytes
,column_value_push_off_row_count
,column_value_pull_in_row_count
,row_lock_count
,row_lock_wait_count
,row_lock_wait_in_ms
,page_lock_count
,page_lock_wait_count
,page_lock_wait_in_ms
,index_lock_promotion_attempt_count
,index_lock_promotion_count
,page_latch_wait_count
,page_latch_wait_in_ms
,page_io_latch_wait_count
,page_io_latch_wait_in_ms,
now)
select database_id
,object_id
,index_id
,partition_number
,leaf_insert_count
,leaf_delete_count
,leaf_update_count
,leaf_ghost_count
,nonleaf_insert_count
,nonleaf_delete_count
,nonleaf_update_count
,leaf_allocation_count
,nonleaf_allocation_count
,leaf_page_merge_count
,nonleaf_page_merge_count
,range_scan_count
,singleton_lookup_count
,forwarded_fetch_count
,lob_fetch_in_pages
,lob_fetch_in_bytes
,lob_orphan_create_count
,lob_orphan_insert_count
,row_overflow_fetch_in_pages
,row_overflow_fetch_in_bytes
,column_value_push_off_row_count
,column_value_pull_in_row_count
,row_lock_count
,row_lock_wait_count
,row_lock_wait_in_ms
,page_lock_count
,page_lock_wait_count
,page_lock_wait_in_ms
,index_lock_promotion_attempt_count
,index_lock_promotion_count
,page_latch_wait_count
,page_latch_wait_in_ms
,page_io_latch_wait_count
,page_io_latch_wait_in_ms
,@now
from sys.dm_db_index_operational_stats(@dbid,@objid,@indid,@partitionid)
go


init_index_operational_stats

CREATE proc dbo.init_index_operational_stats
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
--
-- create indexstats table if it doesn't exist, otherwise truncate
--
set nocount on
if not exists (select 1 from dbo.sysobjects where
id=object_id(N'[dbo].[indexstats]') and OBJECTPROPERTY(id, N'IsUserTable')
= 1)
create table dbo.indexstats (
database_id smallint NOT NULL
,object_id int NOT NULL
,index_id int NOT NULL
,partition_number int NOT NULL
,leaf_insert_count bigint NOT NULL
,leaf_delete_count bigint NOT NULL
,leaf_update_count bigint NOT NULL
,leaf_ghost_count bigint NOT NULL
,nonleaf_insert_count bigint NOT NULL
,nonleaf_delete_count bigint NOT NULL
,nonleaf_update_count bigint NOT NULL
,leaf_allocation_count bigint NOT NULL
,nonleaf_allocation_count bigint NOT NULL
,leaf_page_merge_count bigint NOT NULL
,nonleaf_page_merge_count bigint NOT NULL
,range_scan_count bigint NOT NULL
,singleton_lookup_count bigint NOT NULL
,forwarded_fetch_count bigint NOT NULL
,lob_fetch_in_pages bigint NOT NULL
,lob_fetch_in_bytes bigint NOT NULL
,lob_orphan_create_count bigint NOT NULL
,lob_orphan_insert_count bigint NOT NULL
,row_overflow_fetch_in_pages bigint NOT NULL
,row_overflow_fetch_in_bytes bigint NOT NULL
,column_value_push_off_row_count bigint NOT NULL
,column_value_pull_in_row_count bigint NOT NULL
,row_lock_count bigint NOT NULL
,row_lock_wait_count bigint NOT NULL
,row_lock_wait_in_ms bigint NOT NULL
,page_lock_count bigint NOT NULL
,page_lock_wait_count bigint NOT NULL
,page_lock_wait_in_ms bigint NOT NULL
,index_lock_promotion_attempt_count bigint NOT NULL
,index_lock_promotion_count bigint NOT NULL
,page_latch_wait_count bigint NOT NULL
,page_latch_wait_in_ms bigint NOT NULL
,page_io_latch_wait_count bigint NOT NULL
,page_io_latch_wait_in_ms bigint NOT NULL
,now datetime default getdate())
else truncate table dbo.indexstats
go


add_column

create proc dbo.add_column (
@add_stmt varchar(500) output,
@find varchar(100)=NULL,
@cols_containing varchar(500)=NULL,
@col_stmt varchar(max))
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- @add_stmt is the result passed back to the caller
-- @find is a keyword from @cols_containing
-- @cols_containing is the list of keywords to include in the report
-- @col_stmt is the statement that will be compared with @find.
-- If @col_stmt contains @find, include this statement.
-- set @add_stmt = @col_stmt
--
declare @length int, @strindex int, @EOS bit
if @cols_containing is NULL
begin
select @add_stmt=@col_stmt
return
end
select @add_stmt = '', @EOS = 0

while @add_stmt is not null and @EOS = 0
@dbid=-1,
select @strindex = charindex(',',@cols_containing)
if @strindex = 0
select @find = @cols_containing, @EOS = 1
else
begin
select @find = substring(@cols_containing,1,@strindex-1)
select @cols_containing =
substring(@cols_containing,
@strindex+1,
datalength(@cols_containing) - @strindex)
end
select @add_stmt=case
--when @cols_containing is NULL then NULL
when charindex(@find,@col_stmt) > 0 then NULL
else ''
end
end
--- NULL indicates this statement is to be passed back through out parm
@add_stmt
if @add_stmt is NULL select @add_stmt=@col_stmt
go


Wait states
이 저장 프로시저들은 차단 분석에 사용할 수 있습니다.

track_waitstats_2005

CREATE proc [dbo].[track_waitstats_2005] (
@num_samples int=10,
@delay_interval int=1,
@delay_type nvarchar(10)='minutes',
@truncate_history nvarchar(1)='N',
@clear_waitstats nvarchar(1)='Y')
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- @num_samples is the number of times to capture waitstats, default is 10
-- times
-- default delay interval is 1 minute
-- delaynum is the delay interval - can be minutes or seconds
-- delaytype specifies whether the delay interval is minutes or seconds
-- create waitstats table if it doesn't exist, otherwise truncate
-- Revision: 4/19/05
--- (1) added object owner qualifier
--- (2) optional parameters to truncate history and clear waitstats
set nocount on
if not exists (select 1
from sys.objects
where object_id = object_id ( N'[dbo].[waitstats]') and
OBJECTPROPERTY(object_id, N'IsUserTable') = 1)
create table [dbo].[waitstats]
([wait_type] nvarchar(60) not null,
[waiting_tasks_count] bigint not null,
[wait_time_ms] bigint not null,
[max_wait_time_ms] bigint not null,
[signal_wait_time_ms] bigint not null,
now datetime not null default getdate())

If lower(@truncate_history) not in (N'y',N'n')
begin
raiserror ('valid @truncate_history values are ''y'' or
''n''',16,1) with nowait
end
If lower(@clear_waitstats) not in (N'y',N'n')
begin
raiserror ('valid @clear_waitstats values are ''y'' or
''n''',16,1) with nowait
end
If lower(@truncate_history) = N'y'
truncate table dbo.waitstats

If lower (@clear_waitstats) = N'y'
-- clear out waitstats
dbcc sqlperf ([sys.dm_os_wait_stats],clear) with no_infomsgs

declare @i int,
@delay varchar(8),
@dt varchar(3),
@now datetime,
@totalwait numeric(20,1),
@endtime datetime,
@begintime datetime,
@hr int,
@min int,
@sec int

select @i = 1
select @dt = case lower(@delay_type)
when N'minutes' then 'm'
when N'minute' then 'm'
when N'min' then 'm'
when N'mi' then 'm'
when N'n' then 'm'
when N'm' then 'm'
when N'seconds' then 's'
when N'second' then 's'
when N'sec' then 's'
when N'ss' then 's'
when N's' then 's'
else @delay_type
end

if @dt not in ('s','m')
begin
raiserror ('delay type must be either ''seconds'' or
''minutes''',16,1) with nowait
return
end
if @dt = 's'
begin
select @sec = @delay_interval % 60, @min = cast((@delay_interval / 60)
as int), @hr = cast((@min / 60) as int)
end
if @dt = 'm'
begin
select @sec = 0, @min = @delay_interval % 60, @hr =
cast((@delay_interval / 60) as int)
end
select @delay= right('0'+ convert(varchar(2),@hr),2) + ':' +
+ right('0'+convert(varchar(2),@min),2) + ':' +
+ right('0'+convert(varchar(2),@sec),2)

if @hr > 23 or @min > 59 or @sec > 59
begin
select 'delay interval and type: ' + convert
(varchar(10),@delay_interval) + ',' + @delay_type + ' converts to ' +
@delay
raiserror ('hh:mm:ss delay time cannot > 23:59:59',16,1) with nowait
return
end
while (@i <= @num_samples)
begin
select @now = getdate()
insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
@now
from sys.dm_os_wait_stats

insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
'Total',
sum([waiting_tasks_count]),
sum([wait_time_ms]),
0,
sum([signal_wait_time_ms]),
@now
from [dbo].[waitstats]
where now = @now

select @i = @i + 1
waitfor delay @delay
end
--- create waitstats report
execute dbo.get_waitstats_2005
go
exec dbo.track_waitstats @num_samples=6
,@delay_interval=30
,@delay_type='s'
,@truncate_history='y'
,@clear_waitstats='y'


get_waitstats_2005

CREATE proc [dbo].[track_waitstats_2005] (
@num_samples int=10,
@delay_interval int=1,
@delay_type nvarchar(10)='minutes',
@truncate_history nvarchar(1)='N',
@clear_waitstats nvarchar(1)='Y')
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- @num_samples is the number of times to capture waitstats, default is 10
-- times
-- default delay interval is 1 minute
-- delaynum is the delay interval - can be minutes or seconds
-- delaytype specifies whether the delay interval is minutes or seconds
-- create waitstats table if it doesn't exist, otherwise truncate
-- Revision: 4/19/05
--- (1) added object owner qualifier
--- (2) optional parameters to truncate history and clear waitstats
set nocount on
if not exists (select 1
from sys.objects
where object_id = object_id ( N'[dbo].[waitstats]') and
OBJECTPROPERTY(object_id, N'IsUserTable') = 1)
create table [dbo].[waitstats]
([wait_type] nvarchar(60) not null,
[waiting_tasks_count] bigint not null,
[wait_time_ms] bigint not null,
[max_wait_time_ms] bigint not null,
[signal_wait_time_ms] bigint not null,
now datetime not null default getdate())

If lower(@truncate_history) not in (N'y',N'n')
begin
raiserror ('valid @truncate_history values are ''y'' or
''n''',16,1) with nowait
end
If lower(@clear_waitstats) not in (N'y',N'n')
begin
raiserror ('valid @clear_waitstats values are ''y'' or
''n''',16,1) with nowait
end
If lower(@truncate_history) = N'y'
truncate table dbo.waitstats

If lower (@clear_waitstats) = N'y'
-- clear out waitstats
dbcc sqlperf ([sys.dm_os_wait_stats],clear) with no_infomsgs

declare @i int,
@delay varchar(8),
@dt varchar(3),
@now datetime,
@totalwait numeric(20,1),
@endtime datetime,
@begintime datetime,
@hr int,
@min int,
@sec int

select @i = 1
select @dt = case lower(@delay_type)
when N'minutes' then 'm'
when N'minute' then 'm'
when N'min' then 'm'
when N'mi' then 'm'
when N'n' then 'm'
when N'm' then 'm'
when N'seconds' then 's'
when N'second' then 's'
when N'sec' then 's'
when N'ss' then 's'
when N's' then 's'
else @delay_type
end

if @dt not in ('s','m')
begin
raiserror ('delay type must be either ''seconds'' or
''minutes''',16,1) with nowait
return
end
if @dt = 's'
begin
select @sec = @delay_interval % 60, @min = cast((@delay_interval / 60)
as int), @hr = cast((@min / 60) as int)
end
if @dt = 'm'
begin
select @sec = 0, @min = @delay_interval % 60, @hr =
cast((@delay_interval / 60) as int)
end
select @delay= right('0'+ convert(varchar(2),@hr),2) + ':' +
+ right('0'+convert(varchar(2),@min),2) + ':' +
+ right('0'+convert(varchar(2),@sec),2)

if @hr > 23 or @min > 59 or @sec > 59
begin
select 'delay interval and type: ' + convert
(varchar(10),@delay_interval) + ',' + @delay_type + ' converts to ' +
@delay
raiserror ('hh:mm:ss delay time cannot > 23:59:59',16,1) with nowait
return
end
while (@i <= @num_samples)
begin
select @now = getdate()
insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
@now
from sys.dm_os_wait_stats

insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
'Total',
sum([waiting_tasks_count]),
sum([wait_time_ms]),
0,
sum([signal_wait_time_ms]),
@now
from [dbo].[waitstats]
where now = @now

select @i = @i + 1
waitfor delay @delay
end
--- create waitstats report
execute dbo.get_waitstats_2005
go
exec dbo.track_waitstats @num_samples=6
,@delay_interval=30
,@delay_type='s'
,@truncate_history='y'
,@clear_waitstats='y'

get_waitstats_2005

CREATE proc [dbo].[get_waitstats_2005] (
@report_format varchar(20)='all',
@report_order varchar(20)='resource')
as
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- this proc will create waitstats report listing wait types by
-- percentage.
-- (1) total wait time is the sum of resource & signal waits,
-- @report_format='all' reports resource & signal
-- (2) Basics of execution model (simplified)
-- a. spid is running then needs unavailable resource, moves to
-- resource wait list at time T0
-- b. a signal indicates resource available, spid moves to
-- runnable queue at time T1
-- c. spid awaits running status until T2 as cpu works its way
-- through runnable queue in order of arrival
-- (3) resource wait time is the actual time waiting for the
-- resource to be available, T1-T0
-- (4) signal wait time is the time it takes from the point the
-- resource is available (T1)
-- to the point in which the process is running again at T2.
-- Thus, signal waits are T2-T1
-- (5) Key questions: Are Resource and Signal time significant?
-- a. Highest waits indicate the bottleneck you need to solve
-- for scalability
-- b. Generally if you have LOW% SIGNAL WAITS, the CPU is
-- handling the workload e.g. spids spend move through
-- runnable queue quickly
-- c. HIGH % SIGNAL WAITS indicates CPU can't keep up,
-- significant time for spids to move up the runnable queue
-- to reach running status
-- (6) This proc can be run when track_waitstats is executing
--
-- Revision 4/19/2005
-- (1) add computation for CPU Resource Waits = Sum(signal waits /
-- total waits)
-- (2) add @report_order parm to allow sorting by resource, signal
-- or total waits
--
set nocount on

declare @now datetime,
@totalwait numeric(20,1),
@totalsignalwait numeric(20,1),
@totalresourcewait numeric(20,1),
@endtime datetime,@begintime datetime,
@hr int,
@min int,
@sec int

if not exists (select 1
from sysobjects
where id = object_id ( N'[dbo].[waitstats]') and
OBJECTPROPERTY(id, N'IsUserTable') = 1)
begin
raiserror('Error [dbo].[waitstats] table does not exist',
16, 1) with nowait
return
end

if lower(@report_format) not in ('all','detail','simple')
begin
raiserror ('@report_format must be either ''all'',
''detail'', or ''simple''',16,1) with nowait
return
end
if lower(@report_order) not in ('resource','signal','total')
begin
raiserror ('@report_order must be either ''resource'',
''signal'', or ''total''',16,1) with nowait
return
end
if lower(@report_format) = 'simple' and lower(@report_order) <> 'total'
begin
raiserror ('@report_format is simple so order defaults to
''total''',
16,1) with nowait
select @report_order = 'total'
end


select
@now=max(now),
@begintime=min(now),
@endtime=max(now)
from [dbo].[waitstats]
where [wait_type] = 'Total'

--- subtract waitfor, sleep, and resource_queue from Total
select @totalwait = sum([wait_time_ms]) + 1, @totalsignalwait =
sum([signal_wait_time_ms]) + 1
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total' ,'WAITFOR',
'***total***') and
now = @now

select @totalresourcewait = 1 + @totalwait - @totalsignalwait

-- insert adjusted totals, rank by percentage descending
delete waitstats
where [wait_type] = '***total***' and
now = @now

insert into waitstats
select
'***total***',
0,@totalwait,
0,
@totalsignalwait,
@now

select 'start time'=@begintime,'end time'=@endtime,
'duration (hh:mm:ss:ms)'=convert(varchar(50),@endtime-
@begintime,14),
'report format'=@report_format, 'report order'=@report_order

if lower(@report_format) in ('all','detail')
begin
----- format=detail, column order is resource, signal, total. order by
resource desc
if lower(@report_order) = 'resource'
select [wait_type],[waiting_tasks_count],
'Resource wt (T1-T0)'=[wait_time_ms]-[signal_wait_time_ms],
'res_wt_%'=cast (100*([wait_time_ms] -
[signal_wait_time_ms]) /@totalresourcewait as
numeric(20,1)),
'Signal wt (T2-T1)'=[signal_wait_time_ms],
'sig_wt_%'=cast (100*[signal_wait_time_ms]/@totalsignalwait as
numeric(20,1)),
'Total wt (T2-T0)'=[wait_time_ms],
'wt_%'=cast (100*[wait_time_ms]/@totalwait as numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by 'res_wt_%' desc

----- format=detail, column order signal, resource, total. order by signal
desc
if lower(@report_order) = 'signal'
select [wait_type],
[waiting_tasks_count],
'Signal wt (T2-T1)'=[signal_wait_time_ms],
'sig_wt_%'=cast (100*[signal_wait_time_ms]/@totalsignalwait
as numeric(20,1)),
'Resource wt (T1-T0)'=[wait_time_ms]-[signal_wait_time_ms],
'res_wt_%'=cast (100*([wait_time_ms] -
[signal_wait_time_ms]) /@totalresourcewait as
numeric(20,1)),
'Total wt (T2-T0)'=[wait_time_ms],
'wt_%'=cast (100*[wait_time_ms]/@totalwait as
numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by 'sig_wt_%' desc

----- format=detail, column order total, resource, signal. order by total
desc
if lower(@report_order) = 'total'
select
[wait_type],
[waiting_tasks_count],
'Total wt (T2-T0)'=[wait_time_ms],
'wt_%'=cast (100*[wait_time_ms]/@totalwait as numeric(20,1)),
'Resource wt (T1-T0)'=[wait_time_ms]-[signal_wait_time_ms],
'res_wt_%'=cast (100*([wait_time_ms] -
[signal_wait_time_ms]) /@totalresourcewait as numeric(20,1)),
'Signal wt (T2-T1)'=[signal_wait_time_ms],
'sig_wt_%'=cast (100*[signal_wait_time_ms]/@totalsignalwait as
numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by 'wt_%' desc
end
else
---- simple format, total waits only
select
[wait_type],
[wait_time_ms],
percentage=cast (100*[wait_time_ms]/@totalwait as numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by percentage desc

---- compute cpu resource waits
select
'total waits'=[wait_time_ms],
'total signal=CPU waits'=[signal_wait_time_ms],
'CPU resource waits % = signal waits / total waits'=
cast (100*[signal_wait_time_ms]/[wait_time_ms] as
numeric(20,1)),
now
from [dbo].[waitstats]
where [wait_type] = '***total***'
order by now
go


declare @now datetime
select @now = getdate()
select getdate()

[역주1] 원문에서는 RECOMPILE 쿼리 힌트를 추가로 언급하고 있으나 온라인 설명서, 프로파일러 등을 참조해 보면 OPTION(RECOMPILE)과 같은 것으로 판단됩니다.

[역주2] 원문에 별다른 언급이 없어서, 역자 임의로 추가합니다

[역주3] sys.dm_exec_query_optimizer_info DMV에는 원문에 언급된 두 가지 카운터 이외에 상당히 많은 카운터 항목들이 제공됩니다. 오히려 더 유용한 정보들이 많이 있습니다. 반드시 살펴보시길 권장합니다.

[역주4] 카디널리티(Cardinality)는 수학에서 집합의 원소의 개수를 의미합니다. 여기서는 테이블의 행 수를 의미합니다.

[역주5] 역자가 실제로 튜닝 컨설팅을 하면서 경험한 바로는 DOP의 개수에 따라서 2배 이상의 높은 CPU가 사용되는 경우를 자주 경험했습니다.

[역주6] 원문 코드의 주석 처리가 제대로 되어 있지 못해, 역자의 판단으로 주석 처리를 합니다.

[역주7] 한글 버전을 기준으로 언급합니다.

[역주8] SQL Server가 클라이언트로 결과 집합을 전송하는 기본 방식으로, 이전의 Firehose에 해당합니다. 전진-전용, 읽기-전용 방식입니다.

[역주9] 원본의 주소가 달라 변경합니다.

[역주10] 원본의 주소가 달라 변경합니다.

[역주11] 온라인 설명서의 번역을 따릅니다.

[역주12] 제품에 포함된 한글 오류 메시지와는 다르게 번역하였습니다.

[역주13] 원본에는 생략되었던 열입니다. 결과와 맞추기 위해 추가합니다.

[역주14] 원문의 offset 계산 공식은 잘못되었습니다. 온라인 설명서에서 sys.dm_exec_sql_text DMV의 예제를 참조하십시오.

[역주15] 한글 오류 메시지에는 victim에 대한 직접적인 해석 대신 “교착 상태가 발생...”으로 표현되어 있습니다. 해당 메시지를 직접 참조하십시오.

[역주16] 추적 플래그(Trace Flag)를 말합니다.

[역주17] 온라인 설명서에 찾을 수 없으며, 쿼리로도 실행되지 않습니다. 역자의 판단으로 내용 상 sys.dm_os_wait_stats 해당될 것으로 보입니다.

[역주18] 원문에서 실수로 반복된 항목이 아닐까 생각됩니다.

[역주19] 원본 예제는 각 예제별 설명과 실제 코드 간의 차이가 있어서, 역자 임의로 조정했습니다.

[역주20] 이 부분을 실제 Database ID로 변경하거나, DB_ID() 함수를 사용합니다.

+ Recent posts