<범위 조건을 같다 조건으로 바꿔주는 기준일자 테이블>

 

이어지는 이야기 .03’에서 기준일자 테이블에 대해 설명했다. 이번에는 기준일자 테이블을 활용해 성능을 개선하는 경우를 살펴보자.

 


SQL BOOSTER 에 이어지는 이야기들입니다.~!
SQL BOOSTER 를 보신 분들께 좀 더 도움을 드리고자 추가로 작성한 내용입니다.

www.aladin.co.kr/shop/wproduct.aspx?ItemId=216383877

 

SQL BOOSTER

프로젝트 성공을 위한 SQL 필독서. 프로젝트를 진행하는 순서처럼 구성되어 있다. 프로젝트 투입을 위해 필요한 SQL 기술을 설명하고, 성능 테스트를 위해 필요한 기술을 설명한 뒤에 마지막으로

www.aladin.co.kr

 

설명의 편의상 반말체로 작성한 점 양해바랍니다.  pdf 파일도 첨부드리니 다운 받아 보셔도 됩니다.

SQL_Booster_이어지는이야기05.pdf
0.27MB

 

복합인덱스를 이용해 데이터를 찾을 때, 선두 컬럼이 같다(=) 조건으로 사용되어야 뒤쪽 컬럼도 데이터 검색에 효율적으로 사용될 수 있다. (이 내용은 SQL BOOSTER‘6.3.1. 복합 인덱스 컬럼 선정과 순서#1’에 설명되어 있다.) 바꿔 말하면, 복합 인덱스의 선두 컬럼에는 범위 조건이, 뒤쪽 컬럼에는 같다(=) 조건이 사용된 SQL이 있다면, 선두 컬럼의 범위 조건을 같다(=) 조건으로 변경해 성능을 향상 시킬 수도 있다. 여기서는 일자 테이블을 이용해 날짜에 대한 범위 조건을 같다(=) 조건으로 변경해 볼 예정이다.

이와 같은 내용은 이미 많은 사람들이 알고 있는 기술이다. 정확히 기억나지 않지만, 어느 책에선가 본 기억도 있으며, 구글에서 MySQL 성능 관련 자료를 찾다가도 발견한 적이 있다. 직접 실습해보고 싶은 독자들을 위해 이어지는 이야기에서 다루어 보기로 했다. SQL BOOSTER는 실제 연습 해볼 수 있게 구성되어 있는 장점을 가지고 있다.

 

특정 고객들의 고객별 20171월 주문 금액을 조회하는SQL을 만들어 보자. 여기서는 T_ORD_BIG 테이블을 사용한다.

T_ORD_BIG의 인덱스를 확인해보자. ‘이어지는 이야기 .02’에서 인덱스 리스트를 조회하는 SQL은 이미 살펴보았다. T_ORD_BIG에는 아래와 같이 인덱스가 구성되어 있다.

T_ORD_BIG의 인덱스 리스트
INDEX_OWNER     TABLE_NAME    INDEX_NAME          IND_COLS             
============    ==========    =============       =====================
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_1	ORD_YMD	             
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_2	RNO	                 
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_3	ORD_YMD,CUS_ID	     
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_4       CUS_ID,ORD_YMD,ORD_ST
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_TEST    ORD_SEQ	

많은 인덱스가 있지만, 여기서는 ORD_YMD, CUS_ID 순서로 구성된 X_T_ORD_BIG_3 인덱스만 사용할 수 있다고 가정한다. 주어진 인덱스에서 최대의 성능이 나오도록 튜닝 연습을 할 필요가 있다. 인덱스로 모든 것을 해결하기 보다는 주어진 상황에서 먼저 해결책을 찾는 것이 좋다. (인덱스로 계속 덧칠한 시스템을 볼 때면 너무 가슴이 아프다.)

 

특정 고객들의 고객별 주문 금액을 조회하는 SQL은 아래와 같다. X_T_ORD_BIG_3 인덱스를 사용하도록 힌트를 주었다.

[SQL-1] 특정 고객들의 고객별 2017년 1월 주문 금액 조회하기
SELECT  /*+ INDEX(T2 X_T_ORD_BIG_3) */
        T1.CUS_ID
        ,SUM(T2.ORD_AMT) ORD_AMT
FROM    M_CUS T1
        ,T_ORD_BIG T2
WHERE   T2.CUS_ID = T1.CUS_ID
AND     T1.CUS_GD = 'A'
AND     T1.RGN_ID = 'A'
AND     T2.ORD_YMD LIKE '201701%'
GROUP BY T1.CUS_ID;

 

실행계획은 아래와 같다.

[실행계획-1] 특정 고객들의 고객별 2017년 1월 주문 금액 조회하기
-------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name          | Starts | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |               |      1 |     18 |00:00:07.48 |     736K|
|   1 |  SORT GROUP BY NOSORT          |               |      1 |     18 |00:00:07.48 |     736K|
|   2 |   NESTED LOOPS                 |               |      1 |    540K|00:01:24.55 |     736K|
|   3 |    NESTED LOOPS                |               |      1 |    540K|00:01:21.99 |     196K|
|*  4 |     TABLE ACCESS BY INDEX ROWID| M_CUS         |      1 |     20 |00:00:00.01 |       3 |
|   5 |      INDEX FULL SCAN           | PK_M_CUS      |      1 |     90 |00:00:00.01 |       1 |
|*  6 |     INDEX RANGE SCAN           | X_T_ORD_BIG_3 |     20 |    540K|00:00:54.67 |     196K|
|   7 |    TABLE ACCESS BY INDEX ROWID | T_ORD_BIG     |    540K|    540K|00:00:02.38 |     540K|
-------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   4 - filter(("T1"."RGN_ID"='A' AND "T1"."CUS_GD"='A'))
   6 - access("T2"."ORD_YMD" LIKE '201701%' AND "T2"."CUS_ID"="T1"."CUS_ID")
       filter(("T2"."ORD_YMD" LIKE '201701%' AND "T2"."CUS_ID"="T1"."CUS_ID"))

실행계획의 총 Buffers736K. 실행계획의 6번 항목을 보면 X_T_ORD_BIG_3을 이용해 NL 조인을 처리하고 있다. X_T_ORD_BIG_3 인덱스는 ORD_YMD, CUS_ID 순서로 구성되어 있다. 조인 처리시에 ORD_YMD에는 범위(LIKE) 조건이, CUS_ID에는 같다(=) 조건이 사용된다. 선두 컬럼이 범위 조건으로 사용되어 CUS_ID 조건은 조인에서 좋은 역할을 할 수 없게 된다.

사실 위 SQLCUS_ID, ORD_YMD 순서로 구성된 X_T_ORD_BIG_4 인덱스를 사용하는 것이 성능이 더 좋다. 여기서는 성능 개선 연습을 위해 무조건 X_T_ORD_BIG_3 인덱스만 사용하기로 한다.

X_T_ORD_BIG_3 인덱스를 효율적으로 사용하려면 ORD_YMD가 같다(=) 조건으로 사용되어야 한다. 이를 위해 기준일자 테이블을 활용할 수 있다. 아래 SQL을 보자.

[SQL-2] 특정 고객들의 고객별 2017년 1월 주문 금액 조회하기 – 기준일자 테이블 사용
SELECT  /*+ NO_MERGE(T1) INDEX(T2 X_T_ORD_BIG_3) */
        T1.CUS_ID
        ,SUM(T2.ORD_AMT) ORD_AMT
FROM    (
        SELECT  A.CUS_ID ,B.BAS_YMD
        FROM    M_CUS A
                ,C_BAS_YMD B
        WHERE   A.CUS_GD = 'A'
        AND     A.RGN_ID = 'A'
        AND     B.BAS_YM = '201701'
        ) T1
        ,T_ORD_BIG T2
WHERE   T2.CUS_ID = T1.CUS_ID
AND     T2.ORD_YMD = T1.BAS_YMD
GROUP BY T1.CUS_ID;

[SQL-2]M_CUS C_BAS_YMD를 카테시안-조인 한 후에 T_ORD_BIG과 조인 처리하고 있다. 조회할 고객별로 1월 한 달의 일자 데이터를 만든 후에 T_ORD_BIGORD_YMDCUS_ID를 같다(=) 조건으로 공급해주는 것이다. 이로 인해 ORD_YMD가 선두 컬럼인 X_T_ORD_BIG_3 인덱스를 효율적으로 사용해 조인을 처리할 수 있게 된다. 실행계획을 살펴보자.

[SQL-2] 특정 고객들의 고객별 2017년 1월 주문 금액 조회하기 – 기준일자 테이블 사용
-------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name           | Starts | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                |      1 |     18 |00:00:03.97 |     544K|
|   1 |  HASH GROUP BY                |                |      1 |     18 |00:00:03.97 |     544K|
|   2 |   NESTED LOOPS                |                |      1 |    540K|00:00:04.28 |     544K|
|   3 |    NESTED LOOPS               |                |      1 |    540K|00:00:00.20 |    4057 |
|   4 |     VIEW                      |                |      1 |    620 |00:00:00.01 |       8 |
|   5 |      MERGE JOIN CARTESIAN     |                |      1 |    620 |00:00:00.01 |       8 |
|*  6 |       TABLE ACCESS FULL       | M_CUS          |      1 |     20 |00:00:00.01 |       6 |
|   7 |       BUFFER SORT             |                |     20 |    620 |00:00:00.01 |       2 |
|*  8 |        INDEX RANGE SCAN       | X_C_BAS_YMD_01 |      1 |     31 |00:00:00.01 |       2 |
|*  9 |     INDEX RANGE SCAN          | X_T_ORD_BIG_3  |    620 |    540K|00:00:00.14 |    4049 |
|  10 |    TABLE ACCESS BY INDEX ROWID| T_ORD_BIG      |    540K|    540K|00:00:03.71 |     540K|
-------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   6 - filter(("A"."RGN_ID"='A' AND "A"."CUS_GD"='A'))
   8 - access("B"."BAS_YM"='201701')
   9 - access("T2"."ORD_YMD"="T1"."BAS_YMD" AND "T2"."CUS_ID"="T1"."CUS_ID")

Buffers736K에서 544K로 줄어들었다. 조인은 한 번 늘어났지만, 성능은 오히려 좋아졌다. X_T_ORD_JOIN_3 인덱스의 선두 컬럼인 ORD_YMD를 같다(=) 조건으로 처리했기 때문이다.

 

 

항상 그래왔듯이, 실행계획을 보고 실제로 성능이 개선되었는지, 자신이 원하는 방향으로 SQL이 처리되었는지 확인하는 것이 중요하다. 데이터에 따라 성능이 더 나빠질 수도 있으며, 실행계획이 다르게 풀릴 수도 있다. 여기서 사용한 예제는 필자가 몇 번의 수고 끝에, 기준일자 테이블을 사용하면 성능이 좋은 경우를 억지로 찾아낸 것이다. 중요한 건 이러한 방법이 있다는 것을 기억하고, 상황에 따라 응용해 보고, 실행계획까지 확인하는 것이다.

 

오늘은 여기까지입니다. 감사합니다.

 

<SQL을 변경한 성능 개선 .02>

 

개발 현장에는 다양한 요구 사항의 SQL이 있다. 예를 들어, 주문(T_ORD_JOIN)이 두 건 이상인 고객의 주문 로우 데이터를 보여주는 SQL을 작성해야 한다고 가정해 보자. 아래와 같이 SQL을 작성할 수 있다.

 


SQL BOOSTER 에 이어지는 이야기들입니다.~!
SQL BOOSTER 를 보신 분들께 좀 더 도움을 드리고자 추가로 작성한 내용입니다.

www.aladin.co.kr/shop/wproduct.aspx?ItemId=216383877

 

SQL BOOSTER

프로젝트 성공을 위한 SQL 필독서. 프로젝트를 진행하는 순서처럼 구성되어 있다. 프로젝트 투입을 위해 필요한 SQL 기술을 설명하고, 성능 테스트를 위해 필요한 기술을 설명한 뒤에 마지막으로

www.aladin.co.kr

설명의 편의상 반말체로 작성한 점 양해바랍니다.  pdf 파일도 첨부드리니 다운 받아 보셔도 됩니다.

SQL_Booster_이어지는이야기04.pdf
0.17MB

 

[SQL-1] 주문이 두 건 이상인 고객의 주문 로우 데이터 보여주기
SELECT  /*+ GATHER_PLAN_STATISTICS */
        T1.CUS_ID ,T1.ORD_YMD ,T1.ORD_QTY
FROM    (
            SELECT  A.CUS_ID
            FROM    T_ORD_JOIN A
            GROUP BY A.CUS_ID
            HAVING COUNT(*)>=2
        ) T0
        ,T_ORD_JOIN T1
WHERE   T1.CUS_ID = T0.CUS_ID
ORDER BY T1.CUS_ID ,T1.ORD_YMD;

 

인라인-뷰에서 주문이 두 건 이상인 고객을 찾아서 T_ORD_JOIN과 다시 조인해서 조회하는 SQL이다. SQL의 실행계획은 다음과 같다.

[실행계획-1] 주문이 두 건 이상인 고객의 주문 데이터 보여주기
-------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name           | Starts | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                |      1 |   1100 |00:00:02.98 |   47234 |
|   1 |  SORT ORDER BY                |                |      1 |   1100 |00:00:02.98 |   47234 |
|   2 |   NESTED LOOPS                |                |      1 |   3224K|00:00:02.75 |   47228 |
|   3 |    NESTED LOOPS               |                |      1 |   3224K|00:00:00.96 |   18187 |
|   4 |     VIEW                      |                |      1 |     90 |00:00:00.32 |    9025 |
|*  5 |      FILTER                   |                |      1 |     90 |00:00:00.32 |    9025 |
|   6 |       HASH GROUP BY           |                |      1 |     90 |00:00:00.31 |    9025 |
|   7 |        INDEX FAST FULL SCAN   | X_T_ORD_JOIN_1 |      1 |   3224K|00:00:00.23 |    9025 |
|*  8 |     INDEX RANGE SCAN          | X_T_ORD_JOIN_1 |     90 |   3224K|00:00:00.34 |    9162 |
|   9 |    TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |   3224K|   3224K|00:00:00.96 |   29041 |
-------------------------------------------------------------------------------------------------

실행계획을 보면, 7번 단계에서 X_T_ORD_JOIN_1 인덱스를 FAST FULL SCAN해서 두 건 이상인 고객을 찾아 낸 후, 다시 T_ORD_JOIN과 조인을 처리하고 있다. SQL을 작성한 대로 실행계획이 만들어졌다. Buffers를 보면 47,234. X_T_ORD_JOIN_1 인덱스를 모두 읽어서 처리했기 때문이다.

사실 T_ORD_JOIN 테이블의 데이터는 해당 테스트에 적당하게 구성되어 있지 않다. 모든 고객의 주문이 두 건 이상이기 때문이다. 그냥 조회해도 결과는 같다.

어쨌든 현업의 요구 사항 자체가 이와 같다면, 데이터 구성에 상관 없이 위와 같은 SQL을 작성해야만 한다. 조건에 맞는 모든 데이터를 조회해야 한다면, 위의 SQL도 나쁘다고 할 수는 없다. 하지만 우선 조회가 가능한 몇 건을 조회해야 한다면 어떨까? 우선 몇 건 조회만을 위해서는 너무 많은 IO가 발생했다.

아래와 같이 SQL을 변경해보면 어떨까?

[SQL-2] 주문이 두 건 이상인 고객의 주문 로우 데이터 보여주기 – EXISTS 사용
SELECT  /*+ GATHER_PLAN_STATISTICS */
        T1.CUS_ID ,T1.ORD_YMD ,T1.ORD_QTY
FROM    T_ORD_JOIN T1
WHERE   EXISTS(
            SELECT  /*+ USE_NL(A) */
                    *
            FROM    T_ORD_JOIN A
            WHERE   A.CUS_ID = T1.CUS_ID
	  AND     A.ROWID != T1.ROWID)
ORDER BY T1.CUS_ID ,T1.ORD_YMD;

T_ORD_JOIN CUS_IDORD_YMD 순서로 읽어가면서, CUS_ID는 같으면서 ROWID가 다른 데이터가 존재할 때만 조회를 하고 있다. 실행계획을 제어하기 위해 EXISTS 서브쿼리에 USE_NL 힌트도 사용했다. [SQL-1]의 결과와 정렬 순서는 약간 차이가 있을 수 있지만, 우선 몇 건만 조회하기에는 성능이 최적인 SQL이다. 실행계획을 살펴보자.

[실행계획-2] 주문이 두 건 이상인 고객의 주문 로우 데이터 보여주기 – EXISTS 사용
------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name           | Starts | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                |      1 |   1100 |00:00:00.01 |      94 |
|   1 |  NESTED LOOPS SEMI           |                |      1 |   1100 |00:00:00.01 |      94 |
|   2 |   TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |   1100 |00:00:00.01 |      60 |
|   3 |    INDEX FULL SCAN           | X_T_ORD_JOIN_2 |      1 |   1100 |00:00:00.01 |      28 |
|*  4 |   INDEX RANGE SCAN           | X_T_ORD_JOIN_1 |   1100 |   1100 |00:00:00.01 |      34 |
------------------------------------------------------------------------------------------------

Buffers94로 개선되었다. SQL-1은 총 Buffers47,234였다.

SQL을 변경해서 성능을 개선했다. 물론, 우선 몇 건만 보여주는 경우이면서 적절한 인덱스가 있을 때만 사용할 수 있는 방법이다.

 

현업의 요구 사항은 무궁무진하다. 요구 사항에 따라 SQL을 무작정 작성할 것이 아니라, 성능을 향상 할 방법을 고민해 보고 다양한 방법으로 SQL을 작성해보기 바란다. SQL은 언제나 변화무쌍하다.

 

 

오늘은 여기까지입니다. 감사합니다.

SQL BOOSTER 에 이어지는 이야기들입니다.~!

SQL BOOSTER 를 보신 분들께 좀 더 도움을 드리고자 추가로 작성한 내용입니다.

www.aladin.co.kr/shop/wproduct.aspx?ItemId=216383877

 

SQL BOOSTER

프로젝트 성공을 위한 SQL 필독서. 프로젝트를 진행하는 순서처럼 구성되어 있다. 프로젝트 투입을 위해 필요한 SQL 기술을 설명하고, 성능 테스트를 위해 필요한 기술을 설명한 뒤에 마지막으로

www.aladin.co.kr

 

설명의 편의상 반말체로 작성한 점 양해바랍니다.

pdf 파일을 다운 받아 보셔도 됩니다.

 

SQL_Booster_이어지는이야기03.pdf
0.40MB

 

 

 

<꽤 괜찮은 기준일자 테이블>

 

기준일자 테이블을 사용하는 사이트가 가끔 있다. 기준일자는 업무에 직접적으로 관련은 없지만 일자 정보만 모아서 저장해 놓은 테이블이다. 필자가 처음 맡았던 시스템에는 이미 기준일자 테이블을 누군가 설계 해놓았다. 덕분에 기준일자 테이블을 사용할 수 있었고 기준일자 테이블을 더욱 확장해 매우 유용하게 사용했다.

 

 

아래와 같은 형태로 기준일자 테이블을 생성할 예정이다.

[그림 1]

기준일자(BAS_YMD) 컬럼은 문자열로 ‘YYYYMMDD’ 형태의 일자 데이터를 저장한다. 기준일(BAS_DT)DATE형으로 일자를 저장한다. 논리명에 문자열은 일자’, DATE형은 을 쓰기로 한다. 기준요일은 기준일에 대한 요일 값이다. 기준일자순번은 기준일자 순서에 따라 1부터 시작하는 연속된 값을 부여한다. 이 값을 이용해 일자 연산을 대신할 수 있다. 기준주는 기준일자가 속한 주를 뜻한다. 기준월, 기준분기, 기준반기, 기준년도도 마찬가지다.

아래 스크립트를 이용해 기준일자 테이블을 생성할 수 있다.

[SQL-1] 기준일자 테이블 생성
CREATE TABLE C_BAS_YMD
(  BAS_YMD VARCHAR2(8) NOT NULL
  ,BAS_DT DATE NULL
  ,BAS_DY VARCHAR2(40) NULL
  ,BAS_YMD_SEQ NUMBER(9) NULL
  ,BAS_YW VARCHAR(6) NULL
  ,BAS_YM VARCHAR(6) NULL
  ,BAS_YQ VARCHAR(5) NULL
  ,BAS_YH VARCHAR(5) NULL
  ,BAS_YY VARCHAR(6) NULL
  );
  
ALTER TABLE C_BAS_YMD
  ADD CONSTRAINT PK_C_BAS_YMD PRIMARY KEY(BAS_YMD);
  
CREATE UNIQUE INDEX UX_C_BAS_YMD_01 ON C_BAS_YMD(BAS_DT);

CREATE UNIQUE INDEX UX_C_BAS_YMD_02 ON C_BAS_YMD(BAS_YMD_SEQ);

CREATE INDEX X_C_BAS_YMD_01 ON C_BAS_YMD(BAS_YM,BAS_YMD);

 

기준일자 테이블을 만든 후에는 다음 SQL을 이용해 기준일자 데이터를 생성할 수 있다. 201611일부터 20301231일까지의 기준일자를 생성한다.

[SQL-2] 기준일자 데이터 생성
INSERT INTO C_BAS_YMD
        (BAS_YMD,BAS_DT,BAS_YMD_SEQ)
SELECT  TO_CHAR(TO_DATE('20160101','YYYYMMDD') + ROWNUM,'YYYYMMDD') BAS_YMD
        ,TO_DATE('20160101','YYYYMMDD') + ROWNUM BAS_DT
        ,ROWNUM BAS_YMD_SEQ
FROM    DUAL T1
CONNECT BY TO_DATE('20160101','YYYYMMDD') + ROWNUM <= TO_DATE('20301231','YYYYMMDD');

COMMIT;

 

C_BAS_YMD에는 BAS_YMD(기준일자), BAS_DT(기준일), BAS_YMD_SEQ(기준일자순번) 값만 입력되어 있다. 나머지 값들은 UPDATE로 채워 넣을 예정이다. 아래 SQL은 기준주(BAS_YW)를 제외한 나머지 값들을 채워 넣는 SQL이다. 기준주는 로직이 조금 복잡해 별도 처리한다.

[SQL-3] 기준월, 기준분기, 기준반기, 기준년도, 기준요일 UPDATE
UPDATE  C_BAS_YMD T1
SET     T1.BAS_YM = SUBSTR(T1.BAS_YMD,1,6)
        ,T1.BAS_YQ = SUBSTR(T1.BAS_YMD,1,4)||CEIL(TO_NUMBER(SUBSTR(T1.BAS_YMD,5,2)) / 3)
        ,T1.BAS_YH = SUBSTR(T1.BAS_YMD,1,4)||CEIL(TO_NUMBER(SUBSTR(T1.BAS_YMD,5,2)) / 6)
        ,T1.BAS_YY = SUBSTR(T1.BAS_YMD,1,4)
        ,T1.BAS_DY = CASE TO_CHAR(T1.BAS_DT,'D') 
                                WHEN '1' THEN 'SUN'
                                WHEN '2' THEN 'MON'
                                WHEN '3' THEN 'TUE'
                                WHEN '4' THEN 'WED'
                                WHEN '5' THEN 'THU'
                                WHEN '6' THEN 'FRI'
                                WHEN '7' THEN 'SAT' END
        ;

COMMIT;

 

[SQL-3]은 설명 없이도 이해하는데 아무 어려움이 없을 것이다. 아래 [SQL-4]는 기준주를 UPDATE하는 SQL이다. SQL을 먼저 살펴보자.

[SQL-4] 기준주 UPDATE
UPDATE  C_BAS_YMD T1
SET     T1.BAS_YW = 
        CASE
WHEN TO_CHAR(T1.BAS_DT,'MMDD') <= '0107' AND TO_CHAR(T1.BAS_DT,'IW') >= 50 THEN 
TO_CHAR(T1.BAS_DT,'YYYY')-1||TO_CHAR(T1.BAS_DT,'IW')
            WHEN TO_CHAR(T1.BAS_DT,'MMDD') >= '1224' AND TO_CHAR(T1.BAS_DT,'IW') = 01 THEN 
TO_CHAR(T1.BAS_DT,'YYYY')+1||TO_CHAR(T1.BAS_DT,'IW')
            ELSE TO_CHAR(T1.BAS_DT,'YYYY')||TO_CHAR(T1.BAS_DT,'IW')
        END;

C_BAS_YMD의 기준일(BAS_DT) 값을 이용해 기준주를 구하고 있다. TO_CHAR(T1.BAS_DT,’IW’)를 실행하면, 해당 일의 주 값을 알 수 있다.

첫 번째 WHEN 부분부터 살펴보자. 기준일이 17일보다 작으면서, 기준일의 주 값이 50보다 크면, 기준일의 년도가 아니라, 전년도의 년도를 가져와 주 값과 결합한다. 아래와 같은 경우다. 201611일이 2016-53주일리는 없다. 그러므로 전년도인 2015-53으로 만들어야 한다. (비교에서 년도는 제외하고 년월만 사용한다.)

[SQL-5] 첫 번째  WHEN, 기준일이 1월7일보다 작으면서 주 값이 50 이상인 경우
SELECT TO_CHAR(TO_DATE('20160101'),'IW') FROM DUAL; -- 결과 값은 53주가 나온다. 

[SQL-4]의 두 번째 WHEN 부분을 살펴보자. 기준일이 1224일보다 크면서, 주 값이 1주인 경우다. 이 경우에는 기준일의 년도의 다음 년도를 가져와 주를 결합한다. 아래와 같은 경우다. (비교에서 년도는 제외하고 년월만 사용한다.)

[SQL-5] 두 번째 WHEN, 기준일이 12월24일보다 크면서 주 값이 1인 경우.
SELECT TO_CHAR(TO_DATE('20191231'),'IW') FROM DUAL; -- 결과 값은 1주가 나온다. 

첫 번쨰, 두 번째 WHEN은 년초의 몇 일이 작년 주에 속하거나, 년말의 몇 일이 내년 주에 속하는 경우를 예외처리한 것이다. 세 번째 WHEN은 나머지 경우로 기준일의 년도를 그대로 사용하면 되는 경우다.

위에서 첫 번째 두 번째 WHEN절에서 17, 1224일을 사용했는데 꼭 그 날이 아니어도 된다. 대략 년초, 년말이면 된다.

데이터가 제대로 입력되었는지는 아래 SQL로 확인해볼 수 있다.

[SQL-6] 주 데이터 확인
SELECT  T1.BAS_YW
        ,COUNT(*)
        ,MIN(T1.BAS_DT) FRS_DT
        ,MAX(T1.BAS_DT) LST_DT
        ,MAX(T1.BAS_DY) KEEP(DENSE_RANK FIRST ORDER BY T1.BAS_DT ASC)
        ,MAX(T1.BAS_DY) KEEP(DENSE_RANK LAST ORDER BY T1.BAS_DT ASC)
FROM    C_BAS_YMD T1
GROUP BY T1.BAS_YW;

기준일자 테이블에 첫 주와 마지막 주를 제외하고 모두 7일씩 제대로 데이터가 만들어진 것을 확인할 수 있다. [SQL-6]에는 KEEP이 사용되었다. KEEP의 사용법은 정희락님의 불친절한 SQL 프로그래밍을 참고해보길 추천한다.

이제 기준일자(C_BAS_YMD) 테이블에 기본적인 데이터를 모두 채워 넣었다. 이 테이블은 개발 곳곳에서 유용하게 사용할 수 있다. 사용자 화면에 아래 그림과 같이 주를 고르는 콤보 박스를 만들어야 한다고 가정해보자. 현재일 기준으로 최근 30주 데이터가 나와야 한다.

[그림 2]

개발자 입장에서 보면, 이와 같은 컨트롤을 만드는 일이 어렵지는 않다. 더욱이 개발 환경이 갈수록 좋아져서 많은 것들이 모듈화가 잘 되어 있다. 어쨌든, 위와 같은 컨트롤에 값을 채워 넣는 작업을 SQL로도 쉽게 해결 할 수 있다. 현재일 기준으로 최근 30주 데이터를 구하기 위해서는 아래 SQL을 사용할 수 있다.

[SQL-7] 현재일 기준으로 최근 30주 가져오기.(현재 2019년12월10일이라고 가정)
SELECT  T1.BAS_YW
FROM    (
        SELECT  T1.BAS_YW||'('||MIN(T1.BAS_YMD)||'~'||MAX(T1.BAS_YMD)||')' BAS_YW
        FROM    C_BAS_YMD T1
        WHERE   T1.BAS_YMD <= '20191210'
        GROUP BY T1.BAS_YW
        ORDER BY T1.BAS_YW DESC
        ) T1
WHERE   ROWNUM <= 30;

 

SQL의 실행 결과는 아래와 같다.

결과-7] 현재일 기준으로 최근 30주 가져오기.(현재 2019년12월10일이라고 가정)
BAS_YW
=========================
201950(20191209~20191210)
201949(20191202~20191208)
201948(20191125~20191201)
201947(20191118~20191124)
…

