Nineteen@newsmth

虚荣,那是我最爱的原罪~~
随笔 - 12, 文章 - 0, 评论 - 238, 引用 - 6
数据加载中……

写有效率的SQL查询(III)

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

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

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

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

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

这个“某些情况”,就是你的需求,你的系统行为。一般情况下,所有人对要在什么样的字段上创建聚集索引都非常了解;但是不是所有的人都对应该在什么样的系统行为下,不创建聚集索引了解。假设你的表中有字段col1, col2,col3,col4等等,col1col2的分布密度很低。你观察了系统行为,发现一半的查询是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的约束,聚集索引也就被无意识的建上了。就像一个记录用户活动的日志表,一般会有这么几个典型字段:IdLogTimeUserId。实际上对这种表的查询,大多集中在LogTimeUserId上,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出来。而且常常会臆测在t1t2上建个涵盖索引(或者分别在t1t2上建索引)会让性能提升。

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

 

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

 

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索引的区别在于,涵盖索引的所有列都是键列,索引行的物理存储顺序就是col1col2的顺序,这也是误区6之所以称为误区的原因。涵盖索引可以是聚集索引,也可以是非聚集索引。

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

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

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

         OK,给你一条SQL语句:

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

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

 

最后讲讲如何拿到在文中频频提到的系统行为统计信息。这东西说白了就是各种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的用户在某段时间内的记录.

posted on 2007-08-08 22:47 Nineteen@newsmth 阅读(32783) 评论(29)  编辑 收藏

评论

#1楼    回复  引用  查看    

第一条是不对的。在任何时候都应该为所有的表都加上clustered index。SQL Server本身实现的方式导致,堆的情况下会有很多问题:
1. 会比较容易引起corruption,尤其在SQL2000及之前的版本。
2. 没有办法给出精确的数据库使用大小的报告。

其次,使用clustered index的key来查找一行,和使用rowid查找一行效率是一样的,从概念上来看,似乎多一个查找的过程,其实从实现上来看rowid也会有一个查找的过程。

最后,如果没有clustered index,如果你的non-clustered index没有cover你的查询,很容易导致table scan,会导致性能大幅下降。有clustered index的情况,还有可能clustered index seek + bookmark lookup
2007-08-09 00:02 | Justin Shen      

#2楼    回复  引用  查看    

还是每一个表都有一个主建的好。而且主键是自增ID。
我一直都这么做。会有很多好处的。
1、修改数据的时候 update tt set col1 = '' where ID = 2 ,很快就可以找到要修改的数据。

2、删除数据的时候,恩,同上。

3、分页显示数据的时候,这个最有用了,网站的话至少有80%可以直接用ID排序;项目的话,也会有50%的情况可以直接用ID排序,非聚集索引也会利用聚集索引吧。
2007-08-09 06:30 | 金色海洋(jyk)      

#3楼 [楼主]   回复  引用  查看    

@Justin Shen

呵呵,请注意使用堆而不使用聚集表条件。偶并没有说使用聚集表不对,只是说某些个别的情况下,应该使用堆。
在我遇见过的问题中,确确实实存在应该使用堆的情况。
这个问题俺小心的向GTech的大师求证过,经过仔细的讨论,最终的结论应该不会错。:)

使用聚集索引和RID查找数据效率上确实没啥差别,但是谁也不会也无法通过RID索引表。就像我文中提到的,你的系统行为决定你有一种以上的查询需要更快速的支持,一张表上你又无法创建一个以上的聚集索引,这时候,堆,就是更好的选择。

另外,文中偶一再提到按照系统行为调整索引,所以如果到最后,你的查询还是没有被cover,那是索引调整的问题。

最后,本文摘要中说,sql2k5,呵呵。
------------------------
偶在sql2k5里面尚未见到过clustered index seek + key lookup;只见过nonclustered index seek + key lookup.您的笔误?
2007-08-09 07:30 | Nineteen@newsmth      

#4楼 [楼主]   回复  引用  查看    

@金色海洋(jyk)

就像我举到的例子中提到的日志表。对日志表不进行更新或删除在我来看一项基本原则。
对于分页,难道不能按时间排序进行排序、分页吗?

