友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
SQL语言艺术(PDF格式)-第9部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
where (condition on E2)) E
where (joins and other conditions)
另一个“查询条件用错了地方”的经典例子,和在含有 group by 子句的查询中进行过滤操作有
关。你可以过滤分了组的字段,也可以过滤聚合(aggregate)结果(例如检查 count() 的结果
是否小于某阈值),或者同时过滤两者;SQL 允许在 having 子句中使用这类条件,但应该在
group by 完成后才进行过滤(比如排序之后再进行聚合操作)。任何影响聚合函数(aggregate
…………………………………………………………Page 46……………………………………………………………
function)结果的条件都应放在 having 子句中,因为在 group by 之前无从知道聚合函数的结
果。任何与聚合无关的条件都应放在 where 子句中,从而减少为进行group by而必须执行的排
序操作所处理的数据量。
现在回过头来看客户与订单的例子,我承认先前处理订单的方法比较复杂。在订单完成之前,
必须经历几个阶段,这些都记录在表orderstatus中,该表的主要字段有:ordid(订单ID)、status、
statusdate(时间戳)等,主键由ordid和statusdate组成。我们的需求是列出所有尚未标记为完
成状态的订单(假设所有交易都已终止)的下列字段:订单号、客户名、订单的最后状态,以
及设置状态的时间。最终,我们写出下列查询,滤掉已完成的订单,并找出订单当前状态:
select c。custname; o。ordid; os。status; os。statusdate
from customers c;
orders o;
orderstatus os
where o。ordid = os。ordid
and not exists (select null
from orderstatus os2
where os2。status = 'PLETE'
and os2。ordid = o。ordid)
and os。statusdate = (select max(statusdate)
from orderstatus os3
where os3。ordid = o。ordid)
and o。custid = c。custid
乍一看,这个查询很合理,但事实上,它让人非常担心。首先,上面代码中有两个子查询,但
它们嵌入的方式和前一个例子的方式不同,它们只是彼此间接相关的。最让人担心的是,这两
个子查询访问相同的表,而且该表在外层已经被访问过。我们编写的过滤条件质量如何呢?因
为只检查了订单是否完成,所以它不是非常精确。
这个查询如何执行的呢?很显然,可以扫描 orders 表,检查每一条订单记录是否为已完成状
态——注意,仅通过表 orders 即可找出所要信息似乎令人高兴,但实际情况并非如此,因为
只有上述活动之后,才能检查最新状态的日期,即必须按照子查询编写的顺序来执行。
上述两个子查询是关联子查询,这很不好。因为必须要扫描 orders 表,这意味着我们必须检
查 orders 的每条订单记录状态是否为 “PLETE”,虽然检查状态的子查询执行很快,但多
次重复执行就不那么快了。而且,若第一个子查询没找到 “PLETE” 状态时,还必须执行
第二个子查询。那么,何不试试非关联子查询呢?
要编写非关联子查询,最简单的办法是在第二个子查询上做文章。事实上,在某些 SQL 方言
中,我们可以这么写:
and (o。ordid; os。statusdate) = (select ordid; max(statusdate)
from orderstatus
group by ordid)
这个子查询会对 orderestatus 作“全扫描”,但未必是坏事,下面会对此加以解释。
重写的子查询条件中,等号左端的“字段对”有点别扭,因为这两个字段来自不同的表,其实不
必这样。我们想让orders和orderstatus的订单ID相等,但优化器能感知这一点吗?答案是不一
…………………………………………………………Page 47……………………………………………………………
定。所以优化器可能依然先执行子查询,依然要把orders和orderstatus这两个表连接起来。我
们应该将查询稍加修改,使优化器更容易明白我们的描述,最终按照“先获得子查询的结果,然
后再连接orders和orderstatus表”的顺序工作:
and (os。ordid; os。statusdate) = (select ordid; max(statusdate)
from orderstatus
group byordid)
这次,等号左端的字段来自相同的表,从而不必连接orders和orderstatus这两个表了。尽管好
的优化器可能会帮我们做到这一点,但保险起见,一开始就指定这两个字段来自相同的表是更
明智的选择。为优化器保留最大的自由度总是上策。
前面已经看到了,非关联子查询可以变成内嵌视图,且改动不大。下面,我们写出“列出待办订
单”的整个查询语句:
select c。custname; o。ordid; os。status; os。statusdate
from customersc;
orders o;
orderstatus os;
(select ordid; max(statusdate) laststatusdate
from orderstatus
group byordid) x
where o。ordid = os。ordid
and not exists (select null
from orderstatus os2
where os2。status = 'PLETE'
and os2。ordid = o。ordid)
and os。statusdate = x。laststatusdate
and os。ordid = x。ordid
and o。custid = c。custid
但还有问题,如果最终状态确实是“PLETE”,我们就没有必要用子查询检查其最新状态了。
内嵌视图能帮我们找出最后状态,无论它是不是“PLETE”。所以我们把查询改为“检查已知
的最新状态”,这个过滤条件非常令人满意:
select c。custname; o。ordid; os。status; os。statusdate
from customers c;
orders o;
orderstatus os;
(select ordid; max(statusdate) laststatusdate
from orderstatus
group byordid) x
where o。ordid = os。ordid
and os。statusdate = x。laststatusdate
and os。ordid = x。ordid
and os。status != 'PLETE'
…………………………………………………………Page 48……………………………………………………………
and o。custid = c。custid
如果进一步利用 OLAP 或SQL 引擎提供的分析功能,还可以避免对orderstatus的重复参照。
不过就此打住,来思考一下我们是如何修改查询的,更重要的是“执行路径(execution path)”
为何。基本上,正常路径是先扫描orders表,接着利用orderstatus表上预计非常高效的索引进
行访问。在最后一版的代码中,我们改用完整扫描orderstatus的方法,这是为了执行group by。
orderstatus中的记录条数一定会比 orders 中的大好几倍,然而,只以要扫描的数据量来看,
估计前者比较小(而且可能小很多),这取决于为每张订单保存了多少信息。
无法确定哪种方法一定更好,这一切都取决于实际数据。补充说明一点,最好别在预期会增大
的表上做全表扫描操作(若能把搜索限制在最近一个月或几个月的数据上则会好些)。不过,最
后一版的代码肯定比第一版的(在where子句用子查询)要好。
在结束“大数据量查询”的话题之前,有个特殊情况值得一提。当查询要返回非常大量的数据时,
该查询很可能不是某个用户坐在电脑前敲入的命令,而是来自于某个批处理操作。即便“预备阶
段”稍长,只要整个处理能达到令人满意的结果,就是可以接受的。当然,不要忘了,无论是不
是预备阶段,都会需要资源——CPU、内存,可能还有临时磁盘空间。即使最基本的查询完全
相同,优化器在返回大量数据时所选择的路径,仍可能会与返回少量数据时完全不同,了解这
一点是有用的。
总结:尽早过滤掉不需要的数据。
取出数据在表中的比例
The Proportions of Retrieved Data
有个典型的说法:当查询返回的记录数超过表中数据总量的 10% 时,就不要使用索引。这种
说法暗示,当(常规)索引的键指向表中不足10%的记录时,它是高效的。正如第3章中所指出
的,这个经验法则建立于许多公司仍对关系数据库有所怀疑的年代,那时,关系数据库一般用
于部门级数据库,包含十万行数据的表就被认为是大型表。与含有五亿行数据的表相比,十万
行的 10% 不值一提。所以,执行计划“佳者恒佳”仅是个美好的愿望罢了。
就算不考虑“10%的记录”这条“经验法则(rule of thumb)”产生的年代(现在的表大小早已今非
昔比了),要知道,返回的记录数除了与期望响应时间有关之外,它本身并无意义。例如,计算
十亿行数据的某字段的平均值,虽然返回结果只有一行,但DBMS 要做大量工作。甚至没有任
何聚合处理,DBMS要访问的数据页的数量也会造成影响。因为要访问的数据页并非只依赖索
引:第3章曾指出,表中记录的物理顺序与索引顺序是否一致,对要访问的页数有极大影响;第
5章将讨论的一些物理实现也会造成影响,由于数据的物理存储方式不同,检索出相同数量的记
…………………………………………………………Page 49……………………………………………………………
录所要访问的数据页数量可能差异很大;此外,有的访问路径将以串行方式执行,有的则以大
规模并行(parallelized)方式执行……。因此,再别拿“10%的记录”这根鸡毛当令箭了。
总结:当查询的结果集很大时,索引未必必要。
SQL “ ”
SSQQLL语句为了返回结果集或更改数据,必须访问一定数量的数据。““战斗””的环境和条件,决定
“ ” 4 “ ”
了我们““进攻””那些数据的方法。就如第44章所讨论的,““进攻””取决于:结果集的数据量、必须
“ ”
访问的数据量、可动用的““部队””(过滤条件)。
任何大型的、复杂的查询,都可以被分成一连串较简单的步骤,其中一些步骤可以并行执行,
就像综合战役通常要面对敌军的不同部队。每次战斗的结果差异可能很大,但关键是最后的综
合结果。
当我们分析查询的每个步骤时可能不会深入执行细节,但这些步骤可能的组合数量跟国际象棋
不相上下,可以非常复杂。
本章讨论存取经过适当规范化的数据时,经常遇到的情况。虽然本章主要讨论查询,但也适用
where
于更新和删除操作,只要它们也有wwhheerree 子句,毕竟要先读取数据才能修改数据。无论是单纯
为了查询、还是更新或删除记录,过滤数据会遇到的最典型情况有九种:
小结果集,源表较少,查询条件直接针对源表
小结果集,查询条件涉及源表之外的表
小结果集,多个宽泛条件,结果取交集
小结果集,一个源表,查询条件宽泛且涉及多个源表之外的表
大结果集
结果集来自基于一个表的自连接
结果集以聚合函数为基础获得
结果集通过简单搜索或基于日期的范围搜索获得
结果集和别的数据存在与否有关
本章将依次讨论上述各种情况。至于例子,有的简单明了,有的较为复杂(来自实际案例)。
虽然案例大小存在差异,但解决问题的模式是相通的。
通常,在执行查询时,应过滤掉所有不属于结果集的数据,这意味着应尽量采用最高效的搜索
4
条件。决定先执行哪个条件,通常是优化器的工作。但是,正如第44章所述,优化器必须考虑
—— “ ”
大量不同情况————例如表的物理结构、查询编写方式等,所以优化器未必总能““理解正确””。因
此,提高性能还有很多事情可做,下面对九种模式的讨论中,每种模式均是如此。
小结果集,直接条件
Small Result Set; Direct SpecificCriteria
对于典型的在线交易处理,多为返回小结果集的查询,源表数量较少,查询条件也是“直接”针
对源表的。当我们要通过一组条件查询出少许记录时,首先要注意的就是索引。
一般而言,通过一个表或通过两个表的连接查询较少记录,只要确保查询有适当的索引支持即
…………………………………………………………Page 50……………………………………………………………
可。然而,当很多表连接在一起,并且查询条件要参照不同的表时(例如 TA 和 TB),会面临
连接顺序的问题。连接顺序的选择,取决于如何更快地过滤不想要的记录。如果统计数据足够
精确地反映了表的内容,优化器有可能对连接顺序做出适当选择。
当查询仅返回少量记录,且过滤条件直接针对源表时,我们必须保证这些过滤条件高效;对于
非常重要的条件,必须事先为相应字段加上索引,以便查询时使用。
索引可用性
Index Usability
如第3章所述,对某字段使用函数时,则该字段上的索引并不能起作用。当然,你可以建立函数
索引(functional index),这意味着要对函数的结果加索引,而不是为字段加索引。
注意,“函数调用”不光是指“显式函数调用”。如果你将某类型的字段与一个不同类型的字段或
常量进行比较,则DBMS会执行“隐式类型转换”(隐式调用一个转换函数),如你所料,这会对
性能造成影响。
一旦确定重要的搜索条件上有索引,而查询编写方式也的确能因索引而提高性能,我们还须进
一步区别如下两种情况:
使用唯一性索引(unique index)检索单条记录
非唯一性索引(non…unique index)或基于唯一性索引的范围扫描(range scan)
查询的效率与索引的使用
QueryEfficiency andIndex Usage
需要连接(join)表时,唯一性索引非常有用。然而,当程序获得的原始输入(primitive input)
不是查询语句需要的主键值时,必须通过编程来解决转换问题。
这里的“原始输入”指程序接受的数据,可能由使用者输入,也可能从文件中读入。如果查询语
句需要的主键值本身,就是根据原始输入利用另一个查询所获得的结果,则说明设计不合理。
因为这意味着一个查询的输出被用作另一个查询的输入,应该考虑合并这两个查询。
总结:优秀的查询未必来自优秀的程序。
数据散布
Data Disper
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!