在MySQL的InnoDB存儲引擎中count(*)函數的優化


寫這篇文章之前已經看過了很多數據庫方面的優化內容,大部分都是加索引、使用事務、要什么select什么等等。然而,只是停留在閱讀的層面上,很少有實踐,因為沒有遇到真實的項目,一切都是紙上談兵。實踐是檢驗真理的唯一標准,於是就想在數據庫上測試一些性能優化的方案,比如索引之類的,但是不想使用假的數據,於是就想着能不能抓取網上的一些數據來作分析,后來自己通過PHP抓取了一些數據(查看抓取數據博文),抓了大約110W的用戶數據之后,當然需要統計一下具體的數量,於是我使用了以下的SQL語句(我使用的存儲引擎是InnoDB):

SELECT COUNT(*) FROM zh_user;

然而,發現需要運行14-20s的時間才能看到結果。

這樣的時間開銷在真實的環境的用戶體驗是十分差的,試想一下,打開一個頁面還要等接近20s才能看到數據,別說20s,就算是3s也是十分差的,於是便想在這方面做優化。

 

存儲引擎

在MySQL中,日常開發中比較常用的有MyISAM和InnoDB兩種存儲引擎。兩者之間的其中一個區別是使用count(*)函數計算表的具體行數。

因為MyISAM會保存表的具體行數,因此這段代碼在MyISAM存儲引擎中執行,MyISAM只要簡單地讀出保存好的行數即可。因此,如果表中沒有使用事務之類的操作,這是最好的優化方案。然而,InnoDB存儲引擎不會保存表的具體行數,因此,在InnoDB存儲引擎中執行這段代碼,InnoDB要掃描一遍整個表來計算有多少行。

 

查詢優化命令--Explain

要弄懂查詢性能在哪,首先,需要知道導致查詢緩慢的瓶頸在哪。explain命令顯示的rows是核心的性能指標,rows大,說明mysql需要掃描的行數就多,絕大部分rows大的語句執行一定很快。所以優化語句基本上都是在優化rows。

首先,當前表的結構:

 

表的當前索引:

 

再看看Explain的結果:

可以看到,mysql掃描了整個表來執行本次查詢。

 

奇怪的地方

在數據表的設計中,我是添加了唯一索引的,但是后來有一個語句是根據其中一個字段統計數量,當時添加了一個普通的索引,當我再執行了一遍上面的SQL語句,發現只需要0.2-0.3s的時間就能統計出表中的行數。

不禁嚇了一跳,誤打誤撞就發現了優化的方法:在InnoDB中,除了唯一索引之外,在其他字段添加一個普通索引(稱為輔助索引)就能夠提升count(*)函數的性能。但是這是為什么呢?

加了索引之后的表結構:

 

當前的索引:

 

Explain一下:

同樣是掃描一樣的行數,為什么添加一個普通索引就可以提高這么多的性能?於是便開始查找資料和閱讀文檔弄懂這個問題。

 

count(*)函數執行原理

正如在不同的存儲引擎中,count(*)函數的執行是不同的。在MyISAM存儲引擎中,count(*)函數是直接讀取數據表保存的行記錄數並返回,而在InnoDB存儲引擎中,count(*)函數是先從內存中讀取表中的數據到內存緩沖區,然后掃描全表獲得行記錄數的。在使用count函數中加上where條件時,在兩個存儲引擎中的效果是一樣的,都會掃描全表計算某字段有值項的次數。

 

索引原理

因為是添加了索引之后才得到性能上的提升,於是便想到從索引的角度來探索。

根據官方文檔上的定義:索引是幫助MySQL高效獲取數據的數據結構。可以得知,索引的本質就是數據結構,添加索引的目的就是為了提高查詢的效率。

使用索引的查詢可以類比到字典,如果要查”mysql“這個單詞,我們首先會定位到m字母,然后在m字母下面的單詞中找y字母,以此類推,直到找到mysql這個單詞,就能看到它在第幾頁,然后就去該頁獲取該單詞更多的信息。想象一下,如果沒有索引,那你就要在字典里一頁一頁的翻閱,效率十分低下。使用索引就是通過這樣不斷地縮小查詢的范圍來篩選出最終的結果。

那么在數據庫也是一樣的,但顯然在數據庫里使用索引要復雜許多。

 

磁盤存取與預讀

