Skip to content

14화 · 파고들기 ​

필요할 때 찾아보는 문서다. 처음 읽을 때는 1_사건.md만으로 충분하다. 이 문서의 숫자는 모두 실측값이다. ora-hanorder(13화 종료 상태)의 복사본(_C14 · _P14)과 ora-burger에서 쟀고, 실습 스크립트 번호를 함께 적는다. 경과 시간(초)은 그날 장비 사정을 탄다. 판정은 redo, 블록 변경, Buffers로 하고, 초는 보조로만 읽는다.

찾는 것바로 가기
DML이 행마다 치르는 다섯 가지 (인덱스 · redo/undo · 제약 · 트리거 · HWM)§1 부수비용
적재 청구서 한 장: 02·04·08·09·10·12화가 넘긴 것§2 청구서
무기 넷: 집합 처리 · APPEND · NOLOGGING · 인덱스 끄고 켜기§3 무기
APPEND의 대가: 배타 락 · 빈 칸 무시 · 꺼지는 조건 · ORA-12838 · 8MB§4 대가
NOLOGGING과 로그 모드: 연습 DB에서는 왜 차이가 없나§5 NOLOGGING
파티션 유형§6 유형
Pruning: Pstart/Pstop 읽는 법 · 키를 가공하면§7 Pruning
LOCAL Prefixed · Nonprefixed · GLOBAL · 파티션 정리와 인덱스§8 인덱스
파티션의 크기: 첫 익스텐트 8MB · 버거원 3GB 해부§8.5 크기
병렬: QC · Granule · TQ · PQ_DISTRIBUTE · Downgrade · Free에서 본 것§9 병렬
P1 리비전 · 정합성§10 P1
월말 정리: DELETE · SHRINK · DROP PARTITION§11 월말
측정값 전부§12 측정
Free에서 못 잰 것§13 한계
원천 좌표§14 좌표

1. DML의 부수비용 다섯 ​

행 하나를 넣는 일 자체는 싸다. 비싼 것은 그 행에 따라붙는 일들이다.

#부수비용무엇을 하나이 화 실측 (5만 건 INSERT · 실습/01)
1인덱스 유지인덱스마다 정렬된 자리를 찾아 키를 끼운다. 자리가 없으면 리프 블록을 쪼갠다인덱스 1개당 redo 약 12MB · 블록 변경 약 10만(행당 2)
2redo · undo바꾼 내용(redo)과 바꾸기 전 내용(undo)을 둘 다 적는다(← 13화)인덱스 0개 redo 2.44MB · undo 0.13MB → 3개 redo 38.55MB · undo 13.31MB
3제약FK는 행마다 부모 키를 찾아간다. CHECK는 행마다 식을 계산한다FK 하나: redo 그대로 · 논리 읽기 453,243 → 547,221(행당 약 +2)
4트리거행 트리거는 행마다 PL/SQL을 호출한다. 그리고 여러 행을 한 번에 쓰던 INSERT가 한 행씩 쓰게 된다행 트리거를 붙이면: redo 38.55 → 50.95MB · 블록 변경 301,707 → 403,838(인덱스 하나를 더 붙인 것과 비슷)
5HWM일반 INSERT는 빈 칸을 찾아 쓰고, APPEND는 고수위선 위에 쓴다(← 05화)DELETE 33만 건 뒤 되채우기: 일반 6,016블록 · APPEND 7,990블록

3번과 4번은 APPEND를 꺼 버린다. 대상 테이블에 FK(자식 쪽)나 트리거가 있으면 직접 경로가 조용히 일반 INSERT로 바뀐다(§4.3).


2. 적재 청구서 — 한 장으로 정산 ​

2.1 인덱스 개수별 (T_ORD_BIG 복사본 · 5만 건 INSERT · 워밍업 2회 뒤 · 실습/01) ​

인덱스생긴 화redoundo블록 변경논리 읽기경과(보조)
0개—2.44MB0.13MB1,9212,7630.16초
1개 (ORD_DT)02화14.13MB4.34MB102,481153,7393.2초
2개 +(ORD_DT, ORD_AMT)02화 커버링26.09MB8.72MB201,060301,95613.8초
3개 +(ORD_DT, ORD_SEQ)09화 포스트잇38.55MB13.31MB301,704453,23022.6초
4개 +(CUS_ID, ORD_AMT)08화 기각안50.62MB17.79MB397,312596,97752.0초
(T2-002) 3개에서 (ORD_DT)만 뺀 2개 (DT_AMT, DT_SEQ)04화 승인 대기26.85MB9.10MB201,131302,05911.4초
  • 04화에서 잰 0개, 1개, 2개 값(2.44 · 14.13 · 26.10MB)이 거의 그대로(26.09) 다시 나온다. redo는 회차가 바뀌어도 소수점 둘째 자리까지 거의 같다. 경과 시간은 리두 로그 교체 대기에 좌우되어 회차마다 크게 흔들린다.
  • 0 → 3개: redo 15.8배 · 블록 변경 157배 · 논리 읽기 164배.

2.2 화별로 넘어온 청구서 ​

넘긴 화넘긴 것이 화에서 정산한 값
02인덱스 2개의 적재 비용§2.1의 2개 줄 · 100만 건 재적재 [A]의 몸통
03인덱스 1개가 늘린 redo(ora-dist)같은 원리: T_ORD_BIG에서 인덱스 1개당 redo 약 12MB / 5만 건
0405_DML_청구서.sql 1차 증거 · IX_ORD_BIG_DT 폐기 승인 대기§2.1 재측정 · T2-002(§2.1 마지막 줄)
05인덱스 2개 48MB(테이블 47MB보다 크다) · 월말 DELETE인덱스 3개 75MB(22 + 26 + 27) · §11
08기각한 커버링 (CUS_ID, ORD_AMT)§2.1의 4개 줄: 만들었다면 redo +12.07MB
09인덱스 3개의 적재 비용§2.1의 3개 줄
10인덱스 3개를 단 채 일반 INSERT로 100만 건을 넣으면 오래 걸린 이유 · 통계 수집 단계와 시간을 다툰 진짜 원인§2.3: 329초 중 309.5초가 리두 로그 교체 대기 · 통계 수집은 1.06초
12FK 인덱스 · 인덱스 맨 오른쪽 블록§2.4: 순번 키 인덱스, 보통 vs 역순

