Tag Archives: Performance

MySQL InnoDB에서 데이터 1건 변경 시에도 테이블 잠금 현상이 발생할 수 있다!!

Overview

MySQL 트랜잭션 Isolation Level로 인한 장애 사전 예방 법” 포스팅에서 관련 주제를 다룬 적이 있습니다. InnoDB에서 Create Table .. As Select .. 과 같이 사용하는 경우 테이블 잠금이 발생할 수 있는 상황과 회피할 수 있는 팁이었죠.

테이블 Full-Scan 구문이 실행 시 발생할 수 있는 문제에 관한 것입니다. 하지만 때로는 변경 대상이 1 건이라도 쿼리 타입에 따라 테이블 잠금 같은 현항이 발생할 수도 있습니다.

이번 포스팅에서는 이에 관해 정리해보도록 하겠습니다. ^^

Symptoms

다음과 같이 “NOT IN” 과 같이 부정적인 의미의 조건 구문을 사용 시 테이블 잠금 현상이 발생할 수 있습니다. NOT IN 결과 값이 1건일지로도요..

update test set col1 = 0
where col1 not in (0, 1);

이 경우 Read Committed 로 트랜잭션 Isolation Level을 설정해도 인덱스 칼럼 유무에 따라 테이블 잠금 현상이 발생하기도 하죠. 물론 Repeatable Read보다는 경우의 수가 크지는 않습니다.

현재 트랜잭션 Isolation Level은 다음과 같이 확인 할 수 있습니다.

mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)

Prepare Test

1) 테스트 환경 구성

## 테이블 생성
create table test(
  i int unsigned not null auto_increment,
  j int unsigned not null,
  k int unsigned not null,
  l int unsigned not null,
  primary key(i)
) engine = innodb;

## 타겟 칼럼과 관계 없는 인덱스 생성
alter table test add key(l);

타겟 칼럼에 인덱스를 추가하는 경우 다음과 같이 수행합니다.

## 타겟 칼럼에 인덱스 생성
alter table test add key(j);

2) 데이터 생성

빠른 데이터 생성을 위해 아래와 같이 Insert Into .. Select.. 구문을 사용합니다.

## 데이터 1 건 생성
insert into test (j, k, l)
values
(
    crc32(rand()) mod 2,
    crc32(rand()) mod 2,
    crc32(rand()) mod 2
);

## 하단 쿼리 17건 실행
insert into test (j, k, l)
select
    crc32(rand()) mod 2,
    crc32(rand()) mod 2,
    crc32(rand()) mod 2
from test;

3) 타겟 데이터 설정

타겟 데이터와 무관한 데이터를 구분하기 위해 i 값을 10000 이상인 데이터에 한에서 랜덤하게 업데이트 수행합니다.

## 타겟 칼럼1
update test set j = 99
where i > 10000
order by rand()
limit 1000;

## 비 타겟 칼럼1 - 인덱스 없음
update test set k = 99
where i > 10000
order by rand()
limit 1000;

## 비 타겟 칼럼2 - 인덱스 있음
update test set l = 99
where i > 10000
order by rand()
limit 1000;

Test Scenario

테스트는 Transaction Isolation Level(Repeatable Read, Read Committed), 인덱스 칼럼 여부, 데이터 칼럼 접근 방식(PK, Index, Full-Scan) 그리고 쿼리 타입(“in”, “not in”)을 구분하여 각각 테스트하였습니다.

원활한 테스트를 위해 모든 세션에 자동 커밋 DISABLE 설정합니다.

SET SESSION AUTOCOMMIT = 0;

Transaction Isolation 레벨은 테스트 상황에 맞게 다음과 같이 설정합니다.

## REPEATABLE READ 인 경우 테스트 환경
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;

## READ COMMITTED 인 경우 테스트 환경
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;

세션1

변경 대상 데이터는 i 값이 10,000보다 큰 행입니다. 하단 결과 테이블에 있는 쿼리를 질의문으로 수행한 상태에서 세션2 쿼리를 실행합니다.

세션2

i 값이 10,000 이하인 데이터 타겟 데이터과 관계없는 데이터를 업데이트 수행합니다.

update test set j = 1 where i = 1;

Test Result

테스트 결과를 캡쳐해서 알아보기는 힘들지만, 다양한 케이스를 테스트하여 정리합니다. ^^

