성능이 느려지는 3가지 요인
- 동시 사용자 수의 증가
- 데이터 양의 증가
- 비효율적인 SQL문 작성
DB 성능 개선시 필요한 개초 개념
- 인덱스
- 실행 계획
그중에 SQL튜닝을 먼저 고려해야 한다
- SQL 튜닝을 제외한 나머지 방법은 추가적인 시스템을 구축해야 한다
- 조금 더 복잡해진 시스템구조로 관리비용과 금전적, 시간적 비용이 든다
- 하지만 SQL 튜닝은 기존의 시스템 변경없이 성능을 개선할 수 있다
- 근본적인 문제를 해결하는 방법은 SQL 튜닝

SQL 튜닝의 핵심
- 스토리지 엔진에서 데이터를 찾기 쉽게 바꾸기
- 스토리지 엔진으로부터 가져오는 데이터의 양 줄이기
가장 많이 활용되는 방법은 인덱스 활용이다.
-> 무작정 인덱스를 적용한다고 해서 해결되는게 아니라 적절하게 활용해야 한다
# 인덱스 생성
# CREATE INDEX 인덱스명 ON 테이블명 (컬럼명);
CREATE INDEX idx_age ON users(age);
# SHOW INDEX FROM 테이블명;
SHOW INDEX FROM users;
CREATE INDEX idx_부서_이름 ON users (부서, 이름);