2.3 100만 건 재적재 — 운영 그대로 (실습/02 [A]) ​

단계redoundo블록 변경논리 읽기경과그중 로그 교체 대기
② 적재 + 커밋 (일반 INSERT · 인덱스 3개)916.6MB344.4MB5,455,2667,949,703329초309.5초
④ 통계 수집1.4MB015,61737,3271.06초0
  • 경과 시간의 94%, 98%가 기다림이다(2회: 309.5 / 329초, 532.2 / 542초). 연습 DB의 온라인 리두 로그는 10MB × 2다. 916.6MB를 쓰려면 10MB짜리 로그를 아흔 번 넘게 갈아 끼워야 하고, 갈아 끼울 때마다 앞 로그에 적힌 변경 블록이 아직 디스크에 안 내려갔으면 멈춘다. 이 대기가 log file switch (checkpoint incomplete)다.
  • 10화는 같은 배치를 9분 13초(다른 작업과 겹친 날은 22분 46초)로 쟀다. 이 화에서는 329초, 542초(2회, 검증 실행은 585초)였고 redo는 916.6MB, 916.1MB였다. 시간은 흔들려도 redo는 흔들리지 않는다.
  • 켠 채 적재한 인덱스는 같은 100만 행인데도 크기가 이렇게 다르다.
인덱스갓 만든 것 (CREATE INDEX)켠 채 일반 INSERT로 채운 것끄고 켜기(REBUILD)
(ORD_DT)2,6534,374 · 4,369(+65%)2,653
(ORD_DT, ORD_AMT)3,2063,555(+11%)3,206
(ORD_DT, ORD_SEQ)3,3445,505 · 5,373(+61~65%)3,344

무작위 순서로 끼워 넣으면 리프가 반씩 쪼개지면서(50-50 분할) 반쯤 빈 블록이 남는다. (ORD_DT, ORD_AMT)가 덜 커진 것은 같은 날짜 안에서 금액이 흩어져 있어 분할 위치가 달라지기 때문이다.

2.4 인덱스 맨 오른쪽 블록 — 보통 vs 역순 키 (실습/01 마지막 · ← 12화) ​

순번(ORD_SEQ) 5만 개를 기존 값보다 큰 값으로, 한 세션에서 넣는다.

인덱스redo리프 분할그중 90-10 분할블록 변경범위 조건 BETWEEN
보통 (ORD_SEQ)5.96MB1071075,305INDEX RANGE SCAN
역순 (ORD_SEQ) REVERSE14.19MB00102,904INDEX FULL SCAN · 2,230블록(100행)
  • 보통 인덱스는 새 키를 맨 오른쪽 리프 한 곳에 몰아 넣는다. 세션이 하나뿐이면 오히려 가장 싸다. 꽉 찬 블록을 90-10으로 쪼개 거의 빈칸 없이 이어 붙이기 때문이다.
  • 문제는 여러 세션이 동시에 넣을 때 그 한 블록에 몰린다는 점이다(12화 실측: buffer busy waits · enq: TX - index contention).
  • 역순 키는 몰림을 흩는 대신 키마다 다른 블록을 고치므로 블록 변경이 19배, redo가 2.4배로 늘고, 범위 검색을 잃는다(BETWEEN이 찾아가지 못하고 인덱스 전체를 훑는다).
  • 다른 처방(해시 파티션 인덱스로 오른쪽 끝을 N개로 나누기, 키 앞에 분산 값 붙이기)도 대가의 구조는 같다. 흩는 만큼 범위 검색과 정렬을 잃는다. 다중 세션 경합의 크기는 Free의 CPU 2개로는 의미 있게 잴 수 없다(§13).

3. 무기 넷 ​

3.1 같은 100만 행, 넣는 법만 다르다 (실습/02 [A] · 실습/03) ​

넣는 법적재 redo적재 undo적재 블록 변경적재 경과인덱스 재생성합계 경과
[A] 일반 INSERT · 인덱스 켠 채916.6MB344.4MB5,455,266329초—329초
[B] APPEND · 켠 채268.8MB126.66MB205,1718.3초—8.3초
[E] 일반 INSERT · 끄고 → 켜기48.4MB2.61MB45,3831.05초0.4MB · 5.08초6.1초
[C] APPEND · 끄고 → 켜기0.2MB0.04MB1,8720.26초0.4MB · 4.74초5.0초
[D] [C] + NOLOGGING0.2MB0.04MB1,8700.22초0.4MB · 4.71초4.9초

2회차: [B] 269.1MB · 11.0초 / [E] 48.4 + 0.4MB · 1.1 + 4.6초 / [C] 0.2 + 0.4MB · 0.24 + 4.71초 / [D] 0.2 + 0.4MB. redo는 같고 초만 흔들린다. 02의 [A]는 2회차에 916.1MB · 542초였다.

표에서 읽을 것:

  • [A] → [E]: 인덱스를 끄기만 해도 329초 → 6.1초. 329초의 몸통은 테이블이 아니라 인덱스였다.
  • [A] → [B]: 직접 경로만으로는 268.8MB가 남는다. 테이블 몫은 직접 경로로 덜 수 있지만, 켜져 있는 인덱스는 적재가 끝날 때 한꺼번에 고쳐 넣으면서 redo와 undo를 만든다.
  • [C]는 두 무기를 함께 쓴 것이다. 테이블은 직접 경로로 넣고, 인덱스는 다 넣은 뒤 정렬해서 새로 짓는다. 재생성한 인덱스는 갓 만든 크기다(2,653 · 3,206 · 3,344).
  • [C] ≈ [D]. 이 연습 DB가 NOARCHIVELOG라서다(§5).

3.2 무기별 정리 ​

