DB 성능 저하의 원인은 크게 3가지가 있다.
DB 성능을 개선하는 방법에도 여러가지가 있다.
SQL 튜닝
을 먼저 고려해야 하는 이유는 뭘까?성능 개선을 위해서는 MySQL 구조 파악이 필요하다. 전체 구조를 파악하여 어디서 성능이 저하되는지, 주된 원인이 무엇인지 파악할 수 있기 때문이다.
MySQL은 엔진 플러그인 구조
이다. 따라서 SQL 레이어와 스토리지 레이어를 분리해서, 여러 종류의 스토리지 엔진을 사용할 수 있도록 설계되어 있다.
아키텍처를 보면 다음과 같다.
MYSQL 엔진 (SQL 레이어)
파싱
은 SQL 레이어에서 가장 먼저 수행되는 단계로 SQL 문장이 문법적으로 맞는지 검사하고, 논리적 구조(파스 트리)로 변환한다.옵티마이저
는 비용 기반으로 실행계획을 세운다.스토리지 엔진 (데이터 레이어)
대표적인 스토리지 엔진:
InnoDB
: 기본엔진, 트랜잭션, 외래키, ACID를 지원한다.MyISAM
: 예전 기본 엔진, 빠르지만 트랜잭션 및 락을 지원하지 않는다. MySQL 성능 문제의 대부분은 "스토리지 엔진에서 데이터를 느리게 가져오는 경우"에 발생한다.
이런 병목을 줄이기 위해서는:
인덱스 생성
: 자주 사용되는 조건 컬럼에 적절한 인덱스 설정커버링 인덱스
: 쿼리 결과가 전부 인덱스에 있으면 디스크 접근 없이 처리 가능EXPLAIN/ANALYZE
를 통한 실행 계획 분석 및 인덱스 활용 여부 확인결국, 효율적인 인덱스 설계와 옵티마이저가 인덱스를 잘 쓰게 만드는 쿼리 작성이 SQL 튜닝의 핵심이다.
데이터를 빨리 찾기 위해 특정 컬럼을 기준으로 미리 정렬해 놓은 표라고 생각하면 편하다.
CREATE TABLE users(
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100),
age INT
);
SELECT * FROM users;
-- 높은 재귀 횟수를 허용하도록 설정
-- 더미데이터 개수와 맞춰서 작성하면
SET SESSION cte_max_recursion_depth = 1000000;
INSERT INTO users(name, age)
WITH RECURSIVE cte (n) AS
(
SELECT 1
UNION ALL
SELECT n+1 FROM cte WHERE n <1000000
) -- 생성하고 싶은 더미데이터 갯
SELECT
CONCAT('User', LPAD(n,7,'0')), -- 'User' 다음에 7자리 숫자로 구성된 이름 생성
FLOOR(1 + RAND() * 1000) AS age -- 1부터 1000 사이의 랜덤 값으로 나이 생성
FROM cte;
SELECT * FROM users
WHERE age=23;
인덱스를 사용하기 전과 후의 쿼리 조회 시간의 변화를 통해 인덱스를 활용한 쿼리 성능 향상이 가능하다는 것을 알 수 있다.
CREATE INDEX idx_age ON users(age);
SHOW INDEX FROM users;
SELECT * FROM users
WHERE age=23;
PK
도 인덱싱의 한 종류이기 때문에 PK에는 인덱스가 기본적으로 적용된다. 클러스터링 인덱스
라고도 부른다.
InnoDB 테이블은 반드시 하나의 클러스터형 인덱스를 가지며, PK가 없을 때는 UNIQUE NOT NULL
제약 조건이 붙은 인덱스 중 하나를 자동으로 선택한다.
그것도 없다면 내부적으로 숨겨진 rowid
컬럼을 생성해 클러스터 인덱스로 활용한다.
UNIQUE 옵션을 설정하면 인덱스가 자동으로 같이 생성되기 때문에 조회 성능 향상된다. 이런 특징 때문에 고유 인덱스(Unique Index)
라고도 부른다.
멀티 컬럼 인덱스란, 2개 이상의 컬럼을 묶어서 설정하는 인덱스를 뜻한다. 즉, 데이터를 빨리 찾기 위해 2개 이상의 컬럼을 기준으로 미리 정렬해놓은 표이다.
멀티 컬럼 인덱스의 특징을 이해하고 생성해야 효과적으로 사용할 수 있다.
(이름, 부서)
의 순서대로 멀티 컬럼 인덱스를 구성했다면 먼저 일치하는 이름
을 찾은 뒤, 일치하는 이름
에서 부서
를 찾는 식으로 처리한다. 따라서 멀티 컬럼 인덱스를 구성할 때는 데이터 중복도가 낮은(카디널리티가 높은) 컬럼이 앞쪽으로 오는 게 좋은 경우가 많다.
(항상 그런 건 아니니 실행 계획과 SQL문 실행 속도를 측정해서 판단하도록 하자.)
SQL문을 실행시킬 때 필요한 모든 컬럼을 갖고 있는 인덱스를 커버링 인덱스(Covering Index)라고 한다.
인덱스를 사용하면 데이터를 조회할 때의 성능이 향상된다. 하지만 무분별한 인덱스의 사용은 오히려 쓰기 작업(삽입, 수정, 삭제)의 성능을 저하시킬 수 있다.
인덱스를 추가한다는 건 인덱스용 테이블이 추가적으로 생성된다는 뜻이다. 따라서 쓰기 작업을 하게 되면 원장 테이블과 인덱스용 테이블 모두에 데이터를 넣어야 한다. 인덱스의 개수가 많아지면 많아질 수록 작업을 해야하는 테이블은 더 많아지기 때문에 성능은 더 느려질 수 밖에 없다.
인덱스가 있는 컬럼이라도 해당 컬럼을 가공해서 조건에 사용하면 인덱스를 사용할 수 없고, 전체 스캔이 일어나 오히려 성능 저하를 유발할 수 있다.
변경 전 - 컬럼 가공
-- 인덱스가 있는 created_at 컬럼
CREATE INDEX idx_users_created_at ON users(created_at);
-- 인덱스가 사용되지 않는 경우
SELECT * FROM users
WHERE DATE(created_at) = '2025-07-22';
변경 후 - 함수나 연산을 우변으로 이동하거나 범위 조건으로 바꿔야 함
-- 인덱스 사용 가능 (created_at에 가공 없음)
SELECT * FROM users
WHERE created_at >= '2025-07-22 00:00:00'
AND created_at < '2025-07-23 00:00:00';
위에서 인덱스를 알아봤다. 대충 인덱스를 왜 사용해야 하고 인덱스를 사용했을때 성능이 좋아진다는 것을 시각적으로 확인할 수 있었다.
다음 포스팅에서는 실행 계획
을 사용하여 쿼리 성능을 향상하는 방법을 알아보자.