1) “NOT IN” 테스트

  • Repeatable Read : Not In  사용 시 모든 경우 Lock 발생
  • Read Committed : 인덱스(PK 포함)를 사용하여 접근 시 Lock 발생

not in result

2) “IN” 테스트

  • Repeatable Read : 풀스캔 시 Lock 발생, 인덱스(PK포함) 접근 시 Lock 발생 안함
  • Read Committed : 풀스캔, 인덱스 스캔 모두 Lock 발생 안함

in test result

Solutions

1) Transaction Isolation Level 변경 & IN 구문 사용

Transaction Isolation Level을 Read Committed로 변경하고, IN 구문을 사용하도록 합니다. Not In 사용이 반드시 필요하다면 Limit 구문을 통해 한번에 많은 데이터 변경 작업이 일어나지 않도록 유도합니다.

2) Select 후 결과 값으로 데이터 접근

NOT IN 구문이 필요하다면, Select 구문으로 따로 구분하고 그 결과 값으로 데이터를 다시 업데이트합니다.  MySQL의 Repeatable Read에서는 스냅샷 형태로 Select 데이터 일관성을 보장하며, 데이터 업데이트와 무관한 Select 결과 값에는 다른 세션에서 데이터 변경 작업이 가능합니다.

Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(
                   "select i from test where j not in (0, 1)"
               );
int i = 0;
String inStr = "";
while(rs.next()){
    inStr += ((i++) > 0 ? "," : "") + rs.getInt(1);
}
rs.close();
stmt.executeUpdate(
    "update i set j = 0 where i in (" + inStr + ")"
);

Conclusion

복잡한 위의 결과를 간단하게 정리하면 다음과 같습니다.

  1. 트랜잭션 Isolation Level이 Read Committed더라도 NOT IN 과 같은 부정적인 조건 질의 시 인덱스 칼럼으로 데이터에 접근하는 상황에서는 Lock이 발생합니다.
  2. 트랜잭션 Isolation Level이 Read Committed인 상태에서 IN 구문과 같은 긍정적인 조건 질의 시에는 연관없는 데이터에는 Lock을 걸지 않습니다.
  3. 트랜잭션 Isolation Level이 Repeatable Read인 경우 NOT IN 사용 시 무조건 테이블 Lock 현상이 발생합니다.
  4. 트랜잭션 Isolation Level이 Repeatable Read인 상태에서 IN 구문을 사용하는 경우에는  인덱스 칼럼으로 접근 시에는 Lock 현상이 발생하지 않습니다.

가장 안정적으로 Lock을 피하기 위해서는 트랜잭션 Isolation Level을 Read Committed로 설정하고, 긍적적인 조건만 주는 경우입니다.

물론 인덱스가 있는 상황이라면, 부정적인 조건 사용 시에도 데이터 접근이 빠르므로, 변경 대상 데이터가 많거나 트랜잭션이 오래 유지되지 않는 한 큰 문제는 없습니다. ^^

단, Read Committed로 변경 시 Binary Log 포멧이 자동으로 ROW 형태로 변경되기 때문에, Binary Log 파일이 급격하게 늘어날 수 있는 경우가 발생할 수 있으니, 내부 검토가 반드시 필요합니다.

사용할수록 나날이 새로운 사실이 발견되는 MySQL이네요.

감사합니다.

서비스 로직 흐름을 변경하여 DB 튜닝을 해보자!

Overview

오래전 메가존(현 혜택존) 서비스를 담당하던 시기, 포인트 관련된 부분에서 심각한 성능 저하 및 유효성 취약 문제가 발생하였습니다. 장비 고도화를 통해 관련 문제를 해결하기에 앞서, 서비스 로직 및 테이블 재구성을 통한 최적화 작업을 통해 문제를 해결하였습니다.

이에 관해 간단하게 소개하도록 하겠습니다.

Problems

다음과 같이 크게 두 가지 문제가 있었습니다.

  1. 성능 이슈
    1. 로그 테이블은 거대한 한 개의 테이블로 구성
    2. 데이터 누적에 따라 성능이 급격하게 저하
  2. 유효성 이슈
    1. 자바 어플리케이션에서만 유효성 체크 – 비 정상적인 사용 존재
    2. 포인트가 현금처럼 사용될 수 있으므로 반드시 필요함

Solutions

1) 파티셔닝을 통한 성능 최적화

