这种不带条件的统计,因为不需要扫描数据行,只需要扫描索引页的页头(就像下面的 m_slotCnt = 6 表示这一页有6条数据),所以索引页越少,速度越快,PAGE HEADER:
Page @0x072B0000m_pageId = (1:769) m_headerVersion = 1 m_type = 1
m_typeFlagBits = 0x4 m_level = 0 m_flagBits = 0x0
m_objId (AllocUnitId.idObj) = 1341 m_indexId (AllocUnitId.idInd) = 256
Metadata: AllocUnitId = 72057594125811712
Metadata: PartitionId = 72057594080526336 Metadata: IndexId = 1
Metadata: ObjectId = 545437017 m_prevPage = (0:0) m_nextPage = (0:0)
pminlen = 8 m_slotCnt = 6 m_freeCnt = 7908
m_freeData = 272 m_reservedCnt = 0 m_lsn = (10393:1231:2)
m_xactReserved = 0 m_xdesId = (0:0) m_ghostRecCnt = 0
m_tornBits = 0
直接顺序读取数据页。
只不过不仅是扫页头,而是要扫索引的全部索引页.
它只是个B+树结构,不可能只读页头就能读到数据总数.
不带条件的扫描,如select * from tb,有聚集索引的话,不可能去走非聚集索引的.
sql server不会读了非聚集再去利用非聚集索引页上的聚集索引键值去找非非聚集索引列的.这在成本上更耗.
聚集索引和非聚集索引都是相同的B树结构,但聚集引的叶子层摆放着表数据,扫描聚集索引,就会扫描数据行本身,这个读取的数据页最多
而非聚集索引叶子层放着是非聚集索引键值和定位符,不会摆放数据(只有索引键),所以读取的页数会比较少.
DBCC PAGE(数据库名,文件号,页号,1)
我说的是select count(*) from tb.
你是不是说加了where条件了?
下面是一个完整的数据页,由三部分组成:页头、数据、偏移表PAGE HEADER:
Page @0x0742A000m_pageId = (1:771) m_headerVersion = 1 m_type = 1
m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0x0
m_objId (AllocUnitId.idObj) = 1342 m_indexId (AllocUnitId.idInd) = 256
Metadata: AllocUnitId = 72057594125877248
Metadata: PartitionId = 72057594080591872 Metadata: IndexId = 1
Metadata: ObjectId = 545437017 m_prevPage = (0:0) m_nextPage = (0:0)
pminlen = 8 m_slotCnt = 9 m_freeCnt = 7817
m_freeData = 357 m_reservedCnt = 0 m_lsn = (10393:1264:2)
m_xactReserved = 0 m_xdesId = (0:0) m_ghostRecCnt = 0
m_tornBits = 0 Allocation StatusGAM (1:2) = ALLOCATED SGAM (1:3) = ALLOCATED
PFS (1:1) = 0x60 MIXED_EXT ALLOCATED 0_PCT_FULL DIFF (1:6) = CHANGED
ML (1:7) = NOT MIN_LOGGED DATA:
Slot 0, Offset 0x60, Length 27, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C06000000000: 30000800 01000000 0200fc01 001b0064 †0..............d
00000010: 006f0062 00650061 007200†††††††††††††.o.b.e.a.r. Slot 1, Offset 0x7b, Length 29, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C07B00000000: 30000800 02000000 0200fc01 001d0064 †0..............d
00000010: 00610077 00750067 00750069 00††††††††.a.w.u.g.u.i. Slot 2, Offset 0x98, Length 29, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C09800000000: 30000800 03000000 0200fc01 001d0066 †0..............f
00000010: 00630075 0061006e 00640079 00††††††††.c.u.a.n.d.y. Slot 3, Offset 0x14c, Length 25, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C14C00000000: 30000800 04000000 0200fc01 0019006c †0..............l
00000010: 00690062 0069006e 00†††††††††††††††††.i.b.i.n. Slot 4, Offset 0x110, Length 33, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C11000000000: 30000800 05000000 0200fc01 00210068 †0............!.h
00000010: 0065006c 006c006f 0077006f 0072006b †.e.l.l.o.w.o.r.k
00000020: 00†††††††††††††††††††††††††††††††††††. Slot 5, Offset 0x131, Length 27, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C13100000000: 30000800 06000000 0200fc01 001b0070 †0..............p
00000010: 0061006f 006c0075 006f00†††††††††††††.a.o.l.u.o. Slot 6, Offset 0xb5, Length 23, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C0B500000000: 30000800 07000000 0200fc01 00170068 †0..............h
00000010: 00780067 006800††††††††††††††††††††††.x.g.h. Slot 7, Offset 0xcc, Length 41, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C0CC00000000: 30000800 08000000 0200fc01 00290068 †0............).h
00000010: 00610070 00700079 0066006c 00790073 †.a.p.p.y.f.l.y.s
00000020: 0074006f 006e0065 00†††††††††††††††††.t.o.n.e. Slot 8, Offset 0xf5, Length 27, DumpStyle BYTERecord Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP VARIABLE_COLUMNSMemory Dump @0x4EE1C0F500000000: 30000800 09000000 0200fc01 001b0068 †0..............h
00000010: 00610069 00770065 007200†††††††††††††.a.i.w.e.r. OFFSET TABLE:Row - Offset
8 (0x8) - 245 (0xf5)
7 (0x7) - 204 (0xcc)
6 (0x6) - 181 (0xb5)
5 (0x5) - 305 (0x131)
4 (0x4) - 272 (0x110)
3 (0x3) - 332 (0x14c)
2 (0x2) - 152 (0x98)
1 (0x1) - 123 (0x7b)
0 (0x0) - 96 (0x60) 这个数据页有9条数据,你可以直接从页头里读取到;或者遍历一下数据部分,数一数一共几条,结果还是9,不过比从页头读取就慢多了,,,
set statistics io on
select count(*) from tb
记下这里共扫描了多少页,假如现在是用的索引ix_01
那么我们可以看看ix_01共有多少页:
dbcc showcontig('tb','ix_01')这里可以看出,需要扫描索引ix_01的所有页,才能count出来。
直接Ctrl+L就可以知道它用的是聚集索引还是非聚集索引,
“根页”???
看来我们对数据的物理存储方式认知不一样,呵呵
我认为没有什么根页,就是一个个数据页(索引页也是数据页),有几百个索引页也就是把几百个记录数加在一起就是总数了,,,
然后按B+结构,往下划分,直到索引的叶子层。
叶子层之上是层层的结构页,结构页应该应该记录其上层或下层的一些信息,我猜想是一个范围链表信息.
如果说每个结构页都要记录下层的数据总数,那么每次更新或i或d原表数据,但需要去更新结构页,个人感觉非常耗资源,
应该只去变动索引叶子层就行了。
同意。
是要到叶子层中扫描一遍才能得到记录总数。
但遍历的顺序不是从根-->中间叶(如果有)-->叶子
而是从IAM页开始顺序读取,因为页面之间有记录上一页下一页。
我们探讨的前提是有聚集索引有非聚集索引
我认count需要走索引根-->中间叶(如果有)-->叶子.
楼主的这些话有几个结论: 1:如果一个表有索引,肯定不是表扫描(不知道大家对表扫描的理解是什么,其实这个概念在联机帮助上都有,这里就不说,这一点是对的,如果有索引,肯定不是扫描,这个最简单就是查看执行计划,你就会看到,没有索引的堆表是"Table Scan",有索引的是"index scan")
2:如果该表上同时具有非聚集索引,那么这时候执行COUNT(*)的效率,肯定比之前只有聚集索引和没有索引更高(这点也是对的,也是可以通过逻辑读取的计数看得出,理论上也有说明,这点,大家可以去测试看看就知道我说的是真是假,如果否定这点,那么请问否定的人,你对效率和逻辑读的理解是什么)
3:统计表的纪录数,是因为页的页头纪录了总数,直接扫描页头就可以了(对于这点,其实没有可以支撑的理论,为什么?因为这个DBCC PAGE本身就是不公开的存储过程,那么其内部的执行是什么样,我们都不知道,所以无从考究)
总的说来,楼主想表达的基本理论其实是正确的,BTW,正确与否,其实实践出真知,其实关于这个我还想说很多,但涉及太多内容,略了.
想像一下,索引在磁盘中的并非是物理排序的,而是逻辑上排序的,即通过根节点和中间层上的键值范围及页面
之间的链接实现的(很像一个排序的双向链表)。
如果索引更新很频繁,那么有些页面可能会跑到表空间的最后面,此时如果再按根->中间->叶子这样顺序访问下来,
那就有可能导致磁盘臂不停的来回移动,我们知道io之所以慢,主要是磁盘臂的定位很费时间,所以sql server不可能
这样去访问。它应该是从iam的第一页开始,按物理顺序依次往前读取页面。
这点从io就已经可以看出来了。我们现在讨论的是这个count分步拆开是一样什么样的过程。
哈哈,那我也无语了。我推荐一本书吧:
Inside Microsoft SQL Server 2005 - The Storage Engine
IAM页是针对于没索引的堆结构,来查找,我们即然有聚集索引,为什么还需要从IAM页扫描?
而且count按b+结构来讲,是需要遍历树的。你要是真明白,仔细给讲讲你的理论吧.
未非序的聚集索引扫描 = Table Scan 同意我的观点吗? 呵。
---我觉得只看目录是不行的,非聚集索引相当于新华字典的部首查字法。
你只看部首查字法,是不可能知道有多少字滴。
count是要遍历树滴。这就是B+数据结构。
就是没加上Order BY 聚集索引键select * from tb
或者是
select count(*) from tb tb上只建有聚集索引.
有聚集也有非聚集,
然后你分步来解释下count的执行过程。
tb有聚集也有非聚集,
select count(*) from tb
然后你分步来解释下count的执行过程。
这问题估计得专业的DBA来出正解
只有聚集时,不走聚集索引,而是顺序往前读,效果跟没有建聚集时的Table Scan一样.
如果有建非聚集,则一定会走非聚集,而且是挑总页数最少的那个非聚集.
难到sql可以直接定位到叶子层不成。
晕.又跑上来了.^_)
我一开始就在说是从iam指向的第一页开始读取了
你说的对,的确是可以直接定位,聚集索引中的所谓叶子层就是数据页。
我码了一些例子,证实了一些你的看法,比如说索引根页中的页头没有包含数下一层的记录数
http://topic.csdn.net/u/20080707/09/3c64cc80-5f7a-4116-8942-81c6deae9c07.html
呵呵,有进步才是最重要的。
这个贴子应该是我在csdn回复最多的一个贴子了,收藏了。