第1章

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

sqlssqqll语言艺术内容介绍本书๰分为12章,每一章包含许多原则或准则,并通过举例的方式对原则ท进行解释说明。这些例子大多来自于实际案例,对九种sql经典查询场景以及其性能影响讨论,非常便于实践,为你的实际工作提出了具体建议。本书适合sql数据库开者、软件架构师,也适合dbaທ,尤其是数据库应用维护人员阅读。资深sql专家stéphanefa肉lt倾力打造《软件架构设计》作者温昱最新译作巧妙借鉴《孙子兵法》的智慧结晶传授25年的sql性能ม与调校经验深入探讨九种常见查询方案及其性能ม前言过去,“信息技术it”的名字还不如今天这般耀眼,被称为“电子数据处理”。其实,尽管当今新潮技术层出不穷,数据处理依然处于我们系统的核心地位,而且需管理的数据量的增长度似乎ๆ比处理器的增长度还快。今天,最重要的集团数据都被保存在数据库中,通过sql语言来访问。sql语言虽有缺点,但非常流行,它从19๗80่年代早ຉ期开始被广泛接受,随后就所向无敌了。如今,年轻开者在接受面试时,没有谁不宣称自己้能熟ງ练应用sql的。sql作为ฦ数据库访问语言,已๐成为ฦ任何基础it课程的必备部分。开者宣传自己熟练掌握sql,其实前๩提是“熟练掌握”的定义是“能够获得功能上正确的结果”。然而,全世界ศ的企业如今都面临ภ数据量的爆炸式增长,所以仅做到“功能正确”是不够的,还必须足够快,所以数据库性能成了许多公司头疼的问题。有趣的是,尽管每个ฐ人都认可性能ม问题๤源自代码,但普遍接受的事实则是开者的要关注点应该是功能ม正确。人们认为:为了便于维护,代码中ณ的数据库访问部分应该尽量简单;“拙劣的sql”应该交给资深的dba去摆弄,他们还会调整几个“有魔力”的数据库参数,于是度就快了——如果数据库还不够快,似乎就该升级硬件了。往往就是这样,那些所谓的“常识”和“可靠方法”最终却是极端有害的。先写低效的代码、后由专家调优,这种做法实际上是自找麻烦。本书认为,先要关注性能的就是开者,而且sql问题绝不仅仅只包含正确编写几个查询这么简单。开者角度看到的性能ม问题和dba从调优角度看到的大相径庭。对dba而言,他尽量从现有的硬件如处理器和存储子系统和特定版本的dbms获得最高性能,他可能ม有些sql技能ม并能调优一个性能极差的sql语句。但对开者而言,他编写的代码可能要运行5到10่年,这些代码将经历一代代的硬件,以及dbຘms各种重要版

-ๅ---ๅ----ๅ-----ๅ--ๅ-ๅ------ๅ-page2---ๅ---ๅ--ๅ-ๅ--ๅ-ๅ---------ๅ--ๅ

本升级例如支持互联网访问、支持网格,不一而足。所以,代码必须从一开始就快、健全。很多开者仅仅是“知道”sql而已,他们没有深刻理解sql及关系理论,实在令人遗憾ย。为ฦ何写作本书sql书主要分为ฦ三种类型:讲授具体sql方แ言的逻辑和语法的书、讲授高级技术及解决问题方法的书、专家与资深dba所需的性能和调优的书。一方面,书๰籍要讲述如何写sql代码;另一方面,要讲如何诊断和修改拙劣的sql代码。在本书๰中,我不再为ฦ新手从头讲解如何写出优秀的sql代码,而是以越单个sql语句的方式看待sql代码,无疑这更加重要。教授语言使用就够难了,那么本书是怎样讲述如何高效使用sql语言的呢?sql的简单性具有欺骗性,它能支持的情况组合的数目几乎是无限的。最初ม,我觉得sql和国际象棋很相似,后来,我悟到明国际象棋是为了教授战争之道。于是,每当出现sql性能难题๤的时候,我都自然而然地将之视为要和一行行数据组成的军队作战。最终,我找到เ了向开者传授如何有效使用数据库的方แ法,这就像教军官如何指挥战争。知识、技能、天赋缺一不可。天赋不能传授,只能ม培养。从写就了《孙子兵法》的孙子到如今的将军,绝大多数战略家都相信这一点,于是他们尽量以简单的格言或规则的方式表达沙场经验,并希望这样能指导真实的战争。我将这种方法用于战争之ใ外的许多领域,本书借鉴了孙子兵法的方แ法和书๰的题目。许多知名it专家冠以科学家称号,而我认为ฦ“艺术”比“科学”更能反映it活动所需的才能、经验和创造力注1。很可能ม是由于我偏爱“艺术”的原因,“科学”派并不赞成我的观点,他们声称每个ฐsql问题都可通过严格分析和参考丰富的经验数据来解决。然而,我不认为这两ä种观点有什么เ不一致。明确的科学方法有助于摆脱单个具体问题的限制,毕竟,sql开必须考虑数据的变化,其中有很大的不确定性。某些表的数据量出乎意料地增长将会如何?同时,用户数量也倍增了,又将会如何?希望数据在线保存好几年将会如何?如此一来,运行在硬件之上的这些程序的行为是否会完全不同?架构级的选择是在赌未来,当然需要明确可靠的理论知识——但这是进一步运用艺术的先决条件。第一次世界ศ大战联军总司令ferdinandfo9checolesupérieuredeguerre的一次演讲中说:战争的艺术和其他艺术一样,有它的历史和原则——否则,就不能ม成其为ฦ艺术。本书不是cookbook,不会列出一串问题๤然后给出“处方”。本书的目标重在帮助开者和他们的经理提出犀利的问题๤。阅读和理解了本书๰之后,你并不是永不再写出丑陋缓慢的查询了——有时这是必须的——但希๶望你是故意而为之ใ、且有充足的理由。目标读者本书的目标读者是:有丰ถ富经验的sql数据库开者他们的经理数据库占重要地位的系统的软件架构师我希望一些dba、尤其是数据库应用维护人员也能喜欢本书。不过,他们不是本书๰的主要目标读者。本书假定

