通過非聚集索引,可以顯著提升count(*)查詢的性能。
有的人可能會說,這個count(*)能用上索引嗎,這個count(*)應該是通過表掃描來一個一個的統計,索引有用嗎?
不錯,一般的查詢,如果用索引查找,也就是用Index Seek了,查詢就會很快。
之所以快,是由於查詢所需要訪問的數據只佔整個表的很小一部分,如果訪問的數據多了,那反而不如通過表掃描來的更快,因爲掃描用的是順序IO,效率更高,比運用隨機IO訪問大量數據的效率高很多。
相應的,如果只需要訪問少量數據,那麼索引查找的效率遠高於表掃描,因爲通過隨機IO來訪問少量數據的效率遠高於通過順序IO來訪問少量數據,之所以掃描的效率較低是由於掃描訪問了很多不需要的數據。
那麼,通過非聚集索引,提升select count(*) from 的查詢速度的本質在於,非聚集索引所佔空間的大小往往,遠小於聚集索引或堆表所佔用的空間大小;
同樣的,表中佔用較少字節的字段的非聚集索引,對於速度的提升效果,也要遠大於,佔用較多字節的字段的非聚集索引,因爲佔用字節少,那麼索引佔用的空間也少,同樣是掃描,只需要更少的時間,對硬盤的訪問次數也更少,那麼速度就會更快了。
下面通過一個實驗,來說明非聚集索引爲什麼能提高count(*)的查詢速度。
1、建表,插入數據
- 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、查看採用聚集索引和非聚集索引後,查詢的資源消耗
- --輸出詳細的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))
另外,下圖的兩個語句一起執行時的執行計劃:
- --刪除主鍵,也就刪除了聚集索引
- 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,是可以查到,看下面的代碼:
- 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
- */
4、那爲什麼非聚集索引來查詢count(*) 的效率是最高的呢?
其實上面分別提到了,通過聚集索引、非聚集索引、表掃描,3種方式來查詢,從執行計劃可以看出來,3種方式都是掃描,那爲什麼非聚集索引效率最高?
其實,很簡單,誰掃描的次數少,也就是掃描的頁數少,那誰的效率當然就高了。
看下面的代碼,就明白了:
- 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