쿼리 최적화
사용 테이블
orders — 주문 (상태, 금액, 일시)
order_items — 주문 상세 (수량, 단가)
products — 상품 (이름, 가격, 재고, 브랜드)
reviews — 리뷰 (평점, 내용)
학습 범위
EXPLAIN, Index, Covering Index, Correlated Subquery, IN vs EXISTS
1. EXPLAIN 읽기
아래 쿼리의 실행 계획을 확인하세요. SCAN TABLE과 SEARCH TABLE의 차이는 무엇인가요?
힌트 1: SCAN TABLE은 전체 테이블을 읽고,
SEARCH TABLE ... USING INDEX는 인덱스로 필요한 행만 찾습니다.
정답
2. 인덱스 효과 비교
인덱스 유무에 따른 실행 계획 차이를 확인하세요.
힌트 1: 인덱스가 있는 칼럼(customer_id)과 없는 칼럼(notes)에 대해
각각 EXPLAIN QUERY PLAN을 실행하여 비교하세요.
정답
3. 서브쿼리 -> JOIN 변환
상관 서브쿼리를 JOIN으로 바꿔 성능을 개선하세요.
힌트 1: 상관 서브쿼리는 행마다 실행됩니다.
미리 GROUP BY로 집계한 서브쿼리를 LEFT JOIN하면 한 번만 실행됩니다.
정답
-- 개선: JOIN으로 한 번에 처리
SELECT
p.name,
p.price,
COALESCE(oi_stats.order_count, 0) AS order_count,
r_stats.avg_rating
FROM products p
LEFT JOIN (
SELECT product_id, COUNT(*) AS order_count
FROM order_items GROUP BY product_id
) oi_stats ON p.id = oi_stats.product_id
LEFT JOIN (
SELECT product_id, ROUND(AVG(rating), 2) AS avg_rating
FROM reviews GROUP BY product_id
) r_stats ON p.id = r_stats.product_id
WHERE p.is_active = 1;
실행 결과 (총 218행 중 상위 7행)
| name | price | order_count | avg_rating |
|---|---|---|---|
| Razer Blade 18 블랙 | 2,987,500.00 | 310 | 3.92 |
| MSI GeForce RTX 4070 Ti Super GAMING X | 1,744,000.00 | 392 | 4.12 |
| 삼성 DDR4 32GB PC4-25600 | 43,500.00 | 871 | 3.94 |
| Dell U2724D | 894,100.00 | 261 | 4.19 |
| G.SKILL Trident Z5 DDR5 64GB 6000MHz 화이트 | 167,000.00 | 743 | 3.61 |
| MSI Radeon RX 9070 VENTUS 3X 화이트 | 383,100.00 | 532 | 4.08 |
| 삼성 DDR5 32GB PC5-38400 | 211,800.00 | 669 | 3.97 |
4. SELECT * 제거
필요한 칼럼만 명시하여 쿼리를 개선하세요.
힌트 1: SELECT * 대신 실제 필요한 칼럼만 나열하면 디스크 I/O가 줄어듭니다.
정답
-- 개선: 필요한 칼럼만
SELECT order_number, customer_id, total_amount, status, ordered_at
FROM orders
WHERE ordered_at LIKE '2024-12%'
ORDER BY total_amount DESC
LIMIT 10;
실행 결과 (총 10행 중 상위 7행)
| order_number | customer_id | total_amount | status | ordered_at |
|---|---|---|---|---|
| ORD-20241213-30931 | 1982 | 17,655,465.00 | confirmed | 2024-12-13 07:50:22 |
| ORD-20241218-31013 | 1910 | 10,836,900.00 | confirmed | 2024-12-18 12:38:50 |
| ORD-20241207-30840 | 1906 | 5,858,600.00 | confirmed | 2024-12-07 13:56:37 |
| ORD-20241224-31105 | 1775 | 5,734,000.00 | confirmed | 2024-12-24 22:55:06 |
| ORD-20241219-31036 | 3516 | 5,414,100.00 | confirmed | 2024-12-19 14:27:47 |
| ORD-20241219-31034 | 2306 | 5,008,700.00 | confirmed | 2024-12-19 21:12:07 |
| ORD-20241227-31152 | 3647 | 4,859,300.00 | confirmed | 2024-12-27 21:46:34 |
5. LIKE 패턴과 인덱스
아래 두 쿼리 중 어느 것이 인덱스를 활용할 수 있나요? A) WHERE name LIKE 'Samsung%' B) WHERE name LIKE '%Samsung%'
힌트 1: 인덱스는 B-Tree 구조입니다. 접두사가 고정된 'Samsung%'은 범위 검색이 가능하지만, '%Samsung%'은 시작을 알 수 없어 전체 스캔합니다.
정답
6. IN vs EXISTS 비교
"리뷰가 있는 상품"을 찾는 세 가지 방법의 실행 계획을 비교하세요.
힌트 1: IN, EXISTS, INNER JOIN 세 가지 방법을 EXPLAIN QUERY PLAN으로 비교하세요.
EXISTS는 첫 매치에서 즉시 반환합니다.
정답
-- 방법 1: IN
EXPLAIN QUERY PLAN
SELECT name, price FROM products
WHERE id IN (SELECT DISTINCT product_id FROM reviews);
-- 방법 2: EXISTS
EXPLAIN QUERY PLAN
SELECT name, price FROM products p
WHERE EXISTS (SELECT 1 FROM reviews r WHERE r.product_id = p.id);
-- 방법 3: JOIN
EXPLAIN QUERY PLAN
SELECT DISTINCT p.name, p.price
FROM products p
INNER JOIN reviews r ON p.id = r.product_id;
7. 커버링 인덱스
자주 실행되는 쿼리에 대해 커버링 인덱스를 설계하세요.
힌트 1: WHERE, ORDER BY, SELECT에 사용된 모든 칼럼을 인덱스에 포함하면 테이블을 읽지 않고 인덱스만으로 결과를 반환합니다.
정답
-- 현재 실행 계획
EXPLAIN QUERY PLAN
SELECT customer_id, status, total_amount
FROM orders
WHERE status = 'pending'
ORDER BY ordered_at DESC;
-- 커버링 인덱스 생성
CREATE INDEX idx_orders_status_covering
ON orders(status, ordered_at DESC, customer_id, total_amount);
-- 개선된 실행 계획 확인
EXPLAIN QUERY PLAN
SELECT customer_id, status, total_amount
FROM orders
WHERE status = 'pending'
ORDER BY ordered_at DESC;
8. 복합 조건 최적화
여러 OR 조건을 IN으로 변환하여 쿼리를 개선하세요.
힌트 1: 여러 OR 조건을 IN (...) 으로 바꾸면 옵티마이저가
인덱스를 더 효율적으로 활용할 수 있습니다.
정답
-- 개선: IN 사용
SELECT order_number, total_amount, ordered_at
FROM orders
WHERE status IN ('pending', 'paid', 'preparing');
-- 실행 계획 비교
EXPLAIN QUERY PLAN
SELECT order_number FROM orders
WHERE status = 'pending' OR status = 'paid' OR status = 'preparing';
EXPLAIN QUERY PLAN
SELECT order_number FROM orders
WHERE status IN ('pending', 'paid', 'preparing');