1. 程式人生 > >高效能MySQL之Count統計查詢

高效能MySQL之Count統計查詢

         近一段時間,有同事問我 “MySQL執行count很慢,有沒有什麼優化的空間”。當時在忙,就回復了一句“innodb裡面count統計都是實時統計,慢一些是正常的”, 週末閒暇下來,想到以前有好多人都問過關於count的問題,今天就聊聊MySQL之Count查詢。

        關於MySQL的count查詢,很多人都會有疑問,同樣在大表中執行 ,有些速度基本不耗時,有些又慢的要死。關於這些問題在《高效能MySQL》這本書中第6.7.1章節有如下相關解釋:

      COUNT()聚合函式,以及如何優化使用了該函式的查詢,很可能是MySQL中最容易被誤解的前10個話題之一,在網上隨便搜尋一下就能看到很多錯誤的理解,可能比我們想象的多得多。

在做優化之前,先來看看COUNT()函式的真正作用是什麼。

COUNT()的作用

COUNT()是一個特殊的函式,有兩種非常不同的作用:它可以統計某個列值的數量,也可以統計行數。在統計列值時要求列值時非空的(不統計NULL)。如果在COUNT()的括號中指定了列或列的表示式,統計的就是這個表示式有值的結果數。因為很多人對NULL理解有問題,所以這裡很容易產生誤解。如果想了解更多關於SQL語句中NULL的含義,建議閱讀一些關於SQL語句基礎的書籍。(關於這個話題,網際網路上的一些資訊是不夠精確的)

COUNT()的另外一個作用是統計結果集的行數。當mysql確認括號內的表示式值不可能為空時,實際上就是在統計行數。最簡單的就是當我們使用COUNT(*)的時候,這種情況下萬用字元*並不會像我們猜想的那樣擴充套件成所有的列,實際上,它會忽略所有的列而直接統計所有的行數。

我們發現一個最常見的錯誤就是,在括號內指定了一個列卻希望統計結果集的行數。如果希望知道的是結果集的行數,最好使用COUNT(*),這樣寫意義清晰,效能也會很好。

於MyISAM的神話

一個容易產生的誤解就是:MyISAM的COUNT()函式總是非常快,不過這是有前提條件的,即只有沒有任何where條件的COUNT(*)才非常快,因為此時無需實際地去計算表的行數。MySQL可以利用儲存引擎的特性直接獲得這個值。如果MySQL知道某列col不可能為NULL值,那麼MySQL內部會將COUNT(col)表示式優化為COUNT(*)。

當統計帶WHERE子句的結果集行數,可以是統計某個列值的數量時,MySQL的COUNT()和其它儲存引擎沒有任何不同,就不再有神話般的速度了。所以在MyISAM引擎表上執行COUNT()有時候比別的引擎快,有時候比別的引擎慢,這受很多因素影響,要視具體情況而定。

《高效能MySQL》這本書只介紹了MyISAM儲存引擎在count上的誤區以及在MyISAM儲存引擎上的count優化,而對於常用的innodb執行Count沒有做過多講解,下面我們就聊聊如何在Innodb上進行count優化。

Innodb儲存引擎:

(1)     innodb儲存引擎的物理結構包含 表空間、段、區、頁、行 五個層級,資料檔案按照主鍵排序儲存在頁中(頁在邏輯上連續),主鍵的位置即為資料儲存位置。

(2)     二級索引儲存的資料為指定欄位的值與主鍵值。當我們通過二級索引統計資料的時候,無需掃描資料檔案;而通過主鍵索引統計資料時,由於主鍵索引與資料檔案存放在一起,所以每次都會掃描資料檔案,故大多數情況下,通過二級索引統計資料效率 >= 基於主鍵統計效率。

(3)    由於二級索引儲存的資料為指定欄位的值與主鍵值,故在無索引覆蓋的情況下,查詢二級索引後會根據二級索引獲取的主鍵到主鍵索引中提取資料,此過程可能造成大量的隨機io,導致查詢速度較慢。

(4)    由於主鍵索引與資料儲存保持一致,故基於主鍵的查詢資料要比通過二級索引查詢資料要快(使用二級索引時,查詢到的資料條數>總條數的20%時候mysql就選擇全表掃描,但在主鍵索引上,即使符合條件的達到 90%依然會走索引)。

count慢的原因:

innodb為聚簇索引同時支援事物,其在count指令實現上採用實時統計方式。在無可用的二級索引情況下,執行count會使MySQL掃描全表資料,當資料中存在大欄位或欄位較多時候,其效率非常低下(每個頁只能包含較少的資料條數,需要訪問的物理頁較多)。

innodb可優化點:

1. 主鍵需要採用佔用空間儘量小的型別且資料具有連續性(推薦自增整形id),這樣有利於減少頁分裂、頁內資料移動,可加快插入速度同時有利於增加二級索引密度(一個數據頁上可以儲存更多的資料)。

2.在表包含大欄位或欄位較多情況下,若存在count統計需求,可建一個較小欄位的二級索引(例 char(1) , tinyint )來進行count統計加速。

下面做個count優化例子:

1.首先我們建立一直innodb表,幷包含大欄位(或包含較多欄位):

CREATE TABLE `qstardbcontent` (
  `id` BIGINT(20) NOT NULL DEFAULT '0',
  `content` MEDIUMTEXT,
  `length` INT(11)  NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=INNODB DEFAULT CHARSET=utf8

2.插入50萬條資料,每條資料 5K

3.執行select count(*) from qstardbcontent

可以看到,近50萬條內容較多的資料執行一個count(*) 就需要耗時 13分28秒

下面我們做個優化,在length欄位上加個索引, 執行sql: ALTER TABLE qstardbcontent ADD KEY(LENGTH);

索引建完成後,再執行 select count(*) from qstardbcontent;

可以看到,整個統計查詢非常快,僅用了 354毫秒就完成了查詢。

加速原因:

我們在innodb表上建立了一個二級索引,Innodb在執行count(*)時候由優化器選擇執行路徑。本例中, 二級索引的儲存空間僅包含length欄位值、資料主鍵,假設二級索引輔助結構不佔用空間(僅計算資料佔用空間),在預設情況下,MySQL的一個數據頁大小為16K,一個頁可儲存的資料條數為 16*1024/(4+8) =1365 ,按照單頁儲存空間佔用為50%(頁分裂現象導致頁不滿)計算,50萬條資料的統計僅需要讀取約732個物理頁,而頁在連續的情況下,資料庫一次可讀取多個連續的頁,資料讀取總量為 16k*732約 12MB,因mysql空間分配為按區分配,每個區1M,一次分配1-5個連續區,當資料量較小,一次僅分配一個區,12M資料會分配在12個區中,按照pc硬碟(轉速7200轉/分) 70m/s 的讀取速度,整個過程的io定址時間(12*8.5ms=102)+讀取時間(12m/70m=171ms)=273ms,而資料解析統計約為 30-100ms,故總耗時會在300ms附近(注:count優化功能在5.1版本並不支援)。