下来,他会做类似的工作,并再一次检查错误代码:色le9um,expiry_date,9ts9herecustomer_id=provided_id之后,他才会处理金融交易。相反,熟练的开者更喜欢像下面这样编写代码假设today返当前日期:updateaounts色tbalan99t9apur9dcredit_limit=pur9dexpiry_datetodayandcustomer_id=provided_idand9um=provided_9um接着,检查被更新的行数。如果结果为0,只需执行下面的一个操作即可判断出错原因:色le9um,aexpiry_date,a9cefromcustomers9tsaonacustomer_id=9da9um=provided_9um9hereustomer_id=provided_id如果此查询没有返回数据,则可断定customer_id的值是错的;如果9ull,则可断定卡号是错的;等等。其实,多数情况下此查询无需被执行。注意你是否注意到,上述第一段代码中使用了9t被误用于存在性检测的绝佳例子。“进攻式编程”的本质特征是:以合理的可能性reasonableproba逼lities为基础。例如,检查

-----------------------page25-----------------------

客户是否存在是毫无意义的——因为既然该客户不存在,那么他的记录根本就不在数据库中!所以,应该先假设没有事情会出错;但如果出错了,就在出错的地方而且只在那个地方采取相应措施。有趣的是,这种方法很像一些数据库系统中采用的“乐观并控制optimisti9trol”,后者会假设update冲突不会生,只在冲突真的生时才进行控制处理。结果,乐观方法比悲观方法的吞吐量高得多。总结:以概论为基础进行编程。假设最可能的结果;不是的确必要,不要采用异常捕捉的处理方式。sql简洁的ssqqllsuinctsql熟练的开者使用尽可能少的sql语句完成尽可能多的事情。相反,拙劣的开者则倾向于严格遵循已制订好的各功能步骤,下面是个真实的例子:--getthestartoftheaountingperiod色le9todtperstafromtperrslt9herefiscal_year=to_charparam_dta,'yyyy'andrslt_period='1'||to_charparam_dta,'mm';--gettheendoftheperiodoutofclosure色le9todtperclosurefromtperrslt9herefiscal_year=to_charparam_dta,'yyyy'andrslt_period='9'||to_charparam_dta,'mm';就算度可以接受,这也是段极糟的代码。很不幸,性能专家经常遇到这种糟糕的代码。既然两个值来自于同一表,为什么要分别用两个不同的语句呢?下面用oracle的bulkcollect子句,一次性将两个值放到数组中,这很容易实现,关键在于对rslt_period进行orderby操作,如下所示:色lectclosure_datebulk9todtperstaarrayfromtperrslt9herefiscal_year=to_charparam_dta,'yyyy'andrslt_periodin'1'||to_charparam_dta,'mm','9'||to_charparam_dta,'mm'orderbyrslt_period;

-----------------------page26-----------------------

于是,这两个日期被分别保存在数组的第一个和第二个位置。其中,bulkcollect是plsql语言特有的,但任何支持显式或隐式数组提取的语言都可如法炮制。其实甚至数组都是不必要的,用以下的小技巧注6,这两个值就可以被提取到两个变量中:色lectmaxdecodesubstrrslt_period,1,1,--checkthefirstcharacter'1',closure_date,--ifit's'1'returnthedate9e9antto_date'14101066','ddmmyyyy',--other9i色somethingoldmaxdecodesubstrrslt_period,1,1,'9',closure_date,--thedate9e9antto_date'14101066','ddmmyyyy',intodtpersta,dtperclosurefromtperrslt9herefiscal_year=to_charparam_dta,'yyyy'andrslt_periodin'1'||to_charparam_dta,'mm','9'||to_charparam_dta,'mm';在这个例子中,预期返回值为两行数据,所以问题是:如何把原本属于一个字段的两行数据,以一行数据两个字段的方式检索出来正如数组提取的例子一样。为此,我们检查rslt_period字段,两行数据的rslt_period字段有不同值;如果找到需要的记录,就返回要找的日期;否则,就返回一个在任何情况下都远比我们所需日期要早的日期此处选了哈斯丁之役battleofhastings的日期。只要每次取出最大值,就可以确保获得需要的日期。这是个非常实用的技巧,也可以应用在字符或数值数据,第11章会有更详细的说明。总结:sql是声明性语言de9guage,所以设法使你的代码越业务过程的规格说明。sqlssqqll的进攻式编程offensive9g9ithsql一般的建议是进行防御式编程9sively,在开始处理之前先检查所有参数的合法性。但实际上,对数据库编程而言,尽量同时做几件事情的进攻式编程有切实的优势。有个很好的例子:进行一连串检查,每当其中一个检查所要求的条件不符时就产生异常。信用

