DB·SQL 실무 가이드 · Part 14

슬로우 쿼리 추적과 진단 도구

느리다는 신고를 받았을 때 원인 SQL을 특정하기

작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.

이 파트에서 다루는 내용

지금 도는 것 확인누적 부하 상위라이선스 경계SQL TraceSQL 출처 태깅
01

느리다는 신고를 받으면 지금 무엇이 도는지부터 봅니다

원인을 짐작하기 전에 현재 상태를 봅니다. 실제로 무거운 쿼리가 돌고 있는지, 아니면 Part 11에서 다룬 락 대기인지에 따라 대응이 완전히 달라집니다.

현재 실행 중인 세션sql
SELECT s.sid,
       s.serial#,
       s.username,
       s.status,
       s.event,                       -- 무엇을 기다리는가
       s.wait_class,                  -- User I/O, Concurrency, Idle ...
       s.seconds_in_wait,
       s.sql_id,
       s.module,
       s.machine,
       q.sql_text
FROM   v$session s
LEFT JOIN v$sql q ON q.sql_id = s.sql_id
WHERE  s.status = 'ACTIVE'
AND    s.username IS NOT NULL        -- 백그라운드 프로세스 제외
ORDER BY s.seconds_in_wait DESC;

wait_class 가 Concurrency 면 락 경합이므로 Part 11로, User I/O 면 실제로 많이 읽고 있는 것이므로 쿼리 튜닝으로 갑니다. 이 한 줄이 방향을 가릅니다.

누적 부하가 큰 SQL 찾기sql
SELECT * FROM (
  SELECT sql_id,
         plan_hash_value,
         executions,
         ROUND(elapsed_time / 1000000, 1)                        AS total_sec,
         ROUND(elapsed_time / GREATEST(executions, 1) / 1000, 1) AS avg_ms,
         buffer_gets,
         ROUND(buffer_gets / GREATEST(executions, 1))            AS gets_per_exec,
         SUBSTR(sql_text, 1, 120)                                AS sql_head
  FROM   v$sql
  WHERE  executions > 0
  ORDER BY elapsed_time DESC
)
WHERE ROWNUM <= 20;

한 번에 오래 걸리는 쿼리보다, 조금 느린데 아주 자주 실행되는 쿼리가 시스템 전체에는 더 해롭습니다. total_sec 로 정렬하면 그런 쿼리가 잡힙니다. gets_per_exec 는 한 번 실행에 몇 블록을 읽는지로, 튜닝 전후 비교에 쓰기 좋습니다.

02

여기서 라이선스 경계를 반드시 알아야 합니다

Oracle의 강력한 진단 도구 상당수는 Enterprise Edition에 딸린 별도 유료 옵션입니다. 설치돼 있고 실행도 되기 때문에 무료라고 착각하기 쉽습니다.

더 곤란한 것은 Oracle이 기능 사용 이력을 자체적으로 기록한다는 점입니다. 라이선스 감사에서 쓴 적 없다고 말할 수 없습니다. 인터넷에서 찾은 쿼리를 무심코 실행했다가 계약 위반이 되는 일이 실제로 있습니다.

AWR
유료 · Diagnostics Pack

DBA_HIST_ 로 시작하는 뷰와 AWR 리포트가 여기 해당합니다. 조회만 해도 사용으로 기록됩니다.

ASH
유료 · Diagnostics Pack

V$ACTIVE_SESSION_HISTORY 입니다. 과거 시점의 세션 상태를 보는 강력한 도구지만 유료입니다.

SQL Tuning Advisor
유료 · Tuning Pack

자동 튜닝 권고 기능입니다. Enterprise Manager의 관련 화면도 대부분 여기 묶입니다.

V$ 뷰
무료

V$SESSION, V$SQL, V$LOCK 등 현재 상태를 보는 동적 성능 뷰는 자유롭게 쓸 수 있습니다. 이 파트의 쿼리들이 여기 해당합니다.

SQL Trace / TKPROF
무료

가장 확실한 증거를 남기는 방법이고 추가 비용이 없습니다. 다음 섹션에서 다룹니다.

Statspack
무료

AWR의 전신입니다. 직접 설치해야 하고 정보량은 적지만 기간별 추이를 보는 목적은 충족합니다.

차단 설정 확인sql
-- NONE 이면 유료 팩 기능이 막혀 있습니다
SHOW PARAMETER control_management_pack_access

-- 값: NONE / DIAGNOSTIC / DIAGNOSTIC+TUNING

라이선스가 없는 환경이라면 이 값을 NONE 으로 두는 것이 안전합니다. 실수로 실행하는 것 자체를 막아 줍니다. 설정 변경은 DBA와 협의합니다.