주의점
- 멀티 컬럼 인덱스는 일반 인덱스처럼 사용할 수 있다
- 부서를 기준으로 정렬이 되어있고 부서내에서 이름이 정렬되어 있기 때문에 부서컬럼만 놓고 봤을때는 부서 인덱스와 동일한 정렬 상태를 갖고 있다
- 하지만 이름 컬럼은 인덱스처럼 사용할 수 없다. 이름 순으로 정렬이 되어있지 않기 때문에
- 따라서 멀티 컬럼 인덱스에서 일반 인덱스처럼 활용할 수 있는 건 처으멩 배치된 컬럼들뿐이다.
멀티컬럼 인덱스를 구성할 때 대분류 -> 중분류 -> 소분류 컬럼순으로 구성하기
- 어떻게 순서를 정하냐에 따라 성능차이가 난다
- 10층짜리 회사에서
박미나를 찾아야 한다고 가정해보면.박미나가 속한부서를 먼저 찾은뒤에부서에서박미나를 찾는게 편하다박미나를 먼저 찾고부서를 물어보는 건 오래 걸린다- 따라서 멀티 컬럼 인덱스를 구성할 때 데이터 중복도가 높은 컬럼이 앞쪽에 오는게 좋은 경우가 많다
select id, name from users
1. 실행계획
- 옵티마이저가 SQL문을 어떤 방식으로 어떻게 처리할지를 계획한걸 의미
- 이 실행계획을 보고 비효율적으로 처리하는 방식이 있는지 점검하고, 비효율적인 부분이 있다면 효율적인 방법으로 SQL문을 실행하게끔 튜닝을 하는 게 목표다
- rows랑 filtered는 정확한 값이 아니라 추정값이다
2. 실행 계획 조회하기
# 실행 계획 조회하기 EXPLAIN [SQL문] # 실행 계획에 대한 자세한 정보 조회하기 EXPLAIN ANALYZE [SQL문] # 예시 EXPLAIN SELECT * FROM users WHERE age = 23;
3. 실행 계획에 대한 자세한 정보 조회하기
EXPLAIN ANALYZE SELECT * FROM users WHERE age = 23;
- 밑에서부터 올라오면서 읽으면 된다
- actual time에서 앞쪽은 읽지말고 뒤쪽만 일거엇 users 풀스캔이 0.0502ms 걸렸다고 알면 된다
- 이 작업을 할때 접근할 데이터 수가 7개
- 그 다음 필터링을 한다는 것이다 0.0552는 전체 시간이다 필터링 시간자체는 0.0552 - 0.0502 하면 된다
1. ALL : 풀테이블 스캔
- 풀 테이블 스캔: 인덱스를 활용하지 않고 테이블을 처음부터 끝까지 뒤져서 데이터를 찾는 방식
- 처음부터 끝까지 다 뒤져서 필요한 데이터를 찾는 방식이다 보니 비효율적이다
2. Index: 풀 인덱스 스캔
- 풀 인덱스 스캔: 인덱스 테이블을 처음부터 끝까지 다 뒤져서 데이터를 찾는 방식
- 인덱스 테이블은 실제 테이블보다 크기가 작기 때문에 풀 테이블스캔보다 효율적이다
- 하지만 인덱스 테이블 전체를 읽어야 하기 때문에 아주 효율적이라고 볼 수 없다
3. Const: 1건의 데이터를 바로 찾을수 있는 경우
- 조회하고자 하는 1건의 데이터를 헤매지 않고 단번에 찾아올 수 있을 때 const 출력
- 고유 인덱스 또는 기본 키를 사용해서 1건의 데이터만 조회하는 경우 const가 출력
- 고유하다면 1건의 데이터를 찾는 순간 나머지 데이터는 볼 필요가 없어진다
4. Range: 인덱스 레인지 스캔
- 인덱스 레인지 스캔: 인덱스를 활용해 범위 형태의 데이터를 조회한 경우
- 범위형태: between, 부등호, in, like를 활용한 데이터 조회
- 효율적이지만 데이터를 조회하는 범위가 클 경우 성능 저하의 원인이 되기도 한다
5. Ref: 비고유 인덱스를 활용하는 경우
- 비고유 인덱스를 사용한 경우(UNIQUE가 아닌 컬럼의 인덱스를 사용한 경우)
- UNIQUE가 아니기 때문에 중복되는 값이 있을 수 있다
- 예를 들어 박지성을 찾는데 정렬은 되어있기 때문에 그 밑에 또 박지성이 있는 경우
컬럼을 가공하면 인덱스를 활용하지 않는다
# User000000으로 시작하는 이름을 가진 유저 조회 EXPLAIN SELECT * FROM users WHERE SUBSTRING(name, 1, 10) = 'User000000'; # 2달치 급여(salary)가 1000 이하인 유저 조회 SELECT * FROM users WHERE salary * 2 < 1000 ORDER BY salary;
그래서 컬럼을 가공하지 않아야한다
# User000000으로 시작하는 이름을 가진 유저 조회 EXPLAIN SELECT * FROM users WHERE name LIKE 'User000000%'; # 2달치 급여(salary)가 1000 이하인 유저 조회 EXPLAIN SELECT * FROM users WHERE salary < 1000 / 2 ORDER BY salary;
EXPLAIN SELECT * FROM users
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 DAY)
AND department = 'Sales'
ORDER BY salary
LIMIT 100;
SELECT p.id, p.title, p.created_at
FROM posts p
JOIN users u ON p.user_id = u.id
WHERE u.name = 'User0000046'
AND p.created_at BETWEEN '2022-01-01' AND '2024-03-07';
# 인덱스 추가
CREATE INDEX idx_name ON users (name);
CREATE INDEX idx_created_at ON posts (created_at);
# posts.created_at 인덱스를 추가하더라도 사용하는 게 비효율적이라고 판단되어서 사용하지 않았다
SELECT *
FROM users
WHERE salary = (SELECT MAX(salary) FROM users)
AND department IN ('Sales', 'Marketing', 'IT');
# department가 중복도도 높고 3개가 검색해야 되기 때문에 salary로 인덱스를 거는게 효과적이다
CREATE INDEX idx_salary ON users (salary);
SELECT u.*
FROM users u
JOIN (
SELECT department, MAX(salary) AS max_salary
FROM users
GROUP BY department
) d ON u.department = d.department AND u.salary = d.max_salary;
# department 그룹을 한 뒤에 최대연봉을 찾는거니까 멀티컬럼이 효과적이다
CREATE INDEX idx_department_salary ON users (department, salary);
# 인덱스를 가공해서 쓰고 있기 때문에 인덱스를 제대로 활용하지 못한다
SELECT *
FROM orders
WHERE YEAR(ordered_at) = 2023
ORDER BY ordered_at
LIMIT 30;
# 이런식으로 컬럼을 가공하지 않아야 인덱스를 제대로 활용한다
SELECT *
FROM orders
WHERE ordered_at >= '2023-01-01 00:00:00'
AND ordered_at < '2024-01-01 00:00:00'
ORDER BY ordered_at
LIMIT 30;
# 간단하게 주문 시간을 인덱스로 만들면 된다
CREATE INDEX idx_ordered_at ON orders (ordered_at);
# 성능향상 전 SQL문
SELECT
st.student_id,
st.name,
AVG(sc.score) AS average_score
FROM
students st
JOIN
scores sc ON st.student_id = sc.student_id
GROUP BY
st.student_id,
st.name,
sc.year,
sc.semester
HAVING
AVG(sc.score) = 100
AND sc.year = 2024
AND sc.semester = 1;
# having에 있는 조건은 where로 옮겨야 데이터를 적게 검색하기 때문에 성능이 향상된다
SELECT
st.student_id,
st.name,
AVG(sc.score) AS average_score
FROM
students st
JOIN
scores sc ON st.student_id = sc.student_id
WHERE
sc.year = 2024
AND sc.semester = 1
GROUP BY
st.student_id,
st.name
HAVING
AVG(sc.score) = 100;
# 성능개선 전 SQL
# join을 하기 위해서 post의 100만건 데이터 like의 100만건 데이터를 훑었다
# 200만건 이상의 데이터를 모았기 때문에 시간이 오래걸렸다
# 좋아요 많은수를 먼저 알고 실행하면 어떨까?
SELECT
p.id,
p.title,
p.created_at,
COUNT(l.id) AS like_count
FROM
posts p
INNER JOIN
likes l ON p.id = l.post_id
GROUP BY
p.id, p.title, p.created_at
ORDER BY
like_count DESC
LIMIT 30;
# likes 테이블로 먼저 좋아요순으로 정렬을 했다 (inner Join)
# 그러면 좋아요가 제일많은 30개가 나올테니까 그걸 가지고 기존에 있었던 테이블과 조인을 한다
# 이렇게 되면 조인이 되는 데이터 수가 확 줄어드니까 성능이 향상된다
SELECT p.*, l.like_count
FROM posts p
INNER JOIN
(SELECT post_id, count(post_id) AS like_count FROM likes l
GROUP BY l.post_id
ORDER BY like_count DESC
LIMIT 30) l
ON p.id = l.post_id;