走向DBA[MSSQL篇] 从SQL语句的角度 提高数据库

系统 2053 0
原文: 走向DBA[MSSQL篇] 从SQL语句的角度 提高数据库的访问性能

最近公司来一个非常虎的dba  10几年的经验 这里就称之为蔡老师吧 在征得我们蔡老同意的前提下  我们来分享一下蔡老给我们带来的宝贵财富 欢迎其他的dba来拍砖


 目录

1、什么是执行计划?执行计划是依赖于什么信息。
2、 统一SQL语句的写法减少解析开销
3、 减少SQL语句的嵌套
4、 使用“临时表”暂存中间结果
5、 OLTP系统SQL语句必须采用绑定变量
6、 倾斜字段的绑定变量窥测问题
7、 begin tran的事务要尽量地小。
8、 一些SQL查询语句应加上nolock
9、加nolock后查询经常发生页分裂的表,容易产生跳读或重复读
10、聚集索引没有建在表的顺序字段上,该表容易发生页分裂
11、使用复合索引提高多个where条件的查询速度
13、使用like进行模糊查询时应注意尽量不要使用前%
14、SQL Server 表连接的三种方式
15、Row_number 会导致表扫描,用临时表分页更好


 什么是执行计划?执行计划是依赖于什么信息。

执行计划是数据库根据SQL语句和相关表的统计信息作出的一个查询方案,这个方案是由查询优化器自动分析产生的,比如一条SQL语句如果用来从一个10万条记录的表中查1条记录,那查询优化器会选择“索引查找”方式,如果该表进行了归档,当前只剩下5000条记录了,那查询优化器就会改变方案,采用“全表扫描”方式。

可见,执行计划并不是固定的,它是“个性化的”。产生一个正确的“执行计划”有两点很重要:
SQL语句是否清晰地告诉查询优化器它想干什么?
查询优化器得到的数据库统计信息是否是最新的、正确的?


统一SQL语句的写法减少解析开销

对于以下两句 SQL 语句,程序员认为是相同的,数据库查询优化器 可能认为是不同的。

select * from dual

Select * From dual

其实就是大小写不同,查询分析器就认为是两句不同的 SQL 语句,必须进行两次解析。生成 2 个执行计划。所以作为程序员,应该保证相同的查询语句在任何地方都一致,多一个空格都不行!


减少SQL语句的嵌套

我经常看到,从数据库中捕捉到的一条 SQL 语句打印出来有 2 A4 纸这么长。一般来说这么复杂的语句通常都是有问题的。我拿着这 2 页长的 SQL 语句去请教原作者,结果他说时间太长,他一时也看不懂了。可想而知,连原作者都有可能看糊涂的 SQL 语句,数据库也一样会看糊涂。

一般,将一个 Select 语句的结果作为子集,然后从该子集中再进行查询,这种一层嵌套语句还是比较常见的,但是根据经验,超过 3 层嵌套,查询优化器就很容易给出错误的执行计划。因为它被绕晕了。像这种类似人工智能的东西,终究比人的分辨力要差些,如果人都看晕了,我可以保证数据库也会晕的。

另外,执行计划是可以被重用的,越简单的 SQL 语句被重用的可能性越高。而复杂的 SQL 语句只要有一个字符发生变化就必须重新解析,然后再把这一大堆垃圾塞在内存里。可想而知,数据库的效率会何等低下。


使用“临时表”暂存中间结果

 简化SQL语句的重要方法就是采用临时表暂存中间结果,但是,临时表的好处远远不止这些,将临时结果暂存在临时表,后面的查询就在tempdb中了,这可以避免程序中多次扫描主表,也大大减少了程序执行中“共享锁”阻塞“更新锁”,减少了阻塞,提高了并发性能。


OLTP系统SQL语句必须采用绑定变量

select * from orderheader where changetime > ‘2010-10-20 00:00:01’
select * from orderheader where changetime > ‘2010-09-22 00:00:01’
以上两句语句,查询优化器认为是不同的SQL语句,需要解析两次。如果采用绑定变量
select * from orderheader where changetime > @chgtime
@chgtime变量可以传入任何值,这样大量的类似查询可以重用该执行计划了,这可以大大降低数据库解析SQL语句的负担。一次解析,多次重用,是提高数据库效率的原则。


 倾斜字段的绑定变量窥测问题

事物都存在两面性,绑定变量对大多数OLTP处理是适用的,但是也有例外。比如在where条件中的字段是“倾斜字段”的时候。

“倾斜字段”指该列中的绝大多数的值都是相同的,比如一张人口调查表,其中“民族”这列,90%以上都是汉族。那么如果一个SQL语句要查询30岁的汉族人口有多少,那“民族”这列必然要被放在where条件中。这个时候如果采用绑定变量@nation会存在很大问题。