-ๅ--ๅ----ๅ--ๅ--ๅ-ๅ---ๅ------ๅ--page3๑-----ๅ--ๅ-ๅ------ๅ---------ๅ

本书假定你已๐精通sql语言。这里所说的“精通”不是指在你大学里学了sql10่1并拿来a+的成绩,当然也并非指你是国际公认的sql专家,而是指你必须ี具有使用sql开数据库应用的经验、必须考虑索引、必须不把5๓0่00行的表当大表。本书的目标不是讲解连接、外连接、索引的基础知识,阅读本书过程中,如果你觉得某个ฐsql结构还显神秘,并影响了整段代码的理解,可先阅读几本其他sql书。另外,我假定读者至少熟悉一种编程语言,ไ并了解计算机程序设计的基本原则。性能ม已很差、用户已抱怨、你已在解决性能ม问题的前线,这就是本书的假定。本书内容我现sql和战争如此相像,以至于我几乎ๆ沿用了《孙子兵法》的大纲,并保持了大部分题目名称注2๐。本书分为1้2章,每一章包含许多原则ท或准则,并通过举例的方式对原则ท进行解释说明,这些例子大多来自于实际案例。第1้章,制定计划:为ฦ性能ม而设计讨论如何设计高性能数据库第2章,动战争:高效访问数据库解释如何进行程序设计才能ม高效访问数据库第3๑章,战术部ຖ署:建立索引揭示为何建立索ิ引,如何建立索引第4章,机动灵活:思考sql语句解释如何设计sql语句第5章,了如指掌:理解物理实现揭示物理实现如何影响性能第6章,锦囊妙计:认识经典sql模式包括经典的sql模式、以及如何处理第7๕章,变换战术:处理层次结构说明如何处理层次数据第8๖章,孰优孰劣:认识困难,处理困难指出如何认识和处理比较棘手的情况第9章,多条战线:处理并讲解如何处理并第10章,集中兵力:应付大数据量讲解如何应付大数据量第11章,精于计谋:挽救响应时间分享一些技巧,以挽救设计糟糕的数据库的性能第1้2章,明察秋毫:监控性能ม收尾,解释如何定义แ和监控性能本书约定本书使用了如下印刷็惯例:等宽cນourier表示ิsql及编程语言的关键字,或表示ิtaທbຘle、索引或字段的名称,抑或表示函数、代码及命令

----ๅ-------ๅ--ๅ----ๅ--ๅ--ๅ-ๅ-page4---ๅ----------ๅ--ๅ--ๅ----ๅ--ๅ

输出。等宽黑体cນourier表示ิ必须由用户逐字键入的命令等文本。此风格仅用于同时包含输入、输出的代码示例。等宽斜体cນourier表示这些文本,应该被用户的值代替。总结:箴言,概ฐ括重要的sql原则。注意提示、建议、一般性注解。为ฦ相关主题有用的附加信息。代码示ิ例本书๰是为了帮助你完成工ื作的。总的来说,你可以将本书的代码用于你的程序和文档,但是,若要大规模复制代码,则必须联系o'reilly申请授权。例如:编程当中用了本书的几段代码,无需授权;但出售或分o'ูreilly书籍中案例的cd-rom光盘,需要授权。再如:回答问题๤时,引用了本书或其中的代码示例,无需授权;但在你的产品文档中ณ大量使用本书代码,需要授权。o'reilly公司感谢但不强制ๆ归属声明。归属声明通常包括书名、作者、出版商、isbຘn。例如“theartofsqlbຘystéphaທnefaທ肉lt9๗ithpeterrobຘsoncopyrightc200่6o'reillymediaທ,0-596-00่894๒-5๓”。如果你对代码示例的使用出了上述范围,请通过permissions@๤oreilly联系出版商。评论与提问我们已尽力核验本书所的信息,尽管如此,仍不能保证本书完全没有瑕疵,而网络世界ศ的变化之快,也使得本书永不过时的保证成为不可能。如果读者现本书内容上的错误,不管是赘字、错字、语意不清,甚至是技术错误,我们都竭诚虚心接受读者指教。如果您有任何问题,请按照以下的联系方式与我们联系。o'reillymedia,in9high9aທynorth色bastopol,9theusor9๗ternationalorlocaທl707๕829-01้04๒fax致谢本书๰原版用英语写作,英语既ຂ不是我的家乡话,又不是我所在国家的语言,所以写这样一本书要求极度乐观回想起来几近疯狂。幸运的是,peterrobson不仅为本书贡献了他在sql和数据库设计方แ面的知识,也贡献了持续的热情来修改我冗长的句子、调整副词位置、斟酌替换词汇。peterrobson和我在好几个大会上都碰过面,我们都是演讲者。jonathangenni9athangennick是o'reilly出版的sqlpo9athaທn是个非常尊重作者的编辑。由于他的专业、他对细节的关注、他的犀利视角,使本书的质量大大提升。同时,jonathaທn也๣使本书๰的语言更具“中大西洋”风味peter和我现,虽然我们保证按美国英语拼写,但还远远不够。

---------ๅ-------ๅ--ๅ--ๅ---page5--------ๅ----------ๅ--ๅ---

