写有效率的SQL查询(III)

系统 1704 0

先说说这些误区。所谓“误区”,有一些是新手很容易犯的错误或者很容易忽略的问题,另外一些,则是像“耗子吃了盐会变成蝙蝠”一样,让我们从小就认为是正确的事情。如下:

1、  表上不管用得着用不着,都加个聚集索引。

我们知道,表以两种方式组织物理存储:有聚集索引的“聚集表”;没有聚集索引的“堆”。在聚集表中,数据行按照聚集索引的顺序存储(这也是为啥一张表最多只能有一个聚集索引的原因);堆中,数据行的存储可以认为是不确定的。

在偶《写有效率的 SQL 查询( II )》中曾经介绍过 DB 引擎如何在聚集表中通过非聚集索引查找目标数据:从非聚集索引树根开始 seek ,查找到目标索引行,然后通过索引行上存储的聚集索引键值,爬聚集索引树,并最终通过聚集索引行上的指针拿到目标数据。

但是堆上的非聚集索引存储的不是聚集索引键值,它存储的是指向目标行的指针。也就是说,如果在同样的表是堆,通过非聚集索引 seek 数据将省掉爬聚集索引树的损耗,而可以直接通过非聚集索引行上的行指针直接拿到目标数据。也就是说,在某些情况下,使用堆可以提高系统效率。

这个“某些情况”,就是你的需求,你的系统行为。一般情况下,所有人对要在什么样的字段上创建聚集索引都非常了解;但是不是所有的人都对应该在什么样的系统行为下,不创建聚集索引了解。假设你的表中有字段 col1, col2,col3,col4 等等, col1 col2 的分布密度很低。你观察了系统行为,发现一半的查询是 XXXX where col1 = YYYY ,另一半的查询是 XXXX where col2 = YYYY 。这种情况下,使用堆就是更好的选择。

2、  primary key 就是聚集索引。

primary key 上是得有索引,但是这个索引可不见得一定得是聚集索引。尽管语句

create table testPK

(

           id int identity ( 1 , 1 ) primary key ,

           fname varchar ( 64 )

)

会在 id 列上创建聚集索引。当然,一般主键都是聚集索引,但也仅仅是“一般”而已。个人感觉,聚集索引的唯一目标就是数据检索,它应该建在什么字段上,完全由系统行为决定。“一般主键都是聚集索引”也仅仅是因为多数情况下, primary key 字段上建所有更有益于效率而已。

create table testPK

(

           id int identity ( 1 , 1 ) primary key nonclustered ,

           fname varchar ( 64 )

)

可以创建primary key为非聚集索引

3、  Log 类的表,有事没事加个自增的 Id 列。

这事相信干过的人很多,哈,而且一般还会顺手在这个 Id 列上加上个 primary key 的约束,聚集索引也就被无意识的建上了。就像一个记录用户活动的日志表,一般会有这么几个典型字段: Id LogTime UserId 。实际上对这种表的查询,大多集中在 LogTime UserId 上, Id 完全没有实际意义。你的客服系统查找的,可能仅仅是某个用户的操作记录(一般按时间排序的),或者你的报表系统要生成每天的用户操作统计。想想看,如果干脆砍了 Id 列,并直接把聚集索引创建在 LogTime 上多爽。

4、  是个表就给加个 primary key 约束

就像 3 中的例子, primary 完全没必要。呵呵,这条看着简单是简单了,犯这错误的人,那也不比 3 少。

5、  where 条件里对同一个表中的列做运算或比较,以为创建某种类型的索引可以提高效率。(这种情况下,任何索引都无法提升性能。解决办法见偶前面的“写有效率的 sql 查询”)

见过了无数的这种写法。最常见的,如:一张用户表里有用户注册时间( t1,YYYYDD ),有退订时间( t2,YYYYDD ),现在让你获取存活时间大于 3 天的用户总数:很多人一不注意,就整一个 select count(*) from Users where t1 – t2 > 3 出来。而且常常会臆测在 t1 t2 上建个涵盖索引(或者分别在 t1 t2 上建索引)会让性能提升。

6、  在表上创建了 col1 col2 顺序的涵盖索引(聚集的或非聚集的),但是 where 条件里就一个 col2 > XXX 。这种情况下,就不如分别在 col1 col2 上创建索引。

 

以上的误区,都是在工作中常常犯或遇到的,没遇到的肯定还有,欢迎各位安达补充:)

 

OK ,接下来我们说说“涵盖索引”和 include 索引。

所谓的涵盖索引,就是传统方式在多个列上创建的索引。“ inlude 索引”是 SQL2k5 提供的新功能,允许添加非键列到非聚集索引的叶节点上。

创建涵盖索引:

create index ix_tb_col1_col2 on tb

(

        col1 ,

        col2

)

创建 include 索引:

create index ix_tb_col1 on tb

