1. 程式人生 > >通過非聚集索引讓select count(*) from 的查詢速度提高幾十倍、甚至千倍

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

意思 topic sni article 分析 簡單 主鍵 begin 應該

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

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

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

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

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

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

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

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

1、建表,插入數據

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



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

[sql] view plain copy 技術分享圖片技術分享圖片
  1. --輸出詳細的IO和時間(cpu、流逝的時間)上的開銷信息
  2. set statistics io on
  3. set statistics time on
  4. /* 采用聚集索引
  5. SQL Server 分析和編譯時間:
  6. CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
  7. (1 行受影響)
  8. ‘test‘。掃描計數 5,邏輯讀取 206147 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
  9. SQL Server 執行時間:
  10. CPU 時間 = 921 毫秒,占用時間 = 277 毫秒。
  11. */
  12. select COUNT(*)
  13. from test with(index (pk_test_id))
  14. /*采用非聚集索引
  15. SQL Server 分析和編譯時間:
  16. CPU 時間 = 0 毫秒,占用時間 = 1 毫秒。
  17. (1 行受影響)
  18. ‘test‘。掃描計數 5,邏輯讀取 4608 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
  19. SQL Server 執行時間:
  20. CPU 時間 = 327 毫秒,占用時間 = 137 毫秒。
  21. */
  22. select count(*)
  23. from test with(index (idx_test_vid))

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

技術分享圖片

那麽如果表沒有聚集索引,也沒有非聚集索引,效率又會怎麽樣呢? [sql] view plain copy 技術分享圖片技術分享圖片
  1. --刪除主鍵,也就刪除了聚集索引
  2. alter table test
  3. drop constraint pk_test_id
  4. --刪除非聚集索引
  5. drop index idx_test_vid on test
  6. /* 表掃描
  7. SQL Server 分析和編譯時間:
  8. CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
  9. SQL Server 執行時間:
  10. CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
  11. SQL Server 分析和編譯時間:
  12. CPU 時間 = 0 毫秒,占用時間 = 1 毫秒。
  13. (1 行受影響)
  14. ‘test‘。掃描計數 5,邏輯讀取 201650 次,物理讀取 0 次,預讀 0 次,lob 邏輯讀取 0 次,lob 物理讀取 0 次,lob 預讀 0 次。
  15. (1 行受影響)
  16. SQL Server 執行時間:
  17. CPU 時間 = 765 毫秒,占用時間 = 233 毫秒。
  18. SQL Server 分析和編譯時間:
  19. CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
  20. SQL Server 執行時間:
  21. CPU 時間 = 0 毫秒,占用時間 = 0 毫秒。
  22. */
  23. select count(*)
  24. 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 技術分享圖片技術分享圖片
  1. use master
  2. go
  3. --下面的數據庫名稱是wcc,需要改成你自己的數據庫名稱
  4. select index_id,
  5. index_type_desc,
  6. alloc_unit_type_desc,
  7. page_count --頁數為:201650
  8. from sys.dm_db_index_physical_stats
  9. (
  10. db_id(‘wcc‘),object_id(‘wcc.dbo.test‘),0,null,‘detailed‘
  11. )d
  12. /*
  13. index_id index_type_desc alloc_unit_type_desc page_count
  14. 0 HEAP IN_ROW_DATA 201650
  15. */

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


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

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

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

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

[sql] view plain copy 技術分享圖片技術分享圖片
  1. use master
  2. go
  3. --index_id為1表示聚集索引
  4. select index_id,
  5. index_type_desc,
  6. alloc_unit_type_desc,
  7. page_count --201650
  8. from sys.dm_db_index_physical_stats
  9. (
  10. db_id(‘wcc‘),object_id(‘wcc.dbo.test‘),1,null,‘detailed‘
  11. )d
  12. where index_level = 0 --只取level為0的,也就是頁子級別
  13. /*
  14. index_id index_type_desc alloc_unit_type_desc page_count
  15. 1 CLUSTERED INDEX IN_ROW_DATA 201650
  16. */
  17. --index_id為2的,表示非聚集索引
  18. select index_id,
  19. index_type_desc,
  20. alloc_unit_type_desc,
  21. page_count --4538
  22. from sys.dm_db_index_physical_stats
  23. (
  24. db_id(‘wcc‘),object_id(‘wcc.dbo.test‘),2,null,‘detailed‘
  25. )d
  26. where index_level = 0
  27. /*
  28. index_id index_type_desc alloc_unit_type_desc page_count
  29. 2 NONCLUSTERED INDEX IN_ROW_DATA 4538
  30. */


聚集索引的葉子節點的頁數是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 的查詢速度提高幾十倍、甚至千倍