값을 잘 보면 잘못된 부분이 있다. 201950주가 20191210일까지 밖에 없다. 201950주는 1215일까지 나와야 한다. 현재일을 기준으로 데이터를 조회했기 때문이다. 주말인 1215일까지 포함되서 조회하기 위해서는 아래와 같이 서브쿼리를 추가해야 한다.

[SQL-8] 현재일 기준으로 최근 30주 가져오기 – 주말을 포함
SELECT  T1.BAS_YW
FROM    (
        SELECT  T1.BAS_YW||'('||MIN(T1.BAS_YMD)||'~'||MAX(T1.BAS_YMD)||')' BAS_YW
        FROM    C_BAS_YMD T1
        WHERE   T1.BAS_YW <= (SELECT A.BAS_YW FROM C_BAS_YMD A WHERE A.BAS_YMD = '20191210')
        GROUP BY T1.BAS_YW
        ORDER BY T1.BAS_YW DESC
        ) T1
WHERE   ROWNUM <= 30;

기준일자 테이블을 사용해 최근 30주를 손쉽게 구했다. SQL의 경우 성능을 고려해 BAS_YW 컬럼에 인덱스를 고민해 볼 수 있다. C_BAS_YMD 테이블에는 필요하다면 얼마든지 인덱스를 만들어도 된다. 데이터가 한 번 입력되고 나면 추가로 입력되거나 변경될 일은 없기 때문이다. (물론 기준일자의 마지막쯤에 다다르면, 다음 일자들에 대한 생성은 필요하다.)

 

