配置环境
这里我会介绍如何建立一个执行本书实例的环境,具体包括以下主题:
- 如何正确地建立SCOTT/TIGER演示模式;
- 需要建立和运行的环境;
- 如何配置SQL*Plus工具AUTOTRACE;
- 如何安装Statspack;
- 如何安装和运行runstats以及本书中用到的其他定制实用程序;
- 本书所用的编码约定。
所有代码(只要不是Oracle自动生成的脚本)都能从Apress网站(http://www.apress.com)的Source Code区下载。
建立SCOTT/TIGER模式
你的数据库里可能已经有SCOTT/TIGER模式(schema)了。经典安装通常就包括这个模式,不过并不要求数据库一定得有这个组件。可以把SCOTT示例模式安装到任何数据库账户下,使用SCOTT账户并没有特殊的意义。如果乐意,你还可以把EMP/DEPT表直接安装在你自己的数据库账户下。
这本书里许多例子都用到了SCOTT模式中的表。如果你想顺利地使用这些例子,也需要有这些表。如果你使用的是一个共享数据库,最好复制这些表,并把你自己的副本安装在SCOTT以外的某个账户下,以避免使用同一数据的其他用户可能带来的副作用。
要创建SCOTT演示表,步骤很简单:
(1)cd [ORACLE_HOME]/sqlplus/demo。
(2)以任意用户身份连接后运行demobld.sql。
注意 对于Oracle 10g及以后版本,必须安装配套光盘中的演示子目录。后面我会列出demobld.sql中必要的部分。
demobld.sql会创建5个表并填入数据。执行结束后,它会自动退出SQL*Plus,所以运行完这个脚本后SQL*Plus窗口将消失,对此不要感到奇怪,这是正常的。
这些标准演示表上没有定义任何引用完整性,但后面有些例子要求表必须有引用完整性,所以运行完demobld.sql后,建议你再执行以下脚本:
alter table emp add constraint emp_pk primary key(empno); alter table dept add constraint dept_pk primary key(deptno); alter table emp add constraint emp_fk_dept foreign key(deptno) references dept; alter table emp add constraint emp_fk_emp foreign key(mgr) references emp; |
这就完成了演示模式的安装。如果以后你整理系统时删除这个模式,只需执行[ORACLE_HOME]/sqlplus/demo/demodrop.sql。这个脚本会删除5个演示表,并退出SQL*Plus。
如果你无法访问demobld.sql,也不用着急,以下脚本就足以运行本书中的示例:
CREATE TABLE EMP (EMPNO NUMBER(4) NOT NULL, ENAME VARCHAR2(10), JOB VARCHAR2(9), MGR NUMBER(4), HIREDATE DATE, SAL NUMBER(7, 2), COMM NUMBER(7, 2), DEPTNO NUMBER(2) ); INSERT INTO EMP VALUES (7369, 'SMITH', 'CLERK', 7902, TO_DATE('17-DEC-1980', 'DD-MON-YYYY'), 800, NULL, 20); INSERT INTO EMP VALUES (7499, 'ALLEN', 'SALESMAN', 7698, TO_DATE('20-FEB-1981', 'DD-MON-YYYY'), 1600, 300, 30); INSERT INTO EMP VALUES (7521, 'WARD', 'SALESMAN', 7698, TO_DATE('22-FEB-1981', 'DD-MON-YYYY'), 1250, 500, 30); INSERT INTO EMP VALUES (7566, 'JONES', 'MANAGER', 7839, TO_DATE('2-APR-1981', 'DD-MON-YYYY'), 2975, NULL, 20); INSERT INTO EMP VALUES (7654, 'MARTIN', 'SALESMAN', 7698, TO_DATE('28-SEP-1981', 'DD-MON-YYYY'), 1250, 1400, 30); INSERT INTO EMP VALUES (7698, 'BLAKE', 'MANAGER', 7839, TO_DATE('1-MAY-1981', 'DD-MON-YYYY'), 2850, NULL, 30); INSERT INTO EMP VALUES (7782, 'CLARK', 'MANAGER', 7839, TO_DATE('9-JUN-1981', 'DD-MON-YYYY'), 2450, NULL, 10); INSERT INTO EMP VALUES (7788, 'SCOTT', 'ANALYST', 7566, TO_DATE('09-DEC-1982', 'DD-MON-YYYY'), 3000, NULL, 20); INSERT INTO EMP VALUES (7839, 'KING', 'PRESIDENT', NULL, TO_DATE('17-NOV-1981', 'DD-MON-YYYY'), 5000, NULL, 10); INSERT INTO EMP VALUES (7844, 'TURNER', 'SALESMAN', 7698, TO_DATE('8-SEP-1981', 'DD-MON-YYYY'), 1500, 0, 30); INSERT INTO EMP VALUES (7876, 'ADAMS', 'CLERK', 7788, TO_DATE('12-JAN-1983', 'DD-MON-YYYY'), 1100, NULL, 20); INSERT INTO EMP VALUES (7900, 'JAMES', 'CLERK', 7698, TO_DATE('3-DEC-1981', 'DD-MON-YYYY'), 950, NULL, 30); INSERT INTO EMP VALUES (7902, 'FORD', 'ANALYST', 7566, TO_DATE('3-DEC-1981', 'DD-MON-YYYY'), 3000, NULL, 20); INSERT INTO EMP VALUES (7934, 'MILLER', 'CLERK', 7782, TO_DATE('23-JAN-1982', 'DD-MON-YYYY'), 1300, NULL, 10); CREATE TABLE DEPT (DEPTNO NUMBER(2), DNAME VARCHAR2(14), LOC VARCHAR2(13) ); INSERT INTO DEPT VALUES (10, 'ACCOUNTING', 'NEW YORK'); INSERT INTO DEPT VALUES (20, 'RESEARCH', 'DALLAS'); INSERT INTO DEPT VALUES (30, 'SALES', 'CHICAGO'); INSERT INTO DEPT VALUES (40, 'OPERATIONS', 'BOSTON'); |
环境
本书中大多数例子都完全能在SQL*Plus环境中运行。除了SQL*Plus之外,不需要再安装和配置其他工具。不过,对于使用SQL*Plus我有一个建议。本书中几乎所有示例都以某种方式使用了DBMS_OUTPUT。要让DBMS_OUTPUT正常工作,必须执行以下SQL*Plus命令:
SQL> set serveroutput on |
如果你像我一样,每次都键入这个命令很快就会厌烦的。幸运的是,SQL*Plus允许建立一个login.sql文件,每次启动SQL*Plus时都会执行这个脚本。另外,还允许设置一个环境变量SQLPATH,这样不论这个login.sql脚本具体在哪个目录中,SQL*Plus都能找到它。
对于本书中的所有示例,我使用的login.sql如下:
define _editor=vi set serveroutput on size 1000000 set trimspool on set long 5000 set linesize 100 set pagesize 9999 column plan_plus_exp format a80 column global_name new_value gname set termout off define gname=idle column global_name new_value gname select lower(user) || '@' || substr( global_name, 1, decode( dot, 0, length(global_name), dot-1) ) global_name from (select global_name, instr(global_name,'.') dot from global_name ); set sqlprompt '&gname> ' set termout on |
下面对这个脚本做些说明:
- DEFINE_EDITOR=VI:设置SQL*Plus使用的默认编辑器。可以把默认编辑器设置为你最喜欢的文本编辑器(而不是字处理器),如记事本(Notepad)或emacs。
- SET SERVEROUTPUT ON SIZE 1000000:这会默认地打开DBMS_OUTPUT(这样就不必每次再键入这个命令了)。另外也将默认缓冲区大小设置得尽可能大。
- SET TRIMSPOOL ON:假脱机输出文本时,会去除文本行两端的空格,而且行宽不定。如果设置为OFF(默认设置),假脱机输出的文本行宽度则等于所设置的LINESIZE。
- SET LONG 5000:设置选择LONG和CLOB列时显示的默认字节数。
- SET LINESIZE 100:设置SQL*Plus显示的文本行宽为100个字符。
- SET PAGESIZE 9999:PAGESIZE可以控制SQL*Plus多久打印一次标题,这里将PAGESIZE设置为一个很大的数(所以每页只有一组标题)。
- q COLUMN PLAN_PLUS_EXP FORMAT A80:设置由AUTOTRACE得到的解释计划输出(explain plan output)的默认宽度。A80通常足以放下整个计划。
login.sql中下面这部分用于建立SQL*Plus提示符:
define gname=idle column global_name new_value gname select lower(user) || '@' || substr( global_name, 1, decode( dot, 0, length(global_name), dot-1) ) global_name from (select global_name, instr(global_name,'.') dot from global_name ); set sqlprompt '&gname> ' set termout on |
COLUMN GLOBAL_NAME NEW_VALUE GNAME指令告诉SQL*Plus取得GLOBAL_NAME列中的最后一个值,并将这个值赋给替换变量GNAME。接下来,我从数据库中选出GLOBAL_NAME,并与我的登录用户名连接。这样得到的SQL*Plus提示符为:
ops$tkyte@ora10g> |
这样一来,我就能知道我是谁,还有我在哪儿。
设置SQL*Plus的AUTOTRACE
AUTOTRACE是SQL*Plus中一个工具,可以显示所执行查询的解释计划(explain plan)以及所用的资源。这本书中大量使用了AUTOTRACE工具。
配置AUTOTRACE的方法不止一种,以下是我采用的方法:
(1)cd [ORACLE_HOME]/rdbms/admin;
(2)作为SYSTEM登录SQL*Plus;
(3)运行@utlxplan;
(4)运行CREATE PUBLIC SYNONYM PLAN_TABLE FOR PLAN_TABLE;
(5)运行GRANT ALL ON PLAN_TABLE TO PUBLIC。
如果愿意,可以把GRANT TO PUBLIC中的PUBLIC替换为某个用户。通过将PLAN_TABLE置为public,任何人都可以使用SQL*Plus进行跟踪(在我看来这并不是件坏事)。这么一来,就不需要每个用户都安装自己的计划表。还有一种做法是,在想要使用AUTOTRACE的每个模式中分别运行@utlxplan。
下一步是创建并授予PLUSTRACE角色:
(1)cd [ORACLE_HOME]/sqlplus/admin;
(2)作为SYS或SYSDBA登录SQL*Plus;
(3)运行@plustrce;
(4)运行GRANT PLUSTRACE TO PUBLIC。
重申一遍,如果愿意,可以把GRANT命令中PUBLIC替换为每个用户。
关于AUTOTRACE
你会自动得到一个AUTOTRACE报告,其中可能列出SQL优化器所用的执行路径,以及语句的执行统计信息。成功执行SQL DML(即SELECT、DELETE、UPDATE、MERGE和INSERT)语句后就会生成这个报告。它对于监视并调优这些语句的性能很有帮助。
控制报告
通过设置AUTOTRACE系统变量可以控制这个报告:
- SET AUTOTRACE OFF:不生成AUTOTRACE报告,这是默认设置。
- SET AUTOTRACE ON EXPLAIN:AUTOTRACE报告只显示优化器执行路径。
- SET AUTOTRACE ON STATISTICS:AUTOTRACE报告只显示SQL语句的执行统计信息。
- SET AUTOTRACE ON:AUTOTRACE报告既包括优化器执行路径,又包括SQL语句的执行统计信息。
- SET AUTOTRACE TRACEONLY:这与SET AUTOTRACE ON类似,但是不显示用户的查询输出(如果有的话)。
配置Statspack
只有作为SYSDBA连接时才能安装Statspack。所以,要想安装Statspack,必须你完成CONNECT /AS SYSDBA操作。在许多安装中,必须由DBA或管理员来完成这个任务。
只要能(作为SYSDBA)连接,安装Statspack就是小菜一碟了,只需运行@spcreate.sql。这个脚本可以在[ORACLE_HOME]/rdbms/admin中找到,作为SYSDBA连接时,应该像下面这样通过SQL*Plus执行这个脚本:
[tkyte@desktop admin]$ sqlplus / as sysdba SQL*Plus: Release 10.1.0.4.0 - Production on Sat Jul 23 16:26:17 2005 Copyright (c) 1982, 2005, Oracle. All rights reserved. Connected to: Oracle Database 10g Enterprise Edition Release 10.1.0.4.0 - Production With the Partitioning, OLAP and Data Mining options sys@ORA10G> @spcreate ... Installing Required Packages ... |
运行spcreate.sql脚本之前,你要了解3个信息:
- 将创建的PERFSTAT模式使用什么密码?
- PERFSTAT使用的默认表空间是什么?
- PERFSTAT使用的临时表空间是什么?
执行脚本时会提示你输入这些信息。如果输入有误,或者不小心取消了安装,在下一次尝试安装Statspack之前应该先用spdrop.sql删除用户(PERFSTAT)和已经安装的视图。安装Statspack会创建一个名为spcpkg.lis的文件。如果出现错误就应该检查这个文件。不过,只要提供了合法的表空间名(而且尚没有PERFSTAT用户),Statspack包应该能顺利地安装。
定制脚本
在这一节中,我会介绍本书所用脚本的相关需求。另外还会分析脚本底层的代码。
runstats
runstats是我开发的一个工具,能对做同一件事的两个不同方法进行比较,得出孰优孰劣的结果。你只需提供两个不同的方法,余下的事情都由runstats负责。runstats只是测量3个要素:
- 墙上时钟(wall clock)或耗用时间(elapsed time):知道墙上时钟或耗用时间很有用,不过这不是最重要的信息。
- 系统统计结果:会并排显示每个方法做某件事(如执行一个解析调用)的次数,并展示二者之差。
- 闩定(latching):这是这个报告的关键输出。
你在本书中会了解到,闩(latch)是一种轻量级的锁。锁(lock)是一种串行化设备,而串行化设备不支持并发。如果应用不支持并发,可扩缩性就比较差,只能支持较少的用户,而且需要更多的资源。构建应用时,我们往往希望应用能很好地扩缩,也就是说,为1位用户服务与为1,000或10,000位用户服务应该是一样的。应用中使用的闩越少,性能就越好。如果一种方法从墙上时钟来看运行时间较长,但是只使用了另一种方法10%的闩,我可能会选择前者。因为我知道,与使用更多闩的方法相比,使用较少闩的方法能更好地扩缩。
runstats最后独立使用,也就是说,最好在一个单用户数据库上运行。我们会测量各个方法的统计结果和闩定(锁定)活动。runstats在运行过程中,不希望其他任务对系统的负载或闩产生影响。只需一个小的测试数据库就能很好地完成这些测试。例如,我就经常使用我的台式机或手提电脑进行测试。
注意 我相信所有开发人员都应该有一个自行控制的试验台(test bed)数据库,可以在它上面尝试自己的想法,而不必要求DBA参与。假定数据库的个人开发版本称“如果数据库用来开发和测试,但不进行部署,那你可以放心使用”。只要有此许可,开发人员都应该在自己的台式机上建立一个数据库。这样你就不会漏掉任何细节!另外,我在会议和讲座上做过一些非正式的调查,发现几乎每个DBA都是从开发人员做起的。通过控制自己的数据库,开发人员能积累丰富的经验并得到最好的培训,能够了解数据库到底是怎样工作的,从长远来看这绝对是一笔不小的财富。
要使用runstats,需要能访问几个V$视图,并创建一个表来存储统计结果,还要创建runstats包。为此,需要访问3个V$表(就是那些神奇的动态性能表):V$STATNAME、V$MYSTAT和V$LATCH。以下是我使用的视图:
create or replace view stats as select 'STAT...' || a.name name, b.value from v$statname a, v$mystat b where a.statistic# = b.statistic# union all select 'LATCH.' || name, gets from v$latch; |
如果你能得到V$STATNAME、V$MYSTAT、V$LATCH和V$TIMER的直接授权,就能直接对这些表执行SELECT操作(相应地可以自行创建视图);否则,可以由其他人对这些表执行SELECT操作为你创建视图,并授予你在这个视图上执行SELECT的权限。
一旦建立视图,接下来只需要一个小表来收集统计结果:
create global temporary table run_stats ( runid varchar2(15), name varchar2(80), value int ) on commit preserve rows; |
最后,需要创建runstats包。其中包含3个简单的API调用:
- runstats测试开始时调用RS_STAT(runstats开始)。
- 正如你想象的,RS_MIDDLE会在测试之间调用。
- 完成时调用RS_STOP,打印报告。
创建runstats包的规范如下:
ops$tkyte@ORA920> create or replace package runstats_pkg 2 as 3 procedure rs_start; 4 procedure rs_middle; 5 procedure rs_stop( p_difference_threshold in number default 0 ); 6 end; 7 / Package created. |
参数P_DIFFERENCE_THRESHOLD用于控制最后打印的数据量。runstats会收集并得到每次运行的统计结果和闩信息,然后打印一个报告,说明每次测试(每个方法)使用了多少资源,以及不同测试(不同方法)的结果之差。可以使用这个输入参数来控制只查看差值大于这个数的统计结果和闩信息。由于这个参数默认为0,所以默认情况下可以看到所有输出。
下面我们逐一分析包体中的过程。包前面是一些全局变量,这些全局变量用于记录每次运行的耗用时间:
ops$tkyte@ORA920> create or replace package body runstats_pkg 2 as 3 4 g_start number; 5 g_run1 number; 6 g_run2 number; 7 |
下面是RS_START例程。这个例程只是清空保存统计结果的表,并填入“上一次”(before)得到的统计结果和闩信息。然后获得当前定时器值,这是一种时钟,可用于计算耗用时间(单位百分之一秒):
8 procedure rs_start 9 is 10 begin 11 delete from run_stats; 12 13 insert into run_stats 14 select 'before', stats.* from stats; 15 16 g_start := dbms_utility.get_time; 17 end; 18 |
接下来是RS_MIDDLE例程。这个例程只是把第一次测试运行的耗用时间记录在G_RUN1中。然后插入当前的一组统计结果和闩信息。如果把这些值与先前在RS_START中保存的值相减,就会发现第一个方法使用了多少闩,以及使用了多少游标(一种统计结果),等等。
最后,记录下一次运行的开始时间:
19 procedure rs_middle 20 is 21 begin 22 g_run1 := (dbms_utility.get_time-g_start); 23 24 insert into run_stats 25 select 'after 1', stats.* from stats; 26 g_start := dbms_utility.get_time; 27 28 end; 29 30 procedure rs_stop(p_difference_threshold in number default 0) 31 is 32 begin 33 g_run2 := (dbms_utility.get_time-g_start); 34 35 dbms_output.put_line 36 ( 'Run1 ran in ' || g_run1 || ' hsecs' ); 37 dbms_output.put_line 38 ( 'Run2 ran in ' || g_run2 || ' hsecs' ); 39 dbms_output.put_line 40 ( 'run 1 ran in ' || round(g_run1/g_run2*100,2) || 41 '% of the time' ); 42 dbms_output.put_line( chr(9) ); 43 44 insert into run_stats 45 select 'after 2', stats.* from stats; 46 47 dbms_output.put_line 48 ( rpad( 'Name', 30 ) || lpad( 'Run1', 10 ) || 49 lpad( 'Run2', 10 ) || lpad( 'Diff', 10 ) ); 50 51 for x in 52 ( select rpad( a.name, 30 ) || 53 to_char( b.value-a.value, '9,999,999' ) || 54 to_char( c.value-b.value, '9,999,999' ) || 55 to_char( ( (c.value-b.value)-(b.value-a.value)), '9,999,999' ) data 56 from run_stats a, run_stats b, run_stats c 57 where a.name = b.name 58 and b.name = c.name 59 and a.runid = 'before' 60 and b.runid = 'after 1' 61 and c.runid = 'after 2' 62 and (c.value-a.value) > 0 63 and abs( (c.value-b.value) - (b.value-a.value) ) 64 > p_difference_threshold 65 order by abs( (c.value-b.value)-(b.value-a.value)) 66 ) loop 67 dbms_output.put_line( x.data ); 68 end loop; 69 70 dbms_output.put_line( chr(9) ); 71 dbms_output.put_line 72 ( 'Run1 latches total versus runs -- difference and pct' ); 73 dbms_output.put_line 74 ( lpad( 'Run1', 10 ) || lpad( 'Run2', 10 ) || 75 lpad( 'Diff', 10 ) || lpad( 'Pct', 8 ) ); 76 77 for x in 78 ( select to_char( run1, '9,999,999' ) || 79 to_char( run2, '9,999,999' ) || 80 to_char( diff, '9,999,999' ) || 81 to_char( round( run1/run2*100,2 ), '999.99' ) || '%' data 82 from ( select sum(b.value-a.value) run1, sum(c.value-b.value) run2, 83 sum( (c.value-b.value)-(b.value-a.value)) diff 84 from run_stats a, run_stats b, run_stats c 85 where a.name = b.name 86 and b.name = c.name 87 and a.runid = 'before' 88 and b.runid = 'after 1' 89 and c.runid = 'after 2' 90 and a.name like 'LATCH%' 91 ) 92 ) loop 93 dbms_output.put_line( x.data ); 94 end loop; 95 end; 96 97 end; 98 / Package body created. |
下面可以使用runstats了。我们将通过例子来说明如何使用runstats对批量插入(INSERT)和逐行处理进行比较,看看哪种方法效率更高。首先建立两个表,要在其中插入1,000,000行记录:
ops$tkyte@ORA10GR1> create table t1 2 as 3 select * from big_table.big_table 4 where 1=0; Table created.
ops$tkyte@ORA10GR1> create table t2 2 as 3 select * from big_table.big_table 4 where 1=0; Table created. |
接下来使用第一种方法插入记录,也就是使用单独一条SQL语句完成批量插入。首先调用RUNSTATS_PKG.RS_START:
ops$tkyte@ORA10GR1> exec runstats_pkg.rs_start; PL/SQL procedure successfully completed.
ops$tkyte@ORA10GR1> insert into t1 select * from big_table.big_table; 1000000 rows created.
ops$tkyte@ORA10GR1> commit; Commit complete. |
下面准备执行第二种方法,即逐行地插入数据:
ops$tkyte@ORA10GR1> exec runstats_pkg.rs_middle; PL/SQL procedure successfully completed.
ops$tkyte@ORA10GR1> begin 2 for x in ( select * from big_table.big_table ) 3 loop 4 insert into t2 values X; 5 end loop; 6 commit; 7 end; 8 / PL/SQL procedure successfully completed. |
最后生成报告:
Name Run1 Run2 Diff STAT...recursive calls 8,089 1,015,451 1,007,362 STAT...db block changes 109,355 2,085,099 1,975,744 LATCH.library cache 9,914 2,006,563 1,996,649 LATCH.library cache pin 5,609 2,003,762 1,998,153 LATCH.cache buffers chains 575,819 5,565,489 4,989,670 STAT...undo change vector size 3,884,940 67,978,932 64,093,992 STAT...redo size 118,854,004 378,741,168 259,887,164
Run1 latches total versus runs -- difference and pct Run1 Run2 Diff Pct 825,530 11,018,773 10,193,243 7.49%
PL/SQL procedure successfully completed. |
mystat
mystat.sql和相应的mystat2.sql用于展示完成某操作之前和之后的某些Oracle“统计结果”的变化情况。mystat.sql只是获得统计结果的开始值:
set echo off 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')||'%' / set echo on |
mystat2.sql用于报告统计结果的变化情况(差值):
set echo off 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')||'%' / set echo on |
例如,要查看某个UPDATE生成的redo数,可以使用以下命令:
big_table@ORA10G> @mystat "redo size" big_table@ORA10G> set echo off
NAME VALUE ------------------------------ ---------- redo size 496
big_table@ORA10G> update big_table set owner = lower(owner) 2 where rownum <= 1000;
1000 rows updated.
big_table@ORA10G> @mystat2 big_table@ORA10G> set echo off
NAME V DIFF ------------------------------ ---------- ---------------- redo size 89592 89,096 |
由此可见,1,000行记录的UPDATE会生成89.096字节的redo。
SHOW_SPACE
SHOW_SPACE例程用于打印数据库段空间利用率信息。其接口如下:
ops$tkyte@ORA10G> desc show_space PROCEDURE show_space Argument Name Type In/Out Default? ------------------------------ ----------------------- ------ -------- P_SEGNAME VARCHAR2 IN P_OWNER VARCHAR2 IN DEFAULT P_TYPE VARCHAR2 IN DEFAULT P_PARTITION VARCHAR2 IN DEFAULT |
参数如下:
- P_SEGNAME:段名(例如,表或索引名)。
- P_OWNER:默认为当前用户,不过也可以使用这个例程查看另外某个模式。
- P_TYPE:默认为TABLE,这个参数表示查看哪种类型的对象(段)。例如,SELECT DISTINCT SEGMENT_TYPE FROM DBA_SEGMENTS会列出合法的段类型。
- P_PARTITION:显示分区对象的空间时所用的分区名。SHOW_SPACE一次只显示一个分区的空间利用率。
这个例程的输出如下,这里段位于一个自动段空间管理(Automatic Segment Space Management, ASSM)表空间中:
big_table@ORA10G> exec show_space('BIG_TABLE'); Unformatted Blocks ..................... 0 FS1 Blocks (0-25) ..................... 0 FS2 Blocks (25-50) ..................... 0 FS3 Blocks (50-75) ..................... 0 FS4 Blocks (75-100)..................... 0 Full Blocks ..................... 14,469 Total Blocks............................ 15,360 Total Bytes............................. 125,829,120 Total MBytes............................ 120 Unused Blocks........................... 728 Unused Bytes............................ 5,963,776 Last Used Ext FileId.................... 4 Last Used Ext BlockId................... 43,145 Last Used Block......................... 296 |
报告的各项结果说明如下:
- Unformatted Blocks:为表分配的位于高水位线(high-water mark, HWM)之下但未用的块数。把未格式化和未用的块加在一起,就是已为表分配但从未用于保存ASSM对象数据的总块数。
- FS1 Blocks-FS4 Blocks:包含数据的格式化块。项名后的数字区间表示各块的“空闲度”。例如,(0-25)是指空闲度为0~25%的块数。
- Full Blocks:已满的块数,不能再对这些执行插入。
- Total Blocks、Total bytes、Total Mbytes:为所查看的段分配的总空间量,单位分别是数据库块、字节和兆字节。
- Unused Blocks、Unused Bytes:表示未用空间所占的比例(未用空间量)。这些块已经分配给所查看的段,但目前在段的HWM之上。
- Last Used Ext FileId:最后使用的文件的文件ID,该文件包含最后一个含数据的区段(extent)。
- Last Used Ext BlockId:最后一个区段开始处的块ID;这是最后使用的文件中的块ID。
- Last Used Block:最后一个区段中最后一个块的偏移量。
如果对象在用户空间管理的表空间中,使用SHOW_SPACE查看时,输出如下:
big_table@ORA10G> exec show_space( 'BIG_TABLE' ) Free Blocks............................. 1 Total Blocks............................ 147,456 Total Bytes............................. 1,207,959,552 Total MBytes............................ 1,152 Unused Blocks........................... 1,616 Unused Bytes............................ 13,238,272 Last Used Ext FileId.................... 7 Last Used Ext BlockId................... 139,273 Last Used Block......................... 6,576 PL/SQL procedure successfully completed. |
这里惟一的区别是报告中最前面的Free Blocks项。这是段的第一个freelist(自由列表)组中的块数。我的脚本只测试了第一个freelist组。如果你想测试多个freelist组,还需要修改这个脚本。
我为以下代码加了注释。这个实用程序直接调用了数据库的DBMS_SPACE API。
create or replace procedure show_space ( p_segname in varchar2, p_owner in varchar2 default user, p_type in varchar2 default 'TABLE', p_partition in varchar2 default NULL ) -- this procedure uses authid current user so it can query DBA_* -- views using privileges from a ROLE, and so it can be installed -- once per database, instead of once per user who wanted to use it authid current_user as l_free_blks number; l_total_blocks number; l_total_bytes number; l_unused_blocks number; l_unused_bytes number; l_LastUsedExtFileId number; l_LastUsedExtBlockId number; l_LAST_USED_BLOCK number; l_segment_space_mgmt varchar2(255); l_unformatted_blocks number; l_unformatted_bytes number; l_fs1_blocks number; l_fs1_bytes number; l_fs2_blocks number; l_fs2_bytes number; l_fs3_blocks number; l_fs3_bytes number; l_fs4_blocks number; l_fs4_bytes number; l_full_blocks number; l_full_bytes number; -- inline procedure to print out numbers nicely formatted -- with a simple label procedure p( p_label in varchar2, p_num in number ) is begin dbms_output.put_line( rpad(p_label,40,'.') || to_char(p_num,'999,999,999,999') ); end; begin -- this query is executed dynamically in order to allow this procedure -- to be created by a user who has access to DBA_SEGMENTS/TABLESPACES -- via a role as is customary. -- NOTE: at runtime, the invoker MUST have access to these two -- views! -- this query determines if the object is an ASSM object or not begin execute immediate 'select ts.segment_space_management from dba_segments seg, dba_tablespaces ts where seg.segment_name = :p_segname and (:p_partition is null or seg.partition_name = :p_partition) and seg.owner = :p_owner and seg.tablespace_name = ts.tablespace_name' into l_segment_space_mgmt using p_segname, p_partition, p_partition, p_owner; exception when too_many_rows then dbms_output.put_line ( 'This must be a partitioned table, use p_partition => '); return; end; -- if the object is in an ASSM tablespace, we must use this API -- call to get space information, otherwise we use the FREE_BLOCKS -- API for the user-managed segments if l_segment_space_mgmt = 'AUTO' then dbms_space.space_usage ( p_owner, p_segname, p_type, l_unformatted_blocks, l_unformatted_bytes, l_fs1_blocks, l_fs1_bytes, l_fs2_blocks, l_fs2_bytes, l_fs3_blocks, l_fs3_bytes, l_fs4_blocks, l_fs4_bytes, l_full_blocks, l_full_bytes, p_partition); p( 'Unformatted Blocks ', l_unformatted_blocks ); p( 'FS1 Blocks (0-25) ', l_fs1_blocks ); p( 'FS2 Blocks (25-50) ', l_fs2_blocks ); p( 'FS3 Blocks (50-75) ', l_fs3_blocks ); p( 'FS4 Blocks (75-100)', l_fs4_blocks ); p( 'Full Blocks ', l_full_blocks ); else dbms_space.free_blocks( segment_owner => p_owner, segment_name => p_segname, segment_type => p_type, freelist_group_id => 0, free_blks => l_free_blks); p( 'Free Blocks', l_free_blks ); end if; -- and then the unused space API call to get the rest of the -- information dbms_space.unused_space ( segment_owner => p_owner, segment_name => p_segname, segment_type => p_type, partition_name => p_partition, total_blocks => l_total_blocks, total_bytes => l_total_bytes, unused_blocks => l_unused_blocks, unused_bytes => l_unused_bytes, LAST_USED_EXTENT_FILE_ID => l_LastUsedExtFileId, LAST_USED_EXTENT_BLOCK_ID => l_LastUsedExtBlockId, LAST_USED_BLOCK => l_LAST_USED_BLOCK ); p( 'Total Blocks', l_total_blocks ); p( 'Total Bytes', l_total_bytes ); p( 'Total MBytes', trunc(l_total_bytes/1024/1024) ); p( 'Unused Blocks', l_unused_blocks ); p( 'Unused Bytes', l_unused_bytes ); p( 'Last Used Ext FileId', l_LastUsedExtFileId ); p( 'Last Used Ext BlockId', l_LastUsedExtBlockId ); p( 'Last Used Block', l_LAST_USED_BLOCK ); end; / |
BIG_TABLE
在全书的例子中,我使用了一个名为BIG_TABLE的表。根据所用的系统,这个表的记录数在1条和400万条之间,而且大小也不定,为200~800MB。不过,不论这样,表结构都是一样的。
为了创建BIG_TABLE,我编写了一个可以完成以下功能的脚本:
q 根据ALL_OBJECTS创建一个空表。这个字典视图用于填充BIG_TABLE。
q 置这个表为NOLOGGING。这是可选的,我之所以这样做是为了提高性能。对测试表使用NOLOGGING模式是安全的;由于生产系统中不会使用这样一个测试表,所以不会启用诸如Oracle Data Guard之类的特性。
q 用ALL_OBJECTS的内容填充表,然后迭代地插入其自身中,每次迭代会使表大小几乎加倍。
q 对这个表创建一个主键约束。
q 收集统计结果。
q 显示表中的行数。
要建立BIG_TABLE表,可以在SQL*Plus提示窗口运行以下脚本,传入希望在表中插入多少行记录。如果达到这个行数,脚本则停止执行。
create table big_table as select rownum id, a.* from all_objects a where 1=0 / alter table big_table nologging; declare l_cnt number; l_rows number := &1; begin insert /*+ append */ into big_table select rownum, a.* from all_objects a; l_cnt := sql%rowcount; commit; while (l_cnt < l_rows) loop insert /*+ APPEND */ into big_table select rownum+1_cnt, OWNER, OBJECT_NAME, SUBOBJECT_NAME, OBJECT_ID, DATA_OBJECT_ID, OBJECT_TYPE, CREATED, LAST_DDL_TIME, TIMESTAMP, STATUS, TEMPORARY, GENERATED, SECONDARY from big_table where rownum <= l_rows-l_cnt; l_cnt := l_cnt + sql%rowcount; commit; end loop; end; /
alter table big_table add constraint big_table_pk primary key(id) / begin dbms_stats.gather_table_stats ( ownname => user, tabname => 'BIG_TABLE', method_opt => 'for all indexed columns', cascade => TRUE ); end; / select count(*) from big_table; |
这里会收集与主键相关的表和索引的基准统计结果。另外,我还收集了加索引的列的统计直方图(这是我的一贯做法)。也可以收集其他列的直方图,但对big_table表来说没有必要这样做。
代码约定
需要指出这本书里使用的一个编码约定,也就是PL/SQL代码中的变量如何命名。例如,考虑如下包体:
create or replace package body my_pkg as g_variable varchar2(25); procedure p( p_variable in varchar2 ) is l_variable varchar2(25); begin null; end; end; |
这里有3个变量,一个全局包变量G_VARIABLE,一个过程形参P_VARIABLE,还有一个局部变量L_VARIABLE。我是根据变量的作用域来命名的。所有全局变量都以G_开头,参数用P_开头,局部变量用L_开头。这样做的这样原因是为了区别PL/SQL变量和数据库表中的列。例如,如果有以下过程:
create procedure p( ENAME in varchar2 ) as begin for x in ( select * from emp where ename = ENAME ) loop Dbms_output.put_line( x.empno ); end loop; end; |
EMP表中ENAME非空的所有行都会打印出来。SQL看到ename=ENAME时,它会把ENAME列与自身比较(这是自然的)。为了避免这种错误,可以使用ename=P.ENAME,也就是说,用过程名来限定对PL/SQL变量的引用,但很容易忘记加过程名进行限定;一旦忘了,就会导致错误。
我总是根据作用域来对变量命名。这样一来,可以很容易地把参数与局部变量和全局变量区分开,而且还可以消除列名和变量名的任何歧义。