728x90

김정선의 SQL Server 컨설팅 이야기

암시적 소유자로 인한 CacheMiss 한 방에 해결하기



사용자 삽입 이미지
김정선(jskim@feelanet.com)

필라넷 DB사업부 수석컨설턴트

SQLServer 아카데미/트라이콤 교육센터 강사

 

Microsoft SQL Server MVP

MCT/MCITP/MCDBA
 



무슨 얘기할려구?

역시 낚시용 제목인가? 너무 거창한 듯 합니다. 이번 컨설팅 사례는 SQL Server에서 개체 참조 시 소유자명을 생략한 경우, 즉 흔히 말하는 암시적 소유자상태에서 Procedure Cache(이젠 Plan Cache라고 부르는)에서 실행할 쿼리의 플랜을 찾지 못하는 현상(CacheMiss)이 발생하는지? 그리고 이를 한 방에 해결할 수 있는 방법이 무엇이 있을까?를 고민하고 그 결과 생각해낸 특정 방법을 테스트해 본 결과입니다. 해당 고객 사에는 전반적인 기술 이슈는 전달을 했지만 아직 직접적으로 반영된 상태는 아닙니다. 그러나 이와 유사한 환경을 가진 다른 사용자 시스템이 많을 것으로 보여 컨설팅 결과를 공유하고자 합니다. 혹시 관심 있다면 테스트해보시기 바랍니다.

 

시스템 환경

 

항목

결과

OS

당연히 Windows Server… ^^

SQL Server 버전

2000

솔루션

CRM 관련 외산 솔루션 (ASP 기반)

 

 

 

 

뭔 일이 있었는데?

 

배경

쿼리 성능의 중요한 한 축을 차지하는 것이 바로 “Procedure(Plan) Cache”의 재사용 능력입니다. 쿼리를 처음 실행 할 때 한 번 컴파일 과정을 거치고 나면, 그 결과로 도출되는 쿼리 계획(Query Plan) Cache에 저장해 두고, 이후 동일한 쿼리가 호출되고 재사용 요건을 만족하게 되면 다시 컴파일 과정을 거치지 않고 해당 쿼리 계획을 Cache에서 바로 참조해서 실행하는 것이죠. 이러한 쿼리 계획 재사용 능력을 높이기 위해서는 기본적으로 두 가지 튜닝 이슈가 발생합니다. 바로 1) 쿼리가 잘 Caching될 수 있도록 도와주는 것 그리고 2) Cache된 계획을 잘 재사용할 수 있도록 도와주는 것이 그것입니다-김정선의 생각 ^^.

 

그런데, Cache된 계획을 재사용할 때 즉 동일한 쿼리 계획이면 재사용 요건을 만족하는가? 라는 것을 평가할 때 중요한 키의 하나로 검사 받는 것이 바로 개체 소유자 지정에 관한 이슈입니다.

 

결론부터 얘기하죠,

SQL Server에서는 개체(특히 이 경우의 프로시저)를 참조할 때 개체 이름만 지정하지 말고 개체 소유자명(owner.objectname)을 함께 지정하는 것입니다-사실 여기에는 개체의 종류 및 기타 요소에 다른 차이가 많으므로 현재 사례로만 국한해서 설명합니다-특히 저장 프로시저의 경우, 프로시저를 만들 때 지정한 사용자명(정확히 소유자로 지정된 사용자명)과 해당 프로시저를 호출할 때 사용자명이 다른 경우에 개체 소유자명을 생략하고 호출하면 문제가 될 수 있다는 것입니다.

 

상황 재현

예를 들어 보죠,

Sales라는 데이터베이스는 로그인 명이 ‘sa’인 관리자가 만들었습니다. 따라서 ‘sa’라는 로그인이 Sales 데이터베이스의 생성자이며 자동으로 해당 DB에서 ‘dbo’라는 사용자명(USER_NAME())으로 작업하게 됩니다. 여기에 해당 DB를 사용하는 ‘John’이라는 로그인이 있으면 해당 DB의 사용자명 또한 ‘John’으로 지정되어 있다고 가정합니다.

 

1) ‘dbo’ 사용자명으로 개체 생성하기

DB를 만들고 소유한, ‘dbo’라는 사용자가 다음과 같이 프로시저를 만듭니다. 두 프로시저의 소유자는 dbo로 지정되었습니다. (명시적이건 암시적이건)

 

예제 프로시저 생성

 

CREATE PROC dbo.pTest1

AS

go

 

CREATE PROC dbo.pTest2

AS

go

 

 

2) ‘John’ 사용자로 개체 참조 시 암시적 소유자명 사용하기

이제 ‘sa’-‘dbo’가 아닌 ‘John’이라는 사용자로 위에서 생성된 프로시저를 호출할 때, 소유자명을 생략한다면 어떻게 될까요?

다른 사용자로 개체 호출 시 Profiler에서 CacheMiss 발생 여부 모니터링

 

SETUSER 'John'

 

-- case1: 명시적 소유자

-- case2: 암시적 소유자

WHILE 1 = 1

