Skip to content

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_FILESblock_size · extent_management · allocation_type · segment_space_management
세그먼트USER_SEGMENTSblocks(할당) · extents · bytes
익스텐트USER_EXTENTSextent_id · blocks · bytes
블록USER_TABLES · DBMS_SPACE.SPACE_USAGEblocks(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,923169
05화 (DBMS_ROWID로 센 실제 사용 블록)1,000,000 / 5,813172

둘 다 맞다. 다른 것을 센 것이다.


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) = 그 세그먼트에서 지금까지 포맷해서 써 본 가장 끝 블록의 위치.

세 문장으로 ​

  1. 전체 스캔(TABLE ACCESS FULL)은 세그먼트 첫 블록부터 고수위선까지 읽는다. 기준은 "행이 어디까지 있나"가 아니다.
  2. DELETE는 블록 안의 행만 지운다. 블록을 반납하지도, 고수위선을 내리지도 않는다.
  3. 고수위선은 올라갈 때는 자동이지만, 내려갈 때는 명시적인 명령이 필요하다.

왜 그렇게 만들었나 ​

DELETE가 고수위선을 자동으로 내리려면 DELETE할 때마다 남은 행을 앞으로 재배치해야 한다. 그러면 이런 문제가 생긴다.

  • DELETE의 비용이 테이블 크기에 비례해 폭증한다
  • 행의 ROWID가 바뀌므로 모든 인덱스를 같이 고쳐야 한다
  • 동시에 그 블록을 읽는 다른 세션과 충돌한다

그래서 오라클은 "지우는 것"과 "정리하는 것"을 분리했다. 정리는 사용자가 명령할 때만 한다.

실측 — 50만 행 중 95% 삭제 ​

시점행수HWM 아래 블록세그먼트 블록익스텐트Buffers
적재 직후500,0003,6653,712443,602
DELETE 475,025행 후24,9753,6653,712443,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 후
UNFORMATTED00
FS1 (0~25% 빔)00
FS2 (25~50% 빔)00
FS3 (50~75% 빔)4731
FS4 (75~100% 빔)3,1233,416
FULL0179

⚠️ 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,6653,712443,602—그대로
SHRINK SPACE COMPACT3,6653,712443,602가능(롤백 아님)유지됨
SHRINK SPACE18020017183불가유지됨
ALTER TABLE MOVE19225617183불가☠️ UNUSABLE
TRUNCATE TABLE0811☠️ 불가 · 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,449735

이전은 사라졌지만(CHAIN_CNT 0) 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
필드뜻
itcITL(Interested Transaction List) 슬롯 수. 이 블록을 동시에 변경할 수 있는 트랜잭션 수
Itl의 FlagC--- 커밋 완료 · --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 전 블록
01001,000
10 (기본값)891,126
40611,630
75273,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 + 나중에 UPDATE1,0003,89876,16976.2%3,90430,549
PCTFREE 10 + 나중에 UPDATE1,1264,15073,49473.5%—29,482
PCTFREE 40 + 나중에 UPDATE1,6305,15856,98457.0%—23,083
PCTFREE 75 + 나중에 UPDATE3,7723,77200%—786
처음부터 최종 길이로 적재3,5203,52000%3,469735

📌 이 표가 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바이트가 되면 PCTFREE 40으로도 57%가 이사 간다. 75를 줘야 0이 된다.

③ 그런데 PCTFREE 75로 만들면 UPDATE 전까지 테이블이 3.8배 뚱뚱해진다. 공짜가 아니다.

④ 진짜 답은 PCTFREE가 아니라 설계다. 코드 컬럼 하나에 나중에 설명 200자를 덧붙이지 않으면 된다.

행 연결 실측 — VARCHAR2(4000) 세 개를 꽉 채우면 ​

값
행수5,000
AVG_ROW_LEN12,022 (블록 8,192보다 크다)
블록15,197
행당 블록3.04
CHAIN_CNT5,000 (전부)
읽는 컬럼Bufferstable fetch continued row
COUNT(C1) (첫 조각)15,2070
COUNT(C3) (끝 조각까지)25,2079,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오브젝트상대파일블록행번호
AAAR2eAAYAAABgLAAA731182461550
AAAR2eAAYAAABgLAAB731182461551
AAAR2eAAYAAABgLAAC731182461552

