하나의 이벤트에 대해 여러 공연 일정이 존재할 수 있으며, 같은 날에도 여러 시간대의 공연이 열릴 수 있습니다.
예를 들어, 다음과 같은 공연 일정을 가진다고 한다면
이 경우, 프론트엔드에서는 단순히 시간 단위가 아닌 날짜 단위(7.20 ~ 7.21)로 공연 기간을 표시하고자 합니다.
이를 위해 event_schedule
테이블의 event_date
를 DATE(event_date)
로 잘라 날짜별로 그룹핑한 후, 해당 날짜 중 가장 이른 공연 시간(MIN(event_date))을 함께 조회하는 방식으로 데이터를 구성하였습니다.
SELECT
DISTINCT event.event_id,
event.title,
event_stage.stage_name,
event.company_id,
event.admin_id,
MIN(event_schedule.event_date),
event.created_at
FROM
event
JOIN
event_schedule
ON event.event_id = event_schedule.event_id
AND event_schedule.deleted_at IS NULL
JOIN
event_stage
ON event_stage.stage_id = event.stage_id
WHERE
event.deleted_at IS NULL
GROUP BY
event.event_id,
event.title,
event_stage.stage_name,
event.company_id,
event.admin_id,
DATE(event_schedule.event_date);
id | select_type | table | type | rows | Extra |
---|---|---|---|---|---|
1 | SIMPLE | event_schedule | ALL | 199,644 | Using where; Using temporary |
1 | SIMPLE | event | eq_ref | 8 | Using where |
1 | SIMPLE | event_stage | ALL | 3 | Using where; Using join buffer |
event_schedule
테이블을 전체 스캔하고 임시 테이블을 사용하여 비효율적임이 나타남기존 쿼리는 다량의 데이터에 JOIN
, GROUP BY
, DATE()
함수, MIN()
집계를 한 번에 처리하면서 성능 병목이 발생했습니다.
이를 개선하기 위해 event_schedule
테이블의 집계를 서브쿼리로 분리하고,
deleted_at
, event_id
, event_date
에 복합 인덱스를 추가하여 성능을 향상시키고자 하였습니다.
SELECT
e.event_id,
e.title,
es.stage_name,
e.company_id,
e.admin_id,
esc.event_day,
esc.min_event_date
FROM
event e
JOIN
event_stage es ON e.stage_id = es.stage_id
JOIN
(
SELECT
event_id,
DATE(event_date) AS event_day,
MIN(event_date) AS min_event_date
FROM
event_schedule
WHERE
deleted_at IS NULL
GROUP BY
event_id,
DATE(event_date)
) esc ON e.event_id = esc.event_id
WHERE
e.deleted_at IS NULL;
CREATE INDEX idx_event_schedule_deleted_event_eventdate
ON event_schedule (deleted_at, event_id, event_date);
id | select_type | table | type | rows | Extra |
---|---|---|---|---|---|
1 | PRIMARY | ALL | 19,964 | ||
1 | PRIMARY | event | eq_ref | 8 | Using where |
1 | PRIMARY | event_stage | ALL | 3 | Using where; Using join buffer |
2 | DERIVED | event_schedule | ALL | 199,644 | Using where; Using temporary |
이전 쿼리에서는 GROUP BY DATE(event_date)
를 사용했지만, 이는 event_date
에 설정된 인덱스를 제대로 활용하지 못합니다.
즉, 함수가 컬럼에 직접 적용되면 MySQL은 해당 컬럼의 인덱스를 사용할 수 없기 때문에, 인덱스를 타지 못하고 전체 테이블 스캔이 발생합니다.
이를 해결하기 위해 날짜 단위로 그룹핑하지 않고, 최소값(MIN)과 최대값(MAX)을 사용해 전체 공연 기간을 파악하는 방식으로 구조를 변경하였습니다.
SELECT
e.event_id,
e.title,
es.stage_name,
e.company_id,
e.admin_id,
esc.min_event_date,
esc.max_event_date
FROM
event e
JOIN
event_stage es ON e.stage_id = es.stage_id
JOIN
(
SELECT
event_id,
MIN(event_date) AS min_event_date,
MAX(event_date) AS max_event_date
FROM
event_schedule
WHERE
deleted_at IS NULL
GROUP BY
event_id
LIMIT 0, 10
) esc ON e.event_id = esc.event_id
WHERE
e.deleted_at IS NULL;
id | select_type | table | type | key | rows | Extra |
---|---|---|---|---|---|---|
1 | PRIMARY | ALL | NULL | 11 | ||
1 | PRIMARY | event | eq_ref | PRIMARY, idx_event_id, idx_event_filtering | 8 | Using where |
1 | PRIMARY | event_stage | eq_ref | PRIMARY | 3 | Using where |
2 | DERIVED | event_schedule | ref | event_id, idx_event_schedule_deleted_event_eventdate | 9 | Using where; Using index |
event_schedule
테이블에서 복합 인덱스를 활용해 빠른 조회가 가능event
와 event_stage
는 기본키 조인으로 빠르게 연결이번 성능 개선 과정에서 실행 계획 분석과 쿼리 구조 변경이 얼마나 중요한지 체감했습니다.
Using temporary
, Using filesort
가 보일 때는 꼭 인덱스와 쿼리 구조를 점검해야 한다는 점을 배웠습니다.