-----------------------page27-----------------------

卡付款的处理中就涉及类似步骤。例如,检查所提交的客户身份和卡号是否有效,以及两者是否匹配;检查信用卡是否过期;最后,检查当前的支付额是否过了信用额度。如果通过了所有检查,支付操作才继续进行。为了完成上述功能,不熟练的开者会写出下列语句,并检查其返回结果:色le9tfromcustomers9herecustomer_id=provided_id接下来,他会做类似的工作,并再一次检查错误代码:色le9um,expiry_date,credit_limitfro毛unts9herecustomer_id=provided_id之后,他才会处理金融交易。相反,熟练的开者更喜欢像下面这样编写代码假设today返当前日期:updateaounts色tbalan99t9apur9dcredit_limit=pur9dexpiry_datetodayandcustomer_id=provided_idand9um=provided_9um接着,检查被更新的行数。如果结果为0,只需执行下面的一个操作即可判断出错原因:色le9um,aexpiry_date,a9cefromcustomers9tsaonacustomer_id=9da9um=provided_9um9hereustomer_id=provided_id如果此查询没有返回数据,则可断定customer_id的值是错的;如果9ull,则可断定卡号是错的;等等。其实,多数情况下此查询无需被执行。注意你是否注意到,上述第一段代码中使用了9t被误用于存在性检测的绝

-----------------------page-----------------------

佳例子。“进攻式编程”的本质特征是:以合理的可能性reasonableproba逼lities为基础。例如,检查客户是否存在是毫无意义的——因为既然该客户不存在,那么他的记录根本就不在数据库中!所以,应该先假设没有事情会出错;但如果出错了,就在出错的地方而且只在那个地方采取相应措施。有趣的是,这种方法很像一些数据库系统中采用的“乐观并控制optimisti9trol”,后者会假设update冲突不会生,只在冲突真的生时才进行控制处理。结果,乐观方法比悲观方法的吞吐量高得多。总结:以概论为基础进行编程。假设最可能的结果;不是的确必要,不要采用异常捕捉的处理方式。ex9s精明地使用异常eexxeeppttiioonnssdis9s勇敢与鲁莽的界线很模糊,我建议进攻式编程,但并不是要你模仿轻步兵旅在balaclava的自杀性冲锋注7。针对异常编程,最终可能落得虚张声势的愚蠢结果,但自负的开者还是对它“推崇备至goforit”,并坚信检查和处理异常能使他们完成任务。正如其名字所暗示的,异常应该是那些例外情况。对数据库编程的具体情况而言,不是所有异常都要求同样的处理方式——这是理解异常的使用是否明智的关键点。有些是“好”异常,应预先抛出;有些是“坏”异常,仅当真正的灾害生时才抛出。例如,以主键为条件进行查询时,如果没有结果返回则开销极少,因为只需检查索引即可判断。然而,如果查询无法使用索引,就必须搜索整个表——当此表数据量很大,所在机器又正在接近满负荷工作时,可能造成灾难。有些异常的处理代价高昂,即使是在最佳情况下也不例外,例如重复键dupli9iqueness”如何保证呢?我们几乎总是建立一个唯一性索引,每次向该索引增加一个键时,都要检查是否违反了该唯一性索引的约束。然而,建立索引项需要记录物理地址,于是就要求先将记录插入表,后将索引项插入索引。如果违反此约束,数据库会取消不完全的插入,并返回违反约束的错误信息。上述这些操作开销巨大。但最大的问题是,整个处理必须围绕个别异常展开,于是我们必须“从个别记录的角度进行思考”,而不是“从数据集出进行思考”,这与关系数据库理论完全背道而驰。多次违反此约束会导致性能严重下降。来看一个oracle的例子。假设在两家公司合并后,电子邮件地址定为的标准格式,最多12个字符,所有空格或引号以下划线代替。如果新的employee表已经建好,并包含3000条从employee_old表中提取并进行标准化处理的电子邮件地址。我们希望每个员工的电子邮件地址具有唯一性,于是fernandolopez的地址为flopez,而franciscolopez的地址为flopez2。实际上,我们实际测试的数据中有33个潜在的重复项,所以我们需要做如下测试:sqlin色rtintoemployee色mp_num,emp_name,emp_firstname,emp_email2色le9um,

