인덱싱을 통한 성능 개선
0. 테스트 환경
서버 : Spring Boot + JPA + MySQL
테스트 도구
MySQL CLI (쿼리 성능 확인)
테스트 내용
✔️쿼리 실행 시간 비교
✔️더미 데이터로 인덱스 추가 전후 쿼리 성능 테스트
테스트 한 API
예약 가능한 좌석 조회 API
동시 예약 요청 처리 API
테스트 데이터 수 ⇒ (더 많은 데이를 넣을 경우 시간이 너무 오래걸림..)
user = 1,005
concert = 1,000
concert_schedule = 3,990
seat = 119,700
reservation = 1,000
1. 기본 기능 쿼리에 대한 인덱스
a. 예약 가능한 좌석 조회 (인덱스 생성)
인덱스가 필요한 이유
1️⃣잦은 조회와 데이터 양 증가
예약 가능한 좌석 조회는 가장 자주 호쵤되는 쿼리 중 하나이며, 매번 전체 테이블을 스캔하면 성능 저하가 발생될 수 있다
추가적으로 동시 예약 요청이 많은 콘서트 시스템의 경우, 데이터 양이 많아질수록 응답 시간이 길어지게 된다
2️⃣ WHERE 조건의 다중 컬럼 사용
WHERE concert_id = ? AND schedule_date = ? AND status = ?concert_id, schedule_date, status는 예약 가능한 좌석 조회 시 항상 사용하는 필터 조건이고,
해당 조건들이 인덱스 없이 검색 될 경우 Full Table Scan이 발생한다
3️⃣콘서트 상태 값 처리 시에도 사용
콘서트 SoldOut 처리 시에도 그룹핑하여 사용하기 때문에 다른 쿼리에 비해 효율이 좋다
인덱스 생성
CREATE INDEX idx_seat_concert_schedule_status ON seat (concert_id, schedule_date, status);
인덱스 적용 이유
☑️Cardinality(선택도) 고려
concert_id와 schedule_date는 다양한 값을 가지며, 선택도가 높기 때문에 다른 컬럼에 비해인덱스 효율 좋다
concert_id⇒ 많은 값이 존재 (선택도가 높다)schedule_date⇒ 날짜별로 정렬될 수 있음 (검색 속도가 빠름)
반면
status는AVAILABLE,RESERVED등 값이 적어 선택도가 낮지만,concert_id와schedule_date로 대부분의 필터링이 이러우진 이후에status로 추가 필터링하므로 효율적일 수 있다따라서 Cardinality가 높은
concert_id와schedule_date를 먼저 배치하고, 이후status를 마지막에 배치
💡잘못된 인덱스 설계 예시
CREATE INDEX idx_seat_status_schedule ON seat (status, schedule_date);비효율적인 이유
status는 값이 AVAILABLE, RESERVED 등 몇 개 안 됨 → 선택도가 낮음schedule_date는 많이 사용되지만, 첫 번째 컬럼이 status일경우 인덱스를 제대로 활용 못 함
인덱스 적용 전
EXPLAIN SELECT * FROM seat WHERE concert_id = 351 AND schedule_date = '2025-04-08' AND status = 'AVAILABLE';
EXPLAIN ANALYZE SELECT * FROM seat WHERE concert_id = 351 AND schedule_date = '2025-04-08' AND status = 'AVAILABLE';
Table Scan 발생 ⇒ 전체 테이블 탐색하면서 데이터를 찾는다
데이터가 많아질수록 O(n) 시간 소요
cost=12014 rows=119100 ⇒ 쿼리가 너무 많은 데이터를 훑어본다 (낮은 효율성)
인덱스 적용 후
EXPLAIN SELECT * FROM seat WHERE concert_id = 351 AND schedule_date = '2025-04-08' AND status = 'AVAILABLE';
EXPLAIN ANALYZE SELECT * FROM seat WHERE concert_id = 351 AND schedule_date = '2025-04-08' AND status = 'AVAILABLE';
인덱스 추가 전후 차이
실행 계획
Full Table Scan
Index Scan
쿼리 실행 시간 (actual time)
32.4ms
0.02ms
행 스캔 (rows)
119,280
1
b. (단일) 좌석 조회 (인덱스 생성 안함)
인덱스가 필요한 이유
1️⃣id는 자주 사용하는 기본키 기반 조회 컬럼
예약 가능한 좌석 조회는 가장 자주 호쵤되는 쿼리 중 하나이며, 매번 전체 테이블을 스캔하면 성능 저하가 발생될 수 있음
추가적으로 인기있는 콘서트에서, 인기있는 좌석의 경우에는 seat 테이블에 데이터가 많을 경우 모든 좌석 데이터를 순차적으로 조회(Full Table Scan)해야하기 때문에 성능 저하가 발생
2️⃣ 특정 좌석이 예약되었는지 확인 할 때도 사용(임시 예약, 결제 요청 시)
MySQL의
FOR UPDATE를 사용하여, 행 수준 잠금(Row Lock)을 사용 할 경우 인덱스가 없다면 전체 테이블을 스캔하여 조건에 맞는 행을 찾고 잠그기 때문에 Full Table Scan이 발생
❗MySql 에서 기본키는 자동으로 클러스터형 인덱스(Clustered Index)가 생성되므로, 별도의 인덱스를 생성 할 필요가 없다. (혹시나.. id가 기본키가 아니라면..ㅎㅎ.. 생성 필요)
2. 기본 기능 외 지연 발생할 수 있는 쿼리
a. 사용자가 최근 30일간 결제한 모든 내역 조회
인덱스가 필요한 이유
1️⃣잦은 조회와 데이터 양 증가
사용자별 결제 내역은 자주 조회되는 쿼리 중 하나이며, 예약 테이블(reservation)이 커질수록 성능 저하 발생 가능
최근 30일 데이터를
paid_at을 기준으로 필터링하는데, 인덱스가 없으면 Full Table Scan이 발생할 수 있다
2️⃣ 사용자 기준 데이터 조회 최적화
WHERE user_id = ? AND paid_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)user_id와 paid_at을 조합한 필터링이므로, 복합 인덱스가 없으면 Full Scan 발생 가능
3️⃣정렬 및 필터링 최적화
결제 내역을 paid_at 기준으로 정렬할 가능성이 높으므로, 해당 인덱스가 정렬 성능까지 개선 가능
인덱스 생성
CREATE INDEX idx_reservation_user_paid ON reservation (user_id, paid_at);
인덱스 적용 이유
☑️Cardinality(선택도)
user_id⇒ 사용자의 예약 내역을 빠르게 찾기 위해 필요paid_at⇒ 날짜 범위를 기준으로 필터링을 최적화하기 위해 필요
인덱스 적용 전
EXPLAIN SELECT r.id, r.price, r.paid_at, s.seat_number, c.id AS concert_id FROM reservation r JOIN seat s ON r.seat_id = s.id JOIN concert c ON s.concert_id = c.id WHERE r.user_id = 1001 AND r.paid_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);
EXPLAIN ANALYZE SELECT r.id, r.price, r.paid_at, s.seat_number, c.id AS concert_id FROM reservation r JOIN seat s ON r.seat_id = s.id JOIN concert c ON s.concert_id = c.id WHERE r.user_id = 1001 AND r.paid_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);
Table Scan (r 테이블) 발생 ⇒ 전체 테이블 탐색하면서 데이터를 찾는다
데이터가 많아질수록 O(n) 시간 소요
인덱스 적용 후
EXPLAIN SELECT r.id, r.price, r.paid_at, s.seat_number, c.id AS concert_id FROM reservation r JOIN seat s ON r.seat_id = s.id JOIN concert c ON s.concert_id = c.id WHERE r.user_id = 1001 AND r.paid_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);
EXPLAIN ANALYZE SELECT r.id, r.price, r.paid_at, s.seat_number, c.id AS concert_id FROM reservation r JOIN seat s ON r.seat_id = s.id JOIN concert c ON s.concert_id = c.id WHERE r.user_id = 1001 AND r.paid_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);
인덱스 추가 전후 차이
실행 계획
Full Table Scan
Index Scan
쿼리 실행 시간 (actual time)
0.46ms
0.0194ms
행 스캔 (rows)
1,000
1
b. 특정 (인기) 콘서트 좌석 현황을 한 페이지에서 모두 조회
인덱스가 필요한 이유
1️⃣조회 데이터 양 증가
인기 콘서트의 좌석 정보를 한 번에 불러와야 하며, 좌석(seat) 테이블이 커질수록 성능 저하 발생 가능
좌석 상태(status)에 따라 필터링하는 경우도 고려 대상이 된다
2️⃣ WHERE 조건의 다중 컬럼 사용
WHERE concert_id = ? AND schedule_date BETWEEN ? AND ?concert_id와 schedule_date를 동시에 조회하므로, 복합 인덱스를 활용해야 성능 최적화 가능
인덱스 생성
CREATE INDEX idx_seat_concert_schedule_status ON seat (concert_id, schedule_date, status);
인덱스 적용 이유
☑️Cardinality(선택도) 고려
concertId→ 콘서트별 좌석을 필터링하는데 필수적scheduleDate→ 콘서트 일정별 좌석을 조회하는 데 필요status→ AVAILABLE 또는 RESERVED 등 상태별 필터링 시 속도를 최적화
❗ 해당 인덱스는 이미 1-a에서 적용되어있다
인덱스 적용 전
EXPLAIN SELECT s.id, s.seat_number, s.status, s.price FROM seat s WHERE s.concertId = 351 AND s.scheduleDate BETWEEN '2025-03-01' AND '2025-03-07' ORDER BY s.scheduleDate;
EXPLAIN ANALYZE SELECT s.id, s.seat_number, s.status, s.price FROM seat s WHERE s.concertId = 351 AND s.scheduleDate BETWEEN '2025-03-01' AND '2025-03-07' ORDER BY s.scheduleDate;
인덱스 적용 후
EXPLAIN SELECT s.id, s.seat_number, s.status, s.price FROM seat s WHERE s.concertId = 351 AND s.scheduleDate BETWEEN '2025-03-01' AND '2025-03-07' ORDER BY s.scheduleDate;
EXPLAIN ANALYZE SELECT s.id, s.seat_number, s.status, s.price FROM seat s WHERE s.concertId = 351 AND s.scheduleDate BETWEEN '2025-03-01' AND '2025-03-07' ORDER BY s.scheduleDate;
인덱스 추가 전후 차이
실행 계획
Full Table Scan
Index Range Scan
쿼리 실행 시간 (actual time)
40ms
0.161ms
행 스캔 (rows)
119,700
30
c. 특정 사용자의 예약 및 결제 내역 통계 조회
인덱스가 필요한 이유
1️⃣자주 실행되는 통계 쿼리
특정 사용자의 예약 내역을 통계
(COUNT(*), SUM(price))로 조회하는 경우 성능 저하 발생 가능예약 테이블이 커질수록 필터링과 집계 연산이 느려질 수 있다
2️⃣ WHERE 조건의 다중 컬럼 사용
WHERE user_id = ? AND paid_at IS NOT NULL특정 사용자 + 결제 완료된 예약을 필터링해야 하므로, 복합 인덱스가 필요하
3️⃣집계 연산 성능 최적화
GROUP BY user_id를 수행할 경우, 인덱스를 활용하면 불필요한 테이블 스캔을 줄일 수 있다
인덱스 생성
CREATE INDEX idx_reservation_user_paid_status ON reservation (user_id, paid_at, status);
인덱스 적용 이유
☑️Cardinality(선택도)
user_id→ 특정 사용자의 예약 데이터를 빠르게 찾기 위해 필요paid_at→ 결제된 예약 내역을 필터링하는 데 최적화status→ 결제 완료(PAID), 예약 취소(CANCELLED) 등 상태별 필터링 가능
인덱스 적용 전
EXPLAIN SELECT r.user_id, COUNT(r.id) AS total_reservations, SUM(r.price) AS total_spent FROM reservation r WHERE r.user_id = 1001 AND r.paid_at IS NOT NULL GROUP BY r.user_id;
EXPLAIN ANALYZE SELECT r.user_id, COUNT(r.id) AS total_reservations, SUM(r.price) AS total_spent FROM reservation r WHERE r.user_id = 1001 AND r.paid_at IS NOT NULL GROUP BY r.user_id;
인덱스 적용 후
EXPLAIN SELECT r.user_id, COUNT(r.id) AS total_reservations, SUM(r.price) AS total_spent FROM reservation r WHERE r.user_id = 1001 AND r.paid_at IS NOT NULL GROUP BY r.user_id;
EXPLAIN ANALYZE SELECT r.user_id, COUNT(r.id) AS total_reservations, SUM(r.price) AS total_spent FROM reservation r WHERE r.user_id = 1001 AND r.paid_at IS NOT NULL GROUP BY r.user_id;
인덱스 추가 전후 차이
실행 계획
Full Table Scan
Index Range Scan
쿼리 실행 시간 (actual time)
0.403ms
0.0183ms
행 스캔 (rows)
1,000
1
d. 가장 많이 예약된 콘서트 조회
인덱스가 필요한 이유
1️⃣콘서트 별 예약 건수 집계 속도 개선
예약 가능한 좌석 조회는 가장 자주 호쵤되는 쿼리 중 하나이며, 매번 전체 테이블을 스캔하면 성능 저하가 발생될 수 있다
추가적으로 동시 예약 요청이 많은 콘서트 시스템의 경우, 데이터 양이 많아질수록 응답 시간이 길어지게 된다
2️⃣ 대량 데이터 그룹화에 대한 죄적화
WHERE concert_id = ? AND schedule_date = ? AND status = ?concert_id, schedule_date, status는 예약 가능한 좌석 조회 시 항상 사용하는 필터 조건이고,
해당 조건들이 인덱스 없이 검색 될 경우 Full Table Scan이 발생한다
3️⃣콘서트 상태 값 처리 시에도 사용
콘서트 SoldOut 처리 시에도 그룹핑하여 사용하기 때문에 다른 쿼리에 비해 효율이 좋음
인덱스 생성
CREATE INDEX idx_reservation_concert ON reservation (concert_id);
인덱스 적용 이유
☑️예약 테이블이 커질수록 성능 저하 가능
콘서트별 예약 건수를 집계(COUNT(*))하여 가장 인기 있는 콘서트를 찾는 쿼리이다.
예약 데이터가 많아질수록 GROUP BY concertId 연산이 부담될 수 있음
☑️WHERE 조건이 없지만 GROUP BY 최적화 필요
concertId를 기준으로 집계해야 하므로, 이 컬럼에 대한 인덱스가 있으면 성능 개선 가능
인덱스 적용 전
EXPLAIN SELECT r.concertId, COUNT(r.id) AS total_reservations FROM reservation r GROUP BY r.concertId ORDER BY total_reservations DESC LIMIT 10;
EXPLAIN ANALYZE SELECT r.concertId, COUNT(r.id) AS total_reservations FROM reservation r GROUP BY r.concertId ORDER BY total_reservations DESC LIMIT 10;
인덱스 적용 후
EXPLAIN SELECT r.concertId, COUNT(r.id) AS total_reservations FROM reservation r GROUP BY r.concertId ORDER BY total_reservations DESC LIMIT 10;
EXPLAIN ANALYZE SELECT r.concertId, COUNT(r.id) AS total_reservations FROM reservation r GROUP BY r.concertId ORDER BY total_reservations DESC LIMIT 10;
인덱스 추가 전후 차이
실행 계획
Temporary Table 사용 + Full Table Scan
Covering Index Scan
쿼리 실행 시간 (actual time)
0.472ms
0.296ms
행 스캔 (rows)
1,000
1000 (Covering Index)
3. 기대 효과 및 최종 결론
1. 검색 성능 향상
인덱싱을 통해 데이터가 미리 구조화되므로, 사용자가 검색할 때 옵티마이저가 최적으로 데이터셋에서 빠르게 결과 도출
전체 테이블 스캔(O(n)) 방식→ B-트리 기반 검색O(log n)으로 전환
2. 시스템의 전반적인 개선
주요 쿼리에서 높은 Cardinality를 가진 컬럼에 인덱스를 집중적으로 적용
사용자 잔액 조회, 콘서트 스케줄 조회, 좌석 조회 등 빈번하게 호출되는 쿼리의 처리 속도가 향상
Last updated