성능 개선 · Oracle SQL

29개 Slow Query를 일괄 개선한 방법: 조인 전에 처리 범위를 줄이고 결과 정합성까지 확인하기

본사·해외법인 LIMS의 29개 Slow Query를 운영 로그와 사용 빈도로 선정하고, 조인 전 필터링·인덱스 조건·조인 구조 개선과 결과 정합성 확인을 통해 24건을 1초 이내로 줄인 과정을 정리합니다.

OracleMySQLSQL 튜닝MyBatis

하나의 Query가 아니라 반복 병목 29건을 다뤘다

2025년 9월부터 10월까지 본사와 해외법인 LIMS에서 반복적으로 병목을 만들던 Slow Query를 정리하고 개선했다. 대상은 본사 10건과 해외법인 19건, 총 29건이었다.

이번 작업은 유난히 느린 SQL 하나를 고치는 일이 아니었다. 서로 다른 업무 기능과 데이터베이스 환경에서 발생한 여러 Query를 같은 기준으로 분석하고, 개선 전후의 성능과 결과 정합성을 함께 확인해야 했다. 그래서 특정 문법이나 힌트보다 다음 세 가지를 일관되게 적용하는 데 집중했다.

  1. 운영 로그와 사용 빈도를 함께 보고 개선 대상을 선정한다.
  2. 조인과 집계 전에 처리할 데이터 범위를 먼저 줄인다.
  3. 수행시간뿐 아니라 개선 전후 결과가 같은지도 확인한다.

개선 자료에 기록된 수행시간을 기준으로 29건 가운데 본사 8건과 해외법인 16건, 총 24건의 개선 후 수행시간을 1초 이내로 줄였다.

24건 1초 이내는 Query별 개선 자료에 기록된 수행시간이다. 장기 운영 SLA나 p95·p99를 뜻하지 않으며, 29건 전체가 1초 이내였다는 의미도 아니다.

단순히 가장 느린 Query부터 고치지 않은 이유

Slow Query 목록에서는 수행시간이 가장 긴 SQL이 먼저 눈에 들어온다. 하지만 운영 영향도는 한 번의 수행시간만으로 결정되지 않는다.

  • 한 번 실행될 때는 매우 느리지만 거의 사용되지 않는 Query
  • 개별 수행시간은 상대적으로 짧지만 반복 호출되는 Query
  • 조회 화면뿐 아니라 배치나 후속 처리 흐름까지 지연시키는 Query

이 세 종류의 우선순위는 같을 수 없다. 운영 로그에서 확인한 수행시간과 실제 사용 빈도를 함께 살펴보고 개선 대상을 정했다. 본사와 해외법인을 나누어 목록을 만들고, 각 Query의 기능·수행 횟수·평균 수행시간·개선 전후 수행시간을 같은 형식으로 기록했다.

이 방식은 “가장 느린 SQL 하나”가 아니라 “운영에서 반복적으로 비용을 만드는 SQL 집합”을 다루기 위한 것이었다.

29건을 같은 형식으로 분석했다

대상이 많을수록 Query마다 기록 방식이 다르면 비교와 재검증이 어려워진다. 각 Query를 다음 형식으로 정리했다.

  • 기능과 호출 지점
  • AS-IS SQL과 TO-BE SQL
  • 사용 가능한 인덱스
  • 개선 전후 실행계획
  • 개선 전후 수행시간
  • 적용한 개선 방법

Oracle과 MySQL 환경이 섞여 있었지만 분석 순서는 같았다. 실행계획에서 많은 행을 읽거나 불필요한 조인·집계를 수행하는 지점을 찾고, SQL 구조를 바꾼 뒤 동일한 기준으로 다시 측정했다.

개선 기록에서 반복적으로 나타난 원인은 크게 세 가지였다.

  1. 데이터 범위를 줄이기 전에 여러 테이블을 조인하는 구조
  2. 기존 인덱스를 충분히 활용하지 못하게 하는 조건
  3. 필요한 일부 값을 얻기 위해 큰 데이터 집합 전체를 조인하거나 같은 서브쿼리를 반복하는 구조