무기어떻게무엇을 덜어 주나대가
집합 처리루프를 INSERT … SELECT, MERGE, UPDATE … 한 문장으로 바꾼다행마다 생기는 왕복 · 커밋 · 행 단위 redo/undo(P1: redo 28.66 → 4.47MB)중간 커밋이 없어지므로 언두가 한 트랜잭션에 쌓인다(13화 ORA-30036이 나던 자리) · 중간에 실패하면 전부 롤백
APPEND(직접 경로)INSERT /*+ APPEND */ … SELECT버퍼 캐시를 거치지 않고 블록째 쓴다 · 데이터 undo 거의 0 · (NOARCHIVELOG이거나 NOLOGGING이면) 데이터 redo 거의 0§4 전체
NOLOGGING테이블 · 인덱스 속성. 직접 경로 연산에만 효과가 있다ARCHIVELOG DB에서 직접 경로의 데이터 redo그 블록은 복구할 수 없다. 적재 뒤 백업이 필수다. FORCE LOGGING DB에서는 무시된다
인덱스 끄고 켜기ALTER INDEX … UNUSABLE → 적재 → ALTER INDEX … REBUILD행마다 하던 인덱스 유지 · 인덱스 비대(+65% → 0)꺼진 동안 그 인덱스를 쓰던 조회가 조용히 다른 길로 간다(Q1 91 → 5,822) · UNIQUE/PK 인덱스는 끌 수 없다(DML이 ORA-01502) · 재생성 시간

집합 처리로 바꿀 수 있는지부터 따진다. 한 문장으로 못 바꾸는 행 단위 로직(행마다 다른 분기, 함수 호출)이 남는다면, 그 비용이 어디서 생기는지는 15화에서 잰다.


4. APPEND의 대가 ​

4.1 배타 락 (실습/04 [1]) ​

커밋 전, 내 세션의 TM 락LMODE다른 트랜잭션이 같은 테이블에 INSERT
일반 INSERT3 (ROW EXCLUSIVE)된다
APPEND6 (EXCLUSIVE)ORA-00054(NOWAIT로 물었을 때) · NOWAIT가 없으면 커밋할 때까지 기다린다

배치가 마감 전에 끝나는 한 아무도 모른다. 마감을 넘기면 낮 업무가 그 테이블에 쓰지 못한다. 이 내용을 T3 「동시 실행 영향」 칸에 적는다.

4.2 빈 칸 무시 — 고수위선 위에만 쓴다 (실습/04 [3] · ← 05화) ​

T_ORD_BIG 복사본 · 2022년 33만 건 DELETE 뒤, 같은 행을 다시 넣는다고수위선 아래 블록전체 스캔 Buffers
DELETE 직후5,915—
일반 INSERT로 되채움6,0165,889
APPEND로 되채움7,9907,862

월말 DELETE와 일상 APPEND를 함께 쓰는 테이블은 매달 한 달치씩 커진다. 05화의 「SHRINK를 매달」 처방도 그만큼 더 비싸진다.

4.3 APPEND가 꺼지는 조건 (실습/01 [01-T1~T3]) ​

실행계획의 첫 줄과 Note를 본다. 에러는 나지 않는다.

대상 테이블계획Note
FK(자식) + 행 트리거LOAD TABLE CONVENTIONALDirect Load disabled because triggers are defined
FK(자식)만LOAD TABLE CONVENTIONALDirect Load disabled because parent referential constraints are present
둘 다 없음LOAD AS SELECT + OPTIMIZER STATISTICS GATHERING—

확인하는 습관: APPEND를 붙였으면 실행계획에서 LOAD AS SELECT를 눈으로 확인한다. 힌트를 붙였다고 해서 직접 경로로 돌았다는 증거가 되지는 않는다.

4.4 ORA-12838 — 이 버전에서는 나지 않는다 (실습/04 [2]) ​

원천(성능고도화 II권)과 19c까지의 문서는 "직접 경로로 넣은 트랜잭션은 커밋 전에 그 테이블을 읽거나 고칠 수 없다(ORA-12838)"고 설명한다. 그런데 이 연습 DB(Oracle AI Database 26ai Free 23.26)에서는 APPEND 직후 같은 트랜잭션의 SELECT COUNT(*)가 1,001,000을 그대로 돌려줬다. 운영 버전이 다르면 책대로 막힌다. 제약도 내 버전에서 재 보고 쓴다.

4.5 온라인 통계 — 히스토그램은 모으지 않는다 (실습/03 [B]) ​

빈 테이블(TRUNCATE 직후)에 직접 경로로 넣으면 23ai는 적재하면서 통계를 모은다(실행계획의 OPTIMIZER STATISTICS GATHERING 줄, NOTES = STATS_ON_LOAD). 테이블 행수와 인덱스 통계까지 새로 갱신된다. 그러나 열 통계를 보면 다르다.

열원본 T_ORD_BIGAPPEND 재적재 직후
ORD_STFREQUENCY(← 10화 정책서 1호)NONE · STATS_ON_LOAD
ORD_DTNONENONE · STATS_ON_LOAD

"APPEND로 바꾸면 통계 수집 단계를 빼도 된다"는 틀린 결론이다. 히스토그램을 정책으로 걸어 둔 테이블은 적재 뒤 수집 단계를 남겨야 한다(연습 DB에서 1.06초). 10화 「통계 수집 정책서」 P3에 덧붙일 사실이다.

4.6 파티션의 첫 익스텐트를 비워 둔다 (실습/04 [4] · 실습/10) ​

이 화에서 가장 큰 함정이다. 자세한 내용은 §8.5에 있다.


5. NOLOGGING과 로그 모드 ​

이 연습 DB (NOARCHIVELOG)운영 (보통 ARCHIVELOG)
일반 INSERTredo 전부redo 전부
직접 경로(APPEND) · LOGGING 테이블데이터 redo 거의 없음(실측 0.2MB / 100만 행)데이터 redo 전부
직접 경로 · NOLOGGING 테이블위와 같다(실측 0.2MB, 차이 없음)데이터 redo 거의 없음 · 그 블록은 미디어 복구 불가
CREATE INDEX · REBUILDredo 거의 없음(실측 0.4MB / 3개)인덱스 크기만큼 · NOLOGGING이면 거의 없음
FORCE LOGGING (DB · 테이블스페이스)—NOLOGGING을 무시하고 전부 기록(Data Guard 환경)
  • 이 DB의 상태: SELECT log_mode, force_logging FROM v$database → NOARCHIVELOG · NO.
  • 그래서 [C]와 [D]가 같다. 연습 DB의 0.2MB를 운영의 기대값으로 옮기면 안 된다. T3의 「환경」 칸에 로그 모드를 적는 이유가 여기에 있다.
  • ARCHIVELOG로 바꿔서 재려면 인스턴스를 재기동해야 하므로 이 교재에서는 하지 않는다(§13).