我还要感谢很多人,他们来自三个不同的大陆,阅读了本书全部或部ຖ分草稿并坦诚地提出意见。他们是:philippebຘertolino、ra9il9s、timgorman、jeaທn-ๅpaulmaທrtin、saທnjaທymishra、anthony摸linaro、tiongsoohua。我特别感激laທrry,因为ฦ本书๰的思想最初来自于我们的e-mail讨论。我也๣要感谢o'reilly的许多人,他们使本书๰得以出版。他们是:mar9o、jaທmiepeppard、mikekohnke、ron逼lodeaທu、jessamynread、andre9๗savikaທs。感谢nan9hardt卓越的手稿编辑工作。特别感谢艳n-arzeldurelle-maທrc慷慨第1้2章用到的图片。感谢paທulmcນ9horter授权我们将他的战争图用于第6๔章。最后,感谢rogermaທn色r和steelbusinessiefing的职员,他们为ฦpeter和我了位于伦敦的办公室还有大量咖啡。感谢qianlenaທashley了本书๰开始引用的《孙子兵法》的中文原文。作者介绍stéphanefa肉lt从1983年开始接触关系数据库。oracນle法国成立早期他即加入此前是短暂的ibm经历和渥太华大学任教生涯๹,并在不久之后对性能和调优产生了兴趣。19๗88๖年他离开了oraທcle,此后一年间,他进行调整,并研究过运筹学。之后,他重操旧业,一直从事数据库咨询工作,并于199๗8年创น办了肉gh色a公司肉gh色a。stéphanefa肉lt出版了fortraທnstru9umériques一书法语,dunod出版社ุ,198๖6,ไ与didiersi摸n合作,并在oraທ9e和色lect分别为ฦ英国和北美oracle用户组杂志以及oracນle杂志在线版上表了许多文章。他还是美国、英国、挪威等众多用户组大会的演讲者。peterrobson毕业于达拉谟大学地质专业19๗68๖年,然后在爱丁堡大学任教,并于197๕5年获得地质学研究型硕士学位。在希腊度过了一段地质学家生涯之ใ后,他开始在纽卡斯尔大学专攻地质和医学数据库。他使用数据库始于1้9๗77年,19๗8๖1年开始使用关系数据库,1985年开始使用oracle,这期间担任过开工程师、数据架构师๲、数据库管理员等角色。1้98๖0่年,peter参加了英国地质普查,负责指导使用关系数据库管理系统。他擅长sql系统,以及从组织级到部门级的数据建模。peter多次出席英国、欧洲、北美的oracle数据库大会,在许多数据库专业杂志上表过文章。他现任英国oracນle用户组委员会主任,可通过peterrobson@justsql联系他。查询的识别ี

-ๅ-------ๅ------ๅ---ๅ---ๅ---page6๔-ๅ------ๅ---------ๅ---ๅ-ๅ--ๅ-

有经验的朋友都知道,把关键系统从开环境切换到生产环境是一场战役,一场甚嚣尘上的战役。通常,在“攻击起日຅d-day”的前几周,性能测试会显示新系统达不到预期要求。于是,找专家,调优sql语句,召集数据库管理员和系统管理员不断ษ开会讨论对策。最后,性能ม总算与以前๩的系统大致相当了尽管新系统用的是价格翻倍的硬件。人们常常使用战术,而忽略了战略。战略要求从大局上把握整个架构与设计。和战争一样,战略的基本原则并不多,且经常被忽视。架构错误的代价非常高,sql程序员必须准备充分,明确目标,了解如何实现目标。在本章中ณ,我们讨论编写高效访问数据库的程序需要实现哪些关键目标。查询的识别queryidentifi9๗qquueerryyiiddeennttiiffiiaattiioonn数个世纪以来,将军通过辨别军装颜๨色和旗๱帜等来判断各部ຖ队的位置,以此检查激战中部队行进情况。同样,当一些进程消耗了过多的cນpu资源时,通常也可以确定是由哪些正被执行的sql语句造成的。但是,要确定是应用的哪部分提交了这些sql语句却困难得多,特别ี是复杂的大型系统包含动态建立的查询的时候。尽管许多产品良好的监控工具,但要确定一小段sql语句与整个ฐ系统的关系,有时却非常困难。因此,要养成为程序和关键模块加注释的习๤惯,在sql中插入注释有助于辨别查询在程序中的位置。例如:9色lectbຘlah这些注释在查错时非常有用。另外,注释也有助于判断ษ单独应用对服务器造成的负载有多大;例如我们希๶望本地应用承担更多工作,需要判断ษ当前硬件是否能承受突高负载,这时注释特别ี有用。有些产品还了专门的记录功能registrationfacilities,将你从“为每个语句加注释”的乏味工ื作中ณ解放出来。例如oraທ9_ຕinfo包,它支持4๒8个ฐ字符的模块名称摸dulename、3๑2๐个字符的动作名称aທ9ame和64个ฐ字符的客户信息,这些字段的内容可由我们定制。在oracle环境下,你可以利ำ用这个ฐ程序包记录哪个应用正在执行,以及它在何时正在做什么。因为应用是通过“oracນlev$动态视图”能显示目前๩内存中生的情况向程序包传递信息的,于是我们可以轻易地掌握这些信息。总结:易识别的语句有助于定位性能问题。保持数据库连接稳定stabຘledataທba色9sssttaaທbຘblleeddaaທttaabbaaທs色eoonnnneettiioonnss建立一个ฐ新的数据库连接,既快又方แ便,但这其中ณ往往掩藏着重复建立数据库连接带来的巨大开销。所以,管理数据库连接必须非常小心。允许多重连接——可能就藏在你的应用中ณ——的后果可能很严å重,下面即是一例。

--ๅ-ๅ----ๅ---ๅ---ๅ----ๅ-----ๅ-paທge7------ๅ------ๅ---ๅ--ๅ-ๅ--ๅ-ๅ--

不久前,我遇到一个应用,要处理很多小的文本文件。这些文本文件最大的也不过一百行,每一行包含要加载的数据及数据库等信息。此例中ณ固然只有一个ฐ数据库实例,但即使有上百个,这里所说明的原理也๣是适用的。处理每个文件的代码如下:openthefileuntiltheendoffileisreachedreadaro9๗9ecttothe色rverspe9色rtthedaທtaທdis9ectclo色thefile上述处理工作令人满意,但当大量小文件都在极短的时间内到达时,可能应用程序来不及处理,于是积压大量待处理文件,花费时间相当可观。我用cນ语言编了个简单的程序来模拟上述情况,以说明频๗繁的数据库连接和中断所造成的系统性能ม下降问题。表2-1้列出了模拟的结果。注意产生表2๐-1结果的程序使用了常规的in色rt语句。顺便提一下,直接加载dire9๗g的技术会更快。表2-1้:连接/中断性能测试结果测试结果依次对每一行作连接/中ณ断74๒行秒连接一次,所有行逐个插入1้681行秒连接一次,以10行为ฦ一数组插入5914行秒连接一次,以100่行为一数组插入919๗0行秒

