通過非聚集索引讓select count(*) from 的查詢速度提高几十倍、甚至千倍

通過非聚集索引,可以顯著提升count(*)查詢的性能。

有的人可能會說,這個count(*)能用上索引嗎,這個count(*)應該是通過表掃描來一個一個的統計,索引有用嗎?


不錯,一般的查詢,如果用索引查找,也就是用Index Seek了,查詢就會很快。

 

之所以快,是由於查詢所需要訪問的數據只佔整個表的很小一部分,如果訪問的數據多了,那反而不如通過表掃描來的更快,因爲掃描用的是順序IO,效率更高,比運用隨機IO訪問大量數據的效率高很多。

 

相應的,如果只需要訪問少量數據,那麼索引查找的效率遠高於表掃描,因爲通過隨機IO來訪問少量數據的效率遠高於通過順序IO來訪問少量數據,之所以掃描的效率較低是由於掃描訪問了很多不需要的數據。

 

那麼,通過非聚集索引,提升select count(*) from 的查詢速度的本質在於,非聚集索引所佔空間的大小往往,遠小於聚集索引或堆表所佔用的空間大小;

同樣的,表中佔用較少字節的字段的非聚集索引,對於速度的提升效果,也要遠大於,佔用較多字節的字段的非聚集索引,因爲佔用字節少,那麼索引佔用的空間也少,同樣是掃描,只需要更少的時間,對硬盤的訪問次數也更少,那麼速度就會更快了。


下面通過一個實驗,來說明非聚集索引爲什麼能提高count(*)的查詢速度。