6. 파티션 유형 ​

유형키쓰는 곳이 화에서
Range값의 구간(VALUES LESS THAN)날짜 · 월별 보관 · 오래된 구간 정리T_ORD_BIG_P14(주문일 월별 36 + PMAX) · T_ORD_LOG_P14 · 버거원 ORD_RANGE
IntervalRange + 새 구간 자동 생성다음 달 파티션을 미리 안 만들어도 됨(실험 중 측정만) Pstop이 1048575로 찍힌다. 앞으로 생길 수 있는 칸까지 포함한 값이다
List값의 목록(VALUES (…))지역 · 상태 · 코드버거원 ORD_LIST(ORD_YM 25칸)
Hash키의 해시균등 분산 · 경합 분산 · partition-wise 조인버거원 서브파티션(HASH(ORD_NO) 8)
Composite두 단계(Range-Hash · List-Hash · Range-List …)1단계로 정리, 2단계로 분산버거원 ORD_LIST_HASH(LIST 25 × HASH 8)
Reference부모 테이블의 파티션을 FK로 물려받음주문 ↔ 주문상세를 같은 칸으로—
System키 없이 애플리케이션이 칸을 지정특수 용도—

라이선스: 파티셔닝은 Enterprise Edition의 유상 옵션이다. Free에는 들어 있어서 이 화의 파티션 실습은 모두 Free에서 했다. 강도현의 2021-03-30 커밋 "파티션은 유상 옵션이라 승인 못 받음"도 이 사정 때문이다. 이번에는 근거 수치(§11)를 붙여 예산을 다시 올린다.


7. Pruning — Pstart / Pstop 판독 ​

Pruning은 조건을 보고 열 필요가 없는 파티션은 아예 열지 않는 것이다. 실행계획의 Pstart, Pstop을 보면 몇 번 칸부터 몇 번 칸까지 열었는지 알 수 있다(DISPLAY_CURSOR(FORMAT => '… +PARTITION')).

7.1 판독표 ​

계획 줄Pstart / Pstop뜻
PARTITION RANGE SINGLE30 / 3030번 칸 하나(정적: 계획을 세울 때 이미 안다)
PARTITION RANGE ITERATOR30 / 3230 ~ 32번 연속
PARTITION RANGE ITERATORKEY / KEY바인드 변수, 서브쿼리, 조인 값으로 실행할 때 고른다(동적)
PARTITION LIST INLISTKEY(I) / KEY(I)IN 목록에 든 칸들
PARTITION RANGE ALL · LIST ALL1 / N전부 연다. Pruning 실패(또는 원래 전부 필요)
PARTITION HASH ALL 아래1 / 8서브파티션 8조각을 전부
(Interval) ALL1 / 1048575앞으로 생길 수 있는 칸 번호의 끝까지
(조인 필터):BF0000조인 상대에서 만든 블룸 필터로 칸을 고른다(동등 조인 + 해시 조인일 때). 이 화의 달력 조인은 범위 조인이라 NL + KEY로 나왔다

7.2 한빛유통 T_ORD_BIG_P14 (주문일 월별 36칸 + PMAX · 실습/07) ​

조건계획Pstart/PstopBuffers
[A] TO_CHAR(ORD_DT,'YYYYMMDD') BETWEEN '20240601' AND '20240630' (Q1 rev00)RANGE ALL1 / 375,868
[B] ORD_DT >= DATE '2024-06-01' AND ORD_DT < DATE '2024-07-01' (Q1 rev01)RANGE SINGLE30 / 30161
[C] 같은 조건 · 바인드 변수RANGE ITERATORKEY / KEY161
[D] TRUNC(ORD_DT,'MM') = DATE '2024-06-01'RANGE ALL1 / 375,868
[E] 6 ~ 8월RANGE ITERATOR30 / 32493
[F] 달력 테이블이 고른 달 (조인 · 서브쿼리)RANGE ITERATORKEY / KEY163 · 165
(기준) 파티션 없는 원본 · Q1 rev02 커버링INDEX RANGE SCAN—91
  • 키를 가공하면 칸을 못 고른다. TO_CHAR도, TRUNC(…,'MM')도 전부 연다. 칸막이가 월 단위여도 마찬가지다. 인덱스 규칙(02화 §3)과 같다. 컬럼이 가공되지 않은 채로 조건에 와야 한다.
  • 가공했을 때의 E-Rows는 다시 2,500이다(02화에서 본 그 고정 추정값).
  • 파티션이 조회를 빠르게 하는 것은 아니다. 6월 한 칸을 통째로 읽은 161은 커버링 인덱스의 91보다 많다. 파티션 덕분에 '한 칸만 열면' 될 뿐, 칸 안은 다시 인덱스 문제다(§8).

7.3 버거원 (ORD_YM = 파티션 키 · 실습/09) ​

조건 (2012년 6월 · 매장별)계획Pstart/PstopBuffers
ORD_LIST · ORD_YM = '201206'LIST SINGLEKEY/KEY → 6 / 61,980
ORD_LIST · SUBSTR(ORD_DT,1,6) = '201206'LIST ALL1 / 2547,460
ORD_LIST · ORD_DT LIKE '201206%'(키가 아닌 컬럼)LIST ALL + 로컬 인덱스1 / 25(인덱스 24번 시작)41,897
ORD_RANGE · ORD_YM BETWEEN '201206' AND '201208'RANGE ITERATOR6 / 85,938
ORD_LIST · ORD_YM IN ('201206','201207','201208')LIST INLISTKEY(I)5,937
ORD_LIST_HASH · ORD_YM = '201206'LIST SINGLE + HASH ALL1 / 8(서브)9,039
(기준) 파티션 없는 ORD · ORD_DT LIKE '201206%'INDEX RANGE SCAN(CF 22,223)—1,089
  • ORD_DT는 파티션 키가 아니다. 파티션 키는 ORD_YM이다. 화면이 ORD_DT로 물으면 칸을 못 고르고, 로컬 인덱스로 칸마다 한 번씩 찾아간다. 게다가 ORD_LIST는 ORD_RANDOM에서 적재돼 CF가 나쁘다(999,024).
  • 파티션 없는 ORD(잘 정렬된 테이블 · 04화)가 가장 적게 읽었다.

