느린 쿼리는 느린 서비스를 만든다
데이터베이스의 성능은 전체 애플리케이션 속도를 결정합니다. 같은 결과를 반환하는 쿼리라도, 작성 방식에 따라 10배 이상의 속도 차이가 날 수 있습니다.
이 가이드에서는 느린 쿼리를 빠르게 최적화하는 방법을 배웁니다. 대규모 데이터셋에서도 밀리초 단위의 응답 시간을 얻을 수 있습니다.
1부: 나쁜 쿼리의 특징
1. SELECT * 사용
❌ 나쁜 예:
SELECT * FROM users;
✅ 좋은 예:
SELECT id, name, email FROM users;이유: 불필요한 컬럼을 조회하면 네트워크 트래픽, 메모리 사용량 증가
2. WHERE 절 없이 전체 테이블 스캔
❌ 나쁜 예:
SELECT * FROM orders WHERE DATE(created_at) = '2026-03-02';
✅ 좋은 예:
SELECT * FROM orders WHERE created_at >= '2026-03-02' AND created_at < '2026-03-03';이유: DATE() 함수는 인덱스 사용 불가. 범위 조건으로 변경하면 인덱스 활용
3. 인덱스 미활용
❌ 나쁜 예:
SELECT * FROM users WHERE CONCAT(first_name, ' ', last_name) = 'John Doe';
✅ 좋은 예:
SELECT * FROM users WHERE first_name = 'John' AND last_name = 'Doe';이유: 함수 사용하면 인덱스 미적용. 직접 비교로 인덱스 활용
4. 비효율적인 JOIN
❌ 나쁜 예:
SELECT * FROM users
INNER JOIN orders ON users.id = orders.user_id
INNER JOIN payments ON orders.id = payments.order_id;문제: 조인 순서, WHERE 절 배치에 따라 성능이 크게 변함
2부: 인덱싱 전략
인덱스의 원리
인덱스 없음 (Full Table Scan):
테이블의 모든 행 검사 → O(n)
인덱스 있음 (Index Seek):
B-Tree 구조로 빠른 검색 → O(log n)1. 기본 인덱스
// 사용자 ID로 자주 조회
CREATE INDEX idx_users_id ON users(id);
// 이메일로 검색
CREATE INDEX idx_users_email ON users(email);2. 복합 인덱스 (Composite Index)
// 자주 함께 검색되는 컬럼
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
쿼리:
SELECT * FROM orders WHERE user_id = 1 AND created_at > '2026-01-01';
→ 두 조건 모두 인덱스 활용3. 인덱스 순서의 중요성
// 인덱스: (user_id, created_at)
✅ 효율적:
WHERE user_id = 1 AND created_at > '2026-01-01' (둘 다 인덱스 활용)
❌ 비효율적:
WHERE created_at > '2026-01-01' (인덱스 미활용)규칙: 자주 사용되는 조건이 먼저, 범위 조건은 마지막
4. 인덱스 설계 가이드
// WHERE 절에서 사용되는 컬럼
CREATE INDEX idx_products_category ON products(category_id);
// ORDER BY에 사용되는 컬럼
CREATE INDEX idx_posts_date ON posts(created_at DESC);
// JOIN 조건에 사용되는 컬럼
CREATE INDEX idx_comments_post ON comments(post_id);3부: 쿼리 최적화 기법
1. WHERE 절 최적화
OR 조건 피하기
❌ 느린 쿼리:
SELECT * FROM users WHERE status = 'active' OR status = 'pending';
✅ 빠른 쿼리:
SELECT * FROM users WHERE status IN ('active', 'pending');NOT IN 대신 LEFT JOIN
❌ 느린 쿼리:
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM bans);
✅ 빠른 쿼리:
SELECT users.* FROM users
LEFT JOIN bans ON users.id = bans.user_id
WHERE bans.user_id IS NULL;2. JOIN 최적화
올바른 JOIN 순서
❌ 비효율적:
SELECT * FROM large_table1
INNER JOIN large_table2 ON ...
INNER JOIN small_table ON ...;
✅ 효율적:
SELECT * FROM small_table
INNER JOIN large_table1 ON ...
INNER JOIN large_table2 ON ...;
→ 작은 테이블부터 시작INNER JOIN vs LEFT JOIN
✅ INNER JOIN (더 빠름):
결과 행이 적음, 옵티마이저 최적화 용이
❌ LEFT JOIN (느림):
모든 LEFT 테이블 행 유지 필요3. GROUP BY 최적화
인덱스 활용
❌ 느린 쿼리:
SELECT category, COUNT(*) FROM products
GROUP BY category;
✅ 빠른 쿼리:
-- 먼저 인덱스 생성
CREATE INDEX idx_products_category ON products(category);
SELECT category, COUNT(*) FROM products
GROUP BY category;WHERE 절 먼저 적용
❌ 느린 쿼리:
SELECT category, COUNT(*) FROM products
GROUP BY category
HAVING COUNT(*) > 10;
✅ 빠른 쿼리:
SELECT category, COUNT(*) as cnt FROM products
WHERE price > 100
GROUP BY category
HAVING cnt > 10;
→ 불필요한 행 먼저 필터링4부: 실행 계획 분석
EXPLAIN 사용
// MySQL
EXPLAIN SELECT * FROM users WHERE id = 1;
결과:
| id | select_type | table | type | key | rows | Extra |
| 1 | SIMPLE | users | ref | PRIMARY | 1 | |
주요 항목:
- type: ALL(전체스캔), index(인덱스스캔), ref(값참조) → ref 이상 권장
- key: 사용된 인덱스
- rows: 조사할 행 수 → 적을수록 좋음
- Extra: 추가 정보성능 비교
// 쿼리 1 (느림)
EXPLAIN SELECT * FROM orders WHERE DATE(created_at) = '2026-03-02';
→ type: ALL, rows: 1000000
// 쿼리 2 (빠름)
EXPLAIN SELECT * FROM orders WHERE created_at >= '2026-03-02' AND created_at < '2026-03-03';
→ type: range, rows: 50
→ 20배 이상의 성능 차이!5부: 자주하는 실수
1. 과도한 인덱스
❌ 모든 컬럼에 인덱스 생성
✅ 자주 검색되는 컬럼만 인덱스
이유: 인덱스는 INSERT/UPDATE 속도를 늦춤
2. LIMIT 없는 대량 조회
❌ SELECT * FROM large_table; (수백만 행)
✅ SELECT * FROM large_table LIMIT 1000; (페이징)
3. 서브쿼리 남용
❌ SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE ...); (느림)
✅ SELECT users.* FROM users INNER JOIN orders ON ... (빠름)
4. 인덱스 칼럼 변형
❌ WHERE UPPER(email) = 'JOHN@EXAMPLE.COM' (인덱스 미적용)
✅ WHERE email = 'john@example.com' (인덱스 적용)
실제 최적화 사례
Before (느린 쿼리)
SELECT u.*, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE YEAR(u.created_at) = 2026
GROUP BY u.id
HAVING COUNT(o.id) > 5;
실행 시간: 15초
스캔된 행: 1,000,000+After (최적화된 쿼리)
-- 인덱스 생성
CREATE INDEX idx_users_created ON users(created_at);
CREATE INDEX idx_orders_user ON orders(user_id);
-- 쿼리 최적화
SELECT u.id, u.name, COUNT(o.id) as order_count
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE u.created_at >= '2026-01-01' AND u.created_at < '2027-01-01'
GROUP BY u.id
HAVING COUNT(o.id) > 5;
실행 시간: 0.2초 (75배 빠름!)도구 활용
SQL 포매터
복잡한 쿼리를 읽기 좋게 정렬하고, 문법을 확인하세요.
마무리
SQL 최적화는 성능 문제의 80%를 해결합니다. 올바른 인덱싱과 효율적인 쿼리 작성으로 데이터베이스를 최고 속도로 운영하세요.