테마
05화 · 파고들기
필요할 때 찾아보는 문서다. 처음 읽을 때는
1_사건.md만으로 충분하다.
| 찾는 것 | 바로 가기 |
|---|---|
| 테이블스페이스 / 세그먼트 / 익스텐트 / 블록 | §1 4계층 |
| 익스텐트가 왜 64KB였다가 1MB가 되나 | §2 AUTOALLOCATE |
| 고수위선(HWM)이 무엇이고 왜 안 내려가나 | §3 HWM |
☠️ SHRINK · MOVE · TRUNCATE 중 무엇을 쓸까 | §4 회수 |
블록 안은 어떻게 생겼나 (ITL · 행 디렉터리 · PCTFREE) | §5 블록 내부 |
| 행 이전 vs 행 연결 | §6 이전과 연결 |
| ROWID에 테이블스페이스가 없는 이유 | §7 ROWID |
VARCHAR2(4000)은 무엇을 쓰고 무엇을 안 쓰나 | §8 선언 길이 |
| 측정값 전체 | §9 측정 |
| 공식문서 좌표 | §10 좌표 |
1. 4계층
테이블스페이스 ─ 데이터파일 묶음. ★ 블록 크기를 여기서 정한다
└ 세그먼트 ─ 테이블 하나 / 인덱스 하나 / 파티션 하나가 차지한 덩어리
└ 익스텐트 ─ 연속된 블록 묶음. 공간이 모자랄 때 '한 번에' 받는 단위
└ 블록 ─ I/O 와 버퍼캐시의 최소 단위. 여기선 8KB| 층 | 보는 뷰 | 핵심 컬럼 |
|---|---|---|
| 테이블스페이스 | USER_TABLESPACES / DBA_DATA_FILES | block_size · extent_management · allocation_type · segment_space_management |
| 세그먼트 | USER_SEGMENTS | blocks(할당) · extents · bytes |
| 익스텐트 | USER_EXTENTS | extent_id · blocks · bytes |
| 블록 | USER_TABLES · DBMS_SPACE.SPACE_USAGE | blocks(HWM 아래) · FS1~FS4 / FULL |
같은 테이블의 블록 수가 네 개인 이유 — T_ORD_BIG 실측
| 숫자 | 뜻 | 값 |
|---|---|---|
USER_SEGMENTS.blocks | 할당받은 전부 (익스텐트 단위라 끝에 자투리가 있다) | 6,016 |
USER_TABLES.blocks | 고수위선 아래 (통계가 센 값) | 5,923 |
전체 스캔 Buffers | 실제로 읽은 블록 | 5,826 |
DBMS_ROWID로 센 distinct 블록 | 행이 실제로 들어 있는 블록 | 5,813 |
네 숫자는 항상 위에서부터 크거나 같다. 차이를 설명하지 못한다면 아직 이해하지 못한 것이다.
5,923 − 5,813 = 110이다. 고수위선 아래에 있지만 행이 하나도 없는 블록이 110개이고, 전체 스캔은 그 110개도 읽는다.
01화의 "한 블록에 169건"은 어느 숫자인가
| 기준 | 계산 | 값 |
|---|---|---|
01화 (통계 blocks) | 1,000,000 / 5,923 | 169 |
05화 (DBMS_ROWID로 센 실제 사용 블록) | 1,000,000 / 5,813 | 172 |
둘 다 맞다. 다른 것을 센 것이다.
2. 익스텐트와 AUTOALLOCATE
USERS 테이블스페이스는 extent_management = LOCAL, allocation_type = SYSTEM이다. 이것이 AUTOALLOCATE로, 익스텐트 크기를 오라클이 정한다.
sql
SELECT bytes/1024 AS "크기_KB", blocks, COUNT(*), MIN(extent_id), MAX(extent_id)
FROM user_extents WHERE segment_name='T_ORD_BIG' GROUP BY bytes, blocks ORDER BY 1;| 세그먼트 | 64KB(8블록) | 1MB(128블록) | 합계 익스텐트 |
|---|---|---|---|
T_ORD_BIG (100만 행) | 16개 (extent_id 0~15) | 46개 (16~61) | 62 |
T_ORD_LOG (50만 행) | 16개 (0~15) | 28개 (16~43) | 44 |
규칙: 처음 16개는 64KB, 그다음부터는 1MB다. 세그먼트가 더 커지면 8MB, 64MB로 커진다.
| 왜 균일하지 않은가 | |
|---|---|
| 작은 테이블에 1MB를 떼 주면 | 테이블이 수천 개인 스키마에서는 디스크가 낭비된다 |
| 큰 테이블에 64KB씩 주면 | 익스텐트 수가 수만 개가 되고 사전 관리 비용이 커진다 |
UNIFORM SIZE로 만든 테이블스페이스는 익스텐트 크기가 모두 같다. 이때는TRUNCATE후에도 첫 익스텐트 크기가 UNIFORM 값이다.
3. 고수위선 — 왜 DELETE로는 안 내려가나
고수위선(High Water Mark) = 그 세그먼트에서 지금까지 포맷해서 써 본 가장 끝 블록의 위치.
세 문장으로
- 전체 스캔(
TABLE ACCESS FULL)은 세그먼트 첫 블록부터 고수위선까지 읽는다. 기준은 "행이 어디까지 있나"가 아니다. DELETE는 블록 안의 행만 지운다. 블록을 반납하지도, 고수위선을 내리지도 않는다.- 고수위선은 올라갈 때는 자동이지만, 내려갈 때는 명시적인 명령이 필요하다.
왜 그렇게 만들었나
DELETE가 고수위선을 자동으로 내리려면 DELETE할 때마다 남은 행을 앞으로 재배치해야 한다. 그러면 이런 문제가 생긴다.
DELETE의 비용이 테이블 크기에 비례해 폭증한다- 행의
ROWID가 바뀌므로 모든 인덱스를 같이 고쳐야 한다 - 동시에 그 블록을 읽는 다른 세션과 충돌한다
그래서 오라클은 "지우는 것"과 "정리하는 것"을 분리했다. 정리는 사용자가 명령할 때만 한다.
실측 — 50만 행 중 95% 삭제
| 시점 | 행수 | HWM 아래 블록 | 세그먼트 블록 | 익스텐트 | Buffers |
|---|---|---|---|---|---|
| 적재 직후 | 500,000 | 3,665 | 3,712 | 44 | 3,602 |
DELETE 475,025행 후 | 24,975 | 3,665 | 3,712 | 44 | 3,602 |
한 블록도 줄지 않는다. A-Rows만 100,000 → 4,995로 줄었다.
DBMS_SPACE.SPACE_USAGE — 블록 상태 분포
ASSM(segment_space_management = AUTO) 테이블스페이스에서만 쓸 수 있다. OUT 파라미터가 12개다.
sql
DBMS_SPACE.SPACE_USAGE(
segment_owner, segment_name, segment_type,
unformatted_blocks, unformatted_bytes,
fs1_blocks, fs1_bytes, -- 0~25% 가 비어 있는 블록
fs2_blocks, fs2_bytes, -- 25~50%
fs3_blocks, fs3_bytes, -- 50~75%
fs4_blocks, fs4_bytes, -- 75~100%
full_blocks, full_bytes);| 상태 | DELETE 직후 | SHRINK SPACE COMPACT 후 |
|---|---|---|
UNFORMATTED | 0 | 0 |
FS1 (0~25% 빔) | 0 | 0 |
FS2 (25~50% 빔) | 0 | 0 |
FS3 (50~75% 빔) | 473 | 1 |
FS4 (75~100% 빔) | 3,123 | 3,416 |
FULL | 0 | 179 |
⚠️ MSSM(수동 세그먼트 공간 관리) 테이블스페이스에서는
SPACE_USAGE의 시그니처가 다르다(OUT 3개). 에러가 나면USER_SEGMENTS.blocks+USER_TABLES.blocks+USER_TABLES.empty_blocks조합으로 대신하고, 그 사실을 기록에 남긴다. 이 화의 측정은 모두 ASSM(USERS, AUTO)에서 했다.
4. 고수위선을 내리는 네 가지 길
같은 출발점(50만 적재 → 95% 삭제, 남은 24,975행)에서 각각 측정했다.
| 조치 | HWM 아래 블록 | 세그먼트 블록 | 익스텐트 | Buffers | 되돌리기 | 인덱스 |
|---|---|---|---|---|---|---|
| (아무것도 안 함) | 3,665 | 3,712 | 44 | 3,602 | — | 그대로 |
SHRINK SPACE COMPACT | 3,665 | 3,712 | 44 | 3,602 | 가능(롤백 아님) | 유지됨 |
SHRINK SPACE | 180 | 200 | 17 | 183 | 불가 | 유지됨 |
ALTER TABLE MOVE | 192 | 256 | 17 | 183 | 불가 | ☠️ UNUSABLE |
TRUNCATE TABLE | 0 | 8 | 1 | 1 | ☠️ 불가 · WHERE 불가 | UNUSABLE 아님 |
고르는 기준
| 상황 | 쓸 것 | 이유 |
|---|---|---|
| 업무 시간에 미리 준비만 | SHRINK SPACE COMPACT | 행만 모은다. 잠금 영향이 작다. 고수위선은 안 내려간다 |
| 야간에 마무리 | SHRINK SPACE | 모으기 + 고수위선 하강. 마지막 순간만 짧게 전체 잠금 |
| 세그먼트를 다른 테이블스페이스로 옮기거나, 압축이나 재정렬도 함께 할 때 | ALTER TABLE MOVE | 세그먼트를 새로 만든다. 작업 중 공간이 두 배 필요 |
| 전부 버려도 될 때 (보관 테이블로 옮긴 뒤) | TRUNCATE | 가장 싸고 가장 위험하다 |
⚠️ 안전 수칙
SHRINK 전에 ENABLE ROW MOVEMENT 설정이 필요하다 | 이 설정은 "행의 ROWID가 바뀌어도 좋다"는 허락이다. 뜻을 모르면 켜면 안 된다 |
MOVE 후에는 반드시 ALTER INDEX ... REBUILD | 안 하면 그 인덱스를 쓰는 쿼리가 전부 ORA-01502로 터진다 |
TRUNCATE는 WHERE가 없고 롤백이 안 된다 | 보관할 게 하나라도 있으면 못 쓴다 |
작업하기 전에 Buffers를 재 둔다 | 재 두지 않으면 "좋아졌다"를 증명할 수 없다(T0 규약) |
| 셋 다 redo를 만든다 | 월말 배치 창에 끼워 넣을 때 그 비용까지 계산해야 한다 → 14화 |
MOVE가 안 고치는 것
ALTER TABLE MOVE는 지금 놓여 있는 물리적 순서 그대로 새 세그먼트에 복사한다. 행 이전으로 순서가 흐트러진 테이블을 MOVE하면 흐트러진 순서가 그대로 굳는다.
CLUSTERING_FACTOR | 인덱스 경유 Buffers | |
|---|---|---|
행 이전 상태 (T_MIG00) | — | 30,549 |
MOVE + INDEX REBUILD 후 | 약 52,200 | 약 10,400 |
처음부터 바르게 적재 (T_MIG_OK) | 3,449 | 735 |
이전은 사라졌지만(
CHAIN_CNT0) 735로는 돌아가지 못한다. 순서까지 고치려면CREATE TABLE ... AS SELECT ... ORDER BY로 다시 적재해야 한다(CF의 원리는 04화에서 봤다).
5. 블록 내부
8KB 블록 하나는 이렇게 생겼다.
┌──────────────────────────────────────────────┐
│ 블록 헤더 (seg/obj · scn · itc · flg · typ) │
│ ITL 슬롯 (itc 개. 트랜잭션 한 칸씩 차지) │
│ 행 디렉터리 (pri[0], pri[1], ... 행의 오프셋) │
├──────────────────────────────────────────────┤
│ ↓ 여기가 빈 공간 (avsp) ← PCTFREE 로 예약 │
├──────────────────────────────────────────────┤
│ ↑ 행 데이터가 블록 끝에서부터 거꾸로 쌓인다 │
│ row N ... row 2 row 1 row 0 │
└──────────────────────────────────────────────┘블록 덤프 실측 (T_MIG10의 한 블록 · PCTFREE 10 · 이전행 74개)
seg/obj: 0x11e00 itc: 2 flg: E- typ: 1 - DATA
Itl Xid Uba Flag Lck Scn/Fsc
0x01 0x0002.015.0000026b 0x000066ce.0136.07 C--- 0 scn 0x...23514a
0x02 0x0005.013.00000268 0x00013ae5.0159.3c --U- 100 fsc 0x00ea....
ntab=1 nrow=100 frre=-1 fsbo=... fseo=... avsp=0x2 tosp=0xec
0x12:pri[0] offs=0x1e8c
0x14:pri[1] offs=0x1d80| 필드 | 뜻 |
|---|---|
itc | ITL(Interested Transaction List) 슬롯 수. 이 블록을 동시에 변경할 수 있는 트랜잭션 수 |
Itl의 Flag | C--- 커밋 완료 · --U- 커밋됐지만 블록 정리(클린아웃) 전 · ---- 빈 슬롯 |
Itl의 Lck | 그 트랜잭션이 이 블록에서 잠근 행 수 |
nrow | 이 블록의 행 개수 (= 행 디렉터리 항목 수) |
pri[n] | n번째 행이 블록 안 몇 번지에 있는지. ROWID 마지막 조각의 숫자가 이 배열의 첨자다 |
avsp | 현재 남은 빈 공간 (available space). PCTFREE 0이면 거의 0 |
tosp | 총 잠재 공간 (total space). 지워진 행 자리까지 포함 |
ITL이 모자라면 어떻게 되는가는 이 화의 범위 밖이다. 여기서는 블록 헤더에
itc칸이 있고INI_TRANS/MAX_TRANS로 조절한다는 것까지만 본다. 나머지는 12화(락)와 13화(undo·클린아웃)에서 다룬다.
PCTFREE — 무엇을 위한 예약인가
PCTFREE n: 블록을(100-n)%까지만 채우고, 나머지는 그 블록에 이미 들어 있는 행이 커질 때를 대비해 남겨 둔다.
PCTFREE | 블록당 행수 (72바이트 행) | UPDATE 전 블록 |
|---|---|---|
| 0 | 100 | 1,000 |
| 10 (기본값) | 89 | 1,126 |
| 40 | 61 | 1,630 |
| 75 | 27 | 3,772 |
PCTFREE는 새 행을 위한 공간이 아니다. 새로 INSERT하는 행은 PCTFREE 선을 넘지 못한다. 그 공간은 오직 UPDATE로 기존 행이 길어질 때만 쓰인다.
6. 행 이전(migration)과 행 연결(chaining)
이름이 비슷하고 둘 다 CHAIN_CNT 컬럼 하나에 잡히지만, 원인도 처방도 다르다.
| 행 이전 (row migration) | 행 연결 (row chaining) | |
|---|---|---|
| 원인 | UPDATE로 행이 커졌는데 그 블록에 자리가 없다 | 행 자체가 블록보다 크다 (또는 컬럼이 255개를 넘는다) |
| 블록에 남는 것 | 머리만 (fb: --H-----) + nrid 포인터 | 첫 조각 (fb: --H-F---) |
ROWID | 안 바뀐다 (그래서 인덱스를 안 고쳐도 된다) | 안 바뀐다 |
| 전체 스캔 비용 | 거의 안 는다 | 뒤 조각을 읽으면 는다 |
| 인덱스 경유 비용 | 폭증한다 | 는다 |
| 처방 | PCTFREE 조정 · MOVE · 설계 변경 | 블록 크기 / 컬럼 설계 변경. MOVE로는 못 고친다 |
잡아내는 법
sql
-- CHAINED_ROWS 테이블이 없으면: @?/rdbms/admin/utlchain.sql
ANALYZE TABLE <테이블> LIST CHAINED ROWS INTO CHAINED_ROWS;
SELECT * FROM CHAINED_ROWS WHERE table_name = '<테이블>';
-- 건수만 보려면
ANALYZE TABLE <테이블> COMPUTE STATISTICS;
SELECT chain_cnt, num_rows, blocks FROM user_tables WHERE table_name='<테이블>';☠️
CHAIN_CNT는ANALYZE로만 채워진다.DBMS_STATS는 이 컬럼을 건드리지 않는다.DBMS_STATS로 통계를 모은 테이블의CHAIN_CNT가 0이라고 해서 이전이 없는 게 아니다. 통계 수집 자체는DBMS_STATS로 하는 것이 표준이다.ANALYZE는 이 목적으로만 쓴다.
계기판
sql
SELECT s.name, m.value FROM v$mystat m, v$statname s
WHERE m.statistic#=s.statistic# AND s.name = 'table fetch continued row';table fetch continued row는 행의 머리를 읽은 뒤 다음 조각을 찾아 한 번 더 간 횟수다. 2만 건 조회에서 T_MIG00은 15,252, T_MIG_OK는 0이었다.
실측 — 같은 최종 데이터, 만드는 방법만 다름 (10만 행)
| 만든 방식 | UPDATE 전 블록 | 최종 블록 | CHAIN_CNT | 이전율 | 전체 스캔 | 인덱스 경유 |
|---|---|---|---|---|---|---|
PCTFREE 0 + 나중에 UPDATE | 1,000 | 3,898 | 76,169 | 76.2% | 3,904 | 30,549 |
PCTFREE 10 + 나중에 UPDATE | 1,126 | 4,150 | 73,494 | 73.5% | — | 29,482 |
PCTFREE 40 + 나중에 UPDATE | 1,630 | 5,158 | 56,984 | 57.0% | — | 23,083 |
PCTFREE 75 + 나중에 UPDATE | 3,772 | 3,772 | 0 | 0% | — | 786 |
| 처음부터 최종 길이로 적재 | 3,520 | 3,520 | 0 | 0% | 3,469 | 735 |
📌 이 표가 05화에서 가장 중요하다
① 전체 스캔으로는 행 이전이 거의 드러나지 않는다(3,904 vs 3,469 = 1.13배). 인덱스를 거쳐야 드러난다(30,549 vs 735 = 41.6배). 두 실행계획 모두
INDEX RANGE SCAN줄은 45블록으로 같다. 차이는 전부TABLE ACCESS BY INDEX ROWID에서 난다.②
PCTFREE는 눈금이 아니라 문턱이다. 72바이트 행이 270바이트가 되면PCTFREE40으로도 57%가 이사 간다. 75를 줘야 0이 된다.③ 그런데
PCTFREE 75로 만들면UPDATE전까지 테이블이 3.8배 뚱뚱해진다. 공짜가 아니다.④ 진짜 답은
PCTFREE가 아니라 설계다. 코드 컬럼 하나에 나중에 설명 200자를 덧붙이지 않으면 된다.
행 연결 실측 — VARCHAR2(4000) 세 개를 꽉 채우면
| 값 | |
|---|---|
| 행수 | 5,000 |
AVG_ROW_LEN | 12,022 (블록 8,192보다 크다) |
| 블록 | 15,197 |
| 행당 블록 | 3.04 |
CHAIN_CNT | 5,000 (전부) |
| 읽는 컬럼 | Buffers | table fetch continued row |
|---|---|---|
COUNT(C1) (첫 조각) | 15,207 | 0 |
COUNT(C3) (끝 조각까지) | 25,207 | 9,999 |
실무 규칙 두 개 ⓐ 한 행이 블록을 넘지 않게 설계한다. 넘을 수밖에 없으면 자주 읽는 컬럼을 앞쪽에 둔다. ⓑ
SELECT *를 쓰면 이 규칙이 무력화된다. 뒤 조각까지 전부 따라가기 때문이다.
7. ROWID 네 조각
AAAR2eAAYAAABgLAAA
└──┬──┘└┬┘└──┬──┘└┬┘
│ │ │ └ 행번호 (3자) : 블록 안 행 디렉터리의 첨자
│ │ └───── 블록번호 (6자)
│ └────────── 상대파일번호(3자)
└──────────────── 데이터오브젝트번호(6자)sql
SELECT ROWID,
DBMS_ROWID.ROWID_OBJECT(ROWID), -- 73118
DBMS_ROWID.ROWID_RELATIVE_FNO(ROWID), -- 24
DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID), -- 6155
DBMS_ROWID.ROWID_ROW_NUMBER(ROWID) -- 0
FROM T_ORD_BIG WHERE ROWNUM <= 3;ROWID | 오브젝트 | 상대파일 | 블록 | 행번호 |
|---|---|---|---|---|
AAAR2eAAYAAABgLAAA | 73118 | 24 | 6155 | 0 |
AAAR2eAAYAAABgLAAB | 73118 | 24 | 6155 | 1 |
AAAR2eAAYAAABgLAAC | 73118 | 24 | 6155 | 2 |
★ 테이블스페이스가 없다 — 세 가지 증거
① 네 조각 어디에도 없다. 오브젝트, 파일, 블록, 행 번호가 전부다.
② 상대 파일 번호는 테이블스페이스마다 따로 센다. 실측값은 다음과 같다.
| 테이블스페이스 | 빅파일 | 절대 파일번호 | 상대 파일번호 |
|---|---|---|---|
USERS | NO | 24 | 24 |
SYSAUX | YES | 22 | 1024 |
SYSTEM | YES | 21 | 1024 |
UNDOTBS1 | YES | 23 | 1024 |
1024번이 세 개다. ROWID에 "파일 1024"라고만 적혀 있으면 어느 테이블스페이스인지 알 길이 없다. 빅파일 테이블스페이스는 데이터파일이 하나뿐이어서 상대 파일 번호가 항상 1024로 고정된다.
③ 오라클의 API부터 그렇게 생겼다.
sql
DBMS_ROWID.ROWID_TO_ABSOLUTE_FNO(row_id, schema_name, object_name)절대 파일 번호를 구하려면 소유자와 객체 이름을 같이 줘야 한다. ROWID 안에 그 정보가 있었다면 인자가 하나면 됐을 것이다.
그래서 따라오는 결과
ROWID는 영구 주소가 아니다 | "지금 놓여 있는 자리"다 |
ALTER TABLE MOVE를 하면 | 블록 번호가 전부 바뀌어 인덱스가 UNUSABLE 상태가 된다 |
SHRINK SPACE를 하려면 | ENABLE ROW MOVEMENT가 필요하다 (= ROWID가 바뀌어도 된다는 허락) |
행 이전은 ROWID를 안 바꾼다 | 그래서 인덱스를 안 고쳐도 되고, 그래서 조회가 두 번 간다 |
애플리케이션이 ROWID를 저장해 두면 | 위 작업 한 번에 전부 무효가 된다 |
블록당 행수를 ROWID로 세기
sql
SELECT COUNT(*),
COUNT(DISTINCT DBMS_ROWID.ROWID_RELATIVE_FNO(ROWID)||'_'||
DBMS_ROWID.ROWID_BLOCK_NUMBER(ROWID))
FROM T_ORD_BIG;T_ORD_BIG 분포 실측: 172행인 블록이 5,750개, 177행 53개, 173행 5개, 176행 2개, 179행 1개, 174행 1개.
8. 선언 길이가 가져가는 것
같은 데이터를 20만 행씩 넣었다. T_W4000은 VARCHAR2(4000) 세 개, T_W30은 VARCHAR2(2)/(30)/(100)이다.
| 무엇을 재나 | VARCHAR2(4000) | 알맞게 선언 | 차이 |
|---|---|---|---|
USER_TABLES.blocks | 874 | 874 | 없음 |
AVG_ROW_LEN | 23 | 23 | 없음 |
USER_SEGMENTS.blocks | 896 | 896 | 없음 |
AVG_COL_LEN (ORD_NM) | 12 | 12 | 없음 |
전체 스캔 Buffers | 821 | 821 | 없음 |
통계 없을 때 E-Bytes (정렬) | 272M | 4,810K | 58배 |
통계 없을 때 E-Temp | 278M | 5,608K | 50배 |
통계 없을 때 Cost (정렬) | 59,755 | 1,401 | 42.7배 |
그때 실제 Buffers | 822 | 822 | 없음 |
(ORD_NM, ORD_MEMO) 인덱스에 긴 값 | ORA-01450 | 그런 값이 못 들어옴 | — |
정리
| 틀린 믿음 | 실제 |
|---|---|
"VARCHAR2(4000)은 4000바이트를 잡아먹는다" | 아니다. 가변 길이라 실제 바이트만 쓴다 |
| "그러니 넉넉하게 선언해도 공짜다" | 아니다. 아래 세 가지를 가져간다 |
- 통계가 없으면 선언 길이가 그대로 비용 계산에 들어간다.
Cost가 42배 차이 나면 조인 순서, 조인 방식, 정렬 여부 판단이 전부 흔들린다. 로그 테이블은 통계 수집 대상에서 빠져 있기 쉽다. - 인덱스 키 길이 한도를 잡아먹는다. 8KB 블록의 최대 키 길이는 6,397바이트라서
VARCHAR2(4000)두 개면 넘는다. 넘으면 느려지는 정도가 아니라ORA-01450으로INSERT가 실패한다. - 언젠가 누가 그 4000자를 진짜로 채운다. 그 순간 §6의 행 이전과 행 연결이 시작된다.
※ 넓은 컬럼이
SELECT목록에 없으면 이런 왜곡이 나타나지 않는다. 실측에서COUNT(*)해시 조인은 양쪽 다E-Bytes 46397로 같았다. 투영되는 컬럼의 선언 길이만 비용에 들어간다.
9. 이 화의 측정값 전부
모두 ora-hanorder(Oracle AI Database 26ai Free 23.26.2.0.0 · db_block_size 8192 · USERS = LOCAL/AUTOALLOCATE/ASSM)에서 잰 값이다(2026-10-18).
① 고수위선 (T_ORD_LOG · 50만 행 → 24,975행) — 실습/00_고수위선.sql
| 단계 | 행수 | HWM 아래 | 세그먼트 | 익스텐트 | Buffers |
|---|---|---|---|---|---|
| 적재 직후 | 500,000 | 3,665 | 3,712 | 44 | 3,602 |
95% DELETE 후 | 24,975 | 3,665 | 3,712 | 44 | 3,602 |
SHRINK SPACE COMPACT | 24,975 | 3,665 | 3,712 | 44 | 3,602 |
SHRINK SPACE | 24,975 | 180 | 200 | 17 | 183 |
ALTER TABLE MOVE | 24,975 | 192 | 256 | 17 | 183 |
TRUNCATE | 0 | 0 | 8 | 1 | 1 |
블록 상태: DELETE 후 FS3 473 / FS4 3,123 / FULL 0 → COMPACT 후 FS3 1 / FS4 3,416 / FULL 179.
② 4계층 (T_ORD_BIG · 100만 행) — 실습/01_ROWID와_4계층.sql
| 항목 | 값 |
|---|---|
| 세그먼트 블록 / 익스텐트 / 크기 | 6,016 / 62 / 47MB |
HWM 아래 블록 (USER_TABLES.blocks) | 5,923 |
전체 스캔 Buffers | 5,826 |
행이 실제로 든 블록 (DBMS_ROWID) | 5,813 |
| 블록당 행수 | 172 (01화 기준으로는 169) |
IX_ORD_BIG_DT | 2,816블록 / 37익스텐트 / 22MB |
IX_ORD_BIG_DT_AMT | 3,328블록 / 41익스텐트 / 26MB |
인덱스 2개 합계 48MB > 테이블 47MB. 02화에서 번 64배의 영수증이다(14화에서 청구된다).
③ 행 이전 · 행 연결 — 실습/02_행이전과_행연결.sql
§6의 두 표를 참조한다. 그 밖의 값은 다음과 같다.
| 항목 | 값 |
|---|---|
table fetch continued row (2만 건 조회) | T_MIG00 15,252 / T_MIG_OK 0 |
MOVE 후 CHAIN_CNT / 블록 | 0 / 3,513 |
MOVE 후 CLUSTERING_FACTOR | 약 52,200 (대조군 3,449) |
MOVE 후 인덱스 경유 Buffers | 약 10,400 |
| 이전행이 가장 몰린 블록 | 한 블록에 85개 |
⚠️ 이 두 수치만은 완전히 재현되지 않는다.
MOVE는 그때그때 테이블스페이스의 빈 공간 상태에 따라 행을 조금씩 다르게 배치한다. 같은 세션에서 반복하면 값이 같지만, 컨테이너 상태가 달라지면 CF 52,206~52,498 /Buffers10,405~10,497 범위에서 흔들렸다(4회 관측). 나머지 수치는 모두 2회 실행해 완전히 일치하는 것을 확인했다.
④ VARCHAR2(4000) — 실습/03_VARCHAR2_4000.sql
§8의 표를 참조한다.
⑤ 블록 덤프 — 실습/90_블록덤프_SYSDBA.sql
T_MIG10의 한 블록(절대파일 24 / 블록 20870 / 이전행 74개): itc: 2 · nrow=100 · avsp=0x2 · tosp=0xec. 행 조각은 두 종류다. tl: 268 fb: --H-FL-- cc: 3(멀쩡한 행)과 tl: 9 fb: --H----- cc: 0 + nrid:(이전된 행의 머리). nrid가 가리키는 블록에는 tl: 275 fb: ----FL-- + hrid:가 있다(몸통. 원래 주소를 들고 있다).
10. 원천 좌표
전체 지도 →
00_자료준비/06_원서와_공식문서_지도.md
성능고도화 (주 원천)
| 주제 | 권-장 |
|---|---|
| 데이터베이스 저장 구조 · 블록 / 익스텐트 / 세그먼트 / 테이블스페이스 | I권 Ch1 |
| 블록 내부 구조 · ITL · 행 디렉터리 | I권 Ch1 |
| 고수위선과 멀티블록 읽기 | I권 Ch1 · Ch6 |
| 행 이전과 행 연결 | I권 Ch6 |
ROWID 구조 | I권 Ch1 |
오라클 공식문서
| 주제 | 매뉴얼 | 장 제목 |
|---|---|---|
| 논리 저장 구조 (TS/세그먼트/익스텐트/블록) | cncpt | Logical Storage Structures |
데이터 블록 내부 · 행 이전과 연결 · PCTFREE | cncpt | Data Blocks |
ROWID 형식 | cncpt | Rowid Format |
SHRINK SPACE · 세그먼트 공간 회수 | admin | Managing Space for Schema Objects |
DBMS_SPACE.SPACE_USAGE | arpls | DBMS_SPACE |
DBMS_ROWID | arpls | DBMS_ROWID |
ANALYZE ... LIST CHAINED ROWS | sqlrf | ANALYZE |
이 화에서 꼭 확인할 뷰
sql
USER_TABLESPACES -- block_size · extent_management · allocation_type · segment_space_management
USER_SEGMENTS -- blocks(할당) · extents · bytes
USER_EXTENTS -- extent_id · blocks · bytes
USER_TABLES -- blocks(HWM 아래) · empty_blocks · avg_row_len · chain_cnt · pct_free · ini_trans
USER_TAB_COLUMNS -- data_length(선언) vs avg_col_len(실제)
V$MYSTAT + V$STATNAME -- table fetch continued row05화는 조각 리비전이 없다
개정계획의 화 유형 분류에서 05화는 (나) 기반화다. Q1~N1 여섯 조각 중 어느 것도 고치지 않는다. 대신 다음 화에서 갚을 빚을 만들고 숫자로 기록한다. 그래서 이 화에는 리비전/ 폴더가 없다.
다음 화가 여기서 가져가는 것
| 화 | 가져가는 것 |
|---|---|
| 04 | MOVE로도 돌아오지 않는 클러스터링 팩터 (약 52,200 vs 3,449) |
| 12 · 13 | ITL 슬롯(itc)과 Lck · --U- 플래그. 모자라면 무슨 일이 생기는가 |
| 14 ★ | 매달 반복되는 DELETE + SHRINK 비용 · TRUNCATE/파티션으로 가야 하는 근거 수치 (3,602 → 3,602 → 183) · 인덱스 2개 48MB의 적재 비용 · MOVE/SHRINK의 redo |
| 15 | BULK 적재 시 PCTFREE와 행 이전 (배치가 상태 컬럼을 UPDATE하는 패턴) |