LabHub

SI DB 운영 · 조회 튜닝과 실행계획 · 이론

실행계획을 읽는다는 것

LabHub 에서 이어서 보기

한 줄 요약

실행계획을 읽는 것은 노드 이름을 훑는 일이 아니라 옵티마이저의 예측과 실제 결과 사이의 괴리를 찾는 일이다.

느리다는 신고가 들어왔다

안정화 기간에 가장 많이 받는 신고가 "화면이 느려요"다.
이때 순서가 있다.

1. 어느 화면/기능이 느린가 (액세스 로그의 응답시간 상위 URL)2. 그 기능이 실행하는 SQL 은 무엇인가3. 그 SQL 의 실행계획은 어떻게 생겼나4. 옵티마이저의 예측과 실제 결과가 얼마나 다른가5. 데이터가 문제인가, 인덱스가 문제인가, 쿼리가 문제인가

1번을 건너뛰고 "DB 가 느린 것 같다"로 시작하면 방향이 흔들린다.
측정 지점을 먼저 특정하는 것이 튜닝의 절반이다.

실행계획은 노드 이름 읽기가 아니다

> **실행계획을 읽는다는 것은 노드 이름을 훑는 일이 아니다.
> 옵티마이저의 예측과 실제 결과 사이의 괴리를 찾는 일이다.**

옵티마이저는 통계를 보고 "이 조건이면 대략 몇 행이 나오겠군" 하고 예측한다.
그 예측이 맞으면 대체로 좋은 계획을 세운다. 틀리면 엉뚱한 계획을 세운다.
그러니 볼 것은 예측 행 수 vs 실제 행 수다.

예측 92,300  실제 91,188   → 오차 1.2%. 통계가 건강하다. 계획을 믿어도 된다예측      1  실제 482,913   → 옵티마이저가 잘못된 전제 위에서 정확히 계산했다

두 번째 경우, 고쳐야 할 것은 쿼리가 아니라 통계다.
ANALYZE 를 돌려 통계를 갱신하면 계획이 통째로 바뀐다.
쿼리를 붙들고 힌트를 붙이기 전에 통계를 먼저 의심하라.

cost 는 시간이 아니다

또 하나 흔한 오해. 실행계획에 나오는 cost시간 단위가 아니다.
"디스크 페이지 하나를 순차로 읽는 비용"을 1.0 으로 두고 상대적으로 매긴 점수다.
그래서 이런 조언은 근거가 없다.

> "cost 가 10000 이 넘으면 위험합니다"

10000 이 0.3초일 수도 있고 30초일 수도 있다. 하드웨어와 캐시 상태에 따라 다르다.
cost 는 같은 쿼리의 두 계획을 비교할 때 쓰는 값이지 절대 기준이 아니다.

부모 노드의 시간은 누적값이다

실행계획에서 각 노드의 실제 시간을 볼 때 반드시 알아야 할 것.

> 부모 노드의 시간은 자식들의 시간을 포함한 누적값이다.

Hash Join  (실제 131ms)  ├─ Seq Scan on orders   (실제 92ms)   ← 여기가 진짜 범인  └─ Hash on customers    (실제 12ms)

Hash Join 이 131ms 라고 조인이 느린 것이 아니다.
그중 92ms 는 orders 스캔이다. 조인 자체가 쓴 시간은 약 27ms 다.
이걸 모르면 엉뚱한 곳을 튜닝한다.

인덱스를 못 타는 대표적인 이유

1. 컬럼에 함수를 씌웠다

-- 인덱스를 못 탄다WHERE substr(ORD_DT, 1, 6) = '202608'-- 범위 조건으로 바꾸면 탄다WHERE ORD_DT >= '20260801' AND ORD_DT < '20260901'

인덱스는 컬럼의 원래 값으로 정렬돼 있다. 함수를 씌우면 그 값이 아니게 되므로
인덱스 순서를 쓸 수 없다. 날짜 문자열, 대소문자 변환, 타입 캐스팅에서 자주 발생한다.

이걸 SARGable 하다고 표현한다. 검색 가능한 조건으로 쓰라는 뜻이다.

2. 복합 인덱스의 앞 컬럼이 조건에 없다

(CUST_ID, ORD_DT) 인덱스에 WHERE ORD_DT = ? 만 있으면 못 탄다.
전화번호부가 성-이름 순으로 정렬돼 있는데 이름만 아는 상황과 같다.

