오라클 DBA가 알려주는 Hash Join 원리와 use_hash 힌트로 쿼리 속도 3배 높이는 법

2026-07-02 · 테크 · 읽는데 8분

데이터베이스 개발자나 DBA라면 한 번쯤 "왜 이 쿼리는 인덱스가 있는데도 느리지?"라는 경험을 해보셨을 겁니다. 특히 수백만 건 이상의 대용량 테이블을 조인할 때는 인덱스 유무보다 조인 방식 자체를 바꾸는 게 더 효과적인 경우가 많습니다. 오라클 Hash Join은 이런 상황에서 성능을 획기적으로 개선할 수 있는 대표적인 조인 기법입니다. 실제로 약 200만 건과 300만 건 테이블을 Nested Loop Join에서 Hash Join으로 전환했더니 3분 걸리던 쿼리가 1분 20초로 줄어든 사례도 있습니다. 이 글에서는 Hash Join의 작동 원리와 use_hash 힌트를 실전에서 제대로 활용하는 방법을 소개하겠습니다.

Hash Join은 어떤 순서로 동작할까

Hash Join은 이름 그대로 해시 함수를 이용해 두 테이블을 연결하는 방식입니다. 크게 두 단계로 진행되는데, 먼저 Build Phase에서는 두 테이블 중 크기가 더 작은 쪽을 Inner Table로 선택해 메모리에 해시 테이블을 만듭니다. 예를 들어 100만 건짜리 테이블과 500만 건짜리 테이블이 있다면, 옵티마이저는 100만 건 테이블을 해시 테이블로 빌드합니다.

그다음 Probe Phase에서는 Outer Table(큰 테이블)을 한 줄씩 스캔하면서 앞서 만든 해시 테이블과 매칭되는 데이터를 찾습니다. 해시 버킷에서 일치하는 Inner Table의 행을 찾으면 바로 조인 결과를 반환합니다. 이 과정에서 인덱스를 전혀 사용하지 않기 때문에, 인덱스가 없거나 조인 조건 컬럼의 선택도가 낮은 대용량 조인에서 특히 빛을 발합니다.

Hash Join의 핵심은 메모리 기반 연산이라는 점입니다. 해시 테이블이 메모리 크기(PGA)를 넘어가면 디스크로 넘어가면서 성능이 급격히 떨어질 수 있으므로, 이 점은 꼭 염두에 두어야 합니다.

use_hash 힌트 사용법과 주의할 점

use_hash 힌트는 옵티마이저에게 "이 조인은 Hash Join 방식으로 해라"고 직접 명령하는 구문입니다. 기본 문법은 다음과 같습니다.

SELECT /*+ use_hash(e d) */
       e.employee_id, e.last_name, d.department_name
FROM   employees e, departments d
WHERE  e.department_id = d.department_id;

이렇게 힌트를 주면 옵티마이저는 통계 정보를 바탕으로 더 작은 테이블을 Build Input으로 자동 선택합니다. 하지만 실제 운영 환경에서는 "작은 테이블"이 항상 명확하지 않거나, 통계 정보가 부정확한 경우도 있습니다. 이럴 때는 다음과 같은 추가 힌트를 함께 사용할 수 있습니다.

  • leading 힌트: 조인 순서를 지정합니다. 예: /*+ leading(d e) use_hash(e) */ → departments를 먼저 읽고 employees와 해시 조인
  • ordered 힌트: FROM 절에 적힌 테이블 순서대로 조인하도록 강제합니다.
  • swap_join_inputs 힌트: 여러 테이블을 해시 조인할 때, 특정 테이블을 Build Input으로 직접 지정하고 싶을 때 사용합니다.

다만 주의할 점이 있습니다. ordered나 leading 힌트는 조인 순서를 결정하는 역할이지, Build Input을 결정하는 것이 아닙니다. 두 테이블만 조인할 때는 자연스럽게 순서가 정해지지만, 세 개 이상의 테이블을 조인할 때는 swap_join_inputs 힌트로 각 단계의 Build Input을 직접 제어해야 합니다.

Hash Join을 선택해야 하는 3가지 상황

모든 조인에 Hash Join이 좋은 것은 아닙니다. 다음 조건을 만족할 때 가장 효과적입니다.

1. 대용량 테이블 간의 조인 한쪽 테이블이 10만 건 미만이라면 Nested Loop Join이 더 빠를 수 있습니다. Hash Join은 보통 수십만 건 이상 테이블에서 진가를 발휘합니다.

2. 조인 조건 컬럼에 적절한 인덱스가 없을 때 인덱스가 없으면 Nested Loop Join은 테이블 풀 스캔을 반복하며 성능이 급락합니다. Hash Join은 인덱스 없이도 빠르게 처리합니다.

