MySQL에서 커버링 인덱스로 쿼리 성능을 높여보자!!

안녕하세요.  오늘 짧지만 재미있는 내용을 하나 공유할까 합니다.

커버링 인덱스(Covering Index)라는 내용인데, 대용량 데이터 처리 시 적절하게 커버링 인덱스를 활용하여 쿼리를 작성하면 성능을 상당 부분 높일 수 있습니다.

커버링 인덱스란?

커버링 인덱스란 원하는 데이터를 인덱스에서만 추출할 수 있는 인덱스를 의미합니다. B-Tree 스캔만으로 원하는 데이터를 가져올 수 있으며, 칼럼을 읽기 위해 굳이 데이터 블록을 보지 않아도 됩니다.

인덱스는 행 전체 크기보다 훨씬 작으며, 인덱스 값에 따라 정렬이 되기 때문에 Sequential Read 접근할 수 있기 때문에, 커버링 인덱스를 사용하면 결과적으로 쿼리 성능을 비약적으로 올릴 수 있습니다.

백문이 불여일견! 아래 테스트를 보시죠.

테이블 생성

먼저 다음과 같이 테이블을 생성합니다.

create table usertest (
 userno int(11) not null auto_increment,
 userid varchar(20) not null default '',
 nickname varchar(20) not null default '',
 .. 중략 ..
 chgdate varchar(15) not null default '',
 primary key (userno),
 key chgdate (chgdate)
) engine=innodb;

약 1,000만 건 데이터를 무작위로 넣고 몇가지 테스트를 해봅니다.

커버링 인덱스(SELECT)

select chgdate , userno
from usertest
limit 100000, 100
************* 1. row *************
           id: 1
  select_type: SIMPLE
        table: usertest
         type: index
possible_keys: NULL
          key: CHGDATE
      key_len: 47
          ref: NULL
         rows: 9228802
        Extra: Using index
1 row in set (0.00 sec)

쿼리 실행 계획의 Extra 필드에 “Using Index” 결과를 볼 수 있는데, 이는 인덱스만으로 원하는 데이터 추출을 하였음을 알 수 있습니다.

이처럼 데이터 추출을 인덱스에서만 수행하는 것을 커버링 인덱스라고 합니다. 아시겠죠? ^^

그렇다면 일반 쿼리와 성능 테스트를 해볼까요?

커버링 인덱스(WHERE)

1) 일반 쿼리

select *
from usertest
where chgdate like '2010%'
limit 100000, 100

쿼리 수행 속도는 30.37초이며, 쿼리 실행 계획은 다음과 같습니다.

************* 1. row *************
           id: 1
  select_type: SIMPLE
        table: usertest
         type: range
possible_keys: CHGDATE
          key: CHGDATE
      key_len: 47
          ref: NULL
         rows: 4352950
        Extra: Using where

Extra 항목에서 “Using where” 내용은, Range 검색 이후 데이터는 직접 데이터 필드에 접근하여 추출한 것으로 보면 됩니다.

2) 커버링 인덱스 쿼리

select a.*
from (
      select userno
      from usertest
      where chgdate like '2012%'
      limit 100000, 100
) b join usertest a on b.userno = a.userno

쿼리 수행 시간은 0.16초이며 실행 계획은 다음과 같습니다.

************* 1. row *************
           id: 1
  select_type: PRIMARY
        table:
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 100
        Extra:
************* 2. row *************
           id: 1
  select_type: PRIMARY
        table: a
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: b.userno
         rows: 1
        Extra:
************* 3. row *************
           id: 2
  select_type: DERIVED
        table: usertest
         type: range
possible_keys: CHGDATE
          key: CHGDATE
      key_len: 47
          ref: NULL
         rows: 4352950
        Extra: Using where; Using index

Extra 에서 “Using Index”를 확인할 수 있습니다.

그렇다면 30초 넘게 수행되는 쿼리가 0.16초로 단축됐습니다. 왜 이렇게 큰 차이가 발생했을까요?

