首页 > 代码库 > 08 高效的SQL
08 高效的SQL
编写高效 SQL 需要以下知识
有关所查询内容的物理组织的知识
数据库能做什么的知识, 例如: 如果你不知道跳跃扫描索引及其用途, 那么你可能会看着模式说”索引丢了”
SQL 所有错综复杂的知识
对目标的真实理解-问题是什么
访问方法
全扫描,
各种类型的索引访问
通过散列 或 ROWIN 方式直接访问. 类似 3PL语言中的GOTO语句.
全扫描
全扫描是读取 ORACLE 的大量数据行的行之有效的方法, 因为数据库使用多块读取, 需要记住, 全扫描并不邪恶, 在很多情况下, 全扫描是获得结果的最快方法.
全扫描与高水位的关系
ROWID 访问
ROWID 是数据的物理地址, 一个ROWID包含关于文件, 块和该块中的行的一行信息. ROWID对于获取一个特定的行来说可能是最快的方法. 但是, 使用ROWID获取成千上万行不是最好的办法. 通常, ROWID用于访问经过某种排序索引扫描后的表, 另外, 还可以直接使用 ROWID, 例如:
select *
from wwc_people_people where ROWID IN (select row_id from wwc_fast_people where staring like :bv and rownum < 101);
另外, 还可以使用 between and, 例如:
select * from t where ROWID between :a1 and :a2
索引扫描
是最常用的访问数据的方式
B*树结构 : 该书中的最低级的块(成为叶子结点)包含了每个索引键值和一个指向正在索引的行的 ROWID(换句话说, 通过索引键值, 找到ROWID, 在通过ROWID去找真正的行), 有趣的是, 索引的叶子结点实际上是一个双向链表, 这样, 要执行区间扫描就很容易, 直接找这个结点的前边或者后边就可以了.
有两个经常发生的主索引扫描: 索引唯一扫描和索引范围扫描 , 索引跳跃扫描, 索引全扫描, 快速全扫描, 索引连接
索引唯一扫描(Index-unique scans), 优化程序知道索引中索引列是唯一的, 从索引查找中至多返回一行.
索引范围扫描, 对于索引范围扫描, 优化器知道可能返回 0行, 1行或多行. 例如 select max(empno) from emp; 这个查询, oracle 不是将全表扫描一遍然后查出最大的, 而是直接通过索引的后面开始读取最后(最高)的值. (因为一般情况下索引是按照从低到高存储的)
索引跳跃扫描, 通常, 为了使用一个索引, 定义在索引前面的列要在查询中引用, 比如: create table t(a, b, c, d, e, f, g); create index t_idx on t (a, b, c);
通常我们可以使用 select * from t where a = :a; select * from t where a = :a and b = :b; 但是下列查询不能使用该索引:
select * from t where b = :b and c = :c; (因为没有从 a 开始)
oracle 8i 已前, 我们可以使用提示 select /*+ index(t t_idx) */ * from t; 不过, 从 oracle 9i 以后,
oracle 9i 中实现了索引跳跃扫描的概念, 它将在以下条件中使用:
- 在谓词中引用索引里的列(例如 上边的 b 和 c)
- 优化程序知道该索引前面的列很少有不连续的值
索引全扫描, 就是说, 首先通过索引结构找到第一个叶子块, 然后由于所有的叶子块组成了一个双向链表, 接下来, 可以通过这个链表的指针关系, 来找到下一个块.
索引快速全扫描, 与索引全扫描有明显不同, 索引快速全扫描有以下特征:
- 它读取索引结构中的每个块, 包括所有的内部分枝块
- 它采用多块读取, 就像表的全扫描一样.
- 它不按排序顺序检索数据
索引连接, 是在表中存在多个索引时针对某个查询所选中的索引路径, 该索引中应该包含了查询中选中的全部列(不需要访问表)
连接概念
嵌套循环连接, 数据库的主要连接方法.
select ename, dname from emp, dept where emp.deptno = dept.deptno
执行计划显示: NESTED LOOPS, 以上语句的执行方法是, 伪代码如下:
for x in (select * from emp)
loop
index lookup ROWID for x.deptno -- 找到每一个deptno 的 row id
select * from dept where rowid = that rowid
output joined record
end loop
执行步骤:
1) 读取 emp 表的第一块.
2) 取出第一行并使用其 DEPTNO 值
3) 至多读取 两个或三个在索引中查找到键值的索引块.
4) 读取DEPT表的引用块.
有嵌套循环的外连接, 执行计划显示: NESTED LOOPS (OUTER)
散列连接, 执行计划显示 hash join
例如:
select t1.object_name, t2.object_name from t t1, t t2
where t1.object_id = t2.object_id and t1.owner=’WMSYS’;
执行计划显示:
在最佳条件下, oracle 将取两个表中较小的, 在本例中, 将取全扫描T1后的 t1.owner=’WMSYS’的结果集-并创建此结果的一个内存中的散列表, 散列表时一个阵列, 与散列键相随的行存储在该索引的项中. 因此, oracle现在有一个可以快速的进行访问的内存结构-阵列, 如果现在从 T2中获取一个 OBJECT_ID值, oracle可以散列它并再内存中检索T1中相匹配的行. 由于此散列表在私有内存中, 访问此数据结构不会招致一般逻辑I/O中的闩锁活动. 一旦 oracle 将较小的表散列到内存中, 则它全扫描较大的表(本例中的T2), 然后对每一行, 它散列检索到的 object_id, 查找散列表中相匹配的行, 返回连接映像. 客户应用系统在得到第一行前需要等待oracle完全扫描T1并进行散列, 然后其他行的获取速度越来越快.
如果是外连接, 首先执行计划显示 hash join (OUTER), 另外, 外连接时, 先针对主表进行散列, 并存在内存中.
通常, 散列连接擅长两个一大一小结果集.
排序合并连接, 执行计划显示, Sort-Merge Join
排序合并连接, 将依次排序第一个输入集, 排序第2个输入集, 然后合并结果.
通常, 排序-合并连接在非相等连接操作中有用: 即, 在连接条件不是一个等式而是范围比较时(比如, <, >=)
例如:
select a.ename, b.ename, a.hiredate, b.hiredate from emp a , emp b
where a.hiredate <= b.hiredate and a.empno <> b.empno;
笛卡尔连接, 两个表没有连接条件的全连接, 比如第一个表有10行, 第2个表有10行, 连接后, 10*10=100 行
反连接(Anti-join) 反连接用于从一个表中返回不在另一个数据源中的数据行, 例如:
select * from dept where deptno not in (select deptno from emp)
测试 not in 问题:
create table t1
as select * from all_objects where rownum <= 10000;
create table t2
as select * from all_objects where rownum <= 9950;
create index t2_idx on t2(object_id);
1) not in
select count(*) from scott.t1 rbo
where object_id not in (select object_id from scott.t2);
2) not exists
select count(*) from scott.t1 rbo where not exists (select null from scott.t2 where t2.object_id = rbo.object_id);
3) 外连接
select count(*) from t1 , t2 rbo where t1.object_id = rbo.object_id(+) and rbo.object_id is NULL
在 CBO 模式下:
not exist 方法最耗时, 逻辑I/O最多
外连接 居中
not in 方法最好
在 RBO 模式下:
not in 方法最不好
外连接 居中
not exist 最好
综上,
1)首先外连接的方法实现反连接, 因为在 RBO和CBO, 性能都可以
2) 在CBO下, 可以使用 not in , 所以, 在使用 not in 时, 使用 hint 语句强制采用 CBO 优化计划.
另外, 在实现反连接时候, 一定要注意参考数据源中出现 null 的情况, 如果数据源字段没有 not null限制, 则应该使用 is not null 条件来限制:
真正理解SQL
with players as
( select ‘P‘ || ROWNUM username
from all_objects
where rownum <= 8),
weeks as
(select rownum week
from all_objects
where rownum <= 7)
select week,
max(decode(rn, 1, username, null)) u1,
max(decode(rn, 2, username, null)) u2,
max(decode(rn, 3, username, null)) u3,
max(decode(rn, 4, username, null)) u4,
max(decode(rn, 5, username, null)) u5,
max(decode(rn, 6, username, null)) u6,
max(decode(rn, 7, username, null)) u7,
max(decode(rn, 8, username, null)) u8
from ( select username,
week,
row_number() over (partition by week order by rnd) rn
from (select username, week, dbms_random.random rnd from players, weeks)
)
group by week;
分析函数, 如上蓝色部分
前边一个函数, over (区域, 空白表示全部区域) 别名
08 高效的SQL