SQL语言艺术(PDF格式)-第8部分
按键盘上方向键 ← 或 → 可快速上下翻页,按键盘上的 Enter 键可回到本书目录页,按键盘上方向键 ↑ 可回到本页顶部!
————未阅读完?加入书签已便下次继续阅读!
中推衍出新的关系。此时,视图可以看作查询的简略表达方式,这是视图最常见的用途之一。
随着查询复杂度的增加,似乎应该把查询拆成一系列独立视图,每个视图代表复杂查询的一部
分。
总结:表明简单的查询背后,可能隐藏着复杂的视图。
…………………………………………………………Page 37……………………………………………………………
不要走极端,完全不使用视图也不合理,一般它们并无坏处。然而,将视图用在复杂查询中时,
我们多半只对视图返回数据中的一小部分感兴趣——可能是几十个字段中的几个字段——这
时,优化器会试图将简单视图重新并入一段更大的查询语句中。但是,一旦查询复杂到一定程
度,此方法就太复杂了,以至于难以保证效率。
在某些情况下,视图的编写方式,能有效地预防优化器把它并入上级语句中。我已提过 rownum,
那是 Oracle 使用的虚拟字段,用来显示记录最初被查到时的顺序。如果在视图中使用rownum,
复杂性会进一步增加。任何想把参照了 rownum 的视图并入上级查询中的尝试,都会改变后续
rownum 的顺序,所以此时不允许优化器改写查询。于是,复杂查询中这种视图将独立执行。
DBMS 优化器常把视图原样并入语句中,把它当成语句执行的一步来运行(注2),而且只使用
视图执行结果中所需要的部分。
视图中执行的操作(典型的例子是通过join获取ID号对应的描述信息),往往与其所属查询的上
下文无关;或者,查询条件很特殊,会淘汰组成视图的一些表。例如,对若干个表进行union
得到的视图,代表了多个子类型,而上级查询的过滤器只针对其中一个子类型,所以unio其实
是不必要的。将“视图”与“视图中出现的表”进行join也有危险,这会强制多次扫描该表并多次访
问相同记录,但其实只扫描一次就足够了。
当视图返回的数据远多于上级查询所需时,放弃使用该视图(或改用一个较简单的视图),通常
可使效率大为改善。首先,用SQL 查询取代主查询中用到的视图。对视图的组成部分有了整体
的了解之后,要去除严格意义上不必要的部分就容易多了。改用较简单视图的效果也不错,从
查询中去除了不必要部分,执行速度快多了。
许多开发者不愿在复杂查询中,再引入复杂的视图,他们认为这会使情况更为复杂。推导与分
解复杂的SQL表达式的确有点令人生畏,不过,和高中时常做的数学表达式推导也差不多。在
我看来,这有助于形成良好的编程风格,值得花些时间去掌握。对于渴望提高编程技巧的开发
者来说,研究上述技巧有利于对查询内部工作原理的深入了解,常常使你受益匪浅。
总结:当视图返回不必要的元素时,别把视图内嵌在查询中,而是应将视图分解,将其组成部
分加到查询主体中。
并发用户数
Number of other Users
最后,在设计 SQL 程序时,并发性(concurrency)是个必须认真对待的因素。写数据库时需
要关注并发性:是否存在数据块访问争用(block…access contention)、阻塞(locking)、或闩
定(latching)(DBMS内部资源阻塞)等重要问题;甚至有时,为保证读取一致性(read
consistency)也会导致某种程度的资源争用。任何服务器的处理能力都是有限的,不管其说明
书有多令人震撼。在机器相同的情况下,很少并发或没有并发操作时设计可能是完美的,但对
有大量并发操作的情况未必完美。排序操作可能没有足够内存可用,于是转而求助于磁盘,引
发了新的资源争用……一些计算密集型(CPUintensive)操作——例如负责复杂计算的函数、
索引区块的重复扫描,均可引起计算机负荷过多。我遇到过一些案例,增加物理 I/O 会使任务
执行效率更高,因为其中计算密集操作的并发执行程度很高,一个进程刚因等待 I/O 而阻塞,
被释放的CPU就被另一个进程占用了,这样一来CPU资源就被充分利用了。一般而言,我们必
须考虑特定商业任务的整体吞吐量(throughput),而不是个别用户的响应时间(response…time)。
…………………………………………………………Page 38……………………………………………………………
注意
第9章将更详细地探讨并发性。
过滤
Filtering
如何限定结果集是最关键的因素之一,有助于你在编写 SQL 语句时判断该用哪些技巧。用来
过滤数据的所有准则,通常被视为是 where 子句中各种各样的条件,我们必须认真研究各种
where 子句(及 having 子句)。
过滤条件的含义
Meaning of Filtering Conditions
若从SQL语法来看,where子句中表达的所有过滤条件当然大同小异。但事实并非如此。有些
过滤条件通过关系理论直接作用于select 运算符:where子句检查记录中的字段是否与指定条
件相符。但其实,where 子句中的条件还可以使用另一个关系运算符 join。自从 SQL92 出现
join 语法后,人们就试图将“join过滤条件”(位在主 from 子句和 where 子句之间)和“select
过滤条件”(位于where子句内)区分开来。从逻辑上讲,连接两个(或多个)表建立了新的关
系。
下面是个常见的连接(join)的例子:
select 。。。。。
from t1
inner join t2
on t1。join1 = t2。joind2
where 。。。
假设表t2中有一字段c2,该不该把 c2上的条件当作 inner join 的额外条件呢?即是否应认为参
与连接的不是“t2表”而是“t2表的子集”呢?或者,假设where 子句中有一些关于t1 字段的条件,
那么这些条件是否会应用到 t1 连接 t2 的结果呢?连接条件放在何处应该都一样,然而其运
行效率却会因优化器不同而异。
除了连接条件和简单的过滤条件之外,还有其他种类的条件。例如,规定返回的记录集为某种
子类型的条件,以及检查另一个表内是否存在特定数据的条件。虽然从 SQL 语法上看它们相
似,但在语义上却未必完全相同。有时条件的计算顺序无足轻重,但有时却影响重大。
下面的例子说明了条件计算顺序的重要性,实际上,在许多商用软件包中都能找到这样的例子。
假设有个 parameters 表,它包含字段:parameter_name、parameter_type、
parameter_value,无论由parameter_type定义了什么参数属性,其中 parameter_ value 均以
字符串表示。(从逻辑上来说,上述情况堪比罗密欧与茱莉叶的悲剧,因为属性parameter_value
所表示的领域类型非常多,所以违反了关系理论的主要规则。)假设进行如下查询:
select * from parameters
…………………………………………………………Page 39……………………………………………………………
where parameter_name like'%size'
and parameter_type = 'NUMBER'
在这个查询中,无论先计算两个条件中的哪一个,都无关紧要。然而,如果增加了以下条件,
计算的顺序就变得非常重要了,其中int()是将字符转换为整数值的函数:
and int(parameter_value) 》 1000
这时,parameter_type上的条件必须先计算,而parameter_value上的条件后计算,否则会因为
试图把非数字字符串转换为整数,而造成运行时错误(假设 parameter_ type字段的类型定义
为char)。如果你无法向数据库说明这一点,那么优化器也无从知道哪个条件应该有较高的优先
权。
总结:查询条件是有差异的,有的好,有的差。
过滤条件的好坏
Evaluation of Filtering Conditions
编写 SQL 语句时,应首先考虑的问题是:
哪些数据是最终需要的,这些数据来自哪些表?
哪些输入值会传递到 DBMS 引擎?
哪些过滤条件能滤掉不想要的记录?
然而要清楚的是,有些数据(主要是用来连接表的数据)可能冗余地存储在几个表中。所以,
即使所需的返回值是某表的主键,也不代表这个表必须出现在from子句中,这个主键可能会以
外键的形式出现在另一个表中。
在编写查询之前,我们甚至应该对过滤条件进行排序,真正高效的条件(可能有多个,涉到不
同的表)是查询的主要驱动力,低效条件只起辅助作用。那么定义高效过滤条件的准则是什么
呢?首先,要看过滤条件能否尽快减少必须处理的数据量。所以,我们必须倍加关注条件的编
写方式,下面通过例子说明这一点。
蝙蝠车买主
假设有四个表: customers、orders、orderdetail、articles,如图4…5所示。注意,图中各表的
方框大小不同,这代表各表的数据量大小,而不代表字段数量;加下划线的字段为主键。
现在假设 SQL 要处理的问题是:找出最近六个月内居住在Gotham市、订购了蝙蝠车的所有客
户。当然,编写这个查询有多种方法,ANSISQL的推崇者可能写出下列语句:
select distinct c。custname
from customersc
join orders o
on o。custid = c。custid
join orderdetail od
on od。ordid = o。ordid
…………………………………………………………Page 40……………………………………………………………
join articles a
on a。artid = od。artid
where c。city= 'GOTHAM'
and a。artname = 'BATMOBILE'
and o。ordered 》= somefunc
其中,somefunc是个函数,返回距今六个月前的具体日期。注意上面用了distinct,因为考虑到
某个客户可以是大买家,最近订购了好几台蝙蝠车。
暂不考虑优化器将如何改写此查询,我们先看一下这段代码的含义。首先,来自customers表的
数据应只保留城市名为 Gotham 的记录。接着,搜索orders表,这意味着custid字段最好有索
引,否则只有通过排序、合并或扫描orders表建立一个哈希表才能保证查询速度。对orders表,
还要针对订单日期进行过滤:如果优化器比较聪明,它会在连接(join)前先过滤掉一些数据,
从而减少后面要处理的数据量;不太聪明的优化器则可能会先做连接,再作过滤,这时在连接
中指定过滤条件利于提高性能,例如:
join orders o
on o。custid = c。custid
and a。ordered 》= somefunc
即使过滤条件与连接(join)无关,优化器也会受到过滤条件的影响。例如,若orderdetail的主
键为(ordid; artid),即ordid为索引的第一个属性,那么我们可以利用索引找到与订单相关的记
录,就和第3章中讲的一样。但如果主键是(artid; ordid)就太不幸了(注意,就关系理论而言,
无论哪个版本都是完全一样),此时的访问效率比(ordid; artid)作为索引时要差,甚至一些数
据库产品无法使用该索引(注3),唯一的希望就是在 ordid 上加独立索引了。
连接了表 orderdetail和orders之后,来看articles表,这不会有问题,因为表 orderdetail 主键
包括 artid字段。最后,检查 articles 中的值是否为Batmobile。查询就这样结束了吗?未必结
束,因为用了distinct ,通过层层筛选的客户名还必须要排序,以剔除重复项目。
分析至此,可以看出这个查询有多种编写方式。下面的语句采用了古老的join语法:
select distinct c。custname
from customersc;
orders o;
orderdetail od;
articles a
where c。city= 'GOTHAM'
and c。custid = o。custid
and o。ordid = od。ordid
and od。artid = a。artid
and a。artname = 'BATMOBILE'
and o。ordered 》= somefunc
本性难移,我偏爱这种较古老的方式。原因只有一个:从逻辑的角度来看,旧方法突显出数据
处理顺序无足轻重这一事实;无论以什么顺序查询表,返回结果都是一样的。customers 表非
常重要,因为最终所需数据都来自该表,在此例中,其他表只起辅助作用。注意,没有适用于
…………………………………………………………Page 41……………………………………………………………
所有问题的解决方案,表连接的方式会因情况不同而异,而决定连接方式取决于待处理数据的
特点。
特定的SQL查询解决特定的问题,而未必适用于另一些问题。这就像药,它能治好这个病人,
却能将另一个病人医死。
蝙蝠车买主的进一步讨论
下面看看查询蝙蝠车买家的其他方法。我认为,避免在最高层使用distinct应该是一条基本规则。
原因在于,即使我们遗漏了连接的某个条件,distinct也会使查询“看似正确”地执行——无可否
认,较旧的SQL语法在此方面问题较大,但ANSI/SQL92 在通过多个字段进行表的连接时也可
能出现问题。发现重复数据容易,但发现数据不准确很难,所以避免在最高层使用distinct应该
是一条基本规则。
发现结果不正确更难,这很容易证明。前面使用 distinct 返回客户名的两个查询,都可能返回
不正确结果。例如,如果恰巧有多位客户都叫“Wayne”,distinct不但会剔除由同个客户的多张
订单产