★ 테이블스페이스가 없다 — 세 가지 증거 ​

① 네 조각 어디에도 없다. 오브젝트, 파일, 블록, 행 번호가 전부다.

② 상대 파일 번호는 테이블스페이스마다 따로 센다. 실측값은 다음과 같다.

테이블스페이스빅파일절대 파일번호상대 파일번호
USERSNO2424
SYSAUXYES221024
SYSTEMYES211024
UNDOTBS1YES231024

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.blocks874874없음
AVG_ROW_LEN2323없음
USER_SEGMENTS.blocks896896없음
AVG_COL_LEN (ORD_NM)1212없음
전체 스캔 Buffers821821없음
통계 없을 때 E-Bytes (정렬)272M4,810K58배
통계 없을 때 E-Temp278M5,608K50배
통계 없을 때 Cost (정렬)59,7551,40142.7배
그때 실제 Buffers822822없음
(ORD_NM, ORD_MEMO) 인덱스에 긴 값ORA-01450그런 값이 못 들어옴—

정리 ​

틀린 믿음실제
"VARCHAR2(4000)은 4000바이트를 잡아먹는다"아니다. 가변 길이라 실제 바이트만 쓴다
"그러니 넉넉하게 선언해도 공짜다"아니다. 아래 세 가지를 가져간다
  1. 통계가 없으면 선언 길이가 그대로 비용 계산에 들어간다. Cost가 42배 차이 나면 조인 순서, 조인 방식, 정렬 여부 판단이 전부 흔들린다. 로그 테이블은 통계 수집 대상에서 빠져 있기 쉽다.
  2. 인덱스 키 길이 한도를 잡아먹는다. 8KB 블록의 최대 키 길이는 6,397바이트라서 VARCHAR2(4000) 두 개면 넘는다. 넘으면 느려지는 정도가 아니라 ORA-01450으로 INSERT가 실패한다.
  3. 언젠가 누가 그 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,0003,6653,712443,602
95% DELETE 후24,9753,6653,712443,602
SHRINK SPACE COMPACT24,9753,6653,712443,602
SHRINK SPACE24,97518020017183
ALTER TABLE MOVE24,97519225617183
TRUNCATE00811

블록 상태: 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
전체 스캔 Buffers5,826
행이 실제로 든 블록 (DBMS_ROWID)5,813
블록당 행수172 (01화 기준으로는 169)
IX_ORD_BIG_DT2,816블록 / 37익스텐트 / 22MB
IX_ORD_BIG_DT_AMT3,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 / Buffers 10,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/세그먼트/익스텐트/블록)cncptLogical Storage Structures
데이터 블록 내부 · 행 이전과 연결 · PCTFREEcncptData Blocks
ROWID 형식cncptRowid Format
SHRINK SPACE · 세그먼트 공간 회수adminManaging Space for Schema Objects
DBMS_SPACE.SPACE_USAGEarplsDBMS_SPACE
DBMS_ROWIDarplsDBMS_ROWID
ANALYZE ... LIST CHAINED ROWSsqlrfANALYZE

이 화에서 꼭 확인할 뷰 ​

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 row

05화는 조각 리비전이 없다 ​

개정계획의 화 유형 분류에서 05화는 (나) 기반화다. Q1~N1 여섯 조각 중 어느 것도 고치지 않는다. 대신 다음 화에서 갚을 빚을 만들고 숫자로 기록한다. 그래서 이 화에는 리비전/ 폴더가 없다.

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

화가져가는 것
04MOVE로도 돌아오지 않는 클러스터링 팩터 (약 52,200 vs 3,449)
12 · 13ITL 슬롯(itc)과 Lck · --U- 플래그. 모자라면 무슨 일이 생기는가
14 ★매달 반복되는 DELETE + SHRINK 비용 · TRUNCATE/파티션으로 가야 하는 근거 수치 (3,602 → 3,602 → 183) · 인덱스 2개 48MB의 적재 비용 · MOVE/SHRINK의 redo
15BULK 적재 시 PCTFREE와 행 이전 (배치가 상태 컬럼을 UPDATE하는 패턴)