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

fmms 13年前
     <p>最近公司来一个非常虎的DBA,10几年的经验,这里就称之为蔡老师吧,在征得我们蔡老同意的前提下 ,我们来分享一下蔡老给我们带来的宝贵财富,欢迎其他的DBA来拍砖。</p>    <hr />    <p><strong> 目录</strong></p>    <p><a href="/misc/goto?guid=4959498459481143395">1、什么是执行计划?执行计划是依赖于什么信息</a><br /> <a href="/misc/goto?guid=4959498459558134408">2、 统一SQL语句的写法减少解析开销</a><br /> <a href="/misc/goto?guid=4959498459639710291">3、 减少SQL语句的嵌套</a><br /> <a href="/misc/goto?guid=4959498459710227056">4、 使用“临时表”暂存中间结果</a><br /> <a href="/misc/goto?guid=4959498459785103281">5、 OLTP系统SQL语句必须采用绑定变量</a><br /> <a href="/misc/goto?guid=4959498459871252033">6、 倾斜字段的绑定变量窥测问题</a><br /> <a href="/misc/goto?guid=4959498459947306977">7、 begin tran的事务要尽量地小</a><br /> <a href="/misc/goto?guid=4959498460024593287">8、 一些SQL查询语句应加上nolock</a><br /> <a href="/misc/goto?guid=4959498460109687283">9、加nolock后查询经常发生页分裂的表,容易产生跳读或重复读</a><br /> <a href="/misc/goto?guid=4959498460182844134">10、聚集索引没有建在表的顺序字段上,该表容易发生页分裂</a><br /> <a href="/misc/goto?guid=4959498460261683780">11、使用复合索引提高多个where条件的查询速度</a><br /> <a href="/misc/goto?guid=4959498460332443329">12、使用like进行模糊查询时应注意尽量不要使用前%</a><br /> <a href="/misc/goto?guid=4959498460405606933">13、SQL Server 表连接的三种方式</a><br /> <a href="/misc/goto?guid=4959498460484792982">14、Row_number 会导致表扫描,用临时表分页更好</a></p>    <hr />    <p><strong><a name="s1"></a>什么是执行计划?执行计划是依赖于什么信息。</strong></p>    <p>执行计划是数据库根据SQL语句和相关表的统计信息作出的一个查询方案,这个方案是由查询优化器自动分析产生的,比如一条SQL语句如果用来从一个 10万条记录的表中查1条记录,那查询优化器会选择“索引查找”方式,如果该表进行了归档,当前只剩下5000条记录了,那查询优化器就会改变方案,采用 “全表扫描”方式。</p>    <p>可见,执行计划并不是固定的,它是“个性化的”。产生一个正确的“执行计划”有两点很重要:</p>    <ol>     <li>SQL语句是否清晰地告诉查询优化器它想干什么?</li>     <li>查询优化器得到的数据库统计信息是否是最新的、正确的?</li>    </ol>    <hr />    <p><strong><a name="s2"></a>统一SQL语句的写法减少解析开销</strong></p>    <p class="p0">对于以下两句<span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句,程序员认为是相同的,数据库查询优化器</span>可能认为是不同的。</p>    <p class="p0">select * from dual</p>    <p class="p0">Select * From dual</p>    <p class="p0">其实就是大小写不同,查询分析器就认为是两句不同的<span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句,必须进行两次解析。生成</span><span style="font-family:Times New Roman;">2</span><span style="font-family:宋体;">个执行计划。所以作为程序员,应该保证相同的查询语句在任何地方都一致,多一个空格都不行!</span></p>    <hr />    <p><strong><a name="s3"></a>减少SQL语句的嵌套</strong></p>    <p class="p0">我经常看到,从数据库中捕捉到的一条<span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句打印出来有</span><span style="font-family:Times New Roman;">2</span><span style="font-family:宋体;">张</span><span style="font-family:Times New Roman;">A4</span><span style="font-family:宋体;">纸这么长。一般来说这么复杂的语句通常都是有问题的。我拿着这</span><span style="font-family:Times New Roman;">2</span><span style="font-family:宋体;">页长的</span><span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句去请教原作者,结果他说时间太长,他一时也看不懂了。可想而知,连原作者都有可能看糊涂的</span><span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句,数据库也一样会看糊涂。</span></p>    <p class="p0">一般,将一个<span style="font-family:Times New Roman;">Select</span><span style="font-family:宋体;">语句的结果作为子集,然后从该子集中再进行查询,这种一层嵌套语句还是比较常见的,但是根据经验,超过</span><span style="font-family:Times New Roman;">3</span><span style="font-family:宋体;">层嵌套,查询优化器就很容易给出错误的执行计划。因为它被绕晕了。像这种类似人工智能的东西,终究比人的分辨力要差些,如果人都看晕了,我可以保证数据库也会晕的。</span></p>    <p class="p0">另外,执行计划是可以被重用的,越简单的<span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句被重用的可能性越高。而复杂的</span><span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句只要有一个字符发生变化就必须重新解析,然后再把这一大堆垃圾塞在内存里。可想而知,数据库的效率会何等低下。</span></p>    <hr />    <p class="p0"><strong><span style="font-family:宋体;"><a name="s4"></a>使用“临时表”暂存中间结果</span></strong></p>    <p class="p0">简化SQL语句的重要方法就是采用临时表暂存中间结果,但是,临时表的好处远远不止这些,将临时结果暂存在临时表,后面的查询就在tempdb中了,这可以避免程序中多次扫描主表,也大大减少了程序执行中“共享锁”阻塞“更新锁”,减少了阻塞,提高了并发性能。</p>    <hr />    <p class="p0"><strong><a name="s5"></a>OLTP系统SQL语句必须采用绑定变量</strong></p>    <p class="p0">select * from orderheader where changetime > ‘2010-10-20 00:00:01’<br /> select * from orderheader where changetime > ‘2010-09-22 00:00:01’<br /> 以上两句语句,查询优化器认为是不同的SQL语句,需要解析两次。如果采用绑定变量<br /> select * from orderheader where changetime > @chgtime<br /> @chgtime变量可以传入任何值,这样大量的类似查询可以重用该执行计划了,这可以大大降低数据库解析SQL语句的负担。一次解析,多次重用,是提高数据库效率的原则。</p>    <hr />    <p class="p0"><strong><a name="s6"></a>倾斜字段的绑定变量窥测问题</strong></p>    <p>事物都存在两面性,绑定变量对大多数OLTP处理是适用的,但是也有例外。比如在where条件中的字段是“倾斜字段”的时候。</p>    <p>“倾斜字段”指该列中的绝大多数的值都是相同的,比如一张人口调查表,其中“民族”这列,90%以上都是汉族。那么如果一个SQL语句要查询30岁的汉族人口有多少,那“民族”这列必然要被放在where条件中。这个时候如果采用绑定变量@nation会存在很大问题。</p>    <p>试想如果@nation传入的第一个值是“汉族”,那整个执行计划必然会选择表扫描。然后,第二个值传入的是“布依族”,按理说“布依族”占的比例可能只有万分之一,应该采用索引查找。但是,由于重用了第一次解析的“汉族”的那个执行计划,那么第二次也将采用表扫描方式。这个问题就是著名的“绑定变量窥测”,建议对于“倾斜字段”不要采用绑定变量。</p>    <hr />    <p><strong><a name="s7"></a>begin tran的事务要尽量地小</strong></p>    <p>SQL Server中一句SQL语句默认就是一个事务,在该语句执行完成后也是默认commit的。其实,这就是begin tran的一个最小化的形式,好比在每句语句开头隐含了一个begin tran,结束时隐含了一个commit。<br /> 有些情况下,我们需要显式声明begin tran,比如做“插、删、改”操作需要同时修改几个表,要求要么几个表都修改成功,要么都不成功。begin tran 可以起到这样的作用,它可以把若干SQL语句套在一起执行,最后再一起commit。好处是保证了数据的一致性,但任何事情都不是完美无缺的。Begin tran付出的代价是在提交之前,所有SQL语句锁住的资源都不能释放,直到commit掉。<br /> 可见,如果Begin tran套住的SQL语句太多,那数据库的性能就糟糕了。在该大事务提交之前,必然会阻塞别的语句,造成block很多。<br /> Begin tran使用的原则是,在保证数据一致性的前提下,begin tran 套住的SQL语句越少越好!有些情况下可以采用触发器同步数据,不一定要用begin tran。</p>    <hr />    <p><strong><a name="s8"></a>一些SQL查询语句应加上nolock</strong></p>    <p class="p0">在<span style="font-family:Times New Roman;">SQL</span><span style="font-family:宋体;">语句中加</span><span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">是提高</span><span style="font-family:Times New Roman;">SQL Server</span><span style="font-family:宋体;">并发性能的重要手段,在</span><span style="font-family:Times New Roman;">oracle</span><span style="font-family:宋体;">中并不需要这样做,因为</span><span style="font-family:Times New Roman;">oracle</span><span style="font-family:宋体;">的结构更为合理,有</span><span style="font-family:Times New Roman;">undo</span><span style="font-family:宋体;">表空间保存“数据前影”,该数据如果在修改中还未</span><span style="font-family:Times New Roman;">commit</span><span style="font-family:宋体;">,那么你读到的是它修改之前的副本,该副本放在</span><span style="font-family:Times New Roman;">undo</span><span style="font-family:宋体;">表空间中。这样,</span><span style="font-family:Times New Roman;">oracle</span><span style="font-family:宋体;">的读、写可以做到互不影响,这也是</span><span style="font-family:Times New Roman;">oracle</span><span style="font-family:宋体;">广受称赞的地方。</span><span style="font-family:Times New Roman;">SQL Server </span><span style="font-family:宋体;">的读、写是会相互阻塞的,为了提高并发性能,对于一些查询,可以加上</span><span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">,这样读的时候可以允许写,但缺点是可能读到未提交的脏数据。使用</span><span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">有</span><span style="font-family:Times New Roman;">3</span><span style="font-family:宋体;">条原则。</span></p>    <p class="p0">(1) 查询的结果用于“插、删、改”的不能加<span style="font-family:Times New Roman;">nolock </span><span style="font-family:宋体;">!</span></p>    <p class="p0">(2) 查询的表属于频繁发生页分裂的,慎用<span style="font-family:Times New Roman;">nolock </span><span style="font-family:宋体;">!</span></p>    <p class="p0">(3) 使用临时表一样可以保存“数据前影”,起到类似<span style="font-family:Times New Roman;">oracle</span><span style="font-family:宋体;">的</span><span style="font-family:Times New Roman;">undo</span><span style="font-family:宋体;">表空间的功能,</span></p>    <p>能采用临时表提高并发性能的,不要用<span style="font-family:Times New Roman;">nolock </span><span style="font-family:宋体;">。</span></p>    <hr />    <p><strong><span style="font-family:宋体;"><a name="s9"></a>加nolock后查询经常发生页分裂的表,容易产生跳读或重复读</span></strong></p>    <p class="p0">加<span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">后可以在“插、删、改”的同时进行查询,但是由于同时发生“插、删、改”,在某些情况下,一旦该数据页满了,那么页分裂不可避免,而此时</span><span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">的查询正在发生,比如在第</span><span style="font-family:Times New Roman;">100</span><span style="font-family:宋体;">页已经读过的记录,可能会因为页分裂而分到第</span><span style="font-family:Times New Roman;">101</span><span style="font-family:宋体;">页,这有可能使得</span><span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">查询在读</span><span style="font-family:Times New Roman;">101</span><span style="font-family:宋体;">页时重复读到该条数据,产生“重复读”。同理,如果在</span><span style="font-family:Times New Roman;">100</span><span style="font-family:宋体;">页上的数据还没被读到就分到</span><span style="font-family:Times New Roman;">99</span><span style="font-family:宋体;">页去了,那</span><span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">查询有可能会漏过该记录,产生“跳读”。</span></p>    <p> 上面提到的哥们,在加了<span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">后一些操作出现报错,估计有可能因为</span><span style="font-family:Times New Roman;">nolock</span><span style="font-family:宋体;">查询产生了重复读,</span><span style="font-family:Times New Roman;">2</span><span style="font-family:宋体;">条相同的记录去插入别的表,当然会发生主键冲突。</span></p>    <hr />    <p class="p0"><strong><a name="s10"></a>聚集索引没有建在表的顺序字段上,该表容易发生页分裂</strong></p>    <p class="p0">比如订单表,有订单编号<span style="font-family:Times New Roman;">orderid</span><span style="font-family:宋体;">,也有客户编号</span><span style="font-family:Times New Roman;">contactid</span><span style="font-family:宋体;">,那么聚集索引应该加在哪个字段上呢?对于该表,订单编号是顺序添加的,如果在</span><span style="font-family:Times New Roman;">orderid</span><span style="font-family:宋体;">上加聚集索引,新增的行都是添加在末尾,这样不容易经常产生页分裂。然而,由于大多数查询都是根据客户编号来查的,因此,将聚集索引加在</span><span style="font-family:Times New Roman;">contactid</span><span style="font-family:宋体;">上才有意义。而</span><span style="font-family:Times New Roman;">contactid</span><span style="font-family:宋体;">对于订单表而言,并非顺序字段。</span></p>    <p class="p0">比如“张三”的“<span style="font-family:Times New Roman;">contactid</span><span style="font-family:宋体;">”是</span><span style="font-family:Times New Roman;">001</span><span style="font-family:宋体;">,那么“张三”的订单信息必须都放在这张表的第一个数据页上,如果今天“张三”新下了一个订单,那该订单信息不能放在表的最后一页,而是第一页!如果第一页放满了呢?很抱歉,该表所有数据都要往后移动为这条记录腾地方。</span></p>    <p class="p0">SQL Server<span style="font-family:宋体;">的索引和</span><span style="font-family:Times New Roman;">Oracle</span><span style="font-family:宋体;">的索引是不同的,</span><span style="font-family:Times New Roman;">SQL Server</span><span style="font-family:宋体;">的聚集索引实际上是对表按照聚集索引字段的顺序进行了排序,相当于</span><span style="font-family:Times New Roman;">oracle</span><span style="font-family:宋体;">的索引组织表。</span><span style="font-family:Times New Roman;">SQL Server</span><span style="font-family:宋体;">的聚集索引就是表本身的一种组织形式,所以它的效率是非常高的。也正因为此,插入一条记录,它的位置不是随便放的,而是要按照顺序放在该放的数据页,如果那个数据页没有空间了,就引起了页分裂。所以很显然,聚集索引没有建在表的顺序字段上,该表容易发生页分裂。</span></p>    <p class="p0">曾经碰到过一个情况,一位哥们的某张表重建索引后,插入的效率大幅下降了。估计情况大概是这样的。该表的聚集索引可能没有建在表的顺序字段上,该表经常被归档,所以该表的数据是以一种稀疏状态存在的。比如张三下过<span style="font-family:Times New Roman;">20</span><span style="font-family:宋体;">张订单,而最近</span><span style="font-family:Times New Roman;">3</span><span style="font-family:宋体;">个月的订单只有</span><span style="font-family:Times New Roman;">5</span><span style="font-family:宋体;">张,归档策略是保留</span><span style="font-family:Times New Roman;">3</span><span style="font-family:宋体;">个月数据,那么张三过去的</span><span style="font-family:Times New Roman;">15</span><span style="font-family:宋体;">张订单已经被归档,留下</span><span style="font-family:Times New Roman;">15</span><span style="font-family:宋体;">个空位,可以在</span><span style="font-family:Times New Roman;">insert</span><span style="font-family:宋体;">发生时重新被利用。在这种情况下由于有空位可以利用,就不会发生页分裂。但是查询性能会比较低,因为查询时必须扫描那些没有数据的空位。</span></p>    <p class="p0">重建聚集索引后情况改变了,因为重建聚集索引就是把表中的数据重新排列一遍,原来的空位没有了,而页的填充率又很高,插入数据经常要发生页分裂,所以性能大幅下降。</p>    <p>对于聚集索引没有建在顺序字段上的表,是否要给与比较低的页填充率?是否要避免重建聚集索引?是一个值得考虑的问题!</p>    <hr />    <p><strong><a name="s11"></a>使用复合索引提高多个where条件的查询速度</strong></p>    <p class="p0">复合索引通常拥有比单一索引更好的选择性。而且,它是特别针对某个<span style="font-family:Times New Roman;">where</span><span style="font-family:宋体;">条件所设立的索引,它已经进行了排序,所以查询速度比单索引更快。复合索引的引导字段必须采用“选择性高”的字段。比如有</span><span style="font-family:Times New Roman;">3</span><span style="font-family:宋体;">个字段:日期,性别,年龄。大家看,应该采用哪个字段作引导字段?显然应该采用“日期”作为引导字段。日期是</span><span style="font-family:Times New Roman;">3</span><span style="font-family:宋体;">个字段中选择性最高的字段。</span></p>    <p class="p0">这里有一个例外,如果日期同时也是聚集索引的引导字段,可以不建复合索引,直接走聚集索引,效率也是比较高的。</p>    <p class="p0">不要把聚集索引建成“复合索引”,聚集索引越简单越好,选择性越高越好!聚集索引包括<span style="font-family:Times New Roman;">2</span><span style="font-family:宋体;">个字段尚可容忍。但是超过</span><span style="font-family:Times New Roman;">2</span><span style="font-family:宋体;">个字段,应该考虑建</span><span style="font-family:Times New Roman;">1</span><span style="font-family:宋体;">个自增字段作为主键,聚集索引可以不做主键。</span></p>    <hr />    <p class="p0"><strong><span style="font-family:宋体;"><a name="s12"></a>使用like进行模糊查询时应注意尽量不要使用前%</span></strong></p>    <p class="p0">有的时候会需要进行一些模糊查询比如</p>    <p class="p0"> Select * from contact where username like ‘%yue%’</p>    <p class="p0">关键词<span style="font-family:Times New Roman;">%yue%</span><span style="font-family:宋体;">,由于</span><span style="font-family:Times New Roman;">yue</span><span style="font-family:宋体;">前面用到了“</span><span style="font-family:Times New Roman;">%</span><span style="font-family:宋体;">”,因此该查询必然走全表扫描,除非必要,否则不要在关键词前加</span><span style="font-family:Times New Roman;">%</span><span style="font-family:宋体;">,</span></p>    <hr />    <p><strong><span style="font-family:宋体;"><a name="s13"></a>SQL Server 表连接的三种方式</span></strong></p>    <p class="p0">   (1) Merge Join</p>    <p class="p0">    (2) Nested Loop Join </p>    <p class="p0">    (3) Hash Join </p>    <p class="p0">SQL Server 2000<span style="font-family:宋体;">只有一种</span><span style="font-family:Times New Roman;">join</span><span style="font-family:宋体;">方式——</span>Nested Loop Join<span style="font-family:宋体;">,如果</span><span style="font-family:Times New Roman;">A</span><span style="font-family:宋体;">结果集较小,那就默认作为外表,</span><span style="font-family:Times New Roman;">A</span><span style="font-family:宋体;">中每条记录都要去</span><span style="font-family:Times New Roman;">B</span><span style="font-family:宋体;">中扫描一遍,实际扫过的行数相当于</span><span style="font-family:Times New Roman;">A</span><span style="font-family:宋体;">结果集行数</span><span style="font-family:Times New Roman;">x B</span><span style="font-family:宋体;">结果集行数。所以如果两个结果集都很大,那</span><span style="font-family:Times New Roman;">Join</span><span style="font-family:宋体;">的结果很糟糕。</span></p>    <p class="p0">SQL Server 2005<span style="font-family:宋体;">新增了</span><span style="font-family:Times New Roman;">Merge Join</span><span style="font-family:宋体;">,如果</span><span style="font-family:Times New Roman;">A</span><span style="font-family:宋体;">表和</span><span style="font-family:Times New Roman;">B</span><span style="font-family:宋体;">表的连接字段正好是聚集索引所在字段,那么表的顺序已经排好,只要两边拼上去就行了,这种</span><span style="font-family:Times New Roman;">join</span><span style="font-family:宋体;">的开销相当于</span><span style="font-family:Times New Roman;">A</span><span style="font-family:宋体;">表的结果集行数加上</span><span style="font-family:Times New Roman;">B</span><span style="font-family:宋体;">表的结果集行数,一个是加,一个是乘,可见</span><span style="font-family:Times New Roman;">merge join </span><span style="font-family:宋体;">的效果要比</span><span style="font-family:Times New Roman;">Nested Loop Join</span><span style="font-family:宋体;">好多了。</span></p>    <p class="p0">如果连接的字段上没有索引,那<span style="font-family:Times New Roman;">SQL2000</span><span style="font-family:宋体;">的效率是相当低的,而</span><span style="font-family:Times New Roman;">SQL2005</span><span style="font-family:宋体;">提供了</span><span style="font-family:Times New Roman;">Hash join</span><span style="font-family:宋体;">,相当于临时给</span><span style="font-family:Times New Roman;">A</span><span style="font-family:宋体;">,</span><span style="font-family:Times New Roman;">B</span><span style="font-family:宋体;">表的结果集加上索引,因此</span><span style="font-family:Times New Roman;">SQL2005</span><span style="font-family:宋体;">的效率比</span><span style="font-family:Times New Roman;">SQL2000</span><span style="font-family:宋体;">有很大提高,我认为,这是一个重要的原因。</span></p>    <p class="p0">总结一下,在表连接时要注意以下几点:</p>    <p class="p0">(1) 连接字段尽量选择聚集索引所在的字段</p>    <p class="p0">(2) 仔细考虑<span style="font-family:Times New Roman;">where</span><span style="font-family:宋体;">条件,尽量减小</span><span style="font-family:Times New Roman;">A</span><span style="font-family:宋体;">、</span><span style="font-family:Times New Roman;">B</span><span style="font-family:宋体;">表的结果集</span></p>    <p class="p0">(3) 如果很多<span style="font-family:Times New Roman;">join</span><span style="font-family:宋体;">的连接字段都缺少索引,而你还在用</span><span style="font-family:Times New Roman;">SQL2000</span><span style="font-family:宋体;">,干紧升级吧</span><span style="font-family:Times New Roman;">.</span></p>    <hr />    <p><strong><span style="font-family:宋体;"><a name="s14"></a>Row_number 会导致表扫描,用临时表分页更好</span></strong></p>    <p>ROW_Number分页的测试结果:<br /> 使用ROW_Number来分页:CPU 时间= 317265 毫秒,占用时间= 423090 毫秒<br /> 使用临时表来分页:CPU 时间= 1266 毫秒,占用时间= 6705 毫秒</p>    <p>ROW_Number实现是基于order by的,排序对查询的影响显而易见。</p>    <hr />    <p><strong><span style="font-family:宋体;">其他</span></strong></p>    <p><span style="font-family:宋体;">诸如</span>有的写法会限制使用索引 </p>    <p class="p0">Select * from tablename where chgdate +7 < sysdate</p>    <p class="p0">Select * from tablename where chgdate < sysdate -7<br /> <br /> 转自:<a href="/misc/goto?guid=4959498460578179803">http://www.cnblogs.com/dubing/archive/2011/12/09/2278090.html</a><br /> </p>