기준일자 테이블을 사용하면, 실적이 없는 일자의 실적도 손쉽게 구할 수 있다. 아래 SQL20173월의 특정 고객 주문을 조회하는 SQL이다. 주문이 없는 날도 0으로 표현해야 한다.

[SQL-9] 특정 고객의 일별 주문 조회
SELECT  T1.BAS_YMD
        ,COUNT(T2.ORD_SEQ) ORD_CNT
FROM    C_BAS_YMD T1
        ,T_ORD_JOIN T2
WHERE   T1.BAS_YM = '201703'
AND     T1.BAS_YMD = T2.ORD_YMD(+)
AND     T2.CUS_ID(+) = 'CUS_0010'
GROUP BY T1.BAS_YMD
ORDER BY T1.BAS_YMD;

 

SQL을 실행하면 CUS_0010 고객의 주문이 없는 일도 모두 0으로 집계해서 조회가 가능하다. 물론 일자 테이블이 없어도 얼마든지 위의 결과를 만들어 낼 수 있다. 아래 SQL을 참고하자.

[SQL-10] 특정 고객의 일별 주문 조회 – CONNECT BY 사용
SELECT  T1.BAS_YMD
        ,COUNT(T2.ORD_SEQ) ORD_CNT
FROM    (
          SELECT TO_DATE('20170301','YYYYMMDD') + (ROWNUM - 1) BAS_YMD 
          FROM   DUAL A
          CONNECT BY TO_DATE('20170301','YYYYMMDD') + (ROWNUM - 1) <= TO_DATE('20170331','YYYYMMDD')
        ) T1
        ,T_ORD_JOIN T2