오라클 엔터프라이즈 버전에서는 테이블 파티셔닝을 제공하지만, 아쉽게도 오라클 스탠다드에서는 관련 기능이 없습니다. 즉, 어플리케이션 레벨에서 적당히 데이터 분산을 유도하는 방법 밖에는 없습니다. 데이터를 분산하는 방법으로는 여러가지가 존재하겠지만, 저는 월별로 테이블을 분산 저장하는 방식을 사용하였습니다.

매월 하단과 같은 스키마의 테이블을 MCHIP”YYYYMM” 형식으로 생성을 합니다.

물론 해당 월 다음 달로 생성을 해야겠죠?

 Name            Type
 --------------- ------------
 SEQ             NUMBER
 USER_NO         VARCHAR2(12)
 POINT           NUMBER
 ISSUE_DATE      DATE

그리고 인덱스 필요 시 데이터 테이블스페이스와는 “물리적으로 분리”된 전용의 테이블스페이스에 생성하여 DISK I/O 비효율도 최소화 유도하였습니다.

데이터 조회는 기본적으로 월별로만 가능하며, 최근 3개월 동안만 조회할 수 있도록 하였고, 필요 시 과거 테이블을 DROP하는 방식으로 변경하였습니다. 만약 최근 3개월 데이터가 필요할 시에는 “UNION ALL” 구문으로 데이터를 가져왔습니다.

다음은 쿼리를 월별로 생성하여 데이터를 저장하는 간단한 자바 프로그램입니다. 저는 SpringFramework 환경에서 구현했었는데, 그것보다는 알아보기 쉬운 단순 자바 프로그램으로 보여드리는 것이 좋을 것 같습니다. (설마 아래 있는 것을 그대로 쓰시는 것은 아니겠죠? ㅎㅎ)

import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test {
    public static void main(String[] args) {
        DateFormat df = new SimpleDateFormat("yyyyMM");
        Date date = new Date();

        // 현재 기준 년월 
        String yyyymm = df.format(date).toString();

        // 쿼리 생성
        String sql = "INSERT INTO MCHIP"+yyyymm+" \n" +
                "(SEQ, USER_NO, POINT, ISSUE_DATE) \n" +
                "VALUES \n" +
                "(SEQ_MCHIP.NEXTVAL, ?, ?, SYSDATE)";

        System.out.println(sql);
    }
}

위와 같이 쿼리를 생성하고, 원하는 테이블에 선별적으로 데이터를 넣습니다. 조회 시에도 위와 같은 방식으로 쿼리를 생성합니다. 만약 한달 이상의 데이터가 필요하다면 UNION ALL로 쿼리를 붙여서 데이터를 조회하면 되겠죠. ^^

2) 트리거를 사용한 유효성 체크

포인트 내역을 저장하는 테이블이 있었다면, 포인트 현황을 조회하기 위한 전용 테이블 또한 존재했습니다. 포인트 현황에 저장된 점수가 사용자가 현재 소지하고 있는 포인트이며, 이를 차감하여 서비스에서 사용하는 방식으로 사용하고 있었습니다.

개인 당 한 건의 데이터만 있기 때문에, 사용자 데이터를 조회하는 것에는 큰 무리가 없었지만, 유효성을 자바 어플리케이션에서만 체크했었기 때문에 여기저기 헛점이 많은 상태였습니다. 물론 내구성이 뛰어나게 개발되었다면 큰 문제는 없었겠지만, 협력사가 시간에 쫓겨서 급하게 개발한 산출물인지라.. 아무래도.. ^^;;

자바 소스 전체를 뒤져서 모든 유효성을 체크하는 것은 거의 무리에 가까웠고, 그래서 제가 채택한 방식은 트리거였습니다. 트리거를 사용하여 사용자 포인트 유효성 여부를 DB레벨에서 체크해서 어플리케이션과 분리하자는 의도였죠.

MCHIP201209 테이블에 포인트가 내역이 들어가는 동시에 유효성 체크 후 정상적이면 포인트 현황 테이블에 반영하는 트리거입니다.

CREATE OR REPLACE TRIGGER TRG01_MCHIP201209
BEFORE INSERT
ON MCHIP201209
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
DECLARE
  POINT NUMBER;