-----------------------page29-----------------------

3emp_name,4emp_firstname,5substrsubstremp_firstname,1,16||translateemp_name,'''','__',1,127fromemployees_old;in色rtintoemployee色mp_num,emp_name,emp_firstname,emp_emailerroratline1:ora-00001:unique9temp_email_uqviolatedelap色d:00:00:00853000条数据中重复33条,比率大约是1%,所以,或许可以心安理得地处理符合标准的99%,并用异常来处理其余部分。毕竟,1%的不符标准数据带来的异常处理开销应该不大。但这个异常处理的开销到底在哪里呢?让我们先从测试数据中剔除“问题记录”,然后再执行相同的测试,比较现:这次测试的总运行时间,与上次几乎相同,都是18秒。然而,从测试数据中剔除“问题记录”之后再执行前面第一段in色rt色lect语句时,度明显比循环快:最终现采用“一次处理一行”的方式导致耗时增加了近50%。那么,在此例中可以不用“一次处理一行”的方式吗?可以,但要先避免使用异常。正是这个通过异常处理解决“问题记录”问题决定,迫使我们采用循序方式的。另外,由于生冲突的电子邮件地址可能不止一个,可以为它们指定某个数字获得唯一性。很容易判断有多少个数据记录生了冲突,增加一个g肉pby子句就可以了。但在分配数字时,如果不使用主数据库系统的分析功能,恐怕比较困难。ora9alyti9,db2则称在线分析处理onlineanalyti9g,olap,sql色rver称之为排名功能rankingfun9。纯粹从sql角度来看,探索此问题的解决方案很有意义。重复的电子邮件地址都可以被赋予一个具唯一性的数字:1赋给年纪最大的员工,2赋给年纪次之的的员工……依次类推。为此,可以编写一个子查询,如果是g肉p中的第一个电子邮件地址就不作操作,而该g肉p中的后续电子邮件地址则加上序号。代码如下:sqlin色rtintoemployee色mp_num,emp_firstname,2emp_name,emp_email3色le9um,4emp_firstname,5emp_name,6de9,1,emp_email,7substremp_email,

-----------------------page30-----------------------

81,12-lengthltrimto_99||ltrimto_910from色le9um,11emp_firstname,12emp_name,13substrsubstremp_firstname,1,114||translateemp_name,'''','__',1,1215emp_email,16ro9_number17overpartitionby18substrsubstremp_firstname,1,119||translateemp_name,'''','__',1,1220orderbyemp_numrn21fromemployees_old223000ro9ap色d:00:00:1168上面的代码避免了一次一行的处理,而且该解决方案的执行时间仅是先前方案的60%。总结:异常处理会迫使我们采用过程式逻辑。应始终使用声明式sql,尽量预测可能的异常情况。sqlssqqll的本质本章我们将深入讨论sql查询,并研究如何根据不同情况的具体要求,来编写sql语句。我们会分析复杂的sql查询语句,将它们拆解成小的语句片断,并讲解这些语句片断如何共同促成了最终查询结果的产生。sqlssqqll的本质thenatureofsql在深入讨论如何编写sql查询之前,我们有必要先了解一些sql自身的基本特性:sql与数据库引擎databa色engine和优化器optimizer是什么关系?哪些因素可能限制优化效率?sqlssqqll与数据库sqlanddataba色s关系数据库的出现,要归功于efcodd的关系理论开创性研究成果。codd的研究成果为数据

-----------------------page31-----------------------

库学科了坚实的数学基础——而在此之前的很长一个时期数据库学科主要是凭经验。这和造桥的历史很相似:几千年前我们就开始建造跨江大桥,但是由于当时的营造商并不完全了解造桥材料和桥梁强度之间的关系,桥梁的设计往往会大大出实际的要求;后来土木工程学的材料强度理论完善了,更先进更安全的桥梁也就随之出现,这表明造桥使用的各种建筑材料得到了充分利用。的确,如今的一些桥梁工程非常浩大,与此类似,现代dbms软件能够处理的数据量之大也是今非昔比了。关系理论之于数据库,正如土木工程学之于桥梁。sql语言、数据库和关系模型三者经常被混淆。数据库的功能主要是存储数据,这些数据符合对现实世界一部分所建立的特定模型。相应地,数据库必须可靠的基础设施infrastructure,无论何时都能够让多个用户使用同一些数据,且在数据被修改时不破坏数据完整性。这要求数据库能够处理来自不同用户的“资源争用9sa9处理过程中遇到机器故障等极端情况下也保持数据一致性。当然,数据库还有很多其他的功能,本书并未涵盖。正如其名,结构化查询语言stru9guage,sql无非是一种语言,虽然它与数据库关系密切。将sql语言和关系数据库等同视之,或者更糟——与关系理论等同视之,都是错误的。这种错误就好比将掌握了电子表软件或文字处理软件视为掌握了“信息技术”。实际上,有些软件产品并非数据库,但它们也支持sql注1。另外,sql在成为标准之前也不得不与诸如rdo或quel等其他语言竞争,这些语言曾被许多理论家认为优于sql。为了解决所谓的“sql问题”,你必须了解两个相关部分:sql查询表达式和数据库优化器。如图4-1所示,这两部分在三个不同区域里协同工作。图的中央是关系理论,这是数学家们尽情挥的区域。简而言之,关系理论支持我们通过一组关系运算符来搜寻满足某些条件的数据,这些关系运算符几乎支持任何基本查询。关键在于,关系理论有严格的数学基础,我们完全可以相信同一结果可由不同的关系表达式来获得,正如在算术中246369完全等于23一样。然而,尽管关系理论有至关重要的理论价值,但一些有重要实践意义的方面它并未涉及,这些方面属于图中所示的“报告需求reportingrequirements”的范围。其中最明显的例子就是结果集的排序:关系理论只关心如何根据查询条件取得正确的数据集;而对我们这些实践者而非理论家而言,关系操作阶段只负责准确无误地找出属于最终数据集的记录,而不同行的相同字段的关系并不是在这个阶段处理,而是完全属于排序操作。另外,关系理论并不涉及各种统计功能例如百分位数等,而这些统计功能经常出现在不同的“sql方言dialect”当中。关系理论所研究的是集合色t,但并不涉及如何为这些集合排序。尽管有许多关于排序的数学理论,但它们都与关系理论无关。必须说明的是,关系操作与上述“报告需求”的不同在于关系操作适用于理论上无限大的、数学意义上的集,无论是操作含有十行数据的表、一万行数据的表、还是一亿行数据的表,我们都能以相同的方式对其施以任何过滤条件。再次强调:当我们只关心找出并返回符合查询条件的数据时,关系理论是完全适用的;然而,当我们需要进行记录排序,或者执行一个大多数人错误地认为它是关系操作的g肉p操作时,却已不再是针对可以无限大的数据集进行操作了,而必须是一个有限数据集,于是这个结果数据集不再是数学意义上的“关系relation”了,至此我们已经出了关系操作层。当然,我们仍然可以利用sql对该数据集进行一些有用的操作。初步总结一下,我们可以将sql查询表示为一个两层的操作,如图4-2所示。第一层是一个关系操作的“核”,它负责找出我们要操作的数据集;第二层是“非关系操作层non-relationallayer”,

-----------------------page32-----------------------

它对有限的数据结果集进行“精雕细刻”从而产生用户期望的最终结果。尽管图4-2简要地表达了sql在数据处理环境中的位置,但sql查询在大多数情况下都比这要复杂得多,图4-2仅仅展示了一个总体的描述。关系操作中的过滤器filter有可能只是一个代名词,其背后是几个独立过滤器的组合,例如通过union结构或子查询来实现;最终,sql语句的构成可以很复杂。稍后还会讨论编写sql语句的问题,但我们接下来先要讨论的是数据物理实现和数据库优化器的关系。总结:千万别把sql查询的执行过程中真正的关系操作和附加的展现层pre色ntationlayer功能混为一谈。sqlssqqll与优化器sqlandtheoptimizer当sql引擎处理查询时,会用优化器找出执行查询最高效的方式。此时关系理论又可以大有作为了——优化器借助关系理论,对开者的语义无误的原始查询进行有效的等价变换,即使原始查询编写得相当笨拙。优化是在数据处理真正被执行时生的。经过变换的查询在执行时可能比语义上等效的其他查询快得多,这因是否存在索引,以及变换与查询是否适应而不同。在第5章我们将介绍各种数据存储模型;有时,特定存储模型决定了查询优化的方式。优化器会检查下列因素:定义了哪些索引、数据的物理布局、可用内存大小,以及可用于执行查询任务的处理器数。优化器还很重视查询直接或间接涉及的表和索引的数据量。最终,优化器根据数据库的实际实现情况对理论上等价的不同优化方案做出权衡,产生有可能是最优的查询执行方案。然而,要记住的关键一点是,尽管优化器在sql查询的“非关系操作层”也偶有用途,但以关系理论为支柱的优化器主要用于关系操作层。sql查询的等价变换还提醒我们:sql原本就是一种声明性语言de9guage。换言之,sql应该是用来表达“要做什么”、而非“如何来做”的。理论上讲,从“要做什么”到“如何来做”的任务就是由优化器来完成的。在第1章、第2章中讨论的sql查询比较简单,但即使从编写技巧层面来说,拙劣的查询语句也会影响优化器的效率。切记,关系理论的数学基础为数据处理了非常严谨的逻辑支持,因此sql艺术本应注重减小“非关系操作层”的厚度,即尽量在关系操作层完成大部分处理,否则优化器在“非关系操作层”难以保证返回的结果数据和原始查询执行的结果一样。另外,在执行非关系操作时这里非关系操作不严格地定义为针对已知结果集的操作,应专注于操作那些解决问题所必需的数据,不要画蛇添足。和当前记录不同,有限数据集必须以某种方式进行临时存储内存或硬盘,这会带来惊人的开销。随着结果集数据量的增大,这种开销会急剧加大,尤其是在主存所剩无几的时候。主存不足会引硬盘数据交换等开销很高的活动。而且,别忘了“索引所指的是硬盘地址,并非临时存储地址”,所以数据一旦进行临时存储,就意味着我们向最快的数据访问方式说再见了哈希方式可能例外。一些sql方言会误导用户,使他们认为自己仍在关系世界中——但其实早就不是关系操作了。举个简单的例子:不是经理的员工当中,哪五个人收入最高?这是个现实生活中很合理的问题,

-----------------------page33-----------------------

但它包含了明显的非关系描述。“找出不是经理的员工”是其中的关系操作部分,由此获得一个有限的员工集合,然后排序。有些sql方言通过在色lect语句中增加特殊子句来限制返回的记录数,很显然,排序和限制记录数都是非关系操作。其他sql方言这里主要是指oracle则采用另外的机制,即用一个名为ro9num的虚拟字段dummy9为查询结果编号——这意味着编号工作生在关系操作阶段。如果查询语句如下:色le9ame,salaryfromemployees9herestatus!='exe9um=somefun9c是个函数,返回距今六个月前的具体日期。注意上面用了distinct,因为考虑到某个客户可以是大买家,最近订购了好几台蝙蝠车。暂不考虑优化器将如何改写此查询,我们先看一下这段代码的含义。先,来自customers表的数据应只保留城市名为gotham的记录。接着,搜索orders表,这意味着custid字段最好有索引,否则只有通过排序、合并或扫描orders表建立一个哈希表才能保证查询度。对orders表,还要针对订单日期进行过滤:如果优化器比较聪明,它会在连接join前先过滤掉一些数据,从而减少后面要处理的数据量;不太聪明的优化器则可能会先做连接,再作过滤,这时在连接中指定过滤条件利于提高性能,例如:joinordersoono9daordered=somefun9无关,优化器也会受到过滤条件的影响。例如,若orderdetail的主键为ordid,artid,即ordid为索引的第一个属性,那么我们可以利用索引找到与订单相关的记录,就和第3章中讲的一样。但如果主键是artid,ordid就太不幸了注意,就关系理论而言,无论哪个版本都是完全一样,此时的访问效率比ordid,artid作为索引时要差,甚至一些数据库产品无法使用该索引注3,唯一的希望就是在ordid上加独立索引了。连接了表orderdetail和orders之后,来看articles表,这不会有问题,因为表orderdetail主键包括artid字段。最后,检查articles中的值是否为bat摸逼le。查询就这样结束了吗?未必结束,因为用了distinct,通过层层筛选的客户名还必须要排序,以剔除重复项目。分析至此,可以看出这个查询有多种编写方式。下面的语句采用了古老的join语法:色le9amefromcustomersc,orderso,orderdetailod,articlesa9here9d9doordid=odordidandodartid=aartidandaartname='bat摸逼le'andoordered=somefunc本性难移,我偏爱这种较古老的方式。原因只有一个:从逻辑的角度来看,旧方法突显出数据处理顺序无足轻重这一事实;无论以什么顺序查询表,返回结果都是一样的。customers表非常重要,因为最终所需数据都来自该表,在此例中,其他表只起辅助作用。注意,没有适用于

-----------------------page41-----------------------

所有问题的解决方案,表连接的方式会因情况不同而异,而决定连接方式取决于待处理数据的特点。特定的sql查询解决特定的问题,而未必适用于另一些问题。这就像药,它能治好这个病人,却能将另一个病人医死。蝙蝠车买主的进一步讨论下面看看查询蝙蝠车买家的其他方法。我认为,避免在最高层使用distinct应该是一条基本规则。原因在于,即使我们遗漏了连接的某个条件,distinct也会使查询“看似正确”地执行——无可否认,较旧的sql语法在此方面问题较大,但ansisql92在通过多个字段进行表的连接时也可能出现问题。现重复数据容易,但现数据不准确很难,所以避免在最高层使用distinct应该是一条基本规则。现结果不正确更难,这很容易证明。前面使用distinct返回客户名的两个查询,都可能返回不正确结果。例如,如果恰巧有多位客户都叫“9ayne”,distinct不但会剔除由同个客户的多张订单产生的重复项目,也会剔除由名字相同的不同客户产生的重复项目。事实上,应该同时返回具唯一性的客户id和客户名,以保证得到蝙蝠车买家的完整清单。在实际中,现这个问题可不容易。要摆脱distinct,可考虑以下思路:客户在gohtam市,而且满足存在性测试,即在最近六个月订购过蝙蝠车。注意,多数但非全部sql方言支持以下语法:色le9amefromcustomersc9here9dexists色le9ullfro摸rderso,orderdetailod,arti9ame='bat摸逼le'andaartid=odartidandodordid=oordidando9doordered=somefunc上例的存在性测试,同一个名字可能出现多次,但每个客户只出现一次,不管他有多少订单。有人认为我对ansisql语法的挑剔有点苛刻指“蝙蝠车买主”的例子,因为上面代码中customers表的地位并没有降低。其实,关键区别在于,新查询中customers表是查询结果的唯一来源嵌套的子查询会负责找出客户子集,而先前的查询却用了join。这个嵌套的子查询与外层的色lect关系十分密切。如代码第11行所示粗体部分,子查询参照了外层查询的当前记录,因此,内层子查询就是所谓的关联子查询correlatedsubquery。此类子查询有个弱点,它无法在确定当前客户之前执行。如果优化器不改写此查询,就必须先找出每个客户,然后逐一检查是否满足存在性测试,当来自gotham市的客户非常少时执行效率倒是很高,否则情况会很糟此时,优秀的优化器应尝试其他执行查询的方式。我们还可以这样编写查询:

-----------------------page42-----------------------

色le9amefromcustomers9here9d9色lectocustidfro摸rderso,orderdetailod,arti9ame='bat摸逼le'andaartid=odartidandodordid=oordidandoordered=somefunc在这个例子中,内层查询不再依赖外层查询,它已变成了非关联子查询uncorrelatedsubquery,只须执行一次。很显然,这段代码采用了原有的执行流程。在本节的前一个例子中,必须先搜寻符合地点条件的客户如均来自gotham,接着依次检查各个订单。而现在,订购了蝙蝠车的客户,可以通过内层查询获得。不过,如果更仔细地分析一下,前后两个版本的代码还有些更微妙的差异。含关联子查询的代码中,至关重要的是orders表中的custid字段要有索引,而这对另一段代码并不重要,因为这时要用到的索引如果有的话是表customers的主键索引。你或许注意到,新版的查询中执行了隐式的distinct。的确,由于连接操作,子查询可能会返回有关一个客户的多条记录。但重复项目不会有影响,因为in条件只检查该项目是否出现在子查询返回的列表中,且in不在乎某值在列表中出现了一次还是一百次。但为了一致性,作为整体,应该对子查询和主查询应用相同的规则,也就是在子查询中也加入存在性测试:色le9amefromcustomers9here9d9色lectocustidfro摸rderso9hereoordered=somefun9ullfro摸rderdetailod,arti9ame='bat摸逼le'andaartid=odartidandodordid=oordid或者:

-----------------------page43-----------------------

色le9amefromcustomers9here9d9色lectcustidfro摸rders9hereordered=somefun9色lectodordidfro摸rderdetailod,arti9ame='bat摸逼le'andaartid=odartid尽管嵌套变得更深、也更难懂了,但子查询内应选择exists还是in的选择规则相同:此选择取决于日期与商品条件的有效性。除非过去六个月的生意非常清淡,否则商品名称应为最有效的过滤条件,因此子查询中用in比exists好,这是因为,先找出所有蝙蝠车的订单、再检查销售是否生在最近六个月,比反过来操作要快。如果表orderdetail的artid字段有索引,这个方法会更快,否则,这个聪明巧妙的举措就会黯然失色。注意每当对大量记录做存在性检查时,选择in还是exists须斟酌。利于多数sql方言,非关联子查询可以被改写成from子句中的内嵌视图。然而,一定要记住的是,in会隐式地剔除重复项目,当子查询改写为from子句中的内嵌视图时,必须要显式地消除重复项目。例如:色le9amefromcustomers9here9d9色lectocustidfro摸rderso,色le9ctodordidfro摸rderdetailod,arti9ame='bat摸逼le'andaartid=odartidx9hereoordered=somefun9dxordid=oordid编写功能等价的查询时,不同的编写方式就好像同义词。在书面语和口语中,同义词的意思虽

-----------------------page44-----------------------

然大致相同,但又有细微差异,因此某个词在特定语境中更合适。同样,数据和处理的具体实现细节可以决定选择哪种查询方式。蝙蝠车买主案例总结前面讨论的各段sql语句,看似意义不大的编程技巧练习,实则不然。关键是“擒获attack”数据的方法有很多,不必按照先customers、然后orders、接着orderdetail和articles的方式来编写查询。现在以箭头表示搜索条件的强度——条件分辨力越强,箭头就越大。假设gotham市的客户非常少,但过去六个月的销售业绩不错,卖出了很多蝙蝠车,此时规划图如图4-6所示。虽然商品名称之上有个过滤条件,但图中的中等大小的箭头指向了表orderdetail,因为该表是真正重要的表。待售商品可能很少,反映出销售收入的百分比;也可能待售商品很多,最畅销的商品之一就是蝙蝠车。相反,如果我们假设多数客户在gotham市,但其中很少的客户买了蝙蝠车,则规划图如图4-7所示。很显然,此时表orderdetail是最大的目标。来自这个表的数据的数据量缩减度越快,查询执行得就越快。还要注意的非常重要的一点是,“过去六个月”并不是个非常精确的条件。但如果我们把条件改为过去两个月,而库中有十年的销售记录,会生什么呢?在这种情况下,如果能先访问到近期的订单借助第5章中描述的一些技术,这些数据或许就聚集在一起,查询的效率就会更高些;找出近期订单后,一方面选取gotham的客户,另一方面则选取蝙蝠车订单。所以,换个角度来看,最好的执行计划并不只相依于数据值,还应该随着时间而不断进化。好了,总结一下。先,解决问题的方法不只一种……而且查询的编写方式经常会与数据隐含的假设相关。殊途同归,最终的结果集都是一样的,但执行度可能有极大差异。查询的编写方式会影响执行路径,尤其是应用无法在真