3. 선택도가 나쁘다

WHERE USE_YN = 'Y' 인데 99% 가 'Y' 라면, 인덱스로 찾아 테이블을 다시 읽는 것보다
그냥 전체를 훑는 것이 빠르다. 옵티마이저가 인덱스를 안 쓰는 것이 옳은 판단일 수 있다.

여기서 중요한 태도: Seq Scan(전체 스캔)이 항상 나쁜 것이 아니다.
작은 테이블이나 대부분의 행을 읽어야 하는 쿼리에서는 전체 스캔이 최선이다.
"스캔이 보이니까 인덱스를 만들자"는 반사적 반응은 인덱스만 늘리고 쓰기를 느리게 한다.

커버링 인덱스

쿼리가 필요로 하는 컬럼이 전부 인덱스에 있으면 테이블을 안 읽어도 된다.

-- 인덱스: (CUST_ID, ORD_DT, ORD_AMT)SELECT ORD_DT, ORD_AMT FROM ORDERS WHERE CUST_ID = ?;-- → 인덱스만 읽고 끝. 테이블 접근이 없다

실행계획에 COVERING INDEX 또는 Index Only Scan 이 보이면 이 상태다.
자주 쓰는 조회에 컬럼 한두 개를 인덱스에 더 넣어 커버링으로 만드는 것은
꽤 효과가 크다. 대신 인덱스가 커지고 쓰기가 느려진다. 트레이드오프다.

N+1 문제

애플리케이션에서 만들어지는 대표적 성능 문제다.

주문 100건 조회       → 쿼리 1회각 주문의 고객명 조회 → 쿼리 100회                       총 101회

쿼리 하나하나는 1ms 라 로그만 보면 다 빠르다.
그런데 왕복 101번이면 네트워크 지연만으로 수백 ms 가 된다.
그리고 데이터가 늘수록 선형으로 나빠진다.

해법은 조인 한 번 또는 IN 절 한 번이다.
ORM 을 쓰면 이 문제가 조용히 생기므로, 개발 중에 **SQL 로그를 켜 놓고
화면 한 번 여는 데 몇 개의 쿼리가 나가는지 세어 보는 습관**이 필요하다.

통계와 ANALYZE

옵티마이저의 판단 근거가 통계다. 그래서 통계가 낡으면 계획이 나빠진다.

이관 후 성능이 갑자기 나빠졌다는 신고의 상당수가 통계 미갱신이다.
쿼리도 인덱스도 그대로인데 계획만 바뀐 것이다. 원인을 모르면 오래 헤맨다.

마지막으로 — 튜닝 결과는 문서로

무엇을 왜 바꿨는지 남겨야 한다. 6개월 뒤 누군가
"이 이상한 인덱스는 뭐지" 하며 지우려 할 때 필요하다.

대상: SCR-021 주문조회증상: p95 4.2초원인: (ORD_DT, CUST_ID) 인덱스가 조회 패턴과 순서가 반대조치: IX_ORDERS_01 (CUST_ID, ORD_DT) 추가, 기존 인덱스 유지(배치가 사용)결과: p95 0.3초. 실행계획이 스캔에서 인덱스 탐색으로 전환

현장에서 만나는 모습

"화면이 느려요" 라는 신고에서 가장 자주 새는 곳은 1 번 단계다. 측정 지점을 특정하지 않고 "DB 가 느린 것 같다" 로 시작하면, 그때부터 하는 모든 일이 추측 위에 쌓인다. 액세스 로그에서 응답시간 상위 URL 을 뽑는 데 걸리는 시간은 1 분이고, 그 1 분이 방향을 정한다.

그다음으로 흔한 것은 인덱스를 만들어 놓고 왜 안 타는지 모르는 상황이다. 원인은 대개 셋 중 하나다 — 컬럼에 함수를 씌웠거나(substr(dt,1,6)='202603'), 복합 인덱스의 앞 컬럼이 조건에 없거나, 선택도가 나빠서 옵티마이저가 일부러 안 쓰기로 했거나. 마지막 경우에 인덱스를 강제하면 오히려 느려진다.

그리고 튜닝 결과는 문서로 남겨야 한다. 인덱스를 더하는 것은 공짜가 아니라 읽기와 쓰기를 맞바꾸는 일이라, 왜 이 인덱스를 만들었는지가 없으면 다음 사람이 지워야 할지 남겨야 할지 판단할 수 없다.