MySQL 쿼리 튜닝 경험기 (1/2)

MySQL 쿼리 튜닝 경험기 (1/2)

Tags
데이터베이스
index
MySQL
Published
Jan 6, 2022
Property

개요

실무에서 서비스를 유지보수하면서 경험한 쿼리 튜닝에 대해 정리해보았다. 실제 유저가 없는 개인 프로젝트와는 달리 실무에서는 많은 데이터를 고려하여 쿼리를 효율적으로 설계하는 것이 중요하다. 본 글에서는 부하가 큰 쿼리들을 추려내고, 해당 쿼리들을 개발비용을 고려하여 튜닝하는 방법에 대하여 다루려고 한다.

문제 배경

  • 서버는 여러 개로 로드밸런싱이 되고 있었으나, DB는 1개로 DB 부하가 큰 상황이었다.
  • 데이터베이스를 scale up 해주는 방법도 있으나, 데이터베이스는 클라우드 중 비용이 매우 비싼 편이다. 돈으로 때우면 때울 수는 있지만 임시방편이다.
  • 서비스가 갑자기 터지면 대부분은 DB 관련 문제였다.
  • 점진적으로 적용 가능한 개선 방안을 찾다가, 시간과 개발 비용을 고려하여 쿼리튜닝을 적용해보기로 하였다.
 

쿼리 튜닝 대상의 우선순위를 정하자

  • 부하가 클 수록
    • 부하가 큰 쿼리는 DB가 죽는 주요 원인이다.
  • 자주 호출될 수록
    • 자주 호출되는 쿼리를 개선해줄 수록 사용자 경험을 개선할 수 있다.
  • 변경이 용이할 수록
    • 개발비용이 적은 튜닝이 좋다.
이러한 요소들을 잘 고려하여서 현실적으로 어떤 쿼리를 튜닝할지를 결정하자.
 
(팁) RDS에서는 Performance Insights를 제공한다. 병목현상이 발생하는 쿼리를 확인하는 데 유용하다.
notion image
 

MySQL EXPLAIN(실행계획)을 보자

튜닝할 쿼리를 정했다면, 해당 쿼리에 대한 EXPLAIN을 살펴보아야 한다.
MySQL Workbench를 활용하면 쉽게 EXPLAIN을 활용할 수 있다.
우선적으로는 Scan 방식과 Query cost에 집중하여 살펴보자.
notion image
자세한 방법은 아래의 글을 참조하면 좋다.
 

튜닝하기 전 알아야 할 것들

B-Tree 인덱스

MySQL은 기본적으로 B-Tree 인덱스 구조이다. 루트 노드, 브랜치 노드, 리프 노드, 데이터 파일로 나뉘어져 있는데, 쉽게 생각하면 메모리 계층 구조를 떠올리면 된다. 인덱스에 있으면 빨리 찾고, 디스크 저장소까지 뒤져야하면 느려진다. 당연히 자주 검색하는 조건을 인덱스에 두면 더 빠를 것이다.
notion image

인덱스

  • SELECT의 개선을 인덱스를 추가해주는 것으로 해결할 수 있다.
  • 물론, 인덱스를 무조건 추가하는 것이 답은 아니다. 인덱스의 추가는 command(insert, update, delete)의 성능 저하를 불러일으킨다. 데이터 양이 커질수록 인덱스의 영향은 커지므로 신중하게 판단하여 결정하여야 한다.
  • 인덱스의 갯수
    • jojoldu님의 글에 따르면, 일반적으로 3~4개 정도가 적절하다. 인덱스가 PK 외에 특별히 걸려있지 않았다면 추가해줄 수 있다. 특별히 검색에 자주 사용하게 되는 컬럼이 있다면 인덱스로 걸어줄 수 있다. (예: 주문 테이블의 상품 아이디)

테이블 스캔

인덱스를 잘 설정했다고 해서 끝이 아니다. 결과적으로 쿼리의 테이블 스캔이 효과적으로 이루어져야한다.
일반적으로 다음 순서대로 빠르며, 가능하면 쿼리가 윗순서의 type으로 스캔하도록 개선하는 것이 더 효율적이다.
  • index range scan
    • const(UNIQUE INDEX SCAN)
    • eq_ref, ref
    • range
  • index full scan
    • index
  • full table scan
    • ALL
EXPLAIN의 type을 참고하면 이 외에도 많은 종류의 스캔 방식이 존재한다.
 
실전적인 쿼리 튜닝 팁이 더 궁금하다면 아래 아티클들을 참고하면 많은 도움이 될 것이다.
MySQL 공식 문서도 친절한 편이니 함께 보면 좋다.
 

사례