8. 파티션 인덱스 — LOCAL Prefixed · Nonprefixed · GLOBAL ​

종류정의장점대가
LOCAL Prefixed테이블과 같은 칸으로 나뉘고, 파티션 키가 인덱스 선두에 온다키 조건이면 한 칸의 인덱스만 본다 · 칸을 정리(DROP)하면 같이 사라진다—
LOCAL Nonprefixed같은 칸으로 나뉘지만, 파티션 키가 선두가 아니다(또는 없다)칸을 정리하면 같이 사라진다조건에 파티션 키가 없으면 칸마다 인덱스를 하나씩 뒤진다
GLOBAL(파티션 안 한 인덱스)테이블 전체에 하나키 없이 찾아도 한 번에 찾는다칸을 DROP/TRUNCATE하면 UNUSABLE이 된다. UPDATE GLOBAL INDEXES를 붙여야 산다
GLOBAL 파티션 인덱스인덱스가 테이블과 다른 기준으로 나뉜다(예: 해시)오른쪽 끝 블록 분산 · 큰 인덱스 관리칸을 정리할 때 GLOBAL과 같은 문제가 생긴다

실측 (실습/07 [G][H] · 실습/09 [3] · 실습/06 [4]) ​

조회인덱스계획Buffers
고객 1명 500건 · 날짜 없이LOCAL Nonprefixed (CUS_ID) · 37칸RANGE ALL · 인덱스 Starts 36(빈 PMAX 제외)572(인덱스 72)
같은 고객 · 2024년 6월같은 인덱스RANGE SINGLE 3016
고객 1명 500건 · 날짜 없이GLOBAL (CUS_ID)TABLE ACCESS BY GLOBAL INDEX ROWID504(인덱스 4)
버거원 주문번호 하나LOCAL Nonprefixed PK (ORD_NO, ORD_YM) · 25칸RANGE ALL · 인덱스 Starts 2450
같은 주문 + ORD_YM같은 PKRANGE SINGLE 19 · INDEX UNIQUE SCAN3
(기준) 파티션 없는 ORDORD_PK (ORD_NO)INDEX UNIQUE SCAN4
Q1 rev01 · 6월LOCAL Prefixed (ORD_DT, ORD_AMT)옵티마이저는 6월 칸 FULL을 골랐다161
같은 Q1 · 로컬 인덱스 강제같은 인덱스RANGE SINGLE + 인덱스89(파티션 없는 커버링 91과 거의 같다)
이력 조회 ORD_SEQ = 4242 · DROP PARTITION 뒤GLOBAL → UNUSABLERANGE ALL + TABLE ACCESS FULL(에러 없음)3,505
같은 조회 · UPDATE GLOBAL INDEXES로 정리한 뒤GLOBAL · VALID · ORPHANED_ENTRIES = YESINDEX RANGE SCAN3

PK나 UNIQUE를 LOCAL로 만들려면 파티션 키가 인덱스에 들어 있어야 한다. 버거원 PK가 (ORD_NO, ORD_YM)인 것도 그래서이고, 같은 이유로 번호만으로 찾으면 nonprefixed 비용(25칸)을 치른다. 23ai는 UPDATE GLOBAL INDEXES로 칸을 들어낼 때 인덱스 항목을 바로 지우지 않고 고아 항목(orphaned entries)으로 남겨 두었다가 나중에 정리한다. 그래서 들어내는 순간에는 비용이 적다(redo 0.01MB).


8.5 파티션의 크기 — 첫 익스텐트 8MB ​

무엇이 일어나나 ​

  1. 이 버전은 테이블 파티션(서브파티션 포함)의 첫 익스텐트를 8MB(1,024블록)로 잡는다. 파티션 없는 테이블은 64KB부터 시작한다.
  2. 직접 경로 적재(INSERT /*+ APPEND */)는 지금 익스텐트의 남은 블록을 '비었음'으로 표시해 둔 채 새 익스텐트부터 쓴다. 그 빈 블록들은 고수위선 아래에 들어간다.
  3. 그래서 빈 칸(또는 TRUNCATE한 칸)에 APPEND하면 칸마다 빈 블록을 약 1,006개 안게 된다. 전체 스캔은 고수위선까지 다 읽는다.
  4. CTAS는 이렇게 하지 않고 첫 익스텐트부터 채운다.

실측 — 한빛유통 (실습/04 [4] · 6월 한 칸 = 27,390행 · 데이터 블록 160) ​

만드는 법세그먼트 합계6월 칸 빈 블록(fs4)6월 전체 스캔 Buffers
파티션 · CTAS288MB(8MB × 36)—161
파티션 · TRUNCATE → APPEND576MB1,0061,169
파티션 STORAGE (INITIAL 64K) · TRUNCATE → APPEND72MB5169
(비교) 파티션 없음 · 원본47MB—(전체 5,826)

실측 — 버거원 3GB 해부 (실습/09 [1] · 실습/10) ​

테이블 (주문 100만 행)세그먼트할당고수위선 아래 블록2012년 6월 전체 스캔
ORD(파티션 없음)1176MB22,422—
ORD_LIST(LIST 25 · 빈 테이블 + APPEND)24384MB48,2761,980(데이터 971 + 빈 1,006)
ORD_LIST_HASH(LIST 25 × HASH 8 · 빈 테이블 + APPEND)1923,072MB223,4109,039(서브마다 데이터 약 121 + 빈 1,006)
같은 설계 · CTAS1921,536MB26,792982
같은 설계 · CTAS + INITIAL 64K192379MB25,640998
  • 3GB의 절반은 칸(192개)마다 8MB씩 잡아 둔 자리이고(읽지는 않는다), 나머지 절반은 APPEND가 비워 둔 빈 블록이다(전체 스캔이 읽는다). 한 달 조회를 9배로 키운 것은 뒤의 절반이다.
  • 빈 서브파티션(P_DEFAULT)은 세그먼트가 없다(지연 세그먼트 생성). 그래서 200개가 아니라 192개다.
  • 같은 문제가 ORD_LIST(24칸)에도 있다. 칸마다 빈 블록이 1,006개라서 ORD(22,422)의 두 배가 넘는 48,276이 나왔다.