----ๅ-ๅ--ๅ---ๅ---ๅ--ๅ-ๅ------ๅ-paທge8๖---ๅ------ๅ--ๅ-ๅ---------ๅ--

此例说明了尽量减少分别ี连接数据库次数的重要性。对比表中ณ前后两次针ฤ对相同数据库的插入操作,明显现性能有显着提升。其实还可以做进一步的优化。因为数据库实例的数量势必有限,所以可以建立一组处理程序haທndler分别ี负责一个数据库连接,每个数据库只连接一次,使性能进一步提高。正如表2-ๅ1้所示ิ,仅连接数据库一次或很少次的简单技巧ู,再加上一点额๩外工作,就能让效率提升200倍以上。当然,在上述改进的基础上,再将欲更新า的数据填入数组,这样就尽可能减少了程序和数据库核心间的交互次数,从而使性能产生了另一次飞跃。这种每次插入几行数据的做法,可以使数据的总处理能力又增加了5๓倍。表2-ๅ1中ณ的结果显示改进后的性能几乎是最初ม的12๐00่倍。为ฦ何有如此大的性能提升?第一个原因,也๣是最大的原因,在于数据库连接是很“重”的操作,消เ耗资源很多。在常见的客户服务器模式中ณ现在仍广为使用,简单的连接操作背后潜藏着如下事实:先,客户端与远程服务器的监听程序listenerprograທm建立联系;接着,监听程序要么เ创建一个进程或线程来执行数据库核心程序,要么เ直接或间接地把客户请求传递给已存在的服务器进程,这取决于此服务器是否为共享服务器。除了这些系统操作创น建进程或线程并开始执行之外,数据库系统还必须为每次色ssion建立新环境,以跟踪它的行为。建立新色ssion前,dbms还要检查密码是否与保存的加密的账户密码相符。或许,dbຘms还要执行登录触器logontrigger,还要初始化存储过程和程序包如果它们是第一次被调用。上面这些还不包括客户端进程和服务器进程之间要完成的握手协议。正因为ฦ如此,连接池9g等保持永久数据库连接的技术对性能ม才如此重要。第二个原因,你的程序甚至包括存储过程和数据库之间的交互也有开销。即使数据库连结已经建立且仍未中断,程序和dbຘms核心之ใ间的上下文切换9text9๗itcນh也有代价。因此,如果dbms支持数据通过数组传递,应毫不犹豫地使用它。如果该数组接口是隐式的aທpi内部使用,但你不能使用,那ว么明智的做法是检查它的默认大小并根据具体需要修改它。当然,任何逐行处理的方式都面临上下文切换的问题,并对性能ม产生严å重影响——本章后面还会多次涉及此问题๤。总结:数据库连接和交互好似万里长城——长度越长,传递消息越耗时。战略๓优先于战术strategybຘeforetaທcນticນsssttrraaທtteeggyybbeeffoorreettaattiiss战略决定战术,反之则谬也๣。思考如何处理数据时,有经验的开者不会着眼于细微步骤,而是着眼于最终结果。要获得想要的结果,最显而易见的方法是按照业务规则规定的顺序按部ຖ就班地处理,但这不是最有效的方法——接下来的例子将显示,刻意关注业务处理流程可能ม会使

--ๅ------ๅ------ๅ--ๅ-ๅ---ๅ---page9-ๅ-------ๅ--ๅ--ๅ-ๅ------ๅ----

我们错失最有效的解决方แ案。几年前,有人给了我一个ฐ存储过程,让我“尝试”着进行一下优化。为什么เ说是“尝试”呢?因为该存储过程已经被优化两ä次了,一次是由原开者,另一次是由á一个自称oracle专家的人。但尽管如此,这个存储过程的执行仍会花上20分钟,使用者无຀法接受。此存储过程的目的,是根据现有库存和各地订单,计算出总厂需要订购的原料数量。大体上,它就是把不同数据源的几个ฐ相同的表聚合aggregate到一个ฐ主表mastertable中。先,将每个ฐ数据源的数据插入主表;接着,对主ว表中的各项数据进行合计并更新;最后,将与合计结果无关的数据从表中删除。针ฤ对每个ฐ数据源,重复执行上述步骤。所有sql语句都不是特别复杂,也๣没有哪个ฐ单独的sql语句特别ี低效。为ฦ了理解这个存储过程,我花了大半天时间,终于现了问题๤:为什么该过程要用这么多步骤呢?在from子句中ณ加上包含union的子查询,就能得到所有数据源的聚合aທggregaທtion。一条色lect语句,只需一步就得到了结果集,而之前๩要通过插入目标表targettable得到เ结果集。优化后,性能的提升非常惊人——从20分钟็减至2๐0秒;当然,之后我花了一些时间验证了结果集,与未优化前๩完全相同。想要获得上述的大幅提高性能,无需特别ี技能,仅要求站在局外思考thinkoutsidethebຘox的能力。之前两ä次优化因“太关注问题本身”而收到了干扰。我们需要大胆的思维,站得远一些,试着从大局的角度看待问题。要问自己้一些关键的问题:写存储过程之前,我们已๐有哪些数据?我们希๶望存储过程返回什么เ结果?再辅以大胆的思维,思考这些问题๤的答案,就能得到一个性能大幅提升的处理方式了。总结:考虑解决方案的细节之ใ前,先站得远一些,把握大局。先定义问题๤,再解决问题๤probຘlemdefinitionbeforesolutionpprroobຘblleemmddeeffiinniittiioonnbbeeffoorreessoolluuttiioonn一知半解是危险的。人们常在听说了新技术或特殊技术之ใ后——有时的确很吸引人——试图采用它作为新的解决方案。普通开者和设计师通常会立即采纳这些新“解决方แ案”,直到เ后来才现它们会产生许多后续问题。现成的解决方แ案中ณ,非规范化设计引人注目。设计伊始,非规范化设计的拥护者就提出此方案,为ฦ了寻求“性能”而无视最终将会面临的升级恶魔——而事实上,在开周期早期,改进设计或学习如何使用join也是一个不错的选择。作为非规范化设计的一种手段,物化视图materializedvie9常被认为ฦ是灵丹妙药。物化视图有时被称为ฦ快照snapshot,这个更加平常的词更形象地反映了可悲的事实:物化视图是某时间点的数据副本。在没有其他办法时,这个理论上遭到เ质疑的技术也未尝不值得一试,借用卡夫卡franzkafka的一句名言:“逻辑诚可贵,生存价更高。”然而,绝大部分问题都可借助传统技术巧妙解决。先,应学会充分利用简单、传统的技术。只有完全掌握了这些技术,才能正确评价它们的局限性,最终现它相当于新技术的潜在优势如果有的话。所有技术方案,都只是我们达到目标的手段。没有经验的开者误把新技术本身当成了目标。

