728x90
인덱스 최적화 팁
저자: Alexander Chigrik

일반적인 개념
본 글에서는 적절한 인덱스를 사용하여 쿼리의 속도를 향상시키는 방법과 SQL Server가 지원하는 인덱스의 종류 및 특정한 상황에서 인덱스를 사용할 때의 장점과 단점 등에 대해서 얘기하고자 한다.

인덱스는 크게 나누어서 클러스터드 인덱스(Clustered Index)와 넌클러스터드 인덱스(Nonclustered Index)가 있다. 클러스터드 인덱스는 특별한 형태의 인덱스로 테이블에 저장되어 있는 레코드의 물리적인 순서를 재구성한 것이다. 따라서 하나의 테이블은 단 하나의 클러스터드 인덱스만을 가질 수 있다. 클러스터드 인덱스의 최종 노드(Leaf Node)는 데이터 페이지를 가지게 된다.

넌클러스터드 인덱스는 인덱스의 논리적인 순서가 디스크상에 존재하는 레코드의 물리적인 저장 순서와 일치하지 않는 인덱스이다. 넌클러스터드 인덱스의 최종 노드는 데이터 페이지 대신 인덱스 열을 가지고 있다.

클러스터드 인덱스나 넌클러스터드 인덱스를 만들 때에 fill factor 옵션을 사용하면 SQL Server가 각각의 인덱스 페이지를 최대 얼마만큼 사용할 것인지를 지정할 수 있다. 인덱스 페이지에 새로운 열을 추가할 만한 공간이 남아있지 않으면 SQL Server는 새로운 인덱스 페이지를 생성하여 기존의 페이지에 있는 열의 일부를 새로운 페이지로 옮기게 되는데 이 과정을 일컬어 페이지 스플릿(Page Split)이라고 한다. 인덱스의 각 페이지에 빈 공간을 확보하기 위해서 fill factor를 적절히 설정함으로써 페이지 스플릿이 발생하는 회수를 줄일 수 있다. fill factor는 1에서100 사이의 값을 가지며 빈 공간으로 남겨두어야 할 인덱스 페이지의 비율을 나타낸다.

fill factor의 기본값은 0이다. fill factor 0은 fill factor 100과 거의 동일한 의미를 갖는데 한가지 차이라고 한다면 fill factor 0의 경우에는 인덱스 트리의 상위 레벨에 어느 정도의 공간을 남겨둔다는 점이 다르다.

커버링 인덱스(Convering Index)는 쿼리에서 참조하는 컬럼을 모두 포함하는 인덱스를 말한다. 따라서 인덱스에 찾고자 하는 데이터가 모두 포함되어 있기 때문에 SQL Server는 굳이 테이블을 참조할 필요가 없다.

데이터 갱신 작업을 많이 한다면(삽입, 갱신, 삭제) 테이블의 단편화가 심하게 발생하게 된다. 단편화의 정도를 측정하고 싶다면 DBCC SHOWCONTIG 명령을 사용하면 된다.

최적화 팁


모든 인덱스는 삽입, 갱신, 삭제 명령이 수행되는 시간을 증가시키기 때문에 인덱스의 개수가 너무 많아서는 안 된다. 하나의 테이블에 최대 4~5개 이내의 인덱스를 사용하도록 한다. 만약 읽기 전용의 테이블을 사용하는 경우라면 인덱스의 숫자가 많아도 상관없다.

복합 인덱스가 포함하는 컬럼 개수는 가능한 적게 유지해야 한다. 이렇게 하면 인덱스의 크기를 줄일 수 있기 때문에 인덱스를 읽는 데 필요한 읽기 작업의 회수가 감소하게 된다.

문자열 컬럼보다는 숫자 컬럼에 인덱스를 생성하는 편이 좋다.

복합 인덱스(멀티 컬럼 인덱스)를 만드는 경우에는 인덱스 키의 순서가 매우 중요하다. 가장 선택도가 높은 컬럼 일수록 키의 왼쪽 편에 위치하게 하여 선택도를 향상시키는 방향으로 컬럼 순서를 구성해야 한다.

여러 개의 테이블을 조인하고 싶으면 정수 키를 대신 만들어서 이 컬럼에 인덱스를 생성하여 사용하도록 한다.

테이블에 삽입 작업이 많지 않다면 정수 타입의 프라이머리 키를 만들어서 대신 사용하도록 한다.

일정한 범위의 값을 조회하거나 GROUP BY, ORDER BY를 이용하여 결과를 정렬할 필요가 있다면 넌클러스터드 인덱스보다는 클러스터드 인덱스를 사용하는 편이 좋다.

동일한 테이블에 대해서 같은 쿼리를 반복적으로 실행해야 하는 경우에는 커버링 인덱스를 만드는 것을 고려해 보기 바란다.

데이터베이스 내의 어떤 테이블이 인덱스를 필요로 하는지 확인하기 위해서는 SQL Server 프로파일러의 트레이스 마법사(Trace Wizard)를 통해서 “Identify Scans of Large Tables” 트레이스를 이용 하면 된다. 이 트레이스는 어떤 테이블이 인덱스를 사용하지 않고 조회 중인지를 보여 준다.

데이터베이스의 모든 인덱스를 재구성 하려면 아직 문서화되지는 않았지만 sp_Msforeachtable 프로시저를 사용하면 된다. 이 프로시저는 CPU를 사용하지 않거나 작업이 뜸한 시간에 실행되도록 스케줄링을 하도록 한다.

sp_Msforeachtable @command1=”print ‘?’ DBCC DBREINDEX (‘?’)” 
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