처방 ​

처방언제
파티션 테이블에 STORAGE (INITIAL …)를 명시한다(칸 크기에 맞게)칸이 작고 많을 때. 서브파티션이면 거의 항상
처음 지을 때는 CTAS로 짓는다이관 · 재구성
칸 단위 재적재는 스테이징 테이블을 CTAS로 짓고 EXCHANGE PARTITION으로 바꿔 넣는다주간 재적재처럼 칸을 통째로 갈아 끼울 때(이 화에서는 재지 않았다)
칸 수는 근거를 두고 정한다. 해시 서브파티션은 무엇을 위해 나누는지(partition-wise 조인, 경합 분산) 적어 둔다설계 승인(T2 「적재 비용」 · 「파티션」 칸)

9. 병렬 — Free에서는 계획부터 직렬이다 ​

9.1 개념 (원천 II권 Ch7) ​

용어뜻
QC (Query Coordinator)병렬 문장을 받은 세션. 일을 나눠 주고 결과를 모은다
PX 서버(슬레이브)실제로 일하는 프로세스. 생산자 집합과 소비자 집합 두 벌로 나뉜다(DOP 8이면 최대 16개)
Granule일을 나누는 단위. 블록 범위(PX BLOCK ITERATOR) 또는 파티션(PX PARTITION RANGE ALL)
TQ (Table Queue)생산자에서 소비자로 가는 통로. 실행계획의 TQ 열(Q1,00 · Q1,01)과 PX SEND / PX RECEIVE 줄로 보인다
IN-OUTP->P(서버끼리 재분배) · P->S(QC로 모음) · PCWP(같은 서버 안에서 이어서) · S->P
PQ_DISTRIBUTE조인할 때 행을 어떻게 나눠 보낼지 정한다: HASH HASH · BROADCAST NONE · NONE NONE(partition-wise) …
Downgrade요청한 DOP만큼 서버를 못 얻으면 DOP를 낮춰서(또는 직렬로) 돈다. V$SYSSTAT의 'Parallel operations downgraded …'에 기록된다

9.2 Free에서 실제로 본 것 (실습/08) ​

증거값
힌트 없음 vs PARALLEL(o 8)의 Plan hash value둘 다 4189269332(같은 계획)
실행계획의 PX … 줄없다
Hint ReportU - Unused PARALLEL(o 8)
V$PQ_TQSTAT · V$PX_SESSION0행 · 0행
queries parallelized · Parallel operations downgraded to serial0 · 0. 강등 기록조차 없다. 계획을 세울 때부터 직렬이다
V$SESSION.PQ_STATUS · PDML_STATUS처음부터 DISABLED. FORCE PARALLEL QUERY, ENABLE PARALLEL DML 뒤에도 DISABLED(에러 없음)
INSERT /*+ APPEND PARALLEL(t 4) */ … SELECT /*+ PARALLEL(o 4) */LOAD AS SELECT(직접 경로는 된다) · 두 PARALLEL 모두 Unused
parallel_max_servers · cpu_count · V$OPTION 'Parallel execution'1 · 2 · FALSE

이 DB에서는 "PARALLEL 8을 붙였더니 빨라졌다"는 말도, "안 빨라졌다"는 말도 할 수 없다. 에러가 없으니 붙여 둔 사람은 효과가 있었다고 믿기 쉽다. 이 화의 판정은 '판정 불가 → 힌트를 뺀다 → 운영에서 단계별 시각과 함께 다시 잰다'였다.

9.3 판독 과제 — EE의 병렬 계획은 이렇게 생겼다 ​

⚠️ 아래는 이 컨테이너에서는 나오지 않는 모양이다. 오라클 문서(VLDB and Partitioning Guide의 Using Parallel Execution)가 설명하는 실행계획의 형태를 우리 테이블 이름으로 옮겼다. 숫자 열(Rows · Cost · Time)은 잰 값이 아니어서 일부러 비웠다.

xplan
SELECT /*+ PARALLEL(o 4) PARALLEL(c 4) */ c.CUS_GD, SUM(o.ORD_AMT)
  FROM T_ORD_BIG o JOIN M_CUS c ON c.CUS_ID = o.CUS_ID GROUP BY c.CUS_GD;

| Id | Operation                     | Name      |    TQ  | IN-OUT | PQ Distrib |
|  0 | SELECT STATEMENT              |           |        |        |            |
|  1 |  PX COORDINATOR               |           |        |        |            |
|  2 |   PX SEND QC (RANDOM)         | :TQ10003  |  Q1,03 | P->S   | QC (RAND)  |
|  3 |    HASH GROUP BY              |           |  Q1,03 | PCWP   |            |
|  4 |     PX RECEIVE                |           |  Q1,03 | PCWP   |            |
|  5 |      PX SEND HASH             | :TQ10002  |  Q1,02 | P->P   | HASH       |
|  6 |       HASH GROUP BY           |           |  Q1,02 | PCWP   |            |
|  7 |        HASH JOIN              |           |  Q1,02 | PCWP   |            |
|  8 |         PX RECEIVE            |           |  Q1,02 | PCWP   |            |
|  9 |          PX SEND HASH         | :TQ10000  |  Q1,00 | P->P   | HASH       |
| 10 |           PX BLOCK ITERATOR   |           |  Q1,00 | PCWC   |            |
| 11 |            TABLE ACCESS FULL  | M_CUS     |  Q1,00 | PCWP   |            |
| 12 |         PX RECEIVE            |           |  Q1,02 | PCWP   |            |
| 13 |          PX SEND HASH         | :TQ10001  |  Q1,01 | P->P   | HASH       |
| 14 |           PX BLOCK ITERATOR   |           |  Q1,01 | PCWC   |            |
| 15 |            TABLE ACCESS FULL  | T_ORD_BIG |  Q1,01 | PCWP   |            |

읽을 것: ① QC는 몇 번 줄인가 ② Granule은 블록 범위인가, 파티션인가 ③ TQ는 몇 개이고 각각 무엇을 보내나 ④ 지금은 두 테이블을 HASH로 재분배했다. PQ_DISTRIBUTE(o BROADCAST NONE)(조인의 두 번째 테이블 o 기준: 바깥 c는 BROADCAST, 안쪽 o는 NONE)로 바꾸면 9번과 13번 줄은 무엇이 되나 ⑤ DOP 4를 다 얻지 못하면 무슨 일이 일어나고 어디에 기록되나. 정답은 3_실습과_기록.md STEP 9에 있다.

