今天下午去微软面试,被问到了海量数据查询优化的问题,因为平时开发的应用数据量比較小,不太关注性能优化的问题,所以不知怎样作答,非常是郁闷。从网上搜索出海量数据查询优化的两篇文章,转载下来,权当学习性能优化的開始。
数据库优化查询计划的方法
数据库系统是管理信息系统的核心,基于数据库的联机事务处理(OLTP)以及联机分析处理(OLAP)是银行、企业、政府等部门最为重要的计算机应用之中的一个。从大多数系统的应用实例来看,查询操作在各种数据库操作中所占领的比重最大,而查询操作所基于的SELECT语句在SQL语句中又是代价最大的语句。举例来说,假设数据的量积累到一定的程度,比方一个银行的账户数据库表信息积累到上百万甚至上千万条记录,全表扫描一次往往须要数十分钟,甚至数小时。假设採用比全表扫描更好的查询策略,往往能够使查询时间降为几分钟,由此可见查询优化技术的重要性。
在应用项目的实施中发现,很多程序猿在利用一些前端数据库开发工具(如PowerBuilder、Delphi等)开发数据库应用程序时,仅仅注重用户界面的华丽,并不重视查询语句的效率问题,导致所开发出来的应用系统效率低下,资源浪费严重。因此,怎样设计高效合理的查询语句就显得非常重要。本文以应用实例为基础,结合数据库理论,介绍查询优化技术在现实系统中的运用。
分析问题
很多程序猿觉得查询优化是DBMS(数据库管理系统)的任务,与程序猿所编写的SQL语句关系不大,这是错误的。一个好的查询计划往往能够使程序性能提高数十倍。查询计划是用户所提交的SQL语句的集合,查询规划是经过优化处理之后所产生的语句集合。DBMS处理查询计划的过程是这样的:在做完查询语句的词法、语法检查之后,将语句提交给DBMS的查询优化器,优化器做完代数优化和存取路径的优化之后,由预编译模块对语句进行处理并生成查询规划,然后在合适的时间提交给系统处理运行,最后将运行结果返回给用户。在实际的数据库产品(如Oracle、Sybase等)的高版本号中都是採用基于代价的优化方法,这样的优化能依据从系统字典表所得到的信息来预计不同的查询规划的代价,然后选择一个较优的规划。虽然如今的数据库产品在查询优化方面已经做得越来越好,但由用户提交的SQL语句是系统优化的基础,非常难设想一个原本糟糕的查询计划经过系统的优化之后会变得高效,因此所写语句的优劣至关重要。以下重点说明改善查询计划的解决方式。
解决这个问题
以下以关系数据库系统Informix为例,介绍改善用户查询计划的方法。
1.合理使用索引
索引是数据库中重要的数据结构,它的根本目的就是为了提高查询效率。如今大多数的数据库产品都採用IBM最先提出的ISAM索引结构。索引的使用要恰到优点,其使用原则例如以下:
●在常常进行连接,可是没有指定为外键的列上建立索引,而不常常连接的字段则由优化器自己主动生成索引。
●在频繁进行排序或分组(即进行group by或order by操作)的列上建立索引。
●在条件表达式中常常常使用到的不同值较多的列上建立检索,在不同值少的列上不要建立索引。比方在雇员表的“性别”列上仅仅有“男”与“女”两个不同值,因此就无必要建立索引。假设建立索引不但不会提高查询效率,反而会严重降低更新速度。
●假设待排序的列有多个,能够在这些列上建立复合索引(compound index)。
●使用系统工具。如Informix数据库有一个tbcheck工具,能够在可疑的索引上进行检查。在一些数据库server上,索引可能失效或者由于频繁操作而使得读取效率降低,假设一个使用索引的查询不明不白地慢下来,能够试着用tbcheck工具检查索引的完整性,必要时进行修复。另外,当数据库表更新大量数据后,删除并重建索引能够提高查询速度。
2.避免或简化排序
应当简化或避免对大型表进行反复的排序。当能够利用索引自己主动以适当的次序产生输出时,优化器就避免了排序的步骤。以下是一些影响因素:
●索引中不包含一个或几个待排序的列;
●group by或order by子句中列的次序与索引的次序不一样;
●排序的列来自不同的表。
为了避免不必要的排序,就要正确地增建索引,合理地合并数据库表(虽然有时可能影响表的规范化,但相对于效率的提高是值得的)。假设排序不可避免,那么应当试图简化它,如缩小排序的列的范围等。
3.消除对大型表行数据的顺序存取
在嵌套查询中,对表的顺序存取对查询效率可能产生致命的影响。比方採用顺序存取策略,一个嵌套3层的查询,假设每层都查询1000行,那么这个查询就要查询10亿行数据。避免这样的情况的主要方法就是对连接的列进行索引。比如,两个表:学生表(学号、姓名、年龄……)和选课表(学号、课程号、成绩)。假设两个表要做连接,就要在“学号”这个连接字段上建立索引。
还能够使用并集来避免顺序存取。虽然在全部的检查列上都有索引,但某些形式的where子句强迫优化器使用顺序存取。以下的查询将强迫对orders表运行顺序操作:
SELECT * FROM orders WHERE (customer_num=104 AND order_num>1001) OR order_num=1008
虽然在customer_num和order_num上建有索引,可是在上面的语句中优化器还是使用顺序存取路径扫描整个表。由于这个语句要检索的是分离的行的集合,所以应该改为例如以下语句:
SELECT * FROM orders WHERE customer_num=104 AND order_num>1001
UNION
SELECT * FROM orders WHERE order_num=1008
这样就能利用索引路径处理查询。
4.避免相关子查询
一个列的标签同一时候在主查询和where子句中的查询中出现,那么非常可能当主查询中的列值改变之后,子查询必须又一次查询一次。查询嵌套层次越多,效率越低,因此应当尽量避免子查询。假设子查询不可避免,那么要在子查询中过滤掉尽可能多的行。
5.避免困难的正规表达式
MATCHES和LIKEkeyword支持通配符匹配,技术上叫正规表达式。但这样的匹配特别耗费时间。比如:SELECT * FROM customer WHERE zipcode LIKE “98_ _ _”
即使在zipcode字段上建立了索引,在这样的情况下也还是採用顺序扫描的方式。假设把语句改为SELECT * FROM customer WHERE zipcode >“98000”,在运行查询时就会利用索引来查询,显然会大大提快速度。
另外,还要避免非開始的子串。比如语句:SELECT * FROM customer WHERE zipcode[2,3] >“80”,在where子句中採用了非開始子串,因而这个语句也不会使用索引。
6.使用暂时表加速查询
把表的一个子集进行排序并创建暂时表,有时能加速查询。有助于避免多重排序操作,并且在其它方面还能简化优化器的工作。比如:
SELECT cust.name,rcvbles.balance,……other columns
FROM cust,rcvbles
WHERE cust.customer_id = rcvlbes.customer_id
AND rcvblls.balance>0
AND cust.postcode>“98000”
ORDER BY cust.name
假设这个查询要被运行多次而不止一次,能够把全部未付款的客户找出来放在一个暂时文件里,并按客户的名字进行排序:
SELECT cust.name,rcvbles.balance,……other columns
FROM cust,rcvbles
WHERE cust.customer_id = rcvlbes.customer_id
AND rcvblls.balance>0
ORDER BY cust.name
INTO TEMP cust_with_balance
然后以以下的方式在暂时表中查询:
SELECT * FROM cust_with_balance
WHERE postcode>“98000”
暂时表中的行要比主表中的行少,并且物理顺序就是所要求的顺序,降低了磁盘I/O,所以查询工作量能够得到大幅降低。
注意:暂时表创建后不会反映主表的改动。在主表中数据频繁改动的情况下,注意不要丢失数据。
7.用排序来代替非顺序存取
非顺序磁盘存取是最慢的操作,表如今磁盘存取臂的来回移动。SQL语句隐藏了这一情况,使得在写应用程序时非常easy写出要求存取大量非顺序页的查询。
有些时候,用数据库的排序能力来替代非顺序的存取能改进查询。
实例分析
以下我们举一个制造公司的样例来说明怎样进行查询优化。制造公司数据库中包含3个表,模式例如以下所看到的:
1.part表
零件号 零件描写叙述 其它列
(part_num) (part_desc) (other column)
102,032 Seageat 30G disk ……
500,049 Novel 10M network card ……
……
2.vendor表
厂商号 厂商名 其它列
(vendor _num) (vendor_name) (other column)
910,257 Seageat Corp ……
523,045 IBM Corp ……
……
3.parven表
零件号 厂商号 零件数量
(part_num) (vendor_num) (part_amount)
102,032 910,257 3,450,000
234,423 321,001 4,000,000
……
以下的查询将在这些表上定期执行,并产生关于全部零件数量的报表:
SELECT part_desc,vendor_name,part_amount
FROM part,vendor,parven
WHERE part.part_num=parven.part_num
AND parven.vendor_num = vendor.vendor_num
ORDER BY part.part_num
假设不建立索引,上述查询代码的开销将十分巨大。为此,我们在零件号和厂商号上建立索引。索引的建立避免了在嵌套中重复扫描。关于表与索引的统计信息例如以下:
表 行尺寸 行数量 每页行数量 数据页数量
(table) (row size) (Row count) (Rows/Pages) (Data Pages)
part 150 10,000 25 400
Vendor 150 1,000 25 40
Parven 13 15,000 300 50
索引 键尺寸 每页键数量 页面数量
(Indexes) (Key Size) (Keys/Page) (Leaf Pages)
part 4 500 20
Vendor 4 500 2
Parven 8 250 60
看起来是个相对简单的3表连接,可是其查询开销是非常大的。通过查看系统表能够看到,在part_num上和vendor_num上有簇索引,因此索引是依照物理顺序存放的。parven表没有特定的存放次序。这些表的大小说明从缓冲页中非顺序存取的成功率非常小。此语句的优化查询规划是:首先从part中顺序读取400页,然后再对parven表非顺序存取1万次,每次2页(一个索引页、一个数据页),总计2万个磁盘页,最后对vendor表非顺序存取1.5万次,合3万个磁盘页。能够看出在这个索引好的连接上花费的磁盘存取为5.04万次。
实际上,我们能够通过使用暂时表分3个步骤来提高查询效率:
1.从parven表中按vendor_num的次序读数据:
SELECT part_num,vendor_num,price
FROM parven
ORDER BY vendor_num
INTO temp pv_by_vn
这个语句顺序读parven(50页),写一个暂时表(50页),并排序。假定排序的开销为200页,总共是300页。
2.把暂时表和vendor表连接,把结果输出到一个暂时表,并按part_num排序:
SELECT pv_by_vn,* vendor.vendor_num
FROM pv_by_vn,vendor
WHERE pv_by_vn.vendor_num=vendor.vendor_num
ORDER BY pv_by_vn.part_num
INTO TMP pvvn_by_pn
DROP TABLE pv_by_vn
这个查询读取pv_by_vn(50页),它通过索引存取vendor表1.5万次,但因为按vendor_num次序排列,实际上仅仅是通过索引顺序地读vendor表(40+2=42页),输出的表每页约95行,共160页。写并存取这些页引发5*160=800次的读写,索引共读写892页。
3.把输出和part连接得到最后的结果:
SELECT pvvn_by_pn.*,part.part_desc
FROM pvvn_by_pn,part
WHERE pvvn_by_pn.part_num=part.part_num
DROP TABLE pvvn_by_pn
这样,查询顺序地读pvvn_by_pn(160页),通过索引读part表1.5万次,因为建有索引,所以实际上进行1772次磁盘读写,优化比例为30∶1。笔者在Informix Dynamic Sever上做相同的实验,发如今时间耗费上的优化比例为5∶1(假设添加数据量,比例可能会更大)。
小 结
20%的代码用去了80%的时间,这是程序设计中的一个著名定律,在数据库应用程序中也相同如此。我们的优化要抓住关键问题,对于数据库应用程序来说,重点在于SQL的运行效率。查询优化的重点环节是使得数据库server少从磁盘中读数据以及顺序读页而不是非顺序读页。
百万数据查询优化技巧三十则
1.对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引。
2.应尽量避免在 where 子句中对字段进行 null 值推断,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num is null
能够在num上设置默认值0,确保表中num列没有null值,然后这样查询:
select id from t where num=0
3.应尽量避免在 where 子句中使用!=或<>操作符,否则将引擎放弃使用索引而进行全表扫描。
4.应尽量避免在 where 子句中使用 or 来连接条件,否则将导致引擎放弃使用索引而进行全表扫描,如:
select id from t where num=10 or num=20
能够这样查询:
select id from t where num=10
union all
select id from t where num=20
5.in 和 not in 也要慎用,否则会导致全表扫描,如:
select id from t where num in(1,2,3)
对于连续的数值,能用 between 就不要用 in 了:
select id from t where num between 1 and 3
6.以下的查询也将导致全表扫描:
select id from t where name like '%abc%'
若要提高效率,能够考虑全文检索。
7.假设在 where 子句中使用參数,也会导致全表扫描。由于SQL仅仅有在执行时才会解析局部变量,但优化程序不能将訪问计划的选择推迟到执行时;它必须在编译时进行选择。然而,假设在编译时建立訪问计划,变量的值还是未知的,因而无法作为索引选择的输入项。如以下语句将进行全表扫描:
select id from t where num=@num
能够改为强制查询使用索引:
select id from t with(index(索引名)) where num=@num
8.应尽量避免在 where 子句中对字段进行表达式操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where num/2=100
应改为:
select id from t where num=100*2
9.应尽量避免在where子句中对字段进行函数操作,这将导致引擎放弃使用索引而进行全表扫描。如:
select id from t where substring(name,1,3)='abc'--name以abc开头的id
select id from t where datediff(day,createdate,'2005-11-30')=0--‘2005-11-30’生成的id
应改为:
select id from t where name like 'abc%'
select id from t where createdate>='2005-11-30' and createdate<'2005-12-1'
10.不要在 where 子句中的“=”左边进行函数、算术运算或其它表达式运算,否则系统将可能无法正确使用索引。
11.在使用索引字段作为条件时,假设该索引是复合索引,那么必须使用到该索引中的第一个字段作为条件时才干保证系统使用该索引,否则该索引将不会被使用,而且应尽可能的让字段顺序与索引顺序相一致。
12.不要写一些没有意义的查询,如须要生成一个空表结构:
select col1,col2 into #t from t where 1=0
这类代码不会返回不论什么结果集,可是会消耗系统资源的,应改成这样:
create table #t(...)
13.非常多时候用 exists 取代 in 是一个好的选择:
select num from a where num in(select num from b)
用以下的语句替换:
select num from a where exists(select 1 from b where num=a.num)
14.并非全部索引对查询都有效,SQL是依据表中数据来进行查询优化的,当索引列有大量数据反复时,SQL查询可能不会去利用索引,如一表中有字段sex,male、female差点儿各一半,那么即使在sex上建了索引也对查询效率起不了作用。
15.索引并非越多越好,索引固然能够提高对应的 select 的效率,但同一时候也减少了 insert 及 update 的效率,由于 insert 或 update 时有可能会重建索引,所以如何建索引须要谨慎考虑,视详细情况而定。一个表的索引数最好不要超过6个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
16.应尽可能的避免更新 clustered 索引数据列,由于 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统须要频繁更新 clustered 索引数据列,那么须要考虑是否应将该索引建为 clustered 索引。
17.尽量使用数字型字段,若仅仅含数值信息的字段尽量不要设计为字符型,这会减少查询和连接的性能,并会添加存储开销。这是由于引擎在处理查询和连接时会逐个比較字符串中每个字符,而对于数字型而言仅仅须要比較一次就够了。
18.尽可能的使用 varchar/nvarchar 取代 char/nchar ,由于首先变长字段存储空间小,能够节省存储空间,其次对于查询来说,在一个相对较小的字段内搜索效率显然要高些。
19.不论什么地方都不要使用 select * from t ,用详细的字段列表取代“*”,不要返回用不到的不论什么字段。
20.尽量使用表变量来取代暂时表。假设表变量包括大量数据,请注意索引很有限(仅仅有主键索引)。
21.避免频繁创建和删除暂时表,以降低系统表资源的消耗。
22.暂时表并非不可使用,适当地使用它们能够使某些例程更有效,比如,当须要反复引用大型表或经常使用表中的某个数据集时。可是,对于一次性事件,最好使用导出表。
23.在新建暂时表时,假设一次性插入数据量非常大,那么能够使用 select into 取代 create table,避免造成大量 log ,以提快速度;假设数据量不大,为了缓和系统表的资源,应先create table,然后insert。
24.假设使用到了暂时表,在存储过程的最后务必将全部的暂时表显式删除,先 truncate table ,然后 drop table ,这样能够避免系统表的较长时间锁定。
25.尽量避免使用游标,由于游标的效率较差,假设游标操作的数据超过1万行,那么就应该考虑改写。
26.使用基于游标的方法或暂时表方法之前,应先寻找基于集的解决方式来解决这个问题,基于集的方法通常更有效。
27.与暂时表一样,游标并非不可使用。对小型数据集使用 FAST_FORWARD 游标通常要优于其它逐行处理方法,尤其是在必须引用几个表才干获得所需的数据时。在结果集中包含“合计”的例程通常要比使用游标运行的速度快。假设开发时间同意,基于游标的方法和基于集的方法都能够尝试一下,看哪一种方法的效果更好。
28.在全部的存储过程和触发器的開始处设置 SET NOCOUNT ON ,在结束时设置 SET NOCOUNT OFF 。无需在运行存储过程和触发器的每一个语句后向client发送 DONE_IN_PROC 消息。
29.尽量避免大事务操作,提高系统并发能力。
30.尽量避免向client返回大数据量,若数据量过大,应该考虑对应需求是否合理。