WHERE   T1.BAS_YMD = T2.ORD_YMD(+)
AND     T2.CUS_ID(+) = 'CUS_0010'
GROUP BY T1.BAS_YMD
ORDER BY T1.BAS_YMD;

 

[SQL-9][SQL-10]의 결과는 동일하다. 하지만 [SQL-9]가 좀 더 깔끔하다는 것을 모두 동의하리라 생각한다.

오라클은 그나마 CONNECT BY가 있기 때문에 기준일자 테이블이 없어도 [SQL-10]과 같은 패턴을 사용할 수 있다. 하지만 MySQLMS-SQLCONNECT BY가 없기 때문에, 기준일자 테이블이 없으면 더 복잡한 방법을 사용해야 한다. (MS-SQLWITH절을 중첩 사용해 CONNECT BY를 구현한다. 사용해 본 사람들은 이러한 방법이 구현도 복잡하며, 성능이 안 좋은 경우도 많다는 것을 알 것이다.)

겨울을 대비해 보일러를 들여 놓는 것처럼, 편리한 개발을 위해 기준일자 테이블 하나 들여 놓기 바란다.

 

끝으로, 숨겨진 퀴즈가 두 개 있다.
첫 번째 퀴즈, [SQL-10]에는 성능에 영향을 주는 잘 못된 부분이 하나 있다. [SQL-9] [SQL-10]의 실행계획을 모두 떠보고, 잘 못된 부분을 찾아 고쳐보기 바란다. 아마도 어렵지 않게 찾아 낼 수 있을 것이다.
두 번쨰 퀴즈, [SQL-9] IO를 좀 더 개선해 보기 바란다. CPU나 메모리 사용량이 좀 더 늘어도 상관 없다. IO를 줄이는 것이 목표다. SQL만 조금 변경해 IO를 개선해 보기 바란다. 절대 힌트는 사용하지 않아야 한다.