试想如果@nation传入的第一个值是“汉族”,那整个执行计划必然会选择表扫描。然后,第二个值传入的是“布依族”,按理说“布依族”占的比例可能只有万分之一,应该采用索引查找。但是,由于重用了第一次解析的“汉族”的那个执行计划,那么第二次也将采用表扫描方式。这个问题就是著名的“绑定变量窥测”,建议对于“倾斜字段”不要采用绑定变量。


begin tran的事务要尽量地小

SQL Server中一句SQL语句默认就是一个事务,在该语句执行完成后也是默认commit的。其实,这就是begin tran的一个最小化的形式,好比在每句语句开头隐含了一个begin tran,结束时隐含了一个commit。
有些情况下,我们需要显式声明begin tran,比如做“插、删、改”操作需要同时修改几个表,要求要么几个表都修改成功,要么都不成功。begin tran 可以起到这样的作用,它可以把若干SQL语句套在一起执行,最后再一起commit。好处是保证了数据的一致性,但任何事情都不是完美无缺的。Begin tran付出的代价是在提交之前,所有SQL语句锁住的资源都不能释放,直到commit掉。
可见,如果Begin tran套住的SQL语句太多,那数据库的性能就糟糕了。在该大事务提交之前,必然会阻塞别的语句,造成block很多。
Begin tran使用的原则是,在保证数据一致性的前提下,begin tran 套住的SQL语句越少越好!有些情况下可以采用触发器同步数据,不一定要用begin tran。


一些SQL查询语句应加上nolock

SQL 语句中加 nolock 是提高 SQL Server 并发性能的重要手段,在 oracle 中并不需要这样做,因为 oracle 的结构更为合理,有 undo 表空间保存“数据前影”,该数据如果在修改中还未 commit ,那么你读到的是它修改之前的副本,该副本放在 undo 表空间中。这样, oracle 的读、写可以做到互不影响,这也是 oracle 广受称赞的地方。 SQL Server  的读、写是会相互阻塞的,为了提高并发性能,对于一些查询,可以加上 nolock ,这样读的时候可以允许写,但缺点是可能读到未提交的脏数据。使用 nolock 3 条原则。

(1) 查询的结果用于“插、删、改”的不能加 nolock 

(2) 查询的表属于频繁发生页分裂的,慎用 nolock 

(3) 使用临时表一样可以保存“数据前影”,起到类似 oracle undo 表空间的功能,

能采用临时表提高并发性能的,不要用 nolock 


加nolock后查询经常发生页分裂的表,容易产生跳读或重复读

nolock 后可以在“插、删、改”的同时进行查询,但是由于同时发生“插、删、改”,在某些情况下,一旦该数据页满了,那么页分裂不可避免,而此时 nolock 的查询正在发生,比如在第 100 页已经读过的记录,可能会因为页分裂而分到第 101 页,这有可能使得 nolock 查询在读 101 页时重复读到该条数据,产生“重复读”。同理,如果在 100 页上的数据还没被读到就分到 99 页去了,那 nolock 查询有可能会漏过该记录,产生“跳读”。

 上面提到的哥们,在加了 nolock 后一些操作出现报错,估计有可能因为 nolock 查询产生了重复读, 2 条相同的记录去插入别的表,当然会发生主键冲突。


聚集索引没有建在表的顺序字段上,该表容易发生页分裂

比如订单表,有订单编号 orderid ,也有客户编号 contactid ,那么聚集索引应该加在哪个字段上呢?对于该表,订单编号是顺序添加的,如果在 orderid 上加聚集索引,新增的行都是添加在末尾,这样不容易经常产生页分裂。然而,由于大多数查询都是根据客户编号来查的,因此,将聚集索引加在 contactid 上才有意义。而 contactid 对于订单表而言,并非顺序字段。

比如“张三”的“ contactid ”是 001 ,那么“张三”的订单信息必须都放在这张表的第一个数据页上,如果今天“张三”新下了一个订单,那该订单信息不能放在表的最后一页,而是第一页!如果第一页放满了呢?很抱歉,该表所有数据都要往后移动为这条记录腾地方。

SQL Server 的索引和 Oracle 的索引是不同的, SQL Server 的聚集索引实际上是对表按照聚集索引字段的顺序进行了排序,相当于 oracle 的索引组织表。 SQL Server 的聚集索引就是表本身的一种组织形式,所以它的效率是非常高的。也正因为此,插入一条记录,它的位置不是随便放的,而是要按照顺序放在该放的数据页,如果那个数据页没有空间了,就引起了页分裂。所以很显然,聚集索引没有建在表的顺序字段上,该表容易发生页分裂。

