파티션 테이블을 설계할 때 운영 관리성(DDL 가용성) 측면에서는 Local Index가 압도적으로 유리합니다. 이 때문에 실무에서는 "파티션 테이블의 인덱스는 무조건 Local로 만든다"는 식의 획일화된 가이드가 통용되기도 합니다.
하지만 대규모 OLTP(온라인 트랜잭션 처리) 환경에서 이러한 원칙만 고집하다가는 Buffer Cache Latch 경합으로 인해 CPU 사용률이 치솟거나, 비즈니스 데이터 무결성이 깨지는 심각한 문제에 직면할 수 있습니다. 파티션 유지보수의 불편함을 감수하고서라도 반드시 Global Index(글로벌 인덱스)를 선택해야만 하는 핵심 시나리오와 내부 동작 메커니즘을 데이터 아키텍처(DA) 및 DBA 관점에서 상세히 파헤쳐 보겠습니다.
1. 개요: 왜 관리가 까다로운 Global Index를 선택하는가?
Global Index는 테이블의 파티션 구조와 무관하게 독립적인 B-Tree 세그먼트를 유지하는 인덱스입니다. 파티션 DROP이나 TRUNCATE 시 인덱스가 UNUSABLE 상태에 빠질 위험(ORA-01502)이 있음에도 불구하고 Global Index를 포기할 수 없는 이유는 명확합니다. 바로 전역적인 데이터 유일성(Global Uniqueness) 보장과 파티션 키가 없는 OLTP 쿼리의 극단적인 I/O 최소화 때문입니다.
- 데이터 무결성 관점: 파티션 키와 무관한 단일 식별자(예: 주문번호, 회원번호)의 중복을 엔진 레벨에서 원천 차단합니다.
- 조회 성능 관점: 수백 개의 파티션으로 쪼개진 테이블에서도 단 3~4개의 블록 I/O만으로 원하는 로우를 즉시 찾아냅니다.
2. 내부 메커니즘: Global Index가 압도적인 성능을 내는 이유
왜 특정 조건에서는 Local Index보다 Global Index가 훨씬 빠를까요?
오라클 내부의 Index Probing(인덱스 탐색) 비용과 정렬(Sort) 오퍼레이션 메커니즘을 통해 그 이유를 확인할 수 있습니다.
2.1 Index Probing 비용과 Buffer Cache Latch 경합 감소
테이블이 일자별로 365개의 파티션으로 나뉘어 있고, 인덱스의 높이(Height, BLEVEL + 1)가 3이라고 가정해 보겠습니다. 이때 파티션 키(일자) 조건 없이 주문번호 = 12345로 단건 조회하는 핵심 OLTP 쿼리가 초당 1,000번씩 유입된다면 내부에서는 다음과 같은 차이가 발생합니다.
- Local Index 스캔 시 (PARTITION RANGE ALL): 옵티마이저는 어느 파티션에 해당 주문번호가 있는지 알 수 없으므로 365개의 로컬 인덱스 세그먼트를 모두 뒤져야 합니다. 각 파티션의 Root 블록과 Branch 블록을 매번 읽어야 하므로, 단 1건의 데이터를 찾기 위해 최소 $365 \times 3 = 1,095$ 블록의 Logical I/O(Consistent Gets)가 발생합니다. 이 과정에서 수많은 세션이 동시에 인덱스 루트 블록을 읽으려 하면서 cache buffers chains Latch 경합이 폭발적으로 증가합니다.
- Global Index 스캔 시 (단일 트리 탐색): 전체 데이터가 하나의 B-Tree에 정렬되어 있으므로 Root $\rightarrow$ Branch $\rightarrow$ Leaf까지 단 3블록의 Logical I/O로 탐색이 끝납니다. 리프 블록에 저장된 10바이트 Extended ROWID 내의 Data Object ID를 읽고 해당 테이블 파티션의 데이터 블록 1개만 추가로 읽으면 되므로, 총 4블록 I/O만에 쿼리가 종료됩니다.
2.2 Top-N 쿼리 및 정렬(ORDER BY) 시 MERGEJOIN SORT 생략
페이징 처리가 필요한 화면에서 파티션 키 조건 없이 최신 데이터 10건을 가져오는 Top-N 쿼리를 수행할 때도 내부 오퍼레이션이 완전히 달라집니다.
-- 파티션 키(SALE_DATE) 조건 없이 최근 주문번호 순으로 상위 10건 페이징 조회
SELECT *
FROM (SELECT /*+ INDEX_DESC(A IDX_SALES_ORDER_NO) */ *
FROM TB_SALES A
WHERE CUST_GRADE = 'VIP'
ORDER BY ORDER_NO DESC)
WHERE ROWNUM <= 10;
- Global Index: 인덱스 리프 블록 자체가 전체 테이블에 대해 ORDER_NO 순으로 완벽히 정렬되어 있습니다. 따라서 가장 우측 리프 블록부터 역방향으로 10건만 읽고 즉시 스캔을 멈춥니다(Stopkey 동작, Sort 생략).
- Local Index: 365개 파티션 각각에서 상위 10건씩을 뽑아낸 뒤, PGA 메모리(Sort Area) 공간에서 이들의 순서를 다시 비교하여 최종 10건을 추려내는 작업이 필요합니다. 즉, 인덱스를 타더라도 내부적으로 정렬 비용이 수반되어 응답 속도가 크게 저하됩니다.
3. 주요 원인 분석: 실무에서 Global Index 도입이 불가피한 4가지 케이스
아키텍처 설계 단계에서 다음과 같은 요건이 존재한다면 Local Index 대신 Global Index를 채택해야 합니다.
3.1 파티션 키가 포함되지 않은 PK / Unique 제약조건 (비즈니스 무결성)
가장 대표적이면서도 타협이 불가능한 케이스입니다. 주문 테이블을 주문일자(ORDER_DATE) 기준으로 Range 파티셔닝했는데, 업무 규칙상 기본키(PK)는 시퀀스로 채번되는 단일 컬럼 주문번호(ORDER_ID)여야 하는 경우입니다.
- 오라클 아키텍처상 파티션 키(ORDER_DATE)가 빠진 상태에서는 Local Unique Index를 생성할 수 없습니다(ORA-14039 에러 발생).
- 애플리케이션에서 조회 시 항상 ORDER_ID 단독으로 조인하거나 조회한다면 PK에 억지로 ORDER_DATE를 결합할 수도 없으므로, 이때는 반드시 Global Unique Index로 PK를 구성해야 합니다.
3.2 파티션 키 조건 없이 유입되는 고빈도 OLTP 핵심 쿼리
테이블은 관리 목적상 생성일자 기준으로 파티셔닝되어 있지만, 실제 온라인 화면에서는 고객번호(CUST_ID)나 계약번호(CONT_NO)만으로 조회하는 트랜잭션이 전체 워크로드의 80% 이상을 차지하는 경우입니다.
- 파티션 개수가 수십~수백 개 이상인 상태에서 이를 Local Non-Prefixed Index로 설계하면, 앞서 살펴본 Index Probing 현상으로 인해 DB 서버의 CPU가 100%에 도달할 수 있습니다.
3.3 시퀀스 기반 PK의 우측 리프 블록 경합 (Right-Hand Index Contention)
시퀀스(SEQUENCE)나 SYSDATE처럼 값이 단조 증가(Monotonically Increasing)하는 컬럼에 인덱스를 생성하고 수많은 세션이 동시에 INSERT를 수행하면, 가장 마지막(우측 끝) 인덱스 리프 블록 하나에 모든 변경이 집중됩니다.
- 이로 인해 enq: TX - index contention 및 buffer busy waits 대기 이벤트가 발생하며 전체 트랜잭션이 멈칫거리는 현상이 나타납니다.
- 이때 테이블 파티션 구조와 상관없이 인덱스만 해시(Hash) 기준으로 여러 세그먼트로 쪼개는 Global Hash Partitioned Index를 도입하면, 입력되는 키 값이 해시 함수를 통해 여러 인덱스 파티션의 리프 블록으로 고르게 분산되어 핫블록(Hot Block) 경합을 완벽히 제거할 수 있습니다.
3.4 테이블 파티션 기준과 주력 검색 범위 조건이 불일치하는 경우
예를 들어, 테이블은 데이터 생명주기(ILM) 관리를 위해 로그적재일자 기준으로 파티셔닝되어 있으나, 분석 및 온라인 배치에서는 정산기준년월이나 지역코드 범위로 대량 스캔이 일어나는 경우입니다.
- 이때 인덱스를 정산기준년월 기준의 Global Range Partitioned Index로 구성하면, 테이블 파티션과 무관하게 인덱스 자체적인 파티션 프루닝(Index Partition Pruning) 효과를 얻을 수 있습니다.
4. 트러블슈팅 및 해결 방안: Global Index 최적 설계 및 운영 베스트 프랙티스
단순히 단일 세그먼트 형태의 Global Index만 쓰는 것이 아니라, 대용량 환경의 경합을 줄이고 관리 리스크를 통제하는 실무 적용 전략입니다.
4.1 Hot Block 경합 해소를 위한 Global Hash Partitioned Index 구축
동시 INSERT가 극심한 OLTP 테이블의 PK 인덱스에 buffer busy waits 경합이 발생한다면, 아래와 같이 Global Hash Partitioned Index로 전환하여 I/O 및 Latch 경합을 분산합니다. (파티션 수는 2의 거듭제곱인 4, 8, 16, 32 등으로 설정하는 것이 해시 분포상 유리합니다.)
-- 시퀀스 기반 PK에서 우측 리프 블록 경합(TX Index Contention)을 방지하기 위한 Global Hash 인덱스 생성
CREATE UNIQUE INDEX PK_TB_ORDERS ON TB_ORDERS (ORDER_ID)
GLOBAL PARTITION BY HASH (ORDER_ID)
PARTITIONS 16
TABLESPACE TBS_IDX_01;
-- 생성된 Global Partitioned Index 메타데이터 확인
SELECT INDEX_NAME, PARTITIONING_TYPE, PARTITION_COUNT, LOCALITY
FROM USER_PART_INDEXES
WHERE INDEX_NAME = 'PK_TB_ORDERS';
4.2 Index Probing으로 인한 Local Index 비효율 구간 진단 쿼리
현재 운영 중인 시스템에서 Local Index를 타느라 불필요한 블록 I/O(PARTITION RANGE ALL)를 과도하게 유발하고 있는 악성 SQL을 찾아내어 Global Index 전환 대상을 선별하는 튜닝 쿼리입니다.
-- V$SQL_PLAN에서 PARTITION RANGE ALL과 LOCAL INDEX 스캔이 결합되어 논리적 I/O(BUFFER_GETS)가 높은 SQL 추출
SELECT S.SQL_ID,
S.EXECUTIONS,
S.BUFFER_GETS,
ROUND(S.BUFFER_GETS / DECODE(S.EXECUTIONS, 0, 1, S.EXECUTIONS), 1) AS GETS_PER_EXEC,
P.OBJECT_NAME AS INDEX_NAME,
SUBSTR(S.SQL_TEXT, 1, 80) AS SQL_TEXT_PART
FROM V$SQL S
JOIN V$SQL_PLAN P ON S.SQL_ID = P.SQL_ID AND S.CHILD_NUMBER = P.CHILD_NUMBER
WHERE P.OPERATION = 'INDEX'
AND P.OPTIONS LIKE '%RANGE SCAN%'
AND EXISTS (
SELECT 1
FROM V$SQL_PLAN P2
WHERE P2.SQL_ID = P.SQL_ID
AND P2.CHILD_NUMBER = P.CHILD_NUMBER
AND P2.OPERATION = 'PARTITION RANGE'
AND P2.OPTIONS = 'ALL'
)
AND (S.BUFFER_GETS / DECODE(S.EXECUTIONS, 0, 1, S.EXECUTIONS)) > 500
ORDER BY GETS_PER_EXEC DESC;
4.3 하이브리드(Hybrid) 인덱스 아키텍처 설계 표준
실무에서는 하나의 파티션 테이블에 Local과 Global 인덱스를 역할에 맞게 혼합하여 설계하는 것이 정석입니다.
| 인덱스 구분 | 적용 대상 및 설계 기준 | 권장 인덱스 아키텍처 |
| 주 식별자 (PK / UK) | 파티션 키가 포함되지 않은 단일 식별자 (예: ORDER_ID) | Global Unique Index (동시 입력 많을 시 Global Hash 적용) |
| 핵심 OLTP 검색키 | 파티션 키 조건 없이 초당 수십 회 이상 단건/소량 조회되는 컬럼 | Global Non-Partitioned Index |
| 일반 검색키 / 배치용 | 화면에서 날짜(파티션 키) 조건이 필수로 들어오거나 야간 배치에서 사용되는 인덱스 | Local Prefixed / Non-Prefixed Index |
운영 시 필수 수칙:
테이블에 Global Index가 단 하나라도 존재한다면, 해당 테이블의 파티션을 관리하는 모든 배치 스크립트나 프로시저(DROP, TRUNCATE, MOVE, SPLIT, EXCHANGE)에는 반드시 UPDATE INDEXES 절을 기본 템플릿으로 포함시켜야 합니다.
"Local Index가 관리하기 편하다"는 이유만으로 모든 인덱스를 Local로 강제하면, 온라인 트랜잭션 성능과 데이터 무결성을 크게 해칠 수 있습니다.
- Global Uniqueness 보장: 파티션 키를 포함하지 않는 순수 식별자(PK/UK)의 중복을 막기 위해서는 Global Unique Index가 유일한 해법입니다.
- OLTP 극한의 응답 속도: 파티션 키 조건이 없는 단건 조회나 Top-N 정렬 쿼리에서는 불필요한 전체 파티션 탐색(Index Probing)과 정렬(Sort)을 생략하는 Global Index가 압도적인 성능을 발휘합니다.
- Global Hash를 통한 경합 해소: 대규모 동시 트랜잭션 환경에서 시퀀스 기반 인덱스의 우측 리프 블록 경합(enq: TX - index contention)을 해결할 때도 Global Hash Partitioned Index가 핵심 아키텍처로 활용됩니다.
