회사에서 코드리뷰를 하던 중, STRAIGHT_JOIN 의 사용기준 에 대해 사수와 이야기를 나누었고, 왜 이를 최대한 지양해야 하는지 에 대한 생각을 정리하기 위해 글을 작성하게 되었습니다.
1. STRAIGHT_JOIN
STRAIGHT_JOIN은 MySQL 옵티마이저가 테이블 조인 순서를 결정하지 못하거나 비효율적인 실행 계획을 선택할 때, 개발자가 조인 순서를 강제로 고정하기 위해 사용하는 힌트입니다. 실제로 특정 인덱스 분포나 데이터 편향으로 인해 옵티마이저가 잘못된 판단을 하는 경우, STRAIGHT_JOIN을 사용하면 즉각적인 성능 개선이 발생할 수 있습니다.
STRAIGHT_JOIN is similar to JOIN, except that the left table is always read before the right table. This can be used for those (few) cases for which the join optimizer processes the tables in a suboptimal order.
예를 들어, 아래 쿼리는 먼저 기간 조건이 걸린 schedule 테이블을 기준으로 대상 범위를 줄인 다음, 그 결과에 대해서만 “이미 처리된 주문이 있는지”를 NOT EXISTS 서브쿼리로 확인합니다.
1
2
3
4
5
6
7
8
9
SELECT o.order_id
FROM orders o
STRAIGHT_JOIN schedule s
ON s.user_id = o.user_id
WHERE s.schedule_at >= ?
AND s.schedule_at < ?
AND NOT EXISTS (SELECT 1
FROM processed_orders p
WHERE p.order_id = o.order_id);
STRAIGHT_JOIN을 사용하지 않으면 MySQL은 기간 조건이 있는 schedule을 먼저 보지 않고, orders나 processed_orders 같은 데이터가 훨씬 많은 테이블부터 접근하는 실행 계획을 선택하는 경우가 있습니다. 그 결과, 초기에 걸러질 데이터까지 모두 스캔하면서 쿼리 비용이 불필요하게 커집니다. 이는 옵티마이저가 판단하죠.
1
2
3
4
5
6
7
+----+-------------+-------------------+------+---------------+------+---------+------------------------------+
| id | select_type | table | type | possible_keys | key | rows | Extra |
+----+-------------+-------------------+------+---------------+------+---------+------------------------------+
| 1 | PRIMARY | orders | ALL | PRIMARY | NULL | 5,200K | |
| 1 | PRIMARY | schedule | ref | idx_user_date | ... | 120K | Using where |
| 2 | SUBQUERY | processed_orders | ref | idx_order_id | ... | 30 | Using index |
+----+-------------+-------------------+------+---------------+------+---------+------------------------------+
STRAIGHT_JOIN 을 사용하면 이를 무시하고 개발자가 원하는대로 쿼리가 실행이 되기 떄문에 다음과 같이 성능이 개선됩니다.
1
2
3
4
5
6
7
+----+-------------+-------------------+------+------------------+------------------+--------+------------------------------+
| id | select_type | table | type | possible_keys | key | rows | Extra |
+----+-------------+-------------------+------+------------------+------------------+--------+------------------------------+
| 1 | PRIMARY | schedule | range| idx_schedule_at | idx_schedule_at | 3,500 | Using where |
| 1 | PRIMARY | orders | ref | idx_user_id | idx_user_id | 12 | |
| 2 | SUBQUERY | processed_orders | ref | idx_order_id | idx_order_id | 1 | Using index |
+----+-------------+-------------------+------+------------------+------------------+--------+------------------------------+
2. 왜 마지막에 사용해야 할까?
다만 STRAIGHT_JOIN은 옵티마이저를 완전히 무력화하는 방식이기 때문에, 근본적인 해결책이 아닌 최후의 수단으로 사용하는 것이 바람직합니다. 데이터 분포가 바뀌거나 인덱스가 추가·변경되면, 과거에 최적이던 조인 순서가 오히려 최악의 실행 계획이 될 수 있으며, 이 경우 MySQL은 이를 스스로 교정할 수 없습니다. 결국 쿼리의 수명과 유지보수성을 크게 떨어뜨리기 때문에, 인덱스 설계, 조건 정리, 통계 갱신으로 해결이 불가능할 때 마지막에 사용하는 것이 맞습니다.
2-1. 데이터 분포 변화에 취약
STRAIGHT_JOIN은 현재 데이터 분포를 기준으로 조인 순서를 고정합니다. 지금은 작은 테이블을 먼저 도는 게 맞아도, 데이터가 쌓이면서 크기가 역전되면 실행계획이 그대로 최악이 됩니다. 이때 MySQL 옵티마이저는 조인 순서를 바꿔서 대응할 수 없습니다.
1
2
3
4
EXPLAIN
SELECT *
FROM user_event ue
STRAIGHT_JOIN user u ON u.id = ue.user_id;
1
2
3
4
5
6
+----+-------------+----------+------+-------+------+---------+
| id | select_type | table | type | key | rows | Extra |
+----+-------------+----------+------+-------+------+---------+
| 1 | SIMPLE | user_event | ALL | NULL | 10,000,000 | |
| 1 | SIMPLE | user | eq_ref| PRIMARY | 1 | |
+----+-------------+----------+------+-------+------+---------+
2-2. 인덱스 추가/삭제 시 자동 최적화 불가
일반 JOIN은 인덱스가 추가되면 옵티마이저가 더 유리한 테이블부터 접근하도록 실행계획을 변경합니다. STRAIGHT_JOIN은 인덱스가 생겨도 작성된 조인 순서를 그대로 강제합니다.
1
2
3
4
EXPLAIN
SELECT *
FROM user u
STRAIGHT_JOIN order_history o ON o.user_id = u.id;
1
2
3
4
5
6
+----+-------------+--------------+------+-------------+------+---------+
| id | select_type | table | type | key | rows | Extra |
+----+-------------+--------------+------+-------------+------+---------+
| 1 | SIMPLE | user | ALL | NULL | 5,000,000 | |
| 1 | SIMPLE | order_history| ref | idx_user_id | 20 | |
+----+-------------+--------------+------+-------------+------+---------+
2-3. 문맥
STRAIGHT_JOIN이 들어간 쿼리는 왜 이 순서인지 맥락을 반드시 알아야 이해할 수 있습니다. 시간이 지나거나 작성자가 바뀌면, 이 조인 순서가 여전히 유효한지 판단하기 어려워집니다.
- 왜 조인 순서가 이렇게 되지?
- 의도가 있었을 텐데, 쿼리를 수정해도 되나?
2-4. MySQL 버전 업 효과 차단
MySQL은 버전이 올라가며 옵티마이저, 히스토그램, 비용 기반 계산이 계속 개선됩니다. STRAIGHT_JOIN을 사용하면 이러한 개선 사항이 해당 쿼리에는 적용되지 않습니다.
3. 정리
STRAIGHT_JOIN은 특정 상황에서 실행 계획을 빠르게 안정화할 수 있지만, 옵티마이저의 자동 최적화와 향후 개선 여지를 함께 제한합니다. 따라서 인덱스 설계나 조건 정리로 해결이 어려운 경우에만 제한적으로 사용하는 것이 바람직합니다. 결국 STRAIGHT_JOIN은 성능 문제를 임시로 제어하는 수단이지, 일반적인 해결책으로 사용하기에는 유지보수 비용이 큽니다.