반응형

본 포스트는 Microsoft MVP인 Andrew Kelly의 블로그에서 제기된 이슈를 참고하여 작성하였다.

그리고, 예제는 MS SQL Server 2005의 예제 데이터베이스인 Adventureworks의 sales.salesorderdetail 테이블을 사용하여 작성되었다.

 

여기서 소개할 내용은 MS SQL Server에 국한된 것만은 아니다. 모든 RDBMS에서 공통되며, 또 증명할 수 있는 내용이다. 이미 많은 책이나 블로그 등에서 다루어진 내용이긴 하지만 많은 개발자들이 실수하는 부분 중 하나인 COUNT(*) 잘못된 사용에 대해서 이야기하도록 하겠다.

 

 

COUNT Vs EXISTS

개발하다가 보면 아래와 같은 프로그램을 자주 접하게 될 것이다.

데이터 존재 여부를 체크하고, 그 체크에 의해 로직이 분기하는 내용이다.

만일 해당 상품번호에 대한 주문 이력이 있다면

     어떤 처리 ...

그렇지 않다면

     다른 처리 ...

 

이와 같은 프로그램을 구현할 때, 블로그 주인장은 아직도 많은 개발자들이 아래와 같은 실수를 범하는 것을 종종 목격할 수 있었다.

IF ( SELECT COUNT(*) FROM sales.salesorderdetail WHERE ProductID = 897 ) > 0

   Print 'Yes' ...

ELSE

   Print 'No' ...

 

단지, 해당 상품의 주문 이력이 있는지 없는지 체크하는 것 뿐임에도 불구하고, 상품주문이력 테이블에서 해당 상품의 주문이력을  전부 COUNT하고 있다. 만일 상품번호가 897의 주문이력이 수십만 혹은 수백만 이상이 된다면 어떤 결과를 초래할까?

생각만 해도 끔찍한 일이 아닐 수 없다.

 

이런 경우에는 COUNT()가 아닌 EXISTS를 사용해야 한다. 훨씬 좋은 성능을 얻을 수 있다.

 

그럼 COUNT()를 사용할 때와 EXISTS를 사용할 때와는 어떤 차이가 있는 것일까?

먼저 아래의 SQL을 살펴보자.

첫번째 IF문에서는 COUNT()를, 두번째 IF문에서는 EXISTS를 사용하여 해당 상품에 대한 주문 이력이 있는지를 체크하고 있다.

해당 테이블에는 상품번호 = 897의 데이터가 2건이 존재하고 있다. 

※ 본 예제 및 결과는 Andrew Kelly의 블로그에서 가져온 내용이다.

 

USE AdventureWorks
GO

 

SET STATISTICS IO ON

GO

 

IF (SELECT COUNT(*) FROM sales.salesorderdetail WHERE ProductID = 897 ) > 0

    Print 'Yes'

 

IF EXISTS (SELECT * FROM sales.salesorderdetail WHERE ProductID = 897)
    Print 'Yes'

 

SET STATISTICS IO OFF

GO

 

Result >>

Table 'SalesOrderDetail'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'SalesOrderDetail'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

위의 결과에서 확인할 수 있듯이 체크부분에서 반환되는 행 수가 적을 경우는 IO의 차이는 크게 느낄 수가 없다.

하지만 체크 부분에서 반환되는 행 수가 많다면 이야기는 달라진다.

 

아래의 예를 살펴보자.

해당 테이블에는 상품번호 = 870의 데이터가 4688건 존재하고 있다. 

※ 본 예제 및 결과는 Andrew Kelly의 블로그에서 가져온 내용이다.

 

USE AdventureWorks
GO

 

SET STATISTICS IO ON

GO

 

IF (SELECT COUNT(*) FROM sales.salesorderdetail WHERE ProductID = 897 ) > 0

    Print 'Yes'

IF EXISTS (SELECT * FROM sales.salesorderdetail WHERE ProductID = 897)
    Print 'Yes'

 

SET STATISTICS IO OFF

GO

 

Result >>

Table 'SalesOrderDetail'. Scan count 1, logical reads 11, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'SalesOrderDetail'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

결과에서 확인할 수 있듯이, Logical read IO가 5배의 차이가 발생하였다.

 

다음 예에서는 인덱스 검색을 하지 않았을 때의 성능 차이를 보여준다.

이 테이블의 전체 테이터 수는 121,317건이며 357건의 데이터가 검색된다.

이번에는 250배의 IO 성능차이를 보임을 확인 할 수 있다.

※ 본 예제 및 결과는 Andrew Kelly의 블로그에서 가져온 내용이다.

 

USE AdventureWorks
GO

 

SET STATISTICS IO ON

GO

 

IF (SELECT COUNT(*) FROM sales.salesorderdetail WHERE ModifiedDate = '20010701 00:00:00.000' ) > 0

    Print 'Yes'

IF EXISTS (SELECT * FROM sales.salesorderdetail WHERE ModifiedDate = '20010701 00:00:00.000')
    Print 'Yes'

 

SET STATISTICS IO OFF

GO

 

Result >>

Table 'SalesOrderDetail'. Scan count 1, logical reads 1241, physical reads 0, read-ahead reads 331, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'SalesOrderDetail'. Scan count 1, logical reads 5, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

BOL에 "하위 쿼리에 행이 있으면 TRUE를 반환한다." 라는 부분이 있다. 

이 말에서 짐작할 수 있듯이, EXISTS 문에서는 하위 쿼리의 결과 집합이 한 건이든 수 천만 건이든 간에 만족하는 결과가 발견되면 그 즉시 TRUE를 반환하고 스캔을 중지하게 된다. 따라서 전체를 검색하는 COUNT(*) 보다 성능 면에서 우수한 것은 당연할 것이다.

반응형

+ Recent posts