BEGIN

--         exec dbo.pTest1

           EXEC pTest1

          

           WAITFOR DELAY '00:00:00.200'

 

--         exec dbo.pTest2

           EXEC pTest2

          

           WAITFOR DELAY '00:00:00.200'

END

SQL Server가 컴파일 과정의 일부인 이름 해석 단계에서 개체를 검사할 때 어떤 규칙을 사용하는지 알고 계시죠? 첫 번째는 누구? 두 번째는 누구? … 무려 다섯 단계 정도나 되죠. 그 첫 번째는? , 현재 사용자입니다. 예제대로라면 “EXEC pTest1”는 사실 상 “EXEC John.pTest1”로 해석이 된다는 것이죠. John.pTest1라는 프로시저가 Cache에 존재하는지 이름 해석을 수행하게 됩니다. 그럼 못 찾겠죠? CacheMiss라는 이벤트가 Profiler를 통해서 발생할 수 있습니다.

 

3) 모니터링 결과

예제는 두 프로시저는 0.2초 간격으로 무한 루프를 돌면서 호출합니다. 다음은 CacheMiss가 발생하는 것을 Profiler에서 추적한 화면입니다.

 

[그림] Profiler에서는 SP:CacheMiss 이벤트 추적 결과

사용자 삽입 이미지
 

CacheMiss가 계속 발생되고 있는 것을 볼 수 있습니다. 그렇다고 프로시저가 아예 호출되지 않는 건 아니겠죠? SQL Server가 첫 번째 현재 사용자명으로 개체를 찾지 못하면, 두 번째로 사용하는 소유자명이 바로 ‘dbo’. 따라서 두 번째 작업에서 개체를 찾고 실행할 수 있게 될 것입니다.

위 예제에서 주석으로 처리된 부분 dbo.pTest1을 해제하고 실행하면 더 이상 CacheMiss는 발생하지 않게 됩니다.

 

이상 김정선의 혼자만의 상상의 나래였습니다 ^^;

 

 

그래서 어떻게 해결했는데?

 

뭐 실행만 되면 되지, CacheMiss 발생한다고 성능이 얼마나 느려질까? 맞습니다. 뭐 얼마나 차이가 있겠습니까 ^^; (확인을 못해본 관계로). 그렇지만, 서두에서 언급한대로 발생하지 않아도 될 부가적인 Cache Miss로 인한 오버헤드는 해결하는 것이 옳을 것입니다. 문제는 여기서 시작됩니다. 결론은 모든 프로시저 호출 시 dbo.를 붙여주어야 한다는 것인데, 프로그램 안에 하드 코딩 된 그 많은 프로시저를 다 고칠 수는 없는 노릇 OTL.

 

뭐 좀 간단한 방법은 없을까? (이것이 모든 관리자의 마음 ^^)-모 드라마에서 유행했던 거 좀 쉽게 쉽게 가자’)-공감 100%.

그래서 생각해 본 방법. 결론은 개체 소유자와 호출자가 동일하면 된다는 것인데. ‘John’을 해당 DB의 소유자(db_owner)로 만들어 버리면, 자동으로 ‘John’‘dbo’ 사용자명으로 실행될 것이고 그럼 자동으로 현재 사용자명이 ‘dbo’가 되어서 첫 번째 CacheMiss를 피할 수 있지 않을까? 라는 것이었습니다. 그것도 프로시저가 호출되는 시점에서 라이브로 작업을 했을 때요……물론 여기엔 어떤 부작용이 있을 수도 있을 겁니다. 어쨌든 테스트해 봤죠 ^^

 

특정 DB의 소유자를 다른 로그인으로 바꾸기

 

USE testdb

 

-- 주의: 기존 사용자면 오류 발생, 제거해야함

-- EXEC sp_adduser 'John'

EXEC sp_dropuser 'John'

EXEC sp_changedbowner 'John'

 

 

DB의 소유자를 변경할 때, ‘sp_changeobejctowner’를 이용하시면 되죠? 주의하실 것이 있습니다.

예제 코드의 주의를 달아두었으니 참조하세요.

 

앞의 반복 호출을 수행하고 있는 상태에서, 이 코드를 수행하면 CacheMiss가 바로 중단되는 것을 알 수 있습니다. OK~

 

 

더 할 말 있남?

너무 심각하게, 진지하게 받아들이지 않으셔도 될 것 같습니다. 그냥 오호~ 정도 ^^.

결론은 간단할 것 같습니다. 그냥 개체 참조할 때 소유자명 꼭 쓰세요~ 귀찮으면 그냥 DB에 오로지 1명의 사용자만 존재하도록 만든다? ^^

 

아참, 프로시저 접두사도 sp_ 시작되면 CacheMiss 2번 이상 발생되는 거 아시죠? ^^

 

 

주의

이 내용은 김정선이 개인적으로 실험한 결과입니다. 절대적인 근거 자료는 아니니 참고 사항으로 보시기 바랍니다. 그 어떤 보장도 하지 않습니다.

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

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

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

+ Recent posts