1. 조인 전에 처리 범위부터 줄였다

가장 자주 적용한 방법은 조인 전에 기준 데이터의 범위를 줄이는 것이었다.

아래 SQL은 실제 테이블과 업무 조건을 제거하고 구조만 단순화한 pseudocode다.

-- AS-IS 구조
SELECT ...
  FROM large_base b
  JOIN detail d
    ON d.base_id = b.id
  JOIN history h
    ON h.base_id = b.id
 WHERE b.scope_key = :scopeKey
   AND b.business_date >= :from
   AND b.business_date < :to;

이 구조에서는 옵티마이저의 판단과 통계 상태에 따라 큰 데이터 집합을 먼저 조인한 뒤 필터링할 수 있다. 조인 대상이 늘어나면 이후의 정렬과 집계 비용도 함께 커진다.

개선 시에는 업무 조건으로 기준 데이터를 먼저 줄인 뒤 필요한 테이블을 조인하도록 구조를 바꿨다.

-- TO-BE 개념 구조
WITH target_base AS (
    SELECT id,
           required_column
      FROM large_base
     WHERE scope_key = :scopeKey
       AND business_date >= :from
       AND business_date < :to
)
SELECT ...
  FROM target_base b
  JOIN detail d
    ON d.base_id = b.id
  JOIN history h
    ON h.base_id = b.id;

핵심은 CTE 자체가 아니다. CTE가 인라인 처리될지 물리화될지는 DBMS와 실행계획에 따라 달라질 수 있다. 인라인 뷰든 CTE든 중요한 것은 조인에 들어가기 전의 행 수를 줄여 후속 조인·정렬·집계로 전달되는 데이터 양을 제한하는 것이다.

2. 새 인덱스보다 기존 조건을 먼저 점검했다

인덱스 관련 성능 문제를 만나면 새 인덱스부터 떠올리기 쉽다. 하지만 이번 개선은 새 인덱스를 일괄 생성한 작업이 아니었다. 기존 인덱스를 활용할 수 있도록 WHERE 절과 JOIN 절의 조건, 복합 인덱스의 선두 컬럼, 업무 범위를 먼저 점검했다.

  • 조회 범위를 결정하는 조건이 빠져 있지 않은가
  • 복합 인덱스의 컬럼 조합과 SQL 조건이 맞는가
  • 선택도가 높은 조건을 조인 전에 적용할 수 있는가
  • 조건식 때문에 인덱스 접근 범위가 불필요하게 넓어지지 않는가

다음 역시 실제 SQL이 아닌 개념 예시다.

-- 범위를 충분히 좁히지 못하는 조건
WHERE target_id = :targetId

-- 업무 범위까지 함께 제공하는 조건
WHERE scope_key = :scopeKey
  AND target_id = :targetId

목표는 실행계획에 “인덱스를 사용했다”는 한 줄을 만드는 것이 아니었다. 실제로 읽는 범위와 후속 조인으로 전달되는 행 수를 줄이는 것이었다.

3. 일부 값을 얻기 위해 전체를 조인하지 않았다

일부 Query는 화면에 표시할 몇 개의 값을 얻기 위해 큰 테이블을 미리 조인하고 있었다. 이런 조인은 기준 행을 늘리고, 중복 제거와 추가 집계를 필요하게 만들 수 있다.

이 경우 핵심 결과 집합을 먼저 만든 뒤 정말 필요한 값만 선택적으로 조회하도록 구조를 바꿨다.

WITH core_result AS (
    SELECT id,
           status
      FROM large_base
     WHERE ...
)
SELECT c.id,
       c.status,
       (
           SELECT l.display_value
             FROM small_lookup l
            WHERE l.lookup_key = c.id
       ) AS display_value
  FROM core_result c;

스칼라 서브쿼리가 항상 조인보다 빠른 것은 아니다. 기준 결과가 큰 상태에서 행마다 서브쿼리를 실행하면 오히려 느려질 수 있다. 이 방법은 핵심 결과를 먼저 충분히 줄일 수 있고, 부가 값의 조회 범위가 제한적인 경우에만 사용했다.

