oralce redo与undo

redo(重作信息)是Oracle在在线(或归档)重作日志文件中记录的信息,万一出现失败时能够利用这些数据来“重放”(或重作)事务。undo(撤销信息)是Oracle在undo段中记录的信息,用于取消或回滚事务。java

1 什么是redo

重作日志文件(redo log file)对Oracle数据库来讲相当重要。它们是数据库的事务日志。Oracle维护着两类重作日志文件:在线(online)重作日志文件归档(archived)重作日志文件。这两类重作日志文件都用于恢复;其主要目的是,万一实例失败或介质失败,它们就能派上用场。
若是数据库所在主机掉电,致使实例失败,Oracle会使用在线重作日志将系统刚好恢复到掉电以前的那个时间点。若是磁盘驱动器出现故障(这是一个介质失败),Oracle会 使用归档重作日志以及在线重作日志将该驱动器上的数据备份恢复到适当的时间点。另外,若是你“无心地”截除了一个表,或者删除了某些重要的信息,而后提交 了这个操做,那么能够恢复受影响数据的一个备份,并使用在线和归档重作日志文件把它恢复到这个“意外”发生前的时间点。
归档重作日志文件实际上就是已填满的“旧”在线重作日志文件的副本。系统将日志文件填满时,ARCH进程会在另外一个位置创建在线重作日志文件的一个副本,也能够在本地和远程位置上创建多个另外的副本。若是因为磁盘驱动器损坏或者其余物理故障而致使失败,就会用这些归档重作日志文件来执行介质恢复。Oracle拿到这些归档重作日志文件,并把它们应用于数据文件的备份,使这些数据文件能“遇上”数据库的其他部分。归档重作日志文件是数据库的事务历史sql

利用闪回技术(flashback),能够执行闪回查询(也就是说,查询过去某个时间点的数据),取消数据库表的删除,将表置回到之前某个时间的状态,等等。所以,如今使用备份和归档重作日志文件来完成传统恢复的状况愈来愈少。不过,执行恢复是DBA最重要的任务,并且DBA在数据库恢复方面绝对不能犯错误。数据库

每一个Oracle数据库都至少有两个在线重作日志组,每一个组中至少有一个成员(重作日志文件)。这些在线重作日志组以循环方式使用。Oracle会写组1中的日志文件,等写到组1中文件的最后时,将切换到日志文件组2,开始写这个组中的文件。等到把日志文件组2写满时,会再次切换回日志文件组1(假设只有两个重作日志文件组;若是有3个重作日志文件组,Oracle固然会继续写第3个组)。编程

数据库之因此成为数据库(而不是文件系统等其余事物),是由于它有本身独有的一些特征,重作日志或事务日志就是其中重要的特性之一。重作日志多是数据库中最重要的恢复结构,不过,若是没有其余部分(如undo段、分布式事务恢复等),但靠重作日志什么也作不了。重作日志是数据库区别于传统文件系统的一个主要因素。Oracle正写到一半的时候有可能发生掉电,利用在线重作日志,咱们就能有效地从这个掉电失败中恢复。归档重作日志则容许咱们从介质失败中恢复,如硬盘损坏,或者因为人为错误而致使数据丢失。若是没有重作日志,数据库提供id保护就比文件系统多不了多少。c#

2 什么是undo

从概念上讲,undo正好与redo相对。你对数据执行修改时,数据库会生成undo信息,这样万一你执行的事务或语句因为某种缘由失败了,或者若是你用一条ROLLBACK语句请求回滚,就能够利用这些undo信息将数据放回到修改前的样子。redo用于在失败时重放事务(即恢复事务),undo则用于取消一条语句或一组语句的做用。与redo不一样,undo在数据库内部存储在一组特殊的段中,这称为undo段(undo segment)。缓存

“回滚段”(rollback segment)和“undo段“(undo segment)通常认为是同义词。使用手动undo管理时,DBA会建立”回滚段“。使用自动undo管理时,系统将根据须要自动地建立和销毁”undo段“。安全

一般对undo有一个误解,认为undo用 于数据库物理地恢复到执行语句或事务以前的样子,但实际上并不是如此。数据库只是逻辑地恢复到原来的样子,全部修改都被逻辑地取消,可是数据结构以及数据库块自己在回滚后可能大不相同。缘由在于:在全部多用户系统中,可能会有数10、数百甚至数千个并发事务。数据库的主要功能之一就是协调对数据的并发访问。也许咱们的事务在修改一些块,而通常来说每每会有许多其余的事务也在修改这些块。所以,不能简单地将一个块放回到咱们的事务开始前的样子,这样会撤销其余人 (其余事务)的工做!性能优化

例如,假设咱们的事务执行了一个INSERT语句,这条语句致使分配一个新区段(也就是说,致使表的空间增大)。经过执行这个INSET,咱们将获得一个新的块,格式化这个块以便使用,并在其中放上一些数据。此时,可能出现另外某个事务,它也向这个块中插入数据。若是要回滚咱们的事务,显然不能取消对这个块的格式化和空间分配。所以,Oracle回滚时,它实际上会作与先前逻辑上相反的工做。对于每一个INSERT,Oracle会完成一个DELETE。对于每一个DELETE,Oracle会执行一个INSERT。对于每一个UPDATE,Oracle则会执行一个“反UPDATE“,或者执行另外一个UPDATE将修改前的行放回去。服务器

这种undo生成对于直接路径操做(direct path operation)不适用,直接路径操做可以绕过表上的undo生成。
怎么才能看到undo生成(undo generation)具体是怎样的呢?也许最容易的方法就是遵循如下步骤:
(1) 建立一个空表。
(2) 对它作一个所有扫描,观察读表所执行的I/O数量。
(3) 在表中填入许多行(但没有提交)
(4) 回滚这个工做,并撤销。
(5) 再次进行全表扫描,观察所执行的I/O数量。session

scott@ORCL>create table t
  2  as
  3  select *
  4  from all_objects
  5  where 1=0;

表已建立。

而后查询这个表,这里在SQL*Plus中启用了AUTOTRACE,以便能测试I/O。

在这个例子中,每次(即每一个用例)都会作两次全表扫描。咱们的目标只是测试每一个用例中第二次完成的I/O。这样能够避免统计在解析和优化期间优化器可能完成的额外I/O。

最初,这个查询须要0个I/O来完成这个表的全表扫描:

scott@ORCL>select * from t;
未选定行

scott@ORCL>set autotrace traceonly statistics
scott@ORCL>select * from t;
未选定行

统计信息
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          0  consistent gets
          0  physical reads
          0  redo size
       1344  bytes sent via SQL*Net to client
        509  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          0  rows processed

scott@ORCL>set autotrace off

接下来,向表中增长大量数据。这会使它“扩大“,不过随后再将其回滚:

scott@ORCL>insert into t select * from all_objects;

已建立71902行。

scott@ORCL>rollback;

回退已完成。

如今,若是再次查询这个表,会发现这一次读表所需的I/O比先前多得多:

scott@ORCL>select * from t;

未选定行

scott@ORCL>set autotrace traceonly statistics
scott@ORCL>select * from t;

未选定行


统计信息
----------------------------------------------------------
          0  recursive calls
          0  db block gets
       1086  consistent gets
          0  physical reads
          0  redo size
       1344  bytes sent via SQL*Net to client
        509  bytes received via SQL*Net from client
          1  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          0  rows processed

前面的INSERT致使将一些块增长到表的高水位线(high-water mark,HWM)之下,这些块没有由于回滚而消失,它们还在那里,并且已经格式化,只不过如今为空。全表扫描必须读取这些块,看看其中是否包含行。这说明,回滚只是一个“将数据库还原“的逻辑操做。数据库并不是真的还原成原来的样子,只是逻辑上相同而已。

2.1 redo和undo如何协做

尽管undo信息存储在undo表空间或undo段中,但也会受到redo的保护。换句话说,会把undo数据当成是表数据或索引数据同样,对undo的修改会生成一些redo,这些redo将计入日志。为何会这样呢?稍后在讨论系统崩溃时发生的状况时将会解释它,到时你会明白了。将undo数据增长到undo段中,并像其余部分的数据同样在缓冲区缓存中获得缓存。

INSERT-UPDATE-DELETE示例场景

做为一个例子,咱们将分析对于下面这组语句可能发生什么状况:

insert into t (x,y) values (1,1);
update t set x = x+1 where x = 1;
delete from t where x = 2;

咱们会沿着不一样的路径完成这个事务,从而获得如下问题的答案:
􀂉 若是系统在处理这些语句的不一样时间点上失败,会发生什么状况?
􀂉 若是在某个时间点上ROLLBACK,会发生什么状况?
􀂉 若是成功并COMMIT,会发生什么状况?

1. INSERT

对于第一条INSERT INTO T语句,redo和undo都会生成。所生成的undo信息足以使INSERT“消失“。INSERT INTO T生成的redo信息则足以让这个插入”再次发生“。

这里缓存了一些已修改的undo块、索引块和表数据块。这些块获得重作日志缓冲区中相应条目的“保护“。

􀁺 假想场景:系统如今崩溃
SGA会被清空,可是咱们并不须要SGA里的任何内容。重启动时就好像这个事务历来没有发生过同样。没有将任何已修改的块刷新输出到磁盘,也没有任何redo刷新输出到磁盘。咱们不须要这些undo或redo信息来实现实例失败恢复。
􀁺 假想场景:缓冲区缓存如今已满
在这种状况下,DBWR必须留出空间,要把已修改的块从缓存刷新输出。若是是这样,DBWR首先要求LGWR将保护这些数据库块的redo条目刷新输出。DBWR将任何有修改的块写至磁盘以前,LGWR必须先刷新输出与这些块相关的redo信息。这是有道理的——若是咱们要刷新输出表T中已修改的块,但没有刷新输出与undo块关联的redo条目,假若系统失败了,此时就会有一个已修改的表T块,而没有与之相关的redo信息。在写出这些块以前须要先刷新输出重作日志缓存区,这样就能重作(重作)全部必要的修改,将SGA放回到如今的状态,从而能发生回滚。
咱们生成了一些已修改的表和索引块。这些块有一些与之关联的undo段块,这3类块都会生成redo来保护本身。重作日志缓冲区 会在如下状况刷新输出:每3秒一次;缓冲区1/3满时或者包含了1MB的缓冲数据;或者是只要发生提交就会刷新输出。重作日志缓冲区还有可能会在处理期间的某一点上刷新输出。

2 UPDATE


UPDATE所带来的工做与INSERT大致同样。不过UPDATE生成的undo量更大;因为存在更新,因此须要保存一些“前“映像。

