← 블로그 목록

SQL 쿼리 최적화: 빠른 데이터베이스 조회의 비결

느린 SQL 쿼리를 빠르게 만드는 방법. 인덱싱, 쿼리 최적화, JOIN 전략, 실행 계획 분석까지 완벽 가이드.

느린 쿼리는 느린 서비스를 만든다

데이터베이스의 성능은 전체 애플리케이션 속도를 결정합니다. 같은 결과를 반환하는 쿼리라도, 작성 방식에 따라 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%를 해결합니다. 올바른 인덱싱과 효율적인 쿼리 작성으로 데이터베이스를 최고 속도로 운영하세요.