첫 번째 쿼리는 Where에서 부분 처리된 결과 셋을 Limit 구문에서 일정 범위를 추출하고, 추출된 값을 데이터 블록에 접근하여 원하는 필드를 가져오기 때문에 수행 속도가 느립니다.

두 번째 쿼리에서도 동일하게 Where에서 부분 처리된 결과 셋이 Limit 구문에서 일정 범위 추출되나, 정작 필요한 값은 테이블의 Primary Key인 userno 값입니다. InnoDB에서 모든 인덱스 Value에는 Primary Key를 값으로 가지기 때문에, 결과적으로 인덱스 접근만으로 원하는 데이터를 가져올 수 있게 됩니다. 최종적으로 조회할 데이터 추출을 위해서 데이터 블록에 접근하는 건 수는 서브 쿼리 안에 있는 결과 갯수, 즉 100건이기 때문에 첫 번째 쿼리 대비 월등하게 좋은 성능이 나온 것입니다.

커버링 인덱스(ORDER BY)

커버링 인덱스를 잘 사용하면 Full Scan 또한 방지할 수 있습니다. 대부분 RDBMS에는 테이블에 대한 통계 정보가 있고, 통계 정보를 활용해서 쿼리 실행을 최적화 합니다.

다음 재미있는 테스트 결과를 보여드리겠습니다. 전체 테이블에서 chgdate 역순으로 400000번째 데이터부터 10 건만 가져오는 쿼리입니다.

1) 일반 쿼리

select *
from usertest
order by chgdate
limit 400000, 100
************* 1. row *************
           id: 1
  select_type: SIMPLE
        table: usertest
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 9228802
        Extra: Using filesort
1 row in set (0.00 sec)

분명 인덱스가 있음에도, Full Scan 및 File Sorting이 발생합니다. 인덱스를 태웠을 때 인덱스 블록을 읽어들이면서 발생하는 비용보다 단순 Full Scan이 더 빠르다고 통계 정보로부터 판단했기 때문이죠. 인덱스도 데이터라는 것은 항상 기억하고 있어야 합니다^^

결과 시간은 책정 불가입니다. (안끝나요~!)

2) 커버링 인덱스 쿼리

위 결과와 다르게 커버링 인덱스는 조금 더 재미있는 결과를 보여줍니다.

select a.*
from (
      select userno
      from usertest
      order by chgdate
      limit 400000, 100
) b join usertest a on b.userno = a.userno
************* 1. row *************
           id: 1
  select_type: PRIMARY
        table:
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 100
        Extra:
************* 2. row *************
           id: 1
  select_type: PRIMARY
        table: a
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: b.userno
         rows: 1
        Extra:
************* 3. row *************
           id: 2
  select_type: DERIVED
        table: usertest
         type: index
possible_keys: NULL
          key: CHGDATE
      key_len: 47
          ref: NULL
         rows: 400100
        Extra: Using index

File Sorting이 발생하지 않고 커버링 인덱스가 사용되었으며, 실행 시간 또한 0.24초로 빠르게 나왔습니다.^^

Conclusion

커버링 인덱스는 InnoDB와 같이 인덱스와 데이터 모두 메모리에 올라와 있는 경우에 유용하게 쓰일 수 있습니다. 물론 커버링 인덱스가 좋기는 하지만, 커버링 인덱스를 사용하기 위해 사용하지 않는 인덱스를 주구장창 만드는 것은 최대한 피해야 하겠죠^^

잊지마세요. 인덱스도 데이터라는 사실을..

 

 

디스크 병목 현상에 따른 DB 성능 리포트

Overview

어느 시스템에서도 병목 현상은 어딘가에 있습니다. DBMS 또한 CPU, Memory, Disk로 구성된 하나의 시스템이기 때문에 당연히 특정 구역에서 병목현상이 발생할 수 있죠. 오늘은 Disk에서 발생하는 병목에 관해서 말씀드리겠습니다.

Memory Processing

디스크 배열(RAID)에 따른 DB 성능 비교 에서, 메모리가 충분하면 아래와 같다는 그래프를 보여드렸습니다.

InnoDB Buffer Pool : 12G
InnoDB Buffer Pool : 12G

