expdp导出表ORA-01555错误

有同事碰到一个问题,在导出一个很小的库时有三张表都提示报错,而其他对象都导出正常。

ORA-02354: error in exporting/importing data
ORA-01555: snapshot too old: rollback segment number with name " " too small
ORA-22924: snapshot too old

这里的报错有点奇怪,因为库才10个g,undo表空间配置了32g,导出时基本无业务运行,按常理不会出现回滚段不足的情况。检查这三张表都包含有lob字段,怀疑是跟这有关,后来结果确实是lob字段数据有异常,处理完后即可顺利导出,现在重现这个例子。

新建测试表

drop table t_lob purge;
drop tablespace test_lob including contents and datafiles;
create tablespace test_lob datafile size 4m;

create table t_lob(a number,b clob)
    lob(b) store as (
    tablespace test_lob
    chunk 8k
    PCTVERSION 2
    disable storage in row);


begin
    for i in 1..20 loop
        insert into t_lob values(i,'this is '||i);
        commit;
    end loop;
end;
/

ALTER TABLE t_lob MODIFY LOB (b) ( PCTVERSION 1 );

begin
    for i in 1..10 loop
        update t_lob set b = i||b;
        commit;
    end loop;
end ;
/

这时候查询sql会出现案例中类似的报错

SQL> select * from t_lob as of timestamp timestamp'2019-09-19 16:31:00';
ERROR:
ORA-01555: snapshot too old: rollback segment number  with name "" too small
ORA-22924: snapshot too old

这里就提到一个参数PCTVERSION,它表示LOB存储空间需要为旧版本的LOB页预留的空间,主要是用来维护更新前的一致性读,默认值为10%。如果这个lob字段会有大量的高并发更新,则需要设置一个较高的值。

PCTVERSION可以在创建字段的时候指定,也可以通过ALTER TABLE修改,如果设置小了,当一个会话想要去访问lob字段的旧版本但是这个旧版本又被覆盖时,那么就会出现ORA-01555的错误。PCTVERSION可以避免旧页被覆盖而去强制扩展段空间。

碰到lob字段的值显示<Value Error>时,可以通过下面的脚本来定位那些行和页出现了异常

declare
  error_1578 exception;
  error_1555 exception;
  error_22922 exception;
  pragma exception_init(error_1578,-1578);
  pragma exception_init(error_1555,-1555);
  pragma exception_init(error_22922,-22922);
  n number;
begin
  for cursor_lob in (select rowid r, <LOB_COLUMN> from <TABLE_NAME>) loop
  begin
    n:=dbms_lob.instr(cursor_lob.<LOB_COLUMN>,hextoraw('889911'));
  exception
    when error_1578 then
      insert into corrupt_lobs values (cursor_lob.r, 1578);
      commit;
    when error_1555 then
      insert into corrupt_lobs values (cursor_lob.r, 1555);
      commit;
    when error_22922 then
      insert into corrupt_lobs values (cursor_lob.r, 22922);
      commit;
    end;
  end loop;
end;
/

然后通过上面查到的rowid,对这些lob字段数据进行清空,最后成功导出

SQL> update <TABLE_NAME> set <LOB_COLUMN> = empty_blob()
     where rowid in (select corrupt_rowid from corrupt_lobs);

( for BLOB and BFILE columns use EMPTY_BLOB; for CLOB and NCLOB columns use EMPTY_CLOB )

-- Or export the table without the corrupted row, like:

% expdp system/<PASSWORD> DIRECTORY=my_dir DUMPFILE=<dump_name>.dmp LOGFILE=<logfile_name>.log TABLES=<SCHEMA_NAME>.<TABLE_NAME> QUERY=\"WHERE rowid NOT IN \(\'<corrupt_rowid>\'\)\"