대규모 병렬 실측(서버 수, TQ별 행 분포 V$PQ_TQSTAT, 다운그레이드)은 별권에서 EE로 한다.


10. P1 — rev00 → rev01 → rev02 ​

10.1 측정 (실습/05 · 10만 건 · 각 판 TRUNCATE부터 · 워밍업 1회 뒤) ​

판경과redoundo커밋논리 읽기블록 변경
rev00 커서 FOR 루프 · 1,000건 커밋0.89초28.66MB6.50MB101110,444204,942
rev01 INSERT … SELECT · 커밋 한 번0.08초4.47MB0.26MB27,4084,791
rev02 + APPEND0.11초0.08MB0.02MB22,858827

2회차: rev00 0.89초 · 28.66MB · 6.50MB / rev01 0.23초 · 4.47MB · 0.26MB / rev02 0.11초 · 0.08MB · 0.02MB. redo와 undo는 같고 초만 흔들린다(rev01 0.08 ↔ 0.23).

  • 커밋 수에는 TRUNCATE의 커밋 1이 들어 있다(13화 §7과 같다).
  • rev01의 redo가 rev00의 1/6인 까닭: 한 문장 INSERT는 여러 행을 한 블록에 한꺼번에 넣고, 그 묶음 단위로 redo와 undo를 적는다. 루프는 행마다 따로 적는다.
  • rev02의 undo 0.02MB: 직접 경로는 고수위선 위의 새 블록에 쓰므로, 롤백은 '고수위선을 안 올리는 것'으로 끝난다. 데이터를 되돌릴 정보를 적지 않는다(← 13화 정정 전 사본함). 2020.07에 강도현이 1,000건씩 끊은 근거(ORA-30036)가 이 판에서는 사라진다.

10.2 13화 「커밋 주기 기준」과의 관계 ​

결정표P1
#1 집합 연산 한 문장으로 쓸 수 있나? → 그렇게 쓴다. 커밋은 끝에 한 번. 언두가 걱정이면 14화쓸 수 있다 → rev01. 언두 걱정 → rev02(undo 0.02MB)
#2 fetch across commit인가?아니다(13화 판정 그대로). 이제 루프 자체가 없다

두 기준은 충돌하지 않는다. 13화 결정표의 1번 줄에서 멈추는 경우이고, 14화는 그 줄의 "언두가 걱정이면"에 숫자를 붙였다. 대가는 §4의 배타 락이다. P1이 마감 전에 끝난다는 전제에서만 안전하다.

10.3 남의 눈 — 적재 중에 보이는 것 (실습/05 [남의 눈]) ​

판5만 번째 행을 넣는 순간 다른 트랜잭션이 세는 정산
rev00(1,000건마다 커밋)50,000(반쯤 적재된 상태가 보인다. 09:03 조민우)
rev01 · rev02(한 문장)중간이 없다. TRUNCATE 뒤 커밋까지 0건, 커밋하는 순간 100,000

10.4 정합성 회귀 (체크리스트 A · B · E · H) ​

#항목결과
A기준rev00 결과(강도현 원본)
B건수 · 합계 · 양방향 MINUS · 번호 중복100,000 = 100,000 · 25,519,833,100 = 25,519,833,100 · 0 / 0 · 0 → 통과
E경계: 번호(STLM_SEQ)는 ORDER BY 없이 읽은 순서로 붙는다지금 T_ORD는 ORD_SEQ 순으로 쌓여 있어 차이가 0이다. 원천이 다시 쌓이면(MOVE, 재적재) 바뀔 수 있다. rev00도 같다
H영향: 정산 테이블을 읽는 화면적재하는 동안 0건 → 커밋 순간 전부. 반쯤 적재된 상태는 사라진다. 마감을 넘기면 배타 락으로 쓰기가 막힌다

11. 월말 정리 — DELETE · SHRINK · DROP PARTITION ​

T_ORD_LOG(05화 생성식 · 50만 행 · 900일 · 주문번호 인덱스 1개) · 한 달 = 2023년 1월 17,235행(실습/06)

정리 방법redoundo블록 변경경과그 뒤 전체 스캔
DELETE4.84MB2.32MB36,6910.06초3,602 → 3,602
(매달 청소) SHRINK SPACE17.96MB6.62MB126,5772.21초3,489
DROP PARTITION0.01MB0.01MB960.04초3,630 → 3,505
DROP PARTITION … UPDATE GLOBAL INDEXES (다음 달)0.01MB0.01MB990.03초인덱스 VALID
(비교) 다음 달을 DELETE4.45MB2.11MB33,8700.05초—

2회차: DELETE 4.84MB · SHRINK 18.00MB → 전체 스캔 3,492 · DROP PARTITION 0.01MB(블록 변경 97). redo는 같고 SHRINK 뒤 블록 수만 몇 개 다르다.

  • 05화의 숫자(3,602)가 같은 생성식에서 그대로 나온다. DELETE는 분필 선을 내리지 않는다.
  • SHRINK는 지운 한 달보다 3.7배 비싸다. 남은 행을 전부 옮겨 쓰기 때문이다. 게다가 매달 해야 한다.
  • 파티션 30칸(+ 빈 PMAX)으로 지은 이력 테이블은 세그먼트 30개 · 240MB를 차지한다(파티션 없는 쪽은 29MB). §8.5에서 본 8MB 첫 익스텐트 때문이다. CTAS로 지었으므로 빈 블록은 없다(전체 스캔 3,630 ≈ 3,602).
  • 연습 DB에서 한 달 DELETE는 0.06초다. 월말 정리의 청구서는 시간이 아니라 '읽는 양'과 '매달의 청소'로 온다.

12. 이 화의 측정값 전부 ​

체인 단계 분해 (연습 DB · 단독 실행) ​