같은 관점에서 불필요한 조인, 중복 서브쿼리, 비효율적인 집계도 함께 정리했다. 중요한 것은 문법을 일괄 치환하는 것이 아니라 각 Query에서 데이터가 불필요하게 커지는 지점을 찾는 것이었다.

수행시간만 줄었다고 완료하지 않았다

Query 튜닝에서 가장 위험한 결과는 “빨라졌지만 다른 데이터를 반환하는 SQL”이다. 업무 시스템의 SQL에는 상태값, 기간, 회사 범위, 예외 조건처럼 성능만 보고 제거할 수 없는 규칙이 포함돼 있기 때문이다.

개선 전후에 다음을 확인했다.

  1. AS-IS와 TO-BE SQL의 실행계획 비교
  2. 동일 조건에서 실행시간 비교
  3. 결과 건수 비교
  4. 반환 값 비교

당시 개선 전후 SQL을 직접 실행해 결과 건수와 값을 대조했다. 다만 이 확인은 자동화된 회귀 테스트가 아니며, 당시 비교 결과·로그·검증표는 별도로 보존하지 않았다. 따라서 보존된 자동 테스트나 검증 로그로 다시 재현 가능한 결과라고 확대하지 않는다.

결과

결과 데이터 표
구분개선 대상개선 후 수행시간 1초 이내
본사10건8건
해외법인19건16건
합계29건24건

24건 1초 이내는 개선 자료에 기록된 Query별 개선 후 수행시간을 기준으로 한 결과다. 장기간 운영 모니터링에서 측정한 백분위 응답시간이나 모든 파라미터에서 보장되는 SLA를 뜻하지 않는다.

일괄 개선에서 정리한 튜닝 기준

실행계획의 노드보다 데이터 흐름을 본다

실행계획은 반드시 확인해야 하지만 계획의 각 노드를 읽는 데서 끝나면 안 된다. 어느 단계에서 데이터가 커지고, 그 데이터가 다음 조인·집계·정렬로 얼마나 전달되는지 함께 봐야 한다.

인덱스 사용 여부보다 읽는 범위를 본다

실행계획에 인덱스가 표시돼도 넓은 범위를 읽으면 충분한 개선이 아니다. 조건과 인덱스 컬럼의 관계, 필터 적용 시점, 실제 처리 행 수를 함께 확인해야 한다.

성능과 정합성을 하나의 완료 조건으로 둔다

수행시간이 줄어도 결과가 달라지면 개선이 아니다. 반대로 결과만 같고 운영 병목이 그대로라면 목표를 달성하지 못한 것이다. 개선 전후 수행시간과 결과를 함께 비교해야 튜닝을 완료할 수 있다.

여러 건을 다룰수록 기록 형식이 중요하다

AS-IS·TO-BE SQL, 실행계획, 수행시간, 개선 방법을 같은 형식으로 남기면 Query별 판단 근거를 다시 확인할 수 있다. 개별 튜닝 경험을 다음 작업에서 재사용할 수 있는 기준으로 바꾸는 데도 도움이 된다.

마무리

이번 작업의 확인된 결과는 본사 10건과 해외법인 19건, 총 29개 Slow Query 개선과 그중 24건의 1초 이내 단축이었다.

하지만 더 중요한 결과는 특정 SQL 문법 하나를 찾은 것이 아니었다. 운영 로그와 사용 빈도로 대상을 정하고, 조인 전에 처리 범위를 줄이며, 기존 인덱스를 활용할 수 있도록 조건을 점검하고, 개선 전후 결과까지 대조하는 일관된 절차를 적용했다는 점이다.

SQL 튜닝은 빠른 Query를 만드는 작업이면서 동시에 기존 업무 규칙을 보존하는 작업이다. 여러 시스템의 Query를 다룰수록 이 두 조건을 같은 비중으로 관리해야 한다.