반응형
반응형

-- DMVStats 관련 내용중

 

디스크로부터 메모리로 데이터를 로드하는 과정에서 지연현상이 발생하게 되면, PageIOLatch 대기가 나타냅니다.

 

메모리 압박이나, 디스크 서브시스템에 문제가 발생한 경우에도, PageIOLatch 대기가 증가합니다.

 

사용자가 버퍼 캐시에서 필요로 하는 데이터 페이지를 찾을 수 없는 경우, SQL Server는 먼저 데이터 버퍼 페이지를 할당한 다음, 필요로 하는 페이지를 디스크로부터 데이터 캐시로 로드하는 동안 해당 버퍼 페이지에 PageIOLatch_EX 배타적 래치를 설정합니다. 그 동안에, 다른 사용자가 해당 버퍼 페이지에 대한 읽기 요청을 하게 되면, SQL Server는 사용자를 대신하여 사용자가 요청한 버퍼 페이지에 대해 PageIOLatch_SH 래치를 요청하고 대기합니다.

 

캐시에 대한 쓰기 작업이 완료되면, PageIOLatch_EX 래치가 해제됩니다. PageIOLatch_EX 래치가 해제되면, 비로소 사용자가 해당 버퍼 페이지를 읽을 수 있도록 허용하며, PageIOLatch_SH 래치도 해제됩니다.

 

결론적으로, PageIOLatch_EX 대기유형과 PageIOLatch_SH 대기유형이 높은 수치로 나타나면, IO 서브시스템에 문제가 발생하고 있다는 것을 나타냅니다.

 

관련된 성능 카운터로는

* Physical disk: average disk sec/read ( > 20 ms 인경우 확인 필요),

* Physical disk: average disk sec/write ( > 40 ms 인경우 확인 필요),

* SQL Server Buffer Manager: Page Life Expectancy

 등이 있습니다.

 

[참고-약어]

* DT - destroy

* EX - exclusive

* KP - keep

* SH - share

* UP - update





[PAGEIOLATCH_SH]

PAGEIOLATCH_SH SELECT 쿼리 시 Shared Latch를 얻기 위해 대기하고 있음을 의미한다. (Disk-to-Memory transfers)

 

 

 

PAGEIOLATCH_DT
 작업이 I/O 요청에 있는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 삭제 모드에 있습니다.
 
PAGEIOLATCH_EX
 작업이 I/O 요청에 있는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 배타 모드에 있습니다.
 
PAGEIOLATCH_KP
 작업이 I/O 요청에 있는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 유지 모드에 있습니다.
 
PAGEIOLATCH_NL
 작업이 I/O 요청에 있는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 Null 모드에 있습니다.
 
PAGEIOLATCH_SH
 작업이 I/O 요청에 있는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 공유 모드에 있습니다.
 
PAGEIOLATCH_UP
 작업이 I/O 요청에 있는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 업데이트 모드에 있습니다.
 
PAGELATCH_DT
 작업이 I/O 요청에 있는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 삭제 모드에 있습니다.
 
PAGELATCH_EX
 작업이 I/O 요청에 없는 버퍼를 래치에서 기다리는 경우에 발생합니다. 래치 요청이 배타 모드에 있습니다.
 
PAGELATCH_KP
 작업이 I/O 요청에 없는 버퍼를 래치에서 대기하는 경우에 발생합니다. 래치 요청이 유지 모드에 있습니다.
 
PAGELATCH_NL
 작업이 I/O 요청에 없는 버퍼를 래치에서 대기하는 경우에 발생합니다. 래치 요청이 Null 모드에 있습니다.
 
PAGELATCH_SH
 작업이 I/O 요청에 없는 버퍼를 래치에서 대기하는 경우에 발생합니다. 래치 요청이 공유 모드에 있습니다.
 
PAGELATCH_UP
 작업이 I/O 요청에 없는 버퍼를 래치에서 대기하는 경우에 발생합니다. 래치 요청이 업데이트 모드에 있습니다.
 
반응형
반응형
DECLARE @I INT

BEGIN TRY
    SET @I = 10 / 0
END TRY
BEGIN CATCH
    SET @I = 0
    PRINT '에러번호 : ' + CAST(ERROR_NUMBER() AS VARCHAR)
    PRINT '에러메시지 : ' + ERROR_MESSAGE()
    PRINT '에러심각도 : ' + CAST(ERROR_SEVERITY() AS VARCHAR)
    PRINT '에러행번호 : ' + CAST(ERROR_LINE() AS VARCHAR)
    PRINT '에러프로시저 : ' + ISNULL(ERROR_PROCEDURE(), '없음')
END CATCH
반응형
반응형
http://technet.microsoft.com/library/Cc917680
반응형
반응형

시스템 데이터베이스를 제외한 사용자 데이터베이스의 전체 테이블 목록과 해당 테이블의 rows 를 확인하는 방법에 대해서 아래와 같이 쿼리를 정리하였습니다.

-- 전체 테이블
EXEC sp_MSForEachDB 'Use [?];

-- 시스템 데이터베이스 제외
IF DB_ID(''?'') > 4
BEGIN
 SELECT ''?'' AS ''Database'', O.name, I.rows FROM SYSOBJECTS O INNER JOIN SYSINDEXES I ON O.id = I.id
 WHERE  O.xtype = ''U'' AND I.indid < 2
 ORDER BY I.rows DESC, O.name ASC
END'


쿼리 실행 결과는 아래와 같습니다.




[참고자료]

sp_MSforeachdb, sp_MSforeachtable 프로시저 활용
http://laigo.kr/307
반응형
반응형

우선.. 비밀번호를 잊어먹은 경우.. 윈도우 인증모드도 막아놨다면??..


싱글모드를 이용해서 복구할 수 있는 방법이 있습니다.


우선 싱글모드에 대해서 설명을 드리자면.. 싱글모드는.. Administrator Group에 있는 사용자에게.. sysadmin 권한을 부여하게 됩니다.


자세한 글은 아래 글들 참고하시기 바랍니다 ^^



http://www.sqler.com/390533


http://msdn.microsoft.com/ko-kr/library/dd207004.aspx


이렇게.. -m 를 주고 시작하게 되면.. 싱글모드로 시작하게 되어서.. 윈도우 계정으로 서버에 접속할 수 있습니다.. 

-f 의 옵션을 주게 되면.. 로그온 트리거도 패스되어 싱글 모드로 접속할 수 있습니다.


근데 문젠.. 싱글모드로 되어버리면.. 내가 접속하기전에.. 시스템이나 기타 다른 어플리케이션 등등이 접속할 수 있기 때문에..

sqlcmd를 통해서 접근하시는 걸 권장드립니다~^^


sqlcmd: http://msdn.microsoft.com/ko-kr/library/ms180944.aspx


물론 싱글모드라면.. 서비스중에 하면 큰일나겠죠~!


참고가 되시길 바랍니다 ~^^

반응형
반응형
 

• 온라인을 통한 기술지원   • SQLER
http://support.microsoft.com/oas
제품별 기술지원 정보 및 온라인을 통한 기술지원 제공.
  http://www.sqler.pe.kr/
SQL Server 팁, 강좌제공 및 활발한 게시판운영
• 뉴스그룹   • MSSQL
http://support.microsoft.com/newsgroups
고객 상호간 또는 Microsoft 기술지원 엔지니어 및 MVP에 의해 지원되는 게시판 형식의 뉴스그룹을 통한 기술지원 제공.
  http://www.mssql.org/
SQL Server 유용한 강좌를 제공하는 개인 홈페이지
• TechNet 온라인   • SQL Server 2005 커뮤니티
www.microsoft.com/korea/technet
IT Pro를 위한 백과사전
  http://www.sqlyukon.co.kr/
SQL Server 2005 최신 정보제공
• MSDN 온라인   • Jangrae's SQL World
www.microsoft.com/korea/msdn
샘플코드, 라이브러리, 기술문서, 제품 다운로드 등 개발자들의 필수 참고 사이트
  http://www.sqlworld.pe.kr/
MS SQL을 공부하시는 분들에게 유익한 정보를 제공
• Microsoft 행사 및 세미나 정보   • OLAP Forum
www.microsoft.com/korea/events
Microsoft 행사 및 세미나 일정 공지
  http://www.olapforum.com/
국내 최고의 OLAP 사이트
• Microsoft 다운로드 센터   • DB 가이드넷
www.microsoft.com/korea/download
최근의 주요 업데이트 프로그램, 서비스 팩 및 기타 유용한 파일 등의 다운로드
  http://www.dbguide.net/
한국데이터베이스진흥센터에서 운영하는 DB 구축·운영 종합정보 사이트
• Microsoft e-Seminar   • MCP월드
http://www.microsoft.com/korea/seminar
Microsoft가 주관하는 모든 세미나의 동영상과 발표자료 제공
  http://www.mcpworld.com/
마이크로소프트 MCP인증 관련 커뮤니티
• Microsoft 교육 및 인증   • 고수닷넷
www.microsoft.com/korea/traincert
Microsoft 자격증 및 Microsoft 공인 교육에 대한 정보 제공
  http://www.gosu.net/
국내 최초 아티클 전무 개발자 커뮤니티
• MDSN HOW-TO 문서      • SQLLEADER
http://www.msdn.microsoft.com/howto
실제 개발과 관련된 절차식 프로그램 가이드 라인 제시
   http://www.sqlleader.com/
Microsoft SQL Server의 정보를 공유 커뮤니티
• Microsoft Patterns & Practices       • SQLWorld

http://www.microsoft.com/practices
애플리케이션의 디자인 배포, 아키텍처, 제작 등에 관련된 Microsoft의 제안

  http://www.sqlworld.pe.kr
반응형
반응형

소규모 프로젝트를 위한 데이터베이스 모델링과 설계

물리적 모델링 실전 가이드

곽중선 l 클라우드나인에서 웹 사이트 구축 컨설팅

지난호까지 이론에 치중했으나, 이번 호에는 실질적으로 데이터베이스에 탑재할 수 있는 설계과정을 살펴보고자 한다. 이전 호에서 설명한 논리 모델을 구체화하여 물리 데이터 모델을 작성해보자. 물리 모델을 작성한다는 것은 실제 데이터베이스에 탑재 가능한 설계도를 작성한다는 의미다. 이같은 연장선상에서 데이터베이스에 테이블을 생성하고 컬럼을 정의하는데 있어서 성능에 영향을 미치는 요소들을 중점적으로 설명하고자 한다. 논리적 모델링은 설계 의도를 기술하기 위한 것이나, 물리적 모델링은 최적의 성능을 실현하기 위한 작업이다.


 

데이터 모델링에서 가장 중요한 작업은 논리 모델링이며, 지난 호에서 이를 자세히 살펴보았다. 그러나 데이터 모델링을 시스템 구축 프로세스 중 사소한 업무로 치부하고 사용자 인터페이스나 프로그램 로직을 구현하는데 집중하는 경우가 많은 게 사실이다. 게다가 모델링 툴(ER-Win) 등을 사용하고 있다면 언제라도 손쉽게 데이터베이스 스키마를 생성하고 물리적인 구현을 할 수도 있다. 단순한 절차에 따라 구현된 데이터베이스는 필요한 정보를 저장할 수 있더라도 DBMS의 특성을 살린 최적의 성능을 반드시 충족시킨다고는 할 수 없다.

개발 단계에서는 아무런 문제가 없던 시스템이 운영 단계로 넘어가면서 급격히 응답시간이 지연되거나, 심지어 시스템이 정지하기도 하는 것은 거의 물리적 모델링을 생략하거나, 적은 시간을 투자했기 때문이다. DB 설계를 잘못한 경우, 최악의 경우에는 시스템 전면 재개발이 필요하다. 따라서, DBMS 설계자는 DBMS의 특성이나 처리 효율을 이해해야 하고, 그런 특성을 고려한 모델을 설계할 줄 알아야 한다.

최근 데이터베이스 옵티마이저(optimizer)의 알고리즘이 적극적으로 개선되고 있다. 또 메모리, 디스크 등 하드웨어 부품의 가격 하락에 힘입어 데이터베이스가 과거보다 대용량의 트랜잭션을 소화할 수 있게 되었다. 이같은 결과로 보다 덜 정교한 논리모델을 그대로 구현해도 대체로 문제가 없다. 그러나 DBMS 제품의 특성, 하드웨어 그리고 처리(알고리즘, 비즈니스 규칙)의 관점에서 모델을 재검토하는 것이 중요하다. 물리 모델로 변환하는 과정에서 경험하고 얻어낸 노하우는 향후 성능 튜닝에서도 활용될 수 있다. 또한 모델의 변환 작업 이 외에도 테이블 컬럼 등의 명칭 부여, 각종 제약의 작성과 같은 부가적인 작업들도 있다.

 

논리 모델을 물리 모델로 변환하는 과정


전체 모델링 과정을 다시 한 번 정리해보자. 개념, 논리 모델에서 DBMS 구현까지의 흐름이다. 물리 모델링은 테이블의 구조와 그에 수반하는 테이블 명, 컬럼 명, 데이터 타입, 데이터 길이, NULL 옵션, 각종 제약의 정의 등을 포함한다. 그런데 데이터를 저장하기 위한 디스크 용량, 디렉토리 상에서의 위치, DBMS 제품 고유의 환경설정 등은 모델링에서 다루지 않기 때문에 실제 데이터베이스를 설치하는 과정은 별도의 학습이 필요하다.


 

물리 모델로 변환

 

물리 모델링 단계에서 절대 지나쳐서는 안 되는 것은 바로 논리모델의 ‘수정’이다. 즉, 논리 모델의 설계를 그대로 유지하려고 해서는 안 된다는 것이다. 논리모델은 업무의 모습(혹은 흐름)을 그대로 투영하여 설계자와 사용자가 손쉽게 이해할 수 있도록 하는데 그 목적이 있다. 달리 말하면, 데이터베이스를 운용하는 하드웨어를 위한 것이 아니라 인간에게 적합한 설계라는 것이다. 반면에 물리 모델은 오라클, MS-SQL, MySQL과 같은 데이터베이스 소프트웨어와 CPU, 메모리를 장착한 하드웨어가 최고의 성능을 발휘하도록 만들어야 한다. 논리 모델과 물리 모델은 언뜻 같아 보이지만 다른 것이라고 이해해야 한다.

아름다운 집을 디자인한다고 생각해보자. 최초의 구상 단계에서는 집 지을 재료는 생각하지 않아도 된다. 다양한 곡선과 아름다운 배치를 떠올려서 스케치할 것이다. 그런데, 집을 지을 재료가 콘크리트와 철근이라면 아무래도 좀 더 딱딱한 모양새로 변형해야 할 것이고, 나무라면 약간 더 곡선을 살릴 수 있을 것이다. 논리 모델과 물리 모델의 차이는 이와 같다. 따라서 논리 모델까지는 데이터베이스의 내부 원리와 특성을 잘 모르더라도 작성할 수 있었으나, 물리 모델을 작성하기 위해서는 각 데이터베이스 솔루션에 대한 지식이 필수다. 똑같은 관계형 데이터베이스라고 하더라도 오라클과 MS-SQL, MySQL은 서로 다른 성능과 특징들을 지니고 있다. 물리 모델을 설계하는 단계에서는 각 제품의 매뉴얼이나 해설서를 꼭 참고하는 것이 좋고, 필수서적 한 권씩은 필히 사서 읽어보거나 비치해둘 것을 권한다.

<그림 1> 논리 모델과 물리 모델

 

개체 통합

 

논리적 모델링을 수행할 때 게시판을 개체 정의하고 그 하위 요소로서 첨부파일, 답글, 덧글 등을 별개의 개체로 선언하고 관계를 선언하였다. 분리하여 표시하는 것이 각기 다른 정보들이 발생하는 시점이나 의미가 다름 등을 쉽게 알아차릴 수 있다는 장점을 지니기 때문이다. 즉, 논리적 모델은 업무의 의미 별로 개체를 상세히 분리해 선언한다. 하지만, 물리적 모델링에서는 개체들을 통합하는 작업부터 시작해야 한다. 애써 추출하고 분리한 개체들을 다시 모으는 작업이 점진적으로 상세히 그려간다는 모델링의 목적에 위배되는 것처럼 보일 것이다. 하지만, 이것은 정말 필요한 작업이다. 물리적 모델링의 목적은 업무에 대한 인간의 이해가 아니라, 하드웨어가 최고의 성능을 내도록 하기 위함임을 다시 기억해주길 바란다.

그렇다면, 둘 이상의 개체 혹은 테이블을 통합할 때 어떠한 장점이 있다는 것일까? 답은 I/O 처리효율 증가와 응답 시간의 단축이다. 데이터베이스는 둘 이상의 테이블을 조인(join)하는 것보다는 하나의 테이블을 참조하는 쪽이 훨씬 빠르기 때문에 테이블을 통합하는 것이 유리하다. 같은 주요 키(Primary key)를 공유하는 테이블들을 통합할 것인지, 혹은 레코드의 사이즈, 데이터의 건수, 혹은 한 블록에 저장되어 있는 레코드 수(버퍼 효율)등을 고려해 통합할 것인지를 결정한다. 보통 테이블의 사이즈가 작은 편이라면 통합하는 쪽이 효율적이다.

다시 게시판에 대한 이야기로 돌아가 보자. 게시판과 첨부파일 개체를 통합할 수 있는가를 생각해보자. 만일 게시판에 첨부할 수 있는 파일을 하나로 한정한다면 두 개의 테이블을 통합해도 된다. 단지 성능 때문에 복수 첨부파일 기능을 포기해야 하는 것은 아니지만, 가능하다면 그렇게 하는 것이 분명히 낫다. 게시물을 조회할 때마다 발생하는 데이터베이스 쿼리 횟수가 줄어든다는 것은 무시할 수 없는 이득인 것이다.

<그림 2> 테이블의 통합

 

레코드 길이에 따른 테이블 분할

 

앞에서 다룬 이야기는 반대로 하나의 테이블을 성능을 위해 둘로 쪼개는 경우도 있다는 것을 암시한다. 한 개체에 포함된 속성의 수가 많아지거나 텍스트 입력을 위해 길이를 크게 설정할 경우 레코드의 최대값이 데이터베이스에서 정의하고 있는 블록의 크기를 넘어버릴 수 있다. 블록 사이즈를 크게 만들어 주면 되지만, 블록 사이즈의 변경은 시스템 전체에 큰 영향을 주기 때문에 간단히 행할 수 있는 것이 아니다. 그러나 아무런 조치도 하지 않는다면 블록에 담을 수 없는 레코드는 확장 영역을 사용하게 되어 처리 효율이 극단적으로 하락하게 된다. 이러한 경우에는 테이블을 분할한다. 분할에 의해 테이블의 조인(join)이 발생하게 되지만 확장 영역으로 분할된 레코드를 검색하는 것보다는 훨씬 부하가 줄어든다.

<그림 3> 비정규화 예

 

참조 엔티티 속성 추가(비정규화)


데이터베이스 모델링에서 중요하게 강조되는 기법으로 정규화(normalization) 과정이라는 것이 있다. 개별 테이블에서 반복되는 데이터가 없어야 한다. 일관성을 유지해야 한다는 원칙, 그리고 이를 수행할 수 있는 방법을 아우르는 것이다. 정규화에서는 기본키(primary key)에 의존하지 않는 속성은 밖으로 꺼내 다른 엔티티에 배치한다. 예를 들어 게시판에 글을 쓴 사용자 정보를 표현할 경우, 게시판 테이블에서는 작성자의 ID만을 포함시키고 상세 정보는 사용자 테이블에서 조인(join)해 읽어오도록 설계한다. 그러나 물리모델로 변환할 때에는 반대로 비정규화를 하기도 한다.

즉, 게시판 작성자의 성명을 게시판 테이블에 포함시키는 것이다. 이렇게 함으로써 게시판 출력 시에 조인(join)을 하지 않고 단일 테이블을 쿼리함으로써 성능을 높일 수 있게 된다. 반면에, 실수로 사용자의 성명이 잘못 입력되어 추후에 사용자 정보를 수정하더라도 게시판에 기록된 작성자 성명은 변경되지 않는다. 이것이 비정규화의 단점이다.

 

필자 메모

기본에 충실해라


데이터베이스를 이해하고, 다양한 이론과 실전을 경험하다 보면 누구나 대용량 시스템을 자유롭게 설계하고, 높은 응답 성능을 낼 수 있도록 구축하고자 하는 욕심이 생기게 마련이다. 그렇다면 대체 무엇을 어떻게 학습해야 하는 것일까? 간단한 테이블을 생성하고, 쿼리를 작성하는 것은 그리 어렵지 않다. 하지만, 운영 데이터가 늘어갈수록 처리 속도는 점차 떨어지고 심지어 다운되는 현상이 발생하기도 한다. 성능을 보장하기 위해서 하드웨어의 용량을 늘려야만 할까?
지금까지 다양한 고객의 데이터베이스를 구축하고, 튜닝해온 경험과 외부의 컨설팅 업체에 의뢰하여 지적받은 설계상의 문제점들에 대한 답들을 요약하면, 바로 기본에 충실해야 한다는 것이다. 데이터베이스를 튜닝하기 위해서 아주 어려운 기술이나 복잡한 도구를 사용해야 한다는 것은 대개 편견이거나, 오해에 불과하다. 하드웨어를 증설하거나, 각종 설정을 조정한다고 한들 성능 개선 효과는 의외로 높지 않다. 예를 들어, 웹 사이트 응답 시간이 10초인데, 2배로 빨라 진다해도 사용자는 여전히 웹 사이트가 느리다고 느낄 것이다.

테이블을 설계하는 시점에 어떻게 분할할 것인가, 주요 키(primary key)와 외부 키(foreign key)를 정의하는 방식, 그리고 인덱스(index)를 어떻게 정의하는 하는가, 쿼리를 수행할 때 어떤 방식으로 조인(join)하는가에 따라서 수배에서 수십 배에 달하는 성능 차이를 가져온다. 쿼리가 수행되는 과정을 이해하고, 테이블 간의 조인(join)과 관계(relation)의 의미를 이해하며, 인덱스가 제대로 사용되는지를 정확히 알아야 한다.
이런 기술이나 이론들은 거의 기초 과정에서 배우게 되는 것들이다. 쿼리가 실행되는 과정(execution plan)을 이해해야 한다. 그리고 데이터베이스가 테이블 데이터들에 접근하는 과정을 알아야 한다. 데이터베이스 제품 혹은 하드웨어가 성능을 보장해줄거라는 환상을 가지지 말라. 그것은 단지 도구일 뿐이다. 깊이 알지 못하면서 본래의 성능을 끌어낼 수 없다. 쿼리와 테이블이라는 용어를 아는 것에 그치지 말고 그 원리를 이해해야 한다. 숨겨진 것은 없다, 단지 발밑을 보지 않았기 때문인 것이다.


코드 개체의 취급

 

직원들의 직위, 학생들의 학년, 학부 등과 같은 데이터가 취할 수 있는 한정된 값이나 값에 대한 명칭(화면 표시에 사용)을 관리하기 위해 코드 개체(코드 테이블)를 정의하고 자료를 저장하는 개체와 관계를 설정하는 방법들이 자주 사용된다. 그러나 코드 개체를 선언하고 데이터를 저장하는 개체와 함께 물리적 모델에 선언하면 개념적으로 이해하는 데는 무리가 없지만 작은 테이블이 다수 발생한다. 더불어 자료를 조회하는 쿼리가 복잡해지고, 자료의 관리가 어려워진다. 게다가 참조 제약을 일일이 설정하는 과정도 매우 번거롭기 때문에 물리적 모델을 작성할 때에는 다음 중 한 가지를 선택한다.

① 개별 구분 테이블을 하나로 모은다(구분 분류, 구분 코드).
② 테이블로 구현하지 않는다(프로그램에서 관리).
③ 테이블로 구현하는 것과 프로그램에서 관리하는 것으로 분리한다.

 

물리 모델에서 설정하는 컬럼

 

지금까지 논리 모델의 변환 과정에 대해서 살펴보았다. 이제 논리모델에서 나타나지 않았지만 추가해야 할 개체와 특성에 대해서살펴보자.

 

시스템 운영에 필요한 컬럼 추가

 

운영 시 복구나 분석 작업을 고려해 처음 데이터를 기록한 작성일시와 작성자, 이후 데이터를 변경할 때마다 최신 갱신일자와 갱신자 등을 설정하기도 한다. 게시판을 예로 들자면 작성자의 ID뿐 아니라 해당 게시물을 작성한 PC의 주소를 기록할 수 있다. 감사(audit)나 시스템 장애(system fault)를 대비하여 모든 테이블에 일률적으로 설정하기도 한다. 참고로, 이 경우에는 트리거(insert나 update 등의 액션이 발생했을 때 이름이나 시스템 시간을 설정)나 insert, update 메소드 안에서 기록하도록 설정해두면 개별 애플리케이션에서 이러한 처리를 부가적으로 할 필요가 없어진다.

 

데이터 타입 정의

 

논리 모델에서는 도메인을 규정하고 논리적인 데이터 타입이나 길이를 정하였다. DBMS는 제품마다 구현되어 있는 데이터 타입이 다르기 때문에 논리 모델 상에서 규정한 데이터 타입을 DBMS의 데이터 타입으로 변환할 필요가 있다. 자료와 데이터베이스 제품 유형에 따라 다음과 같은 타입과 길이를 권장한다.

·성명, 주소, 제목 등 수십,수백 자리의 짧은 문장 최대 길이를 예측할 수 있어야 하며, 예측한 최대 길이보다 긴 문자열이 입력되는 경우는 잘려서 입력된다. 가변길이 문자열 타입을 varchar(오라클에서는 varchar2)사용하며, 데이터베이스 제품과 버전에 따라 저장할 수 있는 최대 문자열 길이가 일정하지 않지만 각 제품의 버전에 따른 기술문서를 숙지해야 한다. 일반적으로 영문 기준으로 2000자까지 저장 가능하다. 한글을 입력할 때는 한 글자가 2byte를 차지하므로 절반 길이의 문자열을 저장할 수 있다는 것을 잊지 말아야 한다. 이름, 명칭을 저장하는 항목의 길이는 50자리, 짧은 설명 등을 포함하는 컬럼은 500자리라는 식의 간단한 문자열 크기 제한 규칙을 정해두는 것도 유용하다. 이름을 저장하는 컬럼을 설계하면서 한글 이름을 떠올리고 20자리 정도를 설정했는데, 외국인 이름이 입력되어서 중요한 정보를 상실하는 경우도 발생한다. 따라서 넉넉한 자릿수를 설정하는 것을 습관화 하는 것이 좋다.

·설명, 게시판 내용 등의 긴 문자열 긴 문자열을 저장하고자 할 경우 신중해야 한다. 대부분의 데이터베이스는 아주 긴 문자열(최대 2GB)을 저장할 수 있는 자료형을 제공하지만, 제품마다 그 명칭이 틀리고(Oracle에서는 CLOB 혹은 long 타입, MS-SQL은 text 타입) 읽고 쓰는 방식이 까다로울 수도 있다. 따라서, 영문 2000자를 넘지 않는다고 판단될 경우에는 가변길이 문자열 타입(varhar, varchar2)을 사용한다(데이터베이스 제품과 버전에 따라서 2000자 이상을 저장할 수도 있다). 게다가 아주 긴 문자열을 저장할 수 있는 타입들은 하나의 테이블에 단지 하나의 컬럼을 선언할 수 있다거나, 읽고 쓰는 속도가 상대적으로 느리다는 등의 단점이 있다. 마치 사용하지 말 것을 권장하는 것으로 해석될 수 있지만, 그런 의미가 아니라 장단점을 알아야 개발 단계의 문제 발생을 사전에 방지할 수 있다는 것이다.

·날짜 및 시간 모든 데이터베이스 제품들은 날짜와 시간을 저장할 수 있는 자료 형식(date, datetime 등)을 제공한다. 더불어 특정 제품에서 제공하는 날짜(시간) 타입을 사용하면 날짜(시간)를 출력하거나, 계산 처리(이전 날짜, 이후 날짜, 두 날짜의 간격 계산 등)를 하기 위한 함수를 사용할 수 있다는 이점이 있다. 그러나 특정 데이터베이스 제품에서 제공하는 함수들은 여지없이 다른 제품에서는 사용할 수 없어 새롭게 익혀야 한다. 게다가, 데이터베이스가 교체될 경우 프로그램을 재개발하거나 다른 버전을 제공해야 하는 경우도 발생한다. 물론 특정 데이터베이스에서만 동작할 것이 확실한 경우라면 괜찮지만, 신중히 결정하기를 권한다. 필자의 경우는 가급적 데이터베이스의 영향을 적게 받고 표준 SQL만으로 개발할 수 있도록 날짜의 경우는 8자리(YYYYMMDD), 시간은 6자리(HHMI24SS)의 char 타입을 선언한다.

·고정 크기의 문자열 데이터베이스에서 짧은 문자열을 저장할 때는 가변길이 문자열 타입(varchar, varchar2 등)과 고정길이 문자열 타입(char) 중 한 가지를 사용할 수 있는데 대개 가변길이 문자열을 사용하게 된다. 두 가지 유형 어느 쪽을 사용할지 결정할 때 성능 상의 차이는 무시할 만한 수준이다. 고정길이 문자열은 해당 컬럼이 ‘Y’, ‘N’ 등의 한자리 플래그(flag) 값이나, 코드를 저장한다는 것을 암시하기 위한 목적으로 사용된다. 즉, 개발자나 사용자가 손쉽고 빠르게 컬럼의 기능을 해석할 수 있도록 돕는 것이다. 그렇다고 코드 컬럼인 경우에만 써야 한다는 규칙은 없다. 업계에서 많이 사용되는 유용한 관습이라고 보면 된다.

·이진 자료(binary data) 최신 데이터베이스 제품들은 이진 데이터(binary data)를 저장할 수 있는 타입을 제공한다. 데이터베이스 설계를 처음 접하는 개발자(혹은 프로젝트 리더)들은 이런 유형을 적극적으로 사용해도 되는 것인지 쉽사리 판단하기 어렵다. 예를 들어, 게시판의 경우 첨부 파일을 디스크에 저장할 것인가, 데이터베이스에 저장할 것인가 하는 문제이다. 필자의 경험 뿐 아니라 대다수의 프로젝트에서는 데이터베이스에 파일의 경로만을 가변길이 문자열 컬럼(varchar)에 저장하고 실제 파일은 디스크에 보관한다. 이진 데이터를 보관하게 되면 데이터베이스의 입출력 부하, 백업 및 복구 시간이 급격히 늘어난다. 또한 관계형 데이터베이스는 주로 텍스트와 숫자 정보를 고속으로 처리하기 위해 설계되었다는 것을 잊지 말아야 하는데, 달리 말하자면 부가적인 기능은 말 그대로 부가적인 기능일 뿐이다. 추가 기능이 제공된다고 해서 그것을 사용하는 것이 언제나 최선일 수는 없다. 설계에 대한 판단과 그에 따른 책임은 분명히 개발자 혹은 프로젝트 관리자가 져야 하는 것이다. 자칫 설계 시의 판단 착오 때문에 적절한 성능이 발휘되지 않았을 때, 뒤늦게 제품의 특성을 잘 몰랐다고 항변해야 소용없는 것이다. 기본값 및 NULL 허용 여부 설정 데이터베이스 기반 애플리케이션을 개발하면서 개발자들이 가장 자주 보게 되는 오류는 무얼까? 물론 데이터베이스 관련 개발에 국한된 것은 아니지만, 아마 NULL 관련 오류(Null pointer 오류라고도 한다)일 것이다. 데이터베이스의 특정 항목에 빈값이 들어 있고, 해당 항목을 읽어내는 프로그램이 주의하지 않을 경우(혹은 빈 값 검사를 안 할 경우)에 NULL 오류가 발생한다. 그런데, 이런 오류는 의외로 쉽게 비켜갈 수 있다. 컬럼을 물리적으로 정의할 때에 기본 값으로 빈 문자열이나, 0과 같은 숫자를 설정하는 것이다. 혹은 입력 시점에 빈 상태를 허용하지 않는 것이다. 테이블을 생성하는 것은 한번 뿐이지만 참조하는 프로그램 코드 위치는 매우 다양할 수 있다. 테이블을 정의할 때 조금 더 신경을 쓰면 프로그램 코드 길이와 테스트 시간을 줄일 수 있다. 더불어 기본값을 지정하면 SELECT 쿼리를 작성하면서 특정 컬럼이 빈값일 때 유효한 값으로 변환하는 NVL 함수 등의 사용 빈도를 줄일 수 있다. 이것은 읽기 성능을 작게나마 향상시킨다.

<그림 4> 데이터 타입 정의

 

물리 명칭 설정

 

논리 모델을 물리 모델로 변환할 경우 모델의 변환이나 속성의 추가 이외에 물리 구현 명칭을 부여하게 된다.

 

테이블, 컬럼명 설정

 

논리 모델에서는 개체와 속성명에 한글 명칭을 부여한다. 그러나 일반적으로 사용되는 프로그래밍 언어에서는 개체의 명칭에 한글을 허용하지 않기 때문에 영문 명칭으로 부여하게 마련이다. 개체의 명칭을 부여하기 위해 명명 규칙(naming rule)과 명명 사전을 정의해 둘 필요가 있다. 이를 통해 중복이 제거된 논리 모델의 상태가 구현까지 이어져 개발이나 유지보수 시 작업의 효율이 높아진다. 또한 DBMS 상의 스키마나 프로그램 개발에서도 통일된 이름을 사용할 수 있기 때문에 영향을 미치는 범위를 쉽게 찾아낼 수 있는 장점도 있다. 구체적으로 설명하자면 한글 이름을 그대로 로마자 표기하면 단어가 길어지므로 한글 논리명에 대해 영문 약어를 설정하고 그 조합으로 명칭을 부여하는 방법을 널리 사용한다. 예를 들어 번호는 ‘NO’, 년월일은 ‘ymd’, 명칭은 ‘nm’으로 설정하고 주문 번호는 order_no, 전자문서는 elec_doc으로 선언하는 식이다.

 

제약 명칭 설정

 

테이블이나 컬럼명 이외에 명명이 필요한 개체로 각종 제약(constraints or restrict) 명칭이 있다. 만약 정의 시에 제약을 부여하지 않으며 DBMS가 자동으로 ‘sysxxxxx’(오라클)와 같은 형태로 일련번호를 부여한다. 실제로 제약 에러가 발생했을 때, 제약 이름만 봐서는 어디에 설정된 제약인지 알 수 없어 에러가 발생한 곳을 찾기 어려워지므로 제약명은 부여하는 것이 좋다. 마찬가지로 주요 키(primary key) 제약은 한 테이블에 하나만 존재하기 때문에 PK + 테이블명으로 선언한다.

<그림 5> 명명 사전 예

 

명명 표준(naming rule)

명명법 혹은 네이밍 표준(naming rule)이라는 용어에 대해서 익숙하지 않을지도 모른다. 하지만, 명명 표준은 프로그래밍에서의 변수, 클래스 이름 정의, 네트워크 구성 등 시스템 정의에서 광범위하게 필요로 하는 것이다. 간단한 시스템을 소수가 개발하는 경우에는 모든 대상의 이름을 부여함에 있어서 표준화가 굳이 필요하지는 않지만, 다양한 사람이 함께 일하는 기업이나 프로젝트에서는 상호간의 혼란을 막고, 대상에 대한 정확한 이해를 도우며, 유지보수를 돕기 위해 이름을 정의하는 표준을 설정하는 것이 중요하다.
간단하지만 구체적인 속성 정의 사례를 들어 명명 표준의 필요성에 대해 알아보도록 하자. ‘날짜’라는 항목은 매우 빈번하게 사용되는 속성인데, 년월일(YYYYMMDD) 처럼 연도(year), 월(month), 일자(date)까지만 기록하는 경우, 날짜와 시간을 함께 기록하는 경우가 있다. 이 두 가지 경우에 모두 날짜(date)라는 속성 명칭을 부여한다면 혼란이 발생하고 말 것이다. 또한 사람들을 구분하기 위해서 아이디(ID) 속성을 사용하는데 그 값으로 사원번호, 주민등록번호, 학번 등을 사용할 수 있기에 역시 같은 문제가 발생할 수 있다. 나아가서 사람의 이름(name) 항목을 생각해보자. 사람의 이름(full name)은 성(family name)과 이름(given name)으로 구분될 수 있다.


명명 표준은 분명 표준이라는 단어를 포함하고 있지만, 공유할 수 있는 표준이 없다. 다양한 기업, 다양한 업무가 존재하기 때문에 각 개체와 속성에 맞는 정확한 표기법을 통일할 수 없는 것이다. 그렇기 때문에 데이터베이스 구축 경험이 많은 업체들은 용어집이라는 책자를 만들어 직원들에게 배포하기도 한다. 작은 프로젝트에서는 표준화가 이루어지기 어려운 것이 현실이지만, 팀 내에서 표준화에 대한 노력을 기울일 필요는 있다. 매사가 저절로 이루어지는 법은 없으니 말이다.

명명법의 기본적인 개념은 자주 사용되는 기본 용어(단어)를 정리하고 데이터의 의미와 역할에 맞는 조합 규칙을 정해둔 다음, 한 번에 이해할 수 있는 이름을 붙이는 것이다. 고전적인 명명 표준으로는 기본어(Prime), 수식어(Qualfier), 분류어(Class)의 조합으로 나타내는 방법이 있다. [Q] + P + C의 순서로 조합하며, 단어는 단어집에 등록되어 있는 단어 중에서 골라내고 없는 경우 단어집에 추가한다. 또한, 주요어는 경우에 따라서 수사어로 사용된다. 기본어(주제어)는 정의하고 싶은 대상을 나타내며, 수식어는 기본어의 의미를 보충하거나 제한한다. 분류어는 속성을 분류하는 것인데, 타입(type)이라고 설명할 수도 있다. 아래 단어집 예시를 보도록 하자.

기본어(주제어) : 매출, 직원, 구매, 상품, 문서, 장비, 배송, 고객 등 개체나 속성 자체를 표현하는 단어
수식어 : 일별, 월별, 기간, 영역, 연령 등 범위를 나타내거나 기본어를 구체화하는 단어
분류어 : 코드, 번호, 종류, 일시, 금액, 비율, 회차, 이름, ID 등 값의 형태를 나타내는 단어

 

참조 제약

 

논리모델에서 주요 키(primary key)와 외부 키(foriegn key)로 관계가 설정되어 있는 곳은 모두 참조 제약으로 구현되어야 한다. 툴(tool)을 사용하고 있다면 자동으로 제약을 생성해 주지만, 물리 모델로 변환할 때 제약 그 자체를 구현할 것인지 아닌지를 생각해볼 필요가 있다. 참조 제약을 정의하면 데이터의 무결성을 확실하게 유지할 수 있다. 그러나 프로그램이 복잡하거나 성능 상의 문제가 발생할 것을 예상한다면 제약을 풀고 프로그램에서 정합성을 유지하는 방법을 고려할 수도 있다. 외부 키를 사용하는 것은 데이터의 무결성을 보장해주는 장점이 있지만 insert, update 시 연결된 테이블을 검색하기 때문에 성능을 떨어뜨린다.

 

그 외 데이터베이스로 구현 가능한 규칙

 

데이터베이스 모델링의 목적은 자료 구조를 정의하는 것이라고 짧게 정의할 수 있다. 그런데, 데이터베이스의 기능은 그렇게 단순하지 않으며 다양한 기능을 추가로 제공한다. 업무 규칙(business rule) 혹은 각종 처리(process)를 DB 내에서 정의할 수 있다는 것이다. 값 제약(value restric), PK 제약, 참조 제약 이외에도 트리거(trigger)나 저장 프로시져(stored procedure) 등 각종 스크립트를 이용하여 규칙을 정의할 수 있다. 규칙을 정의한다는 표현을 달리 해석하면 데이터베이스 내에 프로그램을 선언하고 실행할 수 있다는 의미다. 이로써 얻을 수 있는 장점은 유지보수 단계에서 사용자 프로그램을 컴파일하거나, 재배포하지 않고 규칙을 변경할 수 있다는 것이다. 게다가, 데이터를 변경하는 프로그램이 데이터베이스 내에 저장되어 있기 때문에 처리 속도 또한 수배에서 수십배까지 빠르다. 이런 장점 때문에 대규모 데이터베이스 개발 시에는 트리거와 저장 프로시져를 적극적으로 사용한다.

 

트리거 정의

 

트리거는 테이블의 insert, update, delete 등 작업(action)의 전후에 테이블에 대한 조작(operation)을 정의한 것이다. 시스템 운용 시에 일률적으로 필요한 컬럼의 추가나 DBMS에서 제공하는 참조 제약을 보완하는 목적으로 자주 이용된다. 예시한 소규모 웹 사이트의 경우에는 사용자의 소속이 변경되는 경우, 자동으로 변경된 사용자 이름을 게시판 테이블에 반영하고자 할 경우에 트리거를 설정할 수 있다. 트리거는 데이터베이스 내부에서 동작하는 프로그램이기 때문에, 응용 프로그램을 재배포하거나 다시 컴파일할 필요없이 쉽게 설치, 제거, 변경이 가능하다. 단, 트리거나 저장프로시져의 작성법은 데이터베이스 제품마다 조금씩 다르기 때문에 개발하려면 해당 제품 가이드와 샘플을 참고해야 한다.

 

<리스트 1> MS-SQL 게시판 정의 테이블 스크립트
CREATE TABLE bbs_doc
(
doc_key char(17) COLLATE Korean_Wansung_CS_AS NOT NULL,
/* 문서고유번호 */
bbs_id char(8) NOT NULL, /* 게시판 ID */
doc_subject varchar(255) NOT NULL, /* 문서 제목 */
doc_text text NULL, /* 문서 본문 */
doc_expiration char(14) NOT NULL, /* 문서 폐기일자 */
doc_writer char(8) NOT NULL, /* 문서 작성자 ID */
doc_writer_name varchar(40) NOT NULL, /* 문서 작성자 이름 */
doc_password varchar(20) NULL, /* 문서 비밀번호 */
doc_att_cnt int NULL, /* 첨부 문서 수 */
doc_background char(10) NULL, /* 문서 배경 이미지 */
doc_regdate char(14) NULL, /* 문서 등록 일자 */
doc_ref_cnt int DEFAULT 0, /* 문서 조회 수 */
doc_rec_cnt int DEFAULT 0, /* 문서 추천 수 */
doc_seq int NULL, /* 게시판 문서의 순번 */
doc_subseq int NULL, /* 게시판 문서의 답변 순서 */
doc_indent int NULL, /* 게시판 문서의 답변 레벨 */
doc_child int DEFAULT 0 /* 하위 게시물 수 */
);

ALTER TABLE bbs_doc ADD CONSTRAINT pk_bbs_doc PRIMARY KEY (doc_key);
ALTER TABLE bbs_doc ADD CONSTRAINT fk_bbs_doc FOREIGN KEY (bbs_id) REFERENCES bbs_catalog (bbs_id);
CREATE INDEX idx_bbs_doc ON bbs_doc (bbs_id asc, doc_seq desc, doc_subseq asc);
CREATE INDEX idx_bbs_doc2 ON bbs_doc (resv_date desc, doc_subseq asc);

 

인덱스와 뷰 정의

 

DBMS의 처리 효율을 높이려 할 때에 인덱스는 매우 중요한 역할을 담당한다. 인덱스를 정의하지 않으며 데이터베이스에 저장된 데이터를 처음부터 끝까지 순서대로 검색하기 때문에 테이터 건수가 몇 백만 건으로 늘어갈수록 응답속도가 급격히 떨어지게 된다. 반면 쓸데없이 인덱스를 정의하면 데이터 갱신 시에 테이블에 정의된 수많은 인덱스를 갱신해야 하기 때문에 부하가 발생하고 시스템이 느려지게 된다. 또한 여러 유저가 동시에 접속할 시에 락킹(locking) 때문에 대기 시간이 길어지게 된다. 이러한 상반된 점을 고려하여 설계할 필요가 있다. 빈번하게 액세스 되는 패턴이나 복수의 테이블을 조인하여 액세스하는 경우에는 뷰를 작성해 두기도 한다.

 

<리스트 2> Oracle 게시판 정의 테이블 스크립트
CREATE TABLE bbs_doc
(
doc_key char(17) NOT NULL, /* 문서고유번호 */
bbs_id char(8) NOT NULL, /* 게시판 ID */
doc_subject varchar2(255) NOT NULL, /* 문서 제목 */
doc_text long NULL, /* 문서 본문 */
doc_expiration char(14) NOT NULL, /* 문서 폐기일자 */
doc_writer char(8) NOT NULL, /* 문서 작성자 ID */
doc_writer_name varchar2(40) NOT NULL, /* 문서 작성자 이름 */
doc_password varchar2(20) NULL, /* 문서 비밀번호 */
doc_att_cnt number NULL, /* 첨부 문서 수 */
doc_background char(10) NULL, /* 문서 배경 이미지 */
doc_regdate char(14) NULL, /* 문서 등록 일자 */
doc_ref_cnt number DEFAULT 0, /* 문서 조회 수 */
doc_rec_cnt number DEFAULT 0, /* 문서 추천 수 */
doc_seq number NULL, /* 게시판 문서의 순번 */
doc_subseq number NULL, /* 게시판 문서의 답변 순서 */
doc_indent number NULL, /* 게시판 문서의 답변 레벨 */
doc_child number DEFAULT 0 /* 하위 게시물 수 */
)
PCTFREE 10 PCTUSED 70 TABLESPACE ${db-tblspc-name} STORAGE ( PCTINCREASE 0 );

ALTER TABLE bbs_doc ADD CONSTRAINT pk_bbs_doc PRIMARY KEY (doc_key) USING INDEX TABLESPACE IDX_TBL_SPACE;
ALTER TABLE bbs_doc ADD CONSTRAINT fk_bbs_doc FOREIGN KEY (bbs_id) REFERENCES bbs_catalog (bbs_id);
CREATE INDEX idx_bbs_doc ON bbs_doc (bbs_id asc, doc_seq desc, doc_subseq asc) TABLESPACE IDX_TBL_SPACE;
CREATE INDEX idx_bbs_doc2 ON bbs_doc (resv_date desc, doc_subseq asc) TABLESPACE IDX_TBL_SPACE;

 

물리적 DB 스크립트 작성

 

관계형 데이터베이스 제품들의 DML(Data Manipulation Lanuage) 쿼리는 거의 호환되지만, DDL(Data Difinition Language) 명령 규칙은 각기 다르다. 따라서, 각기 다른 데이터베이스의 테이블, 제약 선언 문법을 이해해야 한다. 다음의 예는 두 가지 데이터베이스에 맞추어 설계한 게시판 정의 테이블 스크립트의 결과다. 이상으로 소규모 데이터베이스를 설계하는 과정에서 필히 알아두어야 할 여러 개념, 용어 그리고 기법들을 설명하였다. 그동안 설명한 과정들은 데이터베이스 모델링을 수행하는 과정에서 꼭 필요한 작업들을 언급한 것이다. 가급적 실무에서 잠깐씩이라도 고민해야봐야 하는 단계들이다. 보다 상세한 기법과 설명을 예시하지 못한 점이 아쉽기는 하지만 우선 제시된 용어와 절차를 이용해 큰 흐름을 이해하기를 바란다. 더불어 보다 대규모 데이터베이스를 다루어야 한다거나 성능에 관심을 가지고 있다면, 인덱스 설계, 트리거 및 저장 프로시저 작성 기법을 중점적으로 학습하는 것이 좋다. 무엇보다 데이터베이스를 제대로 다루기 위해서는 각 제품의 특성을 상세히 파악해야 한다. 이론적인 바탕은 동일하지만 세부 기법은 큰 차이를 가지고 있기 때문이다.

 

이달의 디스켓 : db.zip

참고 자료

1. 실천적 데이터베이스 모델링 입문 |Mano Tadachi | 영진닷컴
2. 대용량 데이터베이스 솔루션 | 이화식 | 엔코아컨설팅
3. 운명적 존재를 위한 데이터베이스 설계(2판) Michael J. Hernandez | 사이텍미디어

 

 

제공 : DB포탈사이트 DBguide.net


출처 : 마이크로 소프트웨어 -[2006년 7월호]
반응형
반응형
소규모 프로젝트를 위한 데이터베이스 모델링과 설계 1


데이터베이스 모델링

 


곽중선|웹에이젼시

 

데이터베이스 모델링 분야는 매우 전문적이고 어렵다는 시각이 지배적이다. 이론 위주의 학습 과정은 물론 실전적인 가이드가 부족한 현실 때문이다. 대용량 데이터베이스 설계는 고도의 기술이 필요하지만, 대다수의 개발자는 소규모 프로젝트에서 DB 설계를 시작한다. 소규모의 데이터베이스 모델링을 위한 실전 가이드를 소개한다.

 

소프트웨어 개발자 치고 관계형 데이터베이스라는 단어를 들어보지 못한 사람은 거의 없을 것이다. 그러나 프로그래머 혹은 개발자들에게 데이터베이스 설계를 요구하면 대부분 고개를 가로젓는다. 너무 어렵다거나, 전문적인 기술이라고 대개 고개를 흔든다. 누구나 데이터베이스를 사용하지만, 제대로 설계할 수 있는 개발자가 드문 게 현실이다. 데이터베이스는 과연 전문가의 전유물인가. 소규모 데이터베이스 정도는 직접 설계해야 할 필요도 있지 않을까? 게다가 데이터베이스 관련 프로젝트의 대다수는 전문 컨설턴트 없이 수행되는 것이 현실이다. 소규모 DB를 설계하는데 필요한 실전 기법을 중심으로 DB설계의 참맛을 느껴보도록 하자.

 

왜 관계형 데이터베이스인가?

 

관계형 데이터베이스란 1970년 IBM의 E. F.Codd 박사가 발표한 논문의 이론에 기반한 데이터 관리 시스템을 통틀어 부르는 명칭이다. Codd 박사는 모든 자료를 2차원 형태의 테이블로 저장하고, 각 테이블의‘관계’를 강조하는 기술을 이용해 복잡한 형태의 자료들을 단순하면서도 일관성 있게 관리할 수 있다는 것을 수학적으로 증명하였다. 컴퓨터를 활용하는 이유는 복잡하고 반복적인 작업을 정확하고 빠르게 수행하기 위한 것인데, 이러한 과정을 수학적 체계에 따라 정립했다는 점에서 RDBMS의 신뢰성, 정확성이 보장된다는 것에 주목해야 한다. 관계형 데이터베이스가 30년 넘게 발전하며, 여전히 중요하게 활용되는 이유는 자료를 단순한 2차원 표 형태로 저장하면서도 매우 복잡한 자료를 정교하게 조합할 수 있다는 양면성에 기인 한다고 할 수 있다. 대용량의 복잡한 데이터 저장용으로만 관계형 데이터베이스를 활용하는 것은 아니다. 관계형 데이터베이스 이전에 Text file, ISAM file, 계층형 DBMS, 네트워크 DBMS가 개발되었고, 관계형 데이터베이스 이후로는 객체형 DBMS 등이 소개되었다. 그러나 복잡한 대용량의 자료를 처리하는데 있어 요구되는 신뢰성, 처리 속도 그리고 이론적인 체계의 완성도 면에서 관계형 데이터베이스를 뛰어넘는 기술이나 솔루션은 없었다. 따라서 인터넷 포털, 쇼핑몰, 대형유통업체, 정부 기관에서 소규모 자료관리 시스템에 이르기까지 체계적으로 자료를 저장하는 모든 영역에서 관계형 데이터베이스가 활용되고 있다. IT 종사자의 경우 RDBMS는 필수적으로 이해해야 하는 기초 기술이 라고 해도 과언이 아니다. 거의 모든 IT 관련 학과와 교육기관에서 RDBMS를 필수과목으로 가르치고 있음에도 불구하고 자신 있게 DBMS를 설계하고 구축할 수 있는 개발자를 찾아보기 어려운 이유는 무엇일까? 교육현장에서는 지나치게 이론 위주로 가르치고 있다는 점을 지적하지 않을 수 없다. 이론이 불필요하다는 것은 아니다. 왜 모델링을 해야 하는가. 왜 DBMS를 사용하는가에 대한 근원적인 설명이나 데이터베이스 모델을 설계하는 실습을 충분히 거치지 않기 때문이라고 생각된다. 설계 관련 실습을 하더라도 지나치게 복잡하고 어려운 예제를 이용하기 때문에 현장에서 필요로 하거나 응용기술을 익히기 어려운 경우가 대부분이다. 논리적으로 완성도가 높은 모델링이 무의미한 것은 아니지만, 한정된 시간과 인력을 활용해 시스템을 구축해야 하는 현실과는 괴리감이 없지 않다. 정교하고 완벽한 설계 기법은 수십 억, 수천 억건 이상의 자료를 관리하는 대규모의 시스템을 개발할 때나 필요하다. 소규모 정보시스템이나, 인터넷 정보조회 시스템을 설계하는 데 있어서는 좀 더 현실적이고 실질적인 기술이 필요하다(어떤 분야에서나 고급 기술과 범용 기술이 함께 공존하는 법인데 데이터베이스 분야에서는 고급 기술만 우대받는 경향이 강하다).

 

왜 모델링을 해야 하는가?

 

시스템 분석 및 설계 단계에서‘모델링’과‘설계’라는 단어가 혼용되는 경우가 많지만 모델링과 설계는 분명히 다른 의미이다. 데이터 모델링이란 현실 세계의 업무(예를 들면 재무회계, 쇼핑몰, 물류관리 등)를 추상화하는 것이다. 반면 데이터 설계라는 것은 모델링 과정을 통해 추출된 정보를 컴퓨터 내부에서 관리할 수 있는 형태로 구체화하며 구조와 관계를 표현하는 것이다. 따라서 모델링과 설계 과정을 분리하여 설명하도록 하겠다. 모델링은 현실을 추상화하는 작업이라고 표현했다. 추상화(abstraction)라는 용어에 대해 거부감을 느끼는 독자도 있을 것이다. 그런데, 추상화는‘요약’이라는 뜻으로도 해석된다. 소프트웨어 공학에서 말하는 추상화는 형이상학적 개념이라기보다는 복잡한 것을 단순화하는 과정이다. 모델링 역시 복잡한 현실을 단순화시켜 컴퓨터 시스템 내부로 이식할 수 있도록 꼭 필요한 요소만 도출하는 과정이다. 만화가가 인물을 그리는 모습을 떠올려 보자. 사람의 외모에서 특징만 강조하고 세부적인 요소는 배제한다. 완벽함이란 더 이상 추가할 것이 없는 상태가 아니라, 더 이상 제외할 것이 없을 때 이뤄진다는 말이 있다. 추상화 혹은 모델링은 달리 말하면 완벽함을 추구하는 과정으로 요약할 수 있겠다. 현실 세계는 무수히 많은 정보로 구성되어 있다. 사람’을 표현할 수 있는 정보는 얼마나 될까? 이름, 신체조건, 나이, 주민번호, 경력사항, 직업, 취미, 학력 등 열거하자면 끝이 없다. 그런데 인터넷 쇼핑몰을 구축할 때‘사람’이라는 대상은‘고객’혹은‘관리자’라는 단어를 통해 좀 더 단순하게 표현되고‘ID’,‘ 주소‘,‘ 나이’,‘ 성별’등 거래 혹은 마케팅에 필요한 정보만 도출해야 한다. 이러한 과정을 모델링이라 한다. 복잡한 현실 혹은 업무에서 ‘관리 및 기록이 필요한 정보’를 추출하는 작업이다. 동시에 필요 없는 정보를 제거하는 작업을 데이터 모델링이라고 한다. 물론 관리 대상을 찾아내고 나열하는 데 그치지 않는다. 이를 이해 하기 쉽고 의사소통이 원활하도록 도와주는 표준화된 표기법으로 문서화하는 작업을 통틀어 말한다. 또한 시스템 구상 단계에서 스케치를 그리는 것, 데이터베이스를 만들기 위한 청사진을 만드는 것으로도 표현할 수 있을 것이다. 모델링 결과는 데이터베이스 설계를 위한 중요한 준비 작업이다. 목표 시스템이 어떤 정보를 포함할지 사전 검증할 수 있는 근거가 된다. 개발팀원 간 혹은 개발팀과 고객 간 의사소통을 위한 중요한 도구가 되며, 시스템을 설계하는 과정에서 아이디어를 체계화하고 정리하는데 도움이 된다. 또한, 현실업무를 파악하는 과정을 기록해 시행착오가 발생했을 때 원인을 파악할 수도 있으므로 시스템 설계 노하우를 축적하는 데도 안성맞춤이다. 예컨대 모델링은 현실 세계의 무수한 정보 가운데 컴퓨터 시스템으로 이식하려는 대상을 추출해 단순화하고 요약하는 과정이다. 설계는 모델링을 통해 확정된 이식 대상을 데이터베이스 설계 규칙에 맞춰 세밀하게 재구성하고, 정해진 표기 방법에 따라 문서화하는 작업이다. 모델링과 설계의 의미는 분명히 다르지만, 프로젝트 수행 과정에서는 모델링과 설계를 순환하면서 진행한다.

 

데이터베이스의 발전 과정
● ISAM(Indexed Sequential Access Method) : 원하는 정보를 찾기위해 순차적으로 접근하거나, 색인(index)을 통해 선택적으로 접근할 수 있는 파일 시스템. 데이터베이스가 보편화된 이후에도 소규모 정보시스템에서 활용된다.

● 계층형 DBMS : 자료를 트리형태로 저장하는 데이터베이스. 윈도우 파일 시스템의 디렉터리 구조를 연상하면 되겠다. 정보를 디렉터리 형태로 분류하기 때문에, 복잡한 구조를 묘사할 수 없다는 단점이 있다.

● 네트워크형 DBMS : 계층형 데이터베이스의 단점을 보완하기 위해 자료간 연결(link)을 망(network) 형태로 자유롭게 연결할 수 있도록 개선했다. 그러나 자료구조가 복잡해지면 이해가 어려운 데다가 자료구조 변경시 디스크에 저장된 데이터의 물리적 구조를 재구성해야 하며, 애플리케이션 역시 다시 개발해야 한다.

● 관계형 DBMS : 모든 데이터를 2차원 형태의 테이블에 저장하며, 자료간 관계를 표현하기 위해 키라는 데이터 값을 이용한다. 관계형이라는 이름을 사용하는 이유는 테이블간 관계를 설정하는 방법에 대한 연구가 주된 과제이기 때문이다. 관계형 데이터베이스의 장점은 장부, 엑셀 문서 등에서 일상적으로 사용하는 표 형태를 사용해 단순한 설계인 경우 구조를 이해하거나 만들기가 용이하다. 확장도 용이하다. 테이블을 생성한 후 손쉽게 항목을 추가할 수 있고, 자료구조가 변경되더라도 이미 개발된 프로그램을 변경하지 않고 확장할 수 있다.

● 객체형 DBMS : 객체지향이라는 패러다임이 도입되면서 프로그램 상에서 생성되는 객체를 그대로 디스크에 저장하거나, 읽어 들이는 제품이 개발되었다. 다양한 제품이 출시되었지만 널리 보급되지는 못했다. 대용량의 자료를 처리하는데 있어 많은 연구가 이뤄졌으나, 강력한 검색 성능으로 시장을 석권하고 있는 관계형 데이터베이스의 아성을 넘기에는 역부족이다. 지금까지 다양한 DBMS 이론이 연구되었으나, 한동안 관계형 데이터베이스의 지위를 뛰어넘는 새로운 기술이 나타나기는 어려울 것이다. 달리 말하면, 세월이 지나도 사장되지 않을 기술이니 익혀두면 지속적으로 적용할 수 있다.


<그림 1> 데이터베이스 유형 개념도

 

데이터 모델의 종류

 

데이터 모델은 일반적으로 개념, 논리, 물리 등 3단계로 분류하며, 세 개의 모델을 순서대로 작성하는 절차를 거친다. 개념 모델과 논리 모델은 추상화 작업에 가까우며, 물리 모델은 설계 작업이라 할 수 있다.


● 개념 모델
가장 먼저 작성하는 모델로 구축하려는 시스템에 대한 각종 요구 조건을 기술하는 것이다. 목표 시스템에 어떤 정보를 담을 것인가, 어떤 결과를 출력할 것인지 등의 요구 조건을 개략적으로 작성한다. 그러나 화면에 표시할 세부 데이터 항목을 모두 기술해야 하는 것은 아니다. 논리 모델은 말 그대로 설계에 반영하는 것이 아닌 시스템 구상 단계의 대략적인 스케치이며, 논리 모델을 만들기 위한 기초 단계이다. 따라서 일단 개략적인 요구사항을 작성한 후 거의 수정하지 않는다. 예를 들어 인터넷 쇼핑몰을 구축할 때 상품 정보, 고객(혹은 회원), 판매자 등 서비스를 운영하는데 필요한 기본적인 관리 대상을 명시한다. 더불어 고객에게 보여줘야 하는 물품 분류, 재고 및 배송 정보 등 업무를 진행하면서 표시해야 하는 결과물을 도출한다. 각 관리 대상에 대한 상세정보는 표현하지 않는데 지나치게 자세히 표현하면 시스템 전체를 한눈에 들여다보기 어렵기 때문이다.

 

● 논리 모델
개념 모델에 시스템이 필요로 하는 데이터 항목을 상세히 기입한 것이 논리 모델이다. 구축 시스템의 이미지를 확고히 하기 위해 실제 화면을 스케치 하거나, 보고서, 차트, 출력물 등 최종 사용자에게 보이는 결과를 확정해 간다. 또한 각 대상(혹은 테이블)에 포함되는 항목(혹은 컬럼)들을 상세히 표현한다. 여기서 파악된 데이터 항목을 골라 데이터베이스 모델 다이어그램을 그린다. 논리 모델을 작성한 후 가급적 시스템을 사용할 사용자와 함께 설계 결과를 검토해봐야 한다. 꼭 필요한 항목임에도 누락되면, 시스템 개발 중 다시 설계단계로 복귀해야 하는 경우도 발생한다. 관계형 데이터베이스는 운영 중 항목을 추가하기 쉽지만 너무 믿어서는 크나큰 손실을 입는다. 프로젝트가 난관에 봉착할
수도 있다. 예외 상황은 언제나 발생할 수 있지만, 그것을 해결하는 것은 시스템이 아니라 개발자의 몫이기 때문이다.


● 물리 모델
실제 운영 가능한 상태 혹은 자료를 입력할 테이블의 형태를 구체적으로 표현하는 것이다. 이 단계에서는 테이블 내역, 컬럼 및 기본 키(primary key), 외부 키(foreign key), 인덱스 등을 설정한다. 논리 모델에서는 특정 데이터베이스 제품에 의존하지 않지만 물리 모델에서는 오라클, DB2, MySQL, MS-SQL 등 다양한 데이터베이스의 문법(규칙)에 따라 표현 방식이 달라진다. 따라서 하나의 논리 모델에 대해 여러 개의 물리 모델이 존재할 수도 있다. 데이터베이스 제품마다 조금씩 작성 규칙이 다르기에 주로 사용하는 데이터베이스를 정해 세부 기법을 익히는 것이 좋다. 개념이나 논리 모델을 작성하는 단계에서 어떤정보를 관리해야 하느냐 하는 문제를 파악하고 분석하는데 집중해야 한다. 그러나 물리 모델은 복잡한 자료들을 어떻게 구성할 것인가 - 집중할 것인가, 분산할 것인가 -와 각 항목의 크기와 타입을 설정하고 각종 제약사항(필수 입력 여부, 기본 키, 외래 키등)을 명시한다. 시스템 성능에 직결되는 인덱스 등을 지정하므로 신중하게 접근하는 것이 좋다. 규모가 작거나 프로젝트 기간이 짧은 경우에는 논리 모델 작성을 건너뛰고 바로 물리 모델을 작성하거나, 간략히 수행하는 경향이 있는데, 가능하다면 논리 모델 작성을 제대로 수행하는 것
이 시스템 완성도를 높이는 길이다. 개발 단계에 진입했을 때 발생할 위험 요소를 방지하기 위해서다. 문제는 급변하는 경영환경으로 단기간에 시스템 구축을 요구받고, 적은 수의 인력으로 수행되는 프로젝트가 만연한 현실에서 이상만을 주장하고 있을 수만은 없다는 것이다. 그렇기 때문에 논리 모델 보다는 물리 모델을 작성하는 방법과 유의사항에 집중하여 논의할 것이다. 물론 처음 데이터베이스 모델링을 접하거나 체계적으로 지식을 습득하고자 한다면 개념 모델 작성 경험을 쌓는 것이 좋다.

 

<그림 2> 개념, 논리, 물리 모델 다이어그램

 

 

데이터 모델링의 구성요소

 

데이터 모델은 엔티티(entity), 속성(attribute), 관계(relation)라는 3대 요소로 구성된다. 데이터 모델링은 이러한 세 가지 요소를 현실 업무에서 추출해 나가는 과정이다. 작업 대상 혹은 목표의 의미를 정확히 이해하는 것은 매우 중요하니 지나치지 말고 이 세 가지 용어의 의미를 주의 깊게 새겨보기 바란다.

 

● 개체 혹은 엔티티(entity)
엔티티(entity)는‘개체’로 번역하며, 물리적 모델에서는 테이블(table)이라고 칭한다. 개체라는 것은 무엇인가? 명백히 다른 것과 구분되어 존재하는 것, 실체나 개념을 의미한다. 사람, 물건, 문서 등을 예로 들 수 있지만 현실 세계에 존재하지 않는 것도 분명 개체가 될 수 있다.‘ 동일한 특성을 가진 정보들의 집합’ 이라 할 수 있고,‘ 관리 대상인 데이터 집합’이라고도 표현한다.2차원 테이블 형태로 표현할 수 있는 모든 자료로 이해해도 될것이다. 모델링 다이어그램에서는 사각형 박스로 표현한다.

● 속성(attribute)
속성이란 개체의 구성요소로 이름, 가격, 시간, 수량 등의 간단한 데이터 항목을 의미한다. 데이터베이스에서 속성은 숫자, 문자, 문자열, 날짜, 이진 데이터(binary data) 등의 단일 값(single value)을 기록하며, 다른 항목과 구분을 위한 이름이 부여되는 최소 저장 단위이다. 모델링 다이어그램에서는 엔티티 박스 내에 표기된다.

 

● 관계(relation)
데이터모델을 구성하는 엔티티간 의존성 혹은 연결 정보를 표현한 것을 관계(relation)라고 한다. 개체는 단독으로 의미를 가지는 경우도 있지만, 대체로 여러 종류의 개체가 조합되어 의미 있는 정보가 된다. 쇼핑몰에서 물건을 구매하는 프로세스에서 ‘상품 구매 기록’을 관리하는 경우를 가정해보자. 구매 기록은 상품, 구매자, 판매자 등의 개체에 대한 정보와 판매 기록 그 자체 (판매금액, 일시, 지불방법)가 조합되어야만 쓸모 있는 정보이다. 이렇듯 다양한 개체간 상관관계 혹은 연결(link)을 정의하는 것이 관계 설정이다. 일 대 일, 일 대 다, 다 대 다 등의 관계가 있으며 비즈니스 규칙을 표현하는데 있어 중요한 역할을 수행한다. 다이어그램 상에서는 점선 혹은 실선으로 표기한다.

<그림3> 데이터베이스의 3대 구성요소

 

 

데이터베이스 모델링 절차

 

● 업무 흐름(Business Workflow) 파악

당연한 이야기일지 모르지만, 모델링에 앞서 사용자의 요구사항을 문서화하는 작업이 선행되어야 한다. 현실 업무에 대한 지침서(manual)가 있다면 다행이지만 참고할만한 문서가 없거나, 업무 흐름을 새롭게 정리해야할 경우도 있다. 일정에 쫓기고, 번거롭다고 해서 간단한 대화만으로 업무 파악을 끝내는 경우가 비일비재하지만 정확한 용어로 업무 흐름을 세심하게 문서화하길 바란다. 의뢰인으로부터 업무 흐름에 대해 설명을 들어가면서 정리하는 내용이 바로 모델링을 위한 재료가 된다. 그 중에서 다양한 정보가 발생하고 관리해야 하는 명사는 개체와 속성이 되고 각 개체를 묘사하는 형용사는 속성이 된다. 각각의 개체가 어떻게 조합되는지 설명하는 동사 혹은 문장은 관계가 된다. 업무 흐름을 상세히 정리할수록 모델링 및 설계 단계가 편해지기 마련이다. 귀찮다고 건너뛰지 말자. 개발자들이 사용자와 마주하는 업무로 인해 많은 스트레스를 받지만, 피하면 모델링 단계에서부터 일이 지연되는 경우가 많다.


● 개체(entity) 추출

개체란 시스템의 관리 대상으로 사람, 물건 등의 실체나 개념을 의미한다. 구축 대상 업무에서 무엇을 개체로 파악해야 하는가에 대한 정답이나 절대적인 공식은 존재하지 않는다. 하지만, 대략적인 선정 기준을 제시할 수는 있다. 업무명세 중에서 명사는 개체가 될 수 있는 후보가 되며, 그 가운데서 장기간 보존해야 하거나, 시스템이 종료한 후에도 지워져 서는 안 되는 정보를 추출한다. 예를 들면 다음과 같다.

- 사람(고객, 직원)이나 조직 체계 (회사, 부서, 대리점)
- 물품 혹은 서류(재고 상품, 문서, 부품), 각종 설비 (건물, 운송수단)
- 각종 개념 (주문 및 거래 기록, 통화 내역, 배송 정보, 은행계좌)위에 나열한 명사들을 살펴보면 현실세계에서 존재하는 대상은 손쉽게 파악할 수 있다. 실물로 존재하는 대상을 찾는 것은 그리 어렵지 않다. 하지만, 관리할 대상 중 개념적으로 존재하는 것을 개체로 선정하지 못하고 자칫 지나치는 경우가 있다. 이러한 불상사를 막기 위해서는 업무흐름(workflow) 혹은 활동을 그려보고 각 단계에서 새롭게 발생하거나, 변화하는 자료가 없는지 점검해 봐야한다. 종이와 펜을 놓고 대략적인 플로우 차트를 그려가면서 입출력되는 정보가 있는지 점검해보는 것도 좋다. 입출력되는 정보 중 보존해야할 정보가 없는지 살펴보기 바란다. 쇼핑몰 구축을 예로 들면 상품에 대한 주문과 발송, 배송 정보
가 발생했을 때, 거래 시점에만 사용한다면 상관없지만, 거래 종료 이후에도 해당 정보를 조회하려 한다면 개체로 선정되어야 한다. 데이터 모델링을 수행함에 있어 시스템 구축을 위한 기술적인 지식보다는 고객이 원하는 바가 무엇인가, 무엇을 관리해야 하는가, 현실 세계의 업무가 어떻게 흘러가는가에 대한 이해가 더욱 중요하다. 개체를 추출하는 작업은 기록 가능한 모든 것을 찾아내는 것이 아니라, 기록할 필요가 있는 것을 찾는 것이기 때문이다.

 

● 개체간의 관계 설정

데이터베이스는 자료를 저장하기 위한 시스템으로 설명할 수 있지만, 그보다 중요한 것은 가치 있는 정보를 어떻게 효율적으로 정확히 조회할 수 있는가 하는 점이다. 다수의 개체가 어떻게 연결되며, 각 개체들은 어떤 관계를 가지고 있는가를 정의하는 작업을 통해 효율적인 검색 조회를 위한 기초가 다져진다. 개체를 추출한 후에는 개체 간 의존 관계(부모 자식 관계)를 파악한다. 또한, 개체간 관계를 분석하다보면 새로운 개체가 나타나기도 한다. 부서와 팀원이라는 두 개의 개체를 추출했다고 가정해보자. 목표 시스템이 인사관리 시스템이라면, 각 팀원은 특정 부서에 속한다는 관계를 파악할 수 있으며, 이러한 관계를 표현하기 위해 나타내는 소속이라는 개체(혹은 개념)를 추가해야 한다.

 

● 주요 키(primary key)와 속성(attribute) 파악 및 제약 설정

데이터베이스 시스템에서는 둘 이상의 개체를 조합하여 정보를 추출하는 경우가 많다. 또한 각 개체에 포함된 정보를 정확하게 검색할 수 있도록 개체에 포함된 데이터를 유일하게 구분할수 있는 정보(주요 키)를 설정해야 한다. 그리고 각 속성 크기와 자료 유형을 결정하며, 속성에 빈 값이 들어가서는 안 되는 식의 제약 사항을 지정한다.

 

● 상향식 분석을 통한 재구성

하향식 분석을 통해 구축 대상 개체, 관계, 속성 등을 파악한 후 사용자에게 나타나는 화면, 통계 보고 등을 확인하고 누락된 항목은 없는지 다시금 모델을 검토하는 작업을 수행해야 한다. 상향식 분석 과정에서는 새로운 개체를 추출하기 보다는 속성을 추가하고, 관계를 더욱 명확히 파악하는 데 중점을 둬야 한다. 이번 호에서는 데이터베이스 모델링 절차의 개괄적인 흐름을 설명했다. 아직 구체적인 사례를 들지 않아 뜬구름 잡는 느낌이겠으나 이론이 뒷받침되지 못한 채 실전 작업에 투입되면 복잡한 시스템 설계 시 난관에 부딪치기 십상이다. 일단 개념, 용어, 그리고 절차에 익숙해지길 바란다. 다음 호에는 이론을 실무와 적용하는 사례를 소개하고, 최종적으로 물리적 DB 설계시 꼭 알아야 두어야할 기법들을 소개한다.

 

 

정리|문경수 objectfinder@imaso.co.kr

 

필자 메모
데이터베이스의 개념을 처음 접하고, 모델링을 배우고 실습하다 보면 자연스럽게 어떤 데이터를 관리할 것인가에 집중하게 된다. 데이터의 관계나 속성을 추출할 때도 실 세계에 존재하는 자료 항목들을 빠짐없이 추출했는가에 관심을 쏟게 된다. 자료를 쌓는데 집중하는 것이다. 이런 태도가 항상 옳은가? 절반은 맞고 절반은 틀리다. 데이터베이스의 우선적인 목표는 자료를 모으는 것이기 때문이다. 하지만, 쌓기만 하면 자료로써 얼마나 가치가 있는가? 원하는 자료를 손쉽게 검색할 수 없다면, 아무리 많은 자료를 모은다고 한들 소용없다. 제대로 된 모델링이란 자료를 손쉽게 찾을 수 있는가? 라는 질문에 그렇다고 답할 수 있어야 한다. 구축된 데이터를 활용하는 다양한 방법이나 목적(통계, 실시간 조회, 변화 추이 분석)에 적합하지 않은 모델링은 필연코 실패할 수밖에 없다. 모델링 단계를 충분히 수행하고도 실패하는 프로젝트는 모델링 단계에서 자료의‘활용’방법(목적)을 예측하지 못했기 때문이다. 모델링 실습 단계에서 이 문제를 하나하나 살펴보기로 하자.

 

제공 : DB포탈사이트 DBguide.net

반응형
반응형
USE jwjung
GO

DROP TABLE T1;
GO

CREATE TABLE T1 (
    제품번호    CHAR(5)    NOT NULL
    ,입고일자    CHAR(8)    NOT NULL
    ,입고수량    INT            NULL
);
GO

ALTER TABLE T1 ADD CONSTRAINT t1_pk PRIMARY KEY (제품번호, 입고일자);
GO

DECLARE @제품번호 CHAR(5), @입고일자 CHAR(8), @입고수량 INT
SET @제품번호 = 'A0001'
SET @입고일자 = CONVERT(CHAR(8), GETDATE(), 112)
SET @입고수량 = 100

UPDATE T1
SET 입고수량 = 입고수량 + @입고수량
WHERE 제품번호 = @제품번호
    AND 입고일자 = @입고일자

IF @@ROWCOUNT = 0
BEGIN
    PRINT '제품번호 ''' + @제품번호 + '''은(는) 입고일자 ''' + @입고일자 + '''에 없음'
    INSERT INTO T1 VALUES (@제품번호, @입고일자, @입고수량)
END
GO

--제품번호 'A0001'은(는) 입고일자 '20120210'에 없음
SELECT * FROM T1;
GO


DECLARE @제품번호 CHAR(5), @입고일자 CHAR(8), @입고수량 INT
SET @제품번호 = 'A0005'
SET @입고일자 = CONVERT(CHAR(8), GETDATE(), 112)
SET @입고수량 = 100

MERGE T1 a
    USING (
        SELECT @제품번호 as 제품번호
                    , @입고일자 as 입고일자
                    , @입고수량 as 입고수량
        FROM T1
        WHERE 제품번호 = 'A0003'
        ) b
    ON a.제품번호 = b.제품번호
    AND a.입고일자 = b.입고일자
    WHEN matched THEN
        UPDATE SET 입고수량 = a.입고수량 + b.입고수량
    WHEN not matched THEN
        INSERT (제품번호, 입고일자, 입고수량)
        VALUES (b.제품번호, b.입고일자, b.입고수량)--;
    OUTPUT $action, inserted.*, deleted.*;
반응형
반응형

김정선의 좋은 글을 찾아서……
SQL Server 인덱스 구성 전략(시리즈-4. 정렬되지 않은 파티션 인덱스)

 


김정선(jskim@feelanet.com) 필라넷 DB사업부 수석컨설턴트

Part 4: 오프라인, 직렬/병렬 파티셔닝(정렬되지 않은 파티션 인덱스 구성)

정렬되지 않은 경우는 힙과 인덱스가 서로 다른 파티션 스킴을 사용하거나, 힙이 파티션되지 않은 경우이다.

이번 글에서 원본 데이터가 파티션 된 경우와 그렇지 않은 2가지 경우의 정렬되지 않은 파티션 인덱스 구성에 대해 다룰 것이다.

 

원본이 파티션 되지 않은 경우

다음 쿼리를 보자.

 

Create Partition Function pf (int) as range right for values (1, 100, 1000)

Create Partition Scheme ps as Partition pf
ALL TO ([PRIMARY])

Create table t (c1 int, c2 int) 테이블은 디폴트로 PRIMARY 파일 그룹에 생성

Create clustered Index idx_t on t(c1) on ps(c1)  -정렬되지 않은 인덱스 구성

 

직렬 계획은 간단하다,

 Index Insert (write data to the in-build index)
   |
 Sort (order by index key)
   |
 Scan (read data from source)

 

Sort 반복연산자(iterator)는 각 파티션당 하나의 정렬 테이블을 만든다(예제에서는 4개의 파티션이 존재하므로 동시의 4개의 정렬 테이블이 만들 것이다). 디폴트로, 데이터 정렬을 위해 사용자 데이터베이스를 사용한다. 이전에 언급한대로, 정렬 데이터에서 모든 데이터를 복사하고 나면 각각의 익스텐트를 해제한다. 이 동작을 통해서 각 파티션 별로 필요한 디스크 공간을 3 x 파티션크기에서 2.2 x 파티션 크기로 줄일 수 있다. 따라서 각 파일 그룹은 2.2 x (해당 파일 그룹에 속한 전체 파티션 크기)만큼의 디스크 공간을 요구한다. SORT_IN_TEMPDB 옵션을 지정한다면, 모든 정렬 테이블은 tempdb에서 다루어질 것이고, 2.2 x (인덱스 전체 크기)만큼의 여유 공간을 tempdb에 요구할 것이다.

 

Index Insert 반복연산자는 sort 반복연산자가 정렬을 완료하고 나면 인덱스 구성을 시작한다. 파티션 수 만큼의 정렬 테이블이 필요하므로, 각 정렬 테이블 당 최소 40페이지가 필요함을 상기한다면, 최소 필요 메모리는 파티션 수 x 40페이지가 될 것이다.

 

병렬 계획이 된다면,

 

X (Exchange)
   |
 Index Insert
   |
 Sort
   |
 Scan

 

각 작업자 스레드가 병렬 처리 수와 해당 파티션 수만큼 계산되어 할당된다 (예를 들어, 4개의 파티션에 4개의 작업자 스레드이면 각 스레드 당 1개의 파티션). Sort 반복연산자를 할당된 각 파티션에 대해 하나의 정렬 테이블을 생성한다. 각 작업자는 원본 데이터를 한 번 스캔하고 그 파티션에 속한 행들을 처리한다, 처리된 행들은 소속된 파티션에 따라 해당 정렬 테이블에 입력된다.

 

모든 정렬 테이블이 완성되면, 인덱스 구성자가 정렬 테이블을 하나씩 처리하며, 각 파티션의 파일 그룹별로 b-tree를 구성하게 된다.

디스크 공간 및 메모리 필요 사항은 이전의 직렬 계획과 동일하다. 두 경우 모드, 모든 정렬 테이블이 완성되기 전까지 인덱스 구성을 시작할 수 없기 때문이다.

 

 

 

원본이 파티션된 경우

원본 테이블이 이미 파티션된 경우엔, 파티션 인덱스를 구성할 때 그 방법을 바꿀 수 있다.

예를 들어, 원본 테이블과 동일한 파티션 함수와 스킴을 사용하되, 새로운 인덱스는 다른 칼럼으로 파티션될 수 있다.

CREATE TABLE t (c1 int, c2 int) ON ps (c2)

……

CREATE CLUSTERED INDEX idx_t ON t(c1) ON ps(c1)

 

직렬 계획은 다음과 같다,

    Index Insert
       |
     Sort
       |
      NL (Nested Loop)
     /    \
 CTS   Scan


Constant Table Scan 연산자에 의해 파티션 ID를 하나씩 넘겨주면, NL 연산자에 의해서 해당 파티션 데이터를 스캔하고 결과를 Sort 반복연산자에 넘겨준다. 여기서부터 원본이 정렬되지 않은 시나리오와 동일하다. 메모리와 디스크 요구 사항 또한 같다.

 

병렬 계획의 경우는,

 

X (Distribute Streams)
       |
     Index Insert
       |
     Sort
       |
       X (Repartition Streams)
       |
      NL
     /   \
   X    Scan
  /
CTS

 

CTS위에 있는 연산자가 Gather Streams 연산자이다, 그 말은 생성자 하나의 다중 소비자를 가진다는 뜻이다 (역주: Gather Stream연산자에 다중 입력 처리를 통해 병렬 처리를 수행하고 이를 단일 출력으로 제공하는 연산자이다). Gather Streams Repartition Streams 연산자 사이에, 원본 파티션 수에 병렬 처리 수를 나눈 만큼의 작업자가 할당된다. 원본은 한 번만 스캔한다.

Repartition Streams 연산자는 쿼리 계획을 두 개의 병렬 처리 영역으로 분리한다. 최상위에 있는 Distribute Streams 연산자와 Repartition Streams 연산자 사이에서는 앞서 Repartition Streams 아래에서 만들어진 작업자 집합과는 또 다른 집합을 가진다. 각 작업자가 대상 파티션에 할당되는데 그 수는 대상 인덱스 파티션 수를 병렬 처리 수로 나눈 만큼 할당된다. 나머지 정렬 및 인덱스 구성 작업은 이전의 원본이 파티션되지 않은 경우의 병렬 처리와 동일하며, 메모리 및 디스크 공간 또한 동일하다.

 

 

여기까지입니다.

계속 살펴봐 주신 분들에게 감사드립니다.

더불어 원본에서 다음 문서가 또 올라오면 이어서 계속 소개해 드리도록 하겠습니다.

 

그럼, 일단 이 시리즈 문서는 접구요,

다음에 또 다른 재미있는 자료를 번역해서 공유하도록 하겠습니다.

 

행복한 하루 되세요~~~


반응형
반응형

김정선의 좋은 글을 찾아서……
SQL Server 인덱스 구성 전략(시리즈-3. 정렬된 파티션 인덱스)

 

 

김정선(jskim@feelanet.com)

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

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

 

Microsoft SQL Server MVP

MCT/MCITP/MCDBA

 

 

Part 3: 오프라인, 직렬/병렬 파티셔닝(정렬된 파티션 인덱스 구성)

파티션 인덱스 구성에는 2가지 주요 범주가 있다:

-       정렬된(Aligned): 해당 개체(테이블)과 인덱스가 동일 파티션 스킴(scheme)을 사용하는 경우
(
역주: 본문에 schema로 적고 있다, 오타일까? 의도적인 것일까? ^^)

-       정렬되지 않은(Non-Aligned): 힙과 인덱스가 서로 다른 파티션 스킴을 사용한 경우

 

정렬된 파티션에 직렬 인덱스 구성

 

NL

                /       \

             CTS   Builder (write data to the in-build index)

                           \

                        [Sort] (order by index key) <-- optional

                             \

                          Scan (read data from source)

 

CTS: Constant Table Scan(이는 인덱스 구성자(builder)에게 파티션 ID를 제공하는 역할)

NL: Nested Loop

 

정렬된 파티션 인덱스를 구성하는 경우엔Constant Table Scan이 각각의 파티션 ID를 제공하고 이를 이용해 한 번에 하나의 파티션을 대상으로 인덱스 구성을 작업을 수행하며 Nested Loop 통해 이러한 작업을 반복 수행하게 된다. 각 정렬 테이블은 한 번에 하나씩 생성되어 처리되고 최종 b-tree 구성도 각 파티션 별로 하나씩 구성하므로 모든 파티션에 대해 정렬 테이블을 유지할 필요가 없다. 결국 한 번에 하나의 정렬 테이블만 있으면 된다.

 

이것이 필요한 디스크 공간에 미치는 영향은:

-       사용자 데이터베이스에서 정렬하는 경우(기본값) 각 파티션 별 해당 파일 그룹에서 정렬한다. 각 파일 그룹별로 2.2 x (파티션 크기) 만큼이 필요한 것이다. 예를 들어, 파일 그룹 FG1, FG2, FG3 3개의 파티션을 가지며 각 인덱스는 1GB, 2GB, 3GB를 소비한다면. 이 경우 FG1 2.2 x 1 = 2.2GB, FG2 2.2 x 2 = 4.4GB 그리고 FG3 2.2 x 3 = 6.6GB의 공간을 요구하는 것이다.

-       SORT_IN_TEMPDB = ON 인덱스옵션을 사용해서, tempdb를 정렬 공간으로 사용하는 경우 정렬 테이블에 대해 tempdb의 동일 공간을 재사용할 수 있게 된다. 한 번에 하나씩 파티션을 정렬하므로 실제론 2.2 x (가장 큰 파티션의 크기)만큼만 필요하게 되는 것이다.

(역주: 원문에는 위 사이즈에 대한 전체 크기 결과를 언급하고 있지만, 역자의 판단으로 설명과 결과가 맞지 않아 해당 부분의 설명은 생략했습니다)

 

메모리 고려 사항

한 번에 하나의 정렬 테이블만을 가진다면, 필요 메모리 크기는 최소 40페이지이다. 따라서 전체 메모리 계산식은

전체 메모리 = 최소 필요 메모리 + 추가 메모리*

 

*추가 메모리는 행 크기 x 예상 행 수로 계산되며 쿼리 최적화 프로그램에 의해서 제공된다.

 

 

 

정렬된 파티션에 병렬 인덱스 구성

파티션 인덱스에 대한 병렬 구성은 스캔과 정렬이 병렬로 수행되며 동시 실제 동시 작업자 수에 따라 실제 동시에 필요한 정렬 테이블수가 결정된다. 파티션은 작업자에 의해서 하나씩 선택되며 한 작업자가 작업을 완료하며 미 처리된 또 다른 파티션을 가지고 처리한다. 각 작업은 0 ~ N 파티션(한 파티션을 여러 작업자 공유하지는 않는다)을 구성한다. 0이 포함된 이유는 DOP > 파티션 수의 경우, 모든 작업자에게 파티션이 하나씩 돌아가지 않을 수도 있기 때문이다. 먼저 온 놈이 임자다 ^^

 

한 파티션을 여러 작업자가 공유하지 않으므로, 가장 큰 파티션에 병목이 발생할 수 있다. 다른 작업자는 모두 작업을 완료했지만 가장 정렬 작업자는 여전히 수행하고 있는 것이다. 더불어 해당 리소스(메모리 스레드 등)는 다른 쿼리에 의해서 재사용하지도 못한다.

 

마지막 단계의 짜집기도 필요 없다. 어차피 각 파티션이 분리된 b-tree에 해당하므로.

 

이것이 필요한 디스크 공간에 미치는 영향은:

-       사용자 데이터베이스에서 정렬하는 경우 각 파일 그룹별 2.2 x (파티션 크기)만큼을 요구한다.

-       tempdb를 사용한 경우 앞서의 직렬 처리에서의 이득을 동일하게 가질 수 없다. 병렬 처리이므로 동시에 여러 정렬 테이블을 처리해야 한다. 파티션 간의 데이터 실제 분포 정보를 모르는 한 2.2 x (인덱스 전체 크기)만큼의 여유 공간을 필요로 한다.

 

메모리 고려 사항

위에서 설명한 내용에 따라, 필요 메모리는 DOP수에 의존한다. ,

 

전체 메모리 = 40 x DOP + 추가 메모리

 

추가 메모리를 직렬/병렬 계획과는 무관하다.

 

 

다음 마지막 주제는,

시리즈-4. 정렬되지 않은 파티션 인덱스 구성

입니다. 또 기다려 주세요~~~

반응형
반응형

김정선의 좋은 글을 찾아서……
SQL Server 인덱스 구성 전략(시리즈-2. 일반인덱스)

 

 

김정선(jskim@feelanet.com)

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

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

 

Microsoft SQL Server MVP

MCT/MCITP/MCDBA

 

 

 

Part 1: 오프라인, 직렬, 일반 인덱스(파티션 되지 않은)

 

Builder (write data to the in-build index)

                           |

                     Sort (order by index key)

                           |

                     Scan (read data from source)

 

 

b-tree 인덱스를 구성하기 위해서 우리는 먼저 원본 데이터를 정렬해야 한다. 작업 순서는 원본 데이터를 스캔하고, 정렬한 뒤(가능한 메모리에서*) 그리고 b-tree를 구성하는 것이다.

 

b-tree를 바로 만들지 않고 먼저 정렬을 하는가? 이론적으로는 정렬할 필요가 없다, 일반적인 DML를 사용해서 바로 인덱스 구성 작업에 데이터를 추가해도 된다, 그러나 이 경우 추가하는 데이터가 랜덤하다면 결국 b-tree 상의 적합한 리프 노드를 먼저 검색한 뒤에 입력하게 된다. b-tree 검색이 빠르긴 하지만, 최적은 아니다. 따라서 인덱스 구성 작업은 인덱스에 필요한 정렬을 사용해서 데이터를 먼저 정렬하고, 인덱스 구성 작업으로 넘겨주는 것이다, 이는 그저 b-tree상에 추가 작업만 요구된다.

 

정렬에서 인덱스 구성자 사이에 데이터를 넘기는 동안 각 익스텐트 별로 모든 행이 복사되는 즉시 해당 익스텐트를 해제한다. 이를 통해 인덱스 구성 시 이론적으로 필요한 작업 공간인 3 x 인덱스 크기(원본 + 정렬 테이블 + b-tree) 2.2 x Index Size(대략)로 줄여준다.

 

*메모리 상에서 정렬된다고 보장하지는 않는다. 메모리 정렬 여부는 가용 메모리와 실제 행 수에 따라 의존적이다. ‘메모리 상에서 정렬은 원론적으로 디스크 상에 정렬 테이블을 할당할 필요가 없으므로 빠르다. 그러나 반드시 필요한 것은 아니다. 물론 메모리 상 정렬 보다는 더 느리지만 디스크에서 데이터를 처리할 수 있다.

 

 

 

각 정렬 테이블(매우 작은 데이터일지라도)은 실행하는데 필요한 최소 40페이지(3,200KB)를 필요로 한다 (뒤에서 다루겠지만 병렬 처리의 경우 동시의 하나 이상의 정렬 테이블을 소비한다). 정렬 메모리를 계산할 때, 메모리 정렬에 필요한 충분한 메모리를 할당하려고 한다. 인덱스 생성 작업 시 최소 40페이지를 제공할 수 없을 정도로 메모리가 부족한 경우 작업은 실패할 수 있다.

 

인덱스 구성 마지막 단계는 항상 통계 정보 구성 작업이다. 적절한 통계 정보는 쿼리 최적화 프로그램(Optimizer)이 더 좋은 쿼리 계획을 산출하는데 도움을 준다, 사용자가 직접 ‘create’ ‘update’ 통계 명령을 이용해서 SQL Server가 특정 개체에 대한 통계 정보를 생성하거나 갱신하도록 강제할 수도 있다. 그러나 새로운 인덱스를 생성할 때, 모든 행을 처리하게 되므로 결국 이 때가 전체 데이터(100%)의 통계 정보를 구성할 수 있는 절호의 기회가 되는 것이다.

 

정리:

직렬 계획과 오프라인으로 일반 인덱스를 구성하기 위해서는 대략 2.2 x 인덱스 크기에 해당하는 디스크 공간과 쿼리 실행자가 절차를 시작하기 위해 필요한 최소 40페이지의 메모리를 요구한다.

 

 

Part 2: 오프라인, 병렬, 일반 인덱스(파티션 되지 않은)

병렬 인덱스 구성 방식은 필요한 통계 정보를 가진 히스토그램이 있는지 없는지에 따라서 달라진다. , 병렬 인덱스 계획은 크게 두 가지 범주를 가진다:

-       히스토그램이 가용

-       히스토그램이 가용하지 않음

 

히스토그램이 가용한 경우(병렬 정렬 및 구성 작업)

 

              X (Exchange)

   |          \            \

         Builder… Build…  Build… (write data to the in-build index)

                           |           |            |

                      Sort…      Sort…  Sort … (order by index key)

                           |          /            /

                       Scan (read data from source)

 

통계 정보가 가용한 경우 병렬 인덱스 구성이 가능하다(파티션 범위 정보를 사용할 수 있고 데이터의 분포를 결정하는데 사용할 수 있기 때문).

 

이 경우 어떻게 데이터를 스캔할까? 첫 번째 키 칼럼에 대한 통계 정보가 필요하다, 따라서 해당 통계 정보가 없으면 샘플링 기반의 통계 정보를 생성하고 이를 통해 병렬 여부와 병렬 처리 방법을 결정하는데 사용한다. 만일 인덱싱 뷰(통계 계획이 없는)와 같이 샘플링 통계 정보를 구성할 수 없는 경우엔, 다른 인덱스 구성 계획이 생성된다. 통계 정보와 히스토그램을 사용해서 데이터 분포를 결정할 수 있다, 이를 통해 병렬 계획에서 적절한 작업 부하 분산을 결정할 수 있으며, 또한 시스템 자원에 대한 고도의 활용률을 위해 병렬 처리 수(DOP, Degree Of Parallelism)를 결정하는데 도움을 주게 된다. 히스토그램으로부터 데이터 분포 상의 각 버킷(bucket)에 대한 행 수를 예상함으로써, N(N = DOP)개의 범위로 작업 부하를 분리하며, 각 작업자(worker)당 하나씩의 범위를 처리하게 된다.

 

데이터를 스캔하기 위해 범위 파티션 스캔을 사용하며, 각 작업자는 해당 범위에 데이터를 이용해서 자신만의 정렬 테이블를 구성하고 그 데이터에 기반한 b-tree를 만들게 된다. 각 작업자는 결국 분리된 개별 정렬 테이블과 b-tree를 가지는 것이다. 이후 인덱스 구성 작업의 마지막 단계에서 이들을 조정하는 담당 스레드(thread)에 의해서 모두 짜집기를 하게 되며, 마지막으로 완성된 b-tree에 대해 전체 데이터 통계정보 구성을 완료하게 된다.

 

히스토그램을 사용한 병렬 인덱스 구성은 최적의 성능을 제공한다. 반면에 이로 이한 문제는 앞서 다루었던 대로 더 많은 메모리를 소비한다는 것이며 만일 충분한 가용 메모리가 없는 경우 작업이 실패할 수도 있다(각 작업자당 개별 정렬 테이블을 만들게 되므로). 필요 시 MAXOP) 옵션을 사용해서 이를 적절히 조정할 수 있다.

 

For example:

Create index idx_t on t(c1, c2)

WITH (MAXDOP = 2)

 

 

 

히스토그램이 가용하지 않은 경우

 

Build (serial) (write data to the in-build index)

                          |

                X (Merge exchange)

                           /          |           \

                      Sort…      Sort…  Sort …(order by index key)

                           |           |            |

                       Scan…    Scan… Scan…(read data from source)   

 

히스토그램을 사용할 수 없다면(예를 들어 뷰에 인덱스를 만드는 경우) 이전 글에서 다루었던 방법을 사용할 수 없다, 따라서 데이터 분포와는 무관하게 일반적인 병렬 스캔을 사용한다.

 

동작 방식

원본 데이터는 병렬로 스캔한다. 그러나 b-tree 구성은 직렬 작업이다. 병렬 처리를 수행하는 각 작업자를 이전에 병렬 스캔 방법과 같이 동일한 방법으로 힙으로부터 특정 페이지를 스캔한다. 스캔 후는 각 작업자 별로 정렬 테이블을 가지고 데이터를 구성하며, 나중에 병합(Merge)을 통해서 데이터를 결합시키게 된다(이전과 같은 개별 b-tree 구조와 짜집기 방식은 사용하지 못한다). 최종 정렬 데이터가 만들어지면 이를 통해 직렬로 인덱스 구성 작업을 수행하게 된다. 왜 이 계획이 상대적으로 느린 걸까? 이는 직렬로 수행되는 인덱스 구성작업과 ‘Merge exchange’에 의해서 발생하는 추가 오버헤드 때문이다.

 

메모리 고려 사항

병렬 인덱스 구성은 동시에 여러 정렬 테이블을 구성하므로 기본 메모리 요구가 더 크며 계산식 또한 약간 달라진다. 메모리 계산식은 1) 필요 메모리, 2) 추가 메모리를 가진다. 필요 메모리를 각 정렬 당 40페이지를 요구했다. 그런데 예를 들어 DOP = 2라고 한다면, 2개의 정렬 테이블에 대해 총 80페이지가 필요 메모리가 되는 것이다. 그러나 추가 메모리는 DOP 설정과 무관하게 동일하다. 이는 전체 행 수가 DOP 설정과 무관하게 동일한 값이기 때문이다. 예를 들어 직렬 계획으로 추가 메모리 500페이지가 필요하다면 병렬 계획도 동일한 요구를 가지는 것이다. 각 작업자는 500/DOP 페이지 만큼의 추가 메모리에 + 40 페이지 필요 메모리를 가질 것이다.

 

다음 주제는,

시리즈-3. 정렬된(Aligned) 파티션(Partitioned) 인덱스

입니다. 또 기다려 주세요~~~

반응형
반응형

김정선의 좋은 글을 찾아서……
SQL Server 인덱스 구성 전략(시리즈-1. 소개)

 

 

김정선(jskim@feelanet.com)

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

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

 

Microsoft SQL Server MVP

MCT/MCITP/MCDBA

 

 

저자: SQL Server Query Processing Team 블로그

원본: 원본 아래와 같이 9개의 포스트입니다만, 번역 주제별 나누어 4개의 포스트 올리겠습니다.

 

( 번째-소개)

http://blogs.msdn.com/sqlqueryprocessing/archive/2006/11/08/index-build-strategy-in-sql-server-introduction-i.aspx

http://blogs.msdn.com/sqlqueryprocessing/archive/2006/11/09/index-build-strategy-in-sql-server-introduction-ii.aspx

 

(두 번째-일반 인덱스)

http://blogs.msdn.com/sqlqueryprocessing/archive/2006/11/20/index-build-strategy-in-sql-server-part-1-offline-serial-no-partitioning.aspx

http://blogs.msdn.com/sqlqueryprocessing/archive/2006/12/11/index-build-strategy-in-sql-server-part-2-offline-parallel-no-partitioning.aspx

http://blogs.msdn.com/sqlqueryprocessing/archive/2006/12/13/index-build-strategy-in-sql-server-part-2-offline-parallel-no-partitioning-non-stats-plan-no-histogram.aspx

 

(세 번째-정렬된 파티션 인덱스)

http://blogs.msdn.com/sqlqueryprocessing/archive/2007/01/16/index-build-strategy-in-sql-server-part-3-offline-serial-parallel-partitioning.aspx

http://blogs.msdn.com/sqlqueryprocessing/archive/2007/01/19/index-build-strategy-in-sql-server-part-3-offline-serial-parallel-partitioning-aligned-partitioned-parallel-index-build.aspx

 

(네 번째-정렬되지 않은 파티션 인덱스)

http://blogs.msdn.com/sqlqueryprocessing/archive/2007/05/08/index-build-strategy-in-sql-server-part-4-1-offline-serial-parallel-partitioning-non-aligned-partitioned-parallel-index-build.aspx

http://blogs.msdn.com/sqlqueryprocessing/archive/2007/05/13/index-build-strategy-in-sql-server-part-4-2-offline-serial-parallel-partitioning-non-aligned-partitioned-index-build.aspx

 

 

 

 

역자 서문

이번엔, Query Processing Team 블로그에 올라온 포스트입니다. 역시 좀 지난 글이긴 하지만 내용이 너무 좋습니다. 에전에 한 번 제목만 보고 스~윽 지나쳤던 글인데, 다시 보니 다른 곳에서는 보기 힘든 유용한 내용들로 소개되고 있었습니다.

 

사실 저로서도 조금 힘들만큼 어려운 내용들을 설명하고 있습니다. 모호한 내용도 있는 것 같습니다. 직접적으로 확인하기 어려운 내용이기도 하구요. SQL Server 인덱스에 대해서 자세히 모른다면 이 내용을 읽고 이해하기가 쉽지는 않을 것입니다

 

그런데 왜 이 포스트 시리즈를 선택했냐구요?

SQL Server가 인덱스를 어떻게 만드는지? 병렬 처리를 어떻게 하는지? 얼만큼의 메모리와 디스크를 요구하는지? 파티션 테이블과 인덱스는 또 어떻게 처리되는지? 정렬된 파티션 인덱스와 그렇치 않은 경우에는 또 어떻게 달라지는지? 그래서 어떻게 작업하는 것이 상황 별로 가장 좋을지에 대한 중요한 힌트를 얻을 수는 내용들이 포함되어 있습니다.

 

그 결론만 참고하셔도 실무에서 아주 유용하게 활용될 수 있는 내용입니다.

 

앞에서 분류된 대로, 9개의 포스트를 주제별로 묶어서 4개의 글로 올리겠습니다.

 

시리즈-1. 소개

시리즈-2. 일반 인덱스

시리즈-3. 정렬된(Aligned) 파티션 인덱스

시리즈-4. 정렬되지(Non-aligned) 않은 파티션 인덱스

 

그럼, 우선 소개 부분부터 올립니다.

 

 

소개(I)

SQL Server의 인덱스 구성 전략은 사용자의 요구에 따라 다양하다. 그 전략에 따라 서로 다른 메모리와 디스크 공간을 요구한다. 이러한 전략들에 대해서 살펴볼 것이다.

 

우선 SQL Server 2005의 어떤 종류의 인덱스 구성 방식이 있는 보자.

 

 

온라인 인덱스 구성 vs. 오프라인 인덱스 구성

SQL Server 2005에서는 온라인으로 인덱스의 만들기, 재구성, 삭제 작업이 가능하다. ONLINE 옵션은 이러한 인덱스 작업을 수행하는 동안에도 사용자가 테이블이나 클러스터형(Clustered) 인덱스와 관련된 비클러스터형(Nonclustered) 인덱스에 접근하는 것이 허용한다. 오프라인으로 클러스터형 인덱스를 구성하거나 재구성하는 등의 DDL 작업을 하면 이는 해당 데이터와 관련 인덱스에 배타적 잠금을 보유하게 되고 이로 인해 다른 사용자가 데이터나 인덱스에 접근하지 못하도록 방해하게 된다.

 

Example:

Create index idx_t ON t(c1, c2)

WITH (ONLINE = ON)

 

 

직렬 인덱스 구성 vs. 병렬 인덱스 구성

다중 프로세서를 가진 컴퓨터에서 인덱스 구문 또한 다른 쿼리를 실행할 때처럼, 스캔, 정렬, 그리고 구성 작업을 수행하는데 병렬 처리가 가능하다. 병렬 처리 수는 최대 병렬 처리 수(sp_configure로 설정한), MAXDOP 인덱스 옵션, 현재 작업부하의 크기, 파티션되지 않은 경우, 첫 번째 키 칼럼의 데이터 분포등에 의해서 결정될 수 있다.

 

Example:

Create index idx_t ON t(c1, c2)

WITH (MAXDOP = 2)

-- 인덱스 구성에 2개의 프로세서 사용

 

 

사용자 데이터베이스 사용 vs. tempdb 사용

인덱스 구성/재구성 작업 시 일반적으로 중간 단계에서 생성되는 정렬 결과를 저장하기 위해서 해당 사용자 데이터베이스를 사용하거나 tempdb 데이터베이스를 사용할 수 있다. 후자의 경우 SORT_IN_TEMPDB 인덱스 옵션으로 지정할 수 있다. 디폴트로 OFF로 설정되면, 정렬 결과는 해당 파일 그룹이나 파티션 스킴(scheme)에 저장된다.

(역주: 뒤에서 논의하겠지만, 직렬/병렬, 파티션/일반 테이블 등등의 따라서 이 옵션의 사용으로 인한 영향력은 달라지게 된다. 따라서 관심 있는 살펴볼 필요가 있는 옵션이다)

 

Example:

Create clustered index idx_t ON t(c1)

WITH (SORT_IN_TEMPDB = ON)

 

 

 

소개(II)

 

파티션(Partitioned) 인덱스 구성 vs. 일반 인덱스(Nonpartitioned) 구성

파티션 테이블과 인덱스는 데이터를 하나 이상의 파일 그룹으로 분리시켜 저장한다. 데이터가 수평 분할이 되므로, 행 그룹 단위로 개별 파티션(분할)에 놓이게 된다. 해당 테이블과 인덱스 데이터에 대해 쿼리나 수정이 일어날 때는 하나의 테이블로서 다루어진다. 단일 인덱스와 테이블에 모든 파티션은 동일 데이터베이스에 존재해야 한다.

 

정렬된 파티션 인덱스 구성하기:

파티션 인덱스가 종속된 해당 테이블과는 별도로 구현될 수 있지만, 일반적으로는 파티션 테이블을 설계하고 나서 그 테이블에 인덱스를 생성하는 것이 적합하다. 이 경우 SQL Server는 인덱스를 구성할 때 자동적으로 해당 테이블과 동일한 파티션 스킴과 파티션 열을 사용해서 파티션 인덱스로 만들어준다. 그 결과 테이블과 동일한 방식의 파티션 되는 것이다. 이것이 테이블과 인덱스가 정렬(Aligned, 혹은 맞춤)되는 것이다.

 

인덱스가 반드시 해당 테이블과 동일한 이름의 파티션 함수를 사용할 필요는 없다. 그러나, 다음 항목들은 동일해야 한다

-       파티션 함수의 인수는 동일 데이터 형식

-       동일 파티션 수

-       동일한 파티션 경계 값

 

또한 파티션 테이블 혹은 클러스터형 인덱스 상에서 비클러스터형 인덱스를 구성하면 파티션 함수를 지정하지 않아도 정렬된 파티션으로 구성된다.

 

Example:

Create Partition Function pf (int)

as range right for values (NULL,  1,  100)

                   

Create Partition Scheme ps

as Partition pf

TO ([PRIMARY], [FileGroup1], [FileGroup1], [FileGroup1])

                   

Create table t (c1 int, c2 int)

on ps(c1)

                   

Create Index idx_t on t(c1)

 

 

일반 인덱스(파티션되지 않은) 구성:

인덱스 생성 시 다른 파티션 스킴이나 개별 파일 그룹을 지정하면 테이블에 정렬되지 않은 인덱스로 만들어진다. 일반 테이블에 파티션 클러스터형 인덱스를 만들어서 파티션 테이블로 바꿀 수도 있다. 물론 이 경우 정렬 인덱스를 아니다.

 

Example:

Create Partition Function pf (int)

as range right for values (NULL,  1,  100)

                   

Create Partition Scheme ps

as Partition pf

TO ([PRIMARY], [FileGroup1], [FileGroup1], [FileGroup1])

                   

Create table t (c1 int, c2 int)

                   

Create clustered Index idx_t on t(c1)

on ps(c1)

 

참고. 기존 파티션 클러스터형 인덱스를 삭제하는 경우, 새로운 Heap 테이블은 그대로 파티션 상태로 남게되는 것이며 동일 파티션 스킴이나 파일 그룹에 위치하는 것이다. 이 경우 원한다면 MOVE TO 옵션을 사용해서 그 위치를 조정할 수 있다.

 

Example:

Drop Index idx_t on t

WITH(MOVE TO new_ps(c1))

 

----------------------------------------------------

여기까지입니다.

소개 부분이라 특별한 이슈는 보이지 않으시죠? 준비 운동 정도로 생각하셔도 될 듯.

 

다음 주제는,

시리즈-2. 일반 인덱스

입니다. 기다려 주세요~~~

반응형
반응형
use master

go


if object_id('sp_lock2') is not null
 drop proc sp_lock2
go

 


create proc sp_lock2
as

set nocount on
set transaction isolation level read uncommitted

-- CTRL - T 모드로 변경 하세요
-- 도구 > 옵션 > 결과 텍스트 > 글꼴 > 굴림체
-- 마스터 에서 돌리세요
-- 최초 김민석
-- SQL Server MVP 2006~2009
-- by minsouk@hotmail.com
-- 수정 하만철
-- 20100624 세션정보 추가 김민석
-- 20110826 세션정보 수정 김민석


/**** object view 생성을 위로 올렸습니다~!! ****/


if object_id('dbo.v_objlist') is not null
drop view v_objlist

declare @viewheader varchar(8000), @viewbody varchar(8000)
select @viewheader ='' , @viewbody =''
if object_id('v_objlist') is not null
drop view v_objlist
set @viewheader = 'create view dbo.v_objlist as '
select
@viewbody = @viewbody + 'union all select db_id('''+quotename(name)+''') dbid
 , name collate database_default name
, id
 from '+quotename(name)+'.dbo.sysobjects '+char(13)+char(10)
from master.dbo.sysdatabases
where dbid > 4
select @viewbody = stuff(@viewbody, 1,10, '')
exec (@viewheader + @viewbody)

 

print N'######################################################################'
print N'세션정보'
print N'######################################################################'

DECLARE @VERSION INT
SELECT @VERSION = SUBSTRING(CAST(SERVERPROPERTY('PRODUCTVERSION') AS VARCHAR(100))
 , 1, CHARINDEX('.',CAST(SERVERPROPERTY('PRODUCTVERSION') AS VARCHAR(100)))-1)

 

IF @VERSION >= 9 BEGIN
SELECT SESSION_ID
 , CASE TRANSACTION_ISOLATION_LEVEL
WHEN 0 THEN '지정되지 않음'
WHEN 1 THEN '커밋되지 않은 읽기'
WHEN 2 THEN '커밋된 읽기'
WHEN 3 THEN '##반복 읽기##'
WHEN 4 THEN '@@직렬화 가능@@'
WHEN 5 THEN 'XX스냅숏XX' ELSE '?' END
, *
FROM SYS.DM_EXEC_SESSIONS
 WHERE SESSION_ID > 50
END


print N'######################################################################'
print N'락인포 어뷰징 확인 200개 ver 0.1'
print N'######################################################################'

select top 200
 rsc_text
 , count(*) cnt
 , case req_status
when 1 then N'허가됨'
when 2 then N'변환중'
when 3 then N'대기중'
 end req_status
 , max(left(db_name(rsc_dbid),30)+case when len(c.name) > 30 then '...' else '' end) dbname
, max(left(c.name,30)+case when len(c.name) > 30 then '...' else '' end) objname
, max(rsc_indid) IndId
 , max(case rsc_type
when 1 then null
 when 2 then 'DB'
 when 3 then 'File'
 when 4 then 'Index'
 when 5 then 'Table'
 when 6 then 'Page'
 when 7 then 'Key'
 when 8 then 'Extent'
 when 9 then 'RID'
 when 10 then 'App'
 end) Type
 , max(case req_mode --(0,3,6,7,8,9)
 when 0 then null
 when 1 then N'Sch-S:스키마 안전성'
when 2 then N'Sch-M:스키마 수정'
when 3 then N'S:공유'
when 4 then N'U:업데이트'
when 5 then N'X:단독'
when 6 then N'IS:내재 공유'
when 7 then N'IU:내재 업데이트'
when 8 then N'IX:내재 단독'
when 9 then N'SIU:공유 내재 업데이트'
when 10 then N'SIX:공유 내재 단독'
when 11 then N'UIX:업데이트 내재 단독'
when 12 then N'BU:대량 작업'
when 13 then N'RangeS_S:공유 키 범위 및 공유 리소스'
when 14 then N'RangeS_U:공유 키 범위 및 업데이트 리소스'
when 15 then N'RangeI_N:삽입 키 범위 및 Null 리소스'
when 16 then N'RangeI_S:RangeI_N 및 S 잠금의 겹침으로 만들어진 키 범위 변환'
when 17 then N'RangeI_U:RangeI_N 및 U 잠금의 겹침으로 만들어진 키 범위 변환'
when 18 then N'RangeI_X:RangeI_N 및 X 잠금의 겹침으로 만들어진 키 범위 변환'
when 19 then N'RangeX_S:RangeI_N 및 RangeS_S. 잠금의 겹침으로 만들어진 키 범위 변환'
when 20 then N'RangeX_U:RangeI_N 및 RangeS_U 잠금의 겹침으로 만들어진 키 범위 변환'
when 21 then N'RangeX_X:단독 키 범위 및 단독 리소스'
 end) Mode
 , max(case req_ownertype
when 1 then N'트랜잭션'
when 2 then N'커서'
when 3 then N'세션'
when 4 then N'ExSession'
 end) req_ownertype
from
master.dbo.syslockinfo a with (nolock)
 left join master.dbo.v_objlist c with (nolock)
 on c.dbid = a.rsc_dbid
 and c.id = a.rsc_objid
where
req_spid <> @@spid
-- and req_status = 1
 and rsc_type <> 2
group by req_status, rsc_text
order by req_status, count(*) desc


/**** N' 추가했습니다~!! ****/
print N'######################################################################'
print N'헤드블럭만 보기 by minsouk@hotmail.com ver 0.1'
print N'######################################################################'

select *
from master.dbo.sysprocesses
where blocked = 0
and spid in (select blocked from master.dbo.sysprocesses where blocked <> 0)

print N'######################################################################'
print N'헤드블럭 쿼리보기 by minsouk@hotmail.com ver 0.1'
print N'######################################################################'

 

/**** adhoc 경우 dbid, objectid 가 null 이라 dbname 보여주기위해 dbid 추가 했습니다!! ****/
declare cur_headblock cursor fast_forward
for
select spid, sql_handle, dbid
 from master.dbo.sysprocesses
where blocked = 0
and spid in (select blocked from master.dbo.sysprocesses where blocked <> 0)
declare @spid varchar(6)
declare @dbid int
declare @handle varbinary(64);
open cur_headblock
fetch next from cur_headblock into @spid, @handle, @dbid
while (@@fetch_status != -1)
begin
 print '#########################'
 print 'dbcc inputbuffer for spid ' + @spid
print '#########################'

 /***** adhoc, proc 구분하고 objname 보게 바꿔봤습니다~!! ****/
 select case when fn.dbid is null then 'AdHoc' else 'Proc' end as qry_type, db_name(@dbid)
dbname, vo.name as objname, [text]
from ::fn_get_sql(@handle) fn
 left outer join v_objlist vo on fn.dbid = vo.dbid and fn.objectid = vo.id
 exec ('dbcc inputbuffer (' + @spid + ')')
 fetch next from cur_headblock into @spid, @handle, @dbid
end
deallocate cur_headblock

print N'######################################################################'
print N'락트리 보기 by minsouk@hotmail.com ver 0.2'
print N'######################################################################'

if object_id ('tempdb..#tbl_sysprocesses') is not null
 drop table #tbl_sysprocesses

create table #tbl_sysprocesses
(
depth int
 , tree varchar(7000)
 , spid int
 , blocked int
 --, sql_handle varbinary(64)
)

insert into #tbl_sysprocesses (depth, tree, spid, blocked)
select 0, cast(spid as varchar(100)) spid , spid, blocked
from master.dbo.sysprocesses
where blocked = 0
and spid in (select blocked from master.dbo.sysprocesses where blocked <> 0)

declare @max_depth int
set @max_depth = 5

while (1=1)
begin
insert into #tbl_sysprocesses (depth, tree, spid, blocked)
select a.depth + 1 depth , a.tree + ' > ' +cast(b.spid as varchar(8000)) tree , b.spid, b.blocked
 from #tbl_sysprocesses a
 inner join master.dbo.sysprocesses b
 on a.spid = b.blocked
 where depth in (select max(depth) from #tbl_sysprocesses)
 and b.spid <> b.blocked
 if @@rowcount = 0 break
 set @max_depth = @max_depth - 1
 if @max_depth <= 1 break
end

declare @cnt varchar(10)
select @cnt = cast(cnt as varchar(10)) from ( select count(*) cnt from sysprocesses where blocked <> 0 ) a

print N'######################################################################'
print N'블럭카운트 : '+@cnt
print N'######################################################################'

select convert(char(10), cast((b.waittime / 1000) * 1.1574074074074073E-5 as datetime) , 108) as[hh:mm:ss]
 , left(a.tree, 40)+case when len(a.tree) > 40 then '...' else '' end locktree, b.*
from #tbl_sysprocesses a
 inner join master.dbo.sysprocesses b
 on a.spid = b.spid
order by tree


print N'######################################################################'
print N'######################################################################'
print N'######################################################################'
print N'락인포 보기 by minsouk@hotmail.com ver 0.5'
print N'######################################################################'
print N'######################################################################'
print N'######################################################################'
print N''

/*
if object_id ('dbo.usp_create_v_objlist') is not null
drop proc dbo.usp_create_v_objlist
*/

--exec dbo.usp_create_v_objlist

--set rowcount 200

print N'######################################################################'
print N'락인포 허가 200개 exclude rsc_type db by minsouk@hotmail.com ver 0.6'
print N'######################################################################'

select top 200
 req_spid spid
 , left(db_name(rsc_dbid),30)+case when len(c.name) > 30 then '...' else '' end dbname
, left(c.name,30)+case when len(c.name) > 30 then '...' else '' end objname
, rsc_indid IndId
 , case rsc_type
when 1 then null
 when 2 then 'DB'
 when 3 then 'File'
 when 4 then 'Index'
 when 5 then 'Table'
 when 6 then 'Page'
 when 7 then 'Key'
 when 8 then 'Extent'
 when 9 then 'RID'
 when 10 then 'App'
 end Type
 , rsc_type
 , rsc_text
 , case req_mode --(0,3,6,7,8,9)
 when 0 then null
 when 1 then N'Sch-S:스키마 안전성'
when 2 then N'Sch-M:스키마 수정'
when 3 then N'S:공유'
when 4 then N'U:업데이트'
when 5 then N'X:단독'
when 6 then N'IS:내재 공유'
when 7 then N'IU:내재 업데이트'
when 8 then N'IX:내재 단독'
when 9 then N'SIU:공유 내재 업데이트'
when 10 then N'SIX:공유 내재 단독'
when 11 then N'UIX:업데이트 내재 단독'
when 12 then N'BU:대량 작업'
when 13 then N'RangeS_S:공유 키 범위 및 공유 리소스'
when 14 then N'RangeS_U:공유 키 범위 및 업데이트 리소스'
when 15 then N'RangeI_N:삽입 키 범위 및 Null 리소스'
when 16 then N'RangeI_S:RangeI_N 및 S 잠금의 겹침으로 만들어진 키 범위 변환'
when 17 then N'RangeI_U:RangeI_N 및 U 잠금의 겹침으로 만들어진 키 범위 변환'
when 18 then N'RangeI_X:RangeI_N 및 X 잠금의 겹침으로 만들어진 키 범위 변환'
when 19 then N'RangeX_S:RangeI_N 및 RangeS_S. 잠금의 겹침으로 만들어진 키 범위 변환'
when 20 then N'RangeX_U:RangeI_N 및 RangeS_U 잠금의 겹침으로 만들어진 키 범위 변환'
when 21 then N'RangeX_X:단독 키 범위 및 단독 리소스'
 end Mode
 , req_mode
 , case req_status
when 1 then N'허가됨'
when 2 then N'변환중'
when 3 then N'대기중'
 end req_status
 , req_refcnt
, req_lifetime
, req_ecid [req_ecid (isParallel)]
 , case req_ownertype
when 1 then N'트랜잭션'
when 2 then N'커서'
when 3 then N'세션'
when 4 then N'ExSession'
 end req_ownertype
 , req_transactionID
 , req_transactionUOW [req_transactionUOW (isDTC)]
from
master.dbo.syslockinfo a with (nolock)
 left join master.dbo.v_objlist c with (nolock)
 on c.dbid = a.rsc_dbid
 and c.id = a.rsc_objid
where
req_spid <> @@spid
 and req_status = 1
 and rsc_type <> 2
order by
spid -- 정렬

print N'######################################################################'
print N'락인포 변환 200개 by minsouk@hotmail.com ver 0.5'
print N'######################################################################'

select top 200
 req_spid spid
 , left(db_name(rsc_dbid),30)+case when len(c.name) > 30 then '...' else '' end dbname
, left(c.name,30)+case when len(c.name) > 30 then '...' else '' end objname
, rsc_indid IndId
 , case rsc_type
when 1 then null
 when 2 then 'DB'
 when 3 then 'File'
 when 4 then 'Index'
 when 5 then 'Table'
 when 6 then 'Page'
 when 7 then 'Key'
 when 8 then 'Extent'
 when 9 then 'RID'
 when 10 then 'App'
 end Type
 , rsc_type
 , rsc_text
 , case req_mode --(0,3,6,7,8,9)
 when 0 then null
 when 1 then N'Sch-S:스키마 안전성'
when 2 then N'Sch-M:스키마 수정'
when 3 then N'S:공유'
when 4 then N'U:업데이트'
when 5 then N'X:단독'
when 6 then N'IS:내재 공유'
when 7 then N'IU:내재 업데이트'
when 8 then N'IX:내재 단독'
when 9 then N'SIU:공유 내재 업데이트'
when 10 then N'SIX:공유 내재 단독'
when 11 then N'UIX:업데이트 내재 단독'
when 12 then N'BU:대량 작업'
when 13 then N'RangeS_S:공유 키 범위 및 공유 리소스'
when 14 then N'RangeS_U:공유 키 범위 및 업데이트 리소스'
when 15 then N'RangeI_N:삽입 키 범위 및 Null 리소스'
when 16 then N'RangeI_S:RangeI_N 및 S 잠금의 겹침으로 만들어진 키 범위 변환'
when 17 then N'RangeI_U:RangeI_N 및 U 잠금의 겹침으로 만들어진 키 범위 변환'
when 18 then N'RangeI_X:RangeI_N 및 X 잠금의 겹침으로 만들어진 키 범위 변환'
when 19 then N'RangeX_S:RangeI_N 및 RangeS_S. 잠금의 겹침으로 만들어진 키 범위 변환'
when 20 then N'RangeX_U:RangeI_N 및 RangeS_U 잠금의 겹침으로 만들어진 키 범위 변환'
when 21 then N'RangeX_X:단독 키 범위 및 단독 리소스'
 end Mode
 , req_mode
 , case req_status
when 1 then N'허가됨'
when 2 then N'변환중'
when 3 then N'대기중'
 end req_status
 , req_refcnt
, req_lifetime
, req_ecid [req_ecid (isParallel)]
 , case req_ownertype
when 1 then N'트랜잭션'
when 2 then N'커서'
when 3 then N'세션'
when 4 then N'ExSession'
 end req_ownertype
 , req_transactionID
 , req_transactionUOW [req_transactionUOW (isDTC)]
from
master.dbo.syslockinfo a with (nolock)
 left join master.dbo.v_objlist c with (nolock)
 on c.dbid = a.rsc_dbid
 and c.id = a.rsc_objid
where
req_spid <> @@spid and req_status = 2
order by
spid -- 정렬

print N'######################################################################'
print N'락인포 대기 200개 by minsouk@hotmail.com ver 0.5'
print N'######################################################################'

select top 200
 req_spid spid
 , left(db_name(rsc_dbid),30)+case when len(c.name) > 30 then '...' else '' end dbname
, left(c.name,30)+case when len(c.name) > 30 then '...' else '' end objname
, rsc_indid IndId
 , case rsc_type
when 1 then null
 when 2 then 'DB'
 when 3 then 'File'
 when 4 then 'Index'
 when 5 then 'Table'
 when 6 then 'Page'
 when 7 then 'Key'
 when 8 then 'Extent'
 when 9 then 'RID'
 when 10 then 'App'
 end Type
 , rsc_type
 , rsc_text
 , case req_mode --(0,3,6,7,8,9)
 when 0 then null
 when 1 then N'Sch-S:스키마 안전성'
when 2 then N'Sch-M:스키마 수정'
when 3 then N'S:공유'
when 4 then N'U:업데이트'
when 5 then N'X:단독'
when 6 then N'IS:내재 공유'
when 7 then N'IU:내재 업데이트'
when 8 then N'IX:내재 단독'
when 9 then N'SIU:공유 내재 업데이트'
when 10 then N'SIX:공유 내재 단독'
when 11 then N'UIX:업데이트 내재 단독'
when 12 then N'BU:대량 작업'
when 13 then N'RangeS_S:공유 키 범위 및 공유 리소스'
when 14 then N'RangeS_U:공유 키 범위 및 업데이트 리소스'
when 15 then N'RangeI_N:삽입 키 범위 및 Null 리소스'
when 16 then N'RangeI_S:RangeI_N 및 S 잠금의 겹침으로 만들어진 키 범위 변환'
when 17 then N'RangeI_U:RangeI_N 및 U 잠금의 겹침으로 만들어진 키 범위 변환'
when 18 then N'RangeI_X:RangeI_N 및 X 잠금의 겹침으로 만들어진 키 범위 변환'
when 19 then N'RangeX_S:RangeI_N 및 RangeS_S. 잠금의 겹침으로 만들어진 키 범위 변환'
when 20 then N'RangeX_U:RangeI_N 및 RangeS_U 잠금의 겹침으로 만들어진 키 범위 변환'
when 21 then N'RangeX_X:단독 키 범위 및 단독 리소스'
 end Mode
 , req_mode
 , case req_status
when 1 then N'허가됨'
when 2 then N'변환중'
when 3 then N'대기중'
 end req_status
 , req_refcnt
, req_lifetime
, req_ecid [req_ecid (isParallel)]
 , case req_ownertype
when 1 then N'트랜잭션'
when 2 then N'커서'
when 3 then N'세션'
when 4 then N'ExSession'
 end req_ownertype
 , req_transactionID
 , req_transactionUOW [req_transactionUOW (isDTC)]
from
master.dbo.syslockinfo a with (nolock)
 left join master.dbo.v_objlist c with (nolock)
 on c.dbid = a.rsc_dbid
 and c.id = a.rsc_objid
where
req_spid <> @@spid
 and req_status = 3
order by
spid -- 정렬

set rowcount 0

print N'######################################################################'
print N'블럭되는 쿼리보기 sql_handle 별 50개 by minsouk@hotmail.com ver 0.2'
print N'######################################################################'

declare cur_blocked cursor fast_forward
for
select top 50 max(spid) spid, sql_handle, max(dbid) dbid from sysprocesses where blocked <> 0
group by sql_handle
--declare @spid varchar(6)
--declare @handle varbinary(64)
open cur_blocked
fetch next from cur_blocked into @spid, @handle, @dbid
while (@@fetch_status != -1)
begin
 print '|||||||||||||||||||||||||'
 print 'dbcc inputbuffer for spid ' + @spid
 print '|||||||||||||||||||||||||'
 select case when fn.dbid is null then 'AdHoc' else 'Proc' end as qry_type, db_name(@dbid)
dbname, vo.name as objname, [text]
from ::fn_get_sql(@handle) fn
 left outer join v_objlist vo on fn.dbid = vo.dbid and fn.objectid = vo.id
 exec ('dbcc inputbuffer (' + @spid + ')')
 fetch next from cur_blocked into @spid, @handle, @dbid
end
deallocate cur_blocked


go


exec dbo.sp_lock2

go

반응형
반응형

오랜만에 Checkpoint관련 글을 포스팅합니다.
오늘 쓰려고 하는 내용은 이전에
올린 글에서 잠깐 언급 하였습니다.

Checkpoint는 인접해 있는 페이지들을 하나의 IO로 처리 할 수 있습니다.
하나의 IO로 여러 페이지를 처리하여, 보다 효율적으로 dirty page를 물리적 디스크로 플러쉬 할 수 있습니다. SQL Server 2000 경우에는 최대 16개의 페이지 였지만 2005 경우에는 최대 32개의 페이지를 하나의 IO 처리 있습니다.

하지만 이렇게 32 페이지를 하나의 IO 플러쉬 하기 위해서는 checkpoint로 내려야 할 Dirty page가 순차적으로 존재해야 합니다. 데이터가 랜덤한 페이지상에 자주 변경되는 비즈니스라면 이러한 동작 방식이 도움을 없을것입니다. 하지만 인접해 있는 페이지가 변경 되는 경우라면 보다 적은 Writes로 많은 페이지를 플러쉬 할 수 있을 것입니다. 하지만 인접해 있는 데이터가 변하지만 페이지 조각화로 인해 위와 같이 동작 하지 못할 수도 있습니다.

그렇다면 정말 위에서 말한것 처럼 동작하는지 테스트 하였습니다.
보다 명확하게 checkpoint의 IO양과 초당 처리속도를 확인해야 하기에 추적 플래그 3502,3504 를 사용하였으며, 데이터 변경을 하여 자동 checkpoint가 발생시에 errorlog에 있는 데이터로 비교하였습니다.

USE [master]

GO

CREATE DATABASE [test] ON  PRIMARY

( NAME = N'test_Data', FILENAME = N'F:\test_Data.MDF' , SIZE = 102400KB , MAXSIZE = UNLIMITED, FILEGROWTH = 51200KB ),

 FILEGROUP [test_Data1]

( NAME = N'test_Data1_01', FILENAME = N'F:\test_Data1_01.mdf' , SIZE = 5GB , MAXSIZE = UNLIMITED, FILEGROWTH = 51200KB )

,( NAME = N'test_Data1_03', FILENAME = N'G:\test_Data1_03.mdf' , SIZE = 5GB , MAXSIZE = UNLIMITED, FILEGROWTH = 51200KB )

 LOG ON

( NAME = N'test_Log', FILENAME = N'E:\test_Log.LDF' , SIZE = 5GB , MAXSIZE = 2048GB , FILEGROWTH = 51200KB )

 

ALTER DATABASE [test] SET RECOVERY SIMPLE

 

use test

go

 

drop table tbl1

 

/*

테스트를 위해 하나의 페이지에 한행만 존재하도록 테이블을 만든다.

*/

SELECT

TOP 4000000

       ROW_NUMBER() OVER(ORDER BY(SELECT 1)) as col1

       ,cast(ROW_NUMBER() OVER(ORDER BY(SELECT 1)) as char(5000)) as col2

INTO tbl1

FROM sys.sysindexes A,sys.sysindexes A1,sys.sysindexes A2,sys.sysindexes A3,sys.sysindexes A4

 

create unique clustered index ixa on tbl1(col1)

 

--sp_spaceused tbl1

/*
             정확한 테스트를 위해 기존에 BP에 있는 메모리 날리기.
*/
checkpoint 1

dbcc dropcleanbuffers


DBCC TRACEON(3504,3502,-1) 



/*

순차적으로 데이터를 변경한다.

하나의 IO작업에 16개페이지(1572070/97851) 내리는 것을 확인 있으며

초당 280메가의 Dirty page 내리는 것을 볼수 있다.

*/

update tbl1

set col2 = newid()

where col1  < 2000000

 

2008-12-16 02:52:28.340 spid10s      Ckpt dbid 11 started (8)

2008-12-16 02:52:28.340 spid10s      About to log Checkpoint begin.

2008-12-16 02:52:28.340 spid10s      Ckpt dbid 11 phase 1 ended (8)

2008-12-16 02:53:12.060 spid10s      FlushCache: cleaned up 1572070 bufs with 97851 writes in 43719 ms (avoided 162901 new dirty bufs)

2008-12-16 02:53:12.060 spid10s                  average throughput: 280.93 MB/sec, I/O saturation: 72605

2008-12-16 02:53:12.060 spid10s                  last target outstanding: 1600

2008-12-16 02:53:12.060 spid10s      About to log Checkpoint end.

2008-12-16 02:53:12.080 spid10s      Ckpt dbid 11 complete


 

/*

랜덤하게 데이터를 변경한다.

하나의 IO작업에 1개페이지(147576/147573) 내리는 것을 확인 있으며

초당 56메가의 Dirty page 내리는 것을 볼수 있다.

*/

update tbl1

set col2 = newid()

where col1%2 = 0

 

2008-12-16 02:56:04.760 spid10s      Ckpt dbid 11 started (8)

2008-12-16 02:56:04.760 spid10s      About to log Checkpoint begin.

2008-12-16 02:56:04.760 spid10s      Ckpt dbid 11 phase 1 ended (8)

2008-12-16 02:56:25.200 spid10s      FlushCache: cleaned up 147579 bufs with 147573 writes in 20438 ms (avoided 1466 new dirty bufs)

2008-12-16 02:56:25.200 spid10s                  average throughput:  56.41 MB/sec, I/O saturation: 55153

2008-12-16 02:56:25.200 spid10s                  last target outstanding: 1600

2008-12-16 02:56:25.200 spid10s      About to log Checkpoint end.

2008-12-16 02:56:25.200 spid10s      Ckpt dbid 11 complete


위와 같은 결과를 보면, 순차적인 데이터 변경으로 발생된 dirty page가 보다 효과적으로 디스크로 플러쉬 되는 것을 확인 할 수 있습니다.


특정 테스트 환경이기에 위 테스트 결과를 절대적으로 신뢰할 수는 없습니다.

송 혁, SQL Server MVP
sqler.pe.kr // sqlleader.com
hyoksong.tistory.com

반응형

'연구개발 > DBA' 카테고리의 다른 글

SQL Server 인덱스 구성 전략(시리즈-1. 소개)  (0) 2012.02.04
sp_lock2  (0) 2012.02.02
파티션 (02. 테이블 파티션)  (0) 2012.01.28
파티션 (01. 파티션 개요)  (0) 2012.01.28
정렬의 최적화 (04. 페이징 처리)  (0) 2012.01.27
반응형

/* 02. 테이블 파티션 */

 

--(1) 파티션 테이블 생성 절차

/*

1.파일 그룹을 생성한다.(선택)

2.파일을 파일 그룹에 추가한다.(선택)

3.파티션 함수를 생성한다.(필수)

             -> 분할 방법과 경계 값을 지정한다.

4.파티션 구성표(Partition Scheme)를 생성한다.(필수)

             -> 파티션 함수에 정의된 각 파티션의 위치(=파일그룹)를 지정한다.

5.파티션 테이블을 생성한다.

             -> 분할(=파티션)하고자 하는 테이블을 파티션 구성표에 생성한다.

*/

 

-- 1) 파티션 함수(Partition Function) 생성

USE jwjung

GO

CREATE PARTITION FUNCTION pf_OrderDate (CHAR(8))

AS RANGE RIGHT

FOR VALUES (

             '1997'   /* 1996년 파티션 */

             ,'1998'  /* 1997년 파티션 */

             ,'1999' /* 1998년 파티션 */

)

GO

 

/* RIGHT 이므로

파티션 번호                                     시작 값                                           종료 값                                                                     비고

             1                                                   허용 가용한 최소 값                         < '1997'                                        1996년도 이하

             2                                                                '1997' <=                                                   < '1998'                                        1997년도

             3                                                                '1998' <=                                                   < '1999'                                        1998년도

             4                                                                '1999' <=                                      허용 가능한 최대 값                        final 파티션(자동 생성됨)

*/

 

SELECT * FROM jwjung.sys.partition_functions

GO

 

--name  function_id         type       type_desc            fanout   boundary_value_on_right create_date         modify_date

--pf_OrderDate   65536   R           RANGE  4           1           2012-01-27 23:33:00.223             2012-01-27 23:33:00.223

 

SELECT * FROM jwjung.sys.partition_range_values

GO

 

--function_id       boundary_id        parameter_id      value

--65536 1           1           1997   

--65536 2           1           1998   

--65536 3           1           1999   

 

 

SELECT CONVERT(DATETIME, '2009-12-31 23:59:59.997')

UNION ALL

SELECT CONVERT(DATETIME, '2009-12-31 23:59:59.997')

UNION ALL

SELECT CONVERT(DATETIME, '2009-12-31 23:59:59.999')

GO

--2009-12-31 23:59:59.997

--2009-12-31 23:59:59.997

--2010-01-01 00:00:00.000

 

 

-- 2) 파티션 구성표(Partition Schema) 생성

--각 파티션이 위치할 파일 그룹을 정의한 파티션 구성표를 만든다.

CREATE PARTITION SCHEME ps_OrderDate

as partition pf_OrderDate

to ([primary], [primary], [primary], [primary])

GO

 

 

--모든 파티션을 한 파일 그룹에 매핑하고자 한다면

CREATE PARTITION SCHEME ps_OrderDate

as partition pf_OrderDate

ALL to ([primary])

GO

 

SELECT * FROM jwjung.sys.partition_schemes

GO

--name  data_space_id     type       type_desc            is_default            function_id

--ps_OrderDate   65618   PS          PARTITION_SCHEME         0           65536

 

 

SELECT a.*, b.name, b.type, b.type_desc

FROM jwjung.sys.destination_data_spaces a

             , jwjung.sys.data_spaces b

WHERE a.data_space_id = b.data_space_id

GO

 

--partition_scheme_id       destination_id     data_space_id     name     type       type_desc

--65618 1           1           PRIMARY            FG         ROWS_FILEGROUP

--65618 2           1           PRIMARY            FG         ROWS_FILEGROUP

--65618 3           1           PRIMARY            FG         ROWS_FILEGROUP

--65618 4           1           PRIMARY            FG         ROWS_FILEGROUP

 

 

-- 3) 파티션 테이블 생성

USE jwjung

GO

CREATE TABLE Orders_Range (

             OrderID                                         INT         IDENTITY(1, 1) NOT NULL

             ,CustomerID                     NCHAR(5)           NULL

             ,EmployeeID                     INT         NULL

             ,OrderDate                       CHAR(8)             NULL

             ,RequiredDate     DATETIME           NULL

             ,ShippedDate      DATETIME           NULL

             ,ShipVia                            INT         NULL

             ,Freight                                         MONEY NULL

             ,ShipName                        NVARCHAR(40)   NULL

             ,ShipAddress                    NVARCHAR(60)   NULL

             ,ShipCity                                        NVARCHAR(15)   NULL

             ,ShipRegion                      NVARCHAR(15)   NULL

             ,ShipPostalCode  NVARCHAR(10)   NULL

             ,ShipCountry                    NVARCHAR(15)   NULL

)

ON ps_OrderDate (OrderDate)       /* 파티션 구성표와 파티션 컬럼을 기술한다 */

GO

 

--파티션 컬럼(=OrderDate)의 데이터 형식, 길이 및 전체 자릿수는 (해당 파티션 구성표가 사용하는) 파티션 함수에

--지정된 입력 파라미터의 것과 일치해야 한다.

 

SELECT a.name as '테이블', f.name as '파티션 함수', c.name as '파티션 구성표'

             , d.destination_id '파티션 번호', e.name as '파일 그룹'

FROM sys.tables a

             , sys.indexes b

             , sys.partition_schemes c

             , sys.destination_data_spaces d

             , sys.data_spaces e

             , sys.partition_functions f

WHERE a.name = 'Orders_Range'

             AND a.object_id = b.object_id

             AND b.index_id in (0, 1)

             AND b.data_space_id = c.data_space_id

             AND c.data_space_id = d.partition_scheme_id

             AND d.data_space_id = e.data_space_id

             AND c.function_id = f.function_id

GO

 

--테이블  파티션 함수          파티션 구성표       파티션 번호          파일 그룹

--Orders_Range   pf_OrderDate      ps_OrderDate      1           PRIMARY

--Orders_Range   pf_OrderDate      ps_OrderDate      2           PRIMARY

--Orders_Range   pf_OrderDate      ps_OrderDate      3           PRIMARY

--Orders_Range   pf_OrderDate      ps_OrderDate      4           PRIMARY

 

 

USE jwjung

GO

 

INSERT INTO Orders_Range

SELECT a.CustomerID, a.EmployeeID

             , CONVERT(CHAR(8), a.OrderDate, 112) as OrderDate

             , a.RequiredDate

             , a.ShippedDate, a.ShipVia, a.Freight, a.ShipName, a.ShipAddress

             , a.ShipCity, a.ShipRegion, a.ShipPostalCode, a.ShipCountry

FROM Northwind.dbo.Orders a

             , (SELECT TOP 100 * FROM Northwind.dbo.Orders) b

GO

 

 

SELECT substring(OrderDate, 1, 4) AS '주문연도'

             , COUNT(*) '건수', SUM(Freight) '합계'

FROM Orders_Range

GROUP BY substring(OrderDate, 1, 4)

ORDER BY 1

GO

 

--주문연도            건수       합계

--1996   15200   1027987.00

--1997   40800   3246877.00

--1998   27000   2219405.00

 

 

--파티션 번호 알아내기

SELECT $partition.pf_OrderDate(OrderDate) as '파티션 번호'

             , substring(OrderDate, 1, 4) AS '주문연도'

             , COUNT(*) '건수', SUM(Freight) '합계'

FROM Orders_Range

GROUP BY $partition.pf_OrderDate (OrderDate), substring(OrderDate, 1, 4)

ORDER BY 1

GO

--파티션 번호        주문연도 건수       합계

--1         1996     15200   1027987.00

--2         1997     40800   3246877.00

--3         1998     27000   2219405.00

 

 

--참고로, 아래처럼 $partition 함수에 특정 값을 입력하면 해당 로우가 어떤 파티션에 저장될지 미리 확인할 수 있다.

SELECT '20091231' as '입력할 값'

             , $partition.pf_OrderDate('20091231') as '파티션 번호'

GO

--입력할 값           파티션 번호

--20091231        4

 

 

 

--(2) 파티션 테이블 실행 계획

SELECT COUNT(*) as cnt, SUM(Freight) as Freight

FROM Orders_Range

WHERE OrderDate BETWEEN '19960101' AND '19961231'

GO

--(1개 행이 영향을 받음)

--테이블 'Orders_Range'. 검색 수 4, 논리적 읽기 수 1990, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

SELECT COUNT(*) as cnt, SUM(Freight) as Freight

FROM Orders_Range

WHERE OrderDate BETWEEN '19960101' AND '19971231'

GO

 

 

DECLARE @p1 CHAR(8), @p2 CHAR(8)

SET @p1 = '19960101'

SET @p2 = '19971231'

 

SELECT COUNT(*) as cnt, SUM(Freight) as Freight

FROM Orders_Range

WHERE OrderDate BETWEEN @p1 AND @p2

GO

--(1개 행이 영향을 받음)

--테이블 'Orders_Range'. 검색 수 2, 논리적 읽기 수 1340, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

DECLARE @p1 CHAR(8), @p2 CHAR(8)

SET @p1 = '19960101'

SET @p2 = '1998'

 

SELECT COUNT(*) as cnt, SUM(Freight) as Freight

FROM Orders_Range

WHERE OrderDate >= @p1

             AND OrderDate < @p2

GO

--(1개 행이 영향을 받음)

--테이블 'Orders_Range'. 검색 수 3, 논리적 읽기 수 1990, 물리적 읽기 수 26, 미리 읽기 수 1344, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

 

 

 

 

 

 

 

 

 

 

SET STATISTICS PROFILE OFF

SET STATISTICS TIME OFF

SET STATISTICS IO OFF

 

반응형

'연구개발 > DBA' 카테고리의 다른 글

sp_lock2  (0) 2012.02.02
순차 vs 랜덤 데이터 변경에 대한 CHECKPOINT 성능 비교  (0) 2012.01.30
파티션 (01. 파티션 개요)  (0) 2012.01.28
정렬의 최적화 (04. 페이징 처리)  (0) 2012.01.27
페이징  (0) 2012.01.26
반응형

/*

파티션

*/

 

/* 01. 파티션 개요

파티션은 SQL Server 2005 버전에 새로 추가된 기능이다. 테이블 및 인덱스를 정의할 때 컬럼 값 기준으로 분할함으로써

(테이블 또는 인덱스를 구성하는) 각 파티션이 물리적으로 다른 곳(=파일 그룹)에 있도록 할 수 있다.

이렇게 하면 SQL Server가 테이블의 전체 로우를 (사용자가 정의한 기준대로) 수평 분할하여 각 파티션에 저장하게 된다. */

 

 

--(1) 분할된 뷰(Partitioned View)

/* SQL Server 2000, 2005 */

USE jwjung

GO

 

SELECT * INTO Orders_1997 FROM Northwind.dbo.Orders

WHERE OrderDate >= CONVERT(DATETIME, '19970101', 112)

             AND OrderDate < CONVERT(DATETIME, '19980101', 112)

GO

--(408개 행이 영향을 받음)

SELECT * INTO Orders_1998 FROM Northwind.dbo.Orders

WHERE OrderDate >= CONVERT(DATETIME, '19980101', 112)

             AND OrderDate < CONVERT(DATETIME, '19990101', 112)

GO

--(270개 행이 영향을 받음)

 

ALTER TABLE Orders_1997 ADD CONSTRAINT Orders_1997_pk PRIMARY KEY (OrderID)

GO

ALTER TABLE Orders_1998 ADD CONSTRAINT Orders_1998_pk PRIMARY KEY (OrderID)

GO

 

/* 분할된 뷰 생성 */

CREATE VIEW Orders_v

AS

SELECT * FROM Orders_1997

UNION ALL

SELECT * FROM Orders_1998

GO

 

SET STATISTICS PROFILE ON

SET STATISTICS TIME ON

SET STATISTICS IO ON

 

SELECT * FROM Orders_v;

GO

 

 

--SQL Server 2000 버전까지는 check 제약이 필수

 

/* SQL Server 2000 */

SELECT *

FROM Orders_v

WHERE OrderDate >= CONVERT(DATETIME, '19970601', 112)

             AND OrderDate < CONVERT(DATETIME, '19970701', 112)

GO

 

--Rows   Executes             StmtText

--30       1           SELECT * FROM [Orders_v] WHERE [OrderDate]>=CONVERT([datetime],@1,(112)) AND [OrderDate]<CONVERT([datetime],@2,(112))

--30       1             |--Concatenation

--30       1                  |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[Orders_1997].[Orders_1997_pk]), WHERE:([jwjung].[dbo].[Orders_1997].[OrderDate]>='1997-06-01 00:00:00.000' AND [jwjung].[dbo].[Orders_1997].[OrderDate]<'1997-07-01 00:00:00.000'))

--0         1                  |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[Orders_1998].[Orders_1998_pk]), WHERE:([jwjung].[dbo].[Orders_1998].[OrderDate]>='1997-06-01 00:00:00.000' AND [jwjung].[dbo].[Orders_1998].[OrderDate]<'1997-07-01 00:00:00.000'))

 

--(30개 행이 영향을 받음)

--테이블 'Orders_1998'. 검색 수 1, 논리적 읽기 수 9, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Orders_1997'. 검색 수 1, 논리적 읽기 수 12, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

EXEC sp_helpindex 'Orders_1997';

GO

--index_name      index_description             index_keys

--Orders_1997_pk            clustered, unique, primary key located on PRIMARY OrderID

EXEC sp_helpindex 'Orders_1998';

GO

--index_name      index_description             index_keys

--Orders_1998_pk            clustered, unique, primary key located on PRIMARY OrderID

 

/* 1997년에 속한 데이터만 조회하도록 검색 조건을 기술했지만, Orders_1997 테이블뿐만 아니라 Orders_1998 테이블까지 전체 스캔했다.

Orders_v 뷰를 포함해 Orders_1997, Orders_1998 테이블 어디에도 데이터 분할에 대한 제약 조건이 없으므로, 옵티마이저 입장에서는 뷰 안에

정의된 모든 테이블을 액세스 할 수 밖에 없다. */

 

ALTER TABLE Orders_1997 ADD CONSTRAINT OrderDate_ck_1997 CHECK (

                                        OrderDate >= CONVERT(DATETIME, '19970101', 112)

             AND OrderDate < CONVERT(DATETIME, '19980101', 112))

GO

 

ALTER TABLE Orders_1998 ADD CONSTRAINT OrderDate_ck_1998 CHECK (

                                        OrderDate >= CONVERT(DATETIME, '19980101', 112)

             AND OrderDate < CONVERT(DATETIME, '19990101', 112))

GO

 

 

/* SQL Server 2000 */

DECLARE @p1 CHAR(8), @p2 CHAR(8)

 

SET @p1 = '19970601'

SET @p2 = '19970701'

 

SELECT *

FROM Orders_v

WHERE OrderDate >= CONVERT(DATETIME, @p1, 112)

             AND OrderDate < CONVERT(DATETIME, @p2, 112)

GO

 

--Rows   Executes             StmtText

--30       1           SELECT * FROM Orders_v WHERE OrderDate >= CONVERT(DATETIME, @p1, 112)        AND OrderDate < CONVERT(DATETIME, @p2, 112)

--30       1             |--Concatenation

--30       1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1998-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1997-01-01 00:00:00.000')))

--30       1                  |    |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[Orders_1997].[Orders_1997_pk]), WHERE:([jwjung].[dbo].[Orders_1997].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1997].[OrderDate]<CONVERT(datetime,[@p2],112)))

--0         1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1999-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1998-01-01 00:00:00.000')))

--0         0                       |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[Orders_1998].[Orders_1998_pk]), WHERE:([jwjung].[dbo].[Orders_1998].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1998].[OrderDate]<CONVERT(datetime,[@p2],112)))

 

--(30개 행이 영향을 받음)

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Orders_1997'. 검색 수 1, 논리적 읽기 수 12, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

--pk 인덱스를 사용할 수 있도록 OrderID 컬럼에 대한 검색조건을 추가하고서 테스트

/* SQL Server 2000 */

DECLARE @p1 CHAR(8), @p2 CHAR(8), @p3 INT

 

SET @p1 = '19970601'

SET @p2 = '19970701'

SET @p3 = 10584

 

SELECT *

FROM Orders_v

WHERE OrderDate >= CONVERT(DATETIME, @p1, 112)

             AND OrderDate < CONVERT(DATETIME, @p2, 112)

             AND OrderID = @p3

GO

 

--Rows   Executes             StmtText

--1         1           SELECT * FROM Orders_v WHERE OrderDate >= CONVERT(DATETIME, @p1, 112) AND OrderDate < CONVERT(DATETIME, @p2, 112) AND OrderID = @p3

--1         1             |--Concatenation

--1         1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1998-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1997-01-01 00:00:00.000')))

--1         1                  |    |--Clustered Index Seek(OBJECT:([jwjung].[dbo].[Orders_1997].[Orders_1997_pk]), SEEK:([jwjung].[dbo].[Orders_1997].[OrderID]=[@p3]),  WHERE:([jwjung].[dbo].[Orders_1997].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1997].[OrderDate]<CONVERT(datetime,[@p2],112)) ORDERED FORWARD)

--0         1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1999-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1998-01-01 00:00:00.000')))

--0         0                       |--Clustered Index Seek(OBJECT:([jwjung].[dbo].[Orders_1998].[Orders_1998_pk]), SEEK:([jwjung].[dbo].[Orders_1998].[OrderID]=[@p3]),  WHERE:([jwjung].[dbo].[Orders_1998].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1998].[OrderDate]<CONVERT(datetime,[@p2],112)) ORDERED FORWARD)

 

--(1개 행이 영향을 받음)

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Orders_1997'. 검색 수 0, 논리적 읽기 수 2, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

-- SQL Server 2005 버전부터는 뷰 정의로 대신 할 수 있음

/* 2000 버전에서는 분할된 뷰에 check 제약 조건이 필수였지만, 2005버전부터는 뷰 정의로 (check 제약의 기능을) 대신할 수 있다.

물론 이전 버전처럼 check 제약 조건을 사용해도 된다. */

 

DROP VIEW Orders_v;

GO

 

ALTER TABLE Orders_1997 DROP OrderDate_ck_1997

GO

 

ALTER TABLE Orders_1998 DROP CONSTRAINT OrderDate_ck_1998

GO

 

/* SQL Server 2005 */

CREATE VIEW Orders_v

AS

SELECT *

FROM Orders_1997

WHERE OrderDate >= CONVERT(DATETIME, '19970101', 112)

             AND OrderDate < CONVERT(DATETIME, '19980101', 112)

UNION ALL

SELECT *

FROM Orders_1998

WHERE OrderDate >= CONVERT(DATETIME, '19980101', 112)

             AND OrderDate < CONVERT(DATETIME, '19990101', 112)

GO

-- 뷰 안의 각 쿼리에 검색조건을 상수 값으로 기술하여 로우의 범위를 명시적으로 제한했다.

 

 

DECLARE @p1 CHAR(8), @p2 CHAR(8)

 

SET @p1 = '19970601'

SET @p2 = '19970701'

 

SELECT * FROM Orders_v

WHERE OrderDate >= CONVERT(DATETIME, @p1, 112)

             AND OrderDate < CONVERT(DATETIME, @p2, 112)

GO

 

--Rows   Executes             StmtText

--30       1           SELECT * FROM Orders_v WHERE OrderDate >= CONVERT(DATETIME, @p1, 112) AND OrderDate < CONVERT(DATETIME, @p2, 112)

--30       1             |--Concatenation

--30       1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1998-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1997-01-01 00:00:00.000')))

--30       1                  |    |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[Orders_1997].[Orders_1997_pk]), WHERE:([jwjung].[dbo].[Orders_1997].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1997].[OrderDate]<CONVERT(datetime,[@p2],112) AND [jwjung].[dbo].[Orders_1997].[OrderDate]>='1997-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders_1997].[OrderDate]<'1998-01-01 00:00:00.000'))

--0         1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1999-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1998-01-01 00:00:00.000')))

--0         0                       |--Table Scan(OBJECT:([jwjung].[dbo].[Orders_1998]), WHERE:([jwjung].[dbo].[Orders_1998].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1998].[OrderDate]<CONVERT(datetime,[@p2],112) AND [jwjung].[dbo].[Orders_1998].[OrderDate]>='1998-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders_1998].[OrderDate]<'1999-01-01 00:00:00.000'))

 

--(30개 행이 영향을 받음)

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Orders_1997'. 검색 수 1, 논리적 읽기 수 12, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

 

DECLARE @p1 CHAR(8), @p2 CHAR(8), @p3 INT

 

SET @p1 = '19970601'

SET @p2 = '19970701'

SET @p3 = 10584

 

SELECT *

FROM Orders_v

WHERE OrderDate >= CONVERT(DATETIME, @p1, 112)

             AND OrderDate < CONVERT(DATETIME, @p2, 112)

             AND OrderID = @p3

GO

 

--Rows   Executes             StmtText

--1         1           SELECT * FROM Orders_v WHERE OrderDate >= CONVERT(DATETIME, @p1, 112) AND OrderDate < CONVERT(DATETIME, @p2, 112) AND OrderID = @p3

--1         1             |--Concatenation

--1         1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1998-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1997-01-01 00:00:00.000')))

--1         1                  |    |--Clustered Index Seek(OBJECT:([jwjung].[dbo].[Orders_1997].[Orders_1997_pk]), SEEK:([jwjung].[dbo].[Orders_1997].[OrderID]=[@p3]),  WHERE:([jwjung].[dbo].[Orders_1997].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1997].[OrderDate]<CONVERT(datetime,[@p2],112) AND [jwjung].[dbo].[Orders_1997].[OrderDate]>='1997-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders_1997].[OrderDate]<'1998-01-01 00:00:00.000') ORDERED FORWARD)

--0         1                  |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p1],112)<'1999-01-01 00:00:00.000' AND CONVERT(datetime,[@p2],112)>'1998-01-01 00:00:00.000')))

--0         0                       |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1003]))

--0         0                            |--Filter(WHERE:(STARTUP EXPR(CONVERT(datetime,[@p2],112)>'1998-01-01 00:00:00.000' AND CONVERT(datetime,[@p1],112)<'1999-01-01 00:00:00.000')))

--0         0                            |    |--Index Seek(OBJECT:([jwjung].[dbo].[Orders_1998].[Orders_1998_pk]), SEEK:([jwjung].[dbo].[Orders_1998].[OrderID]=[@p3]) ORDERED FORWARD)

--0         0                            |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders_1998]), SEEK:([Bmk1003]=[Bmk1003]),  WHERE:([jwjung].[dbo].[Orders_1998].[OrderDate]>=CONVERT(datetime,[@p1],112) AND [jwjung].[dbo].[Orders_1998].[OrderDate]<CONVERT(datetime,[@p2],112) AND [jwjung].[dbo].[Orders_1998].[OrderDate]>='1998-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders_1998].[OrderDate]<'1999-01-01 00:00:00.000') LOOKUP ORDERED FORWARD)

 

--(1개 행이 영향을 받음)

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Orders_1997'. 검색 수 0, 논리적 읽기 수 2, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

--(2) 파티션

--SQL Server는 현재(2005, 2008 버전)까지 Range 파티션만 제공하며 파티션 키는 단일 컬럼만 가능하다.

 

 

SELECT OBJECT_NAME(object_id) as object_name, a.*

FROM Northwind.sys.partitions a

WHERE object_id > 100

ORDER BY 1, index_id

GO

 

 


반응형
반응형

/* 04. 페이지 처리 */

USE jwjung

GO

 

IF OBJECT_ID('Orders') IS NOT NULL

             DROP TABLE Orders

GO

 

SELECT TOP 0 * INTO Orders FROM Northwind.dbo.Orders

GO

 

INSERT INTO Orders

SELECT a.CustomerID, a.EmployeeID, a.OrderDate, a.RequiredDate

             , a.ShippedDate, a.ShipVia, a.Freight, a.ShipName, a.ShipAddress

             , a.ShipCity, a.ShipRegion, a.ShipPostalCode, a.ShipCountry

FROM Northwind.dbo.Orders a

             , (SELECT TOP 10 * FROM Northwind.dbo.Orders) b

GO

--8300

 

ALTER TABLE Orders ADD CONSTRAINT Orders_pk PRIMARY KEY NONCLUSTERED (OrderID)

GO

 

CREATE NONCLUSTERED INDEX Orders_x01 ON Orders (CustomerID, OrderDate, OrderID)

GO

 

EXEC sp_helpindex 'Orders';

 

SET STATISTICS PROFILE ON

SET STATISTICS TIME ON

SET STATISTICS IO ON

 

--(1) 앞쪽 페이지를 주로 조회할 때

SELECT TOP 11 *

FROM Orders

WHERE CustomerID = 'QUICK'

ORDER BY OrderDate, OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT TOP 11 * FROM Orders WHERE CustomerID = 'QUICK' ORDER BY OrderDate, OrderID

--11       1             |--Top(TOP EXPRESSION:((11)))

--11       1                  |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1005]) WITH ORDERED PREFETCH)

--216     1                       |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK') ORDERED FORWARD)

--11       11                     |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 14, 물리적 읽기 수 2, 미리 읽기 수 216, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

--두번째 페이지 (21 = 2 * 10 + 1) - 11(뒷쪽11)

SELECT TOP 11 *

FROM (

             SELECT TOP 21 *

                           , ROW_NUMBER() OVER(ORDER BY OrderDate, OrderID) AS rnum

             FROM Orders

             WHERE CustomerID = 'QUICK'

             ORDER BY OrderDate, OrderID

             ) a

WHERE a.rnum >= 11

ORDER BY a.OrderDate, a.OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT TOP 11 * FROM ( SELECT TOP 21 * , ROW_NUMBER() OVER(ORDER BY OrderDate, OrderID) AS rnum FROM Orders WHERE CustomerID = 'QUICK' ORDER BY OrderDate, OrderID        ) a WHERE a.rnum >= 11 ORDER BY a.OrderDate, a.OrderID

--11       1             |--Top(TOP EXPRESSION:((11)))

--11       1                  |--Filter(WHERE:([Expr1004]>=(11)))

--21       1                       |--Top(TOP EXPRESSION:((21)))

--21       1                            |--Sequence Project(DEFINE:([Expr1004]=row_number))

--21       1                                 |--Segment

--21       1                                      |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1006]) WITH ORDERED PREFETCH)

--21       1                                           |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK') ORDERED FORWARD)

--21       21                                         |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 23, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--SQL 2005이상

DECLARE @페이지번호 INT, @페이지별로우수 INT

 

SET @페이지번호 = 2

SET @페이지별로우수 = 10

 

SELECT TOP (@페이지별로우수 + 1) *

FROM (

             SELECT TOP (@페이지번호 * @페이지별로우수 + 1) *

                           , ROW_NUMBER() OVER (ORDER BY OrderDate, OrderID) AS rnum

             FROM Orders WITH (INDEX(Orders_x01))

             WHERE CustomerID = 'QUICK'

             ORDER BY OrderDate, OrderID

             ) a

WHERE a.rnum >= (@페이지번호 - 1) * @페이지별로우수 + 1

ORDER BY a.OrderDate, a.OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT TOP (@페이지별로우수 + 1) * FROM (           SELECT TOP (@페이지번호 * @페이지별로우수 + 1) * , ROW_NUMBER() OVER (ORDER BY OrderDate, OrderID) AS rnum FROM Orders WITH (INDEX(Orders_x01)) WHERE CustomerID = 'QUICK' ORDER BY OrderDate, OrderID) a WHERE a.rnum >= (@페이지번호 - 1) * @페이지별로우수 + 1 ORDER BY a.OrderDate, a.OrderID

--11       1             |--Top(TOP EXPRESSION:(CONVERT_IMPLICIT(bigint,[@페이지별로우수]+(1),0)))

--11       1                  |--Filter(WHERE:([Expr1004]>=CONVERT_IMPLICIT(bigint,([@페이지번호]-(1))*[@페이지별로우수]+(1),0)))

--21       1                       |--Top(TOP EXPRESSION:(CONVERT_IMPLICIT(bigint,[@페이지번호]*[@페이지별로우수]+(1),0)))

--21       1                            |--Sequence Project(DEFINE:([Expr1004]=row_number))

--21       1                                 |--Segment

--21       1                                      |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1009]) WITH ORDERED PREFETCH)

--21       1                                           |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK') ORDERED FORWARD)

--21       21                                         |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 23, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

 

--SQL Server 2000

DECLARE @페이지번호 INT, @페이지별로우수 INT

 

SET @페이지번호 = 2

SET @페이지별로우수 = 10

 

SELECT *

FROM (

             SELECT TOP (@페이지별로우수 + 1)  *

             FROM (

                           SELECT TOP (@페이지번호 * @페이지별로우수 + 1) *

                           FROM Orders WITH (INDEX(Orders_x01))

                           WHERE CustomerID = 'QUICK'

                           ORDER BY OrderDate, OrderID

                           ) a

             ORDER BY a.OrderDate DESC, a.OrderID DESC

             ) a

ORDER BY a.OrderDate, a.OrderID

GO

 

--위의 두 쿼리는 뒤쪽 페이지로 이동할수록 성능이 나빠진다.

--만약 100번째 페이지를 조회하면, 파생 테이블에서 1,001(=100*10+1) 건을 추출하고서 990(=1,000-11)건을 버리게 된다.

--페이지 번호가 임계치를 넘어가면 (인덱스에서 테이블로의 랜덤 액세스 때문에) 테이블을 전체 스캔하는 것보다 더 많은 페이지 I/O를 일으킬 수 있다.

 

 

 

--(2) 뒤쪽 페이지도 자주 조회할 때

 

--첫번째 페이지

SELECT TOP 11

             ROW_NUMBER() OVER(ORDER BY OrderDate, OrderID) as rnum, *

FROM Orders

WHERE CustomerID = 'QUICK'

ORDER BY OrderDate, OrderID

GO

 

--두번째 페이지

SELECT TOP 11

             ROW_NUMBER() OVER (ORDER BY OrderDate, OrderID) as rnum, *

FROM Orders

WHERE CustomerID = 'QUICK'

             /* 앞 페이지의 마지막 데이터보다 크거나 같아야 한다 */

             AND ((OrderDate = CONVERT(DATETIME, '19960820', 112) AND OrderID >= 38)

                           OR OrderDate > CONVERT(DATETIME, '19960820', 112))

ORDER BY OrderDate, OrderID

GO

 

 

/* 추가 1(화면에는 실제로 10건만 출력된다)은 다음 페이지가 더 있는지 확인하는 용도뿐만 아니라 다음 페이지의 시작점이 되기도 한다.

글자 그대로 일거양득의 효과를 나타낸다. 이번 절의 서두에서 언급했듯이, 위 쿼리처럼 특정 시작점을 이용해 페이지 처리를 하려면 각 로우가(유일하게)

구별되어야 한다. Orders 테이블에는 [CustomerID + OrderDate] 컬럼 조합으로 여러 건이 있으므로 order by 조건에 OrderID 컬럼을 추가했다. */

 

--첫번째 페이지용 쿼리와 두번째 이상 페이지용 쿼리를 합할 수 있다

 

 

DECLARE @OrderDate CHAR(8), @OrderID INT --앞 페이지의 11번째 데이터 저장용 변수

 

SET @OrderDate = '19960820'      --OrderDate 컬럼에는 날짜 값만 들어 있어서 CHAR(8) 형식으로 처리했음

SET @OrderID = 38

 

SELECT TOP 11 *

FROM Orders

WHERE CustomerID = 'QUICK'

             /* 앞 페이지의 마지막 데이터보다 크거나 같아야 한다 */

             AND ((OrderDate = CONVERT(DATETIME, @OrderDate, 112) AND OrderID >= @OrderID)

                           OR OrderDate > CONVERT(DATETIME, @OrderDate, 112))

ORDER BY OrderDate, OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT TOP 11 * FROM Orders WHERE CustomerID = 'QUICK'          /* 앞 페이지의 마지막 데이터보다 크거나 같아야 한다 */ AND ((OrderDate = CONVERT(DATETIME, @OrderDate, 112) AND OrderID >= @OrderID) OR OrderDate > CONVERT(DATETIME, @OrderDate, 112)) ORDER BY OrderDate, OrderID

--11       1             |--Top(TOP EXPRESSION:((11)))

--11       1                  |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1007]) WITH ORDERED PREFETCH)

--11       1                       |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK'),  WHERE:([jwjung].[dbo].[Orders].[OrderDate]=CONVERT(datetime,[@OrderDate],112) AND [jwjung].[dbo].[Orders].[OrderID]>=[@OrderID] OR [jwjung].[dbo].[Orders].[OrderDate]>CONVERT(datetime,[@OrderDate],112)) ORDERED FORWARD)

--11       11                     |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 13, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

/*위 쿼리의 실행계획을 살펴보니 뭔가 이상하다. Index Seek 오퍼레이션을 자세히 보면, [CustomerID] = N'QUICK' 조건만 SEEK:() 부분에 있고

페이지 처리용 조건은 모두 WHERE:() 부분에 나타났다. 이렇게 되면 'QUICK' 조건만 Orders_x01 인덱스의 액세스 범위를 결정하고 정작 페이지 처리에

꼭 필요한 나머지 조건은 필터링 역할 밖에 하지 못한다. */

 

--인덱스 최적으로 액세스하도록 하려면 쿼리를 변경해야 한다.

DECLARE @OrderDate CHAR(8), @OrderID INT --앞 페이지의 11번째 데이터 저장용 변수

 

SET @OrderDate = '19960820'

SET @OrderID = 38

 

SELECT TOP 11 *

FROM (

             SELECT *

             FROM (

                           SELECT TOP 11 *

                           FROM Orders

                           WHERE CustomerID = 'QUICK'

                                        /* 앞 페이지의 마지막 데이터와 같음 */

                                        AND OrderDate = CONVERT(DATETIME, @OrderDate, 112)

                                        AND OrderID >= @OrderID

                           ORDER BY OrderDate, OrderID

                           ) a

             UNION ALL

             SELECT *

             FROM (

                           SELECT TOP 11 *

                           FROM Orders

                           WHERE CustomerID = 'QUICK'

                                        /* 앞 페이지의 마지막 데이터보다 큼 */

                                        AND OrderDate > CONVERT(DATETIME, @OrderDate, 112)

                           ORDER BY OrderDate, OrderID

                           ) a

             ) a

ORDER BY OrderDate, OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT TOP 11 * FROM ( SELECT * FROM (             SELECT TOP 11 * FROM Orders    WHERE CustomerID = 'QUICK'     /* 앞 페이지의 마지막 데이터와 같음 */ AND OrderDate = CONVERT(DATETIME, @OrderDate, 112) AND OrderID >= @OrderID ORDER BY OrderDate, OrderID) a UNION ALL SELECT * FROM (         SELECT TOP 11 *              FROM Orders WHERE CustomerID = 'QUICK'           /* 앞 페이지의 마지막 데이터보다 큼 */         AND OrderDate > CONVERT(DATETIME, @OrderDate, 112) ORDER BY OrderDate, OrderID          ) a         ) a ORDER BY OrderDate, OrderID

--11       1             |--Top(TOP EXPRESSION:((11)))

--11       1                  |--Merge Join(Concatenation)

--10       1                       |--Top(TOP EXPRESSION:((11)))

--10       1                       |    |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))

--10       1                       |         |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK' AND [jwjung].[dbo].[Orders].[OrderDate]=CONVERT(datetime,[@OrderDate],112) AND [jwjung].[dbo].[Orders].[OrderID] >= [@OrderID]) ORDERED FORWARD)

--10       10                     |         |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

--1         1                       |--Top(TOP EXPRESSION:((11)))

--1         1                            |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1004], [Expr1025]) WITH ORDERED PREFETCH)

--1         1                                 |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK' AND [jwjung].[dbo].[Orders].[OrderDate] > CONVERT(datetime,[@OrderDate],112)) ORDERED FORWARD)

--1         1                                 |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1004]=[Bmk1004]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 2, 논리적 읽기 수 15, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--논리적 읽기 수가 13에서 15로 늘어난 탓에 오히려 성능이 떨어진 것처럼 보이지만, 조건에 해당하는 데이터가 워낙 적어서 union all 에 의해 수치가 왜곡되어 보이는 것 뿐이다.

 

 

 

--앞쪽 페이지로 거슬러 올라갈 때

/* 뒤 페이지의 첫 번째 데이터 저장용 변수 */

DECLARE @OrderDate CHAR(8), @OrderID INT

 

/* OrderDate 컬럼은 날짜 값만 관리하므로 편의상 CHAR(8) 형식으로 받아왔음 */

SET @OrderDate = '19960820'

SET @OrderID = 38

 

SELECT *

FROM (

             SELECT TOP 11 *

             FROM (

                           SELECT *

                           FROM (

                                        SELECT TOP 11 *

                                        FROM Orders

                                        WHERE CustomerID = 'QUICK'

                                                     --뒤 페이지의 첫 번째 데이터와 같음

                                                     AND OrderDate = CONVERT(DATETIME, @OrderDate, 112)

                                                     AND OrderID <= @OrderID

                                        ORDER BY OrderDate DESC, OrderID DESC

                           ) a

                           UNION ALL

                           SELECT *

                           FROM (

                                        SELECT TOP 11 *

                                        FROM Orders

                                        WHERE CustomerID = 'QUICK'

                                                     --뒤 페이지의 첫 번째 데이터보다 작음

                                                     AND OrderDate < CONVERT(DATETIME, @OrderDate, 112)

                                        ORDER BY OrderDate DESC, OrderID DESC

                                        ) a

                           ) a

             ORDER BY OrderDate DESC, OrderID DESC

             ) a

ORDER BY OrderDate, OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT * FROM (SELECT TOP 11 * FROM (SELECT * FROM (             SELECT TOP 11 * FROM Orders WHERE CustomerID = 'QUICK'       /* 뒤 페이지의 첫 번째 데이터와 같음 */       AND OrderDate = CONVERT(DATETIME, @OrderDate, 112) AND OrderID <= @OrderID ORDER BY OrderDate DESC, OrderID DESC           ) a UNION ALL SELECT * FROM (SELECT TOP 11 *              FROM Orders WHERE CustomerID = 'QUICK' /* 뒤 페이지의 첫 번째 데이터보다 작음 */ AND OrderDate < CONVERT(DATETIME, @OrderDate, 112) ORDER BY OrderDate DESC, OrderID DESC) a             ) a ORDER BY OrderDate DESC, OrderID DESC ) a ORDER BY OrderDate, OrderID

--11       1             |--Sort(ORDER BY:([Union1011] ASC, [Union1008] ASC))

--11       1                  |--Top(TOP EXPRESSION:((11)))

--11       1                       |--Merge Join(Concatenation)

--1         1                            |--Top(TOP EXPRESSION:((11)))

--1         1                            |    |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))

--1         1                            |         |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK' AND [jwjung].[dbo].[Orders].[OrderDate]=CONVERT(datetime,[@OrderDate],112) AND [jwjung].[dbo].[Orders].[OrderID] <= [@OrderID]) ORDERED BACKWARD)

--1         1                            |         |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

--10       1                            |--Top(TOP EXPRESSION:((11)))

--10       1                                 |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1004], [Expr1025]) WITH ORDERED PREFETCH)

--10       1                                      |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[CustomerID]=N'QUICK' AND [jwjung].[dbo].[Orders].[OrderDate] < CONVERT(datetime,[@OrderDate],112)) ORDERED BACKWARD)

--10       10                                    |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1004]=[Bmk1004]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 2, 논리적 읽기 수 15, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

 

--(3) 적절한 인덱스가 없어서 정렬이 불가피할 때

 

--Orders 테이블의 Orders_x01 인덱스는 [CustomerID + OrderDate + OrderID] 컬럼으로 구성되어 있다.

 

--한번에 10건씩 보여주는 화면에서 10번째 페이지를 조회하는 쿼리

SELECT *

FROM (

             SELECT TOP 11 *

             FROM (

                           SELECT TOP 101 *

                           FROM Orders

                           ORDER BY OrderDate, OrderID

                           ) a

             ORDER BY a.OrderDate DESC, a.OrderID DESC

             ) a

ORDER BY a.OrderDate, a.OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT * FROM (             SELECT TOP 11 * FROM ( SELECT TOP 101 * FROM Orders ORDER BY OrderDate, OrderID) a ORDER BY a.OrderDate DESC, a.OrderID DESC ) a ORDER BY a.OrderDate, a.OrderID

--11       1             |--Sort(ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] ASC, [jwjung].[dbo].[Orders].[OrderID] ASC))

--11       1                  |--Sort(TOP 11, ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] DESC, [jwjung].[dbo].[Orders].[OrderID] DESC))

--101     1                       |--Sort(TOP 101, ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] ASC, [jwjung].[dbo].[Orders].[OrderID] ASC))

--8300   1                            |--Table Scan(OBJECT:([jwjung].[dbo].[Orders]))

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 199, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

SELECT *

FROM (

             SELECT TOP 11 *

             FROM (

                           SELECT TOP 101 *

                           FROM Orders WITH (INDEX(Orders_x01))

                           ORDER BY OrderDate, OrderID

                           ) a

             ORDER BY a.OrderDate DESC, a.OrderID DESC

             ) a

ORDER BY a.OrderDate, a.OrderID

GO

 

--Rows   Executes             StmtText

--11       1           SELECT * FROM (SELECT TOP 11 * FROM (SELECT TOP 101 * FROM Orders WITH (INDEX(Orders_x01)) ORDER BY OrderDate, OrderID) a ORDER BY a.OrderDate DESC, a.OrderID DESC           ) a ORDER BY a.OrderDate, a.OrderID

--11       1             |--Sort(ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] ASC, [jwjung].[dbo].[Orders].[OrderID] ASC))

--11       1                  |--Sort(TOP 11, ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] DESC, [jwjung].[dbo].[Orders].[OrderID] DESC))

--101     1                       |--Sort(TOP 101, ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] ASC, [jwjung].[dbo].[Orders].[OrderID] ASC))

--8300   1                            |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1005]) WITH UNORDERED PREFETCH)

--8300   1                                 |--Index Scan(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]))

--8300   8300                           |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 8340, 물리적 읽기 수 1, 미리 읽기 수 30, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

SELECT c.*

FROM (

             SELECT TOP 11 *

             FROM (

                           SELECT TOP 101 OrderDate, OrderID

                           FROM Orders

                           ORDER BY OrderDate, OrderID

                           ) a

             ORDER BY a.OrderDate DESC, a.OrderID DESC

             ) b

             , Orders c

WHERE b.OrderID = c.OrderID

ORDER BY b.OrderDate, b.OrderID

OPTION (FORCE ORDER, LOOP JOIN)

GO

 

--Rows   Executes             StmtText

--11       1           SELECT c.* FROM (SELECT TOP 11 * FROM (           SELECT TOP 101 OrderDate, OrderID FROM Orders ORDER BY OrderDate, OrderID) a ORDER BY a.OrderDate DESC, a.OrderID DESC           ) b, Orders c WHERE b.OrderID = c.OrderID ORDER BY b.OrderDate, b.OrderID OPTION (FORCE ORDER, LOOP JOIN)

--11       1             |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1004]))

--11       1                  |--Nested Loops(Inner Join, OUTER REFERENCES:([jwjung].[dbo].[Orders].[OrderID]))

--11       1                  |    |--Sort(ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] ASC, [jwjung].[dbo].[Orders].[OrderID] ASC))

--11       1                  |    |    |--Sort(TOP 11, ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] DESC, [jwjung].[dbo].[Orders].[OrderID] DESC))

--101     1                 |    |         |--Sort(TOP 101, ORDER BY:([jwjung].[dbo].[Orders].[OrderDate] ASC, [jwjung].[dbo].[Orders].[OrderID] ASC))

--8300   1                  |    |              |--Index Scan(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]))

--11       11                |    |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_pk] AS [c]), SEEK:([c].[OrderID]=[jwjung].[dbo].[Orders].[OrderID]) ORDERED FORWARD)

--11       11                |--RID Lookup(OBJECT:([jwjung].[dbo].[Orders] AS [c]), SEEK:([Bmk1004]=[Bmk1004]) LOOKUP ORDERED FORWARD)

 

--(11개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 73, 물리적 읽기 수 2, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

SET STATISTICS PROFILE OFF

SET STATISTICS TIME OFF

SET STATISTICS IO OFF

 

반응형
반응형
ALTER PROC [dbo].[prGetNoticeList]
    @TypeID                smallint,
    @PageNo                int,
    @PageSize            tinyint,
    @IsShow                char(1)=null,
    @SearchKey            nvarchar(10) = null,
    @SearchString        nvarchar(10) = null
AS       


SET NOCOUNT ON
    DECLARE @StrSQL nvarchar(1000), @Param nvarchar(200), @SearchPart nvarchar(120), @TotalRow int
    DECLARE @LastPage int, @LastPageRow int

    SET @SearchPart = N''

    IF @IsShow IS NOT NULL
        SET @SearchPart = @SearchPart +  N' AND IsShow = @IsShow '

    IF  @SearchKey IN ('Title','Contents') AND @SearchString <> ''       
        SET @SearchPart = @SearchPart + N' AND Contains(' + @SearchKey + N' , @SearchString)'
   
    --------------------------------------------------------------------------------------------
    -- 총 레코드수 구하기
    --------------------------------------------------------------------------------------------
    SET @Param =  N'@TypeID smallint, @IsShow char(1), @SearchKey varchar(10), @SearchString varchar(10), @TotalRow int output'

    SET @StrSQL = 'SELECT @TotalRow=Count(NoticeNo) FROM dbo.Notice WHERE TypeID=@TypeID ' + @SearchPart
    EXEC sp_executesql @StrSQL, @Param, @TypeID, @IsShow, @SearchKey, @SearchString, @TotalRow output

    --------------------------------------------------------------------------------------------
    -- 마지막페이지수, 레코드수
    --------------------------------------------------------------------------------------------
    SET @LastPageRow = @TotalRow - (@TotalRow/@PageSize) * @PageSize

    IF @LastPageRow = 0        SET @LastPage = (@TotalRow/@PageSize)   
    ELSE                    SET @LastPage = (@TotalRow/@PageSize) +1


    IF @PageNo = 1                            -- 첫번째 페이지    
    BEGIN
        SET @StrSQL =
            'SELECT TOP ' + STR(@PageSize) + '
                NoticeID, TypeID, NoticeNo, Title, Contents, WriterID, WriteDate, NoticeDate, Hit, IsShow, @TotalRow as TotalRow' +
            '  FROM dbo.Notice WHERE TypeID=@TypeID ' + @SearchPart  + ' ORDER BY TypeID, NoticeNo DESC'
    END
    ELSE IF @PageNo >= @LastPage AND @LastPageRow > 0         -- 마지막 페이지
    BEGIN
        SET @StrSQL = '
             SELECT * FROM (
                 SELECT TOP ' + STR(@LastPageRow) + ' NoticeID, TypeID, NoticeNo, Title, Contents, WriterID, WriteDate, NoticeDate,Hit, IsShow, @TotalRow as TotalRow' +
                '  FROM dbo.Notice WHERE TypeID=@TypeID ' + @SearchPart + ' ORDER BY TypeID, NoticeNo ASC
            ) AS B ORDER BY TypeID, NoticeNo DESC'   
    END
    ELSE                                     -- 일반 페이지
    BEGIN
        SET @StrSQL = 'SELECT * FROM ( SELECT TOP ' + STR(@PageSize) + ' * FROM (' +
            ' SELECT TOP ' + STR(@PageNo*@PageSize) + ' NoticeID, TypeID, NoticeNo, Title, Contents, WriterID, WriteDate, NoticeDate,Hit, IsShow, @TotalRow as TotalRow ' +
            ' FROM dbo.Notice WHERE TypeID=@TypeID ' + @SearchPart + '  ORDER BY TypeID, NoticeNo DESC) AS B ORDER BY TypeID, NoticeNo  ASC) as C' +
            ' ORDER BY TypeID, NoticeNo DESC'
    END
    EXEC sp_executesql @StrSQL, @Param, @TypeID, @IsShow, @SearchKey, @SearchString,@TotalRow output

반응형
반응형

/* 03.정렬을 최소화하는 SQL 작성 */

 

USE jwjung

GO

IF OBJECT_ID('Shippers') IS NOT NULL

             DROP TABLE Shippers

GO

IF OBJECT_ID('Orders') IS NOT NULL

             DROP TABLE Orders

GO

 

SELECT * INTO Shippers FROM Northwind.dbo.Shippers

GO

SELECT a.* INTO Orders FROM Northwind.dbo.Orders a, Northwind.dbo.Orders b

GO

 

SELECT COUNT(*) FROM Shippers;

GO

--3

SELECT COUNT(*) FROM Orders;

GO

--688900

 

ALTER TABLE Shippers ADD CONSTRAINT Shippers_pk PRIMARY KEY CLUSTERED (ShipperID)

GO

CREATE NONCLUSTERED INDEX Orders_x01 ON Orders (ShipVia, ShippedDate)

GO

 

EXEC sp_helpindex 'Shippers';

--index_name      index_description             index_keys

--Shippers_pk      clustered, unique, primary key located on PRIMARY ShipperID

 

EXEC sp_helpindex 'Orders';

--index_name      index_description             index_keys

--Orders_x01      nonclustered located on PRIMARY ShipVia, ShippedDate

 

 

SET STATISTICS PROFILE ON

SET STATISTICS TIME ON

SET STATISTICS IO ON

 

--(1) 불필요한 distinct 제거

SELECT DISTINCT ShipVia

FROM Orders

WHERE ShippedDate >= CONVERT(DATETIME, '19970101', 112)

             AND ShippedDate < CONVERT(DATETIME, '19980101', 112)

ORDER BY ShipVia

GO

 

--Rows   Executes             StmtText

--3         1           SELECT DISTINCT ShipVia FROM Orders WHERE ShippedDate >= CONVERT(DATETIME, '19970101', 112) AND ShippedDate < CONVERT(DATETIME, '19980101', 112) ORDER BY ShipVia

--3         1             |--Stream Aggregate(GROUP BY:([jwjung].[dbo].[Orders].[ShipVia]))

--330340            1                  |--Index Scan(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]),  WHERE:([jwjung].[dbo].[Orders].[ShippedDate]>='1997-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders].[ShippedDate]<'1998-01-01 00:00:00.000') ORDERED FORWARD)

 

--(3개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 2229, 물리적 읽기 수 0, 미리 읽기 수 18, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--CPU 시간 = 78밀리초, 경과 시간 = 110밀리초

 

--테이블 간의 관계를 알고 있다면, EXISTS를 사용하여 쿼리를 변경할 수 있다.

--(물론 Orders 테이블의 ShipVia 컬럼에 null 또는 잘못된 값이 없어야 하며, 혹시 있더라도

--그런 값은 무시한다는 전제 조건이 있어야 한다.)

 

SELECT a.ShipperID

FROM Shippers a

WHERE EXISTS

             (

             SELECT 1

             FROM Orders x

             WHERE x.ShipVia = a.ShipperID

                           AND x.ShippedDate >= CONVERT(DATETIME, '19970101', 112)

                           AND x.ShippedDate < CONVERT(DATETIME, '19980101', 112)

             )

ORDER BY a.ShipperID

GO

 

--Rows   Executes             StmtText

--3         1           SELECT a.ShipperID FROM Shippers a WHERE EXISTS ( SELECT 1 FROM Orders x WHERE x.ShipVia = a.ShipperID AND x.ShippedDate >= CONVERT(DATETIME, '19970101', 112)    AND x.ShippedDate < CONVERT(DATETIME, '19980101', 112)             ) ORDER BY a.ShipperID

--3         1             |--Nested Loops(Left Semi Join, OUTER REFERENCES:([a].[ShipperID]))

--3         1                  |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[Shippers].[Shippers_pk] AS [a]), ORDERED FORWARD)

--3         3                  |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01] AS [x]), SEEK:([x].[ShipVia]=[jwjung].[dbo].[Shippers].[ShipperID] as [a].[ShipperID] AND [x].[ShippedDate] >= '1997-01-01 00:00:00.000' AND [x].[ShippedDate] < '1998-01-01 00:00:00.000') ORDERED FORWARD)

 

--(3개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 3, 논리적 읽기 수 16, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Shippers'. 검색 수 1, 논리적 읽기 수 2, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--CPU 시간 = 0밀리초, 경과 시간 = 0밀리초

 

--EXISTS는 세미 조인(Semi Join) 방식으로 수행되는데, 메인 쿼리의 로우를 가지고 서브쿼리의 로우와 끝까지 조인하는 게 아니라

--(메인 쿼리에서 제공된 로우별로) 한 번만 조인에 성공하면 참을 리턴하고 조인을 멈춘다.

 

 

--(2) 비효율적인 count 개선

DECLARE @cnt INT

 

SELECT @cnt = count(*)

FROM Orders

WHERE ShipVia = 3

             AND ShippedDate >= CONVERT(DATETIME, '19970101', 112)

             AND ShippedDate < CONVERT(DATETIME, '19980101', 112)

 

IF @cnt > 0

             PRINT '데이터 있음 : ' + CONVERT(VARCHAR, @cnt)

ELSE

             PRINT '데이터 없음'

 

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 338, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--1         1           SELECT @cnt = count(*) FROM Orders WHERE ShipVia = 3 AND ShippedDate >= CONVERT(DATETIME, '19970101', 112) AND ShippedDate < CONVERT(DATETIME, '19980101', 112)

--0         0             |--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1005],0)))

--1         1                  |--Stream Aggregate(DEFINE:([Expr1005]=Count(*)))

--103750            1                       |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[ShipVia]=(3) AND [jwjung].[dbo].[Orders].[ShippedDate] >= '1997-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders].[ShippedDate] < '1998-01-01 00:00:00.000') ORDERED FORWARD)

 

 

--데이터의 존재 여부만 확인

DECLARE @cnt INT

 

SELECT @cnt = COUNT(*)

WHERE EXISTS

             (

                           SELECT 1

                           FROM Orders

                           WHERE ShipVia = 3

                                        AND ShippedDate >= CONVERT(DATETIME, '19970101', 112)

                                        AND ShippedDate < CONVERT(DATETIME, '19980101', 112)

             )

 

IF @cnt > 0

             PRINT '데이터 있음 : ' + CONVERT(VARCHAR, @cnt)

ELSE

             PRINT '데이터 없음'

 

--Rows   Executes             StmtText

--1         1           SELECT @cnt = COUNT(*) WHERE EXISTS (SELECT 1 FROM Orders WHERE ShipVia = 3 AND ShippedDate >= CONVERT(DATETIME, '19970101', 112) AND ShippedDate < CONVERT(DATETIME, '19980101', 112)             )

--0         0             |--Compute Scalar(DEFINE:([Expr1005]=CONVERT_IMPLICIT(int,[Expr1008],0)))

--1         1                  |--Stream Aggregate(DEFINE:([Expr1008]=Count(*)))

--1         1                       |--Nested Loops(Left Semi Join)

--1         1                            |--Constant Scan

--1         1                            |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[ShipVia]=(3) AND [jwjung].[dbo].[Orders].[ShippedDate] >= '1997-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders].[ShippedDate] < '1998-01-01 00:00:00.000') ORDERED FORWARD)

 

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 3, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

DECLARE @cnt INT

 

SELECT @cnt = COUNT(*)

FROM (

             SELECT TOP 1 ShipVia

             FROM Orders

             WHERE ShipVia = 3

                           AND ShippedDate >= CONVERT(DATETIME, '19970101', 112)

                           AND ShippedDate < CONVERT(DATETIME, '19980101', 112)

             ) a

 

IF @cnt > 0

             PRINT '데이터 있음 : ' + CONVERT(VARCHAR, @cnt)

ELSE

             PRINT '데이터 없음'

 

--Rows   Executes             StmtText

--1         1           SELECT @cnt = COUNT(*) FROM ( SELECT TOP 1 ShipVia FROM Orders           WHERE ShipVia = 3 AND ShippedDate >= CONVERT(DATETIME, '19970101', 112) AND ShippedDate < CONVERT(DATETIME, '19980101', 112)      ) a

--0         0             |--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1005],0)))

--1         1                  |--Stream Aggregate(DEFINE:([Expr1005]=Count(*)))

--1         1                       |--Top(TOP EXPRESSION:((1)))

--1         1                            |--Index Seek(OBJECT:([jwjung].[dbo].[Orders].[Orders_x01]), SEEK:([jwjung].[dbo].[Orders].[ShipVia]=(3) AND [jwjung].[dbo].[Orders].[ShippedDate] >= '1997-01-01 00:00:00.000' AND [jwjung].[dbo].[Orders].[ShippedDate] < '1998-01-01 00:00:00.000') ORDERED FORWARD)

 

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 3, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

--//(3) 부적절한 UNION 대신 UNION ALL 사용

USE jwjung

GO

 

IF OBJECT_ID('Orders_2008') IS NOT NULL

             DROP TABLE Orders_2008

GO

IF OBJECT_ID('Orders_2009') IS NOT NULL

             DROP TABLE Orders_2009

GO

 

SELECT a.*

             , '2008' + SUBSTRING(CONVERT(CHAR(8), OrderDate, 112), 5, 4) AS 기준일자

INTO Orders_2008

FROM Northwind.dbo.Orders a

GO

--(830개 행이 영향을 받음)

SELECT a.*

             , '2009' + SUBSTRING(CONVERT(CHAR(8), OrderDate, 112), 5, 4) AS 기준일자

INTO Orders_2009

FROM Northwind.dbo.Orders a

GO

--(830개 행이 영향을 받음)

 

CREATE UNIQUE INDEX Orders_2008_x01 ON Orders_2008 (기준일자, OrderID)

GO

 

CREATE UNIQUE INDEX Orders_2009_x01 ON Orders_2009 (기준일자, OrderID)

GO

 

SELECT * FROM Orders_2008;

GO

SELECT * FROM Orders_2009;

GO

 

SELECT OrderID, 기준일자, Freight

FROM Orders_2008

WHERE Freight >= 50

UNION

SELECT OrderID, 기준일자, Freight

FROM Orders_2009

WHERE Freight >= 50

GO

--(720개 행이 영향을 받음)

--테이블 'Orders_2009'. 검색 수 1, 논리적 읽기 수 22, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Orders_2008'. 검색 수 1, 논리적 읽기 수 22, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--720     1           SELECT OrderID, 기준일자, Freight FROM Orders_2008 WHERE Freight >= 50 UNION SELECT OrderID, 기준일자, Freight FROM Orders_2009 WHERE Freight >= 50

--720     1             |--Sort(DISTINCT ORDER BY:([Union1008] ASC, [Union1009] ASC, [Union1010] ASC))

--720     1                  |--Concatenation

--360     1                       |--Table Scan(OBJECT:([jwjung].[dbo].[Orders_2008]), WHERE:([jwjung].[dbo].[Orders_2008].[Freight]>=($50.0000)))

--360     1                       |--Table Scan(OBJECT:([jwjung].[dbo].[Orders_2009]), WHERE:([jwjung].[dbo].[Orders_2009].[Freight]>=($50.0000)))

 

--CPU 시간 = 0밀리초, 경과 시간 = 55밀리초

 

--중복된 로우를 제거하고자 'Sort(DISTINCT ORDER BY)' 오퍼레이션이 나타났다.

 

SELECT OrderID, 기준일자, Freight

FROM Orders_2008

WHERE Freight >= 50

UNION ALL

SELECT OrderID, 기준일자, Freight

FROM Orders_2009

WHERE Freight >= 50

GO

 

--Rows   Executes             StmtText

--720     1           SELECT OrderID, 기준일자, Freight FROM Orders_2008 WHERE Freight >= 50 UNION ALL SELECT OrderID, 기준일자, Freight FROM Orders_2009 WHERE Freight >= 50

--720     1             |--Concatenation

--360     1                  |--Table Scan(OBJECT:([jwjung].[dbo].[Orders_2008]), WHERE:([jwjung].[dbo].[Orders_2008].[Freight]>=($50.0000)))

--360     1                  |--Table Scan(OBJECT:([jwjung].[dbo].[Orders_2009]), WHERE:([jwjung].[dbo].[Orders_2009].[Freight]>=($50.0000)))

 

 

--//(4) NL 조인을 활용 - 부분적인 부분범위 처리

USE jwjung

GO

IF OBJECT_ID('Customers') IS NOT NULL

             DROP TABLE Customers

GO

 

SELECT TOP 100000

             IDENTITY(INT, 1, 1) AS CustomerID, CompanyName, ContactName, ContactTitle

             , Address, City, Region, PostalCode, Country, Phone, Fax

INTO Customers

FROM Northwind.dbo.Customers a

             , Northwind.dbo.[Order Details] b

GO

--(100000개 행이 영향을 받음)

 

ALTER TABLE Customers ADD CONSTRAINT Customers_pk PRIMARY KEY CLUSTERED (CustomerID)

GO

 

CREATE INDEX Customers_x01 ON Customers (Country, CompanyName)

GO

 

SELECT TOP 10 * FROM Customers

ORDER BY CompanyName, CustomerID

GO

 

--Rows   Executes             StmtText

--10       1           SELECT TOP 10 * FROM Customers ORDER BY CompanyName, CustomerID

--10       1             |--Top(TOP EXPRESSION:((10)))

--10       1                  |--Parallelism(Gather Streams, ORDER BY:([jwjung].[dbo].[Customers].[CompanyName] ASC, [jwjung].[dbo].[Customers].[CustomerID] ASC))

--40       4                       |--Sort(TOP 10, ORDER BY:([jwjung].[dbo].[Customers].[CompanyName] ASC, [jwjung].[dbo].[Customers].[CustomerID] ASC))

--100000            4                            |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[Customers].[Customers_pk]))

 

--(10개 행이 영향을 받음)

--테이블 'Customers'. 검색 수 5, 논리적 읽기 수 3502, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

SELECT TOP 10 b.*

FROM (

             SELECT TOP 10

                           CompanyName, CustomerID

             FROM Customers

             ORDER BY CompanyName, CustomerID

             ) a

             , Customers b

WHERE a.CustomerID = b.CustomerID

ORDER BY a.CompanyName, a.CustomerID

OPTION (FORCE ORDER, LOOP JOIN)

GO

 

--Rows   Executes             StmtText

--10       1           SELECT TOP 10 b.* FROM ( SELECT TOP 10 CompanyName, CustomerID FROM Customers ORDER BY CompanyName, CustomerID          ) a, Customers b WHERE a.CustomerID = b.CustomerID ORDER BY a.CompanyName, a.CustomerID OPTION (FORCE ORDER, LOOP JOIN)

--10       1             |--Top(TOP EXPRESSION:((10)))

--10       1                  |--Parallelism(Gather Streams, ORDER BY:([jwjung].[dbo].[Customers].[CompanyName] ASC, [jwjung].[dbo].[Customers].[CustomerID] ASC))

--10       4                       |--Nested Loops(Inner Join, OUTER REFERENCES:([jwjung].[dbo].[Customers].[CustomerID]))

--10       4                            |--Parallelism(Distribute Streams, RoundRobin Partitioning)

--10       1                            |    |--Top(TOP EXPRESSION:((10)))

--10       1                            |         |--Parallelism(Gather Streams, ORDER BY:([jwjung].[dbo].[Customers].[CompanyName] ASC, [jwjung].[dbo].[Customers].[CustomerID] ASC))

--40       4                            |              |--Sort(TOP 10, ORDER BY:([jwjung].[dbo].[Customers].[CompanyName] ASC, [jwjung].[dbo].[Customers].[CustomerID] ASC))

--100000            4                            |                   |--Index Scan(OBJECT:([jwjung].[dbo].[Customers].[Customers_x01]))

--10       10                          |--Clustered Index Seek(OBJECT:([jwjung].[dbo].[Customers].[Customers_pk] AS [b]), SEEK:([b].[CustomerID]=[jwjung].[dbo].[Customers].[CustomerID]) ORDERED FORWARD)

 

--(10개 행이 영향을 받음)

--테이블 'Customers'. 검색 수 5, 논리적 읽기 수 970, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

SET STATISTICS PROFILE OFF

SET STATISTICS TIME OFF

SET STATISTICS IO OFF

 

 

 

 

반응형
반응형

/* 정렬의 최적화 */

 

/* 02.인덱스로 정렬을 제거 */

USE jwjung

GO

IF OBJECT_ID('계약') IS NOT NULL

             DROP TABLE 계약

GO

 

CREATE TABLE 계약 (

             계약번호 INT                                                                           IDENTITY

             ,계약일자              CHAR(8)                                                     NOT NULL

             ,계약금액              DECIMAL(12, 2)  NOT NULL

             ,고객번호              CHAR(5)                                                     NOT NULL

             ,담당자ID              INT                                                                           NOT NULL

             ,비고                                 CHAR(1000)                                  NULL

);

GO

 

INSERT INTO 계약

SELECT b.계약일자                                                                                            as 계약일자

             , a.Freight                                                                                                                     as 계약금액

             , a.CustomerID                                                                                   as 고객번호

             , a.EmployeeID                                                                                    as 담당자ID

             , a.ShipName + a.ShipAddress       as 비고

FROM Northwind.dbo.Orders a

             ,(

                           SELECT TOP 365

                                        CONVERT(CHAR(8), CONVERT(DATETIME, '20090101', 112) + ROW_NUMBER() OVER(ORDER BY a.OrderID) - 1, 112) AS 계약일자

                           FROM Northwind.dbo.Orders a, Northwind.dbo.Orders b

             ) b

GO

--(302950개 행이 영향을 받음)

 

ALTER TABLE 계약

ADD CONSTRAINT 계약_pk PRIMARY KEY NONCLUSTERED (계약번호)

GO

 

 

--(1) order by 조건을 만족하도록 인덱스에 컬럼 추가

 

--계약일자 조건만으로 자주 검색한다고 가정하고 아래처럼 계약일자 컬럼 하나로 구성된 인덱스를 추가해보자

CREATE NONCLUSTERED INDEX 계약_x01 ON 계약 (계약일자)

GO

 

SET STATISTICS PROFILE ON

SET STATISTICS TIME ON

SET STATISTICS IO ON

 

SELECT TOP 3 *

FROM 계약

WHERE 계약일자 = '20090630'

ORDER BY 계약금액 DESC

GO

 

--Rows   Executes             StmtText

--3         1           SELECT TOP 3 * FROM 계약 WHERE 계약일자 = '20090630' ORDER BY 계약금액 DESC

--3         1             |--Sort(TOP 3, ORDER BY:([jwjung].[dbo].[계약].[계약금액] DESC))

--830     1                  |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1006]) OPTIMIZED WITH UNORDERED PREFETCH)

--0         0                       |--Compute Scalar(DEFINE:([Expr1005]=BmkToPage([Bmk1000])))

--830     1                       |    |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x01]), SEEK:([jwjung].[dbo].[계약].[계약일자]='20090630') ORDERED FORWARD)

--830     830                   |--RID Lookup(OBJECT:([jwjung].[dbo].[계약]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(3개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 836, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--ORDER BY 순서와 일치하도록 계약_x01 인덱스에 계약금액 컬럼을 추가

DROP INDEX 계약.계약_x01

GO

CREATE CLUSTERED INDEX 계약_x01 ON 계약 (계약일자, 계약금액)

GO

 

SELECT * FROM 계약;

 

SELECT TOP 3 *

FROM 계약

WHERE 계약일자 = '20090630'

ORDER BY 계약금액 DESC

GO

 

--Rows   Executes             StmtText

--3         1           SELECT TOP 3 * FROM 계약 WHERE 계약일자 = '20090630' ORDER BY 계약금액 DESC

--3         1             |--Top(TOP EXPRESSION:((3)))

--3         1                  |--Clustered Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x01]), SEEK:([jwjung].[dbo].[계약].[계약일자]='20090630') ORDERED BACKWARD)

 

--(3개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 4, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--정렬 연산이 사라졌을 뿐만 아니라 논리적 읽기 수도 836에서 4로 줄었다. 계약_x01 인덱스가 [계약일자 + 계약금액] 순서로 정렬되어 있으므로,

--인덱스를 거꾸로 읽으면서 테이블을 세 번만 액세스하면 원하는 결과 집합을 얻는다.(인덱스는 더블링크드리스트 구조)

 

 

--참고) 계약금액을 포함하지 않은 클러스터인덱스

DROP INDEX 계약.계약_x01

GO

CREATE CLUSTERED INDEX 계약_x01 ON 계약 (계약일자)

GO

 

SELECT TOP 3 *

FROM 계약

WHERE 계약일자 = '20090630'

ORDER BY 계약금액 DESC

GO

 

--Rows   Executes             StmtText

--3         1           SELECT TOP 3 * FROM 계약 WHERE 계약일자 = '20090630' ORDER BY 계약금액 DESC

--3         1             |--Sort(TOP 3, ORDER BY:([jwjung].[dbo].[계약].[계약금액] DESC))

--830     1                  |--Clustered Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x01]), SEEK:([jwjung].[dbo].[계약].[계약일자]='20090630') ORDERED FORWARD)

 

--(3개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 123, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

--//컬럼 추가 시 주의사항

--2005버전부터는 인덱스를 만들 때 include 옵션을 사용할 수 있으며, 이 옵션은 테이블 랜덤 액세스를 줄이고자 할 때 매우 유용하다

--그러나 included에 기술된 컬럼은 리프 페이지에만 저장되므로 인덱스 키 값의 정렬에 참여하지 않는다

 

DROP INDEX 계약.계약_x01

GO

CREATE NONCLUSTERED INDEX 계약_x01 ON 계약 (계약일자) INCLUDE (계약금액)

GO

 

SELECT TOP 3 *

FROM 계약

WHERE 계약일자 = '20090630'

ORDER BY 계약금액 DESC

GO

 

--Rows   Executes             StmtText

--3         1           SELECT TOP 3 * FROM 계약 WHERE 계약일자 = '20090630' ORDER BY 계약금액 DESC

--3         1             |--Sort(TOP 3, ORDER BY:([jwjung].[dbo].[계약].[계약금액] DESC))

--830     1                  |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1006]) OPTIMIZED WITH UNORDERED PREFETCH)

--0         0                       |--Compute Scalar(DEFINE:([Expr1005]=BmkToPage([Bmk1000])))

--830     1                       |    |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x01]), SEEK:([jwjung].[dbo].[계약].[계약일자]='20090630') ORDERED FORWARD)

--830     830                   |--RID Lookup(OBJECT:([jwjung].[dbo].[계약]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(3개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 837, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--계약_x01 인덱스에 계약금액 컬럼이 추가되면서 인덱스 페이지가 증가했기 때문에 논리적 읽기 수가 오히려 늘어났다.

 

SELECT * FROM sysindexes

WHERE id = object_id('계약')

 

DBCC IND('jwjung', '계약', 2)

DBCC IND('jwjung', '계약', 5)

 

DBCC TRACEON(3604);

DBCC PAGE('jwjung', 1, 4560, 3);

DBCC PAGE('jwjung', 1, 5512, 3);

DBCC TRACEOFF(3604);

 

 

DROP INDEX 계약.계약_x01

GO

--CREATE CLUSTERED INDEX 계약_x01 ON 계약 (계약일자, 계약금액)

--GO

 

--(2) group by 조건을 만족하도록 인덱스에 컬럼 추가

 

--[담당자ID + 계약일자] 조건으로도 자주 검색한다고 가정하고 아래처럼 복합 컬럼 인덱스를 추가해보자

CREATE NONCLUSTERED INDEX 계약_x02 ON 계약 (담당자ID, 계약일자)

GO

 

SELECT TOP 3 *

FROM (

             SELECT 계약일자, 고객번호

                           , COUNT(*) AS 계약건수, SUM(계약금액) AS 계약금액

             FROM 계약

             WHERE 담당자ID = 9

                           AND 계약일자 BETWEEN '20090301' AND '20090305'

             GROUP BY 계약일자, 고객번호

             ) a

ORDER BY a.계약일자, a.고객번호

GO

 

--Rows   Executes             StmtText

--3         1           SELECT TOP 3 * FROM (SELECT 계약일자, 고객번호   , COUNT(*) AS 계약건수, SUM(계약금액) AS 계약금액 FROM 계약        WHERE 담당자ID = 9 AND 계약일자 BETWEEN '20090301' AND '20090305' GROUP BY 계약일자, 고객번호) a ORDER BY a.계약일자, a.고객번호

--3         1             |--Top(TOP EXPRESSION:((3)))

--0         0                  |--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1012],0)))

--3         1                       |--Stream Aggregate(GROUP BY:([jwjung].[dbo].[계약].[계약일자], [jwjung].[dbo].[계약].[고객번호]) DEFINE:([Expr1012]=Count(*), [Expr1005]=SUM([jwjung].[dbo].[계약].[계약금액])))

--8         1                            |--Sort(ORDER BY:([jwjung].[dbo].[계약].[계약일자] ASC, [jwjung].[dbo].[계약].[고객번호] ASC))

--215     1                                 |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1011]) OPTIMIZED WITH UNORDERED PREFETCH)

--0         0                                      |--Compute Scalar(DEFINE:([Expr1010]=BmkToPage([Bmk1000])))

--215     1                                      |    |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x02]), SEEK:([jwjung].[dbo].[계약].[담당자ID]=(9) AND [jwjung].[dbo].[계약].[계약일자] >= '20090301' AND [jwjung].[dbo].[계약].[계약일자] <= '20090305') ORDERED FORWARD)

--215     215                                  |--RID Lookup(OBJECT:([jwjung].[dbo].[계약]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(3개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 218, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--집계 함수를 사용한다고 무조건 전체범위 처리방식으로 수행되는 것은 아니다. GROUP BY 조건과 인덱스 구성 컬럼이 일치하면 부분범위 처리방식으로 수행되기도 한다.

 

--GROUP BY 순서와 일치하도록 고객번호 컬럼을 계약_x02 인덱스에 추가해 보자

DROP INDEX 계약.계약_x02

GO

CREATE NONCLUSTERED INDEX 계약_x02 ON 계약 (담당자ID, 계약일자, 고객번호)

GO

 

SELECT TOP 3 *

FROM (

             SELECT 계약일자, 고객번호

                           , COUNT(*) AS 계약건수, SUM(계약금액) AS 계약금액

             FROM 계약

             WHERE 담당자ID = 9

                           AND 계약일자 BETWEEN '20090301' AND '20090305'

             GROUP BY 계약일자, 고객번호

             ) a

ORDER BY a.계약일자, a.고객번호

GO

 

--Rows   Executes             StmtText

--3         1           SELECT TOP 3 * FROM (SELECT 계약일자, 고객번호   , COUNT(*) AS 계약건수, SUM(계약금액) AS 계약금액 FROM 계약        WHERE 담당자ID = 9 AND 계약일자 BETWEEN '20090301' AND '20090305' GROUP BY 계약일자, 고객번호) a ORDER BY a.계약일자, a.고객번호

--3         1             |--Top(TOP EXPRESSION:((3)))

--0         0                  |--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1011],0)))

--3         1                       |--Stream Aggregate(GROUP BY:([jwjung].[dbo].[계약].[계약일자], [jwjung].[dbo].[계약].[고객번호]) DEFINE:([Expr1011]=Count(*), [Expr1005]=SUM([jwjung].[dbo].[계약].[계약금액])))

--8         1                            |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1010]) WITH ORDERED PREFETCH)

--8         1                                 |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x02]), SEEK:([jwjung].[dbo].[계약].[담당자ID]=(9) AND [jwjung].[dbo].[계약].[계약일자] >= '20090301' AND [jwjung].[dbo].[계약].[계약일자] <= '20090305') ORDERED FORWARD)

--8         8                                 |--RID Lookup(OBJECT:([jwjung].[dbo].[계약]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(3개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 11, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--정렬 연산이 사라졌을 뿐만 아니라 논리적 읽기 수도 218에서 11로 줄었다. 계약_x02 인덱스가 [담당자ID + 계약일자 + 고객번호] 순서로 정렬되어 있으므로,

--인덱스를 (키 칼럼 값의 순서대로) 따라가면서 값을 집계하면 결과 집합이 자동으로 정렬된다. 그렇게 집계를 계속하다가 [계약일자 + 고객번호] 조합이

--세 번째('TOP 3'를 기술했으므로) 바뀌었을 때 쿼리 수행을 멈추면 된다.

 

 

 

--(3) min, max 대상 컬럼을 인덱스에 추가

DROP INDEX 계약.계약_x01

GO

DROP INDEX 계약.계약_x02

GO

CREATE NONCLUSTERED INDEX 계약_x03 ON 계약 (고객번호)

GO

 

 

SELECT MIN(계약일자), MAX(계약일자)

FROM 계약

WHERE 고객번호 = 'QUICK'

GO

 

--Rows   Executes             StmtText

--1         1           SELECT MIN([계약일자]),MAX([계약일자]) FROM [계약] WHERE [고객번호]=@1

--1         1             |--Stream Aggregate(DEFINE:([Expr1004]=MIN([partialagg1006]), [Expr1005]=MAX([partialagg1007])))

--1         1                  |--Parallelism(Gather Streams)

--1         4                       |--Stream Aggregate(DEFINE:([partialagg1006]=MIN([jwjung].[dbo].[계약].[계약일자]), [partialagg1007]=MAX([jwjung].[dbo].[계약].[계약일자])))

--10220 4                            |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1010]) OPTIMIZED WITH UNORDERED PREFETCH)

--0         0                                 |--Compute Scalar(DEFINE:([Expr1009]=BmkToPage([Bmk1000])))

--10220 4                                 |    |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x03]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK') ORDERED FORWARD)

--10220 10220                         |--RID Lookup(OBJECT:([jwjung].[dbo].[계약]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(1개 행이 영향을 받음)

--테이블 '계약'. 검색 수 3, 논리적 읽기 수 10247, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--SQL Server 실행 시간:

--CPU 시간 = 32밀리초, 경과 시간 = 81밀리초

 

DROP INDEX 계약.계약_x03

GO

CREATE NONCLUSTERED INDEX 계약_x03 ON 계약 (고객번호, 계약일자)

GO

 

SELECT MIN(계약일자), MAX(계약일자)

FROM 계약

WHERE 고객번호 = 'QUICK'

GO

 

--Rows   Executes             StmtText

--1         1           SELECT MIN([계약일자]),MAX([계약일자]) FROM [계약] WHERE [고객번호]=@1

--1         1             |--Nested Loops(Inner Join)

--1         1                  |--Stream Aggregate(DEFINE:([Expr1004]=MIN([jwjung].[dbo].[계약].[계약일자])))

--1         1                  |    |--Top(TOP EXPRESSION:((1)))

--1         1                  |         |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x03]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK') ORDERED FORWARD)

--1         1                  |--Stream Aggregate(DEFINE:([Expr1005]=MAX([jwjung].[dbo].[계약].[계약일자])))

--1         1                       |--Top(TOP EXPRESSION:((1)))

--1         1                            |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x03]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK') ORDERED BACKWARD)

 

--(1개 행이 영향을 받음)

--테이블 '계약'. 검색 수 2, 논리적 읽기 수 6, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

--// min 값을 구할 때 주의 사항

ALTER TABLE 계약 ADD 해지일자 CHAR(8) NULL

GO

 

UPDATE 계약

SET 해지일자 = (CASE WHEN b.rnum = 1 THEN '20090101'

                                                                                                          WHEN b.rnum = 2 THEN '20090630'

                                                                                ELSE '20091231'

                                                                                END)

FROM 계약 a

             , (

                           SELECT TOP 3 계약번호, ROW_NUMBER() OVER(ORDER BY 계약번호) AS rnum

                           FROM 계약

                           WHERE 고객번호 = 'QUICK'

             ) b

WHERE a.계약번호 = b.계약번호

GO

 

SELECT COUNT(*) AS cnt1, COUNT(CASE WHEN 해지일자 IS NOT NULL THEN 1 END) AS cnt2

FROM 계약

GO

 

--cnt1    cnt2

--302950            3

 

--(1개 행이 영향을 받음)

--테이블 '계약'. 검색 수 5, 논리적 읽기 수 43280, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--경고: 집계 또는 다른 SET 작업에 의해 Null 값이 제거되었습니다.

 

CREATE NONCLUSTERED INDEX 계약_x04 ON 계약 (고객번호, 해지일자)

GO

 

SELECT MIN(해지일자) AS min, MAX(해지일자) AS max

FROM 계약

WHERE 고객번호 = 'QUICK'

GO

 

--Rows   Executes             StmtText

--1         1           SELECT MIN([해지일자]) [min],MAX([해지일자]) [max] FROM [계약] WHERE [고객번호]=@1

--1         1             |--Nested Loops(Inner Join)

--1         1                  |--Stream Aggregate(DEFINE:([Expr1004]=MIN([jwjung].[dbo].[계약].[해지일자])))

--1         1                  |    |--Top(TOP EXPRESSION:((1)))

--1         1                  |         |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x04]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK'),  WHERE:([jwjung].[dbo].[계약].[해지일자] IS NOT NULL) ORDERED FORWARD)

--1         1                  |--Stream Aggregate(DEFINE:([Expr1005]=MAX([jwjung].[dbo].[계약].[해지일자])))

--1         1                       |--Top(TOP EXPRESSION:((1)))

--1         1                            |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x04]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK'),  WHERE:([jwjung].[dbo].[계약].[해지일자] IS NOT NULL) ORDERED BACKWARD)

 

--(1개 행이 영향을 받음)

--테이블 '계약'. 검색 수 2, 논리적 읽기 수 41, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--CPU 시간 = 16밀리초, 경과 시간 = 26밀리초

 

-- 계약 테이블에 대한 논리적 읽기 수 41를 검색 수 2로 나누면, min이나 max를 구할 때 인덱스 페이지 I/O가 평균 21

--발생했음을 알 수 있다. 이 수치는 뭔가 이상해 보인다. 계약_x04 인덱스를 순방향이나 역방향으로 탐색하여 곧바로 한 건을

--선택했을 텐데 페이지 I/O가 왜 이렇게 많이 발생했을까?

--어디서 비효율이 발생하는지 확인하고자 min 값만 따로 구해 보았다.

 

SELECT min(해지일자)

FROM 계약

WHERE 고객번호 = 'QUICK'

GO

 

--Rows   Executes             StmtText

--1         1           SELECT MIN([해지일자]) FROM [계약] WHERE [고객번호]=@1

--1         1             |--Stream Aggregate(DEFINE:([Expr1004]=MIN([jwjung].[dbo].[계약].[해지일자])))

--1         1                  |--Top(TOP EXPRESSION:((1)))

--1         1                       |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x04]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK'),  WHERE:([jwjung].[dbo].[계약].[해지일자] IS NOT NULL) ORDERED FORWARD)

 

--(1개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 38, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--논리적 읽기 수가 38이므로 min 값을 구하는 부분에 비효율이 있음을 알 수 있다.

--실행계획의 Index Seek 오퍼레이션을 보면, [고객번호] = 'QUICK' 조건으로 인덱스를 탐색하면서 '[해지일자] IS NOT NULL' 조건으로 필터링한다.

--(집계함수는 NULL 값을 제외하고 처리한다). 전체 데이터 중 대부분이 (정확히 말하면 3건을 제외하고는) NULL 값을 가지므로, 인덱스를 한참 탐색해야만

--NULL이 아닌 값을 만나게 된다. (NULL 값은 인덱스 엔트리에서 다른 값보다 앞쪽에 있다고 설명했다.)

 

SELECT MIN(해지일자)

FROM 계약

WHERE 고객번호 = 'QUICK'

             AND 해지일자 IS NOT NULL

GO

 

--Rows   Executes             StmtText

--1         1           SELECT MIN([해지일자]) FROM [계약] WHERE [고객번호]=@1 AND [해지일자] IS NOT NULL

--1         1             |--Stream Aggregate(DEFINE:([Expr1004]=MIN([jwjung].[dbo].[계약].[해지일자])))

--3         1                  |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x04]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK' AND [jwjung].[dbo].[계약].[해지일자] IsNotNull) ORDERED FORWARD)

 

--(1개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 3, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

SELECT TOP 1 해지일자

FROM 계약

WHERE 고객번호 = 'QUICK'

             AND 해지일자 IS NOT NULL

ORDER BY 해지일자

GO

 

--Rows   Executes             StmtText

--1         1           SELECT TOP 1 해지일자 FROM 계약 WHERE 고객번호 = 'QUICK' AND 해지일자 IS NOT NULL ORDER BY 해지일자

--1         1             |--Top(TOP EXPRESSION:((1)))

--1         1                  |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x04]), SEEK:([jwjung].[dbo].[계약].[고객번호]='QUICK' AND [jwjung].[dbo].[계약].[해지일자] IsNotNull) ORDERED FORWARD)

 

--(1개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 3, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

--//(4) 순위 창 함수의 OVER 절을 만족하도록 인덱스에 컬럼 추가

SELECT a.*

             , ROW_NUMBER() OVER (PARTITION BY 계약일자 ORDER BY 계약번호)

FROM 계약 a

WHERE 고객번호 = 'QUICK'

             AND 계약일자 BETWEEN '20090301' AND '20090331'

ORDER BY 계약일자

GO

 

--(868개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 874, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--868     1           SELECT a.* , ROW_NUMBER() OVER (PARTITION BY 계약일자 ORDER BY 계약번호) FROM 계약 a WHERE 고객번호 = 'QUICK' AND 계약일자 BETWEEN '20090301' AND '20090331' ORDER BY 계약일자

--868     1             |--Sequence Project(DEFINE:([Expr1003]=row_number))

--868     1                  |--Segment

--868     1                       |--Sort(ORDER BY:([a].[계약일자] ASC, [a].[계약번호] ASC))

--868     1                            |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1006]) OPTIMIZED WITH UNORDERED PREFETCH)

--0         0                                 |--Compute Scalar(DEFINE:([Expr1005]=BmkToPage([Bmk1000])))

--868     1                                 |    |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x03] AS [a]), SEEK:([a].[고객번호]='QUICK' AND [a].[계약일자] >= '20090301' AND [a].[계약일자] <= '20090331') ORDERED FORWARD)

--868     868                             |--RID Lookup(OBJECT:([jwjung].[dbo].[계약] AS [a]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

DROP INDEX 계약.계약_x03

GO

CREATE NONCLUSTERED INDEX 계약_x03 ON 계약 (고객번호, 계약일자, 계약번호)

GO

 

SELECT a.*

             , ROW_NUMBER() OVER (PARTITION BY 계약일자 ORDER BY 계약번호)

FROM 계약 a

WHERE 고객번호 = 'QUICK'

             AND 계약일자 BETWEEN '20090301' AND '20090331'

ORDER BY 계약일자

GO

 

--Rows   Executes             StmtText

--868     1           SELECT a.* , ROW_NUMBER() OVER (PARTITION BY 계약일자 ORDER BY 계약번호) FROM 계약 a WHERE 고객번호 = 'QUICK' AND 계약일자 BETWEEN '20090301' AND '20090331' ORDER BY 계약일자

--868     1             |--Sequence Project(DEFINE:([Expr1003]=row_number))

--868     1                  |--Segment

--868     1                       |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1005]) WITH ORDERED PREFETCH)

--868     1                            |--Index Seek(OBJECT:([jwjung].[dbo].[계약].[계약_x03] AS [a]), SEEK:([a].[고객번호]='QUICK' AND [a].[계약일자] >= '20090301' AND [a].[계약일자] <= '20090331') ORDERED FORWARD)

--868     868                        |--RID Lookup(OBJECT:([jwjung].[dbo].[계약] AS [a]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(868개 행이 영향을 받음)

--테이블 '계약'. 검색 수 1, 논리적 읽기 수 875, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

 

 

SET STATISTICS PROFILE OFF

SET STATISTICS TIME OFF

SET STATISTICS IO OFF

 

반응형

'연구개발 > DBA' 카테고리의 다른 글

페이징  (0) 2012.01.26
정렬의 최적화 (03.정렬을 최소화하는 SQL 작성)  (0) 2012.01.25
정렬의 최적화 (01.정렬이 발생하는 경우)  (0) 2012.01.24
조인 순서와 성능  (0) 2012.01.23
조인 join  (0) 2012.01.20
반응형

/*

정렬의 최적화

*/

 

/* 01.정렬이 발생하는 경우 */

 

------기본 셋팅

USE jwjung;

GO

 

IF OBJECT_ID('상품') IS NOT NULL

             DROP TABLE 상품

GO

IF OBJECT_ID('일별상품가격') IS NOT NULL

             DROP TABLE 일별상품가격

GO

 

CREATE TABLE 상품 (

             상품ID                  INT                      NOT NULL

             ,상품명                 SYSNAME           NOT NULL

             ,입력일시              DATETIME           NULL

             ,상품설명              CHAR(2000)       NULL

);

GO

CREATE TABLE 일별상품가격 (

             상품ID                  INT                      NOT NULL

             ,기준일자              CHAR(8)             NOT NULL

             ,상품가격              NUMERIC(12, 0) NOT NULL

             ,비고                                 VARCHAR(1000) NULL

);

GO

 

--상품 테이블에 데이터 입력

INSERT INTO 상품

SELECT TOP 100

             id, name, crdate, name

FROM master.dbo.sysobjects

ORDER BY id DESC

GO

--(100개 행이 영향을 받음)

 

--일별상품가격 테이블에 데이터 입력

INSERT INTO 일별상품가격

SELECT a.상품ID

             , CONVERT(VARCHAR(8), CONVERT(DATETIME, '20090101', 112) + b.rnum - 1, 112)

             , 1000 - b.rnum

             , '비고 : ' + CONVERT(VARCHAR, b.rnum)

FROM 상품 a

             , (

                           SELECT ROW_NUMBER() OVER(ORDER BY a.OrderID) AS rnum

                           FROM Northwind.dbo.Orders a, Northwind.dbo.Orders b

             ) b

WHERE b.rnum <= 365

GO

--(36500개 행이 영향을 받음)

 

ALTER TABLE 상품

ADD CONSTRAINT 상품_pk PRIMARY KEY NONCLUSTERED (상품ID)

GO

 

ALTER TABLE 일별상품가격

ADD CONSTRAINT 일별상품가격_pk PRIMARY KEY NONCLUSTERED (상품ID, 기준일자)

GO

 

EXEC sp_helpindex '상품';

EXEC sp_helpindex '일별상품가격';

 

SELECT * FROM 상품;

GO

--(100개 행이 영향을 받음)

--테이블 '상품'. 검색 수 1, 논리적 읽기 수 34, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

SELECT * FROM 일별상품가격;

GO

--(36500개 행이 영향을 받음)

--테이블 '일별상품가격'. 검색 수 1, 논리적 읽기 수 198, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

DBCC IND('jwjung', '상품', 1);

DBCC IND('jwjung', '상품', 2);

 

DBCC TRACEON(3604);

DBCC PAGE(jwjung, 1, 151, 3);

DBCC TRACEOFF(3604);

 

SET STATISTICS PROFILE ON

SET STATISTICS TIME ON

SET STATISTICS IO ON

--(1) Order by 사용 시

--쿼리에 기술된 order by 조건과 인덱스의 정렬 순서가 일치하지 않으면 정렬이 발생한다.

--둘의 정렬순서가 일치하면 정렬이 발생하지 않지만, 이때도 order by 조건의 컬럼을 가공하면

--정렬이 발생하므로 주의해야 한다.

 

--//order by 조건과 인덱스가 일치하지 않을 때

SELECT TOP 1 *

FROM 일별상품가격 WITH (INDEX(일별상품가격_pk))

ORDER BY 상품ID, 기준일자 DESC

GO

--Rows   Executes             StmtText

--1         1           SELECT TOP 1 * FROM 일별상품가격 WITH (INDEX(일별상품가격_pk)) ORDER BY 상품ID, 기준일자 DESC

--1         1             |--Top(TOP EXPRESSION:((1)))

--1         1                  |--Parallelism(Gather Streams, ORDER BY:([jwjung].[dbo].[일별상품가격].[상품ID] ASC, [jwjung].[dbo].[일별상품가격].[기준일자] DESC))

--3         4                       |--Sort(TOP 1, ORDER BY:([jwjung].[dbo].[일별상품가격].[상품ID] ASC, [jwjung].[dbo].[일별상품가격].[기준일자] DESC))

--36500 4                           |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1005]) WITH UNORDERED PREFETCH)

--36500 4                                 |--Index Scan(OBJECT:([jwjung].[dbo].[일별상품가격].[일별상품가격_pk]))

--36500 36500                         |--RID Lookup(OBJECT:([jwjung].[dbo].[일별상품가격]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

 

--(1개 행이 영향을 받음)

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '일별상품가격'. 검색 수 4, 논리적 읽기 수 36813, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--//적절한 인덱스가 있지만, order by 컬럼을 가공했을 때

--인덱스를 효율적(또는 정상적)으로 액세스할 수 있도록 검색조건을 기술하더라도 order by 컬럼을 가공하면 정렬이 발생한다.

SELECT TOP 1 *

FROM 일별상품가격

WHERE 기준일자 BETWEEN '20090301' AND '20090315'

ORDER BY CONVERT(DATETIME, 기준일자, 112)

GO

--Rows   Executes             StmtText

--1         1           SELECT TOP 1 * FROM 일별상품가격 WHERE 기준일자 BETWEEN '20090301' AND '20090315' ORDER BY CONVERT(DATETIME, 기준일자, 112)

--1         1             |--Sort(TOP 1, ORDER BY:([Expr1004] ASC))

--0         0                  |--Compute Scalar(DEFINE:([Expr1004]=CONVERT(datetime,[jwjung].[dbo].[일별상품가격].[기준일자],112)))

--1500   1                       |--Table Scan(OBJECT:([jwjung].[dbo].[일별상품가격]), WHERE:([jwjung].[dbo].[일별상품가격].[기준일자]>='20090301' AND [jwjung].[dbo].[일별상품가격].[기준일자]<='20090315'))

 

--(1개 행이 영향을 받음)

--테이블 '일별상품가격'. 검색 수 1, 논리적 읽기 수 198, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--불필요한 CONVERT 함수 때문에 인덱스의 정렬 순서를 활용하지 못해서 정렬이 발생했다.

--CONVERT 함수 제거 시 논리적 읽기수 참고

SELECT TOP 1 *

FROM 일별상품가격

WHERE 기준일자 BETWEEN '20090301' AND '20090315'

GO

--(1개 행이 영향을 받음)

--테이블 '일별상품가격'. 검색 수 1, 논리적 읽기 수 32, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

 

 

--//group by 사용 시

--group by 조건에 의해서도 정렬 연산이 발생할 수 있다. 이때는 실행계획에 집계(Aggregate) 연산이 함께 발생한다.

SELECT 기준일자, MAX(상품가격)

FROM 일별상품가격

WHERE 기준일자 BETWEEN '20090301' AND '20090305'

GROUP BY 기준일자

GO

 

--(5개 행이 영향을 받음)

--테이블 '일별상품가격'. 검색 수 1, 논리적 읽기 수 198, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--5         1           SELECT 기준일자, MAX(상품가격) FROM 일별상품가격 WHERE 기준일자 BETWEEN '20090301' AND '20090305' GROUP BY 기준일자

--5         1             |--Stream Aggregate(GROUP BY:([jwjung].[dbo].[일별상품가격].[기준일자]) DEFINE:([Expr1004]=MAX([jwjung].[dbo].[일별상품가격].[상품가격])))

--500     1                  |--Sort(ORDER BY:([jwjung].[dbo].[일별상품가격].[기준일자] ASC))

--500     1                       |--Table Scan(OBJECT:([jwjung].[dbo].[일별상품가격]), WHERE:([jwjung].[dbo].[일별상품가격].[기준일자]>='20090301' AND [jwjung].[dbo].[일별상품가격].[기준일자]<='20090305'))

 

 

--//distinct 사용 시

--select-list distinct를 사용하면 결과 집합에서 중복된 값이 제거되어야 하므로, 이 과정에서 정렬 연산이 발생할 수 있다.

SELECT DISTINCT 상품명

FROM 상품

WHERE 상품명 LIKE 'all%'

GO

--Rows   Executes             StmtText

--5         1           SELECT DISTINCT 상품명 FROM 상품 WHERE 상품명 LIKE 'all%'

--5         1             |--Sort(DISTINCT ORDER BY:([jwjung].[dbo].[상품].[상품명] ASC))

--5         1                  |--Table Scan(OBJECT:([jwjung].[dbo].[상품]), WHERE:([jwjung].[dbo].[상품].[상품명] like N'all%'))

 

--//union 사용 시

--중복된 로우가 제거되고서 결과 집합이 출력

DECLARE @기준일자1 CHAR(8), @기준일자2 CHAR(8)

SET @기준일자1 = '20090101'

SET @기준일자2 = '20091231'

 

SELECT 상품ID, 기준일자, 상품가격

FROM 일별상품가격

WHERE 기준일자 = @기준일자1

UNION

SELECT 상품ID, 기준일자, 상품가격

FROM 일별상품가격

WHERE 기준일자 = @기준일자2

GO

--Rows   Executes             StmtText

--200     1           SELECT 상품ID, 기준일자, 상품가격 FROM 일별상품가격 WHERE 기준일자 = @기준일자1 UNION SELECT 상품ID, 기준일자, 상품가격 FROM 일별상품가격 WHERE 기준일자 = @기준일자2

--200     1             |--Sort(DISTINCT ORDER BY:([Union1008] ASC, [Union1009] ASC, [Union1010] ASC))

--200     1                  |--Concatenation

--100     1                       |--Table Scan(OBJECT:([jwjung].[dbo].[일별상품가격]), WHERE:([jwjung].[dbo].[일별상품가격].[기준일자]=[@기준일자1]))

--100     1                       |--Table Scan(OBJECT:([jwjung].[dbo].[일별상품가격]), WHERE:([jwjung].[dbo].[일별상품가격].[기준일자]=[@기준일자2]))

 

 

--//순위 창 함수 사용 시

SELECT a.*

             , ROW_NUMBER() OVER(ORDER BY 입력일시 DESC)

FROM 상품 a

WHERE 상품명 LIKE 'a%'

GO

--Rows   Executes             StmtText

--5         1             |--Sequence Project(DEFINE:([Expr1003]=row_number))

--5         1                  |--Segment

--5         1                       |--Sort(ORDER BY:([a].[입력일시] DESC))

--5         1                            |--Table Scan(OBJECT:([jwjung].[dbo].[상품] AS [a]), WHERE:([jwjung].[dbo].[상품].[상품명] as [a].[상품명] like N'a%'))

 

 

--//병합 조인 시

--먼저, 상품 테이블에 [상품명 + 상품ID] 컬럼으로 구성된 상품_x01 인덱스를 추가하고

CREATE NONCLUSTERED INDEX 상품_x01 ON 상품(상품명, 상품ID)

GO

 

SELECT TOP 10 *

FROM 상품 a INNER MERGE JOIN 일별상품가격 b

             ON a.상품ID = b.상품ID AND b.기준일자 BETWEEN '20090301' AND '20090331'

WHERE a.상품명 = 'spt_monitor'

ORDER BY b.상품ID, b.기준일자

GO

--Rows   Executes             StmtText

--10       1           SELECT TOP 10 * FROM 상품 a INNER MERGE JOIN 일별상품가격 b ON a.상품ID = b.상품ID AND b.기준일자 BETWEEN '20090301' AND '20090331' WHERE a.상품명 = 'spt_monitor' ORDER BY b.상품ID, b.기준일자

--10       1             |--Sort(TOP 10, ORDER BY:([a].[상품ID] ASC, [b].[기준일자] ASC))

--31       1                  |--Merge Join(Inner Join, MERGE:([a].[상품ID])=([b].[상품ID]), RESIDUAL:([jwjung].[dbo].[상품].[상품ID] as [a].[상품ID]=[jwjung].[dbo].[일별상품가격].[상품ID] as [b].[상품ID]))

--1         1                       |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))

--1         1                       |    |--Index Seek(OBJECT:([jwjung].[dbo].[상품].[상품_x01] AS [a]), SEEK:([a].[상품명]=N'spt_monitor') ORDERED FORWARD)

--1         1                       |    |--RID Lookup(OBJECT:([jwjung].[dbo].[상품] AS [a]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

--2822   1                       |--Sort(ORDER BY:([b].[상품ID] ASC))

--3100   1                            |--Table Scan(OBJECT:([jwjung].[dbo].[일별상품가격] AS [b]), WHERE:([jwjung].[dbo].[일별상품가격].[기준일자] as [b].[기준일자]>='20090301' AND [jwjung].[dbo].[일별상품가격].[기준일자] as [b].[기준일자]<='20090331'))

 

--(10개 행이 영향을 받음)

--테이블 '일별상품가격'. 검색 수 1, 논리적 읽기 수 198, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '상품'. 검색 수 1, 논리적 읽기 수 3, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--NL 조인하도록 조인 힌트를 변경하고서 테스트한 결과

SELECT TOP 10 *

FROM 상품 a INNER LOOP JOIN 일별상품가격 b

             ON a.상품ID = b.상품ID AND b.기준일자 BETWEEN '20090301' AND '20090331'

WHERE a.상품명 = 'spt_monitor'

ORDER BY b.상품ID, b.기준일자

GO

 

--Rows   Executes             StmtText

--10       1           SELECT TOP 10 * FROM 상품 a INNER LOOP JOIN 일별상품가격 b ON a.상품ID = b.상품ID AND b.기준일자 BETWEEN '20090301' AND '20090331' WHERE a.상품명 = 'spt_monitor' ORDER BY b.상품ID, b.기준일자

--10       1             |--Top(TOP EXPRESSION:((10)))

--10       1                  |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1003], [Expr1006]) WITH ORDERED PREFETCH)

--10       1                       |--Nested Loops(Inner Join, OUTER REFERENCES:([a].[상품ID]))

--1         1                       |    |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))

--1         1                       |    |    |--Index Seek(OBJECT:([jwjung].[dbo].[상품].[상품_x01] AS [a]), SEEK:([a].[상품명]=N'spt_monitor') ORDERED FORWARD)

--1         1                      |    |    |--RID Lookup(OBJECT:([jwjung].[dbo].[상품] AS [a]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)

--10       1                       |    |--Index Seek(OBJECT:([jwjung].[dbo].[일별상품가격].[일별상품가격_pk] AS [b]), SEEK:([b].[상품ID]=[jwjung].[dbo].[상품].[상품ID] as [a].[상품ID] AND [b].[기준일자] >= '20090301' AND [b].[기준일자] <= '20090331') ORDERED FORWARD)

--10       10                     |--RID Lookup(OBJECT:([jwjung].[dbo].[일별상품가격] AS [b]), SEEK:([Bmk1003]=[Bmk1003]) LOOKUP ORDERED FORWARD)

 

--(10개 행이 영향을 받음)

--테이블 '일별상품가격'. 검색 수 1, 논리적 읽기 수 12, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '상품'. 검색 수 1, 논리적 읽기 수 3, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

EXEC sp_helpindex '상품';

--index_name      index_description             index_keys

--상품_pk             nonclustered, unique, primary key located on PRIMARY         상품ID

--상품_x01           nonclustered located on PRIMARY 상품명, 상품ID

 

EXEC sp_helpindex '일별상품가격';

--index_name      index_description             index_keys

--일별상품가격_pk nonclustered, unique, primary key located on PRIMARY         상품ID, 기준일자

 

 

SET STATISTICS PROFILE OFF

SET STATISTICS TIME OFF

SET STATISTICS IO OFF

 

반응형

'연구개발 > DBA' 카테고리의 다른 글

정렬의 최적화 (03.정렬을 최소화하는 SQL 작성)  (0) 2012.01.25
정렬의 최적화 (02.인덱스 정렬을 제거)  (0) 2012.01.24
조인 순서와 성능  (0) 2012.01.23
조인 join  (0) 2012.01.20
면접질의  (0) 2012.01.20
반응형

/* 조인 순서와 성능 */

 

/*

1. NL 조인의 순서

             NL 조인은 랜덤 액세스 위주로 수행되므로, 대부분 후행 테이블을 액세스하는 횟수에 따라 성능이 달라진다.

*/

 

--// 큰 집합을 먼저 액세스했을 때

USE Northwind

GO

 

EXEC sp_helpindex 'Orders';

EXEC sp_helpindex '[Order Details]';

 

SELECT COUNT(*) FROM Orders;

GO

--830

 

SELECT COUNT(*) FROM [Order Details];

GO

--2155

 

SET STATISTICS PROFILE ON

SET STATISTICS IO ON

SET STATISTICS TIME ON

GO

 

SELECT *

FROM [Order Details] b, Orders a

WHERE a.OrderID = b.OrderID

OPTION (FORCE ORDER, LOOP JOIN)

GO

--(2155개 행이 영향을 받음)

--테이블 'Orders'. 검색 수 0, 논리적 읽기 수 4453, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Order Details'. 검색 수 1, 논리적 읽기 수 11, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--2155   1           SELECT * FROM [Order Details] b, Orders a WHERE a.OrderID = b.OrderID OPTION (FORCE ORDER, LOOP JOIN)

--2155   1             |--Nested Loops(Inner Join, OUTER REFERENCES:([b].[OrderID], [Expr1004]) WITH UNORDERED PREFETCH)

--2155   1                  |--Clustered Index Scan(OBJECT:([Northwind].[dbo].[Order Details].[PK_Order_Details] AS [b]))

--2155   2155            |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Orders].[PK_Orders] AS [a]), SEEK:([a].[OrderID]=[Northwind].[dbo].[Order Details].[OrderID] as [b].[OrderID]) ORDERED FORWARD)

 

--양쪽 테이블 모두 클러스터형 인덱스로 구성되어 있으므로 북마크 룩업에 의한 랜덤 액세스가 추가로 발생하지는 않는다.

 

 

--//작은 집합을 먼저 액세스했을 때

SELECT *

FROM Orders a, [Order Details] b

WHERE a.OrderID = b.OrderID

OPTION (FORCE ORDER, LOOP JOIN)

GO

--(2155개 행이 영향을 받음)

--테이블 'Order Details'. 검색 수 830, 논리적 읽기 수 1672, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Orders'. 검색 수 1, 논리적 읽기 수 22, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--2155   1           SELECT * FROM Orders a, [Order Details] b WHERE a.OrderID = b.OrderID OPTION (FORCE ORDER, LOOP JOIN)

--2155   1             |--Nested Loops(Inner Join, OUTER REFERENCES:([a].[OrderID]))

--830     1                  |--Clustered Index Scan(OBJECT:([Northwind].[dbo].[Orders].[PK_Orders] AS [a]))

--2155   830              |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[Order Details].[PK_Order_Details] AS [b]), SEEK:([b].[OrderID]=[Northwind].[dbo].[Orders].[OrderID] as [a].[OrderID]) ORDERED FORWARD)

 

 

/* 큰 집합을 먼저 액세스했을 때는 페이지 I/O가 총 4,464(=4,453+11)개 발생했고,

작은 집합을 먼저 액세스했을 때는 페이지 I/O 1,694(=1,672+22)개 발생했다.

NL 조인은 작은 집합을 먼저 액세스하는 게 '일반적으로' 유리하다.

클러스터형 여부가 조인 순서의 효율성에 영향을 미치기도 하지만, 양쪽 테이블에 같은 조건의 인덱스가 있다면 작은 쪽을 먼저 액세스하는 게 유리하다 */

 

 

/*

2. 해시 조인의 순서

해시 조인은 해시 테이블을 구성하는 Build Input의 크기가 성능에 큰 영향을 미친다.

Build Input이 가용 메모리보다 작으면 해시 테이블을 메모리 안에 생성하게 된다.

이렇게 메모리 안에서 한 번에 처리되는 방식을 인-메모리 해시 조인이라고 한다.

만약 Build Input이 가용 메모리를 초과하면 유예(Grace) 해시 조인 또는 재귀(Recursive) 해시 조인으로 수행되는데,

-메모리 해시 조인과는 달리 대상 집합을 분할하여 단계적으로 조인하는 복잡한 과정을 거치게 된다.

*/

 

--//테이블 두 개를 해시 조인할 때

--해시 조인은 양쪽 테이블을 각각 한 번만 액세스하므로 조인 순서를 바꿔도 테이블 검색 수와 논리적 읽기 수는 달라지지 않는다.

--따라서 해시 조인은 CPU와 경과 시간을 기준으로 성능 차이를 비교해야 한다. 조인에 참여하는 두 집합의 크기가 극단적으로

--달라야 성능차이를 확실히 느낄 수 있을 것이다.

 

--=====================================================

USE jwjung

GO

ALTER DATABASE jwjung ADD FILE

(

             name = jwjung_dat2

             ,filename = 'C:\Program Files\Microsoft SQL Server\MSSQL10_50.MSSQLSERVER\MSSQL\DATA\jwjung_dat2.ndf'

             ,size = 100mb

             ,maxsize = 500mb

             ,filegrowth = 50mb

)

GO

 

SELECT TOP 100000

             identity(int, 1, 1) as 고객id

             , CONVERT(VARCHAR(1),

                           CASE WHEN ABS(a.id)%3 = 0 THEN 'A'

                                                                  WHEN ABS(a.id)%3 = 1 THEN 'B'

                                                                  WHEN ABS(a.id)%3 = 2 THEN 'C'

                           END) as 거주지역코드

             , a.*

INTO 고객

FROM master.dbo.syscolumns a

                           , master.dbo.sysindexes b

GO

 

SELECT TOP 5000000

             identity(int, 1, 1) as 계약id, a.고객id

             , CONVERT(DECIMAL(12, 2), b.id) as 계약금액

             , CONVERT(DECIMAL(12, 2), b.id) as 납입금액

             , CONVERT(VARCHAR(1),

                           CASE WHEN b.id%5 IN (0, 1) THEN '1'

                                                     WHEN b.id%5 IN (2, 3) THEN '2'

                           ELSE '9'

                           END) as 계약상태코드

             , b.*

INTO 계약

FROM 고객 a, (SELECT TOP 50 * FROM master.dbo.syscolumns) b

GO

--=====================================================

 

 

SELECT b.거주지역코드, SUM(a.계약금액), SUM(a.납입금액)

FROM 계약 a, 고객 b

WHERE a.고객id = b.고객id

GROUP BY b.거주지역코드

OPTION (FORCE ORDER, HASH JOIN)

GO

 

--(3개 행이 영향을 받음)

--테이블 '계약'. 검색 수 5, 논리적 읽기 수 82070, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '고객'. 검색 수 5, 논리적 읽기 수 1418, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--3         1           SELECT b.거주지역코드, SUM(a.계약금액), SUM(a.납입금액) FROM 계약 a, 고객 b WHERE a.고객id = b.고객id GROUP BY b.거주지역코드 OPTION (FORCE ORDER, HASH JOIN)

--0         0             |--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [globalagg1009]=(0) THEN NULL ELSE [globalagg1011] END, [Expr1007]=CASE WHEN [globalagg1013]=(0) THEN NULL ELSE [globalagg1015] END))

--3         1                  |--Stream Aggregate(GROUP BY:([b].[거주지역코드]) DEFINE:([globalagg1009]=SUM([partialagg1008]), [globalagg1011]=SUM([partialagg1010]), [globalagg1013]=SUM([partialagg1012]), [globalagg1015]=SUM([partialagg1014])))

--12       1                       |--Sort(ORDER BY:([b].[거주지역코드] ASC))

--12       1                            |--Parallelism(Gather Streams)

--12       4                                 |--Hash Match(Partial Aggregate, HASH:([b].[거주지역코드]), RESIDUAL:([jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드] = [jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드]) DEFINE:([partialagg1008]=COUNT_BIG([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1010]=SUM([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1012]=COUNT_BIG([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액]), [partialagg1014]=SUM([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액])))

--5000000          4                                      |--Hash Match(Inner Join, HASH:([a].[고객id])=([b].[고객id]))

--5000000          4                                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([a].[고객id]))

--5000000          4                                           |    |--Table Scan(OBJECT:([jwjung].[dbo].[계약] AS [a]))

--100000            4                                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([b].[고객id]))

--100000            4                                                |--Table Scan(OBJECT:([jwjung].[dbo].[고객] AS [b]))

 

--   CPU 시간 = 14195밀리초, 경과 시간 = 3809밀리초

 

 

SELECT b.거주지역코드, SUM(a.계약금액), SUM(a.납입금액)

FROM 고객 b, 계약 a

WHERE a.고객id = b.고객id

GROUP BY b.거주지역코드

OPTION (FORCE ORDER, HASH JOIN)

GO

--(3개 행이 영향을 받음)

--테이블 '고객'. 검색 수 5, 논리적 읽기 수 1418, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '계약'. 검색 수 5, 논리적 읽기 수 82070, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--3         1           SELECT b.거주지역코드, SUM(a.계약금액), SUM(a.납입금액) FROM 고객 b, 계약 a WHERE a.고객id = b.고객id GROUP BY b.거주지역코드 OPTION (FORCE ORDER, HASH JOIN)

--0         0             |--Compute Scalar(DEFINE:([Expr1006]=CASE WHEN [globalagg1009]=(0) THEN NULL ELSE [globalagg1011] END, [Expr1007]=CASE WHEN [globalagg1013]=(0) THEN NULL ELSE [globalagg1015] END))

--3         1                  |--Stream Aggregate(GROUP BY:([b].[거주지역코드]) DEFINE:([globalagg1009]=SUM([partialagg1008]), [globalagg1011]=SUM([partialagg1010]), [globalagg1013]=SUM([partialagg1012]), [globalagg1015]=SUM([partialagg1014])))

--12       1                       |--Sort(ORDER BY:([b].[거주지역코드] ASC))

--12       1                            |--Parallelism(Gather Streams)

--12       4                                 |--Hash Match(Partial Aggregate, HASH:([b].[거주지역코드]), RESIDUAL:([jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드] = [jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드]) DEFINE:([partialagg1008]=COUNT_BIG([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1010]=SUM([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1012]=COUNT_BIG([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액]), [partialagg1014]=SUM([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액])))

--5000000          4                                      |--Hash Match(Inner Join, HASH:([b].[고객id])=([a].[고객id]))

--100000            4                                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([b].[고객id]))

--100000            4                                           |    |--Table Scan(OBJECT:([jwjung].[dbo].[고객] AS [b]))

--5000000          4                                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([a].[고객id]))

--5000000          4                                                |--Table Scan(OBJECT:([jwjung].[dbo].[계약] AS [a]))

 

--CPU 시간 = 14086밀리초, 경과 시간 = 3727밀리초

 

 

--해시 버킷에 중복된 값이 얼마나 많으냐에 따라 해시 조인의 효율이 달라지기도 하지만,

--대부분 작은 집합을 먼저 액세스하는 게 유리하다.

 

 

/*

3. 테이블 세 개 이상을 해시 조인할 때

해시 조인할 때는 작은 집합을 먼저 액세스하는 게 유리하다. 그러면 1:M:1 관계를 맺고 있는 테이블 세 개를 해시 조인하고자 할 때는

어떤 순서가 유리할까? 집합 크기를 확실히 줄일 수 있는 검색조건이 있으면 그 조건이 먼저 적용되도록 조인 순서를 조정하면 된다.

그러나 별다른 검색조건이 없어서 어떻게 조인해도 집합 크기가 줄어들지 않는다면 순서를 결정하기가 쉽지 않다.

*/

 

USE jwjung

GO

 

SELECT TOP 10

             IDENTITY(INT, 0, 1) as 코드id

             , CONVERT(VARCHAR(1), 'Y') as 사용여부

INTO 코드

FROM master.dbo.syscolumns a

GO

 

ALTER TABLE 코드 ADD CONSTRAINT 코드_pk PRIMARY KEY CLUSTERED (코드id)

GO

 

SELECT b.거주지역코드, a.계약상태코드, SUM(a.계약금액), SUM(a.납입금액)

FROM 코드 c, 계약 a, 고객 b

WHERE a.고객id = b.고객id

             AND a.계약상태코드 = c.코드id

             AND c.사용여부 = 'Y'

GROUP BY b.거주지역코드, a.계약상태코드

OPTION (FORCE ORDER, HASH JOIN)

GO

 

--CPU 시간 = 26785밀리초, 경과 시간 = 7980밀리초

 

--(6개 행이 영향을 받음)

--테이블 '코드'. 검색 수 1, 논리적 읽기 수 2, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '계약'. 검색 수 5, 논리적 읽기 수 82070, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '고객'. 검색 수 5, 논리적 읽기 수 1418, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

--Rows   Executes             StmtText

--6         1           SELECT b.거주지역코드, a.계약상태코드, SUM(a.계약금액), SUM(a.납입금액) FROM 코드 c, 계약 a, 고객 b WHERE a.고객id = b.고객id             AND a.계약상태코드 = c.코드id AND c.사용여부 = 'Y' GROUP BY b.거주지역코드, a.계약상태코드 OPTION (FORCE ORDER, HASH JOIN)

--0         0             |--Compute Scalar(DEFINE:([Expr1008]=CASE WHEN [globalagg1012]=(0) THEN NULL ELSE [globalagg1014] END, [Expr1009]=CASE WHEN [globalagg1016]=(0) THEN NULL ELSE [globalagg1018] END))

--6         1                  |--Stream Aggregate(GROUP BY:([a].[계약상태코드], [b].[거주지역코드]) DEFINE:([globalagg1012]=SUM([partialagg1011]), [globalagg1014]=SUM([partialagg1013]), [globalagg1016]=SUM([partialagg1015]), [globalagg1018]=SUM([partialagg1017])))

--24       1                       |--Sort(ORDER BY:([a].[계약상태코드] ASC, [b].[거주지역코드] ASC))

--24       1                            |--Parallelism(Gather Streams)

--24       4                                 |--Hash Match(Partial Aggregate, HASH:([b].[거주지역코드], [a].[계약상태코드]), RESIDUAL:([jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드] = [jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드] AND [jwjung].[dbo].[계약].[계약상태코드] as [a].[계약상태코드] = [jwjung].[dbo].[계약].[계약상태코드] as [a].[계약상태코드]) DEFINE:([partialagg1011]=COUNT_BIG([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1013]=SUM([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1015]=COUNT_BIG([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액]), [partialagg1017]=SUM([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액])))

--5000000          4                                      |--Hash Match(Inner Join, HASH:([a].[고객id])=([b].[고객id]))

--5000000          4                                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([a].[고객id]))

--5000000          4                                           |    |--Hash Match(Inner Join, HASH:([c].[코드id])=([Expr1010]), RESIDUAL:([Expr1010]=[jwjung].[dbo].[코드].[코드id] as [c].[코드id]))

--40       4                                           |         |--Parallelism(Distribute Streams, Broadcast Partitioning)

--10       1                                           |         |    |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[코드].[코드_pk] AS [c]), WHERE:([jwjung].[dbo].[코드].[사용여부] as [c].[사용여부]='Y'))

--0         0                                           |         |--Compute Scalar(DEFINE:([Expr1010]=CONVERT_IMPLICIT(int,[jwjung].[dbo].[계약].[계약상태코드] as [a].[계약상태코드],0)))

--5000000          4                                           |              |--Table Scan(OBJECT:([jwjung].[dbo].[계약] AS [a]))

--100000            4                                           |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([b].[고객id]))

--100000            4                                                |--Table Scan(OBJECT:([jwjung].[dbo].[고객] AS [b]))

 

--코드 테이블에서 추출한 10건으로 해시 테이블을 생성하고 계약 테이블을 스캔하면서 해시 테이블을 500만 번 탐색했다.

--두 테이블이 코드->계약의 이상적인 순서로 조인하여 500만 건짜리 중간 집합을 만들었다.

--이 집합으로 다시 해시 테이블을 생성하고 고객 테이블을 스캔하면서(두 번째로 만들어진) 해시 테이블을 10만 번 탐색했다.

--이번에는 조인 순서에 문제가 있다. 500만 건으로 해시 테이블을 생성하고서 10만 건으로 이를 탐색한 것이다.

--그렇다고 고객->계약->코드 순서로 조인하면 더 비효율적이다.(테스트결과)

 

--효율적인 순서로 조인하도록 쿼리를 변경하고서 실행

SELECT x.거주지역코드, x.계약상태코드, SUM(x.계약금액), SUM(x.납입금액)

FROM 코드 c,

             (

             SELECT b.거주지역코드, a.계약상태코드, a.계약금액, a.납입금액

             FROM 고객 b INNER HASH JOIN 계약 a

                           ON a.고객id = b.고객id

             ) x

WHERE x.계약상태코드 = c.코드id

             AND c.사용여부 = 'Y'

GROUP BY x.거주지역코드, x.계약상태코드

OPTION (FORCE ORDER, HASH JOIN)

GO

 

--Rows   Executes             StmtText

--6         1           SELECT x.거주지역코드, x.계약상태코드, SUM(x.계약금액), SUM(x.납입금액) FROM 코드 c, (             SELECT b.거주지역코드, a.계약상태코드, a.계약금액, a.납입금액 FROM 고객 b INNER HASH JOIN 계약 a        ON a.고객id = b.고객id) x WHERE x.계약상태코드 = c.코드id AND c.사용여부 = 'Y' GROUP BY x.거주지역코드, x.계약상태코드 OPTION (FORCE ORDER, HASH JOIN)

--0         0             |--Compute Scalar(DEFINE:([Expr1008]=CASE WHEN [globalagg1012]=(0) THEN NULL ELSE [globalagg1014] END, [Expr1009]=CASE WHEN [globalagg1016]=(0) THEN NULL ELSE [globalagg1018] END))

--6         1                  |--Stream Aggregate(GROUP BY:([a].[계약상태코드], [b].[거주지역코드]) DEFINE:([globalagg1012]=SUM([partialagg1011]), [globalagg1014]=SUM([partialagg1013]), [globalagg1016]=SUM([partialagg1015]), [globalagg1018]=SUM([partialagg1017])))

--24       1                       |--Sort(ORDER BY:([a].[계약상태코드] ASC, [b].[거주지역코드] ASC))

--24       1                            |--Parallelism(Gather Streams)

--24       4                                 |--Hash Match(Partial Aggregate, HASH:([b].[거주지역코드], [a].[계약상태코드]), RESIDUAL:([jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드] = [jwjung].[dbo].[고객].[거주지역코드] as [b].[거주지역코드] AND [jwjung].[dbo].[계약].[계약상태코드] as [a].[계약상태코드] = [jwjung].[dbo].[계약].[계약상태코드] as [a].[계약상태코드]) DEFINE:([partialagg1011]=COUNT_BIG([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1013]=SUM([jwjung].[dbo].[계약].[계약금액] as [a].[계약금액]), [partialagg1015]=COUNT_BIG([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액]), [partialagg1017]=SUM([jwjung].[dbo].[계약].[납입금액] as [a].[납입금액])))

--5000000          4                                      |--Hash Match(Inner Join, HASH:([c].[코드id])=([Expr1010]), RESIDUAL:([Expr1010]=[jwjung].[dbo].[코드].[코드id] as [c].[코드id]))

--40       4                                           |--Parallelism(Distribute Streams, Broadcast Partitioning)

--10       1                                           |    |--Clustered Index Scan(OBJECT:([jwjung].[dbo].[코드].[코드_pk] AS [c]), WHERE:([jwjung].[dbo].[코드].[사용여부] as [c].[사용여부]='Y'))

--0         0                                           |--Compute Scalar(DEFINE:([Expr1010]=CONVERT_IMPLICIT(int,[jwjung].[dbo].[계약].[계약상태코드] as [a].[계약상태코드],0)))

--5000000          4                                                |--Hash Match(Inner Join, HASH:([b].[고객id])=([a].[고객id]))

--100000            4                                                     |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([b].[고객id]))

--100000            4                                                     |    |--Table Scan(OBJECT:([jwjung].[dbo].[고객] AS [b]))

--5000000          4                                                     |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([a].[고객id]))

--5000000          4                                                          |--Table Scan(OBJECT:([jwjung].[dbo].[계약] AS [a]))

 

--(6개 행이 영향을 받음)

--테이블 '코드'. 검색 수 1, 논리적 읽기 수 2, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '고객'. 검색 수 5, 논리적 읽기 수 1418, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 '계약'. 검색 수 5, 논리적 읽기 수 82070, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.

 

-- CPU 시간 = 21808밀리초, 경과 시간 = 5803밀리초

 

SET STATISTICS PROFILE OFF

SET STATISTICS IO OFF

SET STATISTICS TIME OFF

GO

 

반응형

'연구개발 > DBA' 카테고리의 다른 글

정렬의 최적화 (02.인덱스 정렬을 제거)  (0) 2012.01.24
정렬의 최적화 (01.정렬이 발생하는 경우)  (0) 2012.01.24
조인 join  (0) 2012.01.20
면접질의  (0) 2012.01.20
데이터베이스 백업과 복원  (0) 2012.01.20
반응형
/* SQL 성능에 큰 영향을 미치는 요소를 나열해 보면 인덱스, 조인 방식 그리고 조인 순서가
최상위권에 속할 것이다.*/

/* 중첩 루프(Nested Loops) 조인 */
USE pubs
GO
SET SHOWPLAN_TEXT ON
GO

SELECT a.emp_id, a.fname, a.lname, a.job_id, b.job_desc
FROM dbo.employee a, dbo.jobs b
WHERE a.job_id = b.job_id
GO
  --|--Nested Loops(Inner Join, OUTER REFERENCES:([a].[job_id]))
  --     |--Clustered Index Scan(OBJECT:([pubs].[dbo].[employee].[employee_ind] AS [a]))
  --     |--Clustered Index Seek(OBJECT:([pubs].[dbo].[jobs].[PK__jobs__117F9D94] AS [b]), SEEK:([b].[job_id]=[pubs].[dbo].[employee].[job_id] as [a].[job_id]) ORDERED FORWARD)
--예상실행계획에서 outer 테이블은 employee고, inner 테이블은 jobs다
--outer에 해당하는 employee 테이블을 스캔하면서 a.job 칼럼 값으로 inner에 해당하는 job 테이블의 PK인덱스를 탐색한다.
--그리고 employee 테이블의 마지막 데이터를 읽을 때까지 이 과정을 반복한다.
SET SHOWPLAN_TEXT OFF
GO


USE pubs
GO
SET NOCOUNT ON

DECLARE @emp_id        empid        --char(9)
            , @fname            varchar(20)
            , @lname            varchar(30)
            , @job_id            smallint
            , @job_desc        varchar(50)

DECLARE c_employee cursor for
    SELECT a.emp_id, a.fname, a.lname, a.job_id
    FROM dbo.employee a

OPEN c_employee

FETCH NEXT FROM c_employee
INTO @emp_id, @fname, @lname, @job_id

WHILE (@@FETCH_STATUS = 0)
BEGIN
    SELECT @job_desc = b.job_desc
    FROM dbo.jobs b
    WHERE b.job_id = @job_id
   
    SELECT @emp_id, @fname, @lname, @job_id, @job_desc

    FETCH NEXT FROM c_employee
    INTO @emp_id, @fname, @lname, @job_id
END

CLOSE c_employee
DEALLOCATE c_employee
GO

SELECT * FROM master.dbo.syscacheobjects;


-- NL(Nested Loops) 조인 시 작은 집합을 드라이빙하는 것이 유리하다.

 
 USE jwjung
 GO
 IF OBJECT_ID('t_small') IS NOT NULL
    DROP TABLE t_small
GO
IF OBJECT_ID('t_big') IS NOT NULL
    DROP TABLE t_big
GO

SELECT * INTO t_small FROM pubs.dbo.syscolumns
GO

CREATE UNIQUE INDEX t_small_x01 ON t_small (id, colid, number)
GO

/* t_big 테이블을 생성한다. */
SELECT TOP 0 * INTO t_big FROM pubs.dbo.syscolumns
GO
/* t_small 테이블의 데이터를 30배로 복제해서 t_big 테이블에 입력한다. */
INSERT INTO t_big
    SELECT a.* FROM t_small a, (SELECT TOP 30 * FROM t_small) b
GO

CREATE INDEX t_big_x01 ON t_big (id, colid, number)
GO

SELECT COUNT(*) as cnt FROM t_small
UNION ALL
SELECT COUNT(*) as cnt FROM t_big
GO


SET SHOWPLAN_TEXT ON
GO


SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    --OPTION (FORCE order, LOOP JOIN)
    --OPTION (FORCE order, HASH JOIN)
GO
SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    OPTION (FORCE order, HASH JOIN)
GO
SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    OPTION (FORCE order, LOOP JOIN)
    --OPTION (FORCE order, HASH JOIN)
GO

SELECT *
FROM t_small a INNER LOOP JOIN t_big b
    ON a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
GO

SET SHOWPLAN_TEXT OFF
GO


SET SHOWPLAN_TEXT ON
GO
/* 테이블 세 개 이상 조인할 때
Customers a -> Orders b -> [Order Details] c 순서로 조인하되
첫 번째 조인은 NL방식, 두 번째 조인은 해시 방식을 사용하도록 기술해보자
*/
SELECT *
FROM Northwind.dbo.Customers a
        , Northwind.dbo.Orders b
        , Northwind.dbo.[Order Details] c
WHERE a.CustomerID = b.CustomerID
    AND b.OrderID = c.OrderID
    OPTION (FORCE ORDER, LOOP JOIN, HASH JOIN)
GO
/* 조인 순서는 지정한 대로 되었지만, 조인 방식은 옵티마이저가 마음대로 결정했다.
예상과 달리 첫 번째 조인이 해시 방식을 사용했고, 두 번째 조인이 NL방식을 사용했다.
왜냐하면, 쿼리 힌트에 둘 이상의 조인 방식을 지정하면 옵티마이저가 비용이 가장 낮은 방식을 선택하기 때문 */

SELECT *
FROM Northwind.dbo.Customers a
        INNER LOOP JOIN Northwind.dbo.Orders b
            ON a.CustomerID = b.CustomerID
        INNER HASH JOIN Northwind.dbo.[Order Details] c
            ON b.OrderID = c.OrderID

SET SHOWPLAN_TEXT OFF
GO

/* NL 조인의 수행 과정 분석 */
SET SHOWPLAN_ALL ON
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'password'
    AND a.length <= 10
ORDER BY a.length DESC
GO

SET SHOWPLAN_ALL OFF
GO

EXEC SP_HELPINDEX 't_small';
GO
--index_name                    index_description                                index_keys
--t_small_x01    nonclustered, unique located on PRIMARY    id, colid, number

EXEC SP_HELPINDEX 't_big';
GO
--index_name            index_description                        index_keys
--t_big_x01    nonclustered located on PRIMARY    id, colid, number


SET STATISTICS PROFILE ON
SET STATISTICS IO ON
SET STATISTICS TIME ON

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'password'
    AND a.length <= 10
ORDER BY a.length DESC
GO

SET STATISTICS PROFILE OFF
SET STATISTICS IO OFF
SET STATISTICS TIME OFF


SELECT avg(cnt) as avg
FROM (
    SELECT name, count(*) as cnt
    FROM t_big
    GROUP BY name
    ) a
GO

CREATE NONCLUSTERED INDEX t_big_x02 ON t_big (name)
GO

SELECT * FROM sysindexes
WHERE id = object_id('t_big')

DBCC IND(jwjung, t_big, 6)

DBCC TRACEON(3604, 3605)
DBCC PAGE(jwjung, 1, 2736, 3)
DBCC TRACEOFF(3604, 3605)

drop index t_big.t_big_x02
EXEC sp_helpindex 't_big'
GO


DROP INDEX t_small.t_small_x01
GO

CREATE UNIQUE index t_small_x01 ON t_small (id, colid, number) INCLUDE (length)
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND a.name = 'password'
    AND a.length <= 10
ORDER BY a.length DESC

EXEC sp_helpindex 't_small';
GO

--index_name                index_description                                    index_keys
--t_small_x01    nonclustered, unique located on PRIMARY    id, colid, number


CREATE NONCLUSTERED INDEX t_big_x02 ON t_big (name)
GO

DROP INDEX t_small.t_small_x01
GO

CREATE UNIQUE INDEX t_small_x01 ON t_small (id, colid, number, length)
GO

SET SHOWPLAN_TEXT ON
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'password'
    AND a.length <= 10
ORDER BY a.length DESC

SET SHOWPLAN_TEXT OFF

/* NC 조인의 특징
1. 조인을 한 로우씩 차례대로 진행한다.
2. 먼저 액세스한 테이블의 처리 범위에 따라 전체 일량이 결정된다.
3. 랜덤(Random) 액세스 위주로 수행된다.
4. 조인 컬럼에 대한 인덱스 전략이 중요하다.
*/

DROP INDEX t_small.t_small_x01
GO
DROP INDEX t_big.t_big_x01
GO
DROP INDEX t_big.t_big_x02
GO

EXEC sp_helpindex 't_small';
EXEC sp_helpindex 't_big';

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    OPTION (FORCE ORDER, LOOP JOIN)
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
GO

/* 네 가지 특징을 종합하면, NL 조인은 소량의 데이터를 처리하거나 앞쪽의 일부 데이터만
빠르게 출력해야 하는 OLTP 환경에 적합한 조인 방식임을 알 수 있다.
반대로 대량의 로우를 추출하여 조인하는 DW 환경에서는 치명적인 약점을 드러내기도 하는데
이를 보완하고자 병합, 해시 등의 조인 방식이 등장했다.
*/



/* 병합 조인(Sort Merge)
조인 연결고리에 equi-join 조건이 하나라도 있어야 한다. 즉 'equal 연결이 없는 between 조인'
또는 '카테션 곱을 만들어내는 cross 조인 등에서는 병합 조인을 사용할 수 없다.
*/
USE Northwind
GO
SET SHOWPLAN_TEXT ON
GO

SELECT *
FROM Customers a, Orders b
WHERE a.CustomerID = b.CustomerID
GO

SELECT *
FROM Customers a INNER LOOP JOIN Orders b
    ON a.CustomerID = b.CustomerID
GO

SET SHOWPLAN_TEXT OFF
GO

EXEC sp_helpindex 'Customers'
GO
--index_name                index_description                                                        index_keys
--City                        nonclustered located on PRIMARY                                City
--CompanyName        nonclustered located on PRIMARY                                CompanyName
--PK_Customers        clustered, unique, primary key located on PRIMARY        CustomerID
--PostalCode                nonclustered located on PRIMARY                                PostalCode
--Region                    nonclustered located on PRIMARY                                Region

EXEC sp_helpindex 'Orders'
GO
--index_name                index_description                                                    index_keys
--CustomerID            nonclustered located on PRIMARY                            CustomerID
--CustomersOrders    nonclustered located on PRIMARY                            CustomerID
--EmployeeID            nonclustered located on PRIMARY                            EmployeeID
--EmployeesOrders    nonclustered located on PRIMARY                            EmployeeID
--OrderDate                nonclustered located on PRIMARY                            OrderDate
--PK_Orders                clustered, unique, primary key located on PRIMARY    OrderID
--ShippedDate            nonclustered located on PRIMARY                            ShippedDate
--ShippersOrders        nonclustered located on PRIMARY                            ShipVia
--ShipPostalCode        nonclustered located on PRIMARY                            ShipPostalCode

SELECT COUNT(*) FROM Customers;
SELECT COUNT(*) FROM Orders;


--힌트로 병합 조인을 제어하는 방법
USE jwjung
GO
IF OBJECT_ID('t_small') IS NOT NULL
    DROP TABLE t_small
GO
IF OBJECT_ID('t_big') IS NOT NULL
    DROP TABLE t_big
GO

SELECT * INTO t_small FROM pubs.dbo.syscolumns
GO
CREATE UNIQUE INDEX t_small_x01 ON t_small(id, colid, number)
GO

--t_big 테이블 생성
SELECT TOP 0 * INTO t_big FROM pubs.dbo.syscolumns
GO

--t_small 테이블의 데이터를 30배로 복제해서 t_big 테이블에 입력
INSERT INTO t_big
    SELECT a.* FROM t_small a, (SELECT TOP 30 * FROM t_small) b
GO

CREATE CLUSTERED INDEX t_big_x01 ON t_big (id, colid, number)
GO

SELECT COUNT(*) AS cnt FROM t_small
UNION ALL
SELECT COUNT(*) AS cnt FROM t_big
GO

SET SHOWPLAN_TEXT ON
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
GO

SET SHOWPLAN_TEXT OFF
GO

EXEC sp_helpindex 't_small';
GO
EXEC sp_helpindex 't_big';
GO
index_name    index_description    index_keys
t_small_x01    nonclustered, unique located on PRIMARY    id, colid, number
index_name    index_description    index_keys
t_big_x01    clustered located on PRIMARY    id, colid, number
SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
OPTION (FORCE ORDER, MERGE JOIN)
GO


/* 병합 조인의 수행 과정 분석 */

SET SHOWPLAN_ALL ON
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'id'
    AND a.length <= 10
ORDER BY a.length DESC
OPTION (MERGE JOIN)
GO

SET SHOWPLAN_ALL OFF
GO

EXEC sp_spaceused 't_small', true;
GO

EXEC sp_spaceused 't_big', true;
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'id'
    AND a.length <= 10
ORDER BY a.length DESC
OPTION (FORCE ORDER, LOOP JOIN)
GO


SELECT *
FROM t_big b, t_small a
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'id'
    AND a.length <= 10
ORDER BY a.length DESC
OPTION (FORCE ORDER, MERGE JOIN)
GO

/* 병합 조인의 특징
1. 양쪽 집합을 정렬하고서 조인한다.
2. 테이블별 검색조건에 따라 전체 일량이 좌우된다.
3. 랜덤 액세스 위주로 수행되지 않는다.
4. 다대다(Many-to-Many) 조인보다는 일대다(One-to-Many)조인에서 효과적이다.
*/


/* 병합 조인 실습 */
use jwjung
GO
IF OBJECT_ID('수험생') IS NOT NULL
    DROP TABLE 수험생
GO
IF OBJECT_ID('등급') IS NOT NULL
    DROP TABLE 등급
GO

--수험생 데이터 10만 건 입력
SELECT TOP 100000
    identity(int, 1, 1) AS 수험생id
    ,cast(abs(a.id) % 101 AS smallint) AS 점수
    ,cast(' ' AS VARCHAR(1)) AS 등급코드, a.*
INTO 수험생
FROM master.dbo.syscolumns a
        , master.dbo.sysindexes b
GO

CREATE TABLE 등급 (
    등급코드    VARCHAR(1)        NOT NULL
    ,시작점수    SMALLINT
    ,종료점수    SMALLINT
);
GO

--등급 코드 5가지 입력
INSERT INTO 등급
SELECT 'A', 90, 100
UNION ALL
SELECT 'B', 75, 89
UNION ALL
SELECT 'C', 60, 74
UNION ALL
SELECT 'D', 50, 59
UNION ALL
SELECT 'F', 0, 49
GO

ALTER TABLE 등급
ADD CONSTRAINT 등급_pk PRIMARY KEY (등급코드)
GO
CREATE INDEX 등급_x01 ON 등급 (시작점수, 종료점수)
GO

SELECT COUNT(*) AS cnt FROM 수험생
UNION ALL
SELECT COUNT(*) AS cnt FROM 등급
GO
GO

SELECT * FROM 수험생;
SELECT * FROM 등급;

EXEC sp_helpindex '수험생';
EXEC sp_helpindex '등급';
--index_name                    index_description                                        index_keys
--등급_pk            clustered, unique, primary key located on PRIMARY    등급코드
--등급_x01        nonclustered located on PRIMARY                            시작점수, 종료점수

BEGIN TRAN
    UPDATE 수험생
    SET 등급코드 = b.등급코드
    --SELECT a.*, b.*
    FROM 수험생 a, 등급 b
    WHERE a.점수 BETWEEN b.시작점수 AND b.종료점수
    GO
COMMIT TRAN
ROLLBACK TRAN



IF OBJECT_ID('tempdb.dbo.#t_copy') IS NOT NULL
    DROP TABLE tempdb.dbo.#t_copy
GO
SELECT TOP 1
    identity(smallint, 0, 1) AS 점수
INTO #t_copy
FROM master.dbo.syscolumns
GO

SELECT * FROM #t_copy;
GO

UPDATE 수험생
SET 등급코드 = x.등급코드
FROM (
    SELECT b.점수, max(a.등급코드) AS 등급코드
    FROM 등급 a, #t_copy b
    WHERE b.점수 >= a.시작점수
        AND b.점수 <= a.종료점수
    GROUP BY b.점수
    ) x INNER MERGE JOIN 수험생 a
ON x.점수 = a.점수
GO





/* 해시 조인(Hash Join)
작은 집합으로 '해시 테이블'을 생성하고 큰 집합을 읽으면서 이 해시 테이블을 탐색한다.
해시 테이블을 만드는 데 사용되는 집합을 Build Input이라고 하고 탐색하는데 사용하는 집합을
Probe Input이라고 한다.

*/
SELECT *
FROM dbo.sysobjects a cross join dbo.sysobjects b
OPTION (hash join)
GO
--메시지 8622, 수준 16, 상태 1, 줄 1
--이 쿼리에 정의된 힌트로 인해 쿼리 프로세서에서 쿼리 계획을 생성할 수 없습니다. 힌트를 지정하거나 SET FORCEPLAN을 사용하지 않고 쿼리를 다시 전송하십시오.

USE jwjung;
GO
IF OBJECT_ID('Orders') IS NOT NULL
    DROP TABLE Orders
GO
IF OBJECT_ID('Customers') IS NOT NULL
    DROP TABLE Customers
GO
SELECT * INTO Orders FROM Northwind.dbo.Orders
GO
SELECT * INTO Customers FROM Northwind.dbo.Customers
GO

SET STATISTICS PROFILE ON

EXEC sp_helpindex 'Customers'
GO

EXEC sp_helpindex 'Orders'
GO

SELECT *
FROM Customers a, Orders b
WHERE a.CustomerID = b.CustomerID
GO


/* NL 조인 시 */
SELECT *
FROM Customers a, Orders b
WHERE a.CustomerID = b.CustomerID
OPTION (FORCE ORDER, LOOP JOIN)
GO

/* 병합 조인 시 */
SELECT *
FROM Customers a, Orders b
WHERE a.CustomerID = b.CustomerID
OPTION (FORCE ORDER, MERGE JOIN)
GO

/* 해시 조인 시 */
SELECT *
FROM Customers a, Orders b
WHERE a.CustomerID = b.CustomerID
OPTION (FORCE ORDER, HASH JOIN)
GO


/* 힌트로 해시 조인을 제어하는 방법 */
USE jwjung
GO
IF OBJECT_ID('t_small') IS NOT NULL
    DROP TABLE t_small
GO
IF OBJECT_ID('t_big') IS NOT NULL
    DROP TABLE t_big
GO

SELECT * INTO t_small FROM pubs.dbo.syscolumns
GO

/* 인덱스는 생성하지 않는다. */
--CREATE UNIQUE INDEX t_small_x01 ON t_small (id, colid, number)
--GO

/* t_big 테이블을 생성한다. */
SELECT TOP 0 * INTO t_big FROM pubs.dbo.syscolumns
GO

/* t_small 테이블의 데이터를 30배로 복제해서 t_big 테이블에 입력한다. */
INSERT INTO t_big SELECT a.* FROM t_small a, (SELECT TOP 30 * FROM t_small) b
GO

/* 인덱스는 생성하지 않는다. */
--CREATE CLUSTERED INDEX t_big_x01 ON t_big (id, colid, number)
--GO

SELECT COUNT(*) as cnt FROM t_small
UNION ALL
SELECT COUNT(*) as cnt FROM t_big
GO
--cnt
--573
--17190

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
GO
--(17190개 행이 영향을 받음)
--테이블 'Worktable'. 검색 수 573, 논리적 읽기 수 36671, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.
--테이블 't_small'. 검색 수 1, 논리적 읽기 수 9, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.
--테이블 't_big'. 검색 수 1, 논리적 읽기 수 240, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.
SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
OPTION (FORCE ORDER, HASH JOIN)
GO
SELECT *
FROM t_small a INNER HASH JOIN t_big b
    ON a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
GO
--(17190개 행이 영향을 받음)
--테이블 'Worktable'. 검색 수 0, 논리적 읽기 수 0, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.
--테이블 't_big'. 검색 수 1, 논리적 읽기 수 240, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.
--테이블 't_small'. 검색 수 1, 논리적 읽기 수 9, 물리적 읽기 수 0, 미리 읽기 수 0, LOB 논리적 읽기 수 0, LOB 물리적 읽기 수 0, LOB 미리 읽기 수 0.


SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'id'
    AND a.length <= 10
ORDER BY a.length DESC
OPTION (HASH JOIN)
SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'id'
    AND a.length <= 10
ORDER BY a.length DESC
OPTION (LOOP JOIN)

-- 해시 조인의 실행계획을 읽을 때도 같은 레벨에서는 위에서 아래로 내려가고 계층 레벨에서는 오른쪽(=하위)에서
--왼쪽(=상위)으로 올라간다. 다만, 아래쪽 테이블이 위쪽 테이블을 탐색한다는 점만 기억하면 된다.
/* NL조인과 비교하면 각 테이블을 한 번만 액세스하는 게 큰 차이점이고, 병합 조인과 비교하면 정렬할 필요가 없다는 게
큰 차이점이다. 이 두 가지가 해시 조인의 가장 큰 장점이다. */


SET STATISTICS PROFILE ON
SET STATISTICS IO ON
SET STATISTICS TIME ON
GO

SELECT *
FROM t_small a, t_big b
WHERE a.id = b.id
    AND a.colid = b.colid
    AND a.number = b.number
    AND b.name = 'id'
    AND a.length <= 10
ORDER BY a.length DESC
OPTION (HASH JOIN)
GO

/* 해시 조인의 특징
1. 조인 대상 집합을 정렬할 필요가 없다.
    - 그러나 정렬 연산이 불필요한 대신 해시 테이블 생성과 탐색에 많은 자원을 사용하므로 특히 OLTP 환경에서 주의해야 한다.
2. 테이블별 검색조건에 따라 전체 일량이 좌우된다.
    - NL 조인은 outer 테이블에서 얻은 조인 컬럼 값으로 inner 테이블을 탐색하므로
    먼저 액세스 한 테이블의 처리 범위에 따라 전체 일량이 결정된다고 했다.
    그러나 해시조인(병합조인과 마찬가지)은 각 테이블을 독립적으로 액세스하므로 테이블별 검색조건에 따라 전체 일량이 좌우된다.
3. 랜덤 액세스 위주로 수행되지 않는다.
    - 해시조인의 트레이스 결과에 나타난 것처럼, 양쪽 테이블의 검색 수는 모두 1이다. 해시 조인은 inner 테이블을 반복적으로
    탐색하지 않으므로 조인 과정에서 랜덤 액세스가 발생하지 않는다.(inner 집합의 로우가 해시 테이블을 탐색하기는 하지만,
    이는 메모리에 생성된 구조체를 액세스하는 것이므로 흔히 말하는 랜덤 액세스와는 다르다). 그러나 각 테이블로부터 대상 로우를
    추출하는 과정에서는 북마크 룩업에 의한 랜덤 액세스가 대량으로 발생할 수 있으므로 주의해야 한다.
4. 조인 컬럼의 인덱스 유무가 조인 속도에 영향을 미치지 않는다.
    - NL 조인과 병합 조인에서 설명했듯이, 이 두 가지 조인 방식은 조인 연결고리의 인덱스가 조인 속도에 큰 영향을 미친다.
    NL 조인은 inner 테이블에 조인 컬럼을 포함하는 인덱스가 절대적으로 필요하고, 병합 조인은 조인 키 순서대로 정렬된 인덱스가
    있어야 정렬 작업을 생략할 수 있다.
    반면, 해시 조인은 해시 함수의 결과 값으로 조인하므로 조인 연결고리에 인덱스가 없어도 조인 과정에 아무런 문제가 없다.

정리 - 해시조인은 대량 데이터를 조인하고자 할 때 1.병합조인에서 큰 집합을 정렬하기 부담스러울 때, 2.조인 연결고리에 적절한
    인덱스가 없을 때, 3.수행빈도가 낮은 배치작업용 쿼리를 수행할 때 가장 효율적인 조인 방식이 된다.
*/






반응형

'연구개발 > DBA' 카테고리의 다른 글

정렬의 최적화 (01.정렬이 발생하는 경우)  (0) 2012.01.24
조인 순서와 성능  (0) 2012.01.23
면접질의  (0) 2012.01.20
데이터베이스 백업과 복원  (0) 2012.01.20
실행계획  (0) 2012.01.20
반응형
/**********************************************************************************
1. RDBMS가 뭔가요?
Relational Database Management System
키(key)와 값(value)들의 간단한 관계를 테이블화 시킨 매우 간단한 원칙의 전산정보 데이터베이스



2. 정규화가 뭔가요?
실세계에서 발생하는 데이터를 수학적인 방법에 의해 구조화시켜 체계적으로 데이터를 관리할 수 있도록 하는 것

3. 각 정규형의 차이점은 무엇인가요? (1차, 2차, 3차 등)
1차 정규화 - 복수의 속성값을 갖는 속성을 분리
                    "모든 엔티티타입에는 하나의 속성만 있어야 하며, 반복되는 속성의 집단은 별도의 엔티티타입으로 분리한다.
2차 정규화 - 주식별자에 종속적이지 않은 속성의 분리
                    부분 종속 속성을 분리
3차 정규화 - 속성에 종속적인 속성의 분리(1차,2차정규화를 통해 분리된 테이블에서 속성 중 주식별자에 종속된 속성 중에서
                    다시 속성간에 종속 관계가 발생한다면 3차 정규화를 진행)
                    이전 종속 속성의 분리
보이스-코드 정규화(BCNF) - 다수의 주식별자 분리
4차 정규화 - 특정 속성 값에 따라 선택적인 속성의 분리(식별자간에 역할이 중복되지 않음)
5차 정규화 - 결합 종속일 경우는 두 개 이상의 N개로 분리

4. 저장 프로시저는 무엇인가요?
일련의 쿼리를 마치 하나의 함수처럼 실행하기 위한 쿼리의 집합

5. 트리거는 무엇인가요?
DML 문이 TABLE에 대해 행해질 때 묵시적으로 수행되는 프로시저
트리거는 TABLE과 별도로 DATABASE에 저장되며 VIEW에 대해서가 아니라 TABLE에 관해서만 정의될 수 있다.

6. 뷰는 무엇인가요?
실제 데이터를 갖고 있는 테이블의 정보를 선별적으로 링크하여 보여주는 것으로써 일종의 바로가기 아이콘과 같은 개념
추가적으로 테이블을 생성하는 낭비없이 외부로 유출되면 안되는 데이터를 가려내고 보여줄 수 있다.

7. 인덱스는 무엇인가요?
영어사전의 "찾아보기" 같이 DATA를 빨리 찾을 수 있도록 하는 것.
종류는 클러스터드 인덱스와 넌클러스터드 인덱스가 있음.
장점 - 검색 속도가 빨라지며 시스템부하가 줄어 시스템 성능이 좋아진다.
단점 - 데이터베이스 공간을 차지하므로 추가적인 공간이 필요함(대략 데이터베이스 크기의 10%)
            생성하는데 시간이 많이 소요될 수 있으며 데이터의 변경 작업(Insert, Update, Delete)이 자주 일어날 경우
            성능이 많이 나빠질 수도 있다.

8. 클러스터드 인덱스와 넌클러스터드 인덱스의 차이는 무엇인가요?
클러스터드 인덱스는
-테이블에 데이터가 추가/수정 될 때 테이블에 실제로 저장되는 물리적인 정렬기준
-테이블당 1개만 생성 가능
-물리적으로 정렬되어 있으므로 검색이 빠르다.
-PK(Primary Key)를 정의하면 자동 생성된다.
넌클러스터드 인덱스
-논리적인 정렬기준
-하나의 테이블에 여러개 생성 가능(최대 249개)

9. 테이블에 설정될 수 있는 인덱스 형태들은 어떤 형태들이 있나요?
클러스터드 인덱스와 넌클러스터드 인덱스

10. 커서는 무엇인가요?
테이블에서 여러 개의 행을 쿼리한 후에, 쿼리의 결과인 행 집합을 한 행씩 처리하기 위한 방식

11. DBCC 명령어는 어떤것들을 사용해 보았나요?
DBCC TRACEON / TRACEOFF / TRACESTATUS
DBCC SQLPERF(LOGSPACE / IOSTATS / LRUSTATS / NETSTATS)
DBCC OPENTRAN
DBCC CHECKDB
DBCC INPUTBUFFER / OUTPUTBUFFER
DBCC PROCCACHE / FREEPROCCACHE
DBCC SHOWCONFIG
DBCC SHOW_STATISTICS
DBCC USEROPTIONS
DBCC INDEXDEFRAG
DBCC DROPCLEANBUFFERS

12. 연결된 서버는 무엇인가요?
다른 데이터베이스와 연결하여 데이터 전달 및 조회를 위함

13. 데이터 정렬(Collation)은 무엇인가요?
Sort

14. 데이터 정렬 종류는 어떤 것들이 있나요?
ASC / DESC

15. 프라이머리 키와 유니크 키의 차이는 무엇인가요?
PRIMARY KEY는  UNIQUE KEY의 부분집합으로 테이블에 한 개 밖에 없으며 NULL 값을 허용하지 않음
UNIQUE KEY는 NULL값 허용- 한개만

16. 일대일, 일대다, 다대다 관계의 테이블 설계는 어떻게 하나요?
일대일 -
하나의 엔티티타입으로 완전히 통합하는 방법
부분 통합을 하는 방법
슈퍼 엔티티타입 생성하는 방법
일대다 -
그냥 쓰면 됨.
다대다 -
PK에 의한 다대다관계는 주식별자 통합
속성에 의한 다대다관계는 부모 엔티티타입에 속성 추가

17. NOLOCK이 무엇인가요?
READ UNCOMMITED

18. DELETE 명령어와 TRUNCATE 명령어의 차이는 무엇인가요?
DELETE는 테이블의 데이터 삭제
TRUNCATE는 데이터만 삭제. 속도는 TRUNCATE가 휠씬 빠름(로그를 쓰지 않기 때문에)

19. Sql Server에서 지원하는 조인방식은 어떤 것들이 있나요?
Nested Loop Join - 루프를 돌며 조건 찾기
Hash Join - 조인에 참여하는 두 개의 테이블 중 상대적으로 크기가 작은 테이블을 해시 함수에 통과시켜 그 결과 값을
                해시 테이블에 저장한다. 그런 다음, 큰 테이블을 한 행, 한 행 해시 테이블에 매칭을 시켜가며 값을 찾아내 간다.
                해시 테이블에는 실제 데이터가 아닌 해시 값이 저장되기 때문에 작은 테이블이 해싱되어야 해시 테이블의
                크기를 작게 유지하여 비교 속도가 빨라지게 된다.
Merge Join - 병합을 위해서는 조인된 양쪽 컬럼이 반드시 정렬되어 있어야 한다.

20. HAVING 절과 WHERE 절과의 차이는 무엇인가요?
HAVING절 - GROUP BY에 대한 조건을 부여하며 집계함수와 함께 사용할 수 있다.
WHERE절 - 조건 검색을 위해 씀

21. 서브쿼리가 무엇인가요?
쿼리 안에 쿼리가 있는 것

22. 서브쿼리의 결과 종류는 어떤 것들이 있나요?
단일행 - 한개의 컬럼만 리턴
복수행 - 실행결과가 여러개의 행을 리턴(복수개의 행과 비교해야 하므로 in연산자를 사용)
복수컬럼 - 실행결과가 복수개의 컬럼, 복수개의 행을 리턴
상호관련 - main query절에 사용된 테이블이 서브쿼리절에 다시 재 사용되는 경우

23. 프로파일러는 무엇인가요?
데이터베이스가 작동할 때 쿼리 및 로그 등의 모니터링을 하기 위한 도구

24. 사용자정의함수는 무엇인가요?
SQL Server 2000에서 추가된 기능으로 다른 일반 프로그래밍에서처럼 자유롭게 함수를 정의하고 활용할 수 있는 것.
함수의 유형은 크게 스칼라, 인라인테이블, 다중문테이블 3가지가 있다.

25. 사용자정의함수의 결과 종류는 어떤 것들이 있나요?
스칼라 - 단일 데이터 값을 리턴하는 함수
인라인테이블 - 테이블 형태의 결과 값을 리턴하는 함수
다중문테이블 - 테이블 형태의 결과 값을 리턴하는 함수로 기본적인 기능은 인라인 테이블 함수와 비슷하지만
                        리턴되어지는 테이블 형식을 직접 정의할 수 있다.

26. SQL Server의 TCP/IP 포트는 무엇인가요? 바꿀 수 있나요?
기본포트는 1433. 당연히 바꿀 수 있으며 보안 문제로 인하여 바꿔서 사용하는 것을 권장

27. SQL Server의 인증 모드는 어떤것이 있나요? 바꿀 수 있나요?
원도우 인증과 SQL Server 인증. 당연히 바꿀 수 있음

28. SQL Server 로긴 유저와 암호는 어디에 저장되어 있나요?
-

29. SQL Server의 버전 정보를 확인할 수 있는 명령어는 무엇인가요?
SELECT @@VERSION

30. SQL server 에이전트는 무엇인가요?
자동화 기능을 위해 필수적인 서비스.-SQL Server Agent 경고, 데이터베이스 메일, 데이터베이스 유지관리 계획, 로그 전달

31. 저장프로시저는 재귀호출이 될까요? 몇단계까지 가능할까요?
재귀호출이 가능하며 32단계까지만 가능.

32. @@ERROR 는 무엇인가요?
시스템 함수로써 Transact_SQL문이 성공적으로 실행되면 0을 반환하고 오류가 발생하면 해당 오류 번호를 반환

33. RAISERROR 는 무엇인가요..?
오류 메시지를 생성하고 세션에 대한 오류 처리를 시작할 수 있음.
sys.messages 카탈로그 뷰에 저장된 사용자 정의 메시지를 참조하거나 동적으로 메시지를 작성할 수 있음.
TRY..CATCH구문 사용

34. 로그전달은 무엇인가요?
주서버와 보조서버에 데이터를 동기화 시킨 후 트랜잭션이 발생되면 주서버의 트랜잭션 로그가 보조서버에도 전달되게끔 설정하는 것

35. 지역 임시 테이블과 전역 임시 테이블의 차이는 무엇인가요?
지역 임시 테이블은 현재 세션에서만 볼 수 있으나 전역 임시 테이블은 모든 세션에서 볼 수 있음.

36. DB이름을 바꿀 수 있는 명령어는 무엇인가요?
EXEC sp_renamedb '이전DB명', '바꿀 DB명'

37. sp_configure 명령어는 무엇인가요?
SQL Server의 설정을 변경하고자 할 시 사용하는 명령어

38. 복제의 종류는 어떤것들이 있는지 차이점을 설명해주세요.
스냅숏 복제(Snapshot Replication) - 정기적으로 게시서버에서 지정된 게시의 모든 데이터를 통째로 구독서버에 전달
트랜잭션 복제(Transaction Replication) - 변경된 데이터만을 전달하는 방식
병합 복제(Merge Replication) - 스냅숏 복제와 트랜잭션 복제는 게시서버에서 구독서버로 전달되는 방식이나
                                                병합 복제는 구독서버에서 게시서버로 변경사항이 전달되는 방식

39. SQL Server 설치시 추가되는 서비스들은 어떤 것들이 있나요?
SQL Server Integration Services
SQL Server Analysis Services
SQL Full-text Filter Daemon Launcher
SQL Server Reporting Services
SQL Server Browser

40. 유저의 권한 변경 키워드 3개는 무엇이 있나요?
GRANT - 보안 주체에 보안 개체에 대한 사용 권한을 부여.
DENY - 보안 주체에 대한 사용 권한을 거부.
REVOKE - 이전에 부여하거나 거부된 사용 권한을 제거.

41. SET QUOTED_IDENTIFIER의 의미는 무엇인가요?
기본값은 ON
SET QUOTED_IDENTIFIER OFF 시 큰따옴표를 사용하여 식별자를 구분 할 수 있음

42. STUFF함수와 REPLACE함수와의 차이는 무엇인가요?
REPLACE 함수는 지정된 문자열 값의 모든 항목을 다른 문자열 값으로 바꿈
STUFF 함수는 다른 문자열에 문자열을 삽입.
SELECT STUFF('abcdef', 2, 3, 'ijklmn');
GO
----------------------------------------------
aijklmn

43. master 데이터베이스를 어떻게 재구축하나요?
    1.서버 차원의 모든 구성 값을 기록
        SELECT * FROM sys.configurations;
    2.SQL Server 인스턴스와 현재 데이터 정렬에 적용된 모든 서비스 팩과 핫픽스를 기록.
        시스템 데이터베이스를 다시 작성한 후 이러한 업데이트를 다시 적용해야 함.
        SELECT
            SERVERPROPERTY('ProductVersion') AS ProductVersion,
            SERVERPROPERTY('ProductLevel') AS ProductLevel,
            SERVERPROPERTY('ResourceVersion') AS ResourceVersion,
            SERVERPROPERTY('ResourceLastUpdateDateTime') AS ResourceLastUpdateDateTime,
            SERVERPROPERTY('Collation') AS Collation;
    3.시스템 데이터베이스의 모든 데이터와 로그 파일의 현재 위치를 기록.
        시스템 데이터베이스를 다시 작성하면 모든 시스템 데이터베이스가 원래 위치에 설치 됨.
        시스템 데이터베이스 데이터나 로그 파일을 다른 위치로 이동한 경우 해당 파일을 다시 이동해야 함.
        SELECT name, physical_name AS current_file_location
        FROM sys.master_files
        WHERE database_id IN (DB_ID('master'), DB_ID('model'), DB_ID('msdb'), DB_ID('temp'));
    4. master, model, msdb 데이터베이스의 현재 백업을 찾음.
    5. SQL Server 인스턴스가 복제 배포자로 구성된 경우 배포 데이터베이스의 현재 백업을 찾습니다.
    6. 시스템 데이터베이스를 다시 작성할 수 있는 권한이 있는지 확인합니다. 작업을 수행하기 위해 sysadmin
        고정 서버 역할의 멤버여야 함.
    7.master, model, msdb 데이터와 로그 템플릿 파일의 복사본이 로컬 서버에 있는지 확인.

44. 각 시스템데이터베이스들의 기능들은 무엇인가요?
master 데이터베이스 - SQL Server 인스턴스에 대한 모든 시스템 수준 정보를 기록.
msdb 데이터베이스 - SQL Server 에이전트에서 알림과 작업을 예약하는데 사용.
model 데이터베이스 - SQL Server에서 생성되는 모든 데이터베이스에 대한 템플릿으로 사용.
Resource 데이터베이스 - SQL Server에 포함된 시스템 개체가 들어 있는 읽기 전용 데이터베이스.
tempdb 데이터베이스 - 임시 개체나 중간 결과 집합을 보관하기 위한 작업 영역.

45. 프라이머리키와 포린키는 무엇인가요?
PRIMARY KEY - 테이블에는 일반적으로 테이블의 각 행을 고유하게 식별하는 값을 가진 열 또는 열 조합이 포함되어
                        있으며 이러한 열이나 열 조합을 테이블의 PRIMARY KEY라고 하며 테이블에 엔터티 무결성을 적용함.
FOREIGN KEY - 두 테이블의 데이터 간 연결을 설정하고 강제 적용하는데 사용하는 열을 말함.

46. 무결성이 무엇인가요? 제약 조건들에 대해서 설명해주세요.
주식별자에 대해 중복되지 않는 값을 갖지 않는 것.
무결성은 4가지 범주로 구성됨.
엔터티 무결성 - 행을 특정 테이블의 고유 엔터티로 정의. 엔터티 무결성은 UNIQUE 인덱스, UNIQUE 제약조건 또는 PRIMARY KEY
                        제약 조건을 통해 테이블의 기본 키나 식별자 열의 무결성을 강제 적용함.
도메인 무결성 - 특정 열에 대한 항목의 유효성. 데이터 형식을 통해 유형을 제한하거나 CHECK 제약 조건 및 규칙을 통해 형식을 제한
                        하거나 FOREIGN KEY 제약 조건, CHECK 제약 조건, DEFAULT 정의, NOT NULL 정의 및 규칙을 통해 가능한 값
                        범위를 제한하여 도메인 무결성을 강제 적용할 수 있음.
참조 무결성 - 행이 입력되거나 삭제될 때 테이블 간에 정의된 관계를 유지.
사용자 정의 무결성 - 다른 무결성 범주에 속하지 않는 특정 업무 규칙을 정의할 수 있음.

제약 조건 - PRIMARY KEY 제약조건, FOREIGN KEY 제약조건, UNIQUE 제약조건, CHECK 제약조건, DEFAULT 정의, NULL 값 허용
PRIMARY KEY 제약조건 - 데이터의 고유성을 보장.
FOREIGN KEY 제약조건 - 외래 키 테이블에 저장되는 데이터를 제어하는 것이지만 기본 키 테이블의 데이터 변경 사항도 제어할 수 있음.
UNIQUE 제약조건 - 열 또는 열 조합에 대해 고유성을 강제 적용하고자 할 때 사용.
DEFAULT 정의 - 레코드의 각 열은 값을 가져야하므로 DEFAULT 정의를 정의하는 것이 보다 좋은 해결책.
NULL 값 허용 - 테이블의 행에서 특정 열에 NULL값을 포함할 수 있는지 여부를 결정.

47. 관계형 테이블의 속성은 무엇인가요?
-

48. 반정규화가 무엇인가요?
성능을 위해 정규화된 데이터 모델의 중복성을 허용하도록 하는 것.

49. 어떻게 @@ERROR 와 @@ROWCOUNT 를 한번에 얻을 수 있나요?
@@ERROR를 @@ROWCOUNT와 함께 사용하여 DML 문 작업의 유효성을 확인할 수 있음.
@@ERROR의 값을 확인하여 오류 표시가 있는지 검사하고 @@ROWCOUNT를 사용하여 업데이트가 테이블의 행에 제대로 적용되었는지 확인.
USE AdventureWorks;
GO
IF OBJECT_ID(N'Purchasing.usp_ChangePurchaseOrderHeader',N'P')IS NOT NULL
    DROP PROCEDURE Purchasing.usp_ChangePurchaseOrderHeader;
GO
CREATE PROCEDURE Purchasing.usp_ChangePurchaseOrderHeader
    (
    @PurchaseOrderID INT
    ,@EmployeeID INT
   )
AS
-- Declare variables used in error checking.
DECLARE @ErrorVar INT;
DECLARE @RowCountVar INT;

-- Execute the UPDATE statement.
UPDATE PurchaseOrderHeader
    SET EmployeeID = @EmployeeID
    WHERE PurchaseOrderID = @PurchaseOrderID;

-- Save the @@ERROR and @@ROWCOUNT values in local
-- variables before they are cleared.
SELECT @ErrorVar = @@ERROR
    ,@RowCountVar = @@ROWCOUNT;

-- Check for errors. If an invalid @EmployeeID was specified
-- the UPDATE statement returns a foreign-key violation error #547.
IF @ErrorVar <> 0
    BEGIN
        IF @ErrorVar = 547
            BEGIN
                PRINT N'ERROR: Invalid ID specified for new employee.';
                 RETURN 1;
            END
        ELSE
            BEGIN
                PRINT N'ERROR: error '
                    + RTRIM(CAST(@ErrorVar AS NVARCHAR(10)))
                    + N' occurred.';
                RETURN 2;
            END
    END

-- Check the row count. @RowCountVar is set to 0
-- if an invalid @PurchaseOrderID was specified.
IF @RowCountVar = 0
    BEGIN
        PRINT 'Warning: The EmployeeID specified is not valid';
        RETURN 1;
    END
ELSE
    BEGIN
        PRINT 'Purchase order updated with the new employee';
        RETURN 0;
    END;
GO

50. Identity 컬럼이 무엇인가요?
현재 값 알아내기 - SELECT @@IDENTITY
테이블의 현재 값 알기 - DBCC CHECKIDENT('tablename')
강제 값 지정 - DBCC CHECKIDENT('tablename', RESEED, 10000)

51. 스케쥴 잡이 무엇인가요?
관리를 자동화하기 위한 서비스 즉 일정이 지정된 관리 태스크를 실행하는 Microsoft Windows 서비스

52. 어떤 인덱스도 가지고 있는 않은 테이블을 무엇이라고 부르고 무슨 목적으로 사용하나요?
작은 테이블 - 검색 위주가 아닌 insert, update가 자주 일어나는 경우에 사용. 인덱스를 쓰는 것 보다는 없는 것이 성능상 유리한 테이블

53. BCP는 무엇인가요? 어떤 경우에 사용하나요?
분할된 뷰를 포함하여 SELECT문이 작동하는 SQL Server 데이터베이스의 어디에서나 데이터를 가져올 수 있도록 사용하는 유틸리티
    데이터 파일로 SQL Server 테이블의 데이터를 대량으로 내보냄.
    쿼리의 데이터를 대량으로 내보냄
    SQL Server 테이블로 데이터 파일의 데이터를 대량으로 가져옴.
    서식 파일을 생성.

54. JOIN절 대신에 서브쿼리를 대체할 수 있나요?
대체할 수 있으나 JOIN 절에 비해 서브쿼리는 성능한 우월하지 않기에 JOIN절을 사용할 수 있다면 권장

55. 오라클과 같은 다른 DBMS에 연결할 수 있나요?
LINKED SERVER를 이용하여 OPENROWSET 이나 OPENDATASOURCE를 쓰기도 함.

56. 테이블이 사용중인 인덱스는 어떻게 알 수 있나요?
sp_helpindex 또는 실행계획을 보면 됨.

57. 다른 인스턴스로 테이블이나 스키마, 뷰 등을 복사하려면 어떻게 해야 하나요?
SSIS(SQL Server Integration Services)를 이용.
SSIS에서 파일을 복사 또는 다운로드하고 이벤트에 응답하여 전자 메일 메시지를 보내며 데이터 웨어하우스를 업데이트하고 데이터를 정리
및 마이닝하며 SQL Server 개체와 데이터를 관리하여 복잡한 비지니스 문제를 해결할 수 있음.

58. 셀프 조인이 무엇인가요?
자기자신과 자기자신이 조인하는 것 즉 자기자신을 참조하는 조인

59. 크로스 조인이 무엇인가요?
카티션 조인 이라고도 하며 한 테이블의 모든 행과 다른 테이블의 모든 행을 짝지어 반환

60. 가상 테이블에 트리거를 사용할 수 있나요?
가상 테이블(VIEW)에 트리거 사용 가능

61. 저장프로시저의 장점들을 설명해주세요.
저장 프로시저는 다수의 클라이언트 응용프로그램에서 서버로 보낼 T-SQL문을 미리 모아서 서버에서 관리하는 데이터로 저장해 둔 것으로
다수의 클라이언트들이 서버에 저장된 T-SQL 문을 공유하는 것이 가능해졌다.
데이터베이스 내에서 테이블 칼럼 등의 구조가 변경되더라도 서버에서 저장 프로시저의 정의만 수정하면 클라이언트는 신경쓰지 않아도 된다.
뷰와 마찬가지로 프로시저의 정의를 몰라도 그 처리 결과를 사용할 수 있기 때문이다.
또한 특정 형식의 데이터만을 추가할 수 있는 테이블이 있는 경우에는 추가 조건에 제한을 주는 프로시저를 정의해두고 해당 프로시저를 통해
서만 접근할 수 있도록 구성할 수도 있다.
저장 프로시저는 T-SQL문을 서버가 미리 저장하고 있으며 T-SQL문을 실행할 때에 처리단위가 되며 Batch처리와 같이 해당 저장 프로시저에서
정의한 T-SQL문을 하나의 배치로 간주하여 한꺼번에 처리하고 최적화시킨다.
T-SQL문이 미리 저장되어 있어 대량의 T-SQL문을 서버로 보내는 대신 단순히 저장 프로시저의 이름과 매개변수만 보내면 된다. 따라서,
프로시저를 사용할 경우 네트워크의 체증은 감소하게 된다.
클라이언트 간의 처리 루틴 공유가 가능하다.
데이터베이스 내부구조를 감춘다.
서버의 보호 및 데이터의 무결성을 구현한다.
쿼리의 저장속도를 향상시켜준다.
네트웍의 과부하를 감소할 수 있다.

62. 데이터웨어하우징이 무엇인가요?
개방형 시스템 도입으로 흩어져 있는 각종 기업정보를 최종 사용자가 쉽게 활용, 신속한 의사결정을 유도하도록 해 기업내 흩어져 있는 방대한
양의 데이터에 쉽게 접근하고 이를 활용할 수 있게 하는 기술

63. OLTP가 무엇인가요?
OnLine Realtime Transaction Processing - 데이터 입력, 조회 등을 위한 트랜잭션 지향의 업무를 쉽게 관리해줌.(업무 프로세스 중심)
데이터의 입력, 조회 등이 많이 일어나므로 소량의 데이터 처리 활용
OLAP(OnLine Analytical Processing)는 사용자로 하여금 데이터를 다른 관점으로 쉽게, 또한 선택적으로 추출하고 바라볼 수 있게 해줌.(사용자의 분석 수행 기반)
조회 관점이므로 대량의 데이터 처리 활용

64. XML은 어떻게 사용하나요?
T-SQL 확장 언어는 FOR XML을 사용하여 관계형 쿼리 결과를 XML에 매핑하고 OPENXML을 사용하여 XML로부터 관계형 뷰를 생성하는
SQL 중심 접근 방식을 제공함.
데이터에 대한 데이터 대량 로드, 쿼리 및 업데이트 기능을 지원하는 XML 중심 접근 방식을 제공하는 AXSD를 사용하여 정의.

65. 실행계획이 무엇인가요? 당신은 언제 사용하는지 어떻게 보는지 설명해주세요.
데이터베이스의 쿼리 실행 시 쿼리에 대한 구조적 처리의 절차를 보여주는 것.
쿼리 짜면서 실행해서 봄.

66. SELECT 문의 처리 순서는?
FROM -> ON -> JOIN -> WHERE -> GROUP BY -> WITH CUBE 또는 WITH ROLLUP -> HAVING -> SELECT -> DISTINCT -> ORDER BY -> TOP

67. 분산/파티셔닝 테이블 원래와 이해
테이블의 행 데이터가 매우 많은 대용량 데이터베이스의 경우 INSERT, UPDATE 등의 작업은 갈수록 느려지게 된다. 이럴 경우, 테이블을 분할하는 것이
시스템 성능에 큰 도움이 된다. 테이블을 분할 할 때는 테이블의 범위에 따라서 서로 다른 파일그룹에 저장하는 것이 가장 보편적이다.
예를 들어, 10년간 데이터가 저장된 테이블이라면 아마도 과거의 데이터는 주로 조회에만 이용될 뿐 거의 변경이 되지 않을 것이다. 그러므로 작년
이전의 데이터와 올해의 데이터를 서로 다른 파일 그룹에 저장한다면 효과적일 수 있다. 또 다른 예로는 월별 데이터의 업데이트가 잦은 데용량
데이터라면 각 월별로 분할 테이블을 구성할 수도 있다. 그런데 분할 테이블에는 꼭 분할 인덱스라는 용어가 같이 따라오게 된다.
    1. 파일그룹으로 분리한 데이터베이스 생성
    2. 파티션 함수를 정의 : 파티션 함수는 테이블 또는 인덱스를 분할하는 방법을 지정
    3. 파티션 구성표를 정의 : 파티션 구성표는 파티션 함수에 의해 생성된 파티션을 파일그룹별로 나눠주는 역할을 한다.
    4. 테이블 정의 시 파티션 구성표를 적용
    5. 데이터 입력 또는 대량 데이터 로드
    6. 자동으로 범위에 따라서 파일 그룹에 분할되어 저장됨.




-------------------------------------------------------------------------------------------------------------------------------------------------------------------
1.  조인의 종류와 각조인들의 장단점을 말하시오
INNER JOIN - 조인을 하는 테이블에서 서로 일치하는 값이 있는 항목만 가져온다.
CROSS JOIN - 양쪽 테이블의 모든 행에 대해 서로 연결한다. WHERE절이 없다.
OUTER JOIN - 어느 한쪽 테이블의 데이터를 모두 가져온다. LEFT OUTER JOIN, RIGHT OUTER JOIN
SELF JOIN - 자기 자신을 다시 조인한다.

2. 조인시 where 구문이 인라인뷰속에 있는거랑  밖에 있는 거랑의 차이점

3. null 데이타가 존재할시 count, sum 연산
COUNT = 0
SUM = NULL

4. 테이블 한개 던져주고 1~4 정규화 시키시오
1차 정규화 - 복수 속성값에 대한 속성의 분리
2차 정규화 - 주식별자에 종속적이지 않은 속성의 분리
3차 정규화 - 속성에 종속적인 속성의 분리
BCNF - 다수의 식별자 분리
4차 정규화 - 특정 속성 값에 따라 선택적인 속성의 분리(식별자간에 역할이 중복되지 않음)
5차 정규화 - 결합 종속일 경우 두 개 이상의 N개로 분리

5. exec와  sp_excutesql로 각각 동적쿼리를 만들고 이 둘의 차이점을 말하시오
문자열 쿼리 - 받아들이는 변수 값에 따른 쿼리 조건 문이나 기타 쿼리문의 변경이 일어날 때 자주 사용


6. 쿼리플랜을 보고 해당 플렌을 설명하시오

7. 모델링 , 모델링Tool ( ER-WIn, ERSTUDIO 사용유무)

8. 파싱 원리및 컴파일유무

9. 인덱스 종류와 설명

10. 함수/프로시저 만들줄 아느냐?


 

1. page 단위와 익스텐트 종류 설명

2. 백업 종류와 시점별 리스토어 설명

3. DB 옵션설명

4. 스트레스 tool과 성능분석/profiller 사용 유무

5. log 쉬핑 / standby 서버, 클러스터링 / 복제 운영

6. dbcc 명령어

7. 분산/파티셔닝 테이블 원리 이해

8. 파일그룹/ Trmp DB 이동




1. update 는 몇가지 종류가 있나요?

2. merge interval 에 대해서 설명해 보세요.

3. execution plan 의  iterator 를 몇가지 분류로 나누어 보세요.

4. static index seek 와 dynamic index seek 의 차이는 무엇인가요?

5. sort warning 과 hash warning 은 언제 발생하나요?


-------------------------------------------------------------------------------------------------------------------------------------------------------------------

DBA 정기업무
일일작업
 1. 네트워크를 포함한 필요한 서비스가 시작(작동)중인지 확인
 2. 윈도우 이벤트 뷰어를 통한 오류 및 경고등의 중요 메시지를 점검 및 문제를 해결
 3. Sql Server 로그에서 오류 및 중요 메시지를 점검하고 문제를 해결한다.
 4. Sql server 에이전트 로그에서 오류를 포함한 중요 메시지를 점검하고 문제를 해결한다.
 5. 에이전트 서비스에 정의해둔 각 작업(job)의 성공여부를 점검하고 필요한 조칠르 취한다.
 6. windows server와 sql server의 주요 카운트를 모니터하면서 이상 증상을 확인한다.
 7. windows의 성능 모니터의 "성능 로그 및 경고 | 카운트 로그"에서 성능 통계정보를 수집한다.
 8. Profiler와 같은 도구를 이용 과도한 리소스 소비, 잠금 유발, 차단(blocking)문제, DeadLoc을 유발하는
    쿼리를 추적, 조치를 취한다.
 9. 디스크 공간은 충분한지 점검한다.
 10. 통계 업데이트 실시


주간작업
 1. 인덱스 조각화 상태를 점검하고 필요시 적절한 방법으로 조각모음을 수행한다.
  (DBCC SHOWCONTIG, DBCC DB REINDEX, DBCC INDEXDEFRAG)
 2. 시스템 및 사용자 데이테베이스의 전체 백업 혹은 차등 백업을 수행한다.
 3. 통계 업데이트 실시
   (UPDATE STATISTICS, SP_UPDATESTATS)
 4. 데이터와 로그 파일에서 불필요하게 과도한 여유 공간을 줄임

 

월간작업
 1. 운영체제 전체 백업
 2. 시스템 및 사용자 데이터베이스의 전체 백업 혹은 차등 백업 수행
 3. 데이터베이스 무결성 검사 수행, 그 결과에 따라 조치를 취함
   (DBCC CHECKTABLE 혹은 DBCC CHECKDB 명령을 이용)
 4. 테스트 장비에서 시스템 및 사용자 데이터베이스를 완전히 복구 및 복원할 수 있도록 시연
 5. SQLDIAG.EXE 수행 및 검토
 6. 각 서버 별로 지난 1개월간 수집한 성능 통계정보를, 기존의 성능 통계정보와 비교하여
    향후 소요되는 S/W, H/W 용량을 예측한다.

 

비정기작업
 1. 트랜잭션 로그 파일이 일정수준 이상으로 채워진 경우, 로그백업 등등을 이용하여 로그 사이즈를 줄인다.
 2. 데이터베이스 구조 변경, 로그인 변경, 서버 구성 옵션 변경등이 있으면 MASTER를 백업한다.
 3. 에이전트 서비스의 작업, 경고, 운영자 및 유지 관리계획 등이 변경되면 MSDB를 백업한다.
 4. 시스템 및 사용자데이터베이스 개체를 추가 혹은 변경한 경우 해당 데이터베이스의 스크립트 백업을 수행한다.
****************************************************************************/






/*
ANSI에서의 정의에 따르면 모두 4가지 레벨의 문제가 발생

LEVEL0        Dirty Read                        READ UNCOMMITTED
LEVEL1        Non-Repeatable Read    READ COMMITTED
LEVEL2        Phantom Read                REPEATABLE
LEVEL3                                            SERIALIZABLE   


 - 커밋되지 않은 읽기(Dirty Read)
 Dirty Page란 커밋되지 않은 즉, 트랜잭션이 완료되지 않은 페이지를 이야기한다.
 
 - 반복하지 않은 읽기(Non-Repeatable Read)
 동일한 데이터를 반복해서 읽어왔을 때 처음 읽어왔던 데이터와 다른 경우가 발생할 수 있다.

 - 팬텀읽기(Phantom Read)
 행 범위의 한 행에 대해 트랜잭션이 삭제 혹은 추가 시에 생겨나는 것으로 처음에는 데이터 값을 읽어왔다가
 다음에는 사라지는 경우나 반대로 처음에는 아무런 값이 없다가 다음에는 값이 생겨나는 경우를 말한다.


락킹 단위
-----------------------------------------------------------------------------------------------------------------------------------------------
리소스                       |    설명
================================================================================
RID                            | 행 식별자는 힙 내의 단일 행을 잠그는 데 사용
-----------------------------------------------------------------------------------------------------------------------------------------------
KEY                            | 인덱스 내의 행 잠금은 직렬화 가능한 트랜잭션에서 키 범위를 보호하는 데 사용
-----------------------------------------------------------------------------------------------------------------------------------------------
PAGE                        | 데이터 또는 인덱스 페이지와 같은 데이터베이스의 8KB 페이지
-----------------------------------------------------------------------------------------------------------------------------------------------
EXTENT                        | 데이터 또는 인덱스 페이지와 같은 인접한 8개의 페이지 그룹
-----------------------------------------------------------------------------------------------------------------------------------------------
HOBT                        | 힙 또는 B-트리, 클러스터형 인덱스가 없는 테이블에서 데이터 페이지의 힙이나 인덱스를 보호하는 잠금
-----------------------------------------------------------------------------------------------------------------------------------------------
TABLE                        | 모든 데이터와 인덱스가 포함된 전체 테이블
-----------------------------------------------------------------------------------------------------------------------------------------------
FILE                            | 데이터베이스 파일
-----------------------------------------------------------------------------------------------------------------------------------------------
APPLICATION            | 응용 프로그램이 지정한 리소스
-----------------------------------------------------------------------------------------------------------------------------------------------
METADATA                | 메타데이터 잠금
-----------------------------------------------------------------------------------------------------------------------------------------------
ALLOCATION_UNIT    | 할당 단위
-----------------------------------------------------------------------------------------------------------------------------------------------
DATABASE                | 전체 데이터베이스
-----------------------------------------------------------------------------------------------------------------------------------------------

공유 잠금 - Shared Lock, S
배타적 락 혹은 단독 락 - Exclusive Lock, X
업데이트 락 - Update Lock, U
내재적 락 - Intent Lock, IS or IX or SIX
스키마 락 - Schema Lock, Sch-M or Sch-S
                Sch-M(Schema Modification Lock)은 테이블 구조를 변경하는 DDL 문과 관련하는 작업을 수행하는 동안 걸리며,
                이 잠금 모드를 수행 중에는 다른 트랜잭션이 해당 테이블을 읽거나 수정할 수 없다.
                Sch-S(Schema Stability Lock)은 쿼리문을 컴파일할 때 테이블 스키마를 참조하기 위해 Sch-S 잠금 모드가 사용되며
                이 모드가 시행 중에는 스키마를 수정하거나 삭제할 수 없다.
대량업데이트 - Bulk Update, BU
키 범위 락 - SERIALIZABLE 트랜잭션 격리 수준을 사용할 때 쿼리가 읽는 핵 범위를 잠근다.
*/



/* 데이터베이스 엔진 격리 수준 */
/*
    READ UNCOMMITTED(커밋되지 않은 읽기)
    READ COMMITTED(커밋된 읽기)
    REPEATABLE READ(반복 읽기)
    SNAPSHOT(스냅숏)
    SERIALIZABLE(직렬화 가능)
*/

/* 여러 사용자가 동시에 하나의 데이터에 접근할 때 발생하는 문제
    Dirty Read(더티 리드, 커밋되지 않은 데이터를 읽기)
    Unrepeatable Read(반복되지 않은 읽기)
    Phantom Read(팬텀, 가상 읽기)
*/

/* Dirty Read
메모리(데이터 캐시)에는 변경이 되었지만 아직 디스크에는 변경되지 않은 데이터

READ UNCOMMITTED 는 Dirty READ를 허용해준다.
*/
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED


/* Unrepeatable Read
트랜잭션 내에서 한 번 읽은 데이터가 트랜잭션이 끝나기 전에 변경되었다면, 다시 읽었을 때 새로운 값이 읽히는 것

SELECT(Shared Lock-공유잠금), INSERT/UPDATE/DELETE(Exclusive Lock-배타적잠금)

한쪽에서 조회(SELECT) 시 - (공유잠금을 한 상태인데도 불구하고)  다른 쪽에서 INSERT/UPDATE/DELETE를 하게되면
반영되어 버려 다시 조회 시 변경되어 버리는 문제점

REPEATABLE READ 는 Unrepeatable Read를 허용해준다.
*/
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ


/* Phantom Read
REPEATABLE READ 격리 수준에서는 트랜잭션이 진행 중인(엄밀히 말하면 공유 잠금이 설정된) 데이터에 대해서
변경 작업을 할 수 없으나 새로운 데이터 입력 작업은 가능하다. 이것을 Phantom Read(가상 읽기)라고 부른다.

Phatom Read 방지를 위해 SERIALIZABLE 로 설정하면 된다. 또는 SNAPSHOT 사용(tempdb에 임시로 저장)
*/
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

SET TRANSACTION ISOLATION LEVEL SNAPSHOT;


/* 잠금 모드
- 공유 잠금(S : Shared Lock) : SELECT 문처럼 데이터를 읽는 작업에 사용. 공유 잠금 사이에는 서로 호환됨
- 업데이트 잠금(U : Update Lock) : 업데이트 할 수 있는 리소스에 사용. 여러 사용자가 업데이트 할 때 발생하는 교착 상태 방지
- 배타 잠금(X : Exclusive Lock) : 데이터 수정 작업이 발생하는 INSERT/UPDATE/DELETE에서 사용.
- 의도 잠금(Intent Lock) : 잠금 계층 구조를 만드는 데 사용. 의도 잠금의 종류에는
의도 공유(IS), 의도 배타(IX), 의도 배타 공유(SIX)가 있음.
- 스키마 잠금(Schema Lock) : 테이블의 스키마에 종속되는 작업이 실행될 때 사용. 스키마 잠금에는 스키마 수정(Sch-M)과
스키마 안정성(Sch-S) 잠금이 있음.
- 대량 업데이트 잠금(Bulk Update Lock) : 데이터를 테이블로 대량 복사할 때와 TABLOCK 힌트가 지정되었을 때 사용.
- 키 범위 잠금 : SERIALIZABLE 트랜잭션 격리 수준을 사용할 때 쿼리가 읽는 행 범위를 보호.
*/

/* 잠금의 정보 확인과 힌트
잠금 정보 - sys.dm_tran_locks
트랜잭션 정보 - sys.dm_tran_database_transactions
sp_helptext sp_lock
*/

SELECT resource_type, resource_database_id, resource_associated_entity_id, request_mode
FROM sys.dm_tran_locks;

--============================================================================
                                                                            현재 잠금
시도하는 잠금                            IS                S                U                IX                SIX            X                Sch-S            Sch-M
--============================================================================
내재된 공유(IS)                        O                O                O                O                O                X                    O                    X
-------------------------------------------------------------------------------------------------------------------------------------------
공유(S)                                    O                O                O                X                X                X                    O                    X
-------------------------------------------------------------------------------------------------------------------------------------------
업데이트(U)                            O                O                X                X                X                X                    O                    X
-------------------------------------------------------------------------------------------------------------------------------------------
의도 배타(IX)                            O                X                X                O                X                X                    O                    X
-------------------------------------------------------------------------------------------------------------------------------------------
의도 배타 공유(SIX)                    O                X                X                X                X                X                    O                    X
-------------------------------------------------------------------------------------------------------------------------------------------
배타(X)                                    X                X                X                X                X                X                    O                    X
-------------------------------------------------------------------------------------------------------------------------------------------
Sch-S                                    O                O                O                O                O                O                    O                    X
-------------------------------------------------------------------------------------------------------------------------------------------
Sch-M                                    X                X                X                X                X                X                    X                    X
-------------------------------------------------------------------------------------------------------------------------------------------

/* 잠금 힌트
HOLDLOCK                    HOLDLOCK은 순차 가능(SERIALIZABLE)과 같다.
NOLOCK                        Dirty Read를 하는 것으로써 커밋되지 않은 읽기(Read Uncommitted)와 동일하며
                                    커밋되지 않은 트랜잭션이나 롤백 된 페이지 집합을 읽을 수도 있다.
PAGLOCK                        주로 단일 테이블 잠금이 취해지는 곳에서 페이지 잠금을 사용한다.
READCOMMITTED            커밋된 읽기(READ COMMITTED) 격리 수준과 같다.
READPAST                    SELECT 문에서만 적용되며, 다른 트랜잭션에 의해 락킹된 로우들은 건너뛰고 읽는다.
READUNCOMMITTED    NOLOCK과 같다.
REPEATABLEREAD            반복 읽기(REPEATABLE READ) 격리 수준과 같다.
ROWLOCK                    로우(ROW) 수준의 락을 사용한다.
SERIALIZABLE                순차 가능(SERIALIZABLE) 격리 수준과 같다.
TABLOCK                        테이블 수준의 락을 사용한다.
TABLOCKX                    테이블에 대해 배타적 락을 사용한다.
UPDLOCK                        테이블을 읽는 중 공유 락 대신 업데이트 락을 사용한다.
XLOCK                            트랜잭션이 끝날 때까지 보유될 배타적 락을 사용한다.
*/
반응형

'연구개발 > DBA' 카테고리의 다른 글

조인 순서와 성능  (0) 2012.01.23
조인 join  (0) 2012.01.20
데이터베이스 백업과 복원  (0) 2012.01.20
실행계획  (0) 2012.01.20
조인 방식 (Join Method)  (0) 2012.01.18
반응형
/* 데이터베이스 백업과 복원
* 데이터베이스 복구모델
    전체(Full) | 대량 로그(Bulked Log) | 단순(Simple)

 * 전체 복구 모델
    전체 백업, 차등 백업, 로그 백업
 * 대량 로그 복구 모델
    전체 백업, 차등 백업, 로그 백업
 * 단순 모델
    전체 백업, 차등 백업, 부분 백업(SQL 2005/2008)   


* 데이터베이스 백업의 종류
 * 전체 백업
    전체 백업을 꼭 받아야 하는 경우
        1. 처음 데이터베이스를 생성했을 때
        2. 트랜잭션 로그를 강제로 비웠을 때
        3. 데이터베이스에 변경이 생겼을 때(ALTER DATABASE 구문 실행 후)
        4. 차등백업과 로그백업을 하기 이전
    BACKUP DATABASE 데이터베이스이름 TO 백업할 파일 또는 장치

    sp_spaceused - 사용공간확인

 * 차등 백업
    마지막 전체 백업 이후에 변경된 모든 데이터를 백업
    전체 차등 백업 | 부분 차등 백업
   
    BACKUP DATABASE 데이터베이스이름 TO 백업할 파일 또는 장치 WITH DIFFERNTIAL

 * 트랜잭션 로그 백업
    BACKUP LOG 데이터베이스이름 TO 백업할 파일 또는 장치

 * 파일 및 파일 그룹 백업
    파일 단위로 백업. 이 경우 파일을 백업할 때 로그 백업도 꼭 같이 수행해야 한다.

 * 부분 백업(전체 백업과 비슷)
    읽기 전용 파일 그룹은 백업하지 않음.
   
    BACKUP DATABASE 데이터베이스이름 READ_WRITE_FILEGROUPS TO 장치

 * 부분 차등 백업(차등 백업과 비슷)


* 백업에 사용되는 기능 및 옵션
 * 백업 압축
 
 * 미러 백업
    BACKUP DATABASE AdventureWorks TO DISK = 'F:\백업장치\adv.bak' MIRROR TO DISK = 'E:\백업장치\adv.bak' WITH FORMAT
   
 * 복사 전용 백업 
    BACKUP DATABASE 데이터베이스이름 TO 장치 WITH COPY_ONLY

 * 체크섬(CHECKSUM)
    백업할 때 백업하는 데이터에 이상이 없는지 확인하면서 백업하는 기능
    주의) 백업할 때 WITH CHECKSUM옵션을 사용해서 백업한 것은 복원할 때도 CHECKSUM옵션을 사용할 수 있다.
    BACKUP DATABSE 데이터베이스이름 TO 장치 WITH CHECKSUM
 
 * 백업매체 초기화
    BACKUP DATABASE 데이터베이스이름 TO 장치 WITH INIT

 * 다중 백업 장치의 초기화
    BACKUP DATABASE 데이터베이스이름 TO 장치1, 장치2, 장치3 WITH FORMAT

 * 비밀번호 지정
    BACKUP DATABASE 데이터베이스이름 TO 장치 WITH PASSWORD = '비밀번호'

 * 백업 중 오류 발생 시 계속 여부(DEFAULT는 STOP ON ERROR)
    BACKUP DATABASE 데이터베이스이름 TO 장치 WITH CONTINUE_AFTER_ERROR
   
 * 진행률 표시
    BACKUP DATABASE 데이터베이스이름 TO 장치 WITH STATS
   
 * 데이터베이스에 문제 발생 시 로그 백업(=비상 로그 백업)
    BACKUP LOG 데이터베이스이름 TO 장치 WITH NO_TRUNCATE
    참고) 데이터베이스가 손상된 경우 NO_TRUNCATE 옵션을 사용하여 비상 로그 백업을 수행할 수 없다면,
        대신에 CONTINUE_AFTER_ERROR 을 지정하여 비상 로그 백업을 수행할 수도 있다.
       
* 데이터베이스 복원

 * 전체 복원 | 차등 복원 | 로그 복원
    * 전체 복원
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치
    * 차등 복원
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치 [WITH DIFFERENTIAL]
    * 로그 복원
        RESTORE LOG 데이터베이스이름 FROM 백업장치

 * 데이터베이스 복원의 기능 및 옵션
    * 복원 완료 및 복원 중
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치 WITH NORECOVERY
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치 WITH RECOVERY(디폴트)
    * 복원 후에 제한된 사용자만 접근 허용
        WITH RESTRICTED_USER 옵션은 db_owner, dbcreator, sysadmin 역할의 권한을 가진 사용자만 접근 가능
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치 WITH RESTRICTED_USER
        RESTORE DATABASE 데이터베이스이름 WITH RECOVERY(복원 완료 표시만)
    * 복원시 데이터 파일의 이동
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치
            WITH MOVE 'AdventureWorks_data' TO 'd:\adv.mdf'(옴기고자하는 장소),
                    MOVE 'AdventureWorks_log' TO 'd:\adv.ldf(옴기고자하는 장소)'
    * 오류가 발생해도 계속 복원하기
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치 WITH CONTINUE_AFTER_ERROR
    * 원래 파일이 있으면 덮어쓰기
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치 WITH REPLACE
    * 정확한 시점까지만 복원하기
        RESTORE DATABASE 데이터베이스이름 FROM 백업장치 WITH STOPAT = '날짜와시간'
    * 데이터베이스 스냅숏으로 복원하기
        RESTORE DATABASE 데이터베이스이름 FROM DATABASE_SNAPSHOT = '스냅숏이름';
    * 파일 온라인 복원
    * 페이지 복원
        RESTORE DATABASE 데이터베이스이름 PAGE = '파일번호:페이지번호' FROM 백업장치
    * 증분 복원
        파일 그룹 단위로 복원을 진행. WITH PARTIAL

*/

USE master
EXEC sp_addumpdevice 'disk', 'backupDevice2', 'F:\백업장치\back2.bak';

/*
한달에 한 번씩 전체백업 수행
일주일에 한 번씩 차등백업 수행
매일 로그 백업 수행
*/
USE master;
GO
DROP DATABASE testDB1;
GO
CREATE DATABASE [testDB1] ON PRIMARY
(
    NAME = N'testDB1',
    FILENAME = N'F:\백업장치\데이터파일\testDB1.mdf'
)
LOG ON
(
    NAME = N'testDB1_log',
    FILENAME = N'F:\백업장치\로그파일\testDB1_log.ldf'
);
GO
USE [testDB1];
CREATE TABLE tbl1 (no INT);

INSERT INTO tbl1 VALUES (10);
--1회 전체백업
BACKUP DATABASE testDB1 TO DISK = 'F:\백업장치\testDB1.bak';

--백업 후 입력
INSERT INTO tbl1 VALUES (20);
INSERT INTO tbl1 VALUES (30);
--2회 차등백업
BACKUP DATABASE testDB1 TO DISK = 'F:\백업장치\testDB1.bak' WITH DIFFERENTIAL;

--백업 후 입력
INSERT INTO tbl1 VALUES (40);
INSERT INTO tbl1 VALUES (50);
--3회 로그백업
BACKUP LOG testDB1 TO DISK = 'F:\백업장치\testDB1.bak' WITH DIFFERENTIAL;

--백업 후 입력
INSERT INTO tbl1 VALUES (60);
--4회 로그백업
BACKUP LOG testDB1 TO DISK = 'F:\백업장치\testDB1.bak' WITH DIFFERENTIAL;

--백업 후 입력
INSERT INTO tbl1 VALUES (70);
INSERT INTO tbl1 VALUES (80);
INSERT INTO tbl1 VALUES (90);
SELECT * FROM tbl1;

--사고발생
USE master;
ALTER DATABASE testDB1
    SET OFFLINE;
-- mdf파일 삭제

ALTER DATABASE testDB1
    SET ONLINE;
--오류는 발생했지만 온라인 상태로 변경됨.

--mdf 파일은 깨졌지만 ldf(로그파일)이 남아있다면 '비상 로그 백업'이 가능
--비상로그백업 실시
BACKUP LOG testDB1 TO DISK = 'F:\백업장치\임시로그백업.bak' WITH NO_TRUNCATE, INIT


USE testDB1;
SELECT * FROM tbl1;

DROP DATABASE 로그백업안할경우;
DROP DATABASE indexDB;
DROP DATABASE partTblDB;
DROP DATABASE sqlDB;
DROP DATABASE stoneDB;
DROP DATABASE tableDB;
--DROP DATABASE testDB1;
DROP DATABASE tranDB;


USE master;
GO
CREATE DATABASE [testDB2] ON PRIMARY
(
    NAME = N'FG1-1',
    FILENAME = N'F:\백업장치\데이터파일\FG1-1.mdf'
),
(
    NAME = N'FG1-2',
    FILENAME = N'F:\백업장치\데이터파일\FG1-2.ndf'
),
FILEGROUP [FG2]
(
    NAME = N'FG2-1',
    FILENAME = N'F:\백업장치\데이터파일\FG2-1.ndf'
),
(
    NAME = N'FG2-2',
    FILENAME = N'F:\백업장치\데이터파일\FG2-2.ndf'
),
FILEGROUP [FG3]
(
    NAME = N'FG3-1',
    FILENAME = N'F:\백업장치\데이터파일\FG3-1.ndf'
),
(
    NAME = N'FG3-2',
    FILENAME = N'F:\백업장치\데이터파일\FG3-2.ndf'
)
LOG ON
(
    NAME = N'testDB2_log',
    FILENAME = N'F:\백업장치\로그파일\testDB2_log.ldf'
)
GO

USE testDB2;
CREATE TABLE tbl1 (num int);
GO
CREATE TABLE tbl2 (num int) ON FG2;
GO
CREATE TABLE tbl3 (num int) ON FG3;;
GO


USE testDB2;
INSERT INTO tbl1 VALUES (10);
INSERT INTO tbl2 VALUES (11);
INSERT INTO tbl3 VALUES (12);
GO

BACKUP DATABASE testDB2 TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제1회-전체백업', INIT
GO

BACKUP LOG testDB2 TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제1회-로그백업';
GO

USE testDB2;
INSERT INTO tbl1 VALUES (20);
INSERT INTO tbl2 VALUES (21);
INSERT INTO tbl3 VALUES (22);
GO

BACKUP DATABASE testDB2 FILEGROUP = 'PRIMARY' TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제2회-파일그룹백업';
GO

BACKUP LOG testDB2 TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제2회-로그백업';
GO


USE testDB2;
INSERT INTO tbl1 VALUES (30);
INSERT INTO tbl2 VALUES (31);
INSERT INTO tbl3 VALUES (32);
GO

BACKUP DATABASE testDB2 FILEGROUP = 'FG2' TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제3회-파일그룹백업';
GO

BACKUP LOG testDB2 TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제3회-로그백업';
GO

USE testDB2;
INSERT INTO tbl1 VALUES (40);
INSERT INTO tbl2 VALUES (40);
INSERT INTO tbl3 VALUES (40);
GO

BACKUP DATABASE testDB2 FILEGROUP = 'FG3' TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제4회-파일그룹백업';
GO

BACKUP LOG testDB2 TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = N'제4회-로그백업';
GO

RESTORE HEADERONLY FROM DISK = 'F:\백업장치\testDB2.bak';

USE testDB2;
INSERT INTO tbl1 VALUES (50);
INSERT INTO tbl2 VALUES (51);
INSERT INTO tbl3 VALUES (52);
GO

SELECT * FROM tbl1;
SELECT * FROM tbl2;
SELECT * FROM tbl3;

ALTER DATABASE testDB2
    SET OFFLINE;
GO

--FG2-1.ndf 삭제

ALTER DATABASE testDB2
    SET ONLINE;
GO


RESTORE HEADERONLY FROM DISK = 'F:\백업장치\testDB2.bak';

USE master;
RESTORE DATABASE testDB2 FILEGROUP = 'FG2' FROM DISK = 'F:\백업장치\testDB2.bak' WITH FILE = 5, NORECOVERY;

BACKUP LOG testDB2 TO DISK = 'F:\백업장치\testDB2.bak' WITH NAME = '제5회-비상로그백업', NO_TRUNCATE;

RESTORE HEADERONLY FROM DISK = 'F:\백업장치\testDB2.bak';


USE master;
RESTORE DATABASE testDB2 FILEGROUP = 'FG2' FROM DISK = 'F:\백업장치\testDB2.bak' WITH FILE = 5, NORECOVERY;
GO
RESTORE DATABASE testDB2 FILEGROUP = 'FG2' FROM DISK = 'F:\백업장치\testDB2.bak' WITH FILE = 6, NORECOVERY;
GO
RESTORE LOG testDB2 FILEGROUP = 'FG2' FROM DISK = 'F:\백업장치\testDB2.bak' WITH FILE = 8, NORECOVERY;
GO
RESTORE LOG testDB2 FROM DISK = 'F:\백업장치\testDB2.bak' WITH FILE = 9, RECOVERY;
GO


USE testDB2;
SELECT * FROM tbl1;
SELECT * FROM tbl2;
SELECT * FROM tbl3;

BACKUP DATABASE testDB2 TO DISK = 'F:\백업장치\testDB2-FULL.bak' WITH NAME = N'전체백업', INIT;
GO
반응형

'연구개발 > DBA' 카테고리의 다른 글

조인 join  (0) 2012.01.20
면접질의  (0) 2012.01.20
실행계획  (0) 2012.01.20
조인 방식 (Join Method)  (0) 2012.01.18
연습문제  (0) 2012.01.16
반응형
/*
-- T-SQL을 질의하면 어떤 일이 발생할까?
SQL Server에는 많은 프로세스들이 존재한다. 이 프로세스들은 데이터 질의에 대한 처리, 데이터 저장, 데이터 유지보수 등과 같이 시스템 관리를
위해 다양한 일들을 수행하는데, 우리가 T-SQL을 질의하면 SQL Server은 해당 쿼리를 수행하기 우해 몇 가지 프로세스를 동작시킨다.

이 중에서 T-SQL에 관련된 프로세스는 크게 아래의 두 개로 요약할 수 있다.
* Relational engine 관련 프로세스
* Storage engine 관련 프로세스
Relational engine 은 보내어진 쿼리를 파싱한 후, 실행 계획을 작성하기 위해 쿼리 옵티마이저에게 해당 쿼리를 보낸다.
이 단계에서 작성된 실행 계획은 다시 Storage engine에 보내어지게 되며,
Storage engine은 실행계획을 바탕으로 데이터를 검색하거나 입력, 갱신, 삭제하는 등 실질적인 데이터 처리를 수행한다.
그리고 Storage engine에는 잠금(lock), 인덱스 유지보수(index maintance), 트랜잭션과 같은 프로세스들이 있다.


** 쿼리 파싱(Query Parsing)
T-SQL을 실행의 첫 단계는 Relational engine에서 쿼리를 파싱하는 일이다. 사용자가 T-SQL을 질의하면 먼저 T-SQL이 정확한 지
구문 검사를 하는데 이를 파싱(Parsing)이라고 한다.
그리고 파싱 프로세스의 결과물을 파스 트리(parse tree), 쿼리 트리(query tree) 혹은 시퀀스 트리(squence tree)라고 하며
쿼리 실행을 위한 논리적 단계를 의미한다.
만일, 질의한 T-SQL이 DML(Data Manipulation Langauge)문이 아니라면 최적화(Optimizing) 단계는 건너 뛰게 된다.
예를 들어 CREATE TABLE 문은 단순히 "테이블을 이렇게 이렇게 작성해 주세요" 라는 의미이기 때문에 최적화 할 수 있는 여지가
없는 것이다. 반대로 질의한 T-SQL이 DML인 경우 algebrizer라고 불리는 프로세스를 호출하여 파스 트리(parse tree)를 통과시킨다.
algebrizer 프로세스에서는 질의된 T-SQL에 관련된 모든 오브젝트(테이블, 컬럼 등)을 체크하고, 이와 동시에 aggregate binding
이라고 불리는 프로세스를 함께 호출하여 GROUP BY, MAX와 같은 집계위치를 체크한다.
이렇게 쿼리 옵티마이저를 통과하여 algebrizer 프로세스가 처리한 결과물을 query processor tree 라고 한다.

** 쿼리 옵티마이저(Query Optimizer)
쿼리 옵티마이저는 Database relational engine이 동작하기 위한 길(혹은 모델)을 제시하는 역할을 하는 소프트웨어 조각이라고
말할 수 있다.
쿼리 옵티마이저는 query processor tree와 통계 정보를 바탕으로 질의된 쿼리를 실행하기 위한 가장 최적화된 방법을 계산한다.
예를 들어, 해당 T-SQL을 실행하기 위해 인덱스를 사용할지, 사용한다면 어떤 인덱스를 사용할지, 혹은 조인을 한다면 어떤 조인
방법을 사용할지 등 많은 것을 결정한다. 그리고 이는 CPU 사용, I/O 그리고 실행 방법에 대한 비용(Cost) 등 다양한 계산 결과에
근거한다. 그래서 우리는 이를 비용 기반 최적화기(Cost-based optimizer)라고 하는 것이다.

쿼리 옵티마이저는 - 실행 계획이 캐쉬되어 있지 않다면 - 해당 T-SQL을 실행하기 위해 여러가지  경우의 수(실행 계획)를 생각하고,
이 중에서 가장 적은 리소스를 사용하는 실행 계획을 선택한다. 그렇다고 쿼리 옵티마이저가 항상 최적의 실행 계획을 제시하는 것은 아니
라는 것을 유의하기 바란다.

만일 아주 심플한 쿼리 - 예를 들어 인덱스를 사용하지 않으며 집계나 계산이 없는... 단순히 하나의 테이블을 쿼리하는 경우 - 인 경우,
실행 계획이 이미 명백하기 때문에 실행 계획을 계산하기 위해 시간을 보내기 보다는 그냥 실행 계획 하나를 적용한다.
그래서 이를 Trival Plan(명백한 플랜)이라고 한다. 반대로 non-trival일 경우, 쿼리 옵티마이저는 실행 계획을 선택하기 위해
비용 기반(cost-based) 연산을 실행하며, 이를 위해 SQL Server 스스로가 유지-관리하는 통계정보를 적용한다.

통계정보는 데이터베이스 내의 컬럼과 인덱스 등에서 수집되며, 데이터의 분포도, 유일성, 선택도 등을 나타낸다. 이 정보는 히스토그램 형태로
표현되는데, 전체 데이터에서 200데이터 포인트에서 추출한 특정 값의 발생 빈도표이다. 이는 "데이터의 데이터" - 흔히들 메타데이터
라고 하는 - 로써 쿼리 옵티마이저가 계산하는데 필요한 정보를 제공되어진다.

만일, 상응하는 컬럼이나 인덱스의 통계정보가 이미 존재한다면 쿼리 옵티마이저는 그것을 사용할 것이다. 기본적으로 WHERE절이나
JOIN ON절의 한 부분으로 선언된 모든 컬럼과 인덱스에 대한 통계정보는 자동으로 생성 및 갱신된다. 반대로 통계정보가 한번도
생성되지 않은 테이블에 대해서는 그 테이블의 실제 크기를 무시하고 하나의 로우만 있다고 쿼리 옵티마이저는 가정한다.
또한 통계정보가 생성되어 있는 임시 테이블의 경우 마치 영구 테이블처럼 히스토그램을 저장하여 사용한다.

이렇게 수집된 통계정보와 함께 앞에서 설명한 Query processor tree를 기반으로 쿼리 옵티마이저는 최적의 실행 계획을 선택하게 된다.
인생사에서도 그렇듯이 선택이라는 것은 참으로 힘든 과정이다. 이것은 쿼리 옵티마이저도 마찬가지이다. 최적의 실행 계획을 도출해 내기 위해
일련의 실행 계획들에 대해 다양한 조인(Join)전략을 테스트하기도 하고, 조인 순서도 바꿔보기도 하고, 때로는 다른 인덱스를 이용해 보는 등
많은 고민을 한다. 이러한 연산이 수행되는 동안 실행 계획 내의 각 스탭에 일련의 숫자가 할당되는데, 이 숫자는 해당 스탭이 수행되는데 필요한
예상 소요 시간을 의미한다.
이를 예상비용(이하 Estimated cost)라고 하며, 결국 각 스텝의 비용의 누적치가 해당 플랜 전체의 비용이 되는 것이다.

여기서 우리는 중요한 사실을 기억해야 한다. Estimated cost는 말 그대로 예상치 라는 것을 말한다. 쿼리 옵티마이저에게 충분한 시간을 주고
매일 통계정보를 갱신시켜 준다면 쿼리를 실행하기 위한 완벽한 실행 계획을 찾을 것이다. 그러나 현실은 그렇지 못하다.
쿼리 옵티마이저가 하나의 T-SQL만 처리하는 것도 아닐 뿐더러, 제한된 통계정보를 바탕으로 수천 혹은 수만 분의 일초 안에 최적의 실행계획을
찾아야 한다. 따라서 Estimated cost는 도구 일 뿐이지 현실을 반영한 것은 아니라는 것을 유의해야 할 것이다.

쿼리 옵티마이저가 실행 계획을 작성하면 실제 실행 계획(Actual execution plan)을 만들고, 이와 똑같은 실행 계획이 캐시에 없다면
Plan cache이라고 알려져 있는 메모리 영역에 저장된다. 이는 아래의 실행 계획 재사용(Excution plan reuse)에서 다시 설명하도록 하겠다.


** 쿼리 실행(Query execution)
실행 계획이 작성되면 Storage engine이 쿼리를 실행한다.


** 실행 계획 재사용(Execution plan reuse)
SQL Server가 실행 계획을 작성하는데 드는 비용과 부하는 상당하다. 따라서 SQL Server는 한 번 작성된 실행 계획을 어딘가에 보관해 두었다가
기회가 되면 다시 사용하려고 한다.
그 어딘가가 plan cache(이전에는 procedure cache)라 불리는 곳이고, 기회라는 것은 똑같은 쿼리가 들어올 때이다.

SQL Server에 쿼리가 질의하며 쿼리 옵티마이저는 예상 실행 계획(Estimated Execution Plan)을 작성한다. 그리고 Storage engine을
통과하기 전에, 작성된 예상 실행 계획과 plan cache에 저장되어 있는 실제 실행 계획(Actual Execution Plan)을 비교하여
일차하는 실행 계획이 존재한다면 이를 사용한다. SQL Server는 실행 계획을 재 사용하므로써 아주 크고 복잡한 쿼리, 혹은 단순하지만
1분에 수 천, 수 만 번 실행되는 작은 쿼리에 대해 실질 실행 계획을 작성해야 하는 오버헤드를 피할 수 있는 것이다.

쿼리 옵티마이저는 각 실행 계획이 병렬 실행(Parallel execution)했을 때 더 좋은 성능을 낼 수 있다고 판단하지 않는 이상 실행 계획을
단 한 번만 저장한다. 하지만 쿼리 옵티마이저가 병렬 실행을 선택한다면, 병렬 실행을 지원하는 또 다른 실행 계획을 작성하고 저장한다.
즉 이 순간 하나의 쿼리에 대해 두 개의 실행 계획이 존재하게 되는 것이다.

한 번 저장된 실행 계획은 메모리에 영원히 저장되는 것은 아니다. SQL Server는 저장된 실행 계획이 얼마나 자주 실행되는지 주기적으로
지켜보고 있다가 다음과 같은 공식에 의거하여 자주 사용하지 않는 실행 계획을 메모리에서 비운다. 이를 age out이라고 한다.
 * age = [estimated cost for the plan] X [the number of used times]
이 작업은 lazywriter라는 내부 프로세스가 담당하는데 비단, plan cache 뿐만 아니라 모든 종류의 캐시를 비우는 일을 한다.

Lazywriter는 아래와 같은 이슈가 발생했을 때 메모리에서 실행 계획을 비운다.
 * 시스템 메모리가 부족하여 시스템에게 메모리를 양보해야 할 경우
 * 실행 계획의 "age"가 0에 도달하여 실행 계획을 더 이상 사용하지 않을 거라 판단할 경우
 * SQL Server에 연결되어 있는 현재 커넥션들이 더 이상 실행 계획을 참조하지 않을 경우
실행 계획은 어떤 이벤트나 액션에 의해 재컴파일 되기도 한다. 실행 계획 재컴파일은 매우 비용이 높은 작업이므로 아래의 내용을
잘 숙지해야 한다.

- 실행 계획 재컴파일을 유발하는 액션들
 * 쿼리가 참조하는 테이블의 스키마나 구조가 바뀌었을 경우
 * 쿼리에서 사용된 인덱스가 바뀌었을 경우
 * 쿼리에서 사용된 인덱스가 삭제되었을 경우
 * 쿼리에서 사용된 통계정보가 갱신되었을 경우
 * sp_recompile 함수가 호출되었을 경우
 * 대량의 insert나 delete를 하는 쿼리를 할 때 (특히 해당 쿼리가 테이블의 key가 참조될 경우)
 * 트리거로 인해 inserted 테이블과 deleted 테이블의 사이즈가 급격히 증가하였을 경우
 * 하나의 쿼리 안에 DDL과 DML이 함께 존재하는 경우(deferred compile)
 * 쿼리 내의 SET 옵션을 변경하는 경우
 * 쿼리에서 사용된 임시 테이블의 스키마나 구조가 변경되었을 경우
 * 쿼리에서 사용된 동적 뷰(dynamic view)가 바뀌었을 경우
 * 쿼리 내의 커서 옵션을 변경한 경우
 * Distributed partitioned view와 같은 remote rowset을 변경한 경우
 * FOR BROWSE 옵션이 변경되어 클라이언트 측 커서(client side cursor)를 사용할 경우
경우에 따라서는 캐시를 비워야 할 필요가 있을 수도 있다. 예를 들어 실행 계획을 컴파일할 때 얼마나 시간이 걸리는지,
혹은 튜닝 전후의 실행 계획을 비교하자 할 때 아래의 명령어를 사용해서 캐시를 비울 수 있다.
DBCC FREEPROCCACHE

아래 쿼리를 사용하면 호출된 T-SQL과 해당 T-SQL이 실행될 때 작성된 XML 실행 계획을 모두 확인할 수 있다.
그리고 XML 실행 계획은 XML 형식으로 직접 확인하거나 그래픽 실행 계획으로 파일을 열어서 확인할 수 있다.

SELECT [cp].[refcounts]
      ,[cp].[usecounts]
      ,[cp].[objtype]
      ,[st].[dbid]
      ,[st].[objectid]
      ,[st].[text]
      ,[qp].[query_plan]
  FROM sys.dm_exec_cached_plans cp
 CROSS APPLY sys.dm_exec_sql_text(cp.plan_handle) st
 CROSS APPLY sys.dm_exec_query_plan(cp.plan_handle) qp;



** 그래픽 실행 계획 해석
SQL Server의 실행 계획에는 약 78개의 실행 계획 연산자(Showplan operator)가 있다.


** Hash Match Join
Hash Match Join 을 이해하기 위해서는 먼저, hasing과 hash table이라는 두 가지 컨셉을 알아야 한다.

Hasing(이하 해싱)은 데이터를 좀 더 빨리 검색하기 위해 고안된 프로그램적인 기술로써, 데이터가 저장되어 있는 주소를 함수로 계산한 후,
계산된 주소로 직접 접근 가능하게 하는 기법이다. 이때 계산에 사용된 함수를 hasing function(이하 해시 함수)이라고 한다.
이 방법을 통해 테이블 내의 특정 행은 그 행을 나타내는 어떤 유일한 값으로 변환할 수 있다.
변환된 값은 암호화되며, 반대로 암호화된 해시 값은 원래의 데이터로 되돌릴 수 있다.

Hash table(이하 해시 테이블)은 해시 함수에 의해 계산된 함수 값(주소)를 저장하고 있는 표 형태의 데이터 구조이다.
일반적으로 bucket과 slot의 배열로 선언되어 있는데, 계산된 함수 값들은 각각의 bucket에 균등하게 나뉘어져 bucket내의 slot에 저장된다.

Hash Match Join 의 메카니즘에 대해 알아보자
먼저, 조인에 참여하는 두 개의 테이블 중 상대적으로 크기가 작은 테이블을 해시 함수에 통과시켜 그 결과 값을 해시 테이블에 저장한다.
그런 다음, 큰 테이블을 한 행, 한 행, 해시 테이블에 매칭을 시켜가며 값을 찾아내 간다.
여기서 기억해야 할 중요한 사실 하나!!!! 해시 테이블에는 실제 데이터가 아닌 해시 값이 저장되기 때문에,
작은 테이블이 해싱되어야 해시 테이블의 크기가 작게 유지하여  비교 속도가 빨라지게 된다.
따라서 해시되는 테이블이 작으면 작을수록 속도가 빨라지게 된다.

Hash Match Join은 아주 작은 테이블과 아주 큰 테이블이 조인할 때 다른 조인들(Nested Loop Join, Merge Join)보다 월등한 성능을 발휘한다.
조인 컬럼에 대한 정렬이 필요없거나, 쓸만한 인덱스가 없을 경우에도 유용한다. Hash Match Join이 발생했다는 것은 다른 조인 방법이 마땅히
없다는 것을 의미한다. 따라서 실행 계획 내에 Hash Match Join 연산이 있다면 다음 사항을 유심히 살펴볼 필요가 있다.
 * 인덱스가 없거나 인덱스가 잘못 작성되어 있는가?
 * 쿼리에서 조건(WHERE)절을 빼먹지는 않았는가?
 * 조건절에 변형이 일어나 인덱스를 사용하지 못하고 있지는 않은가?


** Compute Scalar 연산자는 논리/물리 연산자로써, 식을 계산하여 계산된 스칼라 값을 만든다.
즉, 사칙연산이나 스칼라 함수 사용과 같이 식을 계산할 때 나타난다.

  
** Merge Join
Merge Join 연산을 통해 반환된 모든 행을 조인한다. Merge 즉, 병합을 위해서는 조인된 양쪽 컬럼이 반드시 정렬되어 있어야 한다.
그렇기에 Merge Join 을 Sort Merge Join 이라고 부르기도 한다. 하지만 이 예제에서는 선행 연산들에 의해 조인 컬럼들이
이미 정렬되어 있기 때문에 별도의 정렬 작업이 필요없다.
Merge Join 은 예제처럼 조인 컬럼들이 정렬이 되어 있을 경우 매우 빠르지만 정렬 작업이 필요한 경우에는 비용이 많이 드는 방법이 될 수 있다.


*/

USE AdventureWorks;
SELECT * FROM [dbo].[DatabaseLog] WITH (NOLOCK);
GO
SELECT * FROM [dbo].[DatabaseLog];
GO


SET SHOWPLAN_ALL ON;

SET SHOWPLAN_ALL OFF;

SET STATISTICS PROFILE ON;

SET STATISTICS PROFILE OFF;



SET SHOWPLAN_ALL ON;
GO
SELECT * FROM [dbo].DatabaseLog;
GO


SET SHOWPLAN_XML ON;

SET SHOWPLAN_XML OFF;


SET STATISTICS XML ON;

SET STATISTICS XML OFF;


SET SHOWPLAN_XML ON;
SELECT * FROM dbo.DatabaseLog;
SET SHOWPLAN_XML OFF;


SELECT * FROM Person.ContactType;

USE Sample;
CREATE TABLE ClusteredIndexSeekTest
(
    col1    INT IDENTITY(1,1),
    col2 VARCHAR(10),
    CONSTRAINT ClusteredIndexSeekTest_PK PRIMARY KEY CLUSTERED (col1) WITH FILLFACTOR = 100
);
GO

DECLARE @i    INT
SET @i = 1

WHILE @i <= 200
BEGIN
    INSERT INTO dbo.ClusteredIndexSeekTest(col2) VALUES ('Data');
    SET @i = @i + 1
END
GO


SET NOCOUNT ON
GO

SET SHOWPLAN_ALL ON
GO

SELECT * FROM dbo.ClusteredIndexSeekTest
WHERE col1 = 10;
GO

SET SHOWPLAN_ALL OFF
GO

USE AdventureWorks;
SELECT * FROM Sales.Customer;

USE Sample;
GO
CREATE TABLE ComputeScalarTest
(
    EmpID    INT,
    Salary    MONEY,
    Comm    MONEY
);
GO
INSERT INTO ComputeScalarTest VALUES (1, 2000, 0.33);
INSERT INTO ComputeScalarTest VALUES (2, 1000, 0.05);
INSERT INTO ComputeScalarTest VALUES (3, 1500, 0.01);
INSERT INTO ComputeScalarTest VALUES (4, 500, 0);
GO

SET NOCOUNT ON
GO
SET SHOWPLAN_ALL ON
GO

SELECT EmpID, Salary + (Salary * Comm) AS TotSalary
FROM dbo.ComputeScalarTest;
GO

SET NOCOUNT OFF
SET SHOWPLAN_ALL OFF
GO

USE AdventureWorks;
GO

SELECT c.CustomerID
FROM Sales.SalesOrderDetail od
    JOIN Sales.SalesOrderHeader oh
        ON od.SalesOrderID = oh.SalesOrderID
    JOIN Sales.Customer c
        ON oh.CustomerID = c.CustomerID

반응형

'연구개발 > DBA' 카테고리의 다른 글

면접질의  (0) 2012.01.20
데이터베이스 백업과 복원  (0) 2012.01.20
조인 방식 (Join Method)  (0) 2012.01.18
연습문제  (0) 2012.01.16
테이블의 행 구조  (0) 2012.01.16
반응형

조인 방식 (Join Method)

 

MS SQL에서 지원하는 조인 메소드에 대해 알아보자.

 

 

1. 들어가며

MS SQL에서 지원하는 물리적인 조인 방식에는 크게 3가지가 있다.

 

① 중첩반복(Nested Loops)

② 정렬병합(Sort Merge)

③ 해시매치(Hash Match)

 

이중 Nested Loops와 Sort Merge는 어느 DBMS든 가장 전통적인 조인 방식이고 서로간의 단점을 보완하고자 나왔다. Hash Match의 경우는 위의 두 조인 방식의 단점을 보완하고자 나온 방식이다. 그렇다면 Nested Loops와 Sort Merge의 장점, 특징 등을 알아보고 두 조인 방식의 단점이 무엇이길래 Hash Match가 나왔느냐에 대해 알아보는 것이 이번 주제에 대한 수순임이 분명하다.

 

참고:

초보들을 위해 한마디 하자면...

- 논리적인 조인이란 INNER / LEFT / FULL OUTER / 세미조인 등을 말한다.

- 여기서 설명하는 것은 그것말고 물리적인 것을 얘기하는 것이다.



(GUI 실행계획을 보면 네모친 부분을 말한다.)

 

 

2. 방식 비교

(1) 중첩반복(Nested Loops) 조인

① 그래프 실행 계획 아이콘



그래프 실행 계획 아이콘에서 보듯이 반복처리 안의 뭔가를 또 반복처리하고 있다. 유심히 보라. 괜히 만들어진 아이콘은 아니다.

 

② 수행 방식

아래와 같은 쿼리가 있다고 하자.

 

SELECT COL1, COL2
FROM TAB1 A
INNER JOIN
TAB2 B
ON A.KEY = B.KEY
WHERE A.KEY = '111'
 AND A.COL1 LIKE '222%'
 AND B.COL2 = '333'

 

Nested Loops 처리 방식을 그림으로 보면...

 



 

두가지 가정

- 옵티마이저가 통계 데이터를 보고 TAB1을 선행 테이블(먼저 읽히는 테이블)로 선택

- A.KEY, B.KEY에만 인덱스가 잡혀있다. (Non-Clustered INDEX, 클러스터드라면 인덱스 리프 페이지가 데이터 페이지라 별도의 TAB 액세스는 없을 것이다.)

 

1)  인덱스 페이지에서 A.KEY로 111을 찾는다. 이때, 인덱스 페이지가 KEY컬럼으로 정렬되어 있다면 스캔 방식으로 그대로 인덱스 페이지를 읽어나갈 것이고 없으면 스캔 방식으로 한번에 읽는 것이 아무래도 유리하기 때문에 정렬을 한다.

2) 인덱스 페이지에서 찾은 결과로 만약 A.KEY가 넌클러스터드 인덱스라면 RID를 통해 TAB1의 데이터 페이지를 접근하고 클러스터드 인덱스라면 리프(Leaf) 페이지가 데이터 페이지이기 때문에 별도의 데이터 페이지 접근 없음. 여기서 COL1 LIKE '222%'조건으로 한번 더 필터링 된다.

3) TAB1의 결과를 A.KEY = B.KEY 연결고리를 통해 INDEX2를 랜덤 액세스를 한다.

즉, B.KEY = '111'(데이터를 읽었기에 상수값으로 변경)로 랜덤 액세스를 한다. 이때 INDEX2(B.KEY)에 클러스터드 인덱스가 걸려 있으면 실행 계획에는 Clustered Index Seek가 뜬다. (당연한가?^^) 근데, 앞서 예시한 SQL문의 WHERE절에 B.KEY = [검색인자] 가 별도로 명시되어 있지 않음에도 실행 계획에 Clustered Index Seek가 있다는 점!!! 이런 조인 원리 모르는 사람에게는 헉~ 이건 어디서 나오는거지? 하면서 의문을 갖는 점이기도 하다.

4) TAB2에서도 2)와 동일한 방식으로 처리되어 최종 운반단위로 보내진다.

5) 반복

 

 

③ 특징 정리

- 순차적

일련의 어떤 흐름과 같이 첫 테이블 필터링에서부터 두 테이블간의 연결 및 최종 운반단위 산출까지 반복적이며 순차적으로 진행된다.

 

- 선행적

선행 테이블의 처리 범위가 전체 일의 양을 결정한다. 즉, 후행 테이블의 필터링 조건은 선행 테이블에서 나온 결과 ROW를 한번 더 걸러주는 체크 조건 역할을 할뿐 전체 처리량을 좌우 하는게 아니다. 다만, 부분 범위 처리를 할때 후행 테이블의 처리 범위가 넓다면 운반 단위를 빠르게 채울 수 있으므로 더 좋은 성능을 낼 수도 있다.

 

부연설명 하자면, 위의 예시 SQL문에서

... (생략) ...

WHERE A.KEY = '111' -- 요놈과 ...
 AND A.COL1 LIKE '222%' -- 요놈이 전체 일의 양을 결정
 AND B.COL2 = '333' -- 요놈은 최종 결과를 내보내기 전에 체크만 한다. 다만, 부분 범위 처리를 하고자 할 때 요놈이 없으면 빨리 빨리 운반단위를 채울 수 있으므로 더 빠를 수 있다.

 

- 종속적

후행 테이블은 선행 테이블의 결과값을 받아 처리된다. 즉, 선행 테이블의 결과에 종속적이다. 종속적이 되어서 나쁜 점이라고 하면 후행 테이블의 인덱스를 전체 일의 양을 줄여줄 수 있는 필터링 조건으로 사용 못한다는 점(다시 한번 강조하지만, 체크조건으로만 쓰임)이고 좋은 점은 이것을 역으로 전략적으로 이용할 수 있다는 것이다.

이런 경우를 생각해보자.

 



 

수강 테이블 : 과목코드에 Clustered Index

학생 테이블 : 학번에 Clustered Index

학생과 과목 엔터티의 관계는 M:M관계이다. 따라서, 이를 풀기 위해 수강이라는 엔터티가 도입되었다. 이때, 예를 쉽게 하기 위해 과목 엔터티는 일단 무시한다. 필자의 억지스런 예를 위해서다. :)

 

문제 : 자료구조(과목코드 : 000)를 수강한 모든 학생을 찾아라.

조인을 단순화하기 위해 걍 과목코드를 부여했다. 이해하시라. 그렇다면 쿼리는

 

SELECT 학번, 학생명

FROM 수강 A

INNER JOIN

학생 B

ON A.학번 = B.학번

WHERE A.과목코드 = 000

 

와 같이 될 것이다. 주어진 문제에 따르면 학생 테이블은 WHERE절을 통해 필터링 할 인덱스 컬럼이 없다. 바로 이런 경우...수강과 학생 테이블이 M:1 관계긴 하나 한 학생이 동일한 과목을 두번 수강할리는 없기에(혹 재수강? 커헉~ 그런건 없다고 가정 -_-+) 수강 테이블의 ROW수는 전교 학생들이 그 과목을 모두 수강한다해도 학생 테이블보다 똑같으면 똑같았지 클리가 없다. 따라서, 옵티마이저는 수강 테이블을 선행 테이블로 선택하고 학생 테이블을 후행 테이블로 선택한다.(아니라면 걍 그렇다고 치자. -_-;;) 이에, 수강 테이블에서 과목코드 000인 놈을 모두 찾고 같은 ROW에 학번이 있으니 그 학번으로 학생 테이블을 액세스 (즉, B.학번 = A.학번에서 수강 테이블은 읽혀졌으니 상수값으로 변경되어 B.학번 = '111', B.학번 = '222', B.학번 = '333', ... B.학번 = 'NNN' 이런 식으로)하여 Clustered Index Seek를 한다. 멋지군....짝짝짝...

 

 

- 랜덤 액세스

A.KEY = B.KEY로 선행 테이블의 결과를 통해 후행 테이블을 액세스 할때 랜덤 I/O가 발생한다. 선행 테이블은 최초 ROW만 랜덤 액세스가 발생하고 이후에는 스캔 방식으로 진행한다.

 

- 연결고리 중요성

A.KEY = B.KEY에서 보듯이 TAB1의 처리 ROW를 가지고 TAB2의 인덱스 페이지를 액세스하기 때문에 TAB2의 인덱스 유무가 굉장히 중요하다. 만약, TAB2에 인덱스가 없다면 옵티마이저는 TAB2를 후행 테이블로 선택하지 않는다. 다시 말해 TAB2를 선행 테이블로 선택한다. TAB2를 테이블 스캔하여 나온 결과 ROW를 가지고 인덱스가 있는 TAB1를 액세스하는게 성능면에서 유리하기 때문이다. 

 

 

④ 언제 쓰면 좋은가?

- 부분 범위 처리

다른 조인 방법은 부분 범위 처리가 원천적으로 불가능하나 이 조인 방식에서는 부분 범위 처리가 가능하다. 단, MS-SQL에선 정렬을 하면 처리가 유리해진다라는 판단이 서면 서버에서 자동으로 정렬을 수행하여 사용자가 이를 제어하지 못하므로 부분범위 처리가 불가능해 질 수도 있다.

 

- 처리의 방향성이 필요

다른 테이블의 처리 결과를 받아야만 처리 범위를 확 줄여줄 수 있을 때 사용하면 좋다.

 

- 처리량이 적다.

위에서도 계속 언급한 랜덤 I/O 때문에 선행 테이블의 카디널리티를 획기적으로 줄일 수 있다면 나머지는 수학적인 반복 연결이기에 메모리를 가장 적게 사용하는 좋은 조인 방식이 된다.

 

이 조인 방식의 최대 단점은

두 테이블을 연결할 때의 랜덤 I/O가 가장 큰 부담이다.

 

 

 

(2)  정렬병합(Sort Merge) 조인

① 그래프 실행 계획 아이콘



두개의 테이블이 있고 양쪽을 합치는 듯한 표현...잘보면 재미있다.

 

② 수행 방식

Nested Loops에서 써먹은 쿼리를 그대로 사용하자.

 

SELECT COL1, COL2
FROM TAB1 A
INNER JOIN
TAB2 B
ON A.KEY = B.KEY
WHERE A.KEY = '111'

 AND A.COL1 LIKE '222%'
 AND B.COL2 = '333'

 

Sort Merge 처리 방식을 그림으로 보면...

 



가정

- A.KEY, B.KEY에만 인덱스가 잡혀있다.

 

1) INDEX1에서 A.KEY가 111인 것들을 찾은 후 A.COL1 LIKE '222%'조건으로 최종 필터링된 결과행을 연결고리인 A.KEY의 값으로 정렬해둔다.

 

2) TAB2는 인덱스를 사용 할 수 없으므로(B.KEY로 검색하는게 없음.) TAB2를 테이블 스캔하여 B.COL2 = '333'가 만족하는 행을 연결고리인 B.KEY의 값으로 정렬해둔다. 여기서 알아둘 사항은 1)이 일어난 후 2)가 일어나는게 아니라 1)2)는 동시에 일어난다.

 

3) 두 개의 정렬된 결과를 가지고 A.KEY = B.KEY를 만족하는 결과를 병합(Merge)을 하면서 운반단위로 보내진다. 여기서 병합을 한다는건 양쪽 값을 스캔 방식으로 비교를 해나가다가 어느 한쪽의 값이 커지면 멈추고 커진 값을 다른 쪽 값을 비교하면서 다시 내려가는 것을 말한다. 그러다가 어느 한쪽이라도 EOF(ROW의 끝?)를 만나면 전체 처리 과정이 종료된다.

 

 

③ 특징 정리

- 동시적

Nested Loops 조인에서는 한쪽 테이블이 읽혀져야(선행 테이블) 후행 테이블을 액세스 할 수 있었다. 즉, 순차적으로 액세스 되었다.  하지만, 이 조인 방식은 양쪽 테이블을 동시에 읽고 양쪽 테이블이 조인할 준비가 되었을 때 조인을 시작한다. 즉, 어느 한쪽의 테이블의 처리가 늦어지면 다른 한쪽은 대기해야 한다.

 

- 독립적

Nested Loops 조인에서는 선행 테이블의 처리 결과 ROW가 후행 테이블의 연결고리의 상수값으로 사용되었다. (즉, B.KEY = '111'<- A.KEY가 읽혀져 상수값으로 변경됨) 하지만, 이 조인에서는 처리범위를 줄일 수 있는 거의 유일한 수단은 각자가 가지고 있는 필터링 조건이다. 서로의 테이블이 어떻게 필터링 됐는가는 별 관심이 없다.

 

- 전체 범위 처리

정렬 작업 완료 후에 조인이 일어나므로 부분 범위 처리가 되지 않는다.

 

- 연결고리

정렬된 양쪽 결과를 스캔하는 방식으로 조인이 일어나므로 연결고리는 크게 중요하지 않다. 그냥 사용하지 않는다고 보면 된다.

 

- 체크 조건의 의미

Nested Loops의 후행 테이블의 필터링 조건은 단지 선행 테이블에서 처리된 내용을 최종 운반 단위로 보낼때 그 양을 줄여주는 역할만 할뿐 전체 처리량을 줄이지는 못한다. 즉, 선행 테이블의 처리 결과 ROW수가 10건이었다하면 이 10건을 운반단위로 보내기 전에 후행 테이블의 필터링 조건을 확인하여 조건을 만족하는지 체크만 할 뿐이다. 모두가 다 만족하면 10건이 보내지겠고 만족하지 못하는 ROW가 있으면 제외가 된다. 근데, Sort Merge의 경우 인덱스를 사용하지 않는 단순 체크 조건이라도 머지할 범위를 줄여주기 때문에 상당한 의미가 있다.

 

 

④ 언제 쓰면 좋은가?

- 처리량이 많을때

Nested Loops는 처리량이 많아지면 랜덤 액세스에 대한 부담이 극심해진다. 이때 스캔방식으로 조인되는 Sort Merge를 사용하면 성능상의 이점이 있다.

 

- 연결고리 상태 이상

Nested Loops는 연결고리의 상태가 굉장히 중요하다고 했다. 따라서, 한쪽의 연결고리에 이상이 발생하면 Nested Loops는 심히 고려해봐야 한다. 이때, 연결고리에 영향을 받지 않는 Sort Merge를 쓰면 좋다.

 

 

이 방식의 최대 단점은

정렬에 따른 부담이다.

정렬은 tempdb를 사용한다는 사실은 누구나 알고 있다. 정렬할 양이 극도로 많아 tempdb의 임계치를 넘어버린 경우에는 tempdb에 페이지 할당이 발생하여 순간 전체 데이터베이스에 페이지 잠금이 발생하는 등 DB성능에 심각한 영향을 줄 수도 있다.

물론, 가공없이 Clustered Index를 그대로 사용하게 되면 정렬은 안해도 되니 이때만큼은 정렬의 부담에서 해방된다. 근데, Group By니 Distinct 라든지 이미 정렬된 ROW를 2차 가공하게 되면 이땐 방법이 없다. 무조건 정렬이다.

 

참고 :

MS-SQL에서는 양쪽 테이블에서 필터링되어 나온 값이 각 테이블에서 유니크(Unique) 할때만 이 조인 방식을 사용하려는 경향이 있다는 것을 기억하자. 연결고리로 사용할 키값에 중복이 심하면 잘 선택하지 않으려 한다.

 

 

두 조인의 단점

- Nested Loops : 랜덤 액세스

- Sort Merge : 정렬 (메모리 사용 증가)

 

이렇다는걸 확인 할 수 있다. 그렇다면 Hash Metch가 나온 배경이 어느 정도 짐작이 가지 싶다.

 

 

 

(3) 해시매치(Hash Match) 조인

① 그래프 실행 계획 아이콘



아이콘에서 보듯이 어떤 특정한 값으로 액세스하는 모양이다.

 

Nested Loops가 랜덤 액세스에 대한 부담이 있더라도 적은 양(대용량이라도 선행 테이블의 카디널리티를 획기적으로 줄일수만 있다면)의 데이터를 처리하기에는 이만한 조인은 없다. Sort Merge는 정렬의 부담은 있지만 연결고리에 이상이 있는 경우의 대용량을 처리하기에는 괜찮은 조인 방식이다. 문제는 정렬 자체에 있다기보다 정렬해야 할 양이 너무 많아져 tempdb의 용량을 넘어서 별도의 페이지 할당이 일어나는데 있다. 페이지 할당이 일어나면 할당 잠금이 발생하여 경합의 정도에 따라 서버가 응답하지 않을 수도 있다.

 

이에 해시 조인은 이 둘의 단점에 대한 대안이 될 수 있다.

 

② 개념 및 수행 방식

이 글을 쓰면서도 해시 조인에 대한 내용을 온라인 북과 웹(테크넷, 구글 등), 국내/국외 서적에서 찾아보았는데 정말 내용이 추상적이기만 하고 잘 설명된 서적이나 싸이트가 없었다. 물론, 위에 Nested Loops나 Sort Merge도 내용이 잘 나와 있는건 아니다. 단지, Nested Loops나 Sort Merge는 전통적인 방식이라 어느 DBMS든 비슷한 방식으로 동작 하기에 잘 설명된 내용을 기초로 적긴 했는데 Hash Match는 좀 다르단다. 요즘에도 그런지 모르나 DB2는 이 Hash Join의 개념이 없다고 하니 Hash와 관련된 상세한 원리는 Re-command를 하질 않는다나? 이건 뭔지...(이거 루머일수도 있다 주의)

 

일단 걍 MSSQL 온라인 북에 있는 내용을 살펴보도록 하자. 물론, 봐도 이해는 안가지 싶다.





 

해시 조인에는 빌드 입력과 검색 입력 등 두 가지 입력이 있습니다. 쿼리 최적화 프로그램은 두 가지 입력 중 작은 쪽이 빌드 입력이 될 수 있도록 이러한 역할을 할당합니다.

해시 조인은 여러 가지 유형의 집합 일치 연산, 즉 내부 조인, 왼쪽, 오른쪽, 완전 외부 조인, 왼쪽 및 오른쪽 세미 조인, 교집합, 합집합, 차집합 등에 사용합니다. 또한, 해시 조인의 변형은 중복 요소 제거 및 그룹화(예: SUM(salary) GROUP BY department)를 수행할 수 있습니다. 이러한 수정에서는 빌드 및 검색 역할 모두에 대해 한 개의 입력만 사용합니다.

다음 섹션에서는 인-메모리 해시 조인, 유예 해시 조인 및 재귀 해시 조인 등 여러 해시 조인 유형을 설명합니다.

해시 조인은 먼저 전체 빌드 입력을 스캔하거나 계산한 다음 해시 테이블을 메모리에 작성합니다. 해시 키에 대해 계산된 해시 값에 따라 각 행이 해시 버킷에 삽입됩니다. 전체 빌드 입력이 사용 가능한 메모리보다 작으면 모든 행을 해시 테이블에 삽입할 수 있습니다. 이 빌드 단계 다음으로는 검색 단계가 이어집니다. 전체 검색 입력은 한 번에 한 행씩 스캔 또는 계산되며, 각 검색 행에 대해 해시 키 값이 계산되고 해당 해시 버킷이 스캔되며 일치하는 항목이 생성됩니다.

빌드 입력이 메모리 크기에 맞지 않으면 해시 조인은 몇 개의 단계로 진행됩니다. 이것을 유예 해시 조인이라고 합니다. 각 단계마다 빌드 단계와 검색 단계가 있습니다. 처음에는 전체 빌드 및 검색 입력이 사용되며 해시 키에 대한 해시 함수를 사용하여 여러 파일로 분할됩니다. 해시 키에 대한 해시 함수를 사용하면 2개의 조인 레코드가 모두 동일한 파일 쌍에 있는 것이 보장됩니다. 따라서 2개의 큰 입력을 조인하는 작업이 동일한 작업의 여러 개의 작은 인스턴스로 축소되었습니다. 그런 다음 해시 조인은 분할된 파일의 각 쌍에 적용됩니다.

빌드 입력이 너무 커서 표준 외부 병합에 대한 입력에 여러 개의 병합 수준이 필요한 경우에는 여러 개의 분할 단계와 여러 개의 분할 수준이 요구됩니다. 일부 파티션만 큰 경우에는 해당 파티션에서만 추가 분할 단계가 사용됩니다. 모든 분할 단계를 가능한 한 빠르게 유지하기 위해서는 단일 스레드가 여러 개의 디스크 드라이브를 사용 중인 상태로 유지할 수 있도록 대형의 비동기 I/O 작업이 사용됩니다.

참고:
빌드 입력이 사용 가능한 메모리보다 조금밖에 크지 않다면 인-메모리 해시 조인과 유예 해시 조인의 요소가 단일 단계에서 결합되어 하이브리드 해시 조인이 생성됩니다.

최적화 중에 사용될 해시 조인을 확인하는 것이 항상 가능한 것은 아닙니다. 따라서 SQL Server 는 빌드 입력의 크기에 따라 인-메모리 해시 조인을 사용하여 시작된 후 유예 해시 조인, 재귀 해시 조인으로 점차 전환됩니다.

2개의 입력 중 빌드 입력이 되어야 하는 작은 쪽을 최적화 프로그램이 잘못 예측하는 경우에는 빌드 및 검색 역할이 동적으로 바뀝니다. 해시 조인은 작은 쪽의 오버플로 파일을 빌드 입력으로 사용하게 합니다. 이 기술을 역할 반전이라고 합니다. 역할 반전은 하나 이상의 해시 조인이 디스크에 "spill"된 경우 해시 조인 내에서 발생합니다.





③ 특징 정리 

- 연결고리

각 테이블의 연결고리에 있는 인덱스는 사용하지 않는다. 대신 실시간(?) 인덱스가 생성되어 그것을 통해 조인을 한다. 물론, 조인이 종료되면 삭제된다.

 

- 조인 결과

조인의 결과는 정렬되지 않은 상태로 출력된다. 그래서, 특정 컬럼으로 정렬을 하고 싶다면 ORDER BY절을 이용해야 한다.

 

- 랜덤 액세스

랜덤 액세스가 있으나 Nested Loops와는 달리 빠른 랜덤 액세스란다. (뭔 말인지...)

 

- 메모리 사용

해시 버켓을 만들기 위해 메모리를 꽤 사용한다.

 

④ 언제 쓰면 좋은가?

- 연결고리에 이상이 있거나 연결고리의 인덱스를 사용하지 못할때

Sort Merge처럼 연결고리에 인덱스가 없어도 조인을 하는데 문제가 없다.

 

- 소량과 대용량 테이블을 조인 할때

이 경우가 가장 좋은 성능을 낸다. 소량과 소량을 연결하는데는 차라리 Nested Loops를 쓰는게 좋다. 토깽이를 잡는데 소잡는 칼 쓸 수 없지 않은가...

 

 

 

3. 마치며...

Hash는 상세하게 나온게 없어서 마지막이 흐지부지 됐다. (내가 원래 그럼...-_-;;) 혹시, Hash Join에 관해 상세하게 나와 있는 곳 아시는 분 링크 좀....그리고, 잘못된 점 있으면 지적 바랍니다.

반응형

'연구개발 > DBA' 카테고리의 다른 글

데이터베이스 백업과 복원  (0) 2012.01.20
실행계획  (0) 2012.01.20
연습문제  (0) 2012.01.16
테이블의 행 구조  (0) 2012.01.16
trace blackbox  (0) 2012.01.15
반응형

1번. 아래의 실행계획을 보고.. 어떻게 튜닝 할 것인지 적어주세요.


1번 답 :

좋은 예제가 있어 인용합니다. 이런 저런 상황에 따라 적용방법이 틀려지겠지만

대체적으로 이런 상황에서는 커버드 인덱스를 이용하는 것이 제일 효율적인 것 같네요.

USE Northwind;
GO

sp_helpindex OrdersTest


SET STATISTICS PROFILE ON
SET STATISTICS PROFILE OFF

DROP INDEX OrdersTest.Orders_x01
GO
DROP INDEX OrdersTest.Orders_x02
GO

SELECT CustomerID, ShippedDate, Freight
FROM OrdersTest
WHERE CustomerID LIKE 'BO%'


StmtText
SELECT CustomerID, ShippedDate, Freight
FROM OrdersTest
WHERE CustomerID LIKE 'BO%'
  |--Table Scan(OBJECT:([Northwind].[dbo].[OrdersTest]), WHERE:([Northwind].[dbo].[OrdersTest].[CustomerID] like N'BO%'))


CREATE CLUSTERED INDEX Orders_x01 ON OrdersTest (EmployeeID, ShippedDate)
GO
CREATE NONCLUSTERED INDEX Orders_x02 ON OrdersTest (CustomerID)
GO

SELECT CustomerID, ShippedDate, Freight
FROM OrdersTest WITH (INDEX(Orders_x02))
WHERE CustomerID LIKE 'BO%'
GO

StmtText
SELECT CustomerID, ShippedDate, Freight
FROM OrdersTest WITH (INDEX(Orders_x02))
WHERE CustomerID LIKE 'BO%'
  |--Nested Loops(Inner Join, OUTER REFERENCES:([Uniq1002], [Northwind].[dbo].[OrdersTest].[EmployeeID], [Northwind].[dbo].[OrdersTest].[ShippedDate], [Expr1004]) WITH UNORDERED PREFETCH)
       |--Index Seek(OBJECT:([Northwind].[dbo].[OrdersTest].[Orders_x02]), SEEK:([Northwind].[dbo].[OrdersTest].[CustomerID] >= N'BO' AND [Northwind].[dbo].[OrdersTest].[CustomerID] < N'BP'),  WHERE:([Northwind].[dbo].[OrdersTest].[CustomerID] like N'BO%') ORDERED FORWARD)
       |--Clustered Index Seek(OBJECT:([Northwind].[dbo].[OrdersTest].[Orders_x01]), SEEK:([Northwind].[dbo].[OrdersTest].[EmployeeID]=[Northwind].[dbo].[OrdersTest].[EmployeeID] AND [Northwind].[dbo].[OrdersTest].[ShippedDate]=[Northwind].[dbo].[OrdersTest].[ShippedDate] AND [Uniq1002]=[Uniq1002]) LOOKUP ORDERED FORWARD)

EXEC sp_helpindex OrdersTest
--index_name    index_description                                index_keys
--Orders_x01    nonclustered located on PRIMARY    CustomerID
--Orders_x02    clustered located on PRIMARY    EmployeeID, ShippedDate

DROP INDEX OrdersTest.Orders_x02
GO

--커버드 인덱스 적용
CREATE NONCLUSTERED INDEX Orders_x02 ON OrdersTest (CustomerID) INCLUDE (Freight);
GO

SELECT CustomerID, ShippedDate, Freight
FROM OrdersTest WITH (INDEX(Orders_x02))
WHERE CustomerID LIKE 'BO%'
GO


StmtText
SELECT CustomerID, ShippedDate, Freight
FROM OrdersTest WITH (INDEX(Orders_x02))
WHERE CustomerID LIKE 'BO%'
  |--Index Seek(OBJECT:([Northwind].[dbo].[OrdersTest].[Orders_x02]), SEEK:([Northwind].[dbo].[OrdersTest].[CustomerID] >= N'BO' AND [Northwind].[dbo].[OrdersTest].[CustomerID] < N'BP'),  WHERE:([Northwind].[dbo].[OrdersTest].[CustomerID] like N'BO%') ORDERED FORWARD)


보내온 답 :

1번 문제는.. Key Lookup을 줄이는 방법입니다.

물론 Key Lookup을 하기 때문에.. 테이블내에 Clustered index가 있는 상태입니다.

만약, Clustered index가 없다면.. Key Lookup이 아닌.. RID Lookup을 하게 됩니다.

 

따라서 이를 해소하기 위해.. ncidx_tb_good_7이라는 인덱스에... key lookup을 하는 컬럼을 추가해서..

covered 또는 include index 형태로 구성하는 튜닝 방법이 정답이였습니다.


2번. 커버드 인덱스와 인클루드 인덱스의 차이점에 대해서 설명하시오.

2번 답 :

커버드 인덱스나 인클루드 인덱스는 테이블이나 클러스터형 인덱스에 액세스하지 않고 필요한 열 데이터를 인덱스 내에서 모두 찾을 수 있으므로 성능 향상에 도움이 됨.

단점은 포괄 열이 너무 많으면 인덱스 유지 관리 작업이 많아져 기본 테이블이나 인덱싱된 뷰에 대한 삽입, 업데이트 또는 삭제 작업에 필요한 시간이 늘어남


차이점은 작은 열에 대해 포함한 것은 일반적으로 커버드 인덱스라고 하며

INCLUDE INDEX는 키 열의 크기가 최대값인 900바이트를 초과하는 열을 인덱싱하는 것을 의미함.


참고문언 : http://technet.microsoft.com/ko-kr/library/ms189607%28SQL.100%29.aspx


보내온답 :

1번을 정확이 아시는 분은 2번도 쉽게 답하실 수 있습니다.

 

두개 모두.. 넌클러스터드 인덱스를 어떻게 활용했냐?에 따라서..두가지 형태로 갈리게 됩니다.

select a,b from 테이블 where a=10  일때..

a,b가 하나의 넌 클러스터드 인덱스에 걸려있는 상태라면... key 또는 rid lookup을 하지 않아도 되니..보통 커버링이 되었다고 표현을 합니다.(1번 튜닝 방법)

 

 

예제를 하나 봅시다.

a    b

1    3

1    4

1    7

 

 

a=1,b=6 이 새로 insert 되려고 한다면... 커버링 같은 경우는... 중간에 반드시 낑겨 들어가야 합니다.

왜냐면.. 두개의 컬럼 모두 인덱스 컬럼이기 때문에 그렇죠~!

a    b

1    3

1    4

1    6

1    7

 

이렇게 되겠죠... 이렇게 되면서... 페이지 스플릿이 날 수도 있고.. 그로 인해.. 그 넌 리프레벨에 있는 페이지도 UPDATE 가 될 문제가 발생 할 수 있습니다.

 

 

하지만.. 인클루드 인덱스를 살펴보면.. 

a만 인덱스가 선언되있고.. b include 컬럼이면.. b의 정렬따위 신경을 안쓰기 때문에.

a    b

1    3

1    4

1    7

1    6 <-- 이런 형태로 insert 가 됩니다.

 

또한 중요한 점은 인덱스 제한 한계인 900바이트 이상인 컬럼도 지정이 가능합니다.

 

가량 예를 들면...게시판 내용검색을 한다고 가정합시다...

이건.. 무조건 %검색어%   <-- 이런 형태기 때문에 인덱스를 이용할 수 없습니다.

무조건 Clustered index Scan이 떨어지게 되죠..

 

근데... 인클루드 인덱스는.. varchar(max) include 컬럼에 포함될 수 있습니다...

이렇게 게시물 번호, 내용 두개의 컬럼을 넌클러스터드 인클루드 인덱스로 잡아둔다면..

index Scan을 하기 때문에.. Clustered index Scan을 하는 것보다 훨씬 효율적이겠죠 ^^

 

 

결국.. 인클루드 인덱스는........... 테이블에 반!!시 한개만 존재하는 클러스터디 인덱스의 단점을 극복하기 위해..

 

테이블내에 두개이상의 클러스터드 인덱스을 만드는 방법! 으로 이해하시는게 빠릅니다.

 

사실 2번이 가장 어려운 문제였습니다 ~~

 

여기 글도 한번 참고해보시면 좋겠습니다 ^^

http://www.sqler.com/326192



3번. 

주어진 결과 쿼리


 

결과 값(결과는 30일까지 출력이 되나.. 총 7월 31일까지 출력되야 합니다.)

 

주어진 쿼리

SELECT

  CONVERT(SMALLDATETIME,'2011-07-01') AS CreateDate

 , 3 AS TotalCount

 UNION ALL

SELECT

  CONVERT(SMALLDATETIME,'2011-07-05') AS CreateDate

 , 10 AS TotalCount

 UNION ALL

SELECT

  CONVERT(SMALLDATETIME,'2011-07-11') AS CreateDate

 , 3 AS TotalCount

 UNION ALL

SELECT

  CONVERT(SMALLDATETIME,'2011-07-02') AS CreateDate

 , 20 AS TotalCount

 UNION ALL

SELECT

  CONVERT(SMALLDATETIME,'2011-07-18') AS CreateDate

 , 11 AS TotalCount



3번 답 : 

SELECT x.CreateDate, ISNULL(y.TotalCount, 0) as TotalCount
FROM (
    SELECT DATEADD(DAY, number, '2011-07-01') as CreateDate
    FROM [master].[dbo].[spt_values]
    WHERE type = 'p'
    AND number <= DATEDIFF(DAY, '2011-07-01', '2011-07-31')) x LEFT OUTER JOIN
    (
        SELECT
          CONVERT(SMALLDATETIME,'2011-07-01') AS CreateDate
         , 3 AS TotalCount
         UNION ALL
        SELECT
          CONVERT(SMALLDATETIME,'2011-07-05') AS CreateDate
         , 10 AS TotalCount
         UNION ALL
        SELECT
          CONVERT(SMALLDATETIME,'2011-07-11') AS CreateDate
         , 3 AS TotalCount
         UNION ALL
        SELECT
          CONVERT(SMALLDATETIME,'2011-07-02') AS CreateDate
         , 20 AS TotalCount
         UNION ALL
        SELECT
          CONVERT(SMALLDATETIME,'2011-07-18') AS CreateDate
         , 11 AS TotalCount
    ) y
    ON x.CreateDate = y.CreateDate


보내온답 :

3번은 주어진 쿼리를 통해... 결과처럼 화면에 나오는 화면인데.. 대부분 모두 맞추셨습니다.

문제의 핵심은.... 이럴때 LEFT JOIN으로 풀어야 하는지를 알고 있으십니까?

 

이거 였습니다.

 

 

--//아래 쿼리는 ROW_NUMBER 때문에.. SQL Server 2005 이상부터 수행됩니다.

SELECT

             DEF.CreateDate

,            ISNULL(DAT.TotalCount,0)

FROM (SELECT TOP 31 DATEADD(DAY,ROW_NUMBER() OVER(ORDER BY OBJECT_ID),'2011-06-30') AS CreateDate from sys.objects) DEF

LEFT OUTER JOIN

(

             SELECT

  CONVERT(SMALLDATETIME,'2011-07-01') AS CreateDate

 , 3 AS TotalCount

 UNION ALL

 SELECT

  CONVERT(SMALLDATETIME,'2011-07-05') AS CreateDate

 , 10 AS TotalCount

 UNION ALL

 SELECT

  CONVERT(SMALLDATETIME,'2011-07-11') AS CreateDate

 , 3 AS TotalCount

 UNION ALL

 SELECT

  CONVERT(SMALLDATETIME,'2011-07-02') AS CreateDate

 , 20 AS TotalCount

 UNION ALL

 SELECT

  CONVERT(SMALLDATETIME,'2011-07-18') AS CreateDate

 , 11 AS TotalCount

) DAT ON DEF.CreateDate = DAT.CreateDate

ORDER BY DEF.CreateDate



반응형

'연구개발 > DBA' 카테고리의 다른 글

실행계획  (0) 2012.01.20
조인 방식 (Join Method)  (0) 2012.01.18
테이블의 행 구조  (0) 2012.01.16
trace blackbox  (0) 2012.01.15
테이블 변수 vs 임시 테이블  (0) 2012.01.09
반응형
/*
테이블의 행 구조
Status Bit A / B (1 + 1 byte)
고정 컬럼 길이 (2)
고정길이 데이터(n)
컬럼 수(2)
null bitmap (컬럼마다 1bit)

가변 컬럼 수(2)
컬럼 오프셋(2 * 가변길이)
가변 컬럼 데이터(n)


Header        Fixed Data        컬럼수        NB        VB        Variable Data
                                                        Null
                                                        Block
                                                                    Variable
                                                                    Block
*/


/* 고정 컬럼 테이블 */
USE tempdb;
GO
IF OBJECT_ID('Fixed') IS NOT NULL
    DROP TABLE Fixed
GO
CREATE TABLE Fixed
(
    Col1        CHAR(5)    NOT NULL
    ,Col2    INT            NOT NULL
    ,Col3    CHAR(3)    NULL
    ,Col4    CHAR(6)    NOT NULL
    ,Col5    FLOAT        NOT NULL
)

SELECT * FROM sysindexes
WHERE id = OBJECT_ID('fixed');
--0x790000000100
SELECT * FROM syscolumns
WHERE id = OBJECT_ID('fixed');

INSERT Fixed VALUES ('abcde', 1, NULL, 'aaaa', 12.3);

SELECT * FROM Fixed;

SELECT CONVERT(INT, 0x79);
--121
DBCC TRACEON(3604);
DBCC PAGE(tempdb, 1, 121, 3);
DBCC TRACEOFF(3604);

--0000000000000000:   10001e00 61626364 65    010000 00    000000 †....abcde.......
--                                    4바이트 헤더  a  b c   d   e       Col2(4바이트)   NULL(3바이트)
--0000000000000010:   61616161 2020        9a99 99999999 2840    0500 †aaaa  ......(@..
--                                     a  a  a  a  빈칸두개            Col5                        컬럼 5개
--0000000000000020:   04†††††††††††††††††††††††††††††††††††.               
--                                    NULL이 몇번째 컬럼에 있느냐 - 00100(아래 참고)
    --Col1        CHAR(5)    NOT NULL  (0)
    --,Col2    INT            NOT NULL         (0)
    --,Col3    CHAR(3)    NULL                 (1)
    --,Col4    CHAR(6)    NOT NULL         (0)
    --,Col5    FLOAT        NOT NULL         (0)
--00100 =  2*0(4) + 2*0(3) + 2*2(2) + 2*0(1) + 2*0(0)


/* 가변컬럼 테이블 */
USE tempdb;
GO
IF OBJECT_ID('variable') IS NOT NULL
    DROP TABLE variable
GO
CREATE TABLE variable
(
    Col1        CHAR(3)            NOT NULL
    ,Col2    VARCHAR(250)    NOT NULL
    ,Col3    VARCHAR(5)        NULL
    ,Col4    VARCHAR(20)    NOT NULL
    ,Col5    SMALLINT            NOT NULL
);
GO

SELECT * FROM sysindexes
WHERE id = OBJECT_ID('variable');
--0x7F0000000100
SELECT * FROM syscolumns
WHERE id = OBJECT_ID('variable');

INSERT variable VALUES ('abc', replicate('x', 250), NULL, 'abc', 123);

SELECT * FROM variable;
GO

DBCC TRACEON(3604);
DBCC PAGE(tempdb, 1, 127, 3);
SELECT CONVERT(INT, 0x7F);
DBCC TRACEOFF(3604);
                                                                                                --가변컬럼이 3개(2바이트)
                                                                            -- 컬럼개수 5개(2바이트)
--0000000000000000:   30000900 616263        7b 00    0500    04 0300    0e01    †0.    .abc{........
                                                                                                                -- Col2주소
--                                    헤더4바이트    a  b   c    smallint2바이트     널비트 1바이트(00100)
--0000000000000010:   0e01        1101       78787878 78787878 78787878 †....xxxxxxxxxxxx
                                -- Col3주소     Col4주소        --값
--                0e01위에 있는 주소와 0e01 밑에 있는 주소가 같은 이유는 가변컬럼이기때문에 이 주소에서 시작한다.
--                왜냐면 Col3가 null값이기에 값이 없으므로 같은 주소에서 시작하는 것.
--0000000000000020:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000030:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000040:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000050:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000060:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000070:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000080:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000090:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--00000000000000A0:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--00000000000000B0:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--00000000000000C0:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--00000000000000D0:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--00000000000000E0:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--00000000000000F0:   78787878 78787878 78787878 78787878 †xxxxxxxxxxxxxxxx
--0000000000000100:   78787878 78787878 78787878 78786162 †xxxxxxxxxxxxxxab
--                                                                                                a  b
--0000000000000110:   63†††††††††††††††††††††††††††††††††††c               
--                                      c


반응형

'연구개발 > DBA' 카테고리의 다른 글

조인 방식 (Join Method)  (0) 2012.01.18
연습문제  (0) 2012.01.16
trace blackbox  (0) 2012.01.15
테이블 변수 vs 임시 테이블  (0) 2012.01.09
Admin::Started with SQL Server (서비스시작) 추적플래그 trace  (0) 2012.01.08

+ Recent posts

반응형