3. 배치성 쿼리나 야간 작업처럼 한 번에 대량 데이터를 처리해야 할 때 OLTP(온라인 트랜잭션 처리) 환경에서는 Nested Loop Join이 더 적합할 때가 많습니다. Hash Join은 메모리 사용량이 크고 결과 순서가 보장되지 않기 때문입니다.

반대로 Hash Join을 피해야 할 때도 있습니다. 해시 테이블이 메모리를 초과할 정도로 테이블이 크거나, 실시간으로 빠른 응답이 필요한 작은 데이터 조인에는 적합하지 않습니다.

실전 튜닝 사례 3분 → 1분 20초로 단축

실제 업무에서 발생한 사례를 하나 소개하겠습니다. 약 200만 건 테이블(A)과 300만 건 테이블(B)을 조인하는 일 배치성 쿼리가 있었습니다. 조건 범위가 넓어 인덱스를 생성해도 효율이 떨어졌고, 기존에는 Nested Loop Join을 사용하고 있었습니다.

당시 쿼리 실행 계획을 확인해보니, outer 테이블을 드라이빙으로 해서 inner 테이블을 반복적으로 랜덤 액세스하고 있었습니다. 조건에 걸리는 데이터가 많아질수록 속도는 기하급수적으로 느려졌습니다.

해결 방법은 간단했습니다. use_hash 힌트를 추가해 Hash Join으로 전환한 것입니다. 그 결과 실행 시간이 3분에서 1분 20초로 약 55% 단축되었습니다. 같은 데이터, 같은 조건에서 조인 방식만 바꿨을 뿐인데도 이렇게 큰 차이가 났습니다.

이 사례에서 중요한 점은 "무조건 Hash Join이 좋다"가 아니라, 쿼리의 실행 빈도, 처리해야 할 데이터 규모, 허용 가능한 응답 시간을 종합적으로 고려했다는 것입니다. 하루에 한 번 수행되는 배치였기 때문에 메모리 사용량이 다소 높아도 문제가 없었고, 속도 개선 효과가 훨씬 컸습니다.

Hash Join 사용 전에 반드시 검토할 것

튜닝을 할 때는 항상 "이 조인이 정말 필요한가"를 먼저 생각해야 합니다. 대량 조인이 불가피한 상황인지, 아니면 쿼리 로직을 변경하거나 집계 테이블을 추가하는 등의 방법으로 조인 자체를 피할 수는 없는지 검토하는 게 첫 단계입니다.

또한 Hash Join을 적용하기 전에 다음 사항을 체크하세요.

  • 메모리 설정 확인: PGA_AGGREGATE_TARGET 파라미터가 충분한가? 해시 영역이 부족하면 디스크 I/O가 발생해 오히려 느려집니다.
  • 통계 정보 최신화: 옵티마이저가 올바른 Build Input을 선택하려면 테이블 통계가 최신이어야 합니다. DBMS_STATS.GATHER_TABLE_STATS를 주기적으로 실행하는 습관이 중요합니다.
  • 결과 순서 불보장 인지: Hash Join은 정렬된 결과를 보장하지 않습니다. 특정 정렬 순서가 필요하다면 ORDER BY를 별도로 추가해야 합니다.

Q. Hash Join이 항상 Nested Loop Join보다 빠른가요?

아닙니다. Hash Join은 대용량 데이터 조인에 강점이 있지만, 작은 테이블을 조인하거나 인덱스를 활용한 빠른 액세스가 가능할 때는 Nested Loop Join이 더 효율적일 수 있습니다. 각 조인 방식의 특성을 이해하고 상황에 맞게 선택하는 게 중요합니다.

Q. use_hash 힌트를 줬는데도 옵티마이저가 무시하는 이유는 무엇인가요?

가장 흔한 원인은 힌트 문법 오류나 테이블 alias 불일치입니다. 힌트 안에는 FROM 절에 사용한 alias를 정확히 적어야 합니다. 또한 /*+ */ 주석 안에 띄어쓰기나 쉼표 위치가 잘못되면 힌트가 무시되니 실행 계획을 반드시 확인하세요.

Q. 여러 테이블을 조인할 때 Build Input을 직접 지정하려면 어떻게 하나요?

swap_join_inputs 힌트를 사용하면 됩니다. 예를 들어 세 개 테이블(A, B, C)을 조인할 때 B를 Build Input으로 강제하려면 /*+ leading(A B C) use_hash(B C) swap_join_inputs(B) */ 형태로 작성합니다. 이렇게 하면 A와 B 조인 시 B가 Build Input이 됩니다.