무엇이 가장 많거나 적은 언두를 생성하는가?
- 인덱스로 인해 매우 많은 언두 정보를 생성할 수 있다.
- INSERT는 일반적으로 가장 적은 언두를 생성한다. delete 할 rowid 만 기록.
- UPDATE는 두 번째로 언두 양이 적다. 변경된 바이트만 기록하기 때문.
- DELETE는 일반적으로 가장 많은 언두를 생성한다. 삭제하는 데이터 전체에 대한 언두를 생성한다.
SQLPLUS > create table t
2 as
3 select object_name unindexed,
4 object_name indexed
5 from all_objects
6 /
Table created.
SQLPLUS > create index i_idx on t(indexed);
Index created.
SQLPLUS > exec dbms_stats.gather_table_stats(user,'T');
PL/SQL procedure successfully completed.
SQLPLUS > select used_ublk
2 from v$transaction
3 where addr = ( select taddr
4 from v$session
5 where sid = ( select sid from v$mystat where rownum = 1 )
6 )
7 /
USED_UBLK
----------
69
SQLPLUS > commit ;
Commit complete.
-- UPDATE 가 언두를 저장하는데 69개 블록을 사용함.
-- commit 이후에는 해당 정보를 없애거나 해제하므로,
위 select 쿼리로 다시 조회하면 no rows selected 결과가 출력된다.
SQLPLUS >update t set indexed = lower(indexed) ;
17679 rows updated.
SQLPLUS >select used_ublk
from v$transaction
where addr = ( select taddr
from v$session
where sid = ( select sid from v$mystat where rownum = 1 )
)
/
USED_UBLK
----------
337
-- 3배가 넘는 언두가 생성됐다.
- 결론 : 복잡한 인덱스 구조 자체모습대로 언두를 생성하고 테이블의 모든 row 마다 수정을 했기 때문.
ORA-01555: snapshot too old Error
에러의 원인
- 작업량에 비해 언두 세그먼트가 너무 작다
- 데이터를 fetch 하는 중간 commit 하도록 프로그래밍 했다.
- 블록 클린아웃
ORA-01555 오류의 일반적인 해결책
- UNDO_RETENTION 파라미터를 적절하게 설정 (가장 오래 수행하는 트랜잭션에 걸리는 시간보다 많게.)
- V$UNDOSTAT 은 장시간 수행되는 쿼리에 걸린시간을 확인할 때 사용할 수 있다.
- UNDO_RETENTION 을 근거로 필요한 만큼 언두세그먼트 크기를 증가시킬 수 있도록 공간확보.
- 수동 언두관리 시 좀 더 많은 언두 세그먼트를 추가하라. 그러나 자동 언두관리를 적극 추천한다.
- 쿼리 수행시간을 단축하도록 튜닝하라. 위 세가지 원인에 대한 해결책.
- 객체에 대한 통계정보를 수집하라. 블록클린아웃을 피하기 위함.
- 블록 클린아웃은 대량 UPDATE/INSERT의 결과로, 이후 통계 재수집이 필요하다.
언두 세그먼트가 너무 작다
- 언두 세그먼트는 순환방식으로 평균 약 3~4분마다 언두 공간을 재사용한다.
- 만일 5분 소요되는 리포팅 쿼리가, 쿼리 시작시점 기준 데이터 뷰가 필요할 경우 ORA-01555오류 발생 확률이 높다.
- 쿼리 실행중 덮어써진 데이터에 대한 블록을 다시 찾아간다면, 언두정보가 없기 때문에 에러발생후 종료된다.
(예시) 장시간 실행하는 쿼리의 타임라인
언두 관리 방법 (오라클9i)
- 자동 언두 관리 : UNDO_RETENTION 파라미터로 오라클이 얼마 동안 언두를 유지해야 하는지 지정.
- DBA가 설정한 UNDO_RETENTION 목표치에 맞추기 위해 실행 시에 개별 언두 세그먼트 간에 익스텐스를 재할당 할 수 있다.
- 수동 언두 관리 : DBA가 언두를 관리. 예측 또는 관찰된 작업량에 기반하여 수동으로 언두세그먼트를 만들지 결정한다.
- 수동 관리 언두 세그먼트는 미리 크게 잡아서, 언두 세그먼트 스스로 증가할 기회를 주지 말고 언두 세그먼트 공간에 고정되도록 할당할 필요가 있다.
자동 언두관리 : UNDO_RETENTION
- 쿼리가 얼마나 오래 실행하는지 확인하고 UNDO_RETENTION 파라미터 값을 설정해야 한다.
- 설정 시간만큼 언두를 보존하려 한다.
수동 언두관리
- 쿼리를 처리하는 동안 가장 작은 언두 세그먼트를 선택해서 순환하므로 ORA-01555 발생 확률이 크다.
- 필자는 기존 언두 세그먼트를 사용할 때 롤백 세그먼트 크기를 동일하게 설정한다.
언두세그먼트 공간 사용 테스트
- 작은 언두 테이블스페이스 생성, 작은트랜잭션을 많이 발생시킨다.
- 언두 세그먼트를 사용하는 세션 A 는 테이블 T를 변경할 것. A는 테이블 T전체 스캔 / B세션에서 인덱스를 통해 테이블 T를 읽음
-- 세션 A
SQLPLUS > create undo tablespace undo_small
2 datafile '/tmp/undo.dbf' size 2m
3 autoextend off ;
Tablespace created.
SQLPLUS > alter system set undo_tablespace = undo_small ;
System altered.
- 테이블 T 생성. 테이블에서는 데이터를 무작위로 정렬한다.
SQLPLUS > create table t
2 as
3 select * from all_objects
4 order by dbms_random.random ;
Table created.
SQLPLUS > alter table t add constraint t_pk primary key (object_id) ;
Table altered.
SQLPLUS > exec dbms_stats.gather_table_stats(user,'T',cascade=>true);
PL/SQL procedure successfully completed.
SQLPLUS > begin
for x in ( select rowid rid from t )
loop
update t set object_name = lower(object_name) where rowid = x.rid ;
commit;
end loop ;
end;
/
- 세션B 에서 쿼리를 실행한다. T 테이블을 읽고 각 로우를 처리한다.
SQLPLUS > declare
cursor c is
select /*+ first_rows */ object_name
from t
order by object_id ;
l_object_name t.object_name%type ;
l_rowcnt number := 0 ;
begin
open c;
loop
fetch c into l_object_name ;
exit when c%notfound ;
dbms_lock.sleep( 0.01 ) ;
l_rowcnt := l_rowcnt + 1 ;
end loop ;
close c ;
exception
when others then
dbms_output.put_line ( 'rows fetched = ' || l_rowcnt ) ;
raise;
end ;
/
rows fetched = 301
declare
*
ERROR at line 1:
ORA-01555: snapshot too old: rollback segment number 46 with name
"_SYSSMU46_2555494716$" too small
ORA-06512: at line 21
위 현상을 해결 하기위해서는
- 읽기 프로세스가 충분히 완료할 수 있을 만큼의 UNDO_RETENTION 을 크게 설정해야 함.
- 프로세스가 완료될 때 까지 충분한 언두를 보유하도록 한다.
- 언두 테이블스페이스가 증가하도록 설정하거나, 수동으로 디스크 공간을 더 많이 할당한다.
- 오랫동안 실행하는 프로세스 완료가 약 720초 수행 걸린다고 했을 때, UNDO_RETENTION 을 900 ( 초 단위. 15분 ) 으로 설정.
- 한 번에 1MB 씩 증가해서 2GB까지 증가되도록 언두 테이블스페이스 데이터를 변경.
- 설정 이후 완료됨. 언두 테이블스페이스의 데이터파일 증가.
- snap shot too old 에러 발생 원인은 T 테이블을 인덱스를 경유하여 무작위로 읽어 발생했다.
- 테이블을 빠르게 전체 스캔하면 오류가 나지 않을 가능성이 크다.
- SELECT 와 UPDATE 둘 다 테이블 T 전체를 스캔하나, SELECT스캔하는 동안 UPDATE 를 앞질러 갈 수 있기 때문.
- (SELECT는 읽기만 / UPDATE 는 읽기 + 변경)
지연된 블록 클린아웃
- 블록을 변경한 후 이 블록을 액세스하는 다음 세션이 최종으로 블록을 변경한 트랜잭션이 여전히 활동중인지 체크
- 블록을 변경한 트랜잭션이 활동하지 않음이 확인되면, 해당 블록을 액세스하는 다음 세션이 똑같은 과정을 겪지 않도록 해당 블록을 클린아웃.
- 블록 클린아웃을 위해 블록 헤더로부터 이전 트랜잭션이 사용한 언두 세그먼트를 확인
- 해당 트랜잭션이 커밋했는지, 언제 커밋했는지 확인하는 방법
- COMMIT SCN이 언두 세그먼트 트랜잭션 테이블에 존재하는지와 그 트랜잭션 슬롯이 덮어 써지지 않았다는 것을 확인하는 것.
지연된 블록 클린아웃으로 ORA-01555 오류 발생하기 위한 조건
- 변경 후 커밋했지만, 블록은 자동 클린아웃이 안됨 ( ex. 트랜잭션이 SGA블록 버퍼 캐시의 10% 초과 블록을 변경 )
- 클린아웃되지 않은 블록을 방문하는 세션이 장시간 존재하지 않음.
- 오랜시간 걸리는 쿼리가 시작된 직후, 클린아웃되지 않은 일부 블록을 읽는다. 해당 쿼리가 SCN t1 에 시작, 읽기일관성을 위해 T1 시점으로 롤백한 읽기 일관적인 데이터 뷰를 제공해야 한다. 변경 트랜잭션에 대한 트랜잭션 엔트리는 쿼리가 시작할 때 언두 세그먼트 트랜잭션 테이블에 있다.
- 쿼리 실행중 시스템에서 커밋이 많이 일어나지만, 이 트랜잭션은 해당 블록을 방문하지 않는다.
- 언두 세그먼트에서 트랜잭션 테이블은 순환하면서 잦은 커밋으로 인해 슬롯을 재사용한다. 시스템이 언두 세그먼트 익스텐트를 재사용하다보니 언두 세그먼트 헤더 블록 자체에 대한 읽기가 불가능하게 됐다.
- 언두 세그먼트에 기록된 가장 작은 SCN이 너무 잦은 커밋으로 인해 t1 (쿼리의 읽기 일관성 SCN보다 큼) 보다 커졌다.
지연된 블록 클린아웃으로 발생하는 ORA-01555 오류 발생 테스트
- 큰 테이블 생성 ( 클린아웃 필요한 테이블 블록을 많이 생성 )
SQLPLUS > create undo tablespace undo_small
2 datafile '/tmp/undo.dbf' size 4m
3 autoextend off
4 /
Tablespace created.
SQLPLUS > create table big
2 as
3 select a.*, rpad ('*', 1000, '*') data
4 from all_objects a;
Table created.
SQLPLUS > alter table big add constraint big_pk
2 primary key (object_id);
Table altered.
SQLPLUS > exec dbms_stats.gather_table_stats(user, 'BIG') ;
PL/SQL procedure successfully completed.
SQLPLUS > create table small ( x int, y char(500) );
Table created.
SQLPLUS > insert into small select rownum , 'x' from all_users ;
44 rows created.
SQLPLUS > commit ;
Commit complete.
SQLPLUS > exec dbms_stats.gather_table_stats(user, 'SMALL') ;
PL/SQL procedure successfully completed.
- big 테이블 변경하여 dirty 블록을 만든다. 언두가 작기때문에 가능한 언두를 작게 생성하면서 big테이블 블록 수 만큼 수정.
- 모든 블록을 수정하여 버퍼 캐시에 dirty 블록이 다수 존재하도록 하되 언두 테이블스페이스 용량을 초과하지 않도록 첫 번째 row 만 update 한다.
SQLPLUS > alter system set undo_tablespace = undo_small ;
System altered.
-- 블록의 첫 번째 row id 를 찾아 temporary ( VARCHAR2(1) 필드를 수정한다 )
SQLPLUS > update big
set temporary = temporary
where rowid in
( select r from (
select rowid r, row_number() over ( partition by dbms_rowid.rowid_block_number(rowid) order by rowid) rn
from big ) where rn = 1
)
/
12014 rows updated.
SQLPLUS > commit ;
Commit complete.
- 결과집합을 확정시키기 위해 커서를 오픈하여, 방금 UPDATE 하고 커밋한 데이터를 가져온다.
- 최근 변경 블록이면 오라클이 해당 트랜잭션이 쿼리를 시작하기 전 커밋됐는지 확인할 의무가 있고, 만일 그 정보가 이미 덮어 쓰였다면 쿼리는 실패할 것이다.
- 각 세션이 실행한 스크립트는 test2.sql 은 다음과 같다.
- 9개 세션은 각 루프 내에서 많은 update 트랜잭션을 발생시키고, 모든 세션이 작업 완료할 때 까지 기다리고 커서를 오픈한 세션으로 리턴한다. 결과 출력하면 다음과 같다.
- 지연된 블록 클린아웃으로 인한 ORA-01555 에러 현상은 흔히 발생하진 않는다.
- 대량 UPDATE 작업 후 통계정보 재수집 시 클린아웃 되지않은 블록이 해소되기 때문.
- 대부분 트랜잭션은 버퍼 캐시에 수용가능한 블록의 10%이내로 작업하기 때문에 클린아웃이 필요한 블록을 생성하지 않음.
DML이 적은 테이블 대상으로 SELECT 가 ORA-01555 에러 발생 시 해결책
- 적절한 크기의 트랜잭션을 사용하는지 확인. 필요 이상으로 자주 커밋하지 않는지 확인.
- DBMS_STAT 을 사용하여 통계정보를 수집하라.
- 언두 테이블스페이스 확장공간을 제공하고, 언두 보유시간을 늘려라.
- 쿼리 실행시간을 줄이도록 튜닝하라. 항상 최선의 해결책이자 우선적으로 시도할 작업이다.