1、建表,插入數據

  1. if OBJECT_ID('test'is not null  
  2.    drop table test  
  3. go  
  4.   
  5. create table test  
  6. (  
  7. id int identity(1,1),  
  8. vid int ,  
  9. varchar(600),  
  10. constraint pk_test_id primary key (id)  
  11. )  
  12. go  
  13.   
  14.   
  15.   
  16. insert into test(vid,v)  
  17. select 1,REPLICATE('a',600) union all  
  18. select 2,REPLICATE('b',600) union all  
  19. select 3,REPLICATE('c',600) union all  
  20. select 4,REPLICATE('d',600) union all  
  21. select 5,REPLICATE('e',600) union all  
  22. select 6,REPLICATE('f',600) union all  
  23. select 7,REPLICATE('g',600) union all  
  24. select 8,REPLICATE('h',600) union all  
  25. select 9,REPLICATE('i',600) union all  
  26. select 10,REPLICATE('j',600)  
  27. go  
  28.   
  29.   
  30. --select POWER(2,18) * 10  
  31. --2621440條數據  
  32. begin tran  
  33.     insert into test(vid,v)  
  34.     select vid,v  
  35.     from test  
  36. commit  
  37. go 18  
  38.   
  39.   
  40. --建立非聚集索引  
  41. create index idx_test_vid on test(vid)  


2、查看採用聚集索引和非聚集索引後,查詢的資源消耗

  1. --輸出詳細的IO和時間(cpu、流逝的時間)上的開銷信息  
  2. set statistics io on  
  3. set statistics time on  
  4.   
  5.   
  6. /* 採用聚集索引  
  7.   
  8. SQL Server 分析和編譯時間:   
  9.    CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。  
  10.   
  11. (1 行受影響)  
  12. 表 'test'。掃描計數 5,邏輯讀取 206147 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。  
  13.   
  14.  SQL Server 執行時間:  
  15.    CPU 時間 = 921 毫秒,佔用時間 = 277 毫秒。  
  16. */  
  17. select COUNT(*)  
  18. from test with(index (pk_test_id))  
  19.   
  20.   
  21.   
  22. /*採用非聚集索引  
  23.   
  24. SQL Server 分析和編譯時間:   
  25.    CPU 時間 = 0 毫秒,佔用時間 = 1 毫秒。  
  26.   
  27. (1 行受影響)  
  28. 表 'test'。掃描計數 5,邏輯讀取 4608 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。  
  29.   
  30.  SQL Server 執行時間:  
  31.    CPU 時間 = 327 毫秒,佔用時間 = 137 毫秒。  
  32. */  
  33. select count(*)  
  34. from test with(index (idx_test_vid))  


另外,下圖的兩個語句一起執行時的執行計劃:



那麼如果表沒有聚集索引,也沒有非聚集索引,效率又會怎麼樣呢?
  1. --刪除主鍵,也就刪除了聚集索引  
  2. alter table test  
  3. drop constraint pk_test_id  
  4.   
  5.   
  6. --刪除非聚集索引  
  7. drop index idx_test_vid on test  
  8.   
  9.   
  10. /* 表掃描  
  11.   
  12. SQL Server 分析和編譯時間:   
  13.    CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。  
  14.   
  15.  SQL Server 執行時間:  
  16.    CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。  
  17. SQL Server 分析和編譯時間:   
  18.    CPU 時間 = 0 毫秒,佔用時間 = 1 毫秒。  
  19.   
  20. (1 行受影響)  
  21. 表 'test'。掃描計數 5,邏輯讀取 201650 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。  
  22.   
  23. (1 行受影響)  
  24.   
  25.  SQL Server 執行時間:  
  26.    CPU 時間 = 765 毫秒,佔用時間 = 233 毫秒。  
  27. SQL Server 分析和編譯時間:   
  28.    CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。  
  29.   
  30.  SQL Server 執行時間:  
  31.    CPU 時間 = 0 毫秒,佔用時間 = 0 毫秒。  
  32. */  
  33. select count(*)  
  34. 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,是可以查到,看下面的代碼:

  1. use master  
  2. go  
  3.   
  4. --下面的數據庫名稱是wcc,需要改成你自己的數據庫名稱  
  5. select index_id,  
  6.        index_type_desc,  
  7.        alloc_unit_type_desc,  
  8.        page_count              --頁數爲:201650  
  9. from sys.dm_db_index_physical_stats  
  10. (  
  11. db_id('wcc'),object_id('wcc.dbo.test'),0,null,'detailed'  
  12. )d  
  13.   
  14. /*  
  15. index_id    index_type_desc alloc_unit_type_desc    page_count  
  16. 0           HEAP            IN_ROW_DATA             201650  
  17. */  


之所以能查到,是因爲全表掃描,無非就是把表中所有的頁,都掃描一遍,所以掃描的次數正好是表中的頁數201650.


4、那爲什麼非聚集索引來查詢count(*) 的效率是最高的呢?

其實上面分別提到了,通過聚集索引、非聚集索引、表掃描,3種方式來查詢,從執行計劃可以看出來,3種方式都是掃描,那爲什麼非聚集索引效率最高?

其實,很簡單,誰掃描的次數少,也就是掃描的頁數少,那誰的效率當然就高了。


看下面的代碼,就明白了:

  1. use master  
  2. go  
  3.   
  4. --index_id爲1表示聚集索引  
  5. select index_id,  
  6.        index_type_desc,  
  7.        alloc_unit_type_desc,  
  8.        page_count                --201650  
  9. from sys.dm_db_index_physical_stats  
  10. (  
  11. db_id('wcc'),object_id('wcc.dbo.test'),1,null,'detailed'  
  12. )d  
  13. where index_level = 0  --只取level爲0的,也就是頁子級別  
  14.   
  15. /*  
  16. index_id    index_type_desc     alloc_unit_type_desc   page_count  
  17. 1           CLUSTERED INDEX     IN_ROW_DATA            201650  
  18. */  
  19.   
  20.   
  21.   
  22. --index_id爲2的,表示非聚集索引  
  23. select index_id,  
  24.        index_type_desc,  
  25.        alloc_unit_type_desc,  
  26.        page_count               --4538  
  27. from sys.dm_db_index_physical_stats  
  28. (  
  29. db_id('wcc'),object_id('wcc.dbo.test'),2,null,'detailed'  
  30. )d  
  31. where index_level = 0  
  32.   
  33. /*  
  34. index_id    index_type_desc     alloc_unit_type_desc    page_count  
  35. 2           NONCLUSTERED INDEX  IN_ROW_DATA             4538  
  36. */  

聚集索引的葉子節點的頁數是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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章