BEGIN
  /*** 현재 사용자 포인트 조회 ***/
  SELECT POINT INTO POINT FROM MCHIP_INFO
  WHERE USER_NO = :NEW.USER_NO;
  … 중략…

  /*** 포인트 유효성 체크  ***/
  IF POINT - :NEW.POINT < 0 THEN
    RAISE_APPLICATION_ERROR(-20001,'Not enough point!!');
  END IF;

  /*** 포인트 현황 업데이트 ***/
  UPDATE MCHIP_INFO SET POINT = POINT + POINT_NEW
  WHERE USER_NO = :NEW.USER_NO;
END;
/

Conclusion

오래전에 성능을 최적화한 내용을 정리하였습니다.

당시 서버에 과부하로 인하여 서버 고도화 및 장비 도입을 위한 투자를 검토 중이었으나, 서비스 로직 재구성 이후 서버가 안정적이었기 때문에 기존 장비로 서비스를 진행하였습니다. 게다가 현재 기존 장비 사용률이 CPU 기준 80~90%였던 상황이 10~20%로 유지되는 쾌거를 거두었죠.

데이터 처리량을 최소로 유도하는 것이 성능 최적화의 첫걸음이라는 것을 느낀 경험이었습니다.

좋은 포스팅으로 다시 찾아뵐께요^^;

MySQL 성능 최적화를 위한 몇 가지 팁!!

Overview

트위터에서 우연히 성능 관련 가벼운 아는척(?)을 시작으로 일이 커지고 말았네요. ^^;; 성능 관련된 트윗을 보고 몇 가지 코멘트만 한다는 것이.. ㅎㄷㄷ한 멘션이 되고 말았습니다.

그래서 부족하나마, MySQL 성능 최적화 시 “본능적”으로 이행하는 몇 가지를 정리해보겠습니다.

Global Variable

성능과 연관이 되는 몇 가지 파라메터 변수는 반드시 체크를 하시기 바랍니다. MySQL에서 주로 InnoDB를 사용하는 상태라면 innodb_buffer_pool_size, innodb_log_file_size,  innodb_log_files_in_group, innodb_flush_log_at_trx_commit, innodb_doublewrite, sync_binlog 정도가 성능에 직접적인 영향을 미치는 요소라고 볼 수 있습니다.

  • innodb_buffer_pool_size
    InnoDB에게 할당하는 버퍼 사이즈로 50~60%가 적당하며, 지나치게 많이 할당하면 Swap이 발생할 수 있습니다.
  • innodb_log_file_size
    트랜잭션 로그를 기록하는 파일 사이즈이며, 128MB ~ 256MB가 적당합니다.
  • innodb_log_files_in_group
    트랜잭션 로그 파일 개수로  3개로 설정합니다.
  • innodb_flush_log_at_trx_commit
    서비스 정책에 따라 다르게 설정하겠지만, 저는 일반적으로 2값으로 세팅합니다.
    – 0: 초당 1회씩 트랜잭션 로그 파일(innodb_log_file)에 기록
    – 1: 트랜잭션 커밋 시 로그 파일과 데이터 파일에 기록
    – 2: 트랜잭션 커밋 시 로그 파일에만 기록, 매초 데이터 파일에 기록
  • innodb_doublewrite
    이중으로 쓰기 버퍼를 사용하는지 여부를 설정하는 변수로 활성화 시 innodb_doublewrite 공간에 기록 후 데이터 저장합니다. 저는 활성화합니다.
  • sync_binlog
    트랜잭션 커밋 시 바이너리 로그에 기록할 것인지에 관한 설정이며, 저는 비활성 처리합니다.

참고로 innodb_buffer_pool_size를 32G  메모리 서버에서 24G로 할당한 적이 있는데, SQL트래픽이 많아짐에 따라 Swap이 발생하더군요. 버퍼풀에는 대략 한 시간 정도 Active한 데이터와 인덱스를 담을 수 있는 사이징이라면 적절할 것 같습니다.

sync_binlog는 binlog 파일에 매 트랜잭션마다 기록할 것인지를 설정하는 파라메터인데, BBWC 혹은 FBWC이 없다면 활성화를 권고하지 않습니다. (개인적으로 경험해본 바에 따르면 on/off에 따라서 10~20배 정도 차이가 나기도 하더군요.)

Session Variables

MySQL은 단일 쓰레드에서 Nested Loop 방식으로 데이터를 처리합니다. 물론 5.6 버전부터는 조인 알고리즘이 몇가지 더 추가되기는 하지만, 여전히 미흡하죠. 결국 SQL처리 시 일시적으로 사용하는 Temporary Table이 디스크에 사용되지 않도록 유도하는 것이 제일 중요한 것 같습니다.