💡
본 글의 테이블명, 컬럼명, 쿼리 일부는 실제와는 다르게 임의로 수정, 각색되어 있습니다.
코드를 뜯어보니 인덱스에 대한 개념 없이 코드가 짜여진 코드가 많았고, PK 외에 특별히 인덱스가 존재하지 않는 테이블이 많았다. AWS RDS Performance Insights에서의 데이터를 토대로 부하가 크거나 자주 호출되는 쿼리들부터 튜닝을 시작했다.
 

사례1: 주문 별 적립금 정보 호출 기능 개선.

  • 문제 상황
    • SELECT * FROM points WHERE order_no = ? AND user_id = ? AND point_status = 'N'
      points 테이블은 적립금 적립 내역과 함께 order_no(주문번호)를 함께 기록한다.
      마이페이지에서는 order_no를 기준으로 points 테이블에서 적립금 관련 정보를 출력해주고 있었고, 해당 쿼리의 부하가 매우 큰 상황이었다.
  • 해결책
    • points 테이블에서 검색 조건으로 가장 많이 사용되는 order_no를 Non-Unique Index로 추가해주었다.
  • 결과
    • Full Table Scan을 하던 쿼리가 Non-Unique Key Lookup으로, Query cost는 45262에서 1.2로 개선되었다.
 
notion image
notion image

사례2: 상품 옵션별 추가가격을 가져오는 기능 개선

  • 문제상황
    • SELECT * FROM goods_info WHERE goods_option='선택1_WHITE:230'
      상품 옵션별 추가가격을 가져오는 작은 기능의 쿼리이다. 상품 정보를 불러올 때 추가 옵션이 있는 경우라면 반드시 불러와지고 있었다.
      goods_no, goods_sub 별로 goods_option이 존재하는 구조인데 상품이 달라도 상품 옵션명이 같으면 그냥 가져와주세요~ 하는 쿼리이다. 어떻게 그 동안 아무 문제가 없었지?
  • 해결책
    • SELECT * FROM goods_info WHERE goods_no=391196 and goods_sub=0 and goods_option='선택1_WHITE:230'
      쿼리의 오류를 수정해주었고, goods_info 테이블에 goods_no, goods_sub 으로 다중 칼럼 PRIMARY 키를 걸어주었다. goods_no, goods_sub 은 UNIQUE하므로, 해당하는 레코드는 1건만 저장되어 있으므로, 즉시 스캔이 가능하다.
  • 결과
    • Full Table Scan을 하던 쿼리가 Unique Index Scan(const)로 Query cost는 180258에서 1로 쉽고 빠르게 개선되었다.
notion image
notion image

사례3: ORDER BY 개선

  • 문제 상황
    • SELECT * FROM board ORDER BY created_at DESC
      게시판에서 데이터를 시간 순으로 정렬하여 가져오고 있는데, ORDER BY의 조건으로 created_at을 이용하면서 Full Table Scan을 하고 있었다.
  • 해결책
    • SELECT * FROM board ORDER BY id DESC
      order by id 로 수정하여 인덱스를 타게 해주었다.
  • 결과
    • Query cost 상으로 큰 이득은 없었지만, Index Scan이 가능해졌다.
notion image
notion image

사례4: 이메일 중복 검색 기능 개선

  • 문제상황
    • SELECT count(*) FROM member WHERE email = ?
      회원 가입 시 이메일 정보 중복을 검사하는 기능이 굉장히 느렸다.
  • 해결책
    • 회원 테이블에서 email을 인덱스로 추가해주었다. email이 인덱스로 쓰여질 만큼 중요한 컬럼인지는 고민이 있었다. 그러나 회원 가입은 유저가 처음으로 이용하는 기능이어서 중요성이 높았기 때문에, 기존의 불필요한 인덱스들을 여러개 지우고 email 인덱스를 추가하는 것으로 결정했다.
       
  • 결과
    • Full Table Scan을 하던 쿼리가 Unique Index Scan(const)로 Query cost는 102358에서 1.2로 쉽고 빠르게 개선되었다.
      일부 관리자 기능 중 timeout이 발생하였으나, 불필요한 인덱스 정리 이후에는 문제가 발생하지 않았다.
      서비스가 더욱 커진다면 장기적으로는 큐를 이용하거나, 이메일 관련 테이블을 분리해주는 것으로 개선할 수 있을 것 같다.
notion image
notion image

마치면서

인덱스를 활용하여 매우 간단하게 병목 현상이 발생했던 몇몇 쿼리들을 개선하였다. 쿼리 개선 후 RDS t3.xlarge db의 수명이 좀 더 연장되었다. 시간이 지날 수록 데이터가 많아지면서 사이즈 업에 대한 필요성이 느껴졌는데, 잠깐은 버틸 수 있게 되었다.
2편에서는 가장 사용 빈도가 높고, 가장 부하가 큰 쿼리(일명 한방 쿼리)를 개선하는 내용을 다룰 것이다.
 

이미지 출처