----------ๅ--ๅ------ๅ-----page10-ๅ----------ๅ--ๅ----ๅ--ๅ--ๅ-ๅ-

对于热衷于技术、过于看重技术的人来说,此问题就更为严重。总结:先打基础,再赶时髦:摆弄新า工ื具之前๩,先把手艺学好。直接操作实际数据operaທtionsaທgainstacນtuaທldataooppeerraattiioonnssaaggaaiinnssttaທattuuaທallddaທattaa许多开者喜欢建立临时工作表temporary9orktable,把后续处理使用的大量数据放入其中,然后开始“正式”工作。这种方แ法广受质疑,反映了“跳出业务流程细节考虑问题๤”的能ม力不足。记住,永久表permaທnenttaທble可以设置非常复杂的存储选项在第5章将讨论一些存储选项的设置,而临时表不能。临ภ时表的索ิ引如果有的话可能不是最优的,因此,查询临时表的语句效率比永久ื表的差。另外,查询之前必然先为ฦ临时表填入数据,这自然也๣多了一笔额外的开销。就算使用临ภ时表有充足理由á,若数据量大,也๣绝不能把永久ื表当作临时工作表来用。问题之一在于统计信息的自动收集:若没有实时收集要求,dbms通常会在不活动或活动少时进行统计信息收集,而这时作为临时工作表可能ม为ฦ空,从而使优化器收到了完全错误的信息。这些不正确且有偏差的统计信息可能造成执行计划ฐexe9๗完全不合理,导致性能下降。所以,如果一定要用临时表,应确保数据库知道哪些表是临ภ时的。总结:暂时工ื作表意味着以不太合理的方แ式存储更多信息。sql用ssqqll处理集合色tpro9sqls色ettpprrooeessssiinnggiinnssqqllsql完全基于集合色t来处理数据。对大部分更新或删除操作而言——如果不是针ฤ对整个ฐ表的话——你必须先精确定义แ出要处理的记录的集合。这定义了该处理的粒度granularity,可能是对大量记录的粗粒度操作,有可能ม是只影响少数记录的细粒度操作。将一次“大批量数据的处理”分割成多次“小块处理”是个坏主意,除非对数据库的修改太昂贵,否则不要使用,因为这种方法极其低效:1占用过多的空间保存原始数据,以备事务traທnsa9๗回滚rollbacນk之需;2万一修改失败,回滚消耗过长的实践。许多人认为ฦ,进行大规模修改操作时,应在操作数据的代码中有规律地多安排些mit命令。其实,严å格从实践角度来讲,“从头开始重做”比“确定失败生的时间和位置,接着已๐提交部分重做”要容易得多、简单得多、也๣快得多。处理数据时,应适应数据库的物理实现。考虑事务失败时回滚所需日志的大小,如果要为undo保存的数据量确实巨大,或许应该考虑数据修改的频率问题๤。也๣就是说,将大规模的“每月更新”,改为规模不大的“每周更新”,甚至改为规模更小的“每日຅更新”,或许是个ฐ有效方案。总结:几千个语句,借助游标cursor不断循环,很慢。换成几个语句,处理同样的数据,还是较慢。换成一个语句,解决上述问题,最好。

-----ๅ--ๅ-ๅ---------ๅ----ๅ--ๅpage11้--ๅ-ๅ---ๅ------ๅ---ๅ-ๅ--ๅ--ๅ-ๅ--

