通過非聚集索引讓select count(*) from 的查詢速度提高幾十倍、甚至千倍
通過非聚集索引,可以顯著提升count(*)查詢的性能。
有的人可能會說,這個count(*)能用上索引嗎,這個count(*)應該是通過表掃描來一個一個的統計,索引有用嗎?
不錯,一般的查詢,如果用索引查找,也就是用Index Seek了,查詢就會很快。
之所以快,是由於查詢所需要訪問的數據只占整個表的很小一部分,如果訪問的數據多了,那反而不如通過表掃描來的更快,因為掃描用的是順序IO,效率更高,比運用隨機IO訪問大量數據的效率高很多。
相應的,如果只需要訪問少量數據,那麽索引查找的效率遠高於表掃描,因為通過隨機IO來訪問少量數據的效率遠高於通過順序IO來訪問少量數據,之所以掃描的效率較低是由於掃描訪問了很多不需要的數據。
那麽,通過非聚集索引,提升select count(*) from 的查詢速度的本質在於,非聚集索引所占空間的大小往往,遠小於聚集索引或堆表所占用的空間大小;
同樣的,表中占用較少字節的字段的非聚集索引,對於速度的提升效果,也要遠大於,占用較多字節的字段的非聚集索引,因為占用字節少,那麽索引占用的空間也少,同樣是掃描,只需要更少的時間,對硬盤的訪問次數也更少,那麽速度就會更快了。
下面通過一個實驗,來說明非聚集索引為什麽能提高count(*)的查詢速度。
1、建表,插入數據
[sql] view plain copy
- if OBJECT_ID(‘test‘) is not null
- drop table test
- go
- create table test
- (
- id int identity(1,1),
- vid int ,
- v varchar(600),
- constraint pk_test_id primary key (id)
- )
- go
- insert into test(vid,v)
- select 1,REPLICATE(‘a‘,600) union all
- select 2,REPLICATE(‘b‘,600) union all
- select 3,REPLICATE(‘c‘,600) union all
- select 4,REPLICATE(‘d‘,600) union all
- select 5,REPLICATE(‘e‘,600) union all
- select 6,REPLICATE(‘f‘,600) union all
- select 7,REPLICATE(‘g‘,600) union all
- select 8,REPLICATE(‘h‘,600) union all
- select 9,REPLICATE(‘i‘,600) union all
- select 10,REPLICATE(‘j‘,600)
- go
- --select POWER(2,18) * 10
- --2621440條數據
- begin tran
- insert into test(vid,v)
- select vid,v
- from test
- commit
- go 18
- --建立非聚集索引
- create index idx_test_vid on test(vid)
2、查看采用聚集索引和非聚集索引後,查詢的資源消耗
[sql] view plain copy
- --輸出詳細的IO和時間(cpu、流逝的時間)上的開銷信息
- set statistics io on
- set statistics time on
- /* 采用聚集索引
- SQL Server 分析和編譯時間:
- CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
- (1 行受影響)
- 表 ‘test‘。掃描計數 5,邏輯讀取 206147 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
- SQL Server 執行時間:
- CPU 時間 = 921 毫秒,占用時間 = 277 毫秒。
- */
- select COUNT(*)
- from test with(index (pk_test_id))
- /*采用非聚集索引
- SQL Server 分析和編譯時間:
- CPU 時間 = 0 毫秒,占用時間 = 1 毫秒。
- (1 行受影響)
- 表 ‘test‘。掃描計數 5,邏輯讀取 4608 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
- SQL Server 執行時間:
- CPU 時間 = 327 毫秒,占用時間 = 137 毫秒。
- */
- select count(*)
- from test with(index (idx_test_vid))
另外,下圖的兩個語句一起執行時的執行計劃:
那麽如果表沒有聚集索引,也沒有非聚集索引,效率又會怎麽樣呢? [sql] view plain copy- --刪除主鍵,也就刪除了聚集索引
- alter table test
- drop constraint pk_test_id
- --刪除非聚集索引
- drop index idx_test_vid on test
- /* 表掃描
- SQL Server 分析和編譯時間:
- CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
- SQL Server 執行時間:
- CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
- SQL Server 分析和編譯時間:
- CPU 時間 = 0 毫秒,占用時間 = 1 毫秒。
- (1 行受影響)
- 表 ‘test‘。掃描計數 5,邏輯讀取 201650 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
- (1 行受影響)
- SQL Server 執行時間:
- CPU 時間 = 765 毫秒,占用時間 = 233 毫秒。
- SQL Server 分析和編譯時間:
- CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
- SQL Server 執行時間:
- CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
- */
- select count(*)
- from test
3、從上面的開銷可以看出:
a、通過聚集索引來查詢count(*)時,邏輯讀取次數206147次,執行時間和占用時間分別是921毫秒和277毫秒,從執行計劃中看出,其查詢開銷是96%。
b、非聚集索引的邏輯讀取次數是4608次,而執行時間和占用時間是327毫秒和137毫秒,查詢開銷是4%。
c、表掃描的邏輯讀取次數是201650次,執行時間和占用時間是765毫秒和233毫秒。
這裏需要註意的是,由於兩個執行計劃都采用了並行計劃,導致了執行時間遠大於占用時間,這主要是因為執行時間算的是多個cpu時間的總和,我的筆記本電腦有4個cpu,那麽921/4 大概就是230毫秒左右,也就是每個cpu花在執行上的時間大概是230毫秒左右,和277毫秒就差不多了。
從這些開銷信息可以看出,非聚集索引的邏輯讀取次數是聚集索引的50分之一,執行時間是聚集索引的2-3分之一左右,查詢開銷上是聚集索引的24分之一。
很有意思的是,表掃描的邏輯讀取次數要比聚集索引的要少4497次,這個邏輯讀取次數201650,是可以查到,看下面的代碼:
[sql] view plain copy
- use master
- go
- --下面的數據庫名稱是wcc,需要改成你自己的數據庫名稱
- select index_id,
- index_type_desc,
- alloc_unit_type_desc,
- page_count --頁數為:201650
- from sys.dm_db_index_physical_stats
- (
- db_id(‘wcc‘),object_id(‘wcc.dbo.test‘),0,null,‘detailed‘
- )d
- /*
- index_id index_type_desc alloc_unit_type_desc page_count
- 0 HEAP IN_ROW_DATA 201650
- */
之所以能查到,是因為全表掃描,無非就是把表中所有的頁,都掃描一遍,所以掃描的次數正好是表中的頁數201650.
4、那為什麽非聚集索引來查詢count(*) 的效率是最高的呢?
其實上面分別提到了,通過聚集索引、非聚集索引、表掃描,3種方式來查詢,從執行計劃可以看出來,3種方式都是掃描,那為什麽非聚集索引效率最高?
其實,很簡單,誰掃描的次數少,也就是掃描的頁數少,那誰的效率當然就高了。
看下面的代碼,就明白了:
[sql] view plain copy
- use master
- go
- --index_id為1表示聚集索引
- select index_id,
- index_type_desc,
- alloc_unit_type_desc,
- page_count --201650
- from sys.dm_db_index_physical_stats
- (
- db_id(‘wcc‘),object_id(‘wcc.dbo.test‘),1,null,‘detailed‘
- )d
- where index_level = 0 --只取level為0的,也就是頁子級別
- /*
- index_id index_type_desc alloc_unit_type_desc page_count
- 1 CLUSTERED INDEX IN_ROW_DATA 201650
- */
- --index_id為2的,表示非聚集索引
- select index_id,
- index_type_desc,
- alloc_unit_type_desc,
- page_count --4538
- from sys.dm_db_index_physical_stats
- (
- db_id(‘wcc‘),object_id(‘wcc.dbo.test‘),2,null,‘detailed‘
- )d
- where index_level = 0
- /*
- index_id index_type_desc alloc_unit_type_desc page_count
- 2 NONCLUSTERED INDEX IN_ROW_DATA 4538
- */
聚集索引的葉子節點的頁數是201650,而非聚集索引的 葉子節點的頁數是4538,差了近50倍,而在沒有索引的時候,采用表掃描時,葉子節點的頁數是201650,與聚集索引一樣。
效率的差異不僅在與邏輯讀取次數,因為邏輯讀取效率本身是很高的,是直接在內存中讀取的,但SQL Server的代碼需要掃描內存中的數據201650次,也就是循環201650次,可想而知,cpu的使用率會暴漲,會嚴重影響SQL Server處理正常的請求。
假設這些要讀取的頁面不在內存中,那問題就大了,需要把硬盤上的數據讀到內存,關鍵是要讀201650頁,而通過索引只需要讀取4538次,效率的差距就會更大。
另外,實驗中只是200多萬條數據,如果實際生產環境中有2億條記錄呢?到時候,效率的差距會從幾十倍上升到幾百倍、幾千倍。
5、那是不是只要是非聚集索引,都能提高select count(*) from查詢的效率嗎?
這個問題是由下面的網友提出的問題,而想到的一個問題。
如果按照v列來建索引,而v列的數據類型是varchar(600),所以這個新建的索引,占用的頁數肯定是非常多的,應該僅次於聚集索引的201650頁,那麽完成索引掃描的開銷肯定大於,按vid列建立的非聚集索引,而vid的數據類型是int。
所以,不是只要是非聚集索引,就能提高查詢效率的。
總結一下:
執行select count(*) from查詢的時候,要進行掃描,有人可能會說,掃描性能很差呀,還能提高性能?那麽,難道用索引查找嗎?這樣性能只會更差。
這裏想說的是,沒有最好的技術,只有最適合的技術,要想提高這種select count(*) from查詢的性能,那就只能用掃描。
這裏,要提高效率的關鍵,就是減少要掃描的頁數,而按照占用字節數少的字段,來建立非聚集索引,那麽這個非聚集索引所占用的頁數,遠遠少於聚集索引、按占用字節數較多的列建立的非聚集索引,所占用的頁數,這樣就能提高性能了。
最後,有兩個關於索引的帖子,不錯:
兩個問題:1,(聚集或者非聚集的)索引頁會不會出現也拆分;2,非聚集索引存儲時又沒排序:
http://bbs.csdn.NET/topics/390594730
繼續:非聚集索引行在存儲索引鍵時確實是排序了的,用事實說話,理論+實踐:
http://bbs.csdn.net/topics/390595949
通過非聚集索引讓select count(*) from 的查詢速度提高幾十倍、甚至千倍