먼저 mysqladmin 유틸리티로 현재 Temporary Table 현황을 모니터링 하도록 합니다. 매 초마다 Status 차이를 보여주는 명령어이며, Created_tmp_files 이 꾸준히 많다면 tmp_table_size를 늘려줄 필요가 있습니다. Global Variable 에 설정하는 것보다는 필요 시 Session Variable로 설정하는 것을 권고 드립니다.

mysqladmin -uroot -p extended-status -r -i 1 | grep -E 'Created_tmp|--'

통계성 쿼리 질의 전 아래와 같이 세션 변수 설정(2G로 할당)을 한 후 진행하면 한결 빠르게 쿼리가 처리됩니다.

set session tmp_table_size = 2 * 1024 * 1024 * 1024;
set session max_heap_table_size = 2 * 1024 * 1024 * 1024;

하지만 이것은 어디까지나 디스크 접근을 줄이기 위한 목적이므로, 쿼리 자체를 수정하거나 다른 접근 방법으로 데이터를 처리하는 것이 가장 확실한 방법일 것입니다.

만약 Create Table As Select 혹은 Insert into .. Select 구문을 자주 사용하여 통계 데이터를 입력한다면 Transaction Isolation을 READ-COMMITTED로 변경하시기 바랍니다. 구문 실행 도중 Lock이 발생할 수 있기 때문이죠. ^^

예전에 포스팅한 MySQL 트랜잭션 Isolation Level로 인한 장애 사전 예방 법을 참고하세요.

Schema & SQL

MySQL에서는 서버 변수보다는 Schema와 SQL 특성에 큰 영향을 받는 DBMS 입니다. 일단 서버 설정이 기본적인 사이징정도로만 구성되면, 그 이후로는 Schema와 SQL이 DB특성에 맞게 작성되었는지 여부가 성능에 가장 큰 요소가 됩니다.

MySQL 특징은 예전 포스팅 ““을 참고하시면 되겠습니다.

1) Schema

InnoDB를 주로 사용한다는 가정 하에 말씀 드리겠습니다.

InnoDB 는 Primary Key 순으로 데이터가 저장됩니다. Primary Key가 Rowid처럼 사용되는 것이죠. 만약 Primary Key가 무작위로 입력되는 테이블이라면, 테이블에 데이터가 누적됨에 따라 성능도 비례하게 떨어지게 됩니다. Primary Key는 순차적으로 저장되도록 하며, 만약 구조 상 여의치 않다면 테이블 파티셔닝을 통해서 데이터 파일 사이즈를 최소로 유지하시기 바랍니다.

mysql secondary index

그리고  Secondary Index는 위 그림처럼 Primary Key를 Value 로 가집니다. 모든 Secondary Index 에는 Primary Key를 가지기 때문에 Primary Key의 데이터 타입이 전체 인덱스 사이즈에 큰 영향을 미칩니다.

InnoDB에서는 경우에 따라서, 인덱스가 실 데이터보다 더 큰 경우가 자주 있습니다. 그러한 경우가 있는지를 확인하고, 인덱스 사이즈를 최대한 줄이는 것이 성능상 좋습니다. 간단한 테이블 조회 쿼리입니다.

SELECT
    CONCAT(TABLE_SCHEMA,'.',TABLE_NAME) TABLE_NAME,
    CONCAT(ROUND(TABLE_ROWS/1000000,2),'M') ROWS,
    CONCAT(ROUND(DATA_LENGTH/(1024*1024),2),'M') DATA,
    CONCAT(ROUND(INDEX_LENGTH/(1024*1024),2),'M') IDX,
    CONCAT(ROUND((DATA_LENGTH+INDEX_LENGTH)/(1024*1024),2),'M') TOTAL_SIZE,
    ROUND(INDEX_LENGTH/DATA_LENGTH,2) IDXFRAC,
    ENGINE
FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_SCHEMA NOT IN ('mysql','information_schema', 'performance_schema')
ORDER BY DATA_LENGTH+INDEX_LENGTH DESC;

문자열 인덱스라면 Trigger + CRC32로 사이즈를 줄일 수 있습니다. 트리거에서 Insert혹은 Update가 발생하면 CRC32 함수로 문자열을 Unsigned Int 타입으로 변환하여 특정 칼럼에 저장하고, 해당 칼럼을 인덱스 필드로 사용하는 것입니다.

