数据库实用性能知识
Left join和Inner join
Left Join和Inner Join的关系代数表达含义是不同的。换句话说它们所表达的数学含义是不同的。我们不能说Left Join的性能优于Inner Join或反之。应用时要根据自己的业务要求选择相应的连接方式。
下面是一个外连接使用不当的例子。
select p.pk_invbasdoc, p.pk_calbody, p.pk_invmandoc, b.invcode, b.invname
from bd_invbasdoc b, bd_produce p
where p.pk_invbasdoc(+) = b.pk_invbasdoc
and (p.pk_invbasdoc in ('0001AA1000000047WM16', '0001AA1000000047WM1H',
'0001AA1000000047WM1V', '0001AA1000000047X3O4'))
这里的外连接是没必要的,加了外连接,oracle选择了2个full table + hash outer join,把外连接去掉,语句执行非常快
改写后:
select p.pk_invbasdoc, p.pk_calbody, p.pk_invmandoc, b.invcode, b.invname
from bd_invbasdoc b inner join bd_produce p on p.pk_invbasdoc = b.pk_invbasdoc
where p.pk_invbasdoc in ('0001AA1000000047WM16', '0001AA1000000047WM1H',
'0001AA1000000047WM1V', '0001AA1000000047X3O4')
避免不必要的表连接
Select a.col1, b.col2
from a join b on a.code1=b.code2
left join c on a.code2=c.code3
…
where a.col2=xxx
在简化代码的同时需要考虑性能因素。
尽量使用批处理,减少访问数据库的次数
没有设计合理的批量接口或没有使用缓存等原因,导致SQL访问量巨大,影响系统性能。使用缓存、批量接口、合并类似SQL可以解决类似问题。
Where子句中的列转化
select max(iauditsequence) iauditsequence,
crdcenterid,
cinventoryid,
case
when bauditbatchflag = 'Y' then
vbatch
else
NULL
end vbatch
from v_ia_inoutledger v
where iauditsequence 0
and fdispatchflag = 0
and nprice is not null
and (v.badjustedItemflag < 'Y' or
v.cbilltypecode < 'I9')
and (v.cbiztypeid is null or csourcebilltypecode
< '32')
and v.crdcenterid
|| v.cinventoryid in
('1035V610000000003Y6J1035V61000000002P4FJ',
'1035V610000000003Y6J1035V610000000009KGE',
'1035V610000000003Y6J1035V61000000000MU7V',
'1035V610000000003Y6J1035V61000000000HO8A',
'1035V610000000003Y6J1035V61000000000PT5J')
group by crdcenterid,
cinventoryid,
bauditbatchflag,
case
when bauditbatchflag = 'Y'
then
vbatch
else
NULL
end
两个字段因为发生关联操作导致无法使用cinventoryid上的索引
改写成:
select max(iauditsequence) iauditsequence,
crdcenterid,
cinventoryid,
case
when bauditbatchflag = 'Y' then
vbatch
else
NULL
end vbatch
from v_ia_inoutledger v
where iauditsequence 0
and fdispatchflag = 0
and nprice is not null
and (v.badjustedItemflag < 'Y' or
v.cbilltypecode < 'I9')
and (v.cbiztypeid is null or
csourcebilltypecode < '32')
and
v.cinventoryid in
('1035V61000000002P4FJ',
'1035V610000000009KGE',
'1035V61000000000MU7V',
'1035V61000000000HO8A',
'1035V61000000000PT5J') and v.Crdcenterid='1035V610000000003Y6J'
group by crdcenterid,
cinventoryid,
bauditbatchflag,
case
when bauditbatchflag = 'Y'
then
vbatch
else
NULL
end
还有一些例子如下:
列上有数学运算的,如:
select * from staff_member where salary*2<10000;
使用不等于(<>)运算的,如:
select * from staff_member where dept_no<>2001;
使用substr字符串函数的,如:
select * from staff_member where substr(last_name,1,4)=’FRED’;
‘%’通配符在第一个字符的,如:
select * from staff_member where first_name like ‘%DON’;
字符串连接(||)的,如:
select * from staff_member where first_name||’’=’DONALD’
警惕在in中进行硬编码拼接条件
select …
from table1 a
where a.col in (xxx,xxx,xxx,xxx,xxx,xxx,xxx ….)
这种SQL拼接的条件数量不能过多,如果条件数目过多会严重影响SQL解析时间,甚至SQL无法运行。
如果条件过多可以考虑临时表方式解决。
Null的处理
Oracle中认为Null和’’等效。SQL Server中Null和’’不是一个东西。判断Null时需要使用is null判断,=null的结果永远是false。
oracle结果集not in关联时null的处理
例如
Select * from table where col1 not in (select col1 from table 2)
改成
Select * from table where col1 not in (select col1 from table2 where col1 is not null) and col1 is not null
合理使用索引
使用规则:
1、表的主键、外键必须有索引;
2、数据量超过300的表应该有索引;
3、经常与其他表进行连接的表,在连接字段上应该建立索引;
4、经常出现在Where子句中的字段,特别是大表的字段,应该建立索引;
5、索引应该建在选择性高的字段上;
6、索引应该建在小字段上,对于大的文本字段甚至超长字段,不要建索引;
7、复合索引的建立需要进行仔细分析;尽量考虑用单字段索引代替:
A、正确选择复合索引中的主列字段,一般是选择性较好的字段;
B、复合索引的几个字段是否经常同时以AND方式出现在Where子句中?单字段查询是否极少甚至没有?如果是,则可以建立复合索引;否则考虑单字段索引;
C、如果复合索引中包含的字段经常单独出现在Where子句中,则分解为多个单字段索引;
D、如果复合索引所包含的字段超过3个,那么仔细考虑其必要性,考虑减少复合的字段;
E、如果既有单字段索引,又有这几个字段上的复合索引,一般可以删除复合索引;
8、频繁进行数据操作的表,不要建立太多的索引;
9、删除无用的索引,避免对执行计划造成负面影响
Oracle中exists和in的使用
当父查询的选择性更好时一般使用exists;当子查询的选择性更好时使用in。
例如:
--in example
select xxx from parent_table a
where a.code in (select code from sub_table b where sub_pk=xxx)
--exists example
select xxx from parent_table a
where a.parent_pk=xxx and exists (select 1 from sub_table b where a.code=b.code)
选择最有效的表名顺序(只在基于规则的优化器中有效)
ORACLE的解析器按照从右到左的顺序处理 FROM子句中的表名,因此 FROM子句中写在最后的表(基础表 driving
table)将被最先处理.
在 FROM 子句中包含多个表的情况下,你必须选择记录条数最少的表作为基础表(最后的表).当 ORACLE 处理多个表时, 会运用排序及合并的方式连接它们.首先,扫描第一个表(FROM 子句中最后的那个表)并对记录进行派序,然后扫描第二个表(FROM 子句中最后第二个表),最后将所有从第二个表中检索出的记录与第一个表中合适记录进行合并.
例如:
表 TAB1 16,384 条记录
表 TAB2 1 条记录
选择 TAB2 作为基础表 (最好的方法)
select count(*) from tab1,tab2 执行时间 0.96 秒
选择 TAB2 作为基础表 (不佳的方法)
select count(*) from tab2,tab1 执行时间 26.09 秒
如果有 3 个以上的表连接查询, 那就需要选择交叉表(intersection table)作为基础表, 交叉表是指那个被其他表所引
用的表.
例如:
EMP 表描述了 LOCATION 表和 CATEGORY 表的交集.
SELECT *
FROM LOCATION L ,
CATEGORY C, EMP E
WHERE E.EMP_NO BETWEEN 1000 AND 2000
AND E.CAT_NO = C.CAT_NO
AND E.LOCN = L.LOCN
将比下列 SQL 更有效率
SELECT *
FROM EMP E ,
LOCATION L ,
CATEGORY C
WHERE E.CAT_NO = C.CAT_NO
AND E.LOCN = L.LOCN
AND E.EMP_NO BETWEEN 1000 AND 2000
Where子句中的链接顺序
ORACLE 采用自下而上的顺序解析 WHERE 子句,根据这个原理,表之间的连接必须写在其他 WHERE 条件之前, 那些可
以过滤掉最大数量记录的条件必须写在 WHERE 子句的末尾.
例如:
(低效,执行时间 156.3 秒)
SELECT
FROM EMP E
WHERE SAL > 50000
ND JOB = ‘M N GER’
AND 25 < (SELECT COUNT(*) FROM EMP
WHERE MGR=E.EMPNO);
(高效,执行时间 10.6 秒)
SELECT
FROM EMP E
WHERE 25 < (SELECT COUNT(*) FROM EMP
WHERE MGR=E.EMPNO)
AND SAL > 50000
ND JOB = ‘M N GER’
Select子句中避免使用 select *
当你想在 SELECT 子句中列出所有的 COLUMN 时,使用动态 SQL 列引用 ‘*’ 是一个方便的方法.不幸的是,这是一个非常低效的方法.实际上,ORACLE 在解析的过程中,会将’*’依次转换成所有的列名, 这个工作是通过查询数据字
典完成的,这意味着将耗费更多的时间.
减少数据库的访问次数
一次业务访问数据库不超过5次,当执行每条 SQL 语句时, ORACLE 在内部执行了许多工作:解析 SQL 语句,估算索引的利用率,绑定变量 , 读数据块等等.由此可见,减少访问数据库的次数,就能实际上减少 ORACLE 的工作量.
例如,
以下有三种方法可以检索出雇员号等于 0342 或 0291 的职员.
方法 1 (最低效)
SELECT EMP_NAME , SALARY , GRADE
FROM EMP
WHERE EMP_NO = 342;
SELECT EMP_NAME , SALARY , GRADE
FROM EMP
WHERE EMP_NO = 291;
方法 2 (次低效)
DECLARE
CURSOR C1 (E_NO NUMBER) IS
SELECT EMP_NAME,SALARY,GRADE
FROM EMP
WHERE EMP_NO = E_NO;
BEGIN
OPEN C1(342);
FETCH C1 INTO
方法3(高效)
SELECT A.EMP_NAME , A.SALARY , A.GRADE,
B.EMP_NAME , B.SALARY , B.GRADE
FROM EMP A,EMP B
WHERE A.EMP_NO = 342
AND B.EMP_NO = 291;
注意:
在 SQL*Plus , SQL*Forms 和 Pro*C 中重新设置 ARRAYSIZE 参数, 可以增加每次数据库访问的检索数据量 ,建议值为 200
使用Decode函数来减少处理时间
使用 DECODE 函数可以避免重复扫描相同记录或重复连接相同的表.
DECODE的语法:DECODE(value,if1,then1,if2,then2,if3,then3,...,else),表示如果value 等于if1时,DECODE函数的结果返回then1,...,如果不等于任何一个if值,则返回else。初看一下,DECODE 只能做等于测试,但刚才也看到了,我们通过一些函数或计算替代value,是可以使DECODE函数具备大于、小于或等于功能。
例如:
SELECT COUNT(*),SUM(SAL)
FROM EMP
WHERE DEPT_NO = 0020
AND ENAME LIKE ‘SMITH%’;
SELECT COUNT(*),SUM(SAL)
FROM EMP
WHERE DEPT_NO = 0030
AND ENAME LIKE ‘SMITH%’;
你可以用 DECODE 函数高效地得到相同结果
SELECT COUNT(DECODE(DEPT_NO ,0020, ’*’ ,NULL)) D0020_COUNT
COUNT(DECODE(DEPT_NO ,0030, ’*’ ,NULL)) D0030_COUNT
SUM(DECODE(DEPT_NO,0020,SAL,NULL)) D0020_SAL,
SUM(DECODE(DEPT_NO,0030,SAL,NULL)) D0030_SAL
FROM EMP WHERE ENAME LIKE ‘SMITH%’
类似的,DECODE 函数也可以运用于 GROUP BY 和 ORDER BY 子句中.
整合简单,无关联的数据库访问
如果你有几个简单的数据库查询语句,你可以把它们整合到一个查询中(即使它们之间没有关系)
例如:
SELECT NAME
FROM EMP
WHERE EMP_NO = 1234;
SELECT NAME
FROM DPT
WHERE DPT_NO = 10 ;
SELECT NAME
FROM CAT
WHERE C T_TYPE = ‘RD’
上面的 3 个查询可以被合并成一个:
SELECT E.NAME , D.NAME , C.NAME
FROM CAT C , DPT D , EMP E,DUAL X
WHERE NVL(‘X’ X DUMMY) = NVL(‘X’ E ROWID( ))
AND NVL(‘X’ X DUMMY) = NVL(‘X’ D ROWID( ))
AND NVL(‘X’ X DUMMY) = NVL(‘X’ C ROWID( ))
AND E.EMP_NO( ) = 1234
AND D.DEPT_NO( ) = 10
AND C.T_TYPE( ) = ‘RD’
删除重复记录
最高效的删除重复记录方法 (因为使用了 ROWID)
DELETE FROM EMP E
WHERE E.ROWID > (SELECT MIN(X.ROWID)
FROM EMP X
WHERE X.EMP_NO = E.EMP_NO);
用 TRUNCATE 替代 DELETE
当删除表中的记录时,在通常情况下,回滚段(rollback segments ) 用来存放可以被恢复的信息. 如果你没有 COMMIT事务,ORACLE 会将数据恢复到删除之前的状态(准确地说是恢复到执行删除命令之前的状况) 而当运用TRUNCATE时,回滚段不再存放任何可被恢复的信息.当命令运行后,数据不能被恢复.因此很少的资源被调用,执行时间也会很短.
(注意: TRUNCATE 只在删除全表适用,TRUNCATE 是 DDL 不是 DML)
3.2.17尽量多使用COMMIT
只要有可能,在程序中尽量多使用 COMMIT,这样程序的性能得到提高,需求也会因为 COMMIT 所释放的资源而减少:
COMMIT 所释放的资源:
a.回滚段上用于恢复数据的信息.
b.被程序语句获得的锁
c. redo log buffer 中的空间
d. ORACLE 为管理上述 3 种资源中的内部花费
(注意:在使用 COMMIT 时必须要注意到事务的完整性,现实中效率和事务完整性往往是鱼和熊掌不可得兼)
计算记录条数和一般的观点相反, count(*) 比 count(1)稍快
当然如果可以通过索引检索,对索引列的计数仍旧是最快的.例如 COUNT(EMPNO)
用where子句替换having子句
避免使用 HAVING 子句, HAVING 只会在检索出所有记录之后才对结果集进行过滤. 这个处理需要排序,总计等操作.如果能通过 WHERE 子句限制记录的数目,那就能减少这方面的开销.
例如:
低效:
SELECT REGION,AVG(LOG_SIZE)
FROM LOCATION
GROUP BY REGION
H VING REGION REGION != ‘SYDNEY’
ND REGION != ‘PERTH’ 高效
SELECT REGION,AVG(LOG_SIZE)
FROM LOCATION
WHERE REGION REGION != ‘SYDNEY’
ND REGION != ‘PERTH’
GROUP BY REGION
(注: HAVING 中的条件一般用于对一些集合函数的比较,如 COUNT() 等等. 除此而外,一般的条件应该写在
WHERE 子句中)
3.2.20减少对表的查询
在含有子查询的 SQL 语句中,要特别注意减少对表的查询.
例如:
低效
SELECT TAB_NAME
FROM TABLES
WHERE TAB_NAME = ( SELECT TAB_NAME
FROM TAB_COLUMNS
WHERE VERSION = 604)
AND DB_VER= ( SELECT DB_VER
FROM TAB_COLUMNS
WHERE VERSION = 604)
高效
SELECT TAB_NAME
FROM TABLES
WHERE (TAB_NAME,DB_VER)
= ( SELECT TAB_NAME,DB_VER)
FROM TAB_COLUMNS
WHERE VERSION = 604)
Update 多个 Column 例子:
低效:
UPDATE EMP
SET EMP_CAT = (SELECT MAX(CATEGORY) FROM EMP_CATEGORIES),
SAL_RANGE = (SELECT MAX(SAL_RANGE) FROM EMP_CATEGORIES)
WHERE EMP_DEPT =’ 0020’;
高效:
UPDATE EMP
SET (EMP_CAT, SAL_RANGE)
= (SELECT MAX(CATEGORY) , MAX(SAL_RANGE)
FROM EMP_CATEGORIES)
WHERE EMP_DEPT = ‘0020’;
通过内部函数提高sql效率
SELECT H.EMPNO,E.ENAME,H.HIST_TYPE,T.TYPE_DESC,COUNT(*)
FROM HISTORY_TYPE T,EMP E,EMP_HISTORY H
WHERE H.EMPNO = E.EMPNO
AND H.HIST_TYPE = T.HIST_TYPE
GROUP BY H.EMPNO,E.ENAME,H.HIST_TYPE,T.TYPE_DESC;
通过调用下面的函数可以提高效率.
FUNCTION LOOKUP_HIST_TYPE(TYP IN NUMBER) RETURN VARCHAR2
AS
TDESC VARCHAR2(30);
CURSOR C1 IS
SELECT TYPE_DESC
FROM HISTORY_TYPE
WHERE HIST_TYPE = TYP;
BEGIN
OPEN C1;
FETCH C1 INTO TDESC;
CLOSE C1;
RETURN (NVL(TDESC ’?’))
END;
FUNCTION LOOKUP_EMP(EMP IN NUMBER) RETURN VARCHAR2
AS
ENAME VARCHAR2(30);
CURSOR C1 IS
SELECT ENAME
FROM EMP
WHERE EMPNO=EMP;
BEGIN
OPEN C1;
FETCH C1 INTO ENAME;
CLOSE C1;
RETURN (NVL(EN ME ’?’))
END;
SELECT H.EMPNO,LOOKUP_EMP(H.EMPNO),
H.HIST_TYPE,LOOKUP_HIST_TYPE(H.HIST_TYPE),COUNT(*)
FROM EMP_HISTORY H
GROUP BY H.EMPNO , H.HIST_TYPE;
使用表的别名(Alias)
当在 SQL 语句中连接多个表时, 请使用表的别名并把别名前缀于每个 Column 上.这样一来,就可以减少解析的时间并减少那些由 Column 歧义引起的语法错误.
(注: Column 歧义指的是由于 SQL 中不同的表具有相同的 Column 名,当 SQL 语句中出现这个 Column 时,SQL 解
析器无法判断这个 Column 的归属)
3.2.23. 用 EXISTS 替代 IN
在许多基于基础表的查询中,为了满足一个条件,往往需要对另一个表进行联接.在这种情况下,使用EXISTS(或NOT
EXISTS)通常将提高查询的效率.
低效:
SELECT *
FROM EMP (基础表)
WHERE EMPNO > 0
AND DEPTNO IN (SELECT DEPTNO
FROM DEPT
WHERE LOC = ‘MELB’)
高效:
SELECT *
FROM EMP (基础表)
WHERE EMPNO > 0
AND EXISTS (SELECT ‘X’
FROM DEPT
WHERE DEPT.DEPTNO = EMP.DEPTNO
AND LOC = ‘MELB’)
用 NOT EXISTS 替代 NOT IN
在子查询中,NOT IN 子句将执行一个内部的排序和合并. 无论在哪种情况下,NOT IN 都是最低效的 (因为它对子查询
中的表执行了一个全表遍历). 为了避免使用 NOT IN ,我们可以把它改写成外连接(Outer Joins)或 NOT EXISTS.
例如:
SELECT *
FROM EMP
WHERE DEPT_NO NOT IN (SELECT DEPT_NO
FROM DEPT
WHERE DEPT_C T=’ ’)
为了提高效率.改写为:
(方法一: 高效)
SELECT *
FROM EMP A,DEPT B
WHERE A.DEPT_NO = B.DEPT_NO
AND B.DEPT_NO IS NULL
AND B DEPT_C T = ‘ ’
(方法二: 最高效)
SELECT *
FROM EMP E
WHERE NOT EXISTS (SELECT ‘X’
FROM DEPT D
WHERE D.DEPT_NO = E.DEPT_NO AND DEPT_C T = ‘ ’)
用表连接替换 EXISTS
通常来说 , 采用表连接的方式比 EXISTS 更有效率
SELECT ENAME
FROM EMP E
WHERE EXISTS (SELECT ‘X’
FROM DEPT
WHERE DEPT_NO = E.DEPT_NO
ND DEPT_C T = ‘ ’)
(更高效)
SELECT ENAME
FROM DEPT D,EMP E
WHERE E.DEPT_NO = D.DEPT_NO
ND DEPT_C T = ‘ ’
用 EXISTS 替换 DISTINCT
当提交一个包含一对多表信息(比如部门表和雇员表)的查询时,避免在 SELECT 子句中使用 DISTINCT. 一般可以考虑用EXIST 替换
例如:低效:
SELECT DISTINCT DEPT_NO,DEPT_NAME
FROM DEPT D,EMP E
WHERE D.DEPT_NO = E.DEPT_NO
高效:
SELECT DEPT_NO,DEPT_NAME
FROM DEPT D
WHERE EXISTS ( SELECT ‘X’
FROM EMP E
WHERE E.DEPT_NO = D.DEPT_NO);
EXISTS 使查询更为迅速,因为 RDBMS 核心模块将在子查询的条件一旦满足后,立刻返回结果.
识别’低效执行’的 SQL 语句
用下列 SQL 工具找出低效 SQL:
SELECT EXECUTIONS , DISK_READS, BUFFER_GETS,
ROUND((BUFFER_GETS-DISK_READS)/BUFFER_GETS,2) Hit_radio,
ROUND(DISK_READS/EXECUTIONS,2) Reads_per_run,
SQL_TEXT
FROM V$SQLAREA
WHERE EXECUTIONS>0
AND BUFFER_GETS > 0
AND (BUFFER_GETS-DISK_READS)/BUFFER_GETS < 0.8
ORDER BY 4 DESC;
或
select sql_text
from v$sql
where disk_reads > 1000 or (executions > 0 and buffer_gets/executions
> 30000);
v$sqlarea表字段解析:
SELECT *
FROM (select PARSING_USER_ID as 为语句解析第一条CURSOR的用户,
EXECUTIONS as 语句的执行次数,
SORTS as 语句的排序数,
COMMAND_TYPE as 语句种类,
DISK_READS as 从硬盘读取量,
ELAPSED_TIME as 语句被解析和执行的共用时间,
CPU_TIME as 语句被解析和执行的CPU时间 ,
--ROUND((BUFFER_GETS-DISK_READS)/BUFFER_GETS,2) as 命中率,
BUFFER_GETS as 子游标运行读内存次数,
sql_text as sql语句
FROM v$sqlarea
order BY disk_reads DESC)
where ROWNUM < 10000 and sql语句 like '%uh%' ;
避免死锁
死锁是因为互相等待资源导致的。从理论上讲,数据库死锁无法避免,但可以遵循一定原则使死锁发生的概率尽量降低。主要的原则如下:
- 按照相同的顺序访问对象
- 降低事务大小
- 降低事务隔离级别
按照相同的顺序访问对象可以避免相互持有对方请求资源的情况发生。例如一个操作主从表的处理流程,涉及查询和修改两个步骤。如果查询时是先查主表再查从表,则修改也应先修改主表再修改从表。
降低事务大小的一个主要手段,是将查询操作尽可能地提前(包括用一些中间变量记录下查询结果供后续使用),而把插入、修改等操作集中在方法靠后的部分。这样,可以让一个事务需要持有排它锁的时间尽可能短,减少死锁发生的概率。
事务隔离级别对死锁有重大影响。通常,应只设置到读提交级别,如果提高到重复读或串行化读的级别,由于查询也将持有锁不放,将极大提高死锁发生的概率。
另外也需要警惕中间件与数据库之间发生死锁。
避免阻塞
执行delete和update时,为过滤条件建立合理的索引可以避免全表扫描,减少锁定范围。
在长事务中,如果可能尽量把DML操作放在事务的最后执行,尽量减少锁定时间。
绑定变量的使用
OLTP系统中尽量使用绑定变量,可以减少数据库执行计划的解析时间。
OLAP因为记录波动较大,执行计划重用反而会产生不良的影响,需要分析使用场景。
数据库和高级开发语言应该做各自擅长的事情
目前我们使用的关系数据库是继续关系代数的原理衍生出来的。关系数据库对多集合操作(表关联)和结果集统计(group)有着先天的优势,这类工作应该交给数据库SQL来操作。
Java等高级语言有着丰富的数据结构,长于算法操作。
例如,图等便利、字符串拼接、解析等方面高级语言的性能优势明显强于数据库。
保证SQL高效执行的日常维护
Oracle:
- 应该定期更新统计信息
- 大表记录卸载后应该进行水位线的调整
SQL Server:
1、应该定期进行碎片整理

浙公网安备 33010602011771号