块缓冲区缓存中会有更多新的undo段块。为了撤销这个更新,若是必要,已修改的数据库表和索引块也会放在缓存中。咱们还生成了更多的重作日志缓存区条目。下面假设前面的插入语句生成了一些重作日志,其中有些重作日志已经刷新输出到磁盘上,有些还放在缓存中。
􀁺 假想场景:系统如今崩溃
启动时,Oracle会读取重作日志,发现针对这个事务的一些重作日志条目。给定系统的当前状态,利用重作日志文件中对应插入的redo条目,并利用仍在缓冲区中对应插入的redo信息,Oracle会“前滚”插入。如今有一些undo块(用以撤销插入)、已修改的表块(刚插入后的状态),以及已修改的索引块(刚插入后的状态)。因为系统正在进行崩溃恢复,并且咱们的会话还再也不链接(这是固然),Oracle发现这个事务从未提交,所以会将其回滚。它取刚刚在缓冲区缓存中前滚获得的undo,并将这些undo应用到数据和索引块,使数据和索引块“恢复”为插入发生前的样子。如今一切都回到从前。磁盘上的块可能会反映前面的INSERT,也可能不反映(这取决于在崩溃前是否已经将块刷新输出)。若是磁盘上的块确实反映了插入,而实际上如今插入已经被撤销,当从缓冲区缓存刷新输出块时,数据文件就会反映出插入已撤销。若是磁盘上的块原本就没有反映前面的插入,就不用去管它——这些块之后确定会被覆盖。
这个场景涵盖了崩溃恢复的基本细节。系统将其做为一个两步的过程来完成。首先前滚,把系统放到失败点上,而后回滚还没有提交的全部工做。这个动做会再次同步数据文件。它会重放已经进行的工做,并撤销还没有完成的全部工做。
􀁺 假想场景:应用回滚事务
此时,Oracle会发现这个事务的undo信息可能在缓存的undo段块中(基本上是这样),也可能已经刷新输出到磁盘上(对于很是大的事务,就每每是这种状况)。它会把undo信息应用到缓冲区缓存中的数据和索引块上,或者假若数据和索引块已经不在缓存中,则要从磁盘将数据和索引块读入缓存,再对其应用undo。这些块会恢复为其原来的行值,并刷新输出到数据文件。
这个场景比系统崩溃更常见。须要指出,有一点颇有用:回滚过程当中从不涉及重作日志。只有恢复和归档时会当前重作日志。这对于调优是一个很重要的概念:重作日志是用来写的(而不是用于读)。Oracle不会在正常的处理中读取重作日志。只要你有足够的设备,使得ARCH读文件时,LGWR能写到另外一个不一样的设备,那么就不存在重作日志竞争。Oracle的目标是:能够顺序地写日志,并且在写日志时别人不会读日志

3. DELETE

一样,DELETE会生成undo,块将被修改,并把redo发送到重作日志缓冲区。这与前面没有太大的不一样。实际上,它与UPDATE如此相似。


4. COMMIT

咱们已经看到了多种失败场景和不一样的路径,如今终于到COMMIT了。在此,Oracle会把重作日志缓冲区刷新输出到磁盘。

已修改的块放在缓冲区缓存中;可能有一些块已经刷新输出到磁盘上。重作这个事务所需的所有redo都安全地存放在磁盘上,如今修改已是永久的了。若是从数据文件直接读取数据,可能会看到块仍是事务发生前的样子,由于颇有可能DBWR尚未(从缓冲区缓存)写出这些块。这没有关系,若是出现失败,能够利用重作日志文件来获得最新的块。undo信息会一直存在,除非undo段回绕重用这些undo块。若是某些对象受到影响,Oracle会使用这个undo信息为须要这些对象的会话提供对象的一致读。

3 提交和回滚处理

3.1 COMMIT作什么

COMMIT一般是一个很是快的操做,而不论事务大小如何。

不论事务有多大,COMMIT的响应时间通常都很“平”(flat,能够理解为无高低变化)。这是由于COMMIT并无太多的工做去作,不过它所作的确实相当重要。

这 一点很重要,之因此要了解并掌握这个事实,缘由之一是:这样你就能心无芥蒂地让事务有足够的大小。许多开发人员会人为地限制事务的大 小,分别提交太多的行,而不是一个逻辑工做单元完成后才提交。这样作主要是出于一种错误的信念,即认为能够节省稀有的系统资源,而实际上这只是增长了资源 的使用。若是一行的COMMIT须要X个时间单位,1,000次COMMIT也一样须要X个时间单位,假若采用如下方式执行工做,即每行提交一次共执行1,000次COMMIT,就会须要1000*X各时间单位才能完成。若是只在必要时才提交(即逻辑工做单元结束时),不只能提升性能,还能减小对共享资源的竞争(日志文件、各类内部闩等)。经过一个简单的例子就能展现出过多的提交要花费更长的时间。这里将使用一个Java应用,不过对于大多数其余客户程序来讲,结果可能都是相似的,只有PL/SQL除外。首先,下面是咱们要插入的示例表:

scott@ORCL>create table test
  2  (
  3    ID       NUMBER not null,
  4    CODE     VARCHAR2(20),
  5    DESCR    VARCHAR2(20),
  6    INSERT_USER      VARCHAR2(30) ,
  7    INSERT_DATE DATE
  8  );

表已建立。

scott@ORCL>desc test
 名称
                                   是否为空? 类型
 -------------                    --------  ------------------------

 ID
                                   NOT NULL NUMBER
 CODE
                                            VARCHAR2(20)
 DESCR
                                            VARCHAR2(20)
 INSERT_USER
                                            VARCHAR2(30)
 INSERT_DATE
                                            DATE

Java程序要接受两个输入:要插入(INSERT)的行数(iters),以及两次提交之间插入的行数(commitCnt)。它先链接到数据库,将autocommit(自动提交)设置为off(全部Java代码都应该这么作),而后将doInserts()方法共调用3次:
􀂉 第一次调用只是“热身”(确保全部类都已经加载)。
􀂉 第二次调用指定了要插入(INSERT)的行数,并指定一次提交多少行(即每N行提交一次)。
􀂉 最后一次调用将要插入的行数和一次提交的行数设置为相同的值(也就是说,全部行都插入以后才提交)。
而后关闭链接,并退出:

import corp.creditease.rsc.service.IDataCarrierService;
import corp.creditease.rsc.service.impl.DataCarrierServiceImpl;
import org.junit.Test;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Date;

/**
 * Created by Anna on 2017/7/21.
 */
public class Test1 {