단계지금 (운영 그대로)바꾼 뒤
① 재적재 100만 · 인덱스 3개일반 INSERT 329초(2회차 542초) · redo 916.6MBAPPEND + 끄고 켜기 5.0초 · redo 0.6MB
① 통계 수집1.06초1.06초(유지: 히스토그램)
② 월말 이력 정리DELETE 0.06초 · 전체 스캔 3,602 그대로DROP PARTITION 0.04초(파티션 승인 시)
③ 월마감 집계 PARALLEL(8) (대역: 고객별 합계)0.05초 · 힌트 Unused힌트 제거 · 운영 재측정
④ P1rev00 0.89초 · 28.66MBrev02 0.11초 · 0.08MB

측정 환경 ​

Oracle AI Database 26ai Free Release 23.26.2.0.0 · cpu_count 2 · db_block_size 8192 · NOARCHIVELOG · FORCE_LOGGING NO · 온라인 리두 로그 10MB × 2 · log_buffer 약 4MB · parallel_max_servers 1 · V$OPTION Partitioning TRUE · Parallel execution FALSE · skip_unusable_indexes TRUE · deferred_segment_creation TRUE. 버거원: 같은 버전 · dev 스키마(L2 카드 §4 적재 · 통계 100%).


13. Free에서 못 잰 것 ​

못 잰 것왜대신 한 것
병렬 실행(PX 서버 · TQ 분배 · 다운그레이드)Free는 병렬을 하지 않는다. 계획부터 직렬이다(§9.2)'안 됐다'는 증거 다섯 가지 실측 + PX 계획 판독 과제(§9.3) · 대규모 병렬은 별권
ARCHIVELOG에서의 직접 경로 redo · NOLOGGING 효과로그 모드를 바꾸려면 인스턴스를 재기동해야 하고, 이 교재의 공용 컨테이너 상태가 바뀐다NOARCHIVELOG에서 [C] ≈ [D]를 실측하고, 운영과의 차이는 §5 표로 정리했다
다중 세션의 오른쪽 끝 블록 경합 크기CPU 2개. 12화에서 '이벤트가 생긴다'까지만 쟀다한 세션의 분할 종류 · redo · 범위 검색 상실(§2.4)
운영 체인의 단계별 소요운영 로그에 단계별 시각이 없다연습 DB에서 단계별로 재현했다(T3 재발방지 1번)
데이터 증가에 따른 추세선위와 같다T3 「추세」 칸은 단계별 시각을 수집한 뒤에 채운다
EXCHANGE PARTITION 재적재이 화의 범위를 넘는다§8.5 처방 표에 '재지 않았다'고 표기

14. 원천 좌표 ​

전체 지도 → 00_자료준비/06_원서와_공식문서_지도.md PDF 페이지 = 책 페이지 − 2 · 성능고도화II_목차색인.md (공개 사이트에는 없음)

성능고도화 II권 (주 원천) ​

주제책 페이지
Ch6 테이블 파티셔닝: 기본 구조 · Range · 해시 · 리스트 · 결합624 · 625 · 629 · 630 · 632 · 633
파티션 Pruning: 기본 · 서브쿼리 · 조인 필터 · SQL 조건절 작성 시 주의사항638 · 645 · 648 · 654
인덱스 파티셔닝: 로컬 · 비파티션 · 글로벌 · Prefixed vs Nonprefixed · 구성 예시 · 글로벌의 효용성657 · 659 · 660 · 660 · 662 · 663 · 668
파티셔닝 선택 기준673
Ch7 병렬 처리: 기본 개념 · 테이블 큐 · IN-OUT · 데이터 재분배 · Granule · 대기이벤트682 · 685 · 688 · 690 · 692 · 694
병렬 Order By · Group By · 병렬 조인698 · 700 · 706
(Ch2) 조인을 내포한 DML · MERGE · 다중 테이블 INSERT288 · 295 · 300
(I권) Direct Path I/O (Read · Write)457 · 459 · 460

오라클 공식문서 ​

주제매뉴얼장 제목
직접 경로 INSERT · APPEND · 제약 조건adminManaging Tables: Loading Tables (Direct-Path INSERT)
INSERT 힌트 · NOLOGGINGsqlrfINSERT · Comments (Hints)
파티셔닝 · Pruning · 파티션 인덱스 · 파티션 유지보수(UPDATE INDEXES)vldbgPartitioning Concepts · Partitioning for Availability, Manageability, and Performance · Maintenance of Partitions
병렬 실행 (QC · Granule · TQ · Downgrade)vldbgUsing Parallel Execution
온라인 통계 (대량 적재)tgsqlOptimizer Statistics Concepts: Online Statistics Gathering for Bulk Loads
V$PQ_TQSTAT · V$PX_SESSION · V$LOCK · DBA_INDEX_USAGErefrnDynamic Performance Views · Static Data Dictionary Views
에디션별 기능(파티셔닝 = EE 유상 옵션 · Free의 제한)dblicLicensing Information

이 화에서 꼭 확인할 뷰 · 통계 ​

sql
V$MYSTAT / V$STATNAME   -- redo size · undo change vector size · db block changes · session logical reads
                        -- leaf node splits · leaf node 90-10 splits
V$SESSION_EVENT         -- log file switch (checkpoint incomplete) · log file switch completion
V$LOCK                  -- TM 의 LMODE (3 vs 6)
DBMS_SPACE.SPACE_USAGE  -- full · fs1~fs4 · unformatted  (파티션 · 서브파티션 단위로)
USER_SEGMENTS           -- 세그먼트 수 · bytes · extents · initial_extent
USER_TAB_COL_STATISTICS -- histogram · notes (STATS_ON_LOAD)
DISPLAY_CURSOR(FORMAT => 'ALLSTATS LAST +PARTITION +HINT_REPORT +NOTE')

다음 화가 여기서 가져가는 것 ​

화가져가는 것
15커서 FOR 루프는 왜 느린가: rev00 0.89초와 rev01 0.08초의 차이가 어디서 나는지(SQL ↔ PL/SQL 왕복) · 한 문장으로 못 바꾸는 행 단위 로직 · T3 2호
16 · 17운영 체인을 시간으로 본다. 단계별 시각이 없어서 연습 DB에서 재현해야 했던 부분(AWR · ASH)
18「SQL 코딩 가이드」 후보: 파티션 키는 가공하지 않는다 · 적재 방식(일반/APPEND/끄고 켜기)을 배치 머리에 적는다 · APPEND를 붙였으면 LOAD AS SELECT를 확인한다 · T3 · T2 개정