모든 컬럼을 조회하는 * → 디스크 I/O 증가 + 인덱스 사용 어려움
필요한 컬럼만 명시
-- 모든 컬럼 선택
SELECT * FROM Users WHERE id = 1001;
-- 개선: 필요한 컬럼만 선택
SELECT name, email FROM Users WHERE id = 1001;
HAVING은 집계 이후 필터링
HAVING 보다 WHERE가 빠름
-- HAVING을 사용한 필터링
SELECT user_id, COUNT(*)
FROM Orders
GROUP BY user_id
HAVING user_id = 1001;
-- 개선: 가능하다면 WHERE에서 필터링
SELECT user_id, COUNT(*)
FROM Orders
WHERE user_id = 1001
GROUP BY user_id;
함수 사용시 인덱스 활용이 불가하여 풀스캔 발생
인덱스 컬럼은 함수 없이 비교
-- 인덱스 미사용
SELECT name FROM Orders WHERE UPPER(name) = 'ALICE';
-- 개선: 함수 사용하지 않고 조건 작성
SELECT name FROM Orders WHERE name = 'Alice';
JOIN 순서는 옵티마이저가 결정하지만 잘못된 조건 순서 or 누락 시 Nested Loop 발생 등 성능 저하 가능
각 테이블의 건수, 인덱스 유무 고려 필요
-- 조인 조건 누락 -> 카티션 곱(Cartesian Product) 위험
SELECT * FROM A, B WHERE A.id > 10;
-- 개선: JOIN 명시
SELECT * FROM A JOIN B ON A.key = B.key WHERE A.id > 10;
JOIN이 서브쿼리보다 빠른 경우가 많음
특히 상관 서브쿼리는 루프당 재실행 → 느림
상관 서브쿼리
- 외부 쿼리의 값을 서브쿼리 안에서 사용하는 서브쿼리
-- 느린 상관 서브쿼리
SELECT name FROM Users u
WHERE EXISTS (
SELECT 1 FROM Orders o
WHERE o.user_id = u.id AND o.amount > 100
-- u.id는 Users 테이블의 값. 이를 서브쿼리 안에서 사용중
-- -> Users 테이블 각 행마다 Orders 서브쿼리 실행 -> 비효율
);
-- 개선: 서브쿼리 대신 JOIN 사용
SELECT DISTINCT u.name
FROM Users u
JOIN Orders o ON u.id = o.user_id
WHERE o.amount > 100;
OFFSET은 앞 데이터를 스캔 후 버리는 방식 (스캔이 일어나긴 함)
큰 OFFSET은 성능 저하
OFFSET
커서 기반 페이지네이션
- 이전 페이지의 마지막 값을 기억하고, 그 다음부터 조회하는 방식
- 필요한 지점부터 조회 → 성능 좋음
- 대규모 데이터의 경우 커서 방식이 좋음
-- 큰 OFFSET
SELECT * FROM Orders ORDER BY created_at LIMIT 10 OFFSET 100000;
-- 개선: 커서 방식 또는 마지막 ID 기억해서 조건 필터링
SELECT * FROM Orders
WHERE created_at < '2024-06-01 00:00:00'
ORDER BY created_at DESC
LIMIT 10;
WHERE, JOIN, ORDER BY, GROUP BY 에 빈번히 등장하는 컬럼에 인덱스 고려INSERT, UPDATE, DELETE (쓰기)성능은 감소대량 DML 이후 인덱스가 조각날 수 있음
ALTER INDEX REBUILD 또는 OPTIMIZE TABLE 로 성능 회복 가능
-- 인덱스 리빌드 (MSSQL)
ALTER INDEX [인덱스명] ON [테이블명] REBUILD;
-- 테이블의 모든 인덱스 리빌드
ALTER INDEX ALL ON [테이블명] REBUILD;
-- 조각화율(Fragmentation Percent) 확인
-- 조각화율이 30% 이하라면 ORGANIZE(페이지 재정렬)
-- 30% 이상이라면 REBUILD(페이지 재생성)
SELECT
OBJECT_NAME(ips.object_id) AS TableName,
i.name AS IndexName,
ips.index_type_desc,
ips.avg_fragmentation_in_percent
FROM
sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') AS ips
JOIN sys.indexes AS i ON ips.object_id = i.object_id AND ips.index_id = i.index_id
WHERE
ips.database_id = DB_ID();
| DBMS | 실행 계획 확인 명령어 |
|---|---|
| MySQL | EXPLAIN, EXPLAIN ANALYZE |
| MSSQL | 실행계획 보기 |
| (SSMS에서 ‘실행 계획 보기’ 체크) | |
| Oracle | EXPLAIN PLAN, AUTOTRACE |
| PostgreSQL | EXPLAIN, EXPLAIN ANALYZE |
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
|---|---|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | Orders | ref | idx_user | idx_user | 4 | const | 10 | Using where |
ref, eq_ref, const → 빠른 인덱스 탐색ALL 이면 풀스캔Using filesort, Using temporary → 성능 저하 가능성 있음Using index → 인덱스만으로 SELECT 컬럼을 모두 해결한다는 뜻ALL → 풀스캔index → 인덱스 전체 스캔NULL → 사용 가능한 인덱스 없음 / 옵티마이저가 인덱스 무시➡️ 실행 계획을 보고 직관적으로 알 수 있는건 인덱스 튜닝 전략
➡️ 쿼리 튜닝 / 통계 정보 & 구조적 튜닝은 실행 계획 + 쿼리 내용 + 상황 파악 필요