15. sql语句优化
GitHub仓库地址点这里,资源大全
什么是’执行计划’
执行计划
是数据库根据SQL语句和相关表的统计信息作出的一个查询方案
,这个方案是由查询优化器
自动分析产生的,比如一条SQL语句如果用来从一个 10万条记录的表中查1条记录,那查询优化器会选择“索引查找”方式,如果该表进行了归档,当前只剩下5000条记录了,那查询优化器就会改变方案,采用 “全表扫描”方式。可见,执行计划并不是固定的,它是“带有相当个性的”。
写出统一的sql语句
对于以下两句SQL语句,很多人人认为是相同的,但是,数据库查询优化器认为是不同的。
select * from dual
select * From dual
虽然只是大小写不同,查询分析器就认为是两句不同的SQL语句,必须进行两次解析。生成2个执行计划
。所以作为程序员,应该保证相同的查询语句在任何地方都一致,多一个空格都不行!
不要把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语句的负担。一次解析,多次重用,是提高数据库效率的原则,同时也会有效的解决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条原则。
- 查询的结果用于“插、删、改”的不能加nolock !
- 查询的表属于频繁发生页分裂的,慎用nolock !
使用临时表一样可以
保存“数据前影”,起到类似oracle的undo表空间
的功能,能采用临时表提高并发性能的,不要用nolock 。
聚集索引要建在表的顺序字段上,否则表容易发生页分裂
比如订单表,有订单编号orderid
,也有客户编号contactid
,那么聚集索引应该加在哪个字段上呢?对于该表,订单编号
是顺序添加
的,如果在orderid上加聚集索引,新增的行都是添加在末尾,这样不容易经常产生页分裂
。然而,由于大多数查询
都是根据客户编号
来查的,因此,将聚集索引
加在contactid上才有意义。而contactid
对于订单表而言,并非顺序字段
。
比如“张三”的“contactid”是001,那么“张三”的订单信息必须都放在这张表的第一个数据页上,如果今天“张三”新下了一个订单,那该订单信息不能放在表的最后一页,而是第一页!如果第一页放满了呢?很抱歉,该表所有数据都要往后移动为这条记录腾地方。
SQL Server的索引和Oracle的索引是不同的
,SQL Server的聚集索引
实际上是对表按照聚集索引字段的顺序进行了排序,相当于oracle的索引组织表
。SQL Server的聚集索引就是表本身
的一种组织形式
,所以它的效率是非常高的。也正因为此,插入
一条记录,它的位置不是随便放的,而是要按照顺序
放在该放的数据页
,如果那个数据页没有空间
了,就引起了页分裂
。所以很显然,聚集索引
没有
建在表的顺序字段
上,该表容易发生页分裂。
曾经碰到过一个情况,一位哥们的某张表重建索引后,插入的效率大幅下降了。估计情况大概是这样的。该表的聚集索引可能没有建在表的顺序字段上,该表经常被归档,所以该表的数据是以一种稀疏状态存在的。比如张三下过20张订单,而最近3个月的订单只有5张,归档策略
是保留3个月数据,那么张三过去的 15张订单已经被归档,留下15个空位,可以在insert发生时
重新被利用。在这种情况下由于有空位
可以利用,就不会发生页分裂
。但是查询性能
会比较低
,因为查询时必须扫描
那些没有数据的空位
。
重建聚集索引后情况改变了,因为重建聚集索引
就是把表中的数据重新排列一遍,原来的空位没有
了,而页的填充率
又很高,插入数据经常要发生页分裂,所以性能大幅下降。
对于聚集索引
没有建在顺序字段上的表,是否
要给与比较低的页填充率?是否
要避免重建聚集索引?是一个值得考虑的问题!
加noblock查询要小心
加nolock后查询经常发生页分裂的表,容易产生跳读或重复读;
加nolock后可以在“插、删、改
”的同时
进行查询
,但是由于同时发生“插、删、改”,在某些情况下,一旦
该数据页满了
,那么页分裂
不可避免,而此时nolock的查询
正在发生,比如在第100页
已经读过的记录,可能会因为
页分裂而分到第101页
,这有可能使得nolock查询在读101页时重复读到该条数据,产生“重复读
”。同理,如果在100页上
的数据还没被读到就分到99页
去了,那nolock查询有可能会漏过
该记录,产生“跳读
”。
上面提到的哥们,在加了nolock后一些操作出现报错
,估计有可能因为nolock查询产生了重复读
,2条相同
的记录
去插入别的表,当然会发生主键冲突
。
合理使用模糊查询
有的时候会需要进行一些模糊查询
比如:
select * from contact where username like ‘%yue%’
关键词 %yue%
,由于yue前面用到了“%”,因此该查询必然走全表扫描
,除非必要
,否则不要
在关键词前加%。
注意数据类型的隐式转换对查询的影响
sql server2000
的数据库,我们的程序在提交sql语句的时候,没有使用强类型
提交这个字段的值,由sql server 2000自动转换
数据类型,导致传入的参数与主键字段类型不一致
,这个时候sql server 2000可能就会使用全表扫描
。Sql server2005上没有发现这种问题,但是还是应该注意一下。
sql server版本对表连接的影响
SQL Server 表连接的三种方式:
Merge Join
Nested Loop Join
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好多了。