一般來說,索引本身也很大,不可能全部存儲在內存中,因此索引往往以索引文件的形式存儲在磁盤上。那么數據庫在構建索引的時候就需要先從磁盤讀取數據了,此時就要產生磁盤I/O消耗。而每次的數據讀取,都要經歷尋道時間、旋轉延遲、傳輸時間三個部分。尋道時間是指磁臂移動到指定磁道所需要的時間,一般在5ms以內;旋轉延遲就是磁盤轉速;傳輸時間指的是將數據從磁盤讀出並寫入到內存的時間,這個時間較短,可以忽略不計。相對於內存存取,I/O存取的消耗要高幾個數量級。因此,評價一個數據結構作為索引的優劣最重要的指標就是查找過程中磁盤I/O操作次數的漸進復雜度。換句話說,索引的結構組織要盡量減少查找過程中磁盤I/O的存取次數。

從上面的描述可以得知磁盤I/O是非常高昂的操作,根據操作系統的局部性原理:

當一個數據被用到時,其附近的數據也通常會馬上被使用。

計算機操作系統在這方面做了一些優化,當一次I/O時,不光把當前磁盤地址的數據讀取到內存緩沖區內,而且把相鄰的數據也都讀取到內存緩沖區內。這樣一來,在讀取數據時產生的I/O就少了很多了。因為在數據庫中,每一次I/O讀取的數據我們稱之為一頁(page),一般為4k或8k,也就是說,我們讀取一頁內的數據時,實際上才發生了一次I/O。

根據以上的描述,我們可以初步得出結論,增加索引前后的性能差距體現在磁盤讀取過程。但是在添加新的索引之前,我是添加了一個唯一索引的,后來發現在mysql中,我添加的唯一索引被稱為聚簇索引,而后面添加的索引稱為輔助索引,因此,讓我們再來看看聚簇索引和輔助索引的區別。

 

聚簇索引(clustered index)和輔助索引(secondary index)

聚簇索引(clustered index)

每一個InnoDB存儲引擎下的表都有一個特殊的索引用來保存每一行的數據,稱為聚簇索引。通常情況下,聚簇索引是主鍵的同義詞。在InnoDB中,mysql是這樣選擇聚簇索引的:

如果表中定義了PRIMARY KEY,那么InnoDB就會使用它作為聚簇索引;

否則,如果沒有定義PRIMARY KEY,InnoDB會選擇第一個有NOT NULL約束的唯一索引作為PRIMARY KEY,然后InnoDB會使用它作為聚簇索引;

如果表中沒有定義PRIMARY KEY或者合適的唯一索引。InnoDB會在一個合成的列中自動生成一個包含行ID的隱含的聚簇索引。這些行使用InnoDB賦予這些表的ID進行排序。行ID是6個字節的字段,且作為新行單一地自增。因此,根據行ID排序的行數據在物理上是根據插入的順序進行排序。

聚簇索引如何加速查詢

因為所有的行數據都跟聚簇索引存放在同一個地方,因此,通過聚簇索引訪問數據行會更快。如果表十分大,跟使用不同地方保存數據和索引的存儲組織來說,聚簇索引的結構會節省很多的I/O操作。(比如說,MyISAM使用了一個文件來保存數據以及另一個文件保存索引記錄)。

輔助索引(secondary index)

除了聚簇索引之外的所有索引都被稱為輔助索引。在InnoDB里,輔助索引的每一行記錄都包含每一行的主鍵列,輔助索引指向主鍵。InnoDB使用這個主鍵來查找在聚簇索引中的行。如果主鍵很長,輔助索引會使用更多的空間,因此輔助索引有利於存儲引擎擁有長度更短的主鍵。

 

結論

在第一次使用了唯一索引(u_id)的時候,InnoDB使用了唯一索引作為表的聚簇索引。而在InnoDB存儲引擎中,count(*)函數是先從內存中讀取表中的數據到內存緩沖區,然后掃描全表獲得行記錄數的。因此,使用唯一索引作為聚簇索引的時候,InnoDB需要先讀取110W條的數據到數據緩沖區中,這里發生了很多次I/O,因此造成了主要的時間消耗。而添加了輔助索引后,mysql在執行查詢時會使用內部的優化機制:即使用輔助索引來統計數量。輔助索引保存的是index的值,此時只需要讀取一個字段,I/O減少了,性能就提高了。因此在InnoDB中,如果有統計整張表的數量的需求,可以考慮增加一個輔助索引。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM