핵심 요약
- 인덱스 설계: 선택도(Selectivity)가 높은 컬럼을 활용하고, 커버링 인덱스로 쿼리 효율을 극대화해야 함.
- 조회 성능 최적화: 사전 집계, No-Offset 페이징, 전체 카운팅 지양 등을 통해 불필요한 부하를 줄여야 함.
- 안정적인 운영 관리: 쿼리 타임아웃, 배치 처리, 트랜잭션 범위 최소화로 시스템 장애를 예방해야 함.
목차
1. 효율적인 인덱스 설계
1-1. 선택도(Selectivity) 고려
- 인덱스 생성 시 데이터의 선택도가 높은 컬럼을 사용해야 함.
- 선택도가 높다는 것은 해당 컬럼의 값으로 데이터의 범위를 좁힐 수 있는 정도가 크다는 의미임.
| 컬럼 | 데이터 종류 수 | 선택도 | 인덱스 적합성 |
|---|---|---|---|
| station | 약 10,000개 | 높음 | 적합 |
| type | 5개 (hub 등) | 낮음 | 부적합 |
1-2. 커버링 인덱스 활용
- 커버링 인덱스: 쿼리 실행에 필요한 모든 컬럼을 포함하고 있는 인덱스.
- 실제 데이터 테이블(디스크)에 접근하지 않고 인덱스만으로 데이터를 추출할 수 있어 속도가 매우 빠름.
-- 인덱스: IDX_TIME_TYPE (time, type) 생성 시
-- [Good] 인덱스만으로 조회 가능 (커버링 인덱스)
SELECT time, type FROM metars WHERE time > '2026-01-01' AND type = 'METAR';
-- [Bad] report 컬럼을 찾기 위해 실제 테이블 접근 필요
SELECT time, type, report FROM metars WHERE time > '2026-01-01' AND type = 'METAR';
1-3. FULLTEXT 인덱스에 대한 오해
- 문자열 포함 검색 시
LIKE가 풀스캔을 유발한다는 이유로 무조건FULLTEXT인덱스를 권장하는 것은 잘못된 접근임. FULLTEXT인덱스는 아래 조건들이 만족될 때 고려해야 함.- 적용 조건:
- 데이터가 10만 건 이상으로 매우 많을 것.
- 검색 대상이
TEXT,VARCHAR등 긴 문자열일 것. - 검색 단어가 전체 데이터 중 아주 일부(5% 미만)에만 포함될 것.
핵심 포인트: 인덱스는 데이터의 범위를 효과적으로 줄일 수 있을 때만 의미가 있음.
2. 조회 성능 높이기
2-1. 사전 집계(Pre-aggregation) 활용
- A 테이블과 연관된 B 테이블의 값을 매번 집계(COUNT 등)하는 것은 성능 저하의 주범임.
- 예시: 게시글 목록 조회 시
좋아요개수를 매번SELECT COUNT(id) FROM likes로 계산하는 경우. - 해결:
posts테이블에like_count컬럼을 추가하고, 좋아요 발생 시점에 값을 갱신해 두면 조회 쿼리가 단순해짐.
2-2. No-Offset 페이징 (목록 조회 최적화)
- 일반적인
OFFSET기반 페이징은 뒤로 갈수록 조회 속도가 급격히 느려짐. - 이유:
LIMIT 10 OFFSET 50000은 앞선 5만 개 데이터를 모두 읽고 버린 뒤 10개를 가져오기 때문임. - 해결: 인덱스가 걸린
id를 활용해 범위를 좁히는 방식 사용.
-- [Bad] 앞선 5만 개를 다 읽어야 함
SELECT * FROM posts ORDER BY id DESC LIMIT 10 OFFSET 50000;
-- [Good] 인덱스를 타서 바로 해당 위치부터 조회함
SELECT * FROM posts WHERE id < 50010 ORDER BY id DESC LIMIT 10;
2-3. 전체 카운팅(COUNT) 금지
- 전체 데이터 개수를 세는
COUNT(*)쿼리는 지양해야 함. - 이유: InnoDB 엔진은 트랜잭션 격리 수준을 보장하기 위해, 현재 시점에서 유효한 행을 일일이 추려내며 풀 스캔을 수행하기 때문임.
- 대안:
SHOW TABLE STATUS또는information_schema의 통계치 활용 (약간의 오차 허용 시).- 별도의 통계 테이블을 만들어 개수를 따로 관리.
핵심 포인트: 실시간 계산을 줄이고, 인덱스를 활용해 읽어야 할 데이터의 양 자체를 줄이는 것이 성능 튜닝의 핵심임.
3. 기타 관리 포인트
3-1. 쿼리 타임아웃 설정
- 쿼리 실행 시간이 무한정 길어지도록 방치해선 안 됨.
- 사용자는 응답이 늦으면(5초 등) 새로고침을 반복하며, 이는 서버에 요청 대기열을 폭증시킴.
- 해결: 적절한 타임아웃을 설정해 오래 걸리는 쿼리는 강제로 종료시켜야 서버 전체의 부하를 막을 수 있음.
3-2. 적절한 배치 처리
- 대량의 데이터를 한 번에 삽입하거나 집계하면 DB 리소스를 독점하게 됨.
- 해결: 데이터를 적절한 크기로 쪼개서 수행해야 함.
- 예시: 10년 치 데이터를 1년 단위로 분할 삽입, 월간 집계를 일간 집계 30회로 나누어 수행.
3-3. DB 커넥션 개수 관리
- 트래픽 증가로 API 서버를 증설하더라도, DB의 최대 커넥션(Max Connections) 수가 고정되어 있으면 병목이 발생함.
- DB 서버의 CPU, 메모리 자원 상황을 고려해 커넥션 풀 크기를 적절히 조절해야 함.
3-4. 트랜잭션 범위 관리
- 하나의 트랜잭션 안에 외부 API 호출 등 DB와 무관한 작업이 섞이면 위험함.
- 예시: [회원가입 Insert] → [환영 메일 발송 API] → [Commit].
- 문제: 메일 서버 장애 시 회원가입까지 롤백되어 버림. 또한 API 응답 대기 시간만큼 DB 커넥션을 점유하게 됨.
- 해결: 트랜잭션은 순수하게 DB 작업 단위로만 짧게 묶어야 함.
핵심 포인트: 쿼리 성능뿐만 아니라 연결, 시간, 트랜잭션 범위 등 운영 레벨의 제약 조건을 관리하는 것이 안정성의 기본임.