스스로 고민해보고 문제를 풀어보기 바란다. 고민만으로 충분한 도움이 될 것이기 때문이다.

 

 

오늘은 여기까지입니다. 감사합니다.

 

 

SQL BOOSTER 에 이어지는 이야기들입니다.~! 
SQL BOOSTER 를 보신 분들께 좀 더 도움을 드리고자 추가로 작성한 내용입니다.

www.aladin.co.kr/shop/wproduct.aspx?ItemId=216383877

 

SQL BOOSTER

프로젝트 성공을 위한 SQL 필독서. 프로젝트를 진행하는 순서처럼 구성되어 있다. 프로젝트 투입을 위해 필요한 SQL 기술을 설명하고, 성능 테스트를 위해 필요한 기술을 설명한 뒤에 마지막으로

www.aladin.co.kr

설명의 편의상 반말체로 작성한 점 양해바랍니다.  pdf 파일도 첨부드리니 다운 받아 보셔도 됩니다.

 

<LISTAGG & 테이블의 인덱스 확인하기>

 

오늘은 정말 짧은 글을 하나 올립니다. 앞으로 이어질 글에, 인덱스를 확인해야 하는 경우들이 있기 때문에 짧지만 LISTAGG와 같이 설명하고 넘어가려고 합니다. 


오라클 11g에는 LISTAGG함수가 있다. LISTAGG는 여러 로우를 하나로 만드는 유용한 기능이다. 쉽게 말에 여러 건의 데이터를 콤마로 결합해 한 건으로 보여주기 위해 사용한다. [그림 2-1]을 본다면 이해가 쉽다.

 

그림 2-1

현업의 요구 사항 중에는 [그림 2-1]의 우측과 같이 여러 건의 로우를 콤마를 이용해 한 건으로 보여주기를 원하는 경우가 있다. 이때, LISTAGG를 사용하면 비교적 쉽게 요구 사항을 해결할 수 있다. 하지만 많은 데이터를 LISTAGG로 처리하면 성능적인 이슈가 있을 수 있다. 로우를 모두 끌어 올려 컬럼으로 변경하는 것이기 때문에, 일반적인 데이터 조회보다 성능 부하가 있을 수 밖에 없다.

필자의 경우, 현업에서 여러 건을 콤마로 구분해 한 건으로 보여주고자 하는 요구 사항은 최대한 들어주지 못하는 쪽으로 협의해왔다. 꼭 필요한 경우라면, 건수에 제한을 두고 성능까지 검토한 후에 요구사항을 받아들였다. LISTAGG를 익히기 전에 성능적인 이슈가 있음을 알고 제한적으로 사용해야 함을 꼭 명심해주기 바란다.

C_BAS_CD 테이블을 BAS_CD_DV(기준코드구분) 별로, 기준코드(BAS_CD) 리스트를 한 컬럼으로 보여주는 SQL을 작성해보자. 아래와 같다.

[SQL-1] BAS_CD_DV별 BAS_CD 리스트를 한 컬럼으로 표현
SELECT  T1.BAS_CD_DV
        ,LISTAGG(T1.BAS_CD,',') WITHIN GROUP(ORDER BY T1.BAS_CD) IND_COLS
FROM    C_BAS_CD T1
WHERE   T1.LNG_CD = 'KO'
GROUP BY T1.BAS_CD_DV;

SQL을 실행하면 아래와 같은 결과가 나온다.

[결과-1] BAS_CD_DV별 BAS_CD 리스트를 한 컬럼으로 표현
BAS_CD_DV	          BAS_CD_LST
=========	          ==========
CUS_GD		A,B
GND_TP		FEMLE,MALE
ITM_TP		CLOTHES,COOK,ELEC,PC
...

LISTAGG를 사용하려면 GROUP BY를 같이 사용해야 한다. SQL BOOSTERGROUP BY 부분을 읽었다면 전혀 어렵지 않을 것이다. SUM이나 MAX가 사용될 집계함수 부분만 LISTAGG로 대체하면 된다. LISTAGG에는 WITHIN GROUP절도 같이 사용해야 한다. 아래를 참고하기 바란다.

- LISTAGG(결합할 컬럼명, 구분자) WITHIN GROUP(ORDER BY 정렬기준)

 

이번에는 코드와 코드명을 결합해 코드명까지 보여주도록 LISTAGG를 변형해보자.

[SQL-2] BAS_CD_DV별 BAS_CD, BAS_CD_NM 리스트를 한 컬럼으로 표현

SELECT  T1.BAS_CD_DV
        ,(SELECT A.BAS_CD_DV_NM FROM C_BAS_CD_DV A WHERE A.BAS_CD_DV = T1.BAS_CD_DV) BAS_CD_DV_NM
        ,LISTAGG('['||T1.BAS_CD||']'||T1.BAS_CD_NM,',') WITHIN GROUP(ORDER BY T1.BAS_CD) BAS_CD_LST
FROM    C_BAS_CD T1
WHERE   T1.LNG_CD = 'KO'
GROUP BY T1.BAS_CD_DV;

SQL의 결과는 아래와 같다.

[결과-2] BAS_CD_DV별 BAS_CD, BAS_CD_NM 리스트를 한 컬럼으로 표현
BAS_CD_DV     BAS_CD_DV_NM           BAS_CD_LST
=========     =============          ===========
CUS_GD	    고객등급	       [A]A,[B]B
GND_TP	    성별구분	       [FEMLE]여성,[MALE]남성
ITM_TP	    아이템유형	       [CLOTHES]옷,[COOK]주방도구,[ELEC]가전제품,[PC]컴퓨터
LNG_CD	    언어코드	       [CN]중국어,[EN]영어,[KO]한국어
ORD_ST	    주문상태	       [COMP]완료,[WAIT]대기
PAY_TP	    지불유형	       [BANK]계좌이체,[CARD]카드

여기까지 위의 SQL 들을 직접 입력하고 실행해봤다면, LISTAGG의 사용에는 문제가 없으리라 생각된다. LISTAGG 외에도, 유사한 XMLLAG도 있으니 찾아보기 바란다. 처음에도 이야기 했듯이 성능적인 부분을 고려해 제한적으로 사용하기 바란다.

 

 

사실, LISTAGG에 대해서는 쓰고 싶지 않았다. 프로젝트 여기 저기에서, 무분별하게 LISTAGG가 사용되게 될까 걱정되기 때문이다. 그럼에도 불구하고 LISTAGG에 대해 짤막하게 다룬 이유는, 아래의 인덱스 리스트를 조회하는 SQL을 보여주기 위해서다. 필자가 자주 사용하는 SQL이다.