sql动作丰富的ssqqll语句aທ9-pa9tsaaທttiioonn-ๅ-ppaaທkkeeddssqqllssttaທatteemmeennttsssql不是过程性语言pro9๗guaທge,尽管也可以将过程逻辑procedurallogicນ用于sql,但必须小心。混淆声明性处理de9g和过程逻辑,最常见的例子出现在需要从数据库中提取数据、然后处理数据、然后再插入到数据库时。在一个ฐ程序或程序中的一个ฐ函数接收到特定输入值后,如下情况太常见了:用输入值从数据库中ณ检索ิ到เ一个或多个另外的数据值,然后,借助循环或条件逻辑通常是ifthenel色将一些语句组织起来,对数据库进行操作。大多数情况下,造成上述错误做法的原因有三:根深蒂固的坏习๤惯、sql知识的缺乏็、盲从功能需求规格说明。其实,许多复杂操作往往可由一条sql语句完成。因此,如果用户了一些数据值,尽量不要将操作分解为多条提取中间结果的语句。避免在sql中引入“过程逻辑proceduraທllogic”的主要原因有二。数据库访问,总会跨多个软件层,甚至包括网络访问。即使没有网络访问,也会涉及进程间通讯;额外的存取访问意味着更多的函数调用、更大的带宽,以及更长的等待时间。一旦这些调用要重复多次,其对性能的影响就非常可观了。在sql中引入过程逻辑,意味着性能和维护问题๤应由á你的程序承担。大多数据库系统都了成熟ງ的算法,来处理join等操作,来优化查询以获得更高的效率。基于开销的优化器cost-bຘaທ色doptimizer,cbo是很复杂的软件,它早已๐不像刚ธ推出时那样没什么用了,而在大部ຖ分情况下都是非常出色的成熟产品了,优秀的cbo查询优化的效率极高。然而,cbo所能改变的只有sql语句。如果在一条单独的sql语句中完成尽可能多的操作,那么性能优化可以还由dbms核心负责,你的程序可以充分利用dbms的所有升级。也๣就是说,未来大部分维护工作从程序间接转移给了dbms供货商。当然,“避免在sql中引入过程逻辑”规则ท也有例外。有时过程逻辑确实能ม加快处理度,庞大的sql语句未必总是高效。然而,过程逻辑及其之后的处理相同数据的语句,可以编写到一个单独的sql语句中,cbo就是这么做的,从而获得最高效的执行方แ式。总结:尽可能多地把事情交给数据库优化器来处理。充分利用每次数据库访问profitabledatabຘa色aທes色spprrooffiittaaທbblleeddaທattaabຘbaas色eaaທeesss色ess如果计划ฐ逛好几家商店,你会先决定在每家店买哪些东西。从这一刻起,就要计划按何种顺ิ序购物才能少走冤枉路。每逛一家店,计划东西购买完毕,才逛下一家。这是常识,但其中蕴含的道理许多数据库应用却不懂得。要从一个表中ณ提取多段信息时,采用多次数据库访问的做法非常糟糕,即使多段信息看似“无关”但事实上往往并非如此。例如,如果需要多个ฐ字段的数据,千万不要逐个ฐ字段地提取,而应

--ๅ---ๅ---ๅ---ๅ------ๅ--ๅ-ๅ---page12๐------ๅ---ๅ-ๅ--ๅ--ๅ-ๅ---ๅ-----

一次操作全部ຖ完成。很不幸,面向对象oo的最佳实践提倡为ฦ每个ฐ属性定义一个get方法。不要把oo方法与关系数据库处理混为一谈。混淆关系和面向对象的概念,以及将表等同于类、字段等同于属性,都是致命的错误。总结:在合理范围内,利ำ用每次数据库访问完成尽量多的工作。dbms接近ddbbmmss核心9ellloos色enneessssttootthheeddbbຘmmskeerrnneell代码的执行越接近dbຘms核心,则执行度越快。数据库真正强大之处就在于此,例如,有些数据库管理产品支持扩展,你可以用c等较底层的语言为它编写新功能。用含有指针操作的底层语言有个缺点,即一旦指针ฤ处理出错会影响内存。仅影响到一个用户已很糟糕,何况数据库服务器就像“服务器”名字所指的一样出了问题会影响众多“用户”——服务器内存出了问题๤,所有使用这些数据的无辜的应用程序都会受影响。因此,dbms核心采取了负责任的做法,在沙箱sandbຘox环境中执行程序代码,这样,即使出了问题也๣不会影响到数据。例如,ora99๗和它自身之间实现了一套复杂的通信机制,此机制在某些方面很像控制数据库连结的方แ法,以管理两个或多个服务器上的数据库实例之ใ间的通信。到เ底采用plsql存储过程还是外部cນ函数,应综合比较后决定。如果精心编写外部c函数获得的好处过了建立外部环境和上下文切换9๗g的成本,就应采用外部函数。但需要处理一个ฐ大数据量的表的每一行时,不要使用外部ຖ函数。这需要平衡考虑,解决问题๤时应完全了解备选策略的后果。如要使用函数,始终应选dbms自带的函数。这不仅仅是为ฦ了避免无谓的重复劳动,还因为自带函数在执行时比任何第三方开的代码更接近数据库核心,相应地其效率也๣会高出许多。下面这个简单例子是用oraclesql编写的,显示ิ了使用oracle函数所获得的效率。假设手工输入的文本数据可能包含多个ฐ相邻的“空格”,我们需要一个函数将多个ฐ空格替换为一个空格。如果不采用oraທcledatabaທ色10่g开始的正规表达式regulaທrexpression,函数代码将会是这样:99๗gi女aທr9๗vaທr9gvar9number:=lengthp_string;i逼nary_integer:๘=๡1;๙j逼nary_integer;begin9hilei0loopv_string:๘=substrv_string,1,i||ltrimsubstrv_string,i+1;๙i:=๡instrv_string,ไ'ู';endloop;retur女_string;end;

---ๅ--ๅ-ๅ------ๅ-------ๅ--ๅ--paທge14๒-ๅ---------ๅ----------ๅ--ๅ-

还有第三种方แ法:9๗9gi女ar9varchar2isv_stringvar9g;len1numbຘer;len2๐number;๙bຘeginlen1:=lengthp_string;v_ຕstring:=repla9g,''ู,''ู;len2:๘=lengthv_string;9hilelen2色lectsqueeze1้'ูazerythgfrdtr'2fromdual3aທzerythgfrdtrelap色d:00่:00:0่00่0่sql色lectsqueeze2'aທzerythgfrdtr'2fromdual3๑azerythgfrdtrelaທp色d:0่0่:0่0่:0001sql色lecນtsqueeze3๑'aທzerythgfrdtr'2fromdual3azerythgfrdtrelap色d:00:๘00:๘0่00่0

-ๅ-ๅ-------ๅ--ๅ-ๅ----ๅ-----ๅ--ๅpage15--ๅ--ๅ----ๅ--ๅ--ๅ-ๅ---ๅ------ๅ-