另:偶文中说,在时间字段上创建聚集索引。:)
2007-08-09 07:35 | Nineteen@newsmth      

#5楼    回复  引用    

8错,写的非常好!
2007-08-09 09:33 | henry [未注册用户]

#6楼    回复  引用    

很收益, ^_^
2007-08-09 09:56 | dsj [未注册用户]

#7楼    回复  引用    

对日志表不进行更新或删除在我来看一项基本原则。
-------------------------------------------
这个也太绝对了。日志表在统计分析系统里面常常是最重要的数据源,不进行更新删除几乎无法进行。如果不进行统计分析用,不明白楼主所说的那种日志能够干什么。
2007-08-09 10:36 | roy [未注册用户]

#8楼 [楼主]   回复  引用  查看    

@roy
God....
俺还的的确确没见过统计系统需要更新、删除日志的。
用一个被改变过的数据源怎么能拿到原始统计信息?这样的统计信息有什么意义吗?

另外,统计这种摆明了OLAP操作,怎么能在OLTP上做呢?

日志难道不给客服用?难道不用来排查系统bug?用处貌似很多吧
2007-08-09 11:01 | Nineteen@newsmth      

#9楼    回复  引用    

对第一条还是表示有异议:

记得曾经看过一本书也是说的这个问题,结论是有多个(具体多少个我不清楚,忘了^_^)非聚集索引的表都应该添加一个聚集索引。

原因是如果存在表更新的情况(没有?直接跳过,当我没说),如果没有聚集索引,那么所有的非聚集索引都需要被更新;如果有聚集索引的话,只需要更新聚集索引就可以了,因为所有的非聚集索引最终都是指向聚集索引的。
2007-08-09 11:18 | shen126 [未注册用户]

#10楼    回复  引用  查看    

博主写得这几篇文章都挺不错,不过有一些没有解释到具体的原因,个人感觉这应该算是一个缺憾,尤其是关于性能的(而非LOG表的用法之类)。

本文中,比如:
对于第一条,观察系统行为那里,我觉得这里根本就不是在写SQL语句了,完全就是做系统优化,那么在系统开发初期,这绝对是一个很讨厌的工作,还不如多索引顺序覆盖来得轻松。

对于第五条(连同博主提到的前面的文章),我觉得也是没有把原因说清楚,这里牵扯到索引的行为,比如where col1 - 1 < 2 跟 where col1 < 2 + 1,查询结果是一样的,但是执行的方式却差别很大,如果在col1上加了索引,且数据量相对较大,col1的数据键值也很多……
2007-08-09 11:56 | Reeezak      

#11楼 [楼主]   回复  引用  查看    

@shen126

谢谢。这个问题是这样,如果更新的是聚集索引键值,那毫无疑问,所有的非聚集索引都得跟着更新。如果更新的只是非索引字段,那聚集索引、非聚集索引都不更新。
所以更新并不构成不使用堆的理由:)

再者,俗话说“存在即合理”,堆既然存在,自然有它存在的理由:)
2007-08-09 11:58 | Nineteen@newsmth      

#12楼    回复  引用    

时间字段上创建聚集索引

----
高并发时,
时间有可能是重复的!!!
2007-08-09 12:55 | dx [未注册用户]

#13楼 [楼主]   回复  引用  查看    

@dx

聚集索引和唯一索引是俩个概念,之间完全没有必然关系。
2007-08-09 13:04 | Nineteen@newsmth      

#14楼    回复  引用  查看    

1描述的确不足,是否选择聚集索引不是这么简单
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
2007-08-09 13:12 | RicCC      

#15楼 [楼主]   回复  引用  查看    

@RicCC
赞~~

首先偶只是说:特定情况下需要使用堆表。特定情况,不是多数情况。:)文中偶举的例子说的就是index seek。

其次,偶一再强调需要依据系统行为来调整索引。如果系统行为有太多的scan,太多的写操作,用堆显然不合适。低并发量的插入、非键字段更新,大量的index seek,堆是更合适的结构。

