DB Study

5장. 저장 구조 — 테이블스페이스와 블록

CREATE TABLE 한 줄이 디스크 위에서는 테이블스페이스 → 세그먼트 → 익스텐트 → 블록이라는 4단 계층으로 펼쳐집니다. 블록 안을 들여다보면 row migration과 "지웠는데 왜 느리죠?"의 답이 보입니다.

중급 ⏱ 약 25분 🎬 애니메이션 3개 선수 지식: 1장 Instance/Database, 2장 SGA
🧭
이 장에서 배우는 것

① 논리 저장 계층(tablespace → segment → extent → block)과 물리 파일의 대응 ② 블록 내부 구조(헤더 · ITL · row data · free space) ③ PCTFREE와 row migration / row chaining ④ High Water Mark가 full scan 성능을 좌우하는 이유

5.1 논리 계층 — 테이블은 4단 서랍장에 담긴다

Oracle은 데이터를 논리 구조물리 구조로 나눠서 관리합니다. SQL을 쓰는 우리는 논리 구조(테이블스페이스, 세그먼트)를 보고, OS는 물리 구조(datafile)만 봅니다. 이 분리 덕분에 "테이블을 어느 디스크 파일에 둘까"를 SQL 세계와 독립적으로 바꿀 수 있습니다.

논리 계층은 큰 것부터 작은 것 순서로 이렇습니다.

계층무엇인가비유
Tablespace세그먼트를 담는 논리적 저장 공간. 하나 이상의 datafile로 구성서랍장 전체
Segment테이블 하나, 인덱스 하나처럼 저장 공간을 차지하는 객체 1개서랍 한 칸
Extent세그먼트에 할당되는 연속된 블록 묶음. 공간이 부족하면 extent 단위로 확장서랍 속 칸막이 박스
Data BlockOracle I/O의 최소 단위. 기본 8KB (DB_BLOCK_SIZE)박스 속 종이 한 장
📦 Tablespace: USERS Segment: EMP 테이블 Extent #0 (연속 블록) Extent #1 (확장으로 추가) 8K 8K 8K 8K Data Block = I/O 최소 단위 💾 물리: /u01/oradata/users01.dbf (datafile) extent = 이 파일 안의 연속된 구간에 대응 ① 서랍장: 논리적 저장 공간 ② 서랍 한 칸 = 객체 하나 ③ 공간 부족 → extent 추가 ④ 블록: 기본 8KB ⑤ OS 눈에는 그냥 큰 파일

실제로 눈으로 확인해 봅시다. 딕셔너리 뷰 세 개면 계층 전체가 보입니다.

-- 테이블이 어느 테이블스페이스에, 얼마나 크게 자리 잡았나
SELECT segment_name, tablespace_name, extents, blocks, bytes/1024/1024 AS mb
FROM   user_segments
WHERE  segment_name = 'EMP';

-- extent 하나하나가 어느 파일의 몇 번 블록부터인지
SELECT extent_id, file_id, block_id, blocks
FROM   dba_extents
WHERE  segment_name = 'EMP'
ORDER BY extent_id;

-- 테이블스페이스를 구성하는 물리 파일
SELECT file_name, bytes/1024/1024 AS mb, autoextensible
FROM   dba_data_files
WHERE  tablespace_name = 'USERS';
💡
블록 크기는 왜 8KB인가

블록 크기는 DB 생성 시 DB_BLOCK_SIZE로 정하며 기본값이자 가장 무난한 값이 8KB입니다(2K~32K 지원). 한 번 만들면 바꿀 수 없고, PostgreSQL의 8KB 페이지와 같은 크기라 두 DB를 비교 학습하기에도 좋습니다.

5.2 블록 내부 해부 — 8KB 안에 무엇이 들어있나

블록은 단순한 데이터 덩어리가 아니라 잘 짜인 문서 양식입니다. 위에서 아래로 이렇게 배치됩니다.

블록 헤더 (block header) 블록 주소 · 세그먼트 타입 · SCN 정보 ITL (Interested Transaction List) 이 블록을 고치는 중인 트랜잭션 명단 — 행 락의 집 (8장) Table / Row Directory — 행들의 블록 내 위치 목차 Free Space (빈 공간) PCTFREE가 지키는 UPDATE 대비 여유분 Row Data (행 데이터) 아래에서 위로 채워짐 관리 영역 free space는 가운데서 양쪽으로 줄어든다
Oracle 데이터 블록의 내부 배치 — 헤더는 위에서 아래로, 행 데이터는 아래에서 위로 자란다

5.3 PCTFREE — "UPDATE를 위해 자리를 비워둬라"

행은 INSERT 이후에도 UPDATE로 커질 수 있습니다. NULL이던 컬럼에 긴 문자열이 들어오는 순간이 대표적이죠. 그래서 Oracle은 블록마다 PCTFREE(기본값 10, 단위 %)만큼을 INSERT용으로 쓰지 않고 남겨둡니다.

-- 블록의 20%를 UPDATE 대비 여유 공간으로 남긴다
CREATE TABLE emp (
  empno  NUMBER(6),
  ename  VARCHAR2(50),
  bio    VARCHAR2(4000)   -- 나중에 채워질 큰 컬럼
) PCTFREE 20 TABLESPACE users;

규칙은 간단합니다. 블록의 빈 공간이 PCTFREE 밑으로 내려가면 그 블록에는 더 이상 새 행을 INSERT하지 않습니다. 남은 공간은 오직 기존 행이 커질 때를 위해 예약됩니다.

⚠️
PCTFREE는 트레이드오프