那么,如果每天要调用该空格替换操作几千次呢?我们构造一个接近现实负载的环境,下面的代码将建立一个ฐ用于测试的表并填入随机数据,已๐检测上面三个ฐ函数是否有性能差ๆ异:createtaທblesqueezableraທndom_textvarchaທr25๓0de9ary_integer;j逼nary_integer;k逼nary_integer;v_stringvare1,ไ100;v_string:๘=dbms_randomstring'u',50่;9๗hilej9๗hen9stillsomethingel色el色

-ๅ-----ๅ-ๅ-----ๅ----ๅ--ๅ-ๅ----page1้9-------ๅ-ๅ---ๅ------ๅ--ๅ-ๅ---

end数值或日຅期的比较则ท简单明了。操作字符串ธ可以用oraທcle的greaທtest或least,或者mysql的str9色rt语句增加过程逻辑,具体办法是多重in色rt及条件in色rt注3,并借助merge语句。如果dbຘms了这样语句,毫不犹豫地使用它。也๣就是说,有许多逻辑可以放入sql语句中;虽然仅执行多条语句中的一条这种逻辑价值不大,但如果设法利ำ用ca色、merge或类似功能ม将多条语句合并成一条,价值可就大了。总结:只要有可能ม,应尽量把条件逻辑放到sql语句中,而不是sql的宿主语言中ณ。一次完成多个ฐ更新multipleupdatesaທtoncemmuullttiipplleeuuppddaatteessaaທttoonnee我的基本主张是:如果每次更新的是彼此无关的记录,对一张表连续进行多次update操作还可以接受;否则,就应该把它们合并成一个ฐupdaທte操作。例如,下面是来自实际应用的一些代码注4๒:updatetbຘo_i女oice_ຕextractor色tpga_staທtus=0pdatetbຘo_ຕi女oice_ຕextracນtor色trd_status=๡09hererd_staທtusin1,3aທndi女_type=0;两ä个连续的更新是对同一个表进行的。但它们是否将访问相同的记录呢?不得而知。问题是,搜索条件的效率有多高?任何名为type或status的字段,其值的分布通常是杂乱无章的,所以上面两个updaທte语句极可能对同一个表连续进行两次完整扫描:一个ฐupdate有效地利用了索引,而第二个update不可避免地进行全表扫描;或者,幸运的话,两次updaທte都有效地利用了索引。无论如何,把这两个update合并到一起,几乎不会有损失,只会有好处:updaທtetbo_ຕi女oice_extraທctor色tpgaທ_status=9๗1then0่9hen3then0el色pga_ຕstatus

-ๅ----ๅ-ๅ--ๅ-----ๅ-ๅ--ๅ-ๅ------ๅpage20่-----ๅ----ๅ--ๅ-ๅ---------ๅ--

end,rd_status=๡91้then09hen3then0el色rd_staທtusendsin1้,3๑aທndi女_type=0่;๙上例中ณ,可能出现重复更新相同字段为ฦ相同内容的情况,这的确增加了一小点儿开销。但在多数情况下,一个ฐupdate会比多个updaທte快得多。注意上例中的“逻辑logic”,我们通过cນaທ色语句实现了隐式的条件逻辑impli9allogic,来处理那些符合更新条件的数据记录,并且更新า条件可以有多条。总结:有可能的话,用一个语句处理多个更新;尽量减少对同一个表的重复访问。慎用自定义แ函数carefulu色ofu色r-ๅ9rittenfun9s将自定义函数u色r-9๗rittenfun9๗嵌到sql语句后,它可能ม被调用相当多次。如果在色lecນt语句的选出项列ต表中使用自定义函数,则每返回一行数据就会调用一次该函数。如果自定义函数出现在9here子句中ณ,则每一行数据要成功通过过滤条件都会调用一次该函数;如果此时其他过滤条件的筛选能力不够强,自定义函数被调用的次数就非常可观了。如果自定义函数内部ຖ还要执行一个ฐ查询,会生什么情况呢?每次函数调用都将执行此内部ຖ查询。实际上,这和关联子查询cນorrelatedsubຘquery效果相同,只不过自定义函数的方式阻碍了基于开销的优化器cນost-bຘaທ色doptimizer,cbo对整个查询的优化效果,因为“子查询”隐藏在函数中ณ,数据库优化器鞭长莫及。下面举ะ例说明将sql语句隐藏在自定义函数中的危险性。表flights描述商务航班,有航班号、起飞时间、到达时间及机场iaທta代码注5๓等字段。iata代码均为ฦ三个ฐ字母,有9๗000多个ฐ,它们的解释保存在参照表中,包含城市๦名称若一个城市有多个机场则应为ฦ机场名称、国家名称等。显然,显示航班信息时,应该包含目的城市๦的机场名称,而不是简单的iata代码。在此就遇到了现代软件工ื程中的矛盾之一。被认为是“优良传统”的模块化编程一般情况下非常适用,但对数据库编程而言,代码是开者和数据库引擎的共享活动shaທredactivity,模块化要求并不明确。例如,我们可以遵循模块化原则ท编写一个小函数来查找iaທtaທ代码,并返回完整的机场名称:99๗airport_ຕ9๗9varchar2๐is

------ๅ--ๅ--ๅ----ๅ--ๅ-ๅ------ๅpage21้--ๅ-ๅ--ๅ-ๅ------ๅ-----ๅ-ๅ-----ๅ

9aທmevar9amefromiata_airport_codes9๗herecode=iataທ_9aທme;๙end;对于不熟悉oraທcle语法的读者,在此做个说明,以下查询中ณtruncsysdaທte的返回值为“今天的00:00่aທm”,日຅期计算以天为单位;所以起飞时间的条件是指今天8๖:3๑0่am至4:0่0pm之间。调用airport_cນity函数的查询可以非常简单,例如:色le9๗umbຘer,ไto_chardeparture_time,'hh24๒:๘mi'depaທrture,airport_cityarrival"to"fromflights9heredepaທrture_ຕtimebet9eentrunre_ຕtime这个ฐ查询的执行度令人满意;在我机器上的随机样本中,返回7๕7๕行数据只用了0่18๖秒多次执行的平均值,用户对这样的度肯定满意统计数据表明,此处理访问了303个数据块,53个是从磁盘读出的——而且每行数据有个递归调用。我们还可以用join来重写这段代码,作为查找函数的替代方แ案,当然它看起来会稍微复杂些:色le9umber,ไto_ຕcharfdeparture_ຕtime,'ูhh24:mi'ูdepaທrture,acity"ิto"ิfromflightsf,iaທta_airport_codesa9herea9๗ddeparture_timebet9eentrunre_time