[SQL-3] 인덱스 리스트 조회하기
SELECT  T1.INDEX_OWNER ,T1.TABLE_NAME ,T1.INDEX_NAME
        ,LISTAGG(T1.COLUMN_NAME,',') WITHIN GROUP(ORDER BY T1.COLUMN_POSITION) IND_COLS
FROM    ALL_IND_COLUMNS T1
        ,ALL_INDEXES T2
WHERE   T1.TABLE_NAME = 'T_ORD_BIG'
AND     T1.INDEX_NAME = T2.INDEX_NAME
AND     T1.TABLE_NAME = T2.TABLE_NAME
AND     T1.INDEX_OWNER = T2.OWNER
GROUP BY T1.INDEX_OWNER ,T1.TABLE_NAME ,T1.INDEX_NAME
ORDER BY T1.INDEX_OWNER ,T1.TABLE_NAME ,T1.INDEX_NAME;

 

SQL을 실행하면 아래와 같이, T_ORD_BIG 테이블의 인데스 현황을 보기 좋게 조회할 수 있다.

[결과-3] 인덱스 리스트 조회하기
INDEX_OWNER     TABLE_NAME    INDEX_NAME          IND_COLS             
============    ==========    =============       =====================
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_1	ORD_YMD	             
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_2	RNO	                 
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_3	ORD_YMD,CUS_ID	     
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_4       CUS_ID,ORD_YMD,ORD_ST
ORA_SQL_TEST    T_ORD_BIG	X_T_ORD_BIG_TEST    ORD_SEQ	

빠르게 튜닝을 하려면, 조회하는 테이블의 인덱스들을 빨리 확인해야 한다. 그때, 유용하게 사용하는 스크립트다.

 

오늘 준비한 글은 여기까지입니다. 읽어주셔서 감사합니다.!

 

SQL BOOSTER 에 이어지는 이야기들입니다.~! 
SQL BOOSTER 를 보신 분들께 좀 더 도움을 드리고자 추가로 작성한 내용입니다.

www.aladin.co.kr/shop/wproduct.aspx?ItemId=216383877

 

SQL BOOSTER

프로젝트 성공을 위한 SQL 필독서. 프로젝트를 진행하는 순서처럼 구성되어 있다. 프로젝트 투입을 위해 필요한 SQL 기술을 설명하고, 성능 테스트를 위해 필요한 기술을 설명한 뒤에 마지막으로

www.aladin.co.kr

설명의 편의상 반말체로 작성한 점 양해바랍니다.  pdf 파일도 첨부드리니 다운 받아 보셔도 됩니다.

 

SQL_Booster_이어지는이야기01.pdf
0.23MB

<SQL을 변경한 성능 개선 .01>

SQL BOOSTER를 읽어주신 독자 여러분께 깊은 감사의 마음을 전합니다. 꼼꼼히 읽어주시는 분들의 고마움에 보답하고자 책에는 담지 않았지만, 책에서 구축된 데이터베이스를 사용해 추가로 익힐 수 있는 내용을 짤막하게 시리즈로 적어보려고 합니다. (SQL BOOSTER를 끝까지 읽으면 만들어지는 테이블들을 사용합니다.) 올리는 주기와 시기, 내용을 약속할 수 없는 점 먼저 이해 바랍니다. 본문의 내용은 전달 효율성을 위해 반말체를 사용하는 점 이해 부탁드립니다. 그럼, 시작하도록 하겠습니다.

 

 

하나의 테이블에서 데이터를 조회할 때, 상황에 따라 다양한 구간을 조회해야 할 때가 있다. 예를 들면 아래 SQL과 같다.

-- [SQL-1] ITM_ID에 따라 다양한 구간을 조회 – UNION ALL

SELECT  T1.ITM_ID
        ,COUNT(*) ORD_CNT
FROM    T_ORD_JOIN T1
WHERE   T1.CUS_ID = 'CUS_0002'
AND     T1.ITM_ID = 'ITM002' AND T1.ORD_YMD BETWEEN '20170102' AND '20170103'
GROUP BY T1.ITM_ID
UNION ALL
SELECT  T1.ITM_ID
        ,COUNT(*) ORD_CNT
FROM    T_ORD_JOIN T1
WHERE   T1.CUS_ID = 'CUS_0002'
AND     T1.ITM_ID = 'ITM079' AND T1.ORD_YMD BETWEEN '20170102' AND '20170115'
GROUP BY T1.ITM_ID
UNION ALL
SELECT  T1.ITM_ID
        ,COUNT(*) ORD_CNT
FROM    T_ORD_JOIN T1
WHERE   T1.CUS_ID = 'CUS_0002'
AND     T1.ITM_ID = 'ITM015' AND T1.ORD_YMD BETWEEN '20170102' AND '20170125'
GROUP BY T1.ITM_ID

 

아이템별로 팔리는 주기가 다르다고 가정했을 때 이와 같은 SQL이 나올 수 있다. SQL을 실행계획을 확인해보면 아래와 같다.

[PLAN-1] ITM_ID에 따라 다양한 구간을 조회 – UNION ALL
-------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name           | Starts | A-Rows |   A-Time   | Buffers |
-------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                |      1 |      3 |00:00:00.01 |     106 |
|   1 |  UNION-ALL                    |                |      1 |      3 |00:00:00.01 |     106 |
|   2 |   SORT GROUP BY NOSORT        |                |      1 |      1 |00:00:00.01 |      27 |
|*  3 |    TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |   1000 |00:00:00.01 |      27 |
|*  4 |     INDEX RANGE SCAN          | X_T_ORD_JOIN_2 |      1 |   2000 |00:00:00.01 |      11 |
|   5 |   SORT GROUP BY NOSORT        |                |      1 |      1 |00:00:00.01 |      27 |
|*  6 |    TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |   1000 |00:00:00.01 |      27 |
|*  7 |     INDEX RANGE SCAN          | X_T_ORD_JOIN_2 |      1 |   2000 |00:00:00.01 |      11 |
|   8 |   SORT GROUP BY NOSORT        |                |      1 |      1 |00:00:00.01 |      52 |
|*  9 |    TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |   1000 |00:00:00.01 |      52 |
|* 10 |     INDEX RANGE SCAN          | X_T_ORD_JOIN_2 |      1 |   4000 |00:00:00.01 |      19 |
-------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   3 - filter("T1"."ITM_ID"='ITM002')
   4 - access("T1"."CUS_ID"='CUS_0002' AND "T1"."ORD_YMD">='20170102' AND 
              "T1"."ORD_YMD"<='20170103')
   6 - filter("T1"."ITM_ID"='ITM079')
   7 - access("T1"."CUS_ID"='CUS_0002' AND "T1"."ORD_YMD">='20170102' AND 
              "T1"."ORD_YMD"<='20170115')
   9 - filter("T1"."ITM_ID"='ITM015')
  10 - access("T1"."CUS_ID"='CUS_0002' AND "T1"."ORD_YMD">='20170102' AND 
              "T1"."ORD_YMD"<='20170125')

실행계획을 확인해 보면 Buffers(논리적IO)106이다. 그리고 같은 테이블인 T_ORD_JOIN에 세 번이나 접근하고 있다. SQL에서 T_ORD_JOIN3개를 UNION ALL로 묶었기 때문에 당연한 결과다.

같은 테이블의 반복 사용은 성능 저하로 이어진다. 반복 사용을 제거하기 위해 아래와 같은 SQL을 고민해 볼 수 있다.

-- [SQL-2] ITM_ID에 따라 다양한 구간을 조회 – CASE 사용
SELECT  T1.ITM_ID
        ,COUNT(*) ORD_CNT
FROM    T_ORD_JOIN T1
WHERE   T1.CUS_ID = 'CUS_0002'
AND   CASE WHEN T1.ITM_ID = 'ITM002' AND T1.ORD_YMD BETWEEN '20170102' AND '20170103' THEN 1
           WHEN T1.ITM_ID = 'ITM079' AND T1.ORD_YMD BETWEEN '20170102' AND '20170115' THEN 1
           WHEN T1.ITM_ID = 'ITM015' AND T1.ORD_YMD BETWEEN '20170102' AND '20170125' THEN 1
        END = 1
