一、简介
本文将总结一些Oracle查询优化方法与改写技巧,通过一些案例说明用法,本文为观看《Oracle查询优化改写技巧与案例》一书观后的一些总结,大家有空的话可以去阅读一下,下面直接分点总结一些比较有用的优化技巧。
二、优化技巧
【1】. 判空处理使用is null / is not null,不能使用 = null进行判断
查询结果如上图,yxlx这个字段为空,如果使用 = null判断的话,查询结果不正确。
select t.* from zhxg_yx_yxxcgl_cssz t where t.yxlx = null;
必须使用is null / is not null进行判空处理:
select t.* from zhxg_yx_yxxcgl_cssz t where t.yxlx is null;
【2】. 巧用greatest()/least()/coalesce()函数
greatest(): 返回列表中最大的值,列表中的数据类型必须一致.
least(): 返回列表中最小的值,列表中的数据类型必须一致.
coalesce(): 返回列表中第一个非空的值,列表汇中的数据必须同一类型或者能转换成同一类型,效果类似nvl()/case when表达式.
--greatest 返回值列表中最大值
select greatest('A', 'B', 'C') from dual; -- 返回C
select greatest(null, 'B', 'C') from dual; -- 返回null
--least 返回值列表中最小值
select least('A', 'B', 'C') from dual; -- 返回A
select least(null, 'B', 'C') from dual; -- 返回null
--coalesce 返回该表达式列表的第一个非空value
select coalesce(1, null, 2) from dual; -- 返回1
select coalesce(null, 2, 1) from dual; -- 返回2
select coalesce(t.empno, t.mgr) as col from scott.emp t;
--等价于
select nvl(t.empno, t.mgr) as col from scott.emp t;
--等价于
select case when t.empno is not null then t.empno when t.mgr is not null then t.mgr end as col from scott.emp t;
【3】. 查询语句尽量使用别名,易于读懂分析sql语句
select t.zhlx as 账号类型, t.xxjd as 经度, t.xxwd as 纬度
from ZHXG_YX_YXXCGL_CSSZ t;
--实际项目中,通常都是用简名取别名
select t.zhlx as lx, t.xxjd as jd, t.xxwd as wd from ZHXG_YX_YXXCGL_CSSZ t
--在where子句中使用别名,注意要多嵌套一层
select r.*
from (select t.zhlx as lx, t.xxjd as jd, t.xxwd as wd
from ZHXG_YX_YXXCGL_CSSZ t) r
where r.lx is not null
【4】. select子句中巧用条件判断逻辑
这里主要通过case when子句或者nvl(),nvl2(),decode()函数来实现:
--性别码
select t.xbm as xbm
from ZHXG_YX_YXXCGL_XSJBXX t
where t.pkid = '07cc1b86-9df9-4b2e-9942-d14c7f238f89';
--使用case..when
select t.xbm,
case
when t.xbm = '1' then
'男'
when t.xbm = '2' then
'女'
else
'保密'
end as sex
from ZHXG_YX_YXXCGL_XSJBXX t
where t.pkid = '07cc1b86-9df9-4b2e-9942-d14c7f238f89';
--使用decode
select t.xbm, decode(t.xbm, '1', '男', '2', '女', '保密') as sex
from ZHXG_YX_YXXCGL_XSJBXX t
where t.pkid = '07cc1b86-9df9-4b2e-9942-d14c7f238f89';
【5】. 限制返回记录的行数
--rownum是一个伪列
select rownum, t.pkid, t.xh, t.lqh
from zhxg_yx_yxxcgl_xsjbxx t
where rownum <= 10;
获取rownum = 5这条记录,直接使用如下sql是查询不出结果的,因为rownum是依次给记录加上序号的,需要先将记录查询出来才能确定所处的序号是多少。
select rownum, t.pkid, t.xh, t.lqh
from zhxg_yx_yxxcgl_xsjbxx t
where rownum = 5;
下面进行改写:需要先查询出一些记录,然后进行编号,确定了序号之后才能使用=进行精确查询哪一条。
--获取rownum=5这条记录
select r.rn, r.pkid, r.xh, r.lqh
from (select rownum rn, t.pkid, t.xh, t.lqh
from zhxg_yx_yxxcgl_xsjbxx t
where rownum <= 10) r
where r.rn = 5;
【6】. 从表中随机返回n条记录
这里要注意rownum的不变性,注意先取数据还是先排序,顺序反了,查询结果都会不一样。下面是错误做法:
--错误做法:先取数据后再随机排序(因为rownum已经不变,不管怎么随机,都是同样的数据之间进行排序)
--运行多次发现数据不变,只是顺序变化而已
select t.pkid, t.xh, t.lqh, t.xm, rownum as rm --rownum排好后不变
from zhxg_yx_yxxcgl_xsjbxx t
where rownum <= 3
order by dbms_random.value();
运行多几次,我们发现,如果先取数据的话,对比两张图,发现三条记录对应的rownum是不会变化的,所以不管怎么随机排序都是在这三条记录之间进行随机排序。下面是正确做法:
--正确做法: 先排好序之后再取数据
select r.pkid, r.xh, r.xm, r.lqh, r.oldnum, rownum as newnum --外部查询rownum
from (select t.pkid, t.xh, t.lqh, t.xm, rownum as oldnum --子查询rownum
from zhxg_yx_yxxcgl_xsjbxx t
order by dbms_random.value()) r
where rownum <= 3;
【7】. 模糊查询中转义字符的处理
构建结果集:
with temp as
(select 'ABCEDF' as vname
from dual
union all
select '_BCEFG' as vname
from dual
union all
select '_BCEDF' as vname
from dual
union all
select '_\BCEDF' as vname
from dual
union all
select 'XYCEG' as vname
from dual)
SELECT * from temp
【a】查询vname中包含'_BCE'的记录:
with temp as
(select 'ABCEDF' as vname
from dual
union all
select '_BCEFG' as vname
from dual
union all
select '_BCEDF' as vname
from dual
union all
select '_\BCEDF' as vname
from dual
union all
select 'XYCEG' as vname
from dual)
--查询vname中包含'_BCE'的记录
SELECT * from temp t where t.vname like '_BCE%'
上图中圈起来的记录其实并不符合条件,我们需要对‘_’进行转义处理:
with temp as
(select 'ABCEDF' as vname
from dual
union all
select '_BCEFG' as vname
from dual
union all
select '_BCEDF' as vname
from dual
union all
select '_\BCEDF' as vname
from dual
union all
select 'XYCEG' as vname
from dual)
--查询vname中包含'_BCE'的记录
--SELECT * from temp t where t.vname like '_BCE%'
--转义处理
SELECT * from temp t where t.vname like '\_BCE%' escape '\'
【b】获取'_\BCEDF'这条记录
with temp as
(select 'ABCEDF' as vname
from dual
union all
select '_BCEFG' as vname
from dual
union all
select '_BCEDF' as vname
from dual
union all
select '_\BCEDF' as vname
from dual
union all
select 'XYCEG' as vname
from dual)
--获取'_\BCEDF'这条记录
SELECT * from temp t where t.vname like '_\BCE%' escape '\';
转义之后,报错,如上图,这时候还需要使用 ‘\\’ 进行转义:
with temp as
(select 'ABCEDF' as vname
from dual
union all
select '_BCEFG' as vname
from dual
union all
select '_BCEDF' as vname
from dual
union all
select '_\BCEDF' as vname
from dual
union all
select 'XYCEG' as vname
from dual)
--获取'_\BCEDF'这条记录
SELECT * from temp t where t.vname like '_\\BCE%' escape '\';
【8】. 巧用translate()函数:
- 针对字符进行替换,返回将出现在from中的每个字符替换为to中的相应字符以后的字符串;若from比to字符串长,那么在from中比to中多出的字符将会被删除; 三个参数中有一个是空,返回值也将是空值
--translate(char, from, to)替换函数
select translate('abc张三defabb', 'abcdef', '123456') as temp from dual;
--解析: a - 1 b - 2 c - 3 d - 4 e - 5 f - 6 依次替换字符
--三个参数中,只有有其中一个参数为null,结果就返回null
select translate('abc张三defabb', null, '123456') as temp from dual;
--若from比to字符串长,那么在from中比to中多出的字符将会被删除
select translate('abc张三defabb', 'abcdef', '123') as temp from dual;
【9】. 巧处理排序空值
使用nulls first/ nulls last进行空值排序处理:
--空值排前面
select t.pkid, t.xh, t.lqh, t.xm, t.mm
from ZHXG_YX_YXXCGL_XSJBXX t
where rownum <= 5
order by t.mm nulls first;
--空值排后面
select t.pkid, t.xh, t.lqh, t.xm, t.mm
from ZHXG_YX_YXXCGL_XSJBXX t
where rownum <= 5
order by t.mm nulls last;
【10】. 根据条件取不同的列进行排序
针对这种需求,我们可以在查询语句中多查询出一列,然后再根据这列进行排序。
--按分数进行排序,并且70-75分数段要先排序
select t.pkid, t.gkfs, t.xh, t.xm, t.lqh
from ZHXG_YX_YXXCGL_XSJBXX t
where rownum <= 10
order by t.gkfs;
--第一种方式
select *
from (select t.pkid,
t.gkfs,
t.xh,
t.xm,
t.lqh,
case
when t.gkfs >= 70 and t.gkfs <= 75 then
1
else
2
end as pxh
from ZHXG_YX_YXXCGL_XSJBXX t
where rownum <= 10) r
order by r.pxh, r.gkfs
当然,也可以直接在order by子句中判断:
--第二种方式
select t.pkid, t.gkfs, t.xh, t.xm, t.lqh
from ZHXG_YX_YXXCGL_XSJBXX t
where rownum <= 10
order by case
when t.gkfs >= 70 and t.gkfs <= 75 then
1
else
2
end,
t.gkfs
【11】. union all 与空字符串
在oracle中空字符串' '一般相当于null,但并不说明null 与空字符串' '是等价的,空字符串' '本质上还是varchar2()类型的,null可以被作为任何数据类型。看以下示例:
--''本质上还是varchar2()类型, null可以是任何类型
select 'a' from dual union all select '' from dual;
select 'a' from dual union all select null from dual;
但是下面的代码就会报错了,如果union all两边的数据类型没有对应上,就会报错:
--sysdate 是 时间类型 ''是varchar2类型, 报错
select sysdate from dual union all select '' from dual;
注意和下面代码的区别: null可以充当任何数据类型,所以不会报错。
--sysdate 是 时间类型 null可以是任何类型, 不会报错
select sysdate from dual union all select null from dual;
【12】. 使用union 代替 or可以提高效率
select *
from EMP t
where t.empno = '7788'
or t.ename = 'SCOTT';
--会存在重复数据,必须去重
select *
from EMP t
where t.empno = '7788'
union all
select *
from EMP t
where t.ename = 'SCOTT';
--使用到索引,效率相对较高
select *
from EMP t
where t.empno = '7788'
union
select *
from EMP t
where t.ename = 'SCOTT';
需要注意的是: 有重复数据的数据集使用union后得到的结果可能会不一致。看以下示例:
两个结果集有四条记录是重复的,使用or连接后查询得到五条数据:
select t.deptno
from EMP t
where t.job = 'SALESMAN'
or t.mgr = 7698;
接着,我们使用union来改写以下:
--去重之后,结果不正确
select t.deptno
from EMP t
where t.job = 'SALESMAN'
union
select t.deptno
from EMP t
where t.mgr = 7698;
如上图,union之后,结果集变成一条记录了,明显不正确。接着改写:使用唯一列标识/rowid/rownum来进行区别,可以避免union不必要的去重。
--解决方法一: 使用唯一列
select t.empno, t.deptno
from EMP t
where t.job = 'SALESMAN'
union
select t.empno, t.deptno
from EMP t
where t.mgr = 7698;
--解决方法二:使用rowid(或者对查询一列rownum)
select rowid, t.deptno
from EMP t
where t.job = 'SALESMAN'
union
select rowid, t.deptno
from EMP t
where t.mgr = 7698;
【13】 内连接尽量使用inner join,避免使用from 表1,表2这种方式
--推荐使用 inner join ,容易看清楚两张表之间的关联关系
select t.empno,
t.ename,
t.job,
t.mgr,
t.hiredate,
t.sal,
t.comm,
t.deptno,
d.dname
from emp t
inner join dept d
on d.deptno = t.deptno
where t.deptno = 10;
--不推荐使用
select e.empno,
e.ename,
e.job,
e.mgr,
e.hiredate,
e.sal,
e.comm,
e.deptno,
d2.dname
from emp e, dept d2
where e.deptno = 10
and e.deptno = d2.deptno;
【14】 inner join / left join / right join / full join区别
- inner join: 只返回左右表相匹配的记录。
- left join: 左表返回全部数据,右表如果没有匹配的记录用null填充。
- right join: 右表返回全部数据,左表如果没有匹配的记录用null填充。
- full join: 左右两张表的数据都返回,只有相匹配的才显示在同一行。
看以下示例:
【inner join】:
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
select L.str, L.val, R.str
from L
inner join R
on L.val = R. val
order by L.val;
另一种写法:
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
select L.str, L.val, R.str from L, R where L.val = R. val order by L.val;
【left join】:
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
select L.str, L.val, R.str
from L
left join R
on L.val = R. val
order by L.val;
【right join】:
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
select L.str, L.val, R.str
from L
right join R
on L.val = R. val
order by L.val;
【full join】:
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
select L.str, L.val, R.str
from L
full join R
on L.val = R. val
order by L.val;
【15】. 自关联
自关联表示的是join表自身,只是连接条件的字段可能不一致。
--查询每个员工对应的主管
select t.empno, t.ename, t2.ename as mgrname
from emp t
left join emp t2
on t.mgr = t2.empno;
【16】 not in / not exists / left join
使用这三个实现统计没有员工的部门信息:
--not in
select *
from dept d
where d.deptno not in
--统计所有有员工的部门编号
(select e.deptno from emp e where e.deptno is not null);
--not exists
select *
from dept d
--不存在 e.deptno = d.deptno的部门信息
where not exists (select * from emp e where e.deptno = d.deptno);
--left join
select d.*
from dept d
left join emp e
on e.deptno = d.deptno
where e.deptno is null
以上三条sql执行结果一致。
至于哪一种效率高一点,可以通过查看分析sql执行计划看出来:
explain plan for select *
from dept d
--不存在 e.deptno = d.deptno的部门信息
where not exists (select * from emp e where e.deptno = d.deptno);
select * from table(dbms_xplan.display());
【17】 外连接条件需要注意放的位置
需要特别注意所加上的条件是要过滤外连接join之后的结果还是先过滤之后再进行外连接join操作。看以下示例:
假设我外连接只需要连接右表val= '3'这条记录,那么如果加条件的位置不对,结果截然不同。如下:
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
--假设右表只需要查找val = 3这条记录
--错误写法
select L.str, L.val, R.str
from L
left join R
on L.val = R. val
--加where等于对join之后的整个结果进行过滤
where R.val = '3'
order by L.val;
如上图,查询结果只有一条,并不是像预期那样:
下面提供两种方法,解决这种外连接加条件错误:
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
--假设右表只需要查找val = 3这条记录
--正确写法:
select L.str, L.val, R.str
from L
left join R
on L.val = R. val
and R.val = '3'
order by L.val;
with L as
(select 'left_1' as str, '1' as val
from dual
union all
select 'left_2' as str, '2' as val
from dual
union all
select 'left_3' as str, '3' as val
from dual
union all
select 'left_4' as str, '4' as val
from dual),
R as
(select 'right_3' as str, '3' as val
from dual
union all
select 'right_4' as str, '4' as val
from dual
union all
select 'right_5' as str, '5' as val
from dual
union all
select 'right_6' as str, '6' as val
from dual)
--假设右表只需要查找val = 3这条记录
--正确写法二:
select L.str, L.val, R.str
from L
--先过滤val='3'之后再join
left join (select * from R where R.val = '3') R
on L.val = R. val
order by L.val;
以上两种方式才是正确的。
【18】聚集与内连接
首先构造测试数据:
with emp_bonus as
(select 7934 as empno, 1 as type
from dual
union all
select 7934 as empno, 2 as type
from dual
union all
select 7839 as empno, 3 as type
from dual
union all
select 7782 as empno, 1 as type
from dual)
select * from emp_bonus;
计算员工对应的奖金,以上是员工对应的奖金等级。
先连接:
with emp_bonus as
(select 7934 as empno, 1 as type
from dual
union all
select 7934 as empno, 2 as type
from dual
union all
select 7839 as empno, 3 as type
from dual
union all
select 7782 as empno, 1 as type
from dual)
select e.deptno,
e.sal,
(e.sal * case
when b.type = 1 then
0.1
when b.type = 2 then
0.2
when b.type = 3 then
0.3
end) as bonus
from emp e
inner join emp_bonus b
on b.empno = e.empno
where e.deptno = 10
然后进行统计:
with emp_bonus as
(select 7934 as empno, 1 as type
from dual
union all
select 7934 as empno, 2 as type
from dual
union all
select 7839 as empno, 3 as type
from dual
union all
select 7782 as empno, 1 as type
from dual)
select e.deptno,
sum(e.sal),
sum(e.sal * case
when b.type = 1 then
0.1
when b.type = 2 then
0.2
when b.type = 3 then
0.3
end) as bonus
from emp e
inner join emp_bonus b
on b.empno = e.empno
where e.deptno = 10
group by e.deptno;
--select sum(e.sal) as total from emp e where e.deptno = 10;
查看上图,发现总工资金额为10050,执行统计总金额sql:
select sum(e.sal) as total from emp e where e.deptno = 10;
我们发现总工资金额对不上,原因在于奖金等级那张表有个员工7934的有两条记录,正确的做法应该是先聚合员工的等级信息再进行连接join操作。
【1】先统计每个员工的奖金等级
with emp_bonus as
(select 7934 as empno, 1 as type
from dual
union all
select 7934 as empno, 2 as type
from dual
union all
select 7839 as empno, 3 as type
from dual
union all
select 7782 as empno, 1 as type
from dual)
select b.empno,
sum(case
when b.type = 1 then
0.1
when b.type = 2 then
0.2
when b.type = 3 then
0.3
end) as rate
from emp_bonus b
group by b.empno;
【2】再进行聚合
select sum(e.sal) as total, sum(e.sal * r.rate) as total_bonus, e.deptno
from emp e
inner join(with emp_bonus as (select 7934 as empno, 1 as type
from dual
union all
select 7934 as empno, 2 as type
from dual
union all
select 7839 as empno, 3 as type
from dual
union all
select 7782 as empno, 1 as type
from dual)
select b.empno,
sum(case
when b.type = 1 then
0.1
when b.type = 2 then
0.2
when b.type = 3 then
0.3
end) as rate
from emp_bonus b
group by b.empno) r
on r.empno = e.empno
where e.deptno = 10
group by e.deptno
这样结果就正确了。
【19】聚集与外连接
如果需要返回各个部门的总工资以及总奖金,根据上面的示例,直接使用left join即可。
select sum(e.sal) as total, sum(e.sal * r.rate) as total_bonus, e.deptno
from emp e
left join(with emp_bonus as (select 7934 as empno, 1 as type
from dual
union all
select 7934 as empno, 2 as type
from dual
union all
select 7839 as empno, 3 as type
from dual
union all
select 7782 as empno, 1 as type
from dual)
select b.empno,
sum(case
when b.type = 1 then
0.1
when b.type = 2 then
0.2
when b.type = 3 then
0.3
end) as rate
from emp_bonus b
group by b.empno) r
on r.empno = e.empno
group by e.deptno
【20】多表查询注意空值问题
示例:返回所有比‘ALLEN’提成低的员工
select e.comm, e.empno, e.ename
from emp e
where e.comm < (select e2.comm from emp e2 where e2.ename = 'ALLEN');
对比所有员工的提成结果
我们发现,上面统计结果明显不对,其中还有一部分员工没有提成,也算比‘ALLEN’提成低的范围,这些记录应该也要被查询出来:我们将提成为空的做一下转换,再进行比较。
select e.comm, e.empno, e.ename
from emp e
where nvl(e.comm, 0) <
(select e2.comm from emp e2 where e2.ename = 'ALLEN');
这样结果就正确了。
另外,一些函数如in(null)会直接返回null,需要注意一下。
三、总结
这只是第一部分总结,下一篇继续总结。积少成多,keeping....