6장. Undo와 읽기 일관성
Oracle에서 SELECT는 절대 남의 미커밋 데이터를 보지 않고, 그렇다고 기다리지도 않습니다. 비밀은 "과거를 복원하는 데이터", undo입니다. PostgreSQL과 정반대 방향의 MVCC를 만나봅니다.
① undo tablespace와 undo segment — 되돌리기 데이터의 집 ② DML이 undo를 기록하는 순간 ③ 읽기 일관성 — undo로 CR 블록을 만들어내는 마법 ④ ORA-01555 snapshot too old의 원인과 대책, 그리고 보너스인 Flashback Query
6.1 Undo란 — "되돌리기 위한 데이터"
Undo는 이름 그대로 변경을 되돌리기(undo) 위한 정보입니다. UPDATE로 값을 100에서 200으로 바꾸면, Oracle은 "원래 값은 100이었다"라는 변경 전 이미지(before image)를 따로 보관합니다. 이 정보가 하는 일은 세 가지입니다.
- ROLLBACK — 트랜잭션을 취소할 때 undo를 적용해 원래대로 되돌립니다.
- 읽기 일관성(read consistency) — 다른 세션의 SELECT가 미커밋 변경을 "지우고" 과거 시점의 데이터를 볼 수 있게 합니다. 이 장의 주인공입니다.
- Flashback — "10분 전의 이 테이블"을 조회하는 시간여행 기능의 재료가 됩니다.
undo는 어디에 저장될까요? 전용 undo tablespace 안의 undo segment들입니다. 현대 Oracle(9i 이후)은 Automatic Undo Management가 기본이라, DBA는 undo tablespace 크기만 관리하면 세그먼트 할당은 Oracle이 알아서 합니다.
-- undo 관리 방식과 undo tablespace 확인
SHOW PARAMETER undo;
NAME TYPE VALUE
---------------- -------- ---------
undo_management string AUTO ← 자동 undo 관리 (기본)
undo_retention integer 900 ← undo를 최소 900초(15분) 보존 시도
undo_tablespace string UNDOTBS1
-- 트랜잭션이 지금 어느 undo segment를 쓰는지
SELECT s.sid, s.username, t.xidusn AS undo_seg_num, t.used_ublk
FROM v$transaction t JOIN v$session s ON t.ses_addr = s.saddr;
6.2 DML의 순간 — 블록은 제자리에서 고치고, 과거는 undo로
여기가 Oracle을 이해하는 갈림길입니다. Oracle의 UPDATE는 데이터 블록을 제자리에서(in-place) 고칩니다. 새 버전 행을 만들지 않습니다. 대신 덮어쓰기 직전의 값을 undo segment에 복사해 둡니다.
세션 A가 UPDATE acc SET bal=200 WHERE id=7을 실행합니다. 서버 프로세스가 대상 블록을 Buffer Cache에서 찾습니다. 현재 값은 bal=100.
덮어쓰기 전에, 변경 전 값 "bal=100"이라는 before image를 undo segment에 기록합니다. (이 undo 기록조차 redo의 보호를 받습니다 — 7장에서.)
이제 데이터 블록의 행을 제자리에서 200으로 수정합니다. PostgreSQL처럼 새 버전 행을 만들지 않습니다! 동시에 블록의 ITL에 "TX A가 이 행을 잡고 있다"는 락 정보도 새깁니다.
커밋 후에도 undo는 바로 버려지지 않습니다. 다른 세션의 SELECT가 과거를 복원할 재료로 당분간(undo_retention) 보존됩니다. 이것이 다음 절의 읽기 일관성입니다.
undo segment도 결국 undo tablespace 안의 데이터 블록입니다. 그래서 undo 변경도 redo에 기록되고, Buffer Cache에 캐시되고, DBWn이 디스크에 씁니다. "undo는 redo의 보호를 받는다"는 문장은 7장 인스턴스 복구에서 다시 만납니다.
6.3 읽기 일관성 — CR 블록을 만드는 마법
세션 A가 아직 커밋하지 않은 상태에서, 세션 B가 같은 행을 SELECT하면 어떻게 될까요? 블록에는 이미 bal=200이 적혀 있는데요. Oracle의 답은 "블록을 복제한 뒤 undo를 거꾸로 적용해서 과거 시점의 블록을 재조립한다"입니다. 이렇게 만든 사본을 CR(Consistent Read) 블록이라고 부릅니다.
기준점은 SCN(System Change Number) — Oracle 내부의 논리적 시계입니다. 쿼리가 시작되는 순간의 SCN이 "내가 봐야 할 세상의 시각"으로 고정되고, 그보다 미래의 변경(그리고 모든 미커밋 변경)은 undo로 지워냅니다.
세션 A가 커밋하기 전, 세션 B가 SELECT를 시작합니다. 이 순간의 SCN 5000이 "봐야 할 세상의 시각"으로 고정됩니다.
블록을 열어보니 ITL에 미커밋 트랜잭션 A의 흔적이 있습니다. 현재 블록의 bal=200을 그대로 읽으면 dirty read — Oracle은 절대 허용하지 않습니다.
그렇다고 기다리지도 않습니다. Buffer Cache 안에 블록의 복제본(CR 블록)을 만듭니다.
복제본에 undo의 before image를 거꾸로 적용해 TX A의 변경을 지웁니다. 복제본은 이제 SCN 5000 시점의 모습, bal=100이 됩니다.
세션 B는 CR 블록에서 bal=100을 읽고 즉시 반환합니다. 기다림도, dirty read도 없이 — 이것이 Oracle 읽기 일관성이고, "SELECT는 락을 잡지 않는다"(8장)의 토대입니다.
통계 consistent gets(일관된 읽기)와 data blocks consistent reads - undo records applied를 보면 SELECT가 undo를 몇 번 적용했는지 알 수 있습니다. SELECT name, value FROM v$sysstat WHERE name LIKE '%undo records applied%';
6.4 🏆 PostgreSQL과 정반대 방향 — 같은 MVCC, 다른 설계
PostgreSQL도 Oracle도 MVCC(다중 버전 동시성 제어)를 씁니다. 그런데 버전을 어디에 두는가가 정반대입니다. 이 차이 하나에서 두 DB의 운영 특성 대부분이 갈라져 나옵니다.
PostgreSQL: UPDATE가 새 버전 행을 heap에 추가하고 옛 버전을 남깁니다(PostgreSQL 3장 MVCC). 과거가 테이블 안에 그대로 있으므로 옛 스냅샷 읽기는 공짜지만, dead tuple 청소(VACUUM)가 숙제입니다.
Oracle: UPDATE가 블록을 제자리에서 고치고, 과거는 undo라는 별도 공간에 둡니다. 테이블은 항상 최신 버전만 담아 깔끔하고 VACUUM도 없지만, 과거를 읽으려면 undo를 적용해 재조립하는 비용이 들고, undo가 재사용되면 ORA-01555라는 벽에 부딪힙니다.
| PostgreSQL | Oracle | |
|---|---|---|
| UPDATE 방식 | 새 버전 튜플을 heap에 INSERT | 블록을 제자리(in-place) 수정 |
| 과거 버전 위치 | 테이블(heap) 안에 그대로 | undo tablespace (별도 공간) |
| 과거 읽기 비용 | 낮음 — 옛 튜플을 그냥 읽음 | CR 블록 재조립 비용 (undo 적용) |
| 버전 청소 | VACUUM 필요 (Bloat 위험) | 불필요 — undo는 자동 순환 재사용 |
| 오래된 스냅샷의 한계 | long TX가 VACUUM을 막아 Bloat 유발 | undo 재사용 시 ORA-01555 발생 |
| ROLLBACK 비용 | 저렴 — 새 버전을 무효화만 하면 됨 | 비쌈 — undo를 실제로 적용해 되돌림 |
재미있는 대칭이죠. PostgreSQL은 ROLLBACK이 싸고 청소가 비싸며, Oracle은 청소가 없는 대신 ROLLBACK이 비쌉니다. "대량 배치가 실패해서 ROLLBACK했더니 실행 시간만큼 또 걸렸다"는 Oracle 단골 사연이 여기서 나옵니다.
6.5 ORA-01555: snapshot too old — 유통기한 지난 과거
undo tablespace는 무한하지 않습니다. 공간이 차면 Oracle은 커밋된 지 오래된 undo부터 순환식으로 덮어씁니다. 문제는 아직 그 과거를 필요로 하는 장기 실행 쿼리가 있을 때입니다.
1시간짜리 리포트 쿼리가 SCN 5000 기준으로 읽기 시작했습니다. 도중에 다른 세션들이 바꾼 데이터는 SCN 5000 시점으로 복원해서 읽어야 하므로, 과거 undo가 계속 필요합니다.
그 사이 다른 세션들이 대량 DML을 실행하고 커밋합니다. 새 undo가 쏟아져 들어와 undo tablespace가 가득 찹니다.
공간이 부족해진 Oracle은 커밋된 지 가장 오래된 undo부터 덮어씁니다. 하필 리포트 쿼리가 필요로 하던 SCN 4800의 before image가 사라졌습니다.
리포트 쿼리가 그 블록의 CR 버전을 만들려는 순간 — 재료가 없습니다. ORA-01555: snapshot too old. 쿼리는 실패하고 처음부터 다시 실행해야 합니다.
대책은 "과거를 더 오래 보관하거나, 과거를 덜 오래 필요로 하거나" 둘 중 하나입니다.
| 대책 | 방법 |
|---|---|
| undo 보존 기간 확보 | undo_retention(기본 900초) 상향 + undo tablespace 크기 증설. 공간이 없으면 retention은 지켜지지 않는 '희망 사항'일 뿐입니다. |
| 보존 강제 | ALTER TABLESPACE undotbs1 RETENTION GUARANTEE; — 단, 공간 부족 시 DML이 실패하는 부작용을 감수해야 합니다. |
| 쿼리 짧게 | 장기 리포트를 야간 등 DML이 적은 시간대로 이동, 또는 쿼리 자체를 분할·튜닝. |
| fetch across commit 금지 | 커서를 열어둔 채 루프 안에서 조금씩 커밋하는 패턴은 자기 자신이 필요로 하는 undo를 스스로 밀어내는 자충수입니다. |
undo_retention=900은 "900초 동안 보존하려고 노력한다"는 뜻입니다. 진행 중인 트랜잭션의 undo(active undo)는 절대 덮어쓰지 않지만, 커밋된 undo는 공간이 부족하면 retention 이전이라도 재사용될 수 있습니다. 확실한 보장이 필요하면 RETENTION GUARANTEE + 충분한 공간이 답입니다.
6.6 Flashback Query — undo가 주는 보너스, 시간여행
undo에 과거가 남아 있다면, 그걸 사용자에게도 열어주면 어떨까요? 그것이 Flashback Query입니다. 실수로 지우고 커밋해버린 데이터를 백업 없이 되살리는 실전 필살기입니다.
-- 10분 전의 테이블 모습 조회 (undo가 남아있는 한)
SELECT * FROM emp
AS OF TIMESTAMP SYSTIMESTAMP - INTERVAL '10' MINUTE
WHERE empno = 7369;
-- 실수로 DELETE + COMMIT한 행 복구
INSERT INTO emp
SELECT * FROM emp AS OF TIMESTAMP SYSTIMESTAMP - INTERVAL '10' MINUTE
WHERE empno NOT IN (SELECT empno FROM emp);
-- 행의 변경 이력을 버전별로 추적
SELECT versions_starttime, versions_operation, sal
FROM emp VERSIONS BETWEEN SCN MINVALUE AND MAXVALUE
WHERE empno = 7369;
Flashback Query는 undo를 재료로 쓰므로, undo가 재사용된 과거는 조회할 수 없고 똑같이 ORA-01555가 납니다. "어제 지운 데이터"를 flashback으로 찾으려면 undo_retention과 undo 공간이 그만큼 받쳐줘야 합니다. DDL(TRUNCATE 등)로 날아간 데이터도 이 방법으로는 복구 불가입니다.
✍️ 이해도 체크
Oracle Database 19c Concepts Guide: Data Concurrency and Consistency — multiversion read consistency와 SCN의 정식 정의 · Managing Undo (Administrator's Guide) — undo 자동 튜닝과 retention 관리.