    public static void main(String arr[]) throws Exception {
        try {
            DriverManager.registerDriver(new oracle.jdbc.OracleDriver());
            Connection con = DriverManager.getConnection
                    ("jdbc:oracle:thin:@localhost:1521:orcl",
                            "scott", "123456");
            Integer iters = new Integer(arr[0]);
            Integer commitCnt = new Integer(arr[1]);
            con.setAutoCommit(false);
            doInserts(con, 1, 1);
            doInserts(con, iters.intValue(), commitCnt.intValue());
            doInserts(con, iters.intValue(), iters.intValue());
            con.commit();
            con.close();
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    static void doInserts(Connection con, int count, int commitCount)
            throws Exception {
        PreparedStatement ps =
                con.prepareStatement
                        ("insert into test " +
                                "(id, code, descr, insert_user, insert_date)"
                                + " values (?,?,?, user, sysdate)");
        int rowcnt = 0;
        int committed = 0;
        long start = new Date().getTime();
        for (int i = 0; i < count; i++) {
            ps.setInt(1, i);
            ps.setString(2, "PS - code" + i);
            ps.setString(3, "PS - desc" + i);
            ps.executeUpdate();
            rowcnt++;
            if (rowcnt == commitCount) {
                con.commit();
                rowcnt = 0;
                committed++;
            }
        }
        con.commit();
        long end = new Date().getTime();
        System.out.println
                ("pstatement " + count + " times in " +
                        (end - start) + " milli seconds committed = " + committed);
    }

}

doInserts()方法至关简单。首先准备(解析)一条INSERT语句,以便屡次反复绑定/执行这个INSERT。

而后根据要插入的行数循环,反复绑定和执行这个INSERT。另外,它会检查一个行计数器,查看是否须要COMMIT,或者是否已经不在循环范围内。还要注意,咱们分别在循环以前和循环以后获取了当前时间,从而监视并报告耗用的时间。

下面根据不一样的输入发放运行这个代码:

pstatement 1 times in 0 milli seconds committed = 1
pstatement 10000 times in 5501 milli seconds committed = 10000
pstatement 10000 times in 1961 milli seconds committed = 1

----------------------------------------------------------------------------------------

pstatement 1 times in 0 milli seconds committed = 1
pstatement 100000 times in 20721 milli seconds committed = 10000
pstatement 100000 times in 9157 milli seconds committed = 1

----------------------------------------------------------------------------------------

pstatement 1 times in 31 milli seconds committed = 1
pstatement 1000000 times in 113189 milli seconds committed = 10000
pstatement 1000000 times in 95054 milli seconds committed = 1

 

能够看到,提交得越多,花费的时间就越长。这只是单用户的状况,若是有多个用户在作一样的工做,全部这些用户都过于频繁地提交,那么获得的数字将飞速增加。
在其余相似的状况下,咱们也不止一次地听到过一样的“故事”。例如,若是不使用绑定变量,并且频繁地完成硬解析,这会严重地下降并发性,缘由是存在库缓存竞争和过量的CPU占用。即便转而使用绑定变量,若是过于频繁地软解析,也会带来大量的开销(致使过多软解析的缘由多是:执意地关闭游标,尽管稍后就会重用这些游标)。必须在必要时才完成操做,COMMIT就是这样的一种操做。最好根据业务需求来肯定事务的大小,而不是错误地为了减小数据库上的资源使用而“压缩”事务。

在这个例子中,COMMIT的开销存在两个因素:
􀂉 显然会增长与数据库的往返通讯。若是每一个记录都提交,生成的往返通讯量就会大得多。
􀂉 每次提交时,必须等待redo写至磁盘。这会致使“等待”。在这种状况下,等待称为“日志文件同步”(log file sync)。
􀂉 只需对这个Java应用稍作修改就能够观察到后面这一条。咱们将作两件事情:
􀂉 增长一个DBMS_MONITOR调用,启用对等待事件的SQL跟踪。在Oracle9i中,则要使用alter session set events ‘10046 trace name context forever, level 12’,由于
DBMS_MONITOR是Oracle 10g中新增的。
􀂉 把con.commit()调用改成一条完成提交的SQL语句调用。若是使用内置的JDBC commit()调用,这不会向跟踪文件发出SQL COMMIT语句,而TKPROF(用于格式化跟踪文件的工具)也不会报告完成COMMIT所花费的时间。

若是在每一个INSERT以后都提交,几乎每次都要等待。尽管每次只等待很短的时间,可是因为常常要等待,这些时间就会累积起来。与之造成鲜明对比,若是只提交一次,就不会等待很长时间(实际上,这个时间实在过短了,以致于简直没法度量)。这说明,COMMIT是一个很快的操做;咱们但愿响应时间或多或少是“平”的,而不是所完成工做量的一个函数。

那么,为何COMMIT的响应时间至关“平”,而不论事务大小呢?在数据库中执行COMMIT以前,困难的工做都已经作了。咱们已经修改了数据库中的数据,因此99.9%的工做都已经完成。例如,已经发生了如下操做:
􀂉 已经在SGA中生成了undo块。
􀂉 已经在SGA中生成了已修改数据块。
􀂉 已经在SGA中生成了对于前两项的缓存redo。
􀂉 取决于前三项的大小,以及这些工做花费的时间,前面的每一个数据(或某些数据)可能已经刷新输出到磁盘。
􀂉 已经获得了所需的所有锁。

执行COMMIT时,余下的工做只是:
1.  为事务生成一个SCN。SCN是Oracle使用的一种简单的计时机制,用于保证事务的顺序,并支持失败恢复。SCN还用于保证数据库中的读一致性和检查点。能够把SCN看做一个钟摆,每次有人COMMIT时,SCN都会增1.
2.  LGWR将全部余下的缓存重作日志条目写到磁盘,并把SCN记录到在线重作日志文件中。这一步就是真正的COMMIT。若是出现了这一步,即已经提交。事务条目会从V$TRANSACTION中“删除”,这说明咱们已经提交。
3. V$LOCK中记录着咱们的会话持有的锁,这些所都将被释放,而排队等待这些锁的每个人都会被唤醒,能够继续完成他们的工做
4. 若是事务修改的某些块还在缓冲区缓存中,则会以一种快速的模式访问并“清理”。块清除(Block cleanout)是指清除存储在数据库块首部的与锁相关的信息。实质上讲,咱们在清除块上的事务信息,这样下一个访问这个块的人就不用再这么作了。咱们采用一种无需生成重作日志信息的方式来完成块清除,这样能够省去之后的大量工做。

能够看到,处理COMMIT所要作的工做不多。其中耗时最长的操做要算LGWR执行的活动(通常是这样),由于这些磁盘写是物理磁盘I/O。不过,这里LGWR花费的时间并不会太多,之因此能大幅减小这个操做的时间,缘由是LGWR一直在以连续的方式刷新输出重作日志缓冲区的内容。在你工做期间,LGWR并不是缓存着你作的全部工做;实际上,随着你的工做的进行,LGWR会在后台增量式地刷新输出重作日志缓冲区的内容。这样作是为了不COMMIT等待很长时间来一次性刷新输出全部的redo。
所以,即便咱们有一个长时间运行的事务,但在提交以前,它生成的许多缓存重作日志已经刷新输出到磁盘了(而不是所有等到提交时才刷新输出)。这也有很差的一面,COMMIT时,咱们必须等待,直到还没有写出的全部缓存redo都已经安全写到磁盘上才行。也就是说,对LGWR的调用是一个同步(synchronous)调用。尽管LGWR自己能够使用异步I/O并行地写至日志文件,可是咱们的事务会一直等待LGWR完成全部写操做,并收到数据都已在磁盘上的确认才会返回。
PL/SQL提供了提交时优化(commit-time optimization)。LGWR是一个同步调用,咱们要等待它完成全部写操做。PL/SQL引擎不一样,要认识到直到PL/SQL例程完成以前,客户并不知道这个PL/SQL例程中是否发生了COMMIT,因此PL/SQL引擎完成的是异步提交。它不会等待LGWR完成;相反,PL/SQL引擎会从COMMIT调用当即返回。不过,等到PL/SQL例程完成,咱们从数据库返回客户时,PL/SQL例程则要等待LGWR完成全部还没有完成的COMMIT。所以,若是在PL/SQL中提交了100次,而后返回客户,会发现因为存在这种优化,你只会等待LGWR一次,而不是100次。指导原则是,应该在逻辑工做单元完成时才提交,而不要在此以前草率地提交。

若是你在执行分布式事务或者以最大可能性模式执行Data Guard,PL/SQL中的这种提交时优化可能会被挂起。由于此时存在两个参与者,PL/SQL必须等待提交确实完成后才能继续。

为了说明COMMIT是一个“响应时间很平”的操做,下面将生成不一样大小的redo,并测试插入(INSERT)和提交(COMMIT)的时间。为此,仍是在SQL*Plus中使用AUTOTRACE。首先建立一个大表(要把其中的测试数据插入到另外一个表中),再建立一个空表:

scott@ORCL>@D:\app\Administrator\product\11.2.0\big_table.sql 100000

表已建立。

表已更改。

原值    3: l_rows number := &1;
新值    3: l_rows number := 100000;

PL/SQL 过程已成功完成。

表已更改。

PL/SQL 过程已成功完成。

  COUNT(*)
----------
    100000


scott@ORCL>create table t as select * from big_table where 1=0;

表已建立。

而后在SQL*Plus中运行如下命令:

scott@ORCL>set timing on
scott@ORCL>set autotrace on statistics;
scott@ORCL>insert into t select * from big_table where rownum <= 10;

已建立10行。

已用时间:  00: 00: 00.18

统计信息
----------------------------------------------------------
        459  recursive calls
         56  db block gets
         72  consistent gets
          6  physical reads
       7196  redo size
       1134  bytes sent via SQL*Net to client
       1301  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          2  sorts (memory)
          0  sorts (disk)
         10  rows processed

scott@ORCL>commit;

提交完成。

已用时间:  00: 00: 00.03
scott@ORCL>insert into t select * from big_table where rownum <= 100;

已建立100行。

已用时间:  00: 00: 00.03

统计信息
----------------------------------------------------------
          1  recursive calls
         13  db block gets
          8  consistent gets
          0  physical reads
       9936  redo size
       1134  bytes sent via SQL*Net to client
       1302  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
        100  rows processed

scott@ORCL>commit;

提交完成。

已用时间:  00: 00: 00.00
scott@ORCL>insert into t select * from big_table where rownum <= 1000;

已建立1000行。

已用时间:  00: 00: 00.04

统计信息
----------------------------------------------------------
         65  recursive calls
        208  db block gets
         66  consistent gets
         15  physical reads
     113008  redo size
       1134  bytes sent via SQL*Net to client
       1303  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
       1000  rows processed

scott@ORCL>commit;

提交完成。

已用时间:  00: 00: 00.00
scott@ORCL>insert into t select * from big_table where rownum <= 10000;

已建立10000行。

已用时间:  00: 00: 00.07

统计信息
----------------------------------------------------------
        449  recursive calls
       1807  db block gets
        523  consistent gets
        128  physical reads
    1136052  redo size
       1134  bytes sent via SQL*Net to client
       1304  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
      10000  rows processed

scott@ORCL>commit;

提交完成。

已用时间:  00: 00: 00.00
scott@ORCL>insert into t select * from big_table where rownum <= 100000;

已建立100000行。

已用时间:  00: 00: 04.73

统计信息
----------------------------------------------------------
        353  recursive calls
      13306  db block gets
       4315  consistent gets
       1283  physical reads
   12054296  redo size
       1135  bytes sent via SQL*Net to client
       1305  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
     100000  rows processed

scott@ORCL>commit;

提交完成。

已用时间:  00: 00: 00.00

在此监视AUTOTRACE提供的redo size(redo大小)统计,并经过set timing on监视计时信息。

尝试插入不一样数目的行(行数从10到100,000,每次增长一个数量级)。

插入行数        插入时间(秒)    redo大小(字节)        提交时间(秒)
10                    00.18                  7196                            00.03
100                  00.03                  9936                            00.00
1,000               00.04                  113008                        00.00
10,000             00.07                  1136052                       00.00
100,000           04.73                  12054296                     00.00

能够看到,使用一个精确度为百分之一秒的计数器度量时,随着生成不一样数量的redo(从7196字节到12MB),却几乎测不出COMMIT时间的差别。在咱们处理和生成重作日志时,LGWR也没有闲着,它在后台不断地将缓存的重作信息刷新输出到磁盘上。因此,咱们生成12MB的重作日志信息时,LGWR一直在忙着,可能每1MB左右刷新输出一次。等到COMMIT时,剩下的重作日志信息(即还没有写出到磁盘的redo)已经很少了,可能与建立10行数据生成的重作日志信息相差无几。不论生成了多少redo,结果应该是相似的(但可能不彻底同样)。

3.2 ROLLBACK作什么

把COMMIT改成ROLLBACK,可能会获得彻底不一样的结果。回滚时间绝对是所修改数据量的一个函数。修改上一节中的脚本,要求完成一个ROLLBACK(只需把COMMIT改为ROLLBACK),计时信息将彻底不一样。

ROLLBACK必须物理地撤销咱们所作的工做。相似于COMMIT,必须完成一系列操做。在到达ROLLBACK以前,数据库已经作了大量的工做。可能已经发生的操做以下:
1. 已经在SGA中生成了undo块。
2. 已经在SGA中生成了已修改数据块。
3. 已经在SGA中生成了对于前两项的缓存redo。
4. 取决于前三项的大小,以及这些工做花费的时间,前面的每一个数据(或某些数据)可能已经刷新输出到磁盘。
5. 已经获得了所需的所有锁。
ROLLBACK时,要作如下工做:
1. 撤销已作的全部修改。其完成方式以下:从undo段读回数据,而后实际上逆向执行前面所作的操做,并将undo条目标记为已用。若是先前插入了一行,ROLLBACK会将其删除。若是更新了一行,回滚就会取消更新。若是删除了一行,回滚将把它再次插入。
2. 会话持有的全部锁都将释放,若是有人在排队等待咱们持有的锁,就会被唤醒。
与此不一样,COMMIT只是将重作日志缓冲区中剩余的数据刷新到磁盘。与ROLLBACK相比,COMMIT完成的工做很是少。这里的关键是,除非不得已,不然不会但愿回滚。回滚操做的开销很大,由于你花了大量的时间作工做,还要花大量的时间撤销这些工做。除非你有把握确定会COMMIT你的工做,不然干脆什么也别作。

4 分析redo

做为一名开发人员,应该可以测量你的操做生成了多少redo,这每每很重要。生成的redo越多,你的操做花费的时间就越长,整个系统也会越慢。你不光在影响你本身的会话,还会影响每个会话。redo管理是数据库中的一个串行点。任何Oracle实例都只有一个LGWR,最终全部事务都会归于LGWR,要求这个进程管理它们的redo,并COMMIT其事务,LGWR要作的越多,系统就会越慢。经过查看一个操做会生成多少redo,并对一个问题的多种解决方法进行测试,能够从中找出最佳的方法。

4.1 测量redo

要查看生成的redo量至关简单,能够使用了SQL*Plus的内置特性AUTOTRACE。不过AUTOTRACE只能用于简单的DML,对其余操做就力所不能及了,例如,它没法查看一个存储过程调用作了什么。为此,咱们须要访问两个动态性能视图:
1. V$MYSTAT,其中有会话的提交信息。
2. V$STATNAME,这个视图能告诉咱们V$MYSTAT中的每一行表示什么(所查看的统计名)。
由于我常常要作这种测量,因此使用了两个脚本,分别为mystat和mystat2。mystat.sql脚本把我感兴趣的统计初始值(如redo大小)保存在一个SQL*Plus变量中。

mystat.sql:

set verify off
column value new_val V
define S="&1"
set autotrace off
select a.name, b.value
from v$statname a, v$mystat b
where a.statistic# = b.statistic#
and lower(a.name) like '%' || lower('&S')||'%'
/

mystat2.sql脚本只是打印出该统计的初始值和结束值之差:

set verify off
select a.name, b.value V, to_char(b.value-&V,'999,999,999,999') diff
from v$statname a, v$mystat b
where a.statistic# = b.statistic#
and lower(a.name) like '%' || lower('&S')||'%'
/

下面,能够测量一个给定事务会生成多少redo。咱们只需这样作:

@mystat "redo size"
...process...
@mystat2

例如:

scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size"

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
redo size                                                          28053348
redo size for lost write detection                                        0
redo size for direct writes                                            7576

已用时间:  00: 00: 00.12
scott@ORCL>insert into t select * from big_table;

已建立100000行。

已用时间:  00: 00: 00.97
scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat2

NAME                                                                      V DIFF

---------------------------------------------------------------- ---------- ----------------
redo size                                                          39946884 39,939,308
redo size for lost write detection                                        0 -7,576
redo size for direct writes                                            7576 0

已用时间:  00: 00: 00.01

如上所示,这个INSERT生成了大约39MB的redo。你可能想与一个直接路径INSERT生成的redo作个比较,以下:

scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size"

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
redo size                                                          39946884
redo size for lost write detection                                        0
redo size for direct writes                                            7576

已用时间:  00: 00: 00.01
scott@ORCL>insert /*+ APPEND */ into t select * from big_table;

已建立100000行。

已用时间:  00: 00: 00.45
scott@ORCL>@D:\app\Administrator\product\11.2.0\mystat2

NAME                                                                      V DIFF

---------------------------------------------------------------- ---------- ----------------
redo size                                                          39980616 39,973,040
redo size for lost write detection                                        0 -7,576
redo size for direct writes                                           10072 2,496

已用时间:  00: 00: 00.00
scott@ORCL>set echo off

4.2 redo生成和BEFORE/AFTER触发器

BEFORE触发器要额外的redo信息,即便它根本没有修改行中的任何值
1. BEFORE或AFTER触发器不影响DELETE生成的redo
2. 在Oracle9i Release 2 及之前版本中,BEFORE或AFTER触发器会使INSERT生成一样数量的额外redo。在Oracle 10g中,则不会生成任何额外的redo。
3.在Oracle9i Release 2及之前的全部版本中,UPDATE生成的redo只受BEFORE触发器的影响。AFTER触发器不会增长任何额外的redo。不过,在Oracle 10g中,状况又有所变化。具体表现为:
3.1 总的来说,若是一个表没有触发器,对其更新期间生成的redo量老是比Oracle9i及之前版本中要少。看来这是Oracle着力解决的一个关键问题:对于触发器的表,要减小这种表更新所生成的redo量
3.2 在Oracle 10g中,若是表有一个BEFORE触发器,则其更新期间生成的redo量比9i中更大。
3.3 若是表有AFTER触发器,则更新所生成的redo量与9i中同样。

为了完成这个测试,咱们要使用一个表T,定义以下:

create table t ( x int, y char(N), z date );

可是,建立时N的大小是可变的。在这个例子中,将使用N=30、100、500、1,000和2,000来获得不一样宽度的行。针对不一样大小的Y列运行测试,再来分析结果。使用了一个很小的日志表来保存屡次运行的结果:

scott@ORCL>create table log ( what varchar2(15), -- will be no trigger, after or before
  2  op varchar2(10), -- will be insert/update or delete
  3  rowsize int, -- will be the size of Y
  4  redo_size int, -- will be the redo generated
  5  rowcnt int ) -- will be the count of rows affected
  6  ;

表已建立。

这里使用如下DO_WORK存储过程来生成事务,并记录所生成的redo。子过程REPORT是一个本地过程(只在DO_WORK过程当中可见),它只是在屏幕上报告发生了什么,并把结果保存到LOG表中:

scott@ORCL>create or replace procedure do_work( p_what in varchar2 )
  2  as
  3     l_redo_size number;
  4     l_cnt number := 200;
  5
  6     procedure report( l_op in varchar2 )
  7     is
  8     begin
  9                     select v$mystat.value-l_redo_size
 10                                             into l_redo_size
 11                             from v$mystat, v$statname
 12                             where v$mystat.statistic# = v$statname.statistic#
 13                                             and v$statname.name = 'redo size';
 14
 15                     dbms_output.put_line(l_op || ' redo size = ' || l_redo_size ||
 16                                     ' rows = ' || l_cnt || ' ' ||
 17                                     to_char(l_redo_size/l_cnt,'99,999.9') ||

 18                                     ' bytes/row' );
 19                     insert into log
 20                                     select p_what, l_op, data_length, l_redo_size, l_cnt
 21                                     from user_tab_columns
 22                                     where table_name = 'T'
 23                                     and column_name = 'Y';
 24     end;
 25
 26     procedure set_redo_size
 27     as
 28     begin
 29             select v$mystat.value
 30                             into l_redo_size
 31             from v$mystat, v$statname
 32             where v$mystat.statistic# = v$statname.statistic#
 33                             and v$statname.name = 'redo size';
 34     end;
 35
 36     begin
 37             set_redo_size;
 38             insert into t
 39                             select object_id, object_name, created
 40                             from all_objects
 41                             where rownum <= l_cnt;
 42             l_cnt := sql%rowcount;
 43             commit;
 44             report('insert');
 45
 46             set_redo_size;
 47             update t set y=lower(y);
 48             l_cnt := sql%rowcount;
 49             commit;
 50             report('update');
 51
 52             set_redo_size;
 53             delete from t;
 54             l_cnt := sql%rowcount;
 55             commit;
 56             report('delete');
 57     end;
 58  /

过程已建立。

下面将Y列的宽度设置为2,000,而后运行如下脚原本测试3种场景:没有触发器、有BEFORE触发器,以及有AFTER触发器。

scott@ORCL>alter table t modify y char(2000);
表已更改。

scott@ORCL>exec do_work('no trigger');
insert redo size = 474996 rows = 200   2,375.0 bytes/row
update redo size = 1593920 rows = 200   7,969.6 bytes/row
delete redo size = 472872 rows = 200   2,364.4 bytes/row
PL/SQL 过程已成功完成。

scott@ORCL>create or replace trigger before_insert_update_delete
  2  before insert or update or delete on T for each row
  3  begin
  4     null;
  5  end;
  6  /
触发器已建立

scott@ORCL>truncate table t;
表被截断。

scott@ORCL>exec do_work('before trigger');
insert redo size = 470580 rows = 200   2,352.9 bytes/row
update redo size = 894620 rows = 200   4,473.1 bytes/row
delete redo size = 472528 rows = 200   2,362.6 bytes/row
PL/SQL 过程已成功完成。


scott@ORCL>drop trigger before_insert_update_delete;
触发器已删除。

scott@ORCL>create or replace trigger after_insert_update_delete
  2  after insert or update or delete on T
  3  for each row
  4  begin
  5     null;
  6  end;
  7  /
触发器已建立

scott@ORCL>truncate table t;
表被截断。


scott@ORCL>exec do_work( 'after trigger' );
insert redo size = 501564 rows = 200   2,507.8 bytes/row
update redo size = 854880 rows = 200   4,274.4 bytes/row
delete redo size = 472836 rows = 200   2,364.2 bytes/row

PL/SQL 过程已成功完成。

前面的输出是在把Y大小设置为2,000字节时运行脚本所获得的。完成全部运行后,就能查询LOG表,并看到如下结果:

scott@ORCL>break on op skip 1
scott@ORCL>set numformat 999,999
scott@ORCL>select op, rowsize, no_trig,
  2  before_trig-no_trig, after_trig-no_trig
  3  from
  4  ( select op, rowsize,
  5             sum(decode( what, 'no trigger', redo_size/rowcnt,0 ) ) no_trig,
  6             sum(decode( what, 'before trigger', redo_size/rowcnt, 0 ) ) before_trig,
  7             sum(decode( what, 'after trigger', redo_size/rowcnt, 0 ) ) after_trig
  8  from log
  9  group by op, rowsize
 10  )
 11  order by op, rowsize
 12  /

OP          ROWSIZE  NO_TRIG BEFORE_TRIG-NO_TRIG AFTER_TRIG-NO_TRIG
---------- -------- -------- ------------------- ------------------
delete        2,000    2,364                  -2                 -0

insert        2,000    2,375                 -22                133

update        2,000    7,970              -3,497             -3,695

日志模式(ARCHIVELOG和NOARCHIVELOG模式)不会影响这些结果,这两种模式获得的结果数同样。

触发器对redo生成的影响
DML操做    AFTER触发器         BEFORE触发器
                (10g)                    (10g)
DELETE        不影响                 不影响
INSERT        常量redo             常量redo
UPDATE       增长redo            增长redo

如今你应该知道怎么来估计redo量:
1. 估计你的“事务”大小(你要修改多少数据)。
2. 在要修改的数据量基础上再加10%~20%的开销,具体增长多大的开销取决于你要修改的行数。修改行越多,增长的开销就越小。
3. 对于UPDATE,要把这个估计值加倍。
在大多数状况下,这将是一个很好的估计。UPDATE的估计值加倍只是一个猜想,实际上这取决于你修改了多少数据。之因此加倍,是由于在此假设要取一个X字节的行,并把它更新(UPDATE)为另外一个X字节的行。若是你取一个小行(数据量较少的行),要把它更新为一个大行(数据量较多的行),就不用对这个值加倍(这更像是一个INSERT)。若是取一个大行,而把它更新为一个小行,也不用对这个值加倍(这更像是一个DELETE)。加倍只是一种“最坏状况”,由于许多选项和特性会对此产生影响,例如,存在索引或者没有索引也会影响这个底线。维护索引结构所必须的工做量对不一样的UPDATE来讲是不一样的,此外还有一些影响因素。除了前面所述的固定开销外,还必须把触发器的反作用考虑在内。另外要考虑到Oracle为你执行的隐式操做(如外键上的ON DELETE CASCADE设置)。有了这些考虑,你就能适当地估计redo量以便调整事务大小以及实现性能优化。

4.3 能关掉重作日志生成吗

不能。由于重作日志对于数据库相当重要;它不是开销,不是浪费。重作日志对你来讲确确实实必不可少。这是没法改变的事实,也是数据库采用的工做方式。若是你真的“关闭了redo”,那么磁盘驱动器的任何暂时失败、掉电或每一个软件崩溃都会致使整个数据库不可用,并且不可恢复。有些状况下 执行某些操做时确实能够不生成重作日志。

4.3.1  在SQL中设置NOLOGGING

有些SQL语句和操做支持使用NOLOGGING子句。这并非说:这个对象的全部操做在执行时都不生成重作日志,而是说有些特定操做生成的redo会比日常(即不使用NOLOGGING子句时)少得多。只是说“redo”少得多,而不是“彻底没有redo“。全部操做都会生成一些redo——不论日志模式是什么,全部数据字典操做都会计入日志。只不过使用NOLOGGING子句后,生成的redo量可能会显著减小。下面是使用NOLOGGING子句的一个例子。

为此先在采用ARCHIVELOG模式运行的一个数据库中运行如下命令:

sys@ORCL>select log_mode from v$database;

LOG_MODE
------------
ARCHIVELOG


sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size"

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
redo size                                                                 0
redo size for lost write detection                                        0
redo size for direct writes                                               0

sys@ORCL>set echo off


sys@ORCL>create table t
  2  as
  3  select * from all_objects;

表已建立。

sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2

NAME                                                                      V DIFF

---------------------------------------------------------------- ---------- ----
------------
redo size                                                           8535120 8,535,120
redo size for lost write detection                                        0 0
redo size for direct writes                                         8442060 8,442,060

sys@ORCL>set echo off

这个CREATE TABLE生成了大约8.5MB的redo信息。接下来删除这个表,再重建,不过这一次采用NOLOGGING模式:

sys@ORCL>drop table t;

表已删除。

sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size"

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
redo size                                                           8578096
redo size for lost write detection                                        0
redo size for direct writes                                         8442060

sys@ORCL>set echo off
sys@ORCL>create table t
  2  NOLOGGING
  3  as
  4  select * from all_objects;

表已建立。

sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2

NAME                                                                      V DIFF

---------------------------------------------------------------- ---------- ----------------
redo size                                                           8663676     221,616
redo size for lost write detection                                        0  -8,442,060
redo size for direct writes                                         8443828       1,768

sys@ORCL>set echo off

这一次,只生成了221KB的redo信息。

能够看到,差距很悬殊:原来有8.5MB的redo,如今只有221KB。8.5MB是实际的表数据自己;如今它直接写至磁盘,对此没有生成重作日志。

若是对一个NOARCHIVELOG模式的数据库运行这个测试,就看不到什么差异。在NOARCHIVELOG模式的数据库中,除了数据字典的修改外,CREATE TABLE不会记录日志。若是你想在NOARCHIVELOG模式的数据库上看到差异,能够把对表T的DROP TABLE和CREATE TABLE换成DROP INDEX和CREATE INDEX。默认状况下,不论数据库以何种模式运行,这些操做都会生成日志。从这个例子能够得出一个颇有意义的提示:要按生产环境中所采用的模式来测试你的系统,由于不一样的模式可能致使不一样的行为。你的生产系统可能采用AUCHIVELOG模式运行;假若你执行的大量操做在ARCHIVELOG模式下会生成redo,而在NOARCHIVELOG模式下不会生成redo,你确定想在测试时就发现这一点!

必须很是谨慎地使用NOLOGGING模式,并且要与负责备份和恢复的人沟通以后才能使用。下面假设你建立了一个非日志模式的表,并做为应用的一部分(例如,升级脚本中使用了CREATE TABLE AS SELECT NOLOGGING)。用户白天修改了这个表。那天晚上,表所在的磁盘出了故障。“不要紧“,DBA说”数据库在用ARCHIVELOG模式运行,咱们能够执行介质恢复“。不过问题是,如今没法从归档重作日志恢复最初建立的表,由于根本没有生成日志,使得出现介质失败后DBA没法全面地恢复数据库。这个表将没法恢复。

关于NOLOGGING操做,须要注意如下几点:
1.  事实上,仍是会生成必定数量的redo。这些redo的做用是保护数据字典。这是不可避免的。与之前(不使用NOLOGGING)相比,尽管生成的redo量要少多了,可是确实会有一些redo。
2. NOLOGGING不能避免全部后续操做生成redo。在前面的例子中,我建立的并不是不生成日志的表。只是建立表(CREATE TABLE)这一个操做没有生成日志。全部后续的“正常“操做(如INSERT、UPDATE和DELETE)仍是会生成日志。其余特殊的操做(如使用SQL*Loader的直接路径加载,或使用INSERT /*+ APPEND */语法的直接路径插入)不生成日志(除非你ALTER这个表,再次启用彻底的日志模式)。不过,通常来讲,应用对这个表执行的操做都会生成日志。
3. 在一个ARCHIVELOG模式的数据库上执行NOLOGGING操做后,必须尽快为受影响的数据文件创建一个新的基准备份,从而避免因为介质失败而丢失对这些对象的后续修改。实际上,咱们并不会丢失后来作出的修改,由于这些修改确实在重作日志中;咱们真正丢失的只是要应用这些修改的数据(即最初的数据)。

4.3.2  在索引上设置NOLOGGING

使用NOLOGGING选项有两种方法。1. NOLOGGING关键字潜在SQL命令中。2. 在段(索引或表)上设置NOLOGGING属性,从而隐式地采用NOLOGGING模式来执行操做。例如,能够把一个索引或表修改成默认采用NOLOGGING模式。这说明,之后重建这个索引不会生成日志(其余索引和表自己可能还会生成redo,可是这个索引不会):

sys@ORCL>create index t_idx on t(object_name);

索引已建立。

sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size"

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
redo size                                                          11644980
redo size for lost write detection                                        0
redo size for direct writes                                        11389496

sys@ORCL>set echo off
sys@ORCL>alter index t_idx rebuild;

索引已更改。

sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2

NAME                                                                      V DIFF

---------------------------------------------------------------- ---------- ----------------
redo size                                                          14635016   3,245,520
redo size for lost write detection                                        0 -11,389,496
redo size for direct writes                                        14335208   2,945,712

sys@ORCL>set echo off

这个索引采用LOGGING模式(默认),重建这个索引会生成 3.2MB 的重作日志。不过,能够以下修改这个索引:

sys@ORCL>alter index t_idx nologging;

索引已更改。

sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat "redo size"

NAME                                                                  VALUE
---------------------------------------------------------------- ----------
redo size                                                          14637616
redo size for lost write detection                                        0
redo size for direct writes                                        14335208

sys@ORCL>set echo off
sys@ORCL>alter index t_idx rebuild;

索引已更改。

sys@ORCL>@D:\app\Administrator\product\11.2.0\mystat2

NAME                                                                      V DIFF

---------------------------------------------------------------- ---------- ----------------
redo size                                                          14685008     349,800
redo size for lost write detection                                        0 -14,335,208
redo size for direct writes                                        14340044       4,836

sys@ORCL>set echo off

如今它只生成349KB的redo。可是,如今这个索引没有获得保护(unprotected),若是它所在的数据文件失败而必须从一个备份恢复,咱们就会丢失这个索引数据。如今索引是不可恢复的,因此须要作一个备份。或者,DBA也能够干脆建立索引,由于彻底能够从表数据直接建立索引。

4.3.3 NOLOGGING小结

能够采用NOLOGGING模式执行如下操做:
1.  索引的建立和ALTER(重建)。
2. 表的批量INSERT(经过/*+APPEND */提示使用“直接路径插入“。或采用SQL*Loader直接路径加载)。表数据不生成redo,可是全部索引修改会生成redo,可是全部索引修改会生成redo(尽管表不生成日志,但这个表上的索引却会生成redo!)。
3. LOB操做(对大对象的更新没必要生成日志)。
4.  经过CREATE TABLE AS SELECT建立表。
5. 各类ALTER TABLE操做,如MOVE和SPLIT。
在一个ARCHIVELOG模式的数据库上,若是NOLOGGING使用得当,能够加快许多操做的速度,由于它能显著减小生成的重作日志量。假设你有一个表,须要从一个表空间移到另外一个表空间。能够适当地调度这个操做,让它在备份以后紧接着发生,这样就能把表ALTER为NOLOGGING模式,移到表,建立索引(也不生成日志),而后再把表ALTER回LOGGING模式。如今,原先须要X小时才能完成的操做可能只须要X/2小时。要想适当地使用这个特性,须要DBA的参与,或者必须与负责数据库备份和恢复(或任何备用数据库)的人沟通。

4.4 为何不能分配一个新日志?

Thread 1 cannot allocate new log, sequence 1466
Checkpoint not complete
Current log# 3 seq# 1465 mem# 0: /home/ora10g/oradata/ora10g/redo03.log

警告消息中也可能指出Archival required而不是Checkpoint not complete,可是效果几乎都同样。若是数据库试图重用一个在线重作日志文件,可是发现作不到,就会把这样一条消息写到服务器上的alert.log中。若是DBWR尚未完成重作日志所保护数据的检查点(checkpointing),或者ARCH尚未把重作日志文件复制到归档目标,就会发生这种状况。对最终用户来讲,这个时间点上数据库实际上中止了。它会原地不动。DBWR或ARCH将获得最大的优先级以将redo块刷新输出的磁盘。完成了检查点或归档以后,一切又回归正常。数据库之因此暂停用户的活动,这是由于此时已经没地方记录用户所作的修改了。Oracle试图重用一个在线重作日志文件,可是因为归档进程还没有完成这个文件的复制(Archival required),因此Oracle必须等待(相应地,最终用户也必须等待),直到能安全地重用这个重作日志文件为止。

若是你看到会话由于一个“日志文件切换”、“日志缓冲区空间”或“日志文件切换检查点或归档未完成”等待了很长时间,就极可能遇到了这个问题。若是日志文件大小不合适,或者DBWR和ARCH太慢(须要由DBA或系统管理员调优),在漫长的数据库修改期间,你就会注意到这个问题。“起始”数据库通常会把重作日志的大小定得过小,不适用较大的工做量(包括数据字典自己的起始数据库构建)。一旦启动数据库的加载,你会注意到,前1,000行进行得很快,而后就会呈喷射状进行:1,000进行得很快,而后暂停,接下来又进行得很快,而后又暂停,如此等等。这些就是很明确的提示,说明你遭遇了这个问题。

要解决这个问题,有几种作法:
1. 让DBWR更快一些。让你的DBA对DBWR调优,为此能够启用ASYNC I/O、使用DBWR I/O从属进程,或者使用多个DBWR进程。看看系统产生的I/O,查看是否有一个磁盘(或一组磁盘)“太热”,相应地须要将数据散布开。这个建议对ARCH也适用。这种作法的好处是,你不用付出什么代价就能有所收获,性能会提升,并且没必要修改任何逻辑/结构/代码。
2. 增长更多重作日志文件。在某些状况下,这会延迟Checkpoint not complete的出现,并且过一段时间后,能够把Checkpoint not complete延迟得足够长,使得这个错误可能根本不会出现(由于你给DBWR留出了足够的活动空间来创建检查点)。这个方法也一样适用于Archival required消息。这种方法的好处是能够消除系统中的“暂停”。其缺点是会消耗更多的磁盘空间。
3. 从新建立更大的日志文件。这会扩大填写在线重作日志与重用这个在线重作日志文件之间的时间间隔。若是重作日志文件的使用呈“喷射状”,这种方法一样适用于Archival required消息。假若一段时间内会大量生成日志(如每晚加载、批处理等),其后一段数据却至关平静,若是有更大的在线重作日志,就能让ARCH在平静的期间有足够的时间“遇上来”。这种方法的优缺点与前面增长更多文件的方法是同样的。另外,它可能会延迟检查点的发生,因为(至少)每一个日志切换都会发生检查点,而如今日志切换间隔会更大。
4. 让检查点发生得更频繁、更连续。能够使用一个更小的块缓冲区缓存(不太好),或者使用诸如FAST_START_MTTR_TARGET、LOG_CHECKPOINT_INTERVAL和LOG_CHECKPOINT_TIMEOUT之类的参数设置。这会强制DBWR更 频繁地刷新输出脏块。这种方法的好处是,失败恢复的时间会减小。在线重作日志中应用的工做确定更少。其缺点是,若是常常修改块,可能会更频繁地写至磁盘。 缓冲区缓存本该更有效的,但因为频繁地写磁盘,会致使缓冲区缓存不能充分发挥做用,这可能会影响块清除机制。

4.5 块清除

块清除(block cleanout),即生成所修改数据库块上与“锁定”有关的信息。 
数据锁其实是数据的属性,存储在块首部。下一次访问这个块时,可能必须“清理”这个块,要将这些事务信息删除。这个动做会生成redo,并致使变脏(本来并不脏,由于数据自己没有修改),这说明一个简单的SELECT有可能生成redo,并且可能致使完成下一个检查点时将大量的块写至磁盘。不过,在大多数正常的状况下,这是不会发生的。若是系统中主要是小型或中型事务(OLTP),或者数据仓库会执行直接路径加载或使用DBMS_STATS在加载操做后分析表,你会发现块一般已经获得“清理”。COMMIT时处理的步骤之一是:若是块还在SGA中,就要再次访问这些块,若是能够访问(没有别人在修改这些块),则对这些块完成清理。这个活动称为提交清除(commit cleanout),即清除已修改块上事务信息。最理想的是,COMMIT能够完成块清除,这样后面的SELECT(读)就没必要再清理了。只有块的UPDATE才会真正清除残余的事务信息,因为UPDATE已经在生成redo,所用注意不到这个清除工做。

能够强制清除不发生来观察它的反作用,并了解提交清除是怎么工做的。在与咱们的事务相关的提交列表中,Oracle会记录已修改的块列表。这些列表都有20个块,Oracle会根据须要分配多个这样的列表,直至达到某个临界点。若是咱们修改的块加起来超过了块缓冲区缓存大小的10%,Oracle会中止为咱们分配新的列表。例如,若是缓冲区缓存设置为能够缓存3,000个块,Oracle会为咱们维护最多300个块(3,000的10%)。COMMIT时,Oracle会处理这些包含20个块指针的列表,若是块仍可用,它会执行一个很快的清理。因此,只要咱们修改的块数没有超过缓存中总块数的10%,并且块仍在缓存中而且是可用的,Oracle就会在COMMIT时清理这些块。不然,它只会将其忽略(也就是说不清理)

在下表中填入了5000行,并COMMIT。测量到此为止生成的redo量。而后运行一个SELECT,它会访问每一个块,最后测量这个SELECT生成的redo量。

SELECT会生成redo。不只如此,它还会把这些修改块“弄脏”,致使DBWR再次将块写入磁盘。这是由于块清除的缘故。接下来,我会再一次运行SELECT,能够看到这回没有生成redo。这在乎料之中,由于此时块都已经“干净”了。

sys@ORCL>create table t
  2  ( x char(2000),
  3  y char(2000),
  4  z char(2000)
  5  )
  6  /

表已建立。


sys@ORCL>insert into t
  2  select 'x', 'y', 'z'
  3  from all_objects
  4  where rownum <= 50000;

已建立50000行。


统计信息
----------------------------------------------------------
       4152  recursive calls
     233091  db block gets
      64054  consistent gets
        564  physical reads
  326484496  redo size
       1137  bytes sent via SQL*Net to client
       1317  bytes received via SQL*Net from client
          4  SQL*Net roundtrips to/from client
         31  sorts (memory)
          0  sorts (disk)
      50000  rows processed

sys@ORCL>commit;

提交完成。

上述表 每一个块中包含一行(个人数据库中块大小为8KB)。

scott@ORCL> show parameter db_block_size

NAME                                 TYPE        VALUE
------------------------------------ ----------- ------------------------------
db_block_size                        integer     8192

如今测量读数据是生成的redo量:

sys@ORCL>select *
  2  from t;

已选择50000行。

统计信息
----------------------------------------------------------
          5  recursive calls
          0  db block gets
     100076  consistent gets
      50116  physical reads
       4004  redo size
  302570630  bytes sent via SQL*Net to client
      37182  bytes received via SQL*Net from client
       3335  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
      50000  rows processed

可见,这个SELECT在处理期间生成了大约 4KB 的redo。这表示对T进行全表扫描时修改了 4KB的块首部。DBWR会在未来某个时间把这些已修改的块写回到磁盘上。如今,若是再次运行这个查询:

sys@ORCL>select *
  2  from t;

已选择50000行。

统计信息
----------------------------------------------------------
          0  recursive calls
          0  db block gets
      99951  consistent gets
      50001  physical reads
          0  redo size
  302570630  bytes sent via SQL*Net to client
      37182  bytes received via SQL*Net from client
       3335  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
      50000  rows processed

能够看到,这一次没有生成redo,块都是干净的。

若是把缓冲区缓存设置为能保存至少50,000个块,再次运行前面的例子。会发现,不管哪个SELECT,生成的redo都不多甚至没有——咱们没必要在其中任何一个SELECT语句期间清理脏块。这是由于,咱们修改的5000个块彻底能够在缓冲区缓存的10%中放下,并且咱们是独家用户。别人不会动数据,不会有人致使咱们的数据刷新输出到磁盘,也没有人在访问这些块。在实际系统中,有些状况下,至少某些块不会进行清除,这是正常的。

若是执行一个大的INSERT(如上所述)、UPDATE或DELETE,这种块清除行为的影响最大,它会影响数据库中的许多块(缓存中10%以上的块都会完成块清除)。在此以后,第一个“接触”块的查询会生成少许的redo,并把块弄脏,若是DBWR已经将块刷新输出或者实例已经关闭,可能就会由于这个查询而致使重写这些块,并彻底清理缓冲区缓存。若是Oracle不对块完成这种延迟清除,那么COMMIT的处理就会与事务自己同样长。COMMIT必须从新访问每个块,可能还要从磁盘将块再次读入(它们可能已经刷新输出)。
假设你更新(UPDATE)了大量数据,而后COMMIT。如今对这些数据运行一个查询来验证结果。看上去查询生成了大量写I/O和redo。
在一个OLTP系统中,可能历来不会看到这种状况发生,由于OLTP系统的特色是事务都很短小,只会影响为数很少的一些块。根据设计,全部或者大多数事务都短而精。只是修改几个块,并且这些块都会获得清理。在一个数据仓库中,若是加载以后要对数据执行大量UPDATE,就要把块清除做为设计中要考虑的一个因素。有些操做会在“干净”的块上建立数据。例如,CREATE TABLE AS SELECT、直接路径加载的数据以及直接路径插入的数据都会建立“干净”的块。UPDATE、正常的INSERT或DELETE建立的块则可能须要在第一次读时完成块清除。若是你有以下的处理,就会受到块清除的影响:

1. 将大量新数据批量加载到数据仓库中;
2. 在刚刚加载的全部数据上运行UPDATE(产生须要清理的块);
3. 让人们查询这些数据。
若是块须要清理,第一接触这个数据的查询将带来一些额外的处理

应该在UPDATE之 后本身主动地“接触”数据。你刚刚加载或修改了大量的数据;如今至少须要分析这些数据。可能要自行运行一些报告来验证数据已经加载。这些报告会完成块清 除,这样下一个查询就没必要再作这个工做了。更好的作法是:因为你刚刚批量加载了数据,如今须要以某种方式刷新统计。经过运行DBMS_STATS实用程序来收集统计,就能很好地清理全部块,这是由于它只是使用SQL来查询信息,会在查询当中很天然地完成块清除。

4.6 日志竞争

若是你遭遇到日志竞争,可能会看到对“日志文件同步”事件的等待时间至关长,另外Statspack报告的“日志文件并行写”事件中写次数(写I/O数)可能很大。若是观察到这种状况,就说明你遇到了重作日志的竞争;重作日志写得不够快。发生这种状况可能有许多缘由。其中一个应用缘由是:提交得太过频繁,例如在重复执行INSERT的循环中反复提交。若是提交得太频繁,这不只是很差的编程实践,确定还会引入大量日志文件同步等待。假设你的全部事务都有适当的大小(彻底听从业务规则的要求,而没有过于频繁地提交),但仍是看到了这种日志文件等待,这就有其余缘由了。其中最多见的缘由以下:

1. redo放在一个慢速设备上:磁盘表现不佳。
2. redo与其余频繁访问的文件放在同一个设备上。redo设计为要采用顺序写,并且要放在专用的设备上。若是系统的其余组件(甚至其余Oracle组件)试图与LGWR同时读写这个设备,你就会遭遇某种程度的竞争。在此,只要有可能,你就会但愿确保LGWR拥有这些设备的独占访问权限。
3. 已缓冲方式装载日志设备。你在使用一个“cooked”文件系统(而不是RAW磁盘)。操做系统在缓冲数据,而数据库也在缓冲数据(重作日志缓冲区)。这种双缓冲会让速度慢下来。若是可能,应该以一种“直接”方式了装载设备。具体操做依据操做系统和设备的不一样而有所变化,但通常均可以直接装载。
4. redo采用了一种慢速技术,如RAID-5。RAID-5很合适读,可是用于写时表现则不好。COMMIT期间 咱们必须等待LGWR以确保数据写到磁盘上。

只有有可能,实际上你会但愿至少有5个专用设备来记录日志,最好还有第6个设备来镜像归档日志。因为当前每每使用9GB、20GB、36GB、200GB、300GB和更大的磁盘,要想拥有这么多专用设备变得更加困难。可是若是能留出4块你能找到的最小、最快的磁盘,再有一个或两个大磁盘,就能够很好地促进LGWR和ARCH的工做。

在线重作日志文件是一组Oracle文件,最适合使用RAW磁盘(原始磁盘)。在线重作日志文件不用备份,因此将在线重作日志文件放在RAW分区上而不是cooked文件系统上,这不会影响你的任何备份脚本。ARCH总能把RAW日志转变为cooked文件系统文件(不能使用一个RAW设备来创建归档)。

4.7 临时表和redo/undo

临时表不会为它们的块生成redo。所以,对临时表的操做不是“可恢复的”。修改临时表中的一个块时,不会将这个修改记录到重作日志文件中。不过,临时表确实会生成undo,并且这个undo会计入日志。所以,临时表也会生成一些redo。这是由于你能回滚到事务中的一个SAVEPOINT。能够擦除对临时表的后50个INSERT,而只留下前50个。临时表能够有约束,正常表有的一切临时表均可以有。可能有一条INSERT语句要向临时表中插入500行,但插入到第500行时失败了,这就要求回滚这条语句。因为临时表通常表现得就像“正常”表同样,因此临时表必须生成undo。因为undo数据必须创建日志,所以临时表会为所生成的undo生成一些重作日志。
在临时表上运行的SQL语句主要是INSERT和SELECT。幸运的是,INSERT只生成极少的undo(须要把块恢复为插入前的“没有”状态,而存储“没有”不须要多少空间),另外SELECT根本不生成undo

我创建了一个小测试来演示使用临时表时生成的redo量,同时这也暗示了临时表生成的undo量,由于对于临时表,只会为undo生成日志。为了说明这一点,我采用了配置相同的“永久”表和“临时”表,而后对各个表执行相同的操做,测量每次生成的redo量。这里使用的表以下:

scott@ORCL>create table perm
  2  ( x char(2000) ,
  3  y char(2000) ,
  4  z char(2000) )
  5  /

表已建立。

scott@ORCL>create global temporary table temp
  2  ( x char(2000) ,
  3  y char(2000) ,
  4  z char(2000) )
  5  on commit preserve rows
  6  /

表已建立。

创建了一个小的存储过程,它能执行任意的SQL,并报告SQL生成的redo量。

使用这个例程分别在临时表和永久表上执行INSERT、UPDATE和DELETE:

scott@ORCL>create or replace procedure do_sql( p_sql in varchar2 )
  2  as
  3     l_start_redo number;
  4     l_redo number;
  5  begin
  6     select v$mystat.value
  7             into l_start_redo
  8     from v$mystat, v$statname
  9     where v$mystat.statistic# = v$statname.statistic#
 10             and v$statname.name = 'redo size';
 11
 12     execute immediate p_sql;
 13     commit;
 14
 15     select v$mystat.value-l_start_redo
 16             into l_redo
 17     from v$mystat, v$statname
 18     where v$mystat.statistic# = v$statname.statistic#
 19             and v$statname.name = 'redo size';
 20
 21     dbms_output.put_line
 22             ( to_char(l_redo,'9,999,999') ||' bytes of redo generated for "' ||
 23             substr( replace( p_sql, chr(10), ' '), 1, 25 ) || '"...' );
 24  end;
 25  /

过程已建立。

接下来,对PERM表和TEMP表运行一样的INSERT、UPDATE和DELETE:

scott@ORCL>set serveroutput on format wrapped
scott@ORCL>begin
  2  do_sql( 'insert into perm
  3             select 1,1,1
  4             from all_objects
  5             where rownum <= 500' );
  6
  7  do_sql( 'insert into temp
  8             select 1,1,1
  9             from all_objects
 10             where rownum <= 500' );
 11  dbms_output.new_line;
 12
 13  do_sql( 'update perm set x = 2' );
 14  do_sql( 'update temp set x = 2' );
 15  dbms_output.new_line;
 16
 17  do_sql( 'delete from perm' );
 18  do_sql( 'delete from temp' );
 19  end;
 20  /
 3,293,240 bytes of redo generated for "insert into perm select"...
 66,432 bytes of redo generated for "insert into temp select"...

 2,182,288 bytes of redo generated for "update perm set x = 2"...
 1,100,296 bytes of redo generated for "update temp set x = 2"...

 3,215,200 bytes of redo generated for "delete from perm"...
 3,215,328 bytes of redo generated for "delete from temp"...

PL/SQL 过程已成功完成。

能够看到:
1.  对“实际”表(永久表)的INSERT生成了大量redo。而对临时表几乎没有生成任何redo。对临时表的INSERT只会生成不多的undo数据,并且对于临时表只会为undo数据创建日志。
2. 实际表的UPDATE生成的redo大约是临时表更新所生成redo的两倍。必须保存UPDATE的大约一半(即“前映像”)。对于临时表来讲,没必要保存“后映像”(redo)。
3. DELETE须要几乎相同的redo空间。由于对DELETE的undo很大,而对已修改块的redo很小。所以,对临时表的DELETE与对永久表的DELETE几乎相同。

关于临时表上的DML活动,能够得出如下通常结论:
1. INSERT会生成不多甚至不生成undo/redo活动
2. DELETE在临时表上生成的redo与正常表上生成的redo一样多
3. 临时表的UPDATE会生成正常表UPDATE一半的redo

对于最后一个结论,须要指出有一些例外状况。例如,若是我用2,000字节的数据UPDATE(更新)彻底为NULL的一列,生成的undo数据就很是少。这个UPDATE表现得就像是INSERT。另外一方面,若是我把有2,000字节数据的一列UPDATE为全NULL,对redo生成来讲,这就表现得像是DELETE。平均来说,能够这样认为:临时表UPDATE与实际表UPDATE生成的undo/redo相比,前者是后者的50%。
通常来说,关于建立的redo量有一个常识。若是你完成的操做致使建立undo数据,则能够肯定逆向完成这个操做(撤销操做)的难易程度。若是INSERT2,000字节,逆向操做就很容易,只需回退到无字节便可。若是删除了(DELETE)2,000字节,逆向操做就是要插入2,000字节。在这种状况下,redo量就很大。
避免删除临时表能够使用TRUNCATE,或者只是让临时表在COMMIT以后或会话终止时自动置空。执行方法不会生成undo,相应地也不会生成redo。你可能会尽可能避免更新临时表,除非因为某种缘由必须这样作。把临时表主要用于插入(INSERT)和选择(SELECT)。采用这种方式,就能更优地使用临时表不生成redo的特有能力。

5 分析undo

5.1 什么操做会生成最多和最少的undo?

若是存在索引(或者实际上表就是索引组织表),这将显著地影响生成的undo量,由于索引是一种复杂的数据结构,可能会生成至关多的undo信息。

通常来说,INSERT生成的undo最少,由于Oracle为此需记录的只是要“删除”的一个rowid(行ID)。UPDATE通常排名第二(在大多数状况下)。对于UPDATE,只需记录修改的字节。通常只更新(UPDATE)了整个数据行中不多的一部分。所以,必须在undo中记录行的一小部分。通常来说,DELETE生成的undo最多。对于DELETE,Oracle必须把整行的前映像记录到undo段中。INSERT只生成须要创建日志的不多的undo。UPDATE生成的undo量等于所修改数据的前映像大小,DELETE会生成整个数据集写至undo段。

与加索引列的更新相比,对一个未加索引的列进行更新不只执行得更快,生成的undo也会好得多。例如,下面建立一个有两列的表,这两列包含相同的数据,可是其中一列加了索引:

scott@ORCL>create table t
  2  as
  3  select object_name unindexed,
  4  object_name indexed
  5  from all_objects
  6  /

表已建立。

scott@ORCL>create index t_idx on t(indexed);

索引已建立。

scott@ORCL>exec dbms_stats.gather_table_stats(user,'T');

PL/SQL 过程已成功完成。

下面更新这个表,首先,更新未加索引的列,而后更新加索引的列。咱们须要一个新的V$查询来测量各类状况下生成的undo量。如下查询能够完成这个工做。它先从V$MYSTAT获得咱们的会话ID(SID),在使用这个会话ID在V$SESSION视图中找到相应的会话记录,并获取事务地址(TADDR)。而后使用TADDR拉出(查出)咱们的V$TRANSACTION记录(若是有),选择USED_UBLK列,即已用undo块的个数。因为咱们目前不在一个事务中,这个查询如今应该返回0行:

scott@ORCL>select used_ublk
  2  from v$transaction
  3  where addr = (select taddr
  4                from v$session
  5                where sid = (select sid
  6                             from v$mystat
  7                             where rownum = 1 )
  8                )
  9  /

未选定行

而后在每一个UPDATE以后再使用这个查询:

scott@ORCL>update t set unindexed = lower(unindexed);

已更新71869行。

scott@ORCL>select used_ublk
  2  from v$transaction
  3  where addr = (select taddr
  4                from v$session
  5                where sid = (select sid
  6                             from v$mystat
  7                             where rownum = 1 )
  8                )
  9  /

 USED_UBLK
----------
      1229

scott@ORCL>commit;

提交完成。

scott@ORCL>select used_ublk
  2  from v$transaction
  3  where addr = (select taddr
  4                from v$session
  5                where sid = (select sid
  6                             from v$mystat
  7                             where rownum = 1 )
  8                )
  9  /

未选定行

这个UPDATE使用了1229个块存储其undo。提交会“解放”这些块,或者将其释放,因此若是再次对V$TRANSACTION运行这个查询,它还会显示no rows selected。

更新一样的数据时,不过这一次是加索引的列,会观察到下面的结果:

scott@ORCL>update t set indexed = lower(indexed);

已更新71869行。

scott@ORCL>select used_ublk
  2  from v$transaction
  3  where addr = (select taddr
  4                from v$session
  5                where sid = (select sid
  6                            from v$mystat
  7                            where rownum = 1 )
  8                )
  9  /

 USED_UBLK
----------
      2763

scott@ORCL>commit;

提交完成。

scott@ORCL>select used_ublk
  2  from v$transaction
  3  where addr = (select taddr
  4                from v$session
  5                where sid = (select sid
  6                            from v$mystat
  7                            where rownum = 1 )
  8                )
  9  /

未选定行

更新加索引的列会生成 2倍多的undo。这是由于索引结构自己所固有的复杂性并且咱们更新了这个表中的每一行,移动了这个结构中的每个索引键值

5.2 ORA-01555:snapshot too old错误

致使这个错的一个缘由:提交得太过频繁

1. undo段过小,不足以在系统上执行工做。
2. 程序跨COMMIT获取(实际上这是前一点的一个变体)。
3. 块清除
前两点与Oracle的读一致性模型直接相关。查询的结果是预约的,在Oracle去获取第一行以前,结果就已经定好了。Oracle使用undo段来回滚自查询开始以来有修改的块,从而提供数据库的一致时间点“快照”。例如执行如下语句:

update t set x = 5 where x = 2;
insert into t select * from t where x = 2;
delete from t where x = 2;
select * from t where x = 2;

执行每条语句时都会看到T的一个读一致视图以及X=2的行集,而不论数据库中还有哪些并发的活动。

全部“读”这个表的语句都利用了这种读一致性。在上面所示的例子中,UPDATE读这个表,找到X=2的行(而后UPDATE这些行)。INSERT也要读表,找到X=2的行,而后INSERT,等等。因为两个语句都使用了undo段,都是为了回滚失败的事务并提供读一致性,这就致使了ORA-01555错误。
前面列的第三项也会致使ORA-01555,由于它可能在只有一个会话的数据库中发生,并且这个会话并无修改出现ORA-01555错误时所查询的表!

ORA-01555错误的几种解决方案,通常来讲能够采用下面的方法:
1. 适当地设置参数UNDO_RETENTION(要大于执行运行时间最长的事务所需的时间)。能够用V$UNDOSTAT来肯定长时间运行的查询的持续时间。另外,要确保磁盘上已经预留了足够的空间,使undo段能根据所请求的UNDO_RETENTION增大。
2. 使用手动undo管理时加大或增长更多的回滚段。这样在长时间运行的查询执行期间,覆盖undo数据的可能性就能下降。
3. 减小查询的运行时间(调优)。这样就能下降对undo段的需求,不需求太大的undo段。
4. 收集相关对象的统计信息。因为大批量的UPDATE或INSERT会致使块清除(block cleanout),因此须要在大批量UPDATE或大量加载以后以某种方式收集统计信息。

5.2.1 undo段确实过小

一种场景是:你的系统中事务很小。正因如此,只须要分配很是少的undo段空间。假如,假设存在如下状况:
􀂉 每一个事务平均生成8KB的undo。
􀂉 平均每秒完成其中5个事务(每秒生成40KB的undo,每分钟生成2,400KB的undo)。
􀂉 有一个生成1MB undo的事务平均每分钟出现一次。总的说来,每分钟会生成大约3.5MB的undo。
􀂉 你为系统配置了15MB的undo。
处理事务时,相对于这个数据库的undo需求,这彻底够了。undo段会回绕,平均每3~4分钟左右会重用一次undo段空间。

不过,在一样的环境中,可能有一些报告需求。其中一些查询须要运行至关长的时间,多是5分钟。这就有问题了。若是这些查询须要执行5分钟,并且它们须要查询开始时的一个数据视图,你就极有可能遭遇ORA-01555错误。因为你的undo段会在这个查询执行期间回绕,要知道查询开始以来生成的一些undo信息已经没有了,这些信息已经被覆盖。若是你命中了一个块,而这个块几乎在查询开始的同时被修改,这个块的undo信息就会由于undo段回绕而丢掉,你将收到一个ORA-01555错误。
如下是一个小例子。假设咱们有一个表,其中有块一、二、三、…、1,000,000。下表显示了可能出现的事件序列。

                                            长时间运行的查询时间表
时间(分:秒)  动做
0:00      查询开始
0:01     另外一个会话更新(UPDATE)块1,000,000。将块1,000,000的undo信息记录到某个undo段
0:01 这个UPDATE会话提交(COMMIT)。它生成的undo数据还在undo段中,可是假若咱们须要空间,        选择容许覆盖这个信息
1:00 咱们的查询还在运行。如今更新到块200,000
1:01 进行了大量活动。如今已经生成了稍大于14MB的undo
3:00 查询还在兢兢业业地工做着。如今处理到块600,000左右
4:00 undo段开始回绕,并重用查询开始时(0:00)活动的空间。具体地讲,咱们已经重用了原先0:01时刻         UPDATE 块1,000,000时所用的undo段空间
5:00 查询终于到了块1,000,000。它发现自查询开始以来这个块已经修改过。它找到undo段,试图发现对应这一块的undo来获得一个一致读。此时,它发现所须要的信息已经不存在了。这就产生了ORA-01555错误,查询失败

具体就是这样的。若是如此设置undo段大小,使得颇有可能在执行查询期间重用这些undo段,并且查询要访问被修改的数据,那就也有可能不断地遭遇ORA-01555错误。此时必须把UNDO_RETENTION参数设置得高一些,让Oracle负责肯定要保留多少undo段的大小,让它们更大一些(或者有更多的undo段)。你要配置足够的undo,在长时间运行的查询期间应当可以维持。在前面的例子中,只是针对修改数据的事务来肯定系统undo段的大小,而忘记了还有考虑系统的其余组件。

假若手动地管理undo段,undo段历来不会由于查询而扩大;只有INSERT、UPDATE和DELETE才会让undo段增加。只有当执行一个长时间运行的UPDATE事务时才会扩大手动回滚段。

为了演示这种效果,咱们将建立一个很是小的undo表空间,并有一个生成许多小事务的会话,实际上这能确保这个undo表空间回绕,屡次重用所分配的空间,而不论UNDO_RETENTION设置为多大,由于咱们不容许undo表空间增加。使用这个undo段的会话将修改一个表T。它使用T的一个全表扫描,自顶向下地读表;在另外一个会话中,执行一个查询,它经过一个索引读表T,这个查询会稍微有些随机地读表:先读第1行,而后是第1,000行,接下来是第500行,再后面是第20,001行,如此等等。这样一来,咱们可能会很是随机地访问块,并在查询的处理期间屡次访问块。这种状况下获得ORA-01555错误的机率几乎是100%。因此,在一个会话中首先执行如下命令:

scott@ORCL>create undo tablespace undo_small
  2  datafile 'D:\app\Administrator\oradata\orcl\undosmall.dbf' size 2m
  3  autoextend off
  4  /

表空间已建立。

scott@ORCL>alter system set undo_tablespace = undo_small;

系统已更改。

如今,创建表T来查询和修改。注意 在这个表中随机地对数据排序。CREATE TABLE AS SELECT力图按查询获取的顺序将行放在块中。咱们的目的只是把行弄乱,使它们不至于认为地有某种顺序,从而获得随机的分布:

scott@ORCL>create table t
  2  as
  3  select *
  4  from all_objects
  5  order by dbms_random.random;

表已建立。

scott@ORCL>alter table t add constraint t_pk primary key(object_id)
  2  /

表已更改。

scott@ORCL>exec dbms_stats.gather_table_stats( user, 'T', cascade=> true );

PL/SQL 过程已成功完成。

如今能够执行修改了:

scott@ORCL>begin
  2     for x in ( select rowid rid from t )
  3     loop
  4             update t set object_name = lower(object_name) where rowid = x.rid;
  5             commit;
  6     end loop;
  7  end;
  8  /

在运行这个修改的同时,在另外一个会话中运行一个查询。这个查询要读表T,并处理每一个记录。获取下一个记录以前处理每一个记录所花的时间大约为1/100秒(使用DBMS_LOCK.SLEEP(0.01)来模拟)。在查询中使用了FIRST_ROWS提示,使之使用前面建立的索引,从而经过索引(按OBJECT_ID排序)来读出表中的行。因为数据是随机地插入到表中的,咱们可能会至关随机地查询表中的块。这个查询只运行几秒就会失败:

scott@ORCL>declare
  2     cursor c is
  3                                     select /*+ first_rows */ object_name
  4                                     from t
  5                                     order by object_id;
  6
  7     l_object_name t.object_name%type;
  8     l_rowcnt number := 0;
  9  begin
 10     open c;
 11     loop
 12             fetch c into l_object_name;
 13             exit when c%notfound;
 14             dbms_lock.sleep( 0.01 );
 15             l_rowcnt := l_rowcnt+1;
 16     end loop;
 17     close c;
 18  exception
 19     when others then
 20     dbms_output.put_line( 'rows fetched = ' || l_rowcnt );
 21     raise;
 22  end;
 23  /
rows fetched = 56
declare
*
第 1 行出现错误:
ORA-01555: 快照过旧: 回退段号 32 (名称为 "_SYSSMU32_4133326864$") 太小
ORA-06512: 在 line 21

能够看到,在遭遇ORA-01555:snapshot too old错误而失败以前,它只处理了56个记录。要修正这个错误,咱们要保证作到两点:
1. 数据库中UNDO_RETENTION要设置得足够长,以保证这个读进程完成。这样数据库就能扩大undo表空间来保留足够的undo,使咱们可以完成工做。
2. undo表空间能够增加,或者为之手动分配更多的磁盘空间。

对于这个例子,我认为这个长时间运行的进程须要大约600秒才能完成。个人UNDO_RETENTION设置为900(单位是秒,因此undo保持大约15分钟)。我修改了undo表空间的数据文件,使之一次扩大1MB,直到最大达到2GB:

scott@ORCL>column file_name new_val F
scott@ORCL>select file_name
  2  from dba_data_files
  3  where tablespace_name = 'UNDO_SMALL';

FILE_NAME
--------------------------------------------------------------------------------
D:\APP\ADMINISTRATOR\ORADATA\ORCL\UNDOSMALL.DBF

scott@ORCL>alter database
  2  datafile '&F'
  3  autoextend on
  4  next 1m
  5  maxsize 2048m;
原值    2: datafile '&F'
新值    2: datafile 'D:\APP\ADMINISTRATOR\ORADATA\ORCL\UNDOSMALL.DBF'

数据库已更改。

再次并发地运行这些进程时,两个进程都能顺利完成。这一次undo表空间的数据文件扩大了,由于在此容许undo表空间扩大,并且根据我设置的undo保持时间可知:

scott@ORCL>select bytes/1024/1024
  2  from dba_data_files
  3  where tablespace_name = 'UNDO_SMALL';

BYTES/1024/1024
---------------
             19

所以,这里没有收到错误,咱们成功地完成了工做,并且undo扩大得足够大,能够知足咱们的须要。在这个例子中,之因此会获得错误只是由于咱们经过索引来读表T,并且在全表上执行随机读。若是不是这样,而是执行全表扫描,在这个特例中极可能不会遇到ORA-01555错误。缘由是SELECT和UPDATE都要对T执行全表扫描,而SELECT扫描极可能在UPDATE以前进行(SELECT只须要读,而UPDATE不只要读还有更新,所以可能更慢一些)。若是执行随机读,SELECT就更有可能要读已修改的块(即块中的多行已经被UPDATE修改并且已经提交)。这就展现了ORA-01555的“阴险”,这个错误的出现取决于并发会话如何访问和管理底层表。

5.2.2  延迟的块清除

块清除是致使ORA-01555错误错误的缘由,尽管很难彻底杜绝,不过好在毕竟并很少见,由于可能出现块清除的状况不常发生。在块清除过程当中,若是一个块已被修改,下一个会话访问这个块时,可能必须查看最 后一个修改这个块的事务是否仍是活动的。一旦肯定该事务再也不活动,就会完成块清除,这样另外一个会话访问这个块时就没必要再历经一样的过程。要完成块清除,Oracle会从块首部肯定前一个事务所用的undo段,而后肯定从undo首部能不能看出这个块是否已经提交。

能够用如下两种方式完成这种确认。

一种方式是Oracle能够肯定这个事务好久之前就已经提交,它在undo段事务表中的事务槽已经被覆盖。

另外一种状况是COMMIT SCN还在undo段的事务表中,这说明事务只是稍早前刚提交,其事务槽还没有被覆盖。
要从一个延迟的块清除收到ORA-01555错误,如下条件都必须知足:
1. 首先作了一个修改并COMMIT,块没有自动清理(即没有自动完成“提交清除”,例如修改了太多的块,在SGA块缓冲区缓存的10%中放不下)。
2. 其余会话没有接触这些块,并且在咱们这个“倒霉”的查询(稍后显示)命中这些块以前,任何会话都不会接触它们。
3. 开始一个长时间运行的查询。这个查询最后会读其中的一些块。这个查询从SCN t1开始,这就是读一致SCN,必须将数据回滚到这一点来获得读一致性。开始查询时,上述修改事务的事务条目还在undo段的事务表中。
4. 查询期间,系统中执行了多个提交。执行事务没有接触执行已修改的块。
5. 因为出现了大量的COMMIT,undo段中的事务表要回绕并重用事务槽。最重要的是,将循环地重用原来修改事务的事务条目。另外,系统重用了undo段的区段,以免对undo段首部块自己的一致读。
6. 此外,因为提交太多,undo段中记录的最低SCN如今超过了t1(高于查询的读一致SCN)。
若是查询到达某个块,而这个块在查询开始以前已经修改并提交,就会遇到麻烦。正常状况下,会回到块所指的undo段,找到修改了这个块的事务的状态(换句话说,它会找到事务的COMMIT SCN)。若是这个COMMIT SCN小于t1,查询就能够使用这个块。若是该事务的COMMIT SCN大于t1,查询就必须回滚这个块。不过,问题是,在这种特殊的状况下,查询没法肯定块的COMMIT SCN是大于仍是小于t1。相应地,不清楚查询可否使用这个块映像。这就致使了ORA-01555错误。

为 了真正看到这种状况,咱们将在一个表中建立多个须要清理的块。而后在这个表上打开一个游标,并容许对另外某个表完成许多小事务(不是那个刚更新并打开了游标的表)。最后尝试为该游标获取数据。如今,咱们认为游标 应该能看到全部数据,由于咱们是在打开游标以前完成并提交了表修改。假若此时 获得ORA-01555错误,就说明存在前面所述的问题。要创建这个例子,咱们将使用:
1. 2MB UNDO_SMALL undo 表空间。
2. 4MB的缓冲区缓存,足以放下大约500个块。这样咱们就能够将一些脏块刷新输出到磁盘来观察这种现象。

scott@ORCL>create table big
  2  as
  3  select a.*, rpad('*',1000,'*') data
  4  from all_objects a;

表已建立。

scott@ORCL>exec dbms_stats.gather_table_stats( user, 'BIG' );

PL/SQL 过程已成功完成。

因为使用了这么大的数据字段,每一个块中大约有6~7行,因此这个表中有大量的块。接下来,咱们建立将由多个小事务修改的小表:

scott@ORCL>create table small ( x int, y char(500) );

表已建立。

scott@ORCL>insert into small select rownum, 'x' from all_users;

已建立39行。

scott@ORCL>commit;

提交完成。

scott@ORCL>exec dbms_stats.gather_table_stats( user, 'SMALL' );

PL/SQL 过程已成功完成。

下面把那个大表“弄脏”。因为undo表空间很是小,因此但愿尽量多地更新这个大表的块,同时生成尽量少的undo。为此,将使用一个UPDATE语句来执行该任务。实质上讲,下面的子查询要找出每一个块上的“第一个”行rowid。这个子查询会返回每个数据库块的一个rowid(标识了这个块的一行)。咱们将更新这一行,设置一个VARCHAR2(1)字段。这样咱们就能更新表中的全部块(在这个例子中,块数大约比8,000稍多一点),缓冲区缓存中将会充斥着必须写出的脏块(如今只有500个块的空间)。仍然必须保证只能使用那个小undo表空间。为作到这一点,并且不超过undo表空间的容量,下面构造一个UPDATE语句,它只更新每一个块上的“第一行”。ROW_NUMBER()内置分析函数是这个操做中使用的一个工具;它把数字1指派给表中的数据库块的“第1行”,在这个块上只会更新这一行:

scott@ORCL>update big
  2  set temporary = temporary
  3  where rowid in
  4     (
  5     select r
  6     from (
  7                             select rowid r, row_number() over
  8                             (partition by dbms_rowid.rowid_block_number(rowid) order by rowid)rn
  9                             from big
 10                     )
 11     where rn = 1
 12     )
 13  /

已更新11979行。

scott@ORCL>commit;

提交完成。

如今咱们知道磁盘上有大量脏块。咱们已经写出了一些,可是没有足够的空间把它们都放下。接下来打开一个游标,可是还没有获取任何数据行。打开游标时,结果集是预约的,因此即便Oracle并无具体处理一行数据,打开结果集这个动做自己就肯定了“一致时间点”,即结果集必须相对于那个时间点一致。如今要获取刚刚更新并提交的数据,并且咱们知道没有别人修改这个数据,如今应该能获取这些数据行而不须要如何undo。可是此时就会“冒出”延迟块清除。修改这些块的事务太新了,因此Oracle必须验证在咱们开始以前这个事务是否已经提交,若是这个信息(也存储在undo表空间中)被覆盖,查询就会失败。如下打开了游标:

scott@ORCL>variable x refcursor
scott@ORCL>exec open :x for select * from big;

PL/SQL 过程已成功完成。

启动9个SQL*Plus会话

每一个会话运行的脚本以下:

begin
	for i in 1 .. 1000
	loop
		update small set y = i where x= &1;
		commit;
	end loop;
end;
/

这样一来,就有了9个会话分别在一个循环中启动多个事务。咱们观察到下面的结果:

ERROR:
ORA-01555: snapshot too old: rollback segment number 23 with name "_SYSSMU23$"
too small
no rows selected

它须要许多条件,全部这些条件必须同时存在才会出现这种状况。首先要有须要清理的块,收集统计信息的DBMS_STATS调用就能消除这种块。尽管大批量的更新和大量加载是形成块清除最多见的理由,可是利用DBMS_STATS调用的话,这些操做就再也不成为问题,由于在这种操做以后总要对表执行分析。大多数事务只会接触不多的块,而不到块缓冲区缓存的10%;所以,它们不会生成须要清理的块。万一你发现遭遇了这个问题,即选择(SELECT)一个表时(没有应用其余DML操做)出现了ORA-01555错误,能你能够试试如下解决方案:
1. 首先,保证使用的事务“大小适当”。确保没有没必要要地过于频繁地提交。
2. 使用DBMS_STATS扫描相关的对象,加载以后完成这些对象的清理。因为块清除是极大量的UPDATE或INSERT形成的,因此颇有必要这样作。
3. 容许undo表空间扩大,为之留出扩展的空间,并增长undo保持时间。这样在长时间运行查询期间,undo段事务表中的事务槽被覆盖的可能性就会下降。针对致使ORA-01555错误的另外一个缘由(undo段过小),也一样能够采用这个解决方案(这两个缘由有紧密的关系;块清除问题就是由于处理查询期间遇到了undo段重用,而undo段大小正是重用undo段的一个根本缘由)。实际上,若是把undo表空间设置为一次自动扩展1MB,并且undo保持时间为900秒,再运行前面的例子,对表BIG的查询就能成功地完成了。
4. 减小查询的运行时间(调优)。

相关文章
相关标签/搜索