GROUP BY T1.ITM_ID;

T_ORD_JOIN을 한 번만 사용해 SQL이 간단해졌다. 사실 이와 같은 SQL은 개발 편의성을 위해 작성되는 패턴이다. SQL의 성능 향상과는 아무 연관이 없다. ORD_YMD 조건이나, ITMD_ID 조건에 대해 적절한 인덱스가 있어도 효율적으로 사용할 수 없기 때문이다. 이와 같은 방법보다는 [SQL-1]UNION ALL 방법이 성능에 있어서 훨씬 유리하다. [SQL-2]의 실행계획을 살펴보자. 아래와 같다.

-- [PLAN-2] ITM_ID에 따라 다양한 구간을 조회 – CASE 사용
------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name           | Starts | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                |      1 |      3 |00:00:00.02 |     359 |
|   1 |  HASH GROUP BY               |                |      1 |      3 |00:00:00.02 |     359 |
|*  2 |   TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |   3000 |00:00:00.01 |     359 |
|*  3 |    INDEX RANGE SCAN          | X_T_ORD_JOIN_1 |      1 |  30000 |00:00:00.01 |      87 |
------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(CASE  WHEN ("T1"."ITM_ID"='ITM002' AND "T1"."ORD_YMD">='20170102' AND
   3 - access("T1"."CUS_ID"='CUS_0002')

[SQL-2]의 실행계획에서 총 Buffers359. [SQL-1]보다 세 배 정도 나빠졌다. 테이블 반복을 아무리 제거해도 효율적으로 인덱스를 사용하지 못한다면 성능이 좋아질 수 없다. [PLAN-2]를 보면 X_T_ORD_JOIN_1 인덱스를 이용해 CUS_ID 조건만 access 처리했다. ([PLAN-2]7, 13번 라인을 보면 알 수 있다.)

가능하면 [SQL-1]과 같이 개발하는 것이 성능에도 유리하고 명확해 보인다. 만약에 추가로 성능 개선이 필요하다면 아래와 같은 패턴을 고민해 볼 수 있다. 오라클의 LEASTGREATEST 함수를 사용한 방법이다.

[SQL-3] ITM_ID에 따라 다양한 구간을 조회 – LEAST, GREATEST 사용
SELECT  
T1.ITM_ID
        ,COUNT(*) ORD_CNT
FROM    T_ORD_JOIN T1
WHERE   T1.CUS_ID = 'CUS_0002'
AND     CASE WHEN T1.ITM_ID = 'ITM002' AND T1.ORD_YMD BETWEEN '20170102' AND '20170103' THEN 1
             WHEN T1.ITM_ID = 'ITM079' AND T1.ORD_YMD BETWEEN '20170102' AND '20170115' THEN 1
             WHEN T1.ITM_ID = 'ITM015' AND T1.ORD_YMD BETWEEN '20170102' AND '20170125' THEN 1
        END = 1
AND T1.ORD_YMD BETWEEN LEAST('20170102','20170102','20170102') 
AND GREATEST('20170103','20170115','20170125')
GROUP BY T1.ITM_ID;

[SQL-2]를 그대로 사용하고, 대신에 CUS_ID, ORD_YMD로 구성된 복합인덱스를 효율적으로 사용할 수 있도록 10번과 11번 라인에 조건을 추가했다. LEAST는 매개 변수 값 중에서 가장 작은 값을 돌려준다. GREATESTLEAST의 반대다. LEASTGREATEST의 매개 변수 값은 어느 값을 사용했는지 위 SQL을 보면 쉽게 알 수 있다. 색으로 표시해 놓았다.

실행계획을 통해 성능 개선이 되었는지 확인해보자. 아래와 같다.

[PLAN-3] ITM_ID에 따라 다양한 구간을 조회 – 최적화
------------------------------------------------------------------------------------------------
| Id  | Operation                    | Name           | Starts | A-Rows |   A-Time   | Buffers |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT             |                |      1 |      3 |00:00:00.01 |      52 |
|   1 |  HASH GROUP BY               |                |      1 |      3 |00:00:00.01 |      52 |
|*  2 |   TABLE ACCESS BY INDEX ROWID| T_ORD_JOIN     |      1 |   3000 |00:00:00.01 |      52 |
|*  3 |    INDEX RANGE SCAN          | X_T_ORD_JOIN_2 |      1 |   4000 |00:00:00.01 |      19 |
------------------------------------------------------------------------------------------------
 
Predicate Information (identified by operation id):
---------------------------------------------------
   2 - filter(CASE  WHEN ("T1"."ITM_ID"='ITM002' AND "T1"."ORD_YMD">='20170102' AND … 생략
   3 - access("T1"."CUS_ID"='CUS_0002' AND "T1"."ORD_YMD">='20170102' AND "T1"."ORD_YMD"<='20170125')

Buffers52로 개선되었다. 인덱스를 추가하지 않아도, 조금만 생각해 보면 성능을 개선 할 수 있는 다양한 방법들이 있다. 항상 고민하고 좋은 방법을 찾는 습관을 갖기 바란다.

마지막으로 연습 문제를 하나 남긴다. 아래 SQL을 같은 방법으로 성능 개선을 시도해 보고 실행계획을 확인해 보기 바란다.

[SQL-4] PAY_TP에 따라 다양한 구간을 조회 – LEAST, GREATEST 사용
SELECT  T1.PAY_TP
        ,COUNT(*) ORD_CNT
FROM    T_ORD_BIG T1
WHERE   T1.CUS_ID = 'CUS_0002'
AND     CASE WHEN T1.PAY_TP IS NULL AND T1.ORD_YMD BETWEEN  '20170102' AND '20170103' THEN 1
             WHEN T1.PAY_TP = 'CARD' AND T1.ORD_YMD BETWEEN '20170102' AND '20170115' THEN 1
             WHEN T1.PAY_TP = 'BANK' AND T1.ORD_YMD BETWEEN '20170102' AND '20170120' THEN 1
        END = 1
GROUP BY T1.PAY_TP;

프로젝트 성공을 위한 SQL 필독서

개발자에게 필요한 오라클 SQL 실전서

SQL BOOSTER 를 소개합니다.

책의 대상

- SQL을 좀 더 잘 하고 싶은 모든 분들

- 현재 Back-End 개발자분들

- 앞으로 DB튜닝을 하고 싶은 개발자분들

책 특징

- 책의 예제들은 실제 실행해 볼 수 있게 구성되어 있다.

- 책의 예제들은 실전용이다. 예제를 위한 예제는 최소화했다.

- 초보자를 위한 책은 아니지만, JOIN과 GROUP BY를 원리부터 촘촘하게 설명한다.

이 두가지를 제대로 익혀야만 SQL실력을 Boost할 수 있기 때문이다.

- 트랜잭션, 채번, 페이징 처리, 분석함수 등의 현장에서 쓰이는 기술들을 설명한다.

- 마치 개발 프로젝트를 진행하는 순서처럼 책이 구성되어 있다.

Kick-Off 개발 투입 > 성능,부하테스트 > 오픈,성공적인 마무리

 

책 전체 스크립트 한방 다운로드!

SQL_BOOSTER_스크립트_v1_03.zip
0.06MB

 

아래는 ANSI버젼입니다.

- SQL PLUS 환경에서 실행하는 분들을 위한 스크립트이다.

- ANSI 로 변경하면서 부득이하게 한글이 깨질수 있습니다.

ANSI_SQLBooster_Script.zip
0.03MB

 

목차입니다.

PART. I Kick-Off, 프로젝트 시작을 위한 SQL 기술

Chapter. 1 준비하기

1.1 연습을 위한 환경 구성

1.1.1 연습용 테이블 스페이스 만들기

1.1.2 연습용 사용자 만들기

1.2 연습용 데이터베이스 구성

1.2.1 연습용 테이블 생성하기

1.2.2 연습용 데이터 생성하기

Chapter. 2 GROUP BY와 ROLLUP

2.1 GROUP BY

2.1.1 GROUP BY 이해하기

2.1.2 GROUP BY 컬럼의 변형

2.1.3 집계함수에서 CASE문 활용하기

2.1.4 COUNT 집계함수

2.1.5 중복을 제거한 COUNT

2.1.6 HAVING

2.2 ROLLUP

2.2.1 ROLLUP 이해하기

2.2.2 ROLLUP의 컬럼 순서

2.2.3 GROUPING

2.2.4 ROLLUP 컬럼의 선택

2.3 소계를 구하는 다른 방법

2.3.1 ROLLUP을 대신하는 방법

2.3.2 CUBE

2.3.3 GROUPING SETS

Chapter. 3 JOIN

3.1 INNER-JOIN

3.1.1 INNER-JOIN 이해하기

3.1.2 여러 테이블의 조인

3.1.3 잘 못 작성한 조인(M:1:M 조인)

3.1.4 RANGE-JOIN

3.2 OUTER-JOIN

3.2.1 OUTER-JOIN 이해하기

3.2.2 OUTER-JOIN의 필터 조건

3.2.3 실행이 불가능한 OUTER-JOIN

3.2.4 OUTER-JOIN이 포함된 여러 테이블의 조인

3.2.5 OUTER-JOIN의 응용

3.3 CARTESIAN-JOIN

3.3.1 CARTESIAN-JOIN 이해하기

3.3.2 CARTESIAN-JOIN의 위험성

3.3.3 분석마스터 만들기

3.3.4 테스트 데이터 만들기

Chapter. 4 유용한 SQL 문법

4.1 서브쿼리

4.1.1 서브쿼리의 종류

4.1.2 SELECT 절의 단독 서브쿼리

4.1.3 SELECT 절의 상관 서브쿼리

4.1.4 SELECT 절 서브쿼리 - 단일 값

4.1.5 WHERE 절 단독 서브쿼리

4.1.6 WHERE 절 상관 서브쿼리

4.2 MERGE

4.2.1 MERGE

4.2.2 MERGE를 사용한 UPDATE

4.3 WITH

4.3.1 WITH

4.3.2 WITH 절을 사용한 INSERT

PART. II 성능 테스트, 성능 개선을 위한 SQL 기술

Chapter. 5 성능 개선을 위한 기본 지식

5.1 실행계획

5.1.1 당부의 글

5.1.2 실행계획

5.1.3 실행계획 확인하기

5.1.4 실행계획의 순서

5.1.5 실제 실행계획 확인하기

5.2 성능 개선을 위한 최소한의 지식

5.2.1 옵티마이져(Optimizer)

5.2.2 소프트 파싱, 하드 파싱

5.2.3 IO(Input, Output)

5.2.4 블록(BLOCK)

5.2.5 논리적 IO와 물리적 IO

5.2.6 부분 범위 처리

Chapter. 6 INDEX

6.1 INDEX의 기본 개념

6.1.1 인덱스(INDEX)란?

6.1.2 인덱스의 종류

6.1.3 B*트리 구조와 탐색 방법

6.1.4 데이터를 찾는 방법

6.1.5 데이터를 찾는 방법 - 테이블 전체 읽기

6.1.6 데이터를 찾는 방법 - 인덱스를 이용한 찾기

6.1.7 INDEX RANGE SCAN VS. TABLE ACCESS FULL

6.2 단일 인덱스

6.2.1 단일 인덱스의 컬럼 정하기

6.2.2 단일 인덱스 VS. 복합 인덱스

6.3 복합 인덱스

6.3.1 복합 인덱스 - 컬럼 선정과 순서#1

6.3.2 복합 인덱스 - 컬럼 선정과 순서#2

6.3.3 복합 인덱스 - 컬럼 선정과 순서#3

6.3.4 복합 인덱스 - 컬럼 선정과 순서#4

6.4 인덱스의 활용

6.4.1 인덱스로 커버된 SQL

6.4.2 Predicate Information – ACCESS

6.4.3 너무 많은 인덱스의 위험성

6.4.4 인덱스 설계 과정

Chapter. 7 JOIN과 성능

7.1 조인의 내부적인 처리 방식

7.1.1 조인의 3가지 처리 방식

7.1.2 NESTED LOOPS JOIN

7.1.3 MERGE JOIN

7.1.4 HASH JOIN

7.2 NL 조인과 성능

7.2.1 성능 테스트를 위한 테이블 생성

7.2.2 후행 집합에 필요한 인덱스

7.2.3 선행 집합 변경에 따른 쿼리 변형

7.2.4 조인 횟수를 줄이자#1

7.2.5 조인 횟수를 줄이자#2

7.2.6 여러 테이블의 조인

7.2.7 과도한 성능 개선

7.2.8 선행 집합은 항상 작은 쪽이어야 하는가?

7.3 MERGE 조인과 성능

7.3.1 대량의 데이터 처리

7.3.2 필요한 인덱스

7.4 HASH 조인과 성능

7.4.1 대량의 데이터 처리

7.4.2 빌드 입력 선택의 중요성

7.4.3 대량의 데이터에만 사용할 것인가?

7.4.4 어떤 조인을 사용할 것인가?

PART. III 오픈, 훌륭한 마무리를 위한 SQL 기술

Chapter. 8 OLTP SQL 기술

8.1 트랜잭션

8.1.1 트랜잭션(Transaction)이란?

8.1.2 트랜잭션 테스트

8.1.3 트랜잭션 고립화 수준 – READ COMMITTED

8.2 락(LOCK)

8.2.1 락(LOCK)

8.2.2 SELECT~FOR UPDATE

8.2.3 대기(WAIT) 상태

8.2.4 데드락(DEAD-LOCK, 교착상태)

8.2.5 트랜잭션 최소화

8.2.6 방어 로직

8.2.7 불필요한 트랜잭션의 분리

8.3 문서번호 처리 기술

8.3.1 SELECT MAX 방식

8.3.2 SELECT MAX 방식의 성능

8.3.3 SELECT MAX 방식의 중복 오류

8.3.4 채번 테이블

8.3.5 채번함수

8.4 시퀀스와 ROWNUM

8.4.1 시퀀스 객체(Sequence Object)

8.4.2 잘못 활용한 시퀀스

8.4.3 최근 데이터를 가져오는 기술

Chapter. 9 분석함수

9.1 OVER 절

9.1.1 OVER 절 이해하기

9.1.2 분석 대상

9.1.3 OVER - PARTITION BY

9.1.4 OVER - ORDER BY

9.2 기타 분석함수

9.2.1 순위 분석함수

9.2.2 ROW_NUMBER

9.2.3 LAG, LEAD

9.3 분석함수를 대신하기

9.3.1 분석함수를 대신하는 방법

9.3.2 PARTITION BY를 대신하기

9.3.3 ROW_NUMBER를 대신하기

Chapter. 10 페이징 처리 기술

10.1 페이징 기술

10.1.1 페이징의 종류

10.1.2 DB 페이징

10.1.3 DB-INDEX 페이징

10.2 페이징 성능 높이기

10.2.1 페이징을 위한 카운트 처리

10.2.2 DB-INDEX 페이징의 성능 개선

10.2.3 DB-INDEX 페이징으로 유도하기

10.2.4 DB-INDEX 페이징의 한계

Chapter. 11 SQL 개발 가이드

11.1 WHERE 절 가이드

11.1.1 WHERE 절의 컬럼은 변형하지 않는다

11.1.2 날짜 조건 처리하기

11.1.3 조건 값은 컬럼과 같은 자료형을 사용한다

11.1.4 NOT IN 보다는 IN을 사용한다

11.1.5 불필요한 LIKE는 제거하자

11.2 불필요한 부분 제거하기

11.2.1 불필요한 COUNT는 하지 않는다

11.2.2 COUNT에 불필요한 부분은 제거한다

11.2.3 불필요한 컬럼은 사용하지 않는다

11.2.4 동일 테이블의 반복 서브쿼리를 제거하자

11.3 생각의 전환

11.3.1 사용자 함수 사용의 최소화

11.3.2 작업량을 줄이자

11.3.3 집계 테이블을 고민하자

 

https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=216383877

 

SQL BOOSTER

프로젝트 성공을 위한 SQL 필독서. 프로젝트를 진행하는 순서처럼 구성되어 있다. 프로젝트 투입을 위해 필요한 SQL 기술을 설명하고, 성능 테스트를 위해 필요한 기술을 설명한 뒤에 마지막으로

www.aladin.co.kr

 

감사합니다~!

+ Recent posts