2007-08-09 13:34 | Nineteen@newsmth      

#16楼    回复  引用  查看    

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怎样建倒很有文章,需要极为认真的对待
2007-08-09 14:42 | RicCC      

#17楼 [楼主]   回复  引用  查看    

@RicCC
谢谢~

所谓系统行为决定一切,如果系统的行为表明使用堆更好,就应该使用堆。这个倒是和认真对待clustered index的建立没什么冲突:)

我碰到的真实场景是:有一张表,读的量相对写非常巨大。写操作绝大部分是insert 和更新非键列的值,只有极少量的delete。分析这些读操作,发现在4个非聚集索引上几乎都是index seek。这种情况,使用堆的确是更好的选择。

请允许偶再重复一次“系统行为决定一切”:)就像你提到的找8.1-8.9的log,如果这种查询在OLTP上大量存在,在时间字段上建聚集索引就是不二选择。:)
=====

就index来说,对clustered index seek,它的行为本来就具有你提到的特点:找到8.1的记录,然后往后遍历就OK了;对nonclustered index seek来说,如果是涵盖查询或者include查询,其行为和clustered index seek没啥不同,如果不是涵盖查询或include查询,SQL会根据选择度来判断是做clustered index scan还是nonclustered index seek。

但是偶没注意到过unique index seek,能否稍微解释一下?谢谢:)

2007-08-09 15:30 | Nineteen@newsmth      

#18楼    回复  引用  查看    

index是unique的,index条件都给出来了并且全部是=,每次seek操作输出都只有一条记录,就是unique index seek,oracle是有这个操作的
如果不是unique index seek,就一定会有range index scan。sql server heap表的range index scan需要在IAM跟数据页间切换,效率不好,clustered index就是用于改善这种状况,并且充分利用磁盘设备读取连续数据的优化措施

一定的数据量、数据库吞吐量下,你的做法当然没问题,但负载变大时优化空间可以再挖掘(不考虑数据库结构的优化调整)
例如你的真实场景的例子,去除covering index的情况,考虑下面两点:
1. 一段时间内四个索引的使用频率
2. 一段时间内四个索引seek操作输出的总记录数
基于这两点应当能够得到一个clustered index选择方案。
并且clustered + 一个non clustered index组合起来可以形成一个covering index
2007-08-09 17:31 | RicCC      

#19楼    回复  引用    

楼主喜欢假设,却又拿一种特殊的场景应用于普遍。有点只见树木了。

给数据行(即使是只读的)加一个标识,是沿袭下来的习惯,却也早已成为一种潜在的数据设计规范。它有足够的好处:让你的数据可用。

统计分析要提取转换合并数据。当然要对日志转换。虽然转换可能在另外一个数据库。对转换成功的,要删除以免重复操作。既然是转换,就可能要单条转换或者批量转换。没有ID你的日志拿什么作行的唯一标识。无论单条转换还是批量转换没有标识都很可能有麻烦。其结果就是你的数据不够用,统计时要再加一个标识列...

2007-08-09 19:34 | roy [未注册用户]

#20楼 [楼主]   回复  引用  查看    

@roy

好吧,我假设你没有看到我一再强调的“系统行为决定一切”,也假设你没有看到我一再说明的“在特定条件下”,所以我最终假设你把我认为的“森林中每棵树都有不同,应该根据这些不同进行分别处理”当作“只见树木不见森林”的观点是正确的……

至于统计,真没见过“转换成功的”行需要在原数据中删除……不保留原始数据,这个实在太强。

退一万步,就算我真要这么转,难道不能:
TransactionScope sp = new TransactionScope;
delete from yourLog output deleted.* where LogTime < XXXX;
WriteToOLAPDB();
sp.Complete();

(伪码)。
2007-08-09 21:46 | Nineteen@newsmth      

#21楼    回复  引用    

@Nineteen@newsmth
看起来楼主是个面皮比较薄外加比较认真。文是好文。

能不做什么研究就能从别人那里轻而易举的获取知识应该被认为是一种幸运,所以楼主不必在意那些读个一知半解就胡乱说话的人。
2007-08-10 06:48 | ope [未注册用户]