曾经碰到过一个情况,一位哥们的某张表重建索引后,插入的效率大幅下降了。估计情况大概是这样的。该表的聚集索引可能没有建在表的顺序字段上,该表经常被归档,所以该表的数据是以一种稀疏状态存在的。比如张三下过 20 张订单,而最近 3 个月的订单只有 5 张,归档策略是保留 3 个月数据,那么张三过去的 15 张订单已经被归档,留下 15 个空位,可以在 insert 发生时重新被利用。在这种情况下由于有空位可以利用,就不会发生页分裂。但是查询性能会比较低,因为查询时必须扫描那些没有数据的空位。

重建聚集索引后情况改变了,因为重建聚集索引就是把表中的数据重新排列一遍,原来的空位没有了,而页的填充率又很高,插入数据经常要发生页分裂,所以性能大幅下降。

对于聚集索引没有建在顺序字段上的表,是否要给与比较低的页填充率?是否要避免重建聚集索引?是一个值得考虑的问题!


使用复合索引提高多个where条件的查询速度

复合索引通常拥有比单一索引更好的选择性。而且,它是特别针对某个 where 条件所设立的索引,它已经进行了排序,所以查询速度比单索引更快。复合索引的引导字段必须采用“选择性高”的字段。比如有 3 个字段:日期,性别,年龄。大家看,应该采用哪个字段作引导字段?显然应该采用“日期”作为引导字段。日期是 3 个字段中选择性最高的字段。

这里有一个例外,如果日期同时也是聚集索引的引导字段,可以不建复合索引,直接走聚集索引,效率也是比较高的。

不要把聚集索引建成“复合索引”,聚集索引越简单越好,选择性越高越好!聚集索引包括 2 个字段尚可容忍。但是超过 2 个字段,应该考虑建 1 个自增字段作为主键,聚集索引可以不做主键。


使用like进行模糊查询时应注意尽量不要使用前%

有的时候会需要进行一些模糊查询比如

 Select * from contact where username like ‘%yue%’

关键词 %yue% ,由于 yue 前面用到了“ % ”,因此该查询必然走全表扫描,除非必要,否则不要在关键词前加 %


SQL Server 表连接的三种方式

   (1) Merge Join

    (2) Nested Loop Join 

    (3) Hash Join 

SQL Server 2000 只有一种 join 方式—— Nested Loop Join ,如果 A 结果集较小,那就默认作为外表, A 中每条记录都要去 B 中扫描一遍,实际扫过的行数相当于 A 结果集行数 x B 结果集行数。所以如果两个结果集都很大,那 Join 的结果很糟糕。

SQL Server 2005 新增了 Merge Join ,如果 A 表和 B 表的连接字段正好是聚集索引所在字段,那么表的顺序已经排好,只要两边拼上去就行了,这种 join 的开销相当于 A 表的结果集行数加上 B 表的结果集行数,一个是加,一个是乘,可见 merge join  的效果要比 Nested Loop Join 好多了。

如果连接的字段上没有索引,那 SQL2000 的效率是相当低的,而 SQL2005 提供了 Hash join ,相当于临时给 A B 表的结果集加上索引,因此 SQL2005 的效率比 SQL2000 有很大提高,我认为,这是一个重要的原因。

总结一下,在表连接时要注意以下几点:

(1) 连接字段尽量选择聚集索引所在的字段

(2) 仔细考虑 where 条件,尽量减小 A B 表的结果集

(3) 如果很多 join 的连接字段都缺少索引,而你还在用 SQL2000 ,干紧升级吧 .


Row_number 会导致表扫描,用其他方式例如top方案并且将大表存进临时表会更好

ROW_Number分页的测试结果:
使用ROW_Number来分页:CPU 时间= 317265 毫秒,占用时间= 423090 毫秒
使用top+临时表来分页:CPU 时间= 1266 毫秒,占用时间= 6705 毫秒

ROW_Number实现是基于order by的,排序对查询的影响显而易见。


其他

诸如 有的写法会限制使用索引 

Select * from tablename where chgdate +7 < sysdate

Select * from tablename where chgdate < sysdate -7

前者会抑制chgdate列上的索引 后者不会


本篇先到此 欢迎有爱的同学拍砖

走向DBA[MSSQL篇] 从SQL语句的角度 提高数据库的访问性能


更多文章、技术交流、商务合作、联系博主

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描下面二维码支持博主2元、5元、10元、20元等您想捐的金额吧,狠狠点击下面给点支持吧,站长非常感激您!手机微信长按不能支付解决办法:请将微信支付二维码保存到相册,切换到微信,然后点击微信右上角扫一扫功能,选择支付二维码完成支付。

【本文对您有帮助就好】

您的支持是博主写作最大的动力,如果您喜欢我的文章,感觉我的文章对您有帮助,请用微信扫描上面二维码支持博主2元、5元、10元、自定义金额等您想捐的金额吧,站长会非常 感谢您的哦!!!

发表我的评论
最新评论 总共0条评论