-ๅ--ๅ-ๅ------ๅ---ๅ---ๅ--ๅ-ๅ--ๅ-ๅ-page2๐2----ๅ-ๅ-----ๅ----ๅ--ๅ-ๅ------

这个查询只用了00่5๓秒统计数据同前,但没有递归调用。对于执行时间不到เ02秒的查询来说,度快了3倍似乎无关紧要,但在大型系统中,这些查询每天经常执行数十万次——假设以上查询每天只执行五万次,于是查询的总耗时为2๐5小时。若不使用上述查找函数lookupfun9则ท只需要不到42๐分钟,度提高过300่%,这对大数据量的系统意义重大,最终带来经济上的节约。通常,使用查找函数会使批处理程序的性能极差。而且查询时间的增加,会使同一台机器支持的并用户数减少,我们将在第9章对此展开讨论。总结:优化器对自定义函数的代码无能ม为力。sql简洁的ssqqllsuincນtsql熟练的开者使用尽可能少的sql语句完成尽可能多的事情。相反,拙劣的开者则倾向于严格遵循已๐制ๆ订好的各功能步骤,下面是个ฐ真实的例子:--ๅgetthestaທrtoftheaountingperiod色le9todtperstaທfromtperrslt9๗herefiscaທl_ຕyear=to_ຕcharparaທm_ຕdta,ไ'yyyy'ูandrslt_ຕperiod=๡'1'||to_charparam_ຕdtaທ,'mm';๙--ๅgettheendoftheperiodoutofcນlosure色le9todtperclosurefromtperrslt9herefiscal_year=to_charpaທraທm_dta,ไ'ูyyyy'aທndrslt_period='9'||to_cນharparam_dtaທ,ไ'mm';就算度可以接受,这也是段极糟的代码。很不幸,性能ม专家经常遇到这种糟糕的代码。既然两个ฐ值来自于同一表,为什么要分别用两ä个不同的语句呢?下面用oracle的bulkcollecນt子句,一次性将两个值放到数组中ณ,这很容易实现,关键在于对rslt_period进行orderby操作,如下所示ิ:色lectcນlosure_ຕdatebulk9todtperstaທarrayfromtperrslt9๗herefiscaທl_ຕyear=to_ຕcharparaທm_ຕdta,'yyyy'ูandrslt_ຕperiodin'1'||to_ຕcharpaທram_dta,'mm'ู,

-ๅ-ๅ----ๅ---------ๅ---ๅ-ๅ--ๅ--ๅpage23----ๅ--ๅ-ๅ----ๅ-----ๅ--ๅ-ๅ---ๅ-

'ู9'ู||to_cນhaທrparam_dtaທ,'mm'orderbyrslt_period;于是,这两个ฐ日期被分别ี保存在数组的第一个和第二个ฐ位置。其中,bulkcollect是plsql语言特有的,但任何支持显式或隐式数组提取的语言都可如法炮制。其实甚至数组都是不必要的,用以下的小技巧注6๔,这两ä个值就可以被提取到เ两个变量中ณ:色lectmaxdecນodesubstrrslt_period,1้,1้,ไ--checນkthefirstcharacນter'1',closure_date,--ๅifit's'1้'returnthedaທte9๗e9๗aທntto_date'1410่1้066',ไ'ddmmyyyy',--other9i色somethingoldmaxdecodesubstrrslt_period,1้,1,ไ'9'ู,closure_ຕdaທte,--thedaທte9e9aທntto_date'14๒101066',ไ'ูddmmyyyy'ู,intodtpersta,dtpercນlosurefromtperrslt9herefiscal_year=to_charparaທm_ຕdta,ไ'yyyy'andrslt_ຕperiodin'1'||to_ຕcharpaທraທm_dta,'ูmm'ู,'9๗'||to_chaທrpaທram_dta,'ูmm';在这个例子中,预ไ期返回值为两ä行数据,所以问题是:如何把原本属于一个字段的两行数据,以一行数据两个字段的方แ式检索出来正如数组提取的例子一样。为此,我们检查rslt_period字段,两行数据的rslt_period字段有不同值;如果找到需要的记录,就返回要找的日຅期;否则,就返回一个ฐ在任何情况下都远比我们所需日຅期要早的日期此处选了哈斯丁之役bຘattleofhastings的日期。只要每次取出最大值,就可以确保获得需要的日期。这是个ฐ非常实用的技巧,也๣可以应用在字符或数值数据,第1้1章会有更详细的说明。总结:sql是声明性语言de9guaທge,所以设法使你的代码越业务过程的规格说明。sqlssqqll的进攻式编程offensive9๗g9ithsql一般的建议是进行防御式编程9๗sively,在开始处理之前๩先检查所有参数的合法性。但实际上,对数据库编程而言,尽量同时做几件事情的进攻式编程有切实的优势。有个ฐ很好的例子:进行一连串ธ检查,每当其中一个ฐ检查所要求的条件不符时就产生异常。信用卡付款的处理中就涉及类似步骤。例如,检查所提交的客户身份和卡号是否有效,以及两者是否匹配;检查信用卡是否过期;最后,检查当前๩的支付额是否过了信用额度。如果通过了所

------ๅ--ๅ-ๅ---ๅ------ๅ---ๅ--ๅpage24-ๅ--------ๅ-ๅ-----ๅ----ๅ--ๅ-ๅ-

有检查,支付操作才继续进行。为ฦ了完成上述功能,不熟练的开者会写出下列语句,并检查其返回结果:色le9tfromcustomers9herecustomer_id=provided_ຕid接