#22楼    回复  引用    

@roy

“潜在规范”……
“让数据可用”……

这些理由真是飘渺啊,举一个真实的例子?
2007-08-10 06:50 | ope [未注册用户]

#23楼 [楼主]   回复  引用  查看    

@Reeezak
从我个人的经历来看,前期开发一般都会"臆测"系统行为,并由此多少建一些索引.臆测系统行为时,很少花费太多的精力,并且开发人员也不是太在乎它:),毕竟后面依据实际系统行为调整索引才是重点.

至于在后面你提到的where col1 - col2 > 1的问题(我想你把它写成"where col1 - 2 > 1"是个笔误),这个不论建什么样的索引,都没有算法可以让SQL直接评估出col1 - col2 > 1的选择度.就算是我们自己去想,也想不出个所以然来.
俺偷偷的说,让我解释没地方下嘴,所以就偷懒不说了:)
2007-08-10 16:02 | Nineteen@newsmth      

#24楼    回复  引用    

一定要有一个clustered index,是GTSC-DSD Team一贯给客户的建议。如果你从GTEC的哪个SQL的工程师那里听说要用HEAP,告诉我名字,我让我们Tech Lead去骂他 :)

不使用Heap的很重要的另一个原因是,在Heap的情况下,SQL没有可利用的statistics信息。如果你在做join的时候,SQL需要通过统计信息来判断经过filter后的行数,然后决定使用何种方式,如果是Heap,那么SQL缺乏这方面的信息,经常会选错join的方式。

如果我没有理解错,你说的适用于Heap的状况,是指那种读取了表大部分的数据,导致non-clustered index seek + bookmark lookup的效率低于table scan。

但事实上有clustered index的时候,也是可以做clustered index scan的,效果上和table scan一致的。

当语句返回的行超过1/3的时候,SQL一般就会用index scan而不是non-clustered index seek + lookup的情况。所以说你说的heap的好处是不存在的。

当然如果SQL没有index scan,可能的原因就是统计信息很久没有更新,造成SQL的误判。事实上,实际使用中遇到的问题通常是SQL太容易去clustered-index scan而不去seek。为此,SQL 2008里面加了forceseek的hint。
2007-08-14 14:56 | Justin Shen [未注册用户]

#25楼    回复  引用    

给每个table都加一个没有实际用途的ID之类的列,然后还把clustered index放在上面,真是非常令人发指的行为。clustered index对效率的帮助,完全被浪费了。很多时候,确实是建在时间列上比较好。

自增列其实也最好能避免使用。看到很多为了获取自增之后的值而导致的Deadlock。另外,如果你将来想要用replication,自增列也会给你添很多麻烦。
2007-08-14 15:06 | Justin Shen [未注册用户]

#26楼 [楼主]   回复  引用  查看    

@Justin Shen
赞~~

偶说的适用于heap的情况,在我曾经看到的是这样:
一是查询非常多、几乎不删除、非键列更新,并且,查询结果基本都是单行,而查询条件几乎平均分布到三个以上索引的表。
二是一个每天产生kw级日志的日志表,查询基本上是统计每天产生的日志量、查询指定用户产生的日志(结果集和总行数比,基本趋近于0)。

如果出现了您评论中描述的行为,用堆基本上属于自杀行为:)。

堆有存在的价值,也有使用的条件,偶之所以强调不是所有的表都需要建聚集索引,是因为不少人都忽略了堆的价值:)
2007-08-14 17:11 | Nineteen@newsmth      

#27楼 [楼主]   回复  引用  查看    

@Justin Shen
获取自增之后的值而产生deadlock,这个也许可以通过output关键字避免:)
2007-08-14 17:14 | Nineteen@newsmth      

#28楼    回复  引用    

根据博主给的证据,RicCC说的没错,Heap能使用的范围及其狭隘!
我在CSDN blog转载了博主此系列日志,特此通知!
盼望博主能再出好文!起到抛砖引玉之效!
2008-04-14 15:20 | Damon.King [未注册用户]

标题  
姓名  
主页
Email (只有博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2007-08-14 09:17 编辑过