간단한 트리거 예제입니다. Insert 관련이며, Update 는 비슷하게 정의하시면 되겠죠. ^^

CREATE TRIGGER trg_test_insert
BEFORE INSERT ON test
FOR EACH ROW
BEGIN
  SET NEW.str_crc = CRC32(LOWER(TRIM(NEW.str)));
END$$

질의는 다음과 같이 합니다. 대략 1/43억 확률로 중복 데이터가 발생할 수 있으나, str값을 다시 조회 조건으로 주기 때문에 정상적인 데이터만 가져옵니다.

SELECT * FROM test
WHERE str = 'abcdefg'
AND str_crc = CRC32(LOWER(TRIM('abcdefg')));

이같이 쓰는 이유는 문자열 칼럼에 인덱스를 제거하기 위함이니 헷갈리시면 안됩니다!!

트리거 활용을 잘 하면 DB 자원을 많이 잡아먹는 성능 취약 요소를 최소화할 수 있습니다. 예를 들어 특정 통계 데이터가 자주 필요한 경우라면, 매번 Group By 질의를 하지 않고 트리거로 통계 테이블에 데이터를 적용하면서 수행하는 것도 한가지 방안입니다. 그렇다고 무조건 트리거를 맹신해서는 안됩니다. ^^;; 트리거는 최대한 단순하게 필요한 만큼만!!

2) SQL

SQL 관련은 문제 발생 요소가 너무도 다양해서 자세하게 설명하기가 어렵습니다. 먼저 예전 포스팅 ““를 참고하세요.

추가로 몇 가지 설명 더 드리겠습니다. 일단 다음과 같은 쿼리 습관은 좋지 않습니다.

SELECT * FROM userinfo
WHERE id IN (SELECT id FROM userinfo_log 
             WHERE reg_date > '2012-09-09');

서브 쿼리 실행 후 WHERE 조건을 수행하는 것이 아닌, 매번 데이터를 Nested Loop 탐색을 하면서 서브쿼리를 수행하기 때문에 불필요한 부하가 발생합니다.예전 포스트 ” Maria 1탄 – MySQL의 쌍둥이 형제 MariaDB를 소개합니다.“에 관련된 내용이 있으니 한번 읽어보세요. ^^

또한 다음과 같은 쿼리 습관은 최대한 피하시기 바랍니다.

SELECT ..
FROM (
    SELECT .. FROM .. WHERE ..
) a
INNER JOIN (
    SELECT .. FROM .. WHERE ..
) b ON a.col = b.col;

두 개의 서브 쿼리가 Temporary Table로 내부적으로 처리되면서, 두 테이블 간 풀 스캔이 발생합니다. Nested Loop방식으로 발생하는 풀 스캔은 시스템 성능에 엄청난 타격을 주므로, 테이블 구조를 잘 파악해서 인덱스를 잘 활용할 수 있도록 쿼리를 이쁘게(?) 작성하세요. (중간 테이블이 1만건씩이면 두 개 테이블 연산에는 1억 번의 연산이 필요합니다. ㅎㄷㄷ;;)

불필요한 Left조인이 있는지, 혹은 지나치게 서브 쿼리를 사용하는지, Select 조건에 들어간 칼럼이 반드시 필요한 데이터들인지, 커버링 인덱스를 사용할 수 있는 지 등여러 가지가 있겠습니다만, 다 언급하기에는 한계가 있네요. ^^

한가지 기억하실 것은 MySQL은 단일 쓰레드에서 Nested Loop 방식으로 데이터를 처리하므로, DB 가 처리할 데이터를 최소화 유도해야 한다는 것입니다.

Conclusion

계획없이 작성한 포스팅인만큼 여기저기 부족하고 보완해야할 부분이 여기저기 많습니다. 그치만 MySQL DB 성능을 최적화한다면, 살펴봐야할 몇 가지라는 생각이 들어서 간단하게나마 정리하였습니다. 성능에 직접적인 영향을 주는 환경 변수만 정책에 맞게 설정을 한 후에는 테이블 구조와 SQL을 MySQL 특성에 맞게 작성을 한다면 가시적인 효과를 빠르게 볼 수 있을 것 같네요. ^^

감사합니다.