어떤 경우든 메모리에만 연산이 가능하다면, CPU자원을 거의 활용 가능하다고 할 수 있죠. 그러나 만약 디스크 I/O가 발생하는 순간부터 CPU는 전혀 연산하지 않는 현상이 발생합니다. 바로 디스크 I/O Wait 으로 인한 시스템 병목 현상 발생입니다.

Benchmark

대용량 환경에서 MySQL 성능을 분석하기 위해 2억 데이터 Insert 테스트를 수행하였습니다. (하단 결과에서는 1.3억 데이터까지만 추출하여 그래프를 만들었습니다.)

하단은 테스트를 위해 사용한 테이블 스키마입니다. (간단한 테스트입니다.) 30개 Thread로 동시 Insert 작업을 수행하였고, 건 수에 따라 QPS및 시스템 현황을 살펴보았습니다.

##테이블 정의
CREATE TABLE `test` (
`NO` int(11) NOT NULL AUTO_INCREMENT,
`EMAIL` varchar(128) DEFAULT NULL,
`NAME` varchar(32) NOT NULL,
PRIMARY KEY (`NO`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
PARTITION BY KEY ()PARTITIONS 3;

##테이블 포멧 변경
TRUNCATE TABLE TEST;
ALTER TABLE TEST ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=4;

Benchmark Result

버퍼풀 4G 환경 하의 일반 테이블과 압축 테이블 성능 비교 및 버퍼풀 8G 환경 하에서의 성능 비교입니다.
압축 테이블의 경우 생각보다 많은 CPU 작업이 필요하게 되는데요, 메모리에 모든 데이터가 존재한다면 이 자체가 병목입니다.
하지만 데이터가 급증하여 DISK I/O이슈가 발생하면 반대로 CPU 유휴 자원을 사용하여 병목을 줄일 수 있습니다.

디스크 압축에 따른 QPS 비교
디스크 압축에 따른 QPS 비교
디스크 압축에 따른 DISK I/O 비교
디스크 압축에 따른 DISK I/O 비교

7000만 건 데이터(데이터파일 약 5G) Insert까지는 일반 테이블이 압도적인 성능을 보여줍니다.  7000만 건 이상 데이터 Insert에서는 1/4로 압축한 테이블 성능이 꾸준히 유지됩니다. 9000만 건 데이터 Insert부터 1/4 압축 테이블도 성능 저하가 발생합니다.

가장 괄목할만한 사항은 DISK I/O가 급증하는 시점부터는 CPU자원은 거의 사용하지 않는다는 점입니다.

Conclusion

디스크 병목을 풀기 위해서 더욱 빠른 디스크로 고도화하여 풀 수 있는 방법이 있습니다. 다른 방법은 어차피 사용하지 못할 CPU자원이라면, CPU자원을 일부 소모하더라도 압축 테이블로 저장하여 디스크 병목을 해결할 수 있습니다.

앞선 테스트 결과가 반드시 정답은 아닙니다. 무조건 압축 테이블만이 대안은 아닙니다. 하지만 서비스 특성에 맞게 테스트 시나리오를 작성하고 병목을 사전에 최대한 줄이는 활동이 필요합니다.

무조건 고 사양의 서버로 구성하기 보다는 조금만 데이터를 다른 시각으로 설계한다면, 동일 자원에서 최적의 퍼포먼스를 발휘할 수 있을 것입니다.

동일한 내용은 제가 “파란 개발자 블로그”에 작성한 디스크 병목 현상에 따른 DB 성능 리포트 에서도 확인할 수 있습니다.^^

디스크 배열(RAID)에 따른 DB 성능 비교

Overview

MySQL DBMS 하드웨어 구성 시 어떠한 정책으로 움직이는 것이 가장 효율적일지, 메모리/디스크 설정을 변경하여 테스트를 진행하였습니다. 디스크는 RAID 레벨을 변경하였고, innodb_buffer_pool을 조정함으로써 메모리 환경을 구성하였습니다. 서비스 특성에 따라 하드웨어 구성을 달리함으로써, 장비를 더욱더 효율적으로 사용할 수 있을 것으로 기대됩니다.^^

디스크배열(RAID)란?

RAID란 Redundant Array of Inexpensive Disks의 약자로 디스크를 여러장 묶어서, 데이터 중복성 및 성능 향상을 유도할 수 있는 기법입니다. RAID 기법은 참으로 많이 있으나, 일반적으로 실무에서는 RAID0, RAID1, RAID5, RAID10또는 RAID01을 많이 사용합니다.

더욱 상세한 내용은 하단 블로그를 참조하시면 되겠습니다.
http://blog.naver.com/jevida?Redirect=Log&logNo=140118943472

Benchmark 

테스트 환경은 다음과 같습니다.

  • CPU
    – Intel(R) Xeon(R) CPU E5630  @ 2.53GHz Quad * 2EA
  • Memory(InnoDB Buffer Pool)
    – 12G : 데이터 100%가 메모리 안에 존재 (DISK I/O 없음)
    – 8G : 데이터 80% 가 메모리 안에 존재 (DISK I/O 일부 발생)
    – 4G : 데이터 40% 미만 메모리 안에 존재 (DISK I/O 다량 발생)
  • DISK RAID
    – RAID1 : 데이터/로그 디스크 공유
    – RAID1(2) : 데이터/로그 디스크 분리
    – RAID5 : 데이터/로그 디스크 공유
    – RAID10 : 데이터/로그 디스크 공유
  • 데이터 사이즈
    – 50,000,000건 (11G)
  • 동시 접속 수
    – 1, 5, 10, 15, 20, 30, 100
  • 트랜잭션 구성
    – READ-ONLY : 14 Queries (–oltp-read-only –oltp-skip-trx)
    – READ/WRITE : 19 Queries (–oltp-test-mode=complex)

Benchmark Result

  1. Innodb_Buffer_Pool Size : 12G
    테스트 결과 모두 비슷한 성능을 보여주나 100쓰레드에서는 raid1(2) 시 일부 성능이 저하되었습니다. (iblog에서 데이터 저장 공간에 적용 시 물리적 장치 분리에 의한 성능 저하 요인으로 파악되네요.)

    InnoDB Buffer Pool : 12G
    InnoDB Buffer Pool : 12G
  2. Innodb_Buffer_Pool Size : 8G
    READ/WRITE에서는 디스크 배열에 따라 상당한 성능 차이를 보이는데, Buffer-Pool안의 데이터가 디스크로 Flush 되면서 발생하는 데서 기인한 듯 하고, RAID10이 가장 안정적인 성능을 보여줍니다.

    InnoDB Buffer Pool : 8G
    InnoDB Buffer Pool : 8G
  3. Innodb_Buffer_Pool Size : 4G
    가장 재미있는 결과입니다. 디스크 의존도가 높기 때문에 당연히 Raid-10이 가장 우수합니다. 그 다음으로는 Raid-5, Raid-1 순으로 성능을 보입니다.

    InnoDB Buffer Pool : 4G
    InnoDB Buffer Pool : 4G

Conclusion

위 결과를 바탕으로 다음과 같이 결론짓고 싶습니다.

  • 예산이 허용한다면, 다수의 디스크를 사용한 Raid-10으로 구성
  • 빈번하게 사용되는 데이터가 메모리에 80% 이상 존재한다면 Raid-1도 괜찮은 성능을 보임 (특히 Read 시)
  • 데이터 변경 작업이 많은 DB인 경우 반드시 Raid5또는 Raid10으로 구성
  • 성능 확장에는 메모리 투자가 제일 유리

메모리에 데이터가 모두 존재할 수 있다면, CPU성능에 따라 TPS 결과가 나옵니다. 4000이상 TPS가 나오는 경우 CPU idle이 거의 0% 까지 떨어지기 때문에, 여기서부터는 CPU투자에 따라 결과가 좌우되겠죠.

동일한 내용은 제가 “파란 개발자 블로그”에 작성한 DISK Raid 구성에 따른 DB 성능 비교 분석 에서도 확인할 수 있습니다.^^

감사합니다.