例如,SQL是一种高级语言,具有很大的灵活性,从数据库中提取相同的数据可以用不同形式的SELECT语句来实现,但是,应用程序的性能却随着SELECT语句形式的不同而大相径庭,这是因为不同形式的SELECT语句具有不同的处理成本。在这种情况下,我们就应该选择那些处理成本低廉的SELECT语句,这样,应用程序才会有较好的性能。
DB2通用数据库本身提供一个SQL编译器,该编译器创建编译后的SQL语句,当该编译器编译SQL语句时,它将重新编写这些SQL语句,以生成一种更容易对其进行优化的形式,这个过程称之为“查询重写(query rewrite)”。
然后,SQL编译器产生许多满足用户查询要求的、可选的执行方案,并根据表、索引、列和函数的统计
有一点很重要,需要我们注意,那就是不管存取方案的优劣,SQL编译器(包括查询重写和优化两个阶段)必须从中选择一个,以产生满足应用程序查询要求的结果集,因此,我们在编写查询代码时,只应查询我们需要的数据,不需要的数据就不要查询,这样做的目的是确保SQL编译器能够选择一个最好的存取方案。
编写SELECT语句时一般遵循以下七个方面的准则:
1、在SELECT列表中仅仅指明需要的列
我们在编写SELECT语句时,尽管有时候不需要用到表中所有的字段,但还是习惯用*(表示引用表中所有的字段)来指定表中所有的列,这样做在编程上确实很简单、方便,但这么做的后果是应用程序返回一些我们不需要的列,系统做一些不必要的处理,做一些无用功,徒耗系统宝贵的软、硬件资源,尤其当表中有很多字段时,这种浪费现象就越加明显;而且,这也不是良好的编程习惯,我们不应提倡。
2、使用谓词来限制返回的行数
在SQL编程语言中,按照评估过程中如何使用谓词、何时使用谓词,我们将谓词划分为四大类(这四类谓词各自有不同的处理成本),按性能由高至低排列如下:
范围界定谓词是指那些限定索引扫描范围的谓词,它们为索引搜索提供键值的起始值和/或终止值。索引参数谓词不用于界定搜索范围,但可以根据索引对它进行评估,因为谓词中的列是索引中的一部分。例如,假设表staff中的索引定义在name,dept和years三个字段上,执行下面的SELECT语句:
SELECT name,job,salary FROM staff
WHERE name=’John’
dept=10
years>5
头两个谓词(name=’John’和dept=10)是范围界定谓词,而years>5是索引参数谓词,因为单凭上述
数据库管理器在评估这些谓词的时候将利用索引数据,而不是读取数据库中的基本表。这些范围界定谓词和索引参数谓词通过减少需要从表中读取的行的数目来减少存取的数据页的数目。索引参数谓词不影响被存取的索引页的数目。
数据参数谓词是那些不能被索引管理器评估,却能被数据管理服务(DMS)评估的谓词。通常,这种谓词需要从基本表中存取个别行,如果需要的话,数据管理服务还会提取需要的列来评估该谓词。
例如,假设索引定义在表project的projno列上,而不是deptno列上,执行下面的查询:
SELECT projno,projname,repemp FROM project
WHERE deptno=’D11’
ORDER BY projno
谓词“deptno=’D11’”是数据参数谓词,因为没有索引定义在deptno列上,必须存取基本表来评估该谓词。
剩余谓词是指那些除了对基本表进行简单的存取操作之外,还要进行I/O操作的谓词,包括使用子查询的谓词(子查询中带有ANY,ALL,IN或SOME),以及读取LONG VARCHAR或大对象(LOB)数据的谓词(在DB2中,LONG VARCHAR、大对象和表是分开存放的)。
剩余谓词是用关系数据服务(RDS)来进行评估的,而且,它在这四类谓词中成本最昂贵。由于相对范围界定谓词和索引参数谓词来说,剩余谓词和数据参数谓词的成本比较高,所以,我们应该尽可能地限制范围界定谓词和索引参数谓词界定的行数。
我们来看一下DB2的组件:索引管理器,数据管理服务和关系数据服务。图1显示DB2的各个组件和处理这四类谓词的位置。
图1 DB2 UDB组件和谓词
其实,DB2组件很多,图1只是一个简化后的示意图。
关系数据服务(RDS)从应用程序那里接收到SQL请求,并返回结果集;除了剩余谓词外,RDS将所有谓词都发送给数据管理服务(DMS);剩余谓词由RDS进行评估。
DMS评估数据参数谓词。如果SELECT列表中存在一些不能由索引搜索进行评估的列,那么,DMS就会直接扫描数据页。
索引管理器从DMS那里接收、评估范围界定谓词和索引参数谓词,然后,将数据页的行标识符(RID)返回给DMS。
了解各种谓词的定义、转换条件及其使用场所,对我们深入剖析应用程序,提高应用程序性能是有所裨益的。
3、指定FOR UPDATE子句
如果想更新提取的数据,我们应该在游标定义的SELECT语句中指定FOR UPDATE子句。这样做的好处在于,数据库管理器在开始的时候就可以选择适当的加锁级别,例如,使用U(更新)锁,而不是S(共享)锁,这样,当处理后续的UPDATE语句时,就可以省去从S锁转换到U锁的开销了。
指定FOR UPDATE子句的另外一个好处是可以减少应用程序的死锁机会。死锁是这样一种情形:一个以上的应用程序等待另外一个应用程序释放对数据的锁定,而那些等待的应用程序正占据着另外应用程序所需要的数据,并且对该数据也进行了锁定。我们假设两个应用程序按下列顺序同时对同一行进行提取、更新操作:
(1)应用程序1提取该行
(2)应用程序2提取该行
(3)应用程序1更新该行
(4)应用程序2更新该行
在第4步,应用程序2将等待应用程序1完成更新操作,还要等应用程序1释放占据锁定,然后才能开始应用程序2的更新操作。可是,当我们声明一个游标时不指定FOR UPDATE子句,应用程序1就会获得并占据一个S(共享)锁定,对该行加锁(第1步),这意味着第二个应用程序用不着等待也可以获得并占据一个S锁(第2步)。然后,第一个应用程序试图获得一个U(更新)锁以处理更新语句,但是它必须等第二个应用程序释放其正占据着的S锁(第3步)。同时,第二个应用程序也试图获得一个U锁,并且由于第一个应用程序占据着S锁而进入等待状态(第4步)。这种情形就是死锁,造成的后果是应用程序1或应用程序2的交易不得不回滚。
如果在DECLARE CURSOR语句中指定FOR UPDATE子句,当应用程序1提取该行时,将会给该行加上U锁,应用程序2将会等待应用程序1释放U锁,这样,两个应用程序之间就不会发生死锁了。
下面举例说明如何在SELECT语句中使用FOR UPDATE子句。
EXEC SQL DECLARE c1 CURSOR FOR SELECT * FROM employee
FOR UPDATE OF job;
EXEC SQL OPEN c1;
EXEC SQL FETCH c1 INTO …;
If(strcmp(change,“YES”)= =0)
EXEC SQL UPDATE employee SET job = :newjob
WHERE CURRENT OF c1;
EXEC SQL CLOSE c1;
对于CLI编程,我们可以使用函数SQLSetConnectAttr()将DB2 CLI的连接属性SQL_ATTR_ACCESS_MODE的值设置为:SQL_MODE_READ_WRITE,
4、指定OPTIMIZE FOR n ROWS子句
当我们希望提取的行数远远小于可能返回的行数时,在SELECT语句中指定OPTIMIZE FOR n ROWS子句。基于提取n行的假设,OP
TIMIZE FOR子句会影响查询的优化,同时也决定了通讯缓冲区中行的数目。
SELECT projno,projname,repemp FROM project
WHERE deptno=’D11’ OPTIMIZE FOR 10 ROWS
运用行的分块技术,通过在单一操作中一次性地提取一组行来减轻管理器的负担,这些行存储在缓冲区中,应用程序中的每一个FETCH请求都会从该缓冲区中提取下一行,如果指定OPTIMIZE FOR 10 ROWS,那么,系统会以10行为一组返回给用户。数据库
需要注意的是,OPTIMIZE FOR n ROWS子句既不会限制可以提取的行数,也不会影响提取的结果,但是,该子句会影响应用程序的性能,如果最终提取的行数小于或等于n,该子句会改善性能;否则,如果大于n,性能就会下降。
5、指定FETCH FIRST n ROWS ONLY子句
如果不希望应用程序提取n行以上的记录,我们可以在编程时指定FETCH FIRST n ROWS ONLY子句;反之,如果不指定该子句,结果集中可能就会有很多行(大于n)。注意,该子句不能与FOR UPDATE子句同时使用。
参看下面的例子,程序最多能提取5行。
SELECT projno,projname,repemp FROM project
WHERE deptno=’D11’
FETCH FIRST 5 ROWS ONLY
该子句同OPTIMIZE FOR n ROWS子句一样,也决定通讯缓冲区中行的数目;如果同时指定FETCH FIRST n1 ROWS ONLY子句和OPTIMIZE FOR n2 ROWS子句,则取n1和n2二者中的较小值作为通讯缓冲区的大小。
6、指定FOR FETCH ONLY子句
如果不想更新那些由SELECT语句提取的行,我们可以在SELECT语句中指定FOR FETCH ONLY子句,这么做的好处是,处理应用程序提出的查询请求时可以充分利用行的分块技术,进而改善性能;该子句还能改善数据的并发性,因为使用该子句查询的那些行上不再有独占的锁了。除了FOR FETCH ONLY子句,我们还可以使用FOR READ ONLY子句,二者是同义词,功效一致。
7、避免数据类型转换
我们应尽可能地避免数据类型的转换,特别是数字的数据类型之间的转换。当比较两个值时,使用具有相同数据类型的项目进行比较效率会更高。例如,有两张表:A和B,使用A的A1列和B的B1列进行两表间的连接操作,如下所示:
SELECT * FROM A,B
WHERE A1=B1
如果列A1和B1的数据类型一致,则无需进行数据类型转换;如果不一致,那么,应用程序就会在运行时进行数据类型的转换以比较二者的数值大小,从而影响应用程序的性能。例如,如果A1列的数据类型是decimal,而B1是integer,并且都有一个数值“123”,那么这时就需要进行数据类型转换了,因为表A将“123”存储为“123C”(十六进制表示),而表B存储为“7B”(十六进制表示)。
再者,进行数据类型转换时,由于计算精度的限制,可能会导致错误的发生。
DB2 UDB提供许多数据类型,其中,有用于数字数据的SMALLINT,INTEGER,BIGINT,DECIMAL,REAL,DOUBLE;有用于字符数据的CHAR,VARCHAR,LONG VARCHAR,CLOB;也有用于双字节字符数据的GRAPHIC,VARGRAPHIC,LONG VARGRAPHIC和DBCLOB,等等。由于数据库的存储容量和各种变量的处理成本都取决于数据类型,所以,我们在编程时如何选择适当的数据类型就显得十分重要,以下是选择数据类型时应该遵循的一些准则:
● 对于比较短的列,尽量使用定长的CHAR而不是变长的VARCHAR。虽然,当数据的长度参差不齐时,VARCHAR可以节省数据库存储空间,但是系统需要花费额外的开销去检查每个数据的长度。
● 尽量使用VARCHAR或VARGRAPHIC而不是LONG VARCHAR或LONG VARGRAPHIC。VARCHAR列和LONG VARCHAR列的最大长度差不多,基本一致,VARCHAR列的最大长度是32672字节,LONG VARCHAR列的最大长度是32700字节;同样地,VARGRAPHIC列和LONG VARGRAPHIC列的最大长度也相仿,VARGRAPHIC是16336字节,LONG VARGRAPHIC是16350字节。对于LONG VARCHAR列和LONG VARGRAPHIC列有一些限制,比如,这两列中的数据不能存储在数据库缓冲池中。
● 如果不需要小数部分,则尽量使用整数(SMALLINT,INTEGER,BIGINT)而不是浮点数(REAL或DOUBLE)或十进制数(DECIMAL)。比较而言,整数的处理成本要廉价得多。
● 尽量使用日期--时间(DATE,TIME,TIMESTAMP)而不是字符(CHAR)。日期--时间数据类型会占用较少的数据库存储空间,而且,对于日期--时间数据类型的变量,我们还可以利用系统提供的一些内置函数(如YEAR,MONTH)对其进行计算、转换等处理,一般来说,内置函数要比我们手工编写的函数在性能、效率上都要高一些。
● 尽量使用数字的数据类型而不是字符的数据类型。
凡事都是一分为二,有利有弊的,我们不能既想节省数据库的存储空间,又希望降低应用程序的处理成本,二者只能取其一,不可兼得,这就要求我们在编程时不能仅局限于一点,应该通盘考虑整个应用程序的设计,综合评估,权衡利弊,以求应用程序的性能更优。
结束语
一谈起数据库性能调整、优化,我们脑海中往往就会有这样的概念:优化是系统维护时做的事情,属于后期