EXPLAIN 기초: 점검/리포트 쿼리가 느려질 때 “왜 느린지” 확인하는 방법
포인트 원장(point_history)은 시간이 지날수록 커집니다. 데이터가 커지면, 기능 쿼리(내역/잔액)뿐 아니라 점검 쿼리(원장 vs balance 비교, 월별 집계, ref_id 그룹핑)도 점점 느려집니다.
이때 흔히 “인덱스를 더 만들면 되지 않을까?”라고 생각하지만, 인덱스는 무조건 늘린다고 좋아지지 않습니다. 어떤 쿼리가 어떤 방식으로 실행되는지 모르면 잘못된 인덱스를 만들거나, 효과 없는 튜닝을 반복할 가능성이 큽니다.
그래서 필요한 도구가 EXPLAIN입니다. EXPLAIN은 “이 쿼리를 DB가 어떤 순서로 실행할지”를 보여주는 실행 계획입니다. 즉, 느린 이유를 추측하지 않고 눈으로 확인할 수 있게 해줍니다.
이 단원의 목적: 실행 계획을 보고 “어디를 고칠지” 결정하기
이번 글의 목표는 아래 3가지입니다.
- EXPLAIN 결과에서 최소한의 핵심 항목을 읽을 수 있게 된다
- 포인트 시스템 쿼리에서 자주 나오는 느린 패턴(풀 스캔, filesort)을 구분한다
- 인덱스를 “조회 패턴에 맞게” 설계하는 감각을 만든다
1) EXPLAIN 사용법: SELECT 앞에 붙이면 된다
가장 기본은 SELECT 앞에 EXPLAIN을 붙이는 것입니다.
EXPLAIN SELECT id, action_type, amount, created_at FROM point_history WHERE uid = '11111111-1111-1111-1111-111111111111' AND hide = 0 ORDER BY created_at DESC, id DESC LIMIT 50; 실행 결과는 DBMS/버전에 따라 조금씩 다르지만, 공통적으로 “어떤 테이블을 어떤 방식으로 읽는지”를 보여줍니다.
2) EXPLAIN에서 최소로 봐야 할 6가지 컬럼
EXPLAIN은 항목이 많습니다. 초반에는 아래 6개만 봐도 충분합니다.
- table: 어떤 테이블을 읽는지
- type: 접근 방식(대략 “얼마나 똑똑하게 찾는지”)
- possible_keys: 사용 가능 인덱스 후보
- key: 실제로 선택된 인덱스
- rows: 읽을 것으로 예상하는 행 수(많을수록 위험 신호)
- Extra: 추가 작업(filesort, temporary 등)
이 중 가장 직관적인 위험 신호는 rows가 매우 크다, Extra에 filesort/temporary가 있다입니다.
3) type 컬럼의 감각: ALL이면 일단 의심
type은 접근 방식인데, 초반에는 “좋고 나쁨”을 너무 세밀하게 외우기보다 아래 정도만 잡아도 충분합니다.
- ALL: 테이블 풀 스캔(전체 훑기) 가능성 큼
- range: 인덱스 범위 탐색(기간 조건 등)
- ref: 인덱스로 특정 값 매칭(= 조건)
- const: PK/UNIQUE로 1건 확정 수준
포인트 원장처럼 데이터가 큰 테이블에서 type=ALL이 나오면 대부분 “인덱스가 없거나, 인덱스를 못 쓰는 형태”입니다.
4) 대표 사례 1: “내 포인트 내역”이 느릴 때
포인트 내역은 보통 uid로 필터링하고 최신순으로 정렬합니다. 가장 기본적인 인덱스는 (uid, created_at)입니다.
EXPLAIN SELECT id, action_type, amount, created_at FROM point_history WHERE uid = @uid AND hide = 0 ORDER BY created_at DESC, id DESC LIMIT 50; 여기서 기대하는 모습은 대략 이렇습니다.
- key가 idx_uid_created(또는 유사 인덱스)로 잡힌다
- type이 ref 또는 range로 나온다
- rows가 “uid 전체 행” 수준으로 너무 크지 않다(기간 조건이 있으면 더 줄어든다)
- Extra에 filesort가 없거나, 최소화된다
반대로 다음이면 느릴 가능성이 큽니다.
- key가 NULL(인덱스 미사용)
- type=ALL
- Extra에 Using filesort
5) 대표 사례 2: 점검 쿼리(uid별 SUM)가 느릴 때
uid별 SUM(amount)은 point_history 전체를 그룹핑해야 합니다. 이 쿼리는 구조상 비용이 클 수밖에 없습니다. 다만 인덱스로 “그룹핑/조인” 비용을 줄일 수는 있습니다.
EXPLAIN SELECT uid, SUM(amount) AS ledger_balance FROM point_history WHERE hide = 0 GROUP BY uid; 이런 쿼리는 rows가 크게 나오는 것이 자연스러울 수 있습니다. 중요한 건 “이 점검을 얼마나 자주 돌릴 건지”입니다. 매 요청마다 돌리는 게 아니라, 배치/관리자 점검용으로 스케줄링하거나, 범위를 줄여서 실행하는 식으로 운영합니다.
그리고 아래 인덱스는 점검 쿼리에 간접적으로 도움이 될 수 있습니다.
-- uid 기반 그룹핑에 도움 CREATE INDEX idx_uid_hide ON point_history(uid, hide); 다만 hide의 선택도가 낮으면(대부분 0이면) 효과가 미미할 수 있습니다. 인덱스는 “조건으로 얼마나 잘 걸러지느냐”가 중요합니다. 그래서 EXPLAIN으로 실제로 key가 선택되는지 확인해야 합니다.
6) filesort/temporary: “정렬/그룹핑 비용이 크다”는 신호
Extra에 아래가 보이면 대체로 비용이 큽니다.
- Using filesort: 인덱스 순서로 정렬을 못 해서 별도 정렬 수행
- Using temporary: 그룹핑/정렬을 위해 임시 테이블 사용
예를 들어 내역 조회에서 filesort가 뜬다면, ORDER BY가 인덱스와 맞지 않을 가능성이 있습니다. (uid, created_at, id)처럼 정렬 축을 인덱스에 포함시키는 방향을 검토할 수 있습니다.
7) 인덱스 튜닝의 기본 순서: “쿼리 패턴 → EXPLAIN → 인덱스”
튜닝은 보통 다음 순서가 안전합니다.
- 쿼리를 “자주 쓰는 형태”로 고정한다(WHERE/ORDER BY/그룹 기준 확정)
- EXPLAIN으로 인덱스 사용 여부와 예상 rows를 확인한다
- 정말 필요한 인덱스만 추가한다
- 인덱스 추가 후 다시 EXPLAIN으로 개선 여부를 확인한다
이 과정 없이 인덱스를 늘리면, 쓰기 성능만 떨어지고(INSERT/UPDATE 비용 증가), 기대한 쿼리는 여전히 느린 상황이 생길 수 있습니다.
8) 점검 쿼리를 빠르게 만드는 현실적인 방법: “범위를 줄인다”
uid별 전체 합계를 매번 돌리는 대신, 최근 1일/7일처럼 범위를 줄인 점검을 자주 돌리고, 전체 점검은 하루 1회 같은 형태로 운영하는 방식이 흔합니다.
EXPLAIN SELECT uid, SUM(amount) AS ledger_delta_1d FROM point_history WHERE hide = 0 AND created_at >= NOW() - INTERVAL 1 DAY GROUP BY uid; 이 쿼리는 created_at 조건이 들어가므로 (created_at) 또는 (uid, created_at) 인덱스가 더 직접적으로 도움이 됩니다. 즉, “점검도 조회 패턴”이고, 패턴에 맞게 인덱스를 선택해야 합니다.
다음 글 예고: 제약조건(CONSTRAINT) — UNIQUE/FOREIGN KEY/CHECK로 ‘깨진 데이터’를 미리 막기
속도와 점검을 다뤘다면, 이제는 “애초에 이상한 데이터가 들어오지 않게” 만드는 단계로 넘어갑니다. 다음 글에서는 UNIQUE(중복 요청 방지), FOREIGN KEY(참조 무결성), CHECK(값 범위 제한) 같은 제약조건을 통해 포인트 시스템에서 흔히 발생하는 데이터 오류를 DB 레벨에서 차단하는 방법을 정리합니다.
댓글
댓글 쓰기