데이터가 기하급수적으로 증가하는 현대의 애플리케이션에서 SQL 성능 최적화는 선택이 아닌 필수입니다. 이 글에서는 실제 개발 현장에서 자주 마주치는 성능 문제들과 그 해결책을 구체적인 예제와 함께 살펴보겠습니다.
"2024년 이후 주문한 적이 있는 모든 사용자"를 조회하는 쿼리를 작성한다고 가정해봅시다.
SELECT DISTINCT u.user_id, u.username
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.created_at >= '2024-01-01';
문제점:
DISTINCT가 필요해 중복 제거 과정에서 추가 비용 발생SELECT u.user_id, u.username
FROM users u
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.user_id = u.user_id
AND o.created_at >= '2024-01-01'
);
장점:
DISTINCT 불필요-- 절대 하면 안 되는 쿼리!
SELECT * FROM users u
WHERE u.user_id NOT IN (
SELECT customer_id FROM orders -- NULL 값이 포함될 수 있음
);
위험한 이유:
NOT IN 서브쿼리 결과에 NULL이 하나라도 있으면 전체 결과가 빈 집합이 됩니다.
SELECT * FROM users u
WHERE u.user_id NOT IN (
SELECT customer_id
FROM orders
WHERE customer_id IS NOT NULL
);
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = u.user_id
);
-- Full Table Scan 유발 가능
SELECT * FROM products
WHERE category_id = 1 OR price < 100;
문제점:
SELECT * FROM products WHERE category_id = 1
UNION
SELECT * FROM products WHERE price < 100 AND category_id != 1;
장점:
-- 전략적 인덱스 생성
CREATE INDEX idx_products_category_price ON products(category_id, price);
-- 이제 OR 조건도 효율적으로 처리 가능
SELECT * FROM products
WHERE category_id IN (1, 2, 3) OR price BETWEEN 50 AND 150;
-- 100만 번째 페이지를 조회한다면?
SELECT * FROM transactions
ORDER BY created_at DESC
LIMIT 10 OFFSET 100000;
성능 문제:
-- 첫 페이지
SELECT * FROM transactions
ORDER BY created_at DESC
LIMIT 10;
-- 다음 페이지 (이전 페이지의 마지막 created_at 값 사용)
SELECT * FROM transactions
WHERE created_at < '2024-01-15 10:30:00'
ORDER BY created_at DESC
LIMIT 10;
| 페이지 위치 | OFFSET 방식 | 커서 방식 |
|---|---|---|
| 1-100 페이지 | ~50ms | ~5ms |
| 1000-10000 페이지 | ~500ms | ~5ms |
| 100000+ 페이지 | ~5000ms+ | ~5ms |
-- PostgreSQL
EXPLAIN ANALYZE SELECT ...;
-- MySQL
EXPLAIN FORMAT=JSON SELECT ...;
-- SQL Server
SET STATISTICS IO ON;
SELECT ...;
-- 인덱스 사용 통계 확인 (PostgreSQL)
SELECT
schemaname, tablename, indexname,
idx_tup_read, idx_tup_fetch,
idx_tup_read/nullif(idx_tup_fetch,0) as selectivity
FROM pg_stat_user_indexes
ORDER BY idx_tup_read DESC;
SELECT * 대신 필요한 컬럼만 선택WHERE 절에 함수 사용 최소화JOIN 대신 EXISTS 고려 (특히 중복 제거가 필요한 경우)NOT IN 대신 NOT EXISTS 사용SQL 성능 최적화는 단순히 빠른 쿼리를 작성하는 것을 넘어, 확장 가능하고 유지보수하기 쉬운 시스템을 만드는 기초입니다. 작은 최적화 하나하나가 모여 전체 애플리케이션의 성능을 크게 좌우할 수 있으니, 평상시 이런 패턴들을 의식적으로 연습해보시기 바랍니다.
💡 Pro Tip: 성능 최적화는 측정 없이는 의미가 없습니다. 항상 최적화 전후의 성능을 수치로 비교하고, 실제 운영 환경에서의 효과를 검증하세요.
by claude