UPDATE가 잦고 행이 자주 커지는 테이블은 PCTFREE를 키워야 row migration을 막을 수 있습니다. 반대로 INSERT 후 거의 수정하지 않는 로그성 테이블에 큰 PCTFREE를 주면 블록의 그만큼이 영원히 놀게 되어 저장 공간과 캐시를 낭비합니다.

5.4 Row Migration vs Row Chaining — 행이 블록을 넘칠 때

여유 공간을 남겨도 한계는 옵니다. UPDATE로 커진 행이 블록에 도저히 안 들어가면 어떻게 될까요? Oracle은 두 가지 다른 상황을 구분합니다.

🚚 Row Migration (행 이주)

UPDATE로 커진 행이 원래 블록에 안 들어가서 행 전체가 다른 블록으로 이사. 원래 자리에는 새 주소를 가리키는 포인터만 남습니다. ROWID는 그대로 → 인덱스는 수정 안 해도 되지만, 조회 때마다 블록을 2번 읽습니다.

🔗 Row Chaining (행 사슬)

행이 애초에 블록 하나보다 커서(예: 8KB 블록에 10KB짜리 행) 여러 블록에 걸쳐 쪼개져 저장되는 것. PCTFREE를 아무리 조절해도 못 막고, 블록 크기를 키우거나 행 설계를 바꿔야 합니다.

Block #7 row 1: (101, 'Kim', ...) row 2: bio=NULL row 2: bio='아주 긴 자기소개...' 💥 row 2 → 포인터: Block #12로 이사감 row 3: (103, 'Park', ...) free space: 얼마 안 남음 Block #12 (여유 있음) row 2 전체: (102, 'Lee', bio='아주 긴 자기소개...') 인덱스는 여전히 Block #7의 ROWID를 가리킴 조회 1건 = Block #7 읽기 + 포인터 따라 Block #12 읽기 = I/O 2배 ("table fetch continued row")

migration/chaining이 실제로 있는지 이렇게 진단합니다.

-- 방법 1: 통계로 의심하기 — 포인터를 따라간 횟수
SELECT name, value FROM v$sysstat
WHERE  name = 'table fetch continued row';

-- 방법 2: 테이블 통계 수집 후 확인
EXEC DBMS_STATS.GATHER_TABLE_STATS('SCOTT', 'EMP');
SELECT chain_cnt, num_rows FROM user_tables WHERE table_name = 'EMP';

-- 해결: migration된 행은 테이블 재구성으로 정리
ALTER TABLE emp MOVE;          -- 주의: ROWID가 바뀌므로 인덱스 리빌드 필요
ALTER INDEX emp_pk REBUILD;

5.5 High Water Mark — "지웠는데 왜 full scan이 느리죠?"

High Water Mark(HWM)는 세그먼트에서 "여기까지는 데이터가 채워진 적이 있다"를 표시하는 수위선입니다. 홍수가 지나간 강둑의 물자국과 같습니다 — 물이 빠져도 자국은 남죠.

핵심 규칙 두 가지:

EMP 세그먼트의 블록들 1억 건 INSERT됨 DELETE로 텅 빔 미사용 HWM 🌊 DELETE해도 안 내려감! SELECT COUNT(*) → 빈 블록 포함 HWM까지 전부 읽음 🐢 새 HWM ✂️ SHRINK/TRUNCATE 후: 1블록만 읽고 끝 🚀
-- 대량 DELETE 후 HWM 내리기 (온라인, 세그먼트 축소)
ALTER TABLE emp ENABLE ROW MOVEMENT;   -- 행 이동 허용 (ROWID 변경 허용)
ALTER TABLE emp SHRINK SPACE;          -- 행을 앞쪽으로 모으고 HWM 하향

-- 전부 비울 거라면 DELETE 대신 이것 — HWM이 즉시 리셋된다
TRUNCATE TABLE emp;
🚨
TRUNCATE는 되돌릴 수 없다

TRUNCATE는 DDL이라 undo를 거의 남기지 않고 ROLLBACK이 불가능합니다. HWM 리셋 효과만 보고 무심코 실행하면 안 됩니다. 6장에서 배울 flashback으로도 TRUNCATE된 데이터는 undo 기반으로는 복구할 수 없습니다.

✍️ 이해도 체크

UPDATE로 행이 커져 블록에 들어가지 못할 때 일어나는 row migration의 설명으로 옳은 것은?
✅ row migration은 행 전체가 이사하고 원래 자리에 포인터만 남는 현상입니다. ROWID가 유지되어 인덱스는 그대로지만, 조회 시 블록을 2번 읽게 됩니다(첫 번째 보기는 row chaining의 설명). 예방은 적절한 PCTFREE 설정, 사후 정리는 ALTER TABLE MOVE + 인덱스 리빌드입니다.
1억 건 테이블에서 9,900만 건을 DELETE했는데 SELECT COUNT(*)가 여전히 느립니다. 가장 가능성 높은 원인은?
✅ DELETE는 HWM을 내리지 않으므로, full scan은 여전히 HWM 아래의 (대부분 빈) 블록을 모두 읽습니다. ALTER TABLE ... SHRINK SPACE나 테이블 재구성(MOVE), 전량 삭제라면 TRUNCATE로 HWM을 내려야 합니다.
📚
원문으로 더 깊이

Oracle Database 19c Concepts Guide: Logical Storage Structures · Physical Storage Structures — 블록 포맷, extent 할당 알고리즘, ASSM(Automatic Segment Space Management) 등 심화 내용이 있습니다.