(

        col1

) include ( col2 , col3 , col4 )

         涵盖索引和 include 索引的区别在于,涵盖索引的所有列都是键列,索引行的物理存储顺序就是 col1 col2 的顺序,这也是误区 6 之所以称为误区的原因。涵盖索引可以是聚集索引,也可以是非聚集索引。

         include 索引 include 的列并不影响索引行的物理存储顺序,它们作为一个挂件“挂在”索引行上。挂着这些挂件的作用在于,诸如 select col2, col3, col4 from tb where col1 = XXX 只需要 seek 一把非聚集索引 ix_tb1_col1 OK 了,拿到索引行就拿到了需要的所有数据。挂件们是要占用索引行空间的,我们知道,索引字段宽度要尽可能窄是选择索引的一项基本原则(这项原则背后的原理是尽可能让索引树深度小),所以并不是 include 的字段越多越好,这得跟你的系统行为有一个平衡。

         从上面叙述可以看到,涵盖索引实际上是 include 索引的加强版。也就是说,你的 where 条件里除了涵盖索引的第一个索引列之外还有其他索引列的比较,创建涵盖索引要比 include 索引高效一点点。同样,维护涵盖索引的消耗也会多少高于 Include 索引。

         聚集索引的索引行直接包含了数据行指针,也就是说,通过聚集索引行,可以直接拿到其他所有列的数据,从某种意义上说,聚集索引就是最大的 include 索引,这也是 include 索引只能是非聚集索引的原因所在。

         OK ,给你一条 SQL 语句:

select col1, col2, col3, col4 from tb where col5 > XXX and col6 > yyy

你既可以在上面创建涵盖索引 col5 col6 ,又可以创建 include 索引( col5/col6 include(col1 col2 col3 col4) 。选择如何创建,就要看你的表各字段宽度、系统行为了。在此不再赘述。

 

最后讲讲如何拿到在文中频频提到的系统行为统计信息。这东西说白了就是各种 SQL 的执行次数、逻辑 IO 、物理 IO 、执行消耗 CPU 时间等等等等。想想看,假如你拿了一份系统中所有 SQL 的文本、执行总次数、逻辑 IO 占用总 IO 比例、物理 IO 占用总 IO 比例、平均逻辑 IO 、平均物理 IO 等等等等,你八成能够指出系统瓶颈所在,老板和伙计们的眼光也会会极大的满足你小小的虚荣心,哈。这些东西就在动态视图 sys.dm_exec_query_stats 里面,自个翻翻联机文档吧:)

拿到系统行为统计信息之后,你终于调整了索引,于是系统明显 nb 了。如果你要看看它变得有多 nb ,可以关注动态视图 sys.dm_db_index_usage_stats ,这个也就不多说了。

 

最后,多读联机文档,多做尝试,尽力不用工具而手写 SQL 才是硬道理。

=====================

关于表上是不是都需要一个聚集索引,各位安达展开了剧烈讨论.摘录部分到这里:
from RicCC:
描述的确不足,是否选择聚集索引不是这么简单
1. heap表的查询,除了table scan和covering index之外,都需要bookmark lookup,covering index的使用是有限的,剩下的都是成本很高的操作。除非对这个表的查询很少。
2. heap的数据页之间没有link,顺序读取数据性能低,I/O开销大。除非每次都用unique index seek。
3. heap每个insert数据都是在末尾,并发的insert阻塞问题比较大。因为insert位置一次只能有一个任务加排它锁。可以用clustered改善。
4. delete多时,heap比clustered更浪费磁盘空间,碎片更严重,并且没有正常的方式消除heap数据页的碎片,只能建clustered或者drop table重建。

目前为止我基本没有发现充足的证据使用heap.
============
index seek跟unique index seek不一样,例如你要找8.1-8.9号的log,执行计划里面只会看到一个index seek,它seek的是第一条数据,从第一条数据到最后一条用的是scan,并且heap肯定要用到rid/index lookup,假如要取的是1.1-8.9,rid/index lookup的成本很可能导致sql server放弃index而使用table scan

综合考虑,使用heap的范围实在是太狭窄,clustered index怎样建倒很有文章,需要极为认真的对待.
============
index是unique的,index条件都给出来了并且全部是=,每次seek操作输出都只有一条记录,就是unique index seek,oracle是有这个操作的
如果不是unique index seek,就一定会有range index scan。sql server heap表的range index scan需要在IAM跟数据页间切换,效率不好,clustered index就是用于改善这种状况,并且充分利用磁盘设备读取连续数据的优化措施
========================================================================

Me:到目前为止,我找到的最有理由使用堆的地方是一张每天产生kw级记录的日志表,这张表上的查询主要以查询指定Id的用户在某段时间内的记录.

写有效率的SQL查询(III)


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

微信扫码或搜索:z360901061

微信扫一扫加我为好友

QQ号联系: 360901061

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

【本文对您有帮助就好】

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

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