실무 기준

본인 환경의 에디션과 옵션 계약 범위를 먼저 확인합니다. 확실하지 않으면 V$ 뷰와 SQL Trace만 씁니다. 이 둘로도 대부분의 슬로우 쿼리 진단은 가능합니다.

03

SQL Trace는 가장 확실한 증거입니다

실행계획은 옵티마이저가 무엇을 하려 했는지 보여 주지만, 트레이스는 실제로 무슨 일이 있었는지 파일로 남깁니다. 파싱 횟수, 실제 읽은 블록, 대기한 이벤트가 전부 기록됩니다.

특정 화면이나 배치가 느릴 때, 그 세션만 켜서 무슨 SQL이 몇 번 실행됐는지 확인하는 용도로 씁니다. ORM이 만들어 내는 예상 밖의 쿼리를 잡아낼 때 특히 유용합니다.

세션 트레이스 켜고 끄기sql
-- 내 세션을 추적
ALTER SESSION SET tracefile_identifier = 'my_trace';
EXEC DBMS_SESSION.SESSION_TRACE_ENABLE(waits => TRUE, binds => TRUE);

-- 문제 되는 작업 실행
-- ...

EXEC DBMS_SESSION.SESSION_TRACE_DISABLE;

-- 트레이스 파일 경로 확인
SELECT value FROM v$diag_info WHERE name = 'Default Trace File';

waits => TRUE 는 무엇을 기다렸는지, binds => TRUE 는 바인드 변수에 실제로 어떤 값이 들어갔는지 기록합니다. 값이 몰린 컬럼 문제를 확인할 때 binds 가 결정적입니다.

다른 세션 추적과 TKPROF 변환bash
# 다른 세션을 추적하려면 SID와 SERIAL# 로 지정 (SQL*Plus에서)
#   EXEC DBMS_MONITOR.SESSION_TRACE_ENABLE(123, 45678, TRUE, TRUE);

# 서버에서 트레이스 파일을 읽기 좋게 변환
tkprof my_trace_12345.trc report.txt sys=no sort=exeela

# sort=exeela : 실행 시간이 긴 순서로 정렬
# sys=no      : 내부 재귀 SQL 제외

변환된 리포트에서 볼 것은 세 가지입니다. 같은 SQL의 실행 횟수(예상보다 많으면 N+1 문제), disk 와 query 열의 블록 수, 그리고 대기 이벤트 목록입니다.

04

어느 화면이 던진 SQL인지 알 수 있게 만듭니다

V$SQL에서 무거운 쿼리를 찾았는데 그게 어느 기능에서 나온 것인지 모르면 튜닝을 시작할 수 없습니다. 특히 ORM이 만든 쿼리는 코드에서 검색해도 안 나옵니다.

미리 태그를 붙여 두면 이 문제가 사라집니다. 운영 전에 해 두면 장애 시간이 크게 줄어듭니다.

세션에 출처 남기기sql
BEGIN
  DBMS_APPLICATION_INFO.SET_MODULE(
    module_name => 'ORDER_BATCH',
    action_name => 'DAILY_SUMMARY'
  );
END;
/

-- 이후 V$SESSION 과 V$SQL 에서 출처가 보입니다
SELECT module, action, sql_id, event
FROM   v$session
WHERE  module IS NOT NULL;

Spring이라면 배치 잡 시작 시점이나 인터셉터에서 설정하면 됩니다. 커넥션 풀을 쓰면 반납 전에 초기화해 다음 사용자와 섞이지 않게 합니다.

SQL 자체에 주석으로 표시sql
SELECT /* OrderSummaryBatch.aggregate */
       customer_id, SUM(amount)
FROM   orders
WHERE  order_dt >= :from_dt
GROUP BY customer_id;

주석은 SQL 텍스트에 그대로 남아 V$SQL 에서 검색됩니다. 다만 주석이 다르면 다른 SQL로 취급되므로, 값이 아니라 고정된 출처 표시에만 씁니다. MyBatis XML의 각 쿼리에 붙여 두면 추적이 쉬워집니다.

진단 순서

첫째 V$SESSION 에서 지금 도는 것과 wait_class 를 봅니다. 락 경합이면 Part 11로 갑니다. 둘째 V$SQL 에서 누적 부하 상위 SQL을 찾습니다. 셋째 module 이나 주석으로 출처를 특정합니다. 넷째 그 쿼리의 실제 실행계획을 Part 7의 방법으로 확인합니다. 다섯째 필요하면 SQL Trace로 증거를 남깁니다. 유료 도구는 계약 범위를 확인한 뒤에만 씁니다.

체크

이 파트 완료 기준