友情提示:如果本网页打开太慢或显示不完整,请尝试鼠标右键“刷新”本网页!
SQL语言艺术(PDF格式)-第4部分
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部! 如果本书没有阅读完,想下次继续接着阅读,可使用上方 "收藏到我的浏览器" 功能 和 "加入书签" 功能!
格遵循已制订好的各功能步骤,下面是个真实的例子:
…Get the start of the accounting period
select closure_date
into dtPerSta
from tperrslt
where fiscal_year=to_char(Param_dtAcc;'YYYY')
and rslt_period='1' || to_char(Param_dtAcc;'MM');
…Get the end of the period out of closure
select closure_date
into dtPerClosure
from tperrslt
where fiscal_year=to_char(Param_dtAcc;'YYYY')
and rslt_period='9' || to_char(Param_dtAcc;'MM');
就算速度可以接受,这也是段极糟的代码。很不幸,性能专家经常遇到这种糟糕的代码。既然
两个值来自于同一表,为什么要分别用两个不同的语句呢?下面用Oracle的bulk collect子句,
一次性将两个值放到数组中,这很容易实现,关键在于对rslt_period进行order by操作,如下所
示:
select closure_date
bulk collect into dtPerStaArray
from tperrslt
where fiscal_year=to_char(Param_dtAcc;'YYYY')
and rslt_period in ('1' || to_char(Param_dtAcc;'MM');
'9' || to_char(Param_dtAcc;'MM'))
order by rslt_period;
…………………………………………………………Page 26……………………………………………………………
于是,这两个日期被分别保存在数组的第一个和第二个位置。其中,bulkcollect 是 PL/SQL 语
言特有的,但任何支持显式或隐式数组提取的语言都可如法炮制。
其实甚至数组都是不必要的,用以下的小技巧(注6),这两个值就可以被提取到两个变量中:
select max(decode(substr(rslt_period; 1; 1); …Check the first character
'1'; closure_date;
…If it's '1' return the date we want
to_date('14/10/1066'; 'DD/MM/YYYY')));
…Otherwise something old
max(decode(substr(rslt_period; 1; 1);
'9'; closure_date; …The date wewant
to_date('14/10/1066'; 'DD/MM/YYYY')));
into dtPerSta; dtPerClosure
from tperrslt
where fiscal_year=to_char(Param_dtAcc;'YYYY')
and rslt_period in ('1' || to_char(Param_dtAcc;'MM');
'9' || to_char(Param_dtAcc;'MM'));
在这个例子中,预期返回值为两行数据,所以问题是:如何把原本属于一个字段的两行数据,
以一行数据两个字段的方式检索出来(正如数组提取的例子一样)。为此,我们
检查rslt_period字段,两行数据的rslt_period字段有不同值;如果找到需要的记录,就返回要找
的日期;否则,就返回一个在任何情况下都远比我们所需日期要早的日期(此处选了哈斯丁之
役(battle of Hastings)的日期)。只要每次取出最大值,就可以确保获得需要的日期。这是个
非常实用的技巧,也可以应用在字符或数值数据,第11章会有更详细的说明。
总结:SQL是声明性语言(declarative language),所以设法使你的代码超越业务过程的规格
说明。
SQL
SSQQLL的进攻式编程
Offensive Coding with SQL
一般的建议是进行防御式编程(code defensively),在开始处理之前先检查所有参数的合法性。
但实际上,对数据库编程而言,尽量同时做几件事情的进攻式编程有切实的优势。
有个很好的例子:进行一连串检查,每当其中一个检查所要求的条件不符时就产生异常。信用
…………………………………………………………Page 27……………………………………………………………
卡付款的处理中就涉及类似步骤。例如,检查所提交的客户身份和卡号是否有效,以及两者是
否匹配;检查信用卡是否过期;最后,检查当前的支付额是否超过了信用额度。如果通过了所
有检查,支付操作才继续进行。
为了完成上述功能,不熟练的开发者会写出下列语句,并检查其返回结果:
select count(*)
from customers
where customer_id = provided_id
接下来,他会做类似的工作,并再一次检查错误代码:
select card_num; expiry_date; credit_limit
from accounts
where customer_id = provided_id
之后,他才会处理金融交易。
相反,熟练的开发者更喜欢像下面这样编写代码(假设today()返当前日期):
update accounts
set balance = balance purchased_amount
where balance 》= purchased_amount
and credit_limit 》= purchased_amount
and expiry_date 》 today()
and customer_id = provided_id
and card_num = provided_cardnum
接着,检查被更新的行数。如果结果为 0,只需执行下面的一个操作即可判断出错原因:
select c。customer_id; a。card_num; a。expiry_date;
a。credit_limit; a。balance
from customers c
leftouter join accounts a
on a。customer_id = c。customer_id
and a。card_num = provided_cardnum
where c。customer_id = provided_id
如果此查询没有返回数据,则可断定customer_id 的值是错的;如果 card_num 是 null,则可
断定卡号是错的;等等。其实,多数情况下此查询无需被执行。
注意
你是否注意到,上述第一段代码中使用了count(*)呢?这是个count(*)被误用于存在性检测的绝
…………………………………………………………Page 28……………………………………………………………
佳例子。
“进攻式编程”的本质特征是:以合理的可能性(reasonableprobabilities)为基础。例如,检查
客户是否存在是毫无意义的——因为既然该客户不存在,那么他的记录根本就不在数据库中!
所以,应该先假设没有事情会出错;但如果出错了,就在出错的地方(而且只在那个地方)采
取相应措施。有趣的是,这种方法很像一些数据库系统中采用的“乐观并发控制(optimistic
concurrency control)”,后者会假设update冲突不会发生,只在冲突真的发生时才进行控制处理。
结果,乐观方法比悲观方法的吞吐量高得多。
总结:以概论为基础进行编程。假设最可能的结果;不是的确必要,不要采用异常捕捉的处理
方式。
Exceptions
精明地使用异常(EExxcceeppttiioonnss)
Discerning Use of Exceptions
勇敢与鲁莽的界线很模糊,我建议进攻式编程,但并不是要你模仿轻步兵旅在Balaclava的自杀
性冲锋(注7)。针对异常编程,最终可能落得虚张声势的愚蠢结果,但自负的开发者还是对它“推
崇备至(go for it)”,并坚信检查和处理异常能使他们完成任务。
正如其名字所暗示的,异常应该是那些例外情况。对数据库编程的具体情况而言,不是所有异
常都要求同样的处理方式——这是理解异常的使用是否明智的关键点。有些是“好”异常,应预
先抛出;有些是“坏”异常,仅当真正的灾害发生时才抛出。
例如,以主键为条件进行查询时,如果没有结果返回则开销极少,因为只需检查索引即可判断。
然而,如果查询无法使用索引,就必须搜索整个表——当此表数据量很大,所在机器又正在接
近满负荷工作时,可能造成灾难。
有些异常的处理代价高昂,即使是在最佳情况下也不例外,例如重复键(duplicate key)的探
测。“唯一性(uniqueness)”如何保证呢?我们几乎总是建立一个唯一性索引,每次向该索引增
加一个键时,都要检查是否违反了该唯一性索引的约束。然而,建立索引项需要记录物理地址,
于是就要求先将记录插入表,后将索引项插入索引。如果违反此约束,数据库会取消不完全的
插入,并返回违反约束的错误信息。上述这些操作开销巨大。但最大的问题是,整个处理必须
围绕个别异常展开,于是我们必须“从个别记录的角度进行思考”,而不是“从数据集出发进行思
考”,这与关系数据库理论完全背道而驰。多次违反此约束会导致性能严重下降。
来看一个 Oracle 的例子。假设在两家公司合并后,电子邮件地址定为的标准
格式,最多 12 个字符,所有空格或引号以下划线代替。
如果新的employee表已经建好,并包含3 000 条从employee_old表中提取并进行标准化处理的
电子邮件地址。我们希望每个员工的电子邮件地址具有唯一性,于是Fernando Lopez的地址为
flopez,而Francisco Lopez的地址为flopez2。实际上,我们实际测试的数据中有33 个潜在的
重复项,所以我们需要做如下测试:
SQL》 insert into employees(emp_num; emp_name;
emp_firstname; emp_email)
2 select emp_num;
…………………………………………………………Page 29……………………………………………………………
3 emp_name;
4 emp_firstname;
5 substr(substr(EMP_FIRSTNAME; 1; 1)
6 ||translate(EMP_NAME; ' '''; '_ _'); 1; 12)
7 from employees_old;
insert into employees(emp_num; emp_name; emp_firstname; emp_email)
*
ERROR at line 1:
ORA…00001: unique constraint (EMP_EMAIL_UQ) violated
Elapsed: 00:00:00。85
3 000 条数据中重复 33 条,比率大约是 1%,所以,或许可以心安理得地处理符合标准的
99%,并用异常来处理其余部分。毕竟,1% 的不符标准数据带来的异常处理开销应该不大。
但这个异常处理的开销到底在哪里呢?让我们先从测试数据中剔除“问题记录”,然后再执行相
同的测试,比较发现:这次测试的总运行时间,与上次几乎相同,都是18 秒。然而,从测试数
据中剔除“问题记录”之后再执行前面第一段 insert。。。select 语句时,速度明显比循环快:最终发
现采用“一次处理一行”的方式导致耗时增加了近 50%。那么,在此例中可以不用“一次处理一
行”的方式吗?可以,但要首先避免使用异常。正是这个通过异常处理解决“问题记录”问题决定,
迫使我们采用循序方式的。
另外,由于发生冲突的电子邮件地址可能不止一个,可以为它们指定某个数字获得唯一性。
很容易判断有多少个数据记录发生了冲突,增加 一个groupby子句就可以了。但在分配数字时,
如果不使用主数据库系统提供的分析功能,恐怕比较困难。(Oracle 称为分析功能(analytical
function), DB2 则称在线分析处理(online analyticalprocessing,OLAP),SQLServer 称之为排
名功能(ranking function)。)纯粹从SQL角度来看,探索此问题的解决方案很有意义。
重复的电子邮件地址都可以被赋予一个具唯一性的数字:1赋给年纪最大的员工,2 赋给年纪次
之的的员工……依次类推。为此,可以编写一个子查询,如果是group中的第一个电子邮件地址
就不作操作,而该group中的后续电子邮件地址则加上序号。代码如下:
SQL》insert into employees(emp_num; emp_firstname;
2 emp_name; emp_email)
3 select emp_num;
4 emp_firstname;
5 emp_name;
6 decode(rn; 1;emp_email;
7 substr(emp_email;
…………………………………………………………Page 30……………………………………………………………
8 1;12…length(ltrim(to_char(rn))))
9 ||ltrim(to_char(rn)))
10 from (select emp_num;
11 emp_firstname;
12 emp_name;
13 substr(substr(emp_firstname; 1;1)
14 ||translate(emp_name; '''';'_ _'); 1;12)
15 emp_email;
16 row_number()
17 over (partitionby
18 substr(substr(emp_firstname; 1;1)
19 ||translate(emp_name;' ''';'_ _');1;12)
20 order byemp_num) rn
21 from employees_old)
22 /
3000rows created。
Elapsed: 00:00:11。68
上面的代码避免了一次一行的处理,而且该解决方案的执行时间仅是先前方案的 60%。
总结:异常处理会迫使我们采用过程式逻辑。应始终使用声明式SQL,尽量预测可能的异常情况。
SQL
SSQQLL的本质
本章我们将深入讨论SQL查询,并研究如何根据不同情况的具体要求,来编写SQL语句。我们
会分析复杂的SQL查询语句,将它们拆解成小的语句片断,并讲解这些语句片断如何共同促成
了最终查询结果的产生。
SQL
SSQQLL的本质
The Nature of SQL
在深入讨论如何编写SQL查询之前,我们有必要首先了解一些SQL自身的基本特性:SQL与数
据库引擎(database engine)和优化器(optimizer)是什么关系?哪些因素可能限制优化效率?
SQL
SSQQLL与数据库
SQL and Databases
关系数据库的出现,要归功于E。F。 Codd的关系理论开创性研究成果。Codd的研究成果为数据
…………………………………………………………Page 31……………………………………………………………
库学科提供了坚实的数学基础——而在此之前的很长一个时期数据库学科主要是凭经验。这和
造桥的历史很相似:几千年前我们就开始建造跨江大桥,但是由于当时的营造商并不完全了解
造桥材料和桥梁强度之间的关系,桥梁的设计往往会大大超出实际的要求;后来土木工程学的
材料强度理论完善了,更先进更安全的桥梁也就随之出现,这表明造桥使用的各种建筑材料得
到了充分利用。的确,如今的一些桥梁工程非常浩大,与此类似,现代DBMS软件能够处理的
数据量之大也是今非昔比了。关系理论之于数据库,正如土木工程学之于桥梁。
SQL语言、数据库和关系模型三者经常被混淆。数据库的功能主要是存储数据,这些数据符合
对现实世界一部分所建立的特定模型。相应地,数据库必须提供可靠的基础设施
(infrastructure),无论何时都能够让多个用户使用同一些数据,且在数据被修改时不破坏数据
完整性。这要求数据库能够处理来自不同用户的“资源争用(contention)”,并能在事务
(transaction)处理过程中遇到机器故障等极端情况下也保持数据一致性。当然,数据库还有
很多其他的功能,本书并未涵盖。
正如其名,结构化查询语言(Structured Query Language,SQL)无非是一种语言,虽然它与
数据库关系密切。将SQL语言和关系数据库等同视之,或者更糟——与关系理论等同视之,都
是错误的。这种错误就好比将掌握了电子表软件或文字处理软件视为掌握了“信息技术”。实际上,
有些软件产品并非数据库,但它们也支持SQL(注1)。另外,SQL在成为标准之前也不得不与
诸如RDO或QUEL等其他语言竞争,这些语言曾被许多理论家认为优于SQL。
为了解决所谓的“SQL问题”,你必须了解两个相关部分:SQL查询表达式和数据库优化器。如图
4…1所示,这两部分在三个不同区域里协同工作。图的中央是关系理论,这是数学家们尽情发挥
的区域。简而言之,关系理论支持我们通过一组关系运算符来搜寻满足某些条件的数据,这些
关系运算符几乎支持任何基本查询。关键在于,关系理论有严格的数学基础,我们完全可以相
信同一结果可由不同的关系表达式来获得,正如在算术中246/369完全等于2/3一样。
然而,尽管关系理论有至关重要的理论价值,但一些有重要实践意义的方面它并未涉及,这些
方面属于图中所示的“报告需求(reporting requirements)”的范围。其中最明显的例子就是结果
集的排序:关系理论只关心如何根据查询条件取得正确的数据集;而对我们这些实践者(而非
理论家)而言,关系操作阶段只负责准确无误地找出属于最终数据集的记录,而不同行的相同
字段的关系并不是在这个阶段处理,而是完全属于排序操作。另外,关系理论并不涉及各种统
计功能(例如百分位数等),而这些统计功能经常出现在不同的“SQL方言(dialect)”当中。关系
理论所研究的是集合(set),但并不涉及如何为这些集合排序。尽管有许多关于排序的数学理
论,但它们都与关系理论无关。
必须说明的是,关系操作与上述“报告需求”的不同在于关系操作适用于理论上无限大的、数学
意义上的集,无论是操作含有十行数据的表、一万行数据的表、还是一亿行数据的表,我们都
能以相同的方式对其施以任何过滤条件。再次强调:当我们只关心找出并返回符合查询条件的
数据时,关系理论是完全适用的;然而,当我们需要进行记录排序,或者执行一个大多数人错
误地认为它是关系操作的group操作时,却已不再是针对可以无限大的数据集进行操作了,而必
须是一个有限数据集,于是这个结果数据集不再是数学意义上的“关系(relation)”了,至此我们
已经超出了关系操作层。当然,我们仍然可以利用SQL对该数据集进行一些有用的操作。
初步总结一下,我们可以将SQL查询表示为一个两层的操作,如图4…2所示。第一层是一个关系
操作的“核”,它负责找出我们要操作的数据集;第二层是“非关系操作层(non…relational layer)”,
…………………………………………………………Page 32……………………………………………………………
它对有限的数据结果集进行“精雕细刻”从而产生用户期望的最终结果。
尽管图4…2简要地表达了SQL在数据处理环境中的位置,但SQL查询在大多数情况下都比这要复
杂得多,图4…2仅仅展示了一个总体的描述。关系操作中的过滤器(filter)有可能只是一个代名
词,其背后是几个独立过滤器的组合,例如通过union结构或子查询来实现;最终,SQL语句的
构成可以很复杂。稍后还会讨论编写SQL语句的问题,但我们接下来首先要讨论的是数据物理
实现和数据库优化器的关系。
总结:千万别把SQL查询的执行过程中真正的关系操作和附加的展现层(presentation layer)
功能混为一谈。
SQL
SSQQLL与优化器
SQL and the Optimizer
当SQL引擎处理查询时,会用优化器找出执行查询最高效的方式。此时关系理论又可以大有作
为了——优化器借助关系理论,对开发者提供的语义无误的原始查询进行有效的等价变换,即
使原始查询编写得相当笨拙。
优化是在数据处理真正被执行时发生的。经过变换的查询在执行时可能比语义上等效的其他查
询快得多,这因是否存在索引,以及变换与查询是否适应而不同。在第5章我们将介绍各种数据
存储模型;有时,特定存储模型决定了查询优化的方式。优化器会检查下列因素:定义了哪些
索引、数据的物理布局、可用内存大小,以及可用于执行查询任务的处理器数。优化器还很重
视查询直接或间接涉及的表和索引的数据量。最终,优化器根据数据库的实际实现情况对理论
上等价的不同优化方案做出权衡,产生有可能是最优的查询执行方案。
然而,要记住的关键一点是,尽�
快捷操作: 按键盘上方向键 ← 或 → 可快速上下翻页 按键盘上的 Enter 键可回到本书目录页 按键盘上方向键 ↑ 可回到本页顶部!
温馨提示: 温看小说的同时发表评论,说出自己的看法和其它小伙伴们分享也不错哦!发表书评还可以获得积分和经验奖励,认真写原创书评 被采纳为精评可以获得大量金币、积分和经验奖励哦!