寫這篇文章以前已經看過了不少數據庫方面的優化內容,大部分都是加索引、使用事務、要什麼select什麼等等。然而,只是停留在閱讀的層面上,不多有實踐,由於沒有遇到真實的項目,一切都是紙上談兵。實踐是檢驗真理的惟一標準,因而就想在數據庫上測試一些性能優化的方案,好比索引之類的,可是不想使用假的數據,因而就想着能不能抓取網上的一些數據來做分析,後來本身經過PHP抓取了一些數據(爬取數據博文),抓了大約110W的用戶數據以後,固然須要統計一下具體的數量,因而我使用瞭如下的SQL語句(我使用的存儲引擎是InnoDB):html
SELECT COUNT(*) FROM zh_user;
然而,發現須要運行14-20s的時間才能看到結果。
mysql
這樣的時間開銷在真實的環境的用戶體驗是十分差的,試想一下,打開一個頁面還要等接近20s才能看到數據,別說20s,就算是3s也是十分差的,因而便想在這方面作優化。sql
在MySQL中,平常開發中比較經常使用的有MyISAM和InnoDB兩種存儲引擎。二者之間的其中一個區別是使用count(*)函數計算表的具體行數。數據庫
由於MyISAM會保存表的具體行數,所以這段代碼在MyISAM存儲引擎中執行,MyISAM只要簡單地讀出保存好的行數便可。所以,若是表中沒有使用事務之類的操做,這是最好的優化方案。然而,InnoDB存儲引擎不會保存表的具體行數,所以,在InnoDB存儲引擎中執行這段代碼,InnoDB要掃描一遍整個表來計算有多少行。segmentfault
要弄懂查詢性能在哪,首先,須要知道致使查詢緩慢的瓶頸在哪。explain命令顯示的rows是核心的性能指標,rows大,說明mysql須要掃描的行數就多,絕大部分rows大的語句執行必定很快。因此優化語句基本上都是在優化rows。性能優化
首先,看看錶的結構:
數據結構
表的當前索引:
函數
再看看Explain的結果:
性能
能夠看到,mysql掃描了整個表來執行本次查詢。測試
在數據表的設計中,我是添加了惟一索引的,可是後來有一個語句是根據其中一個字段統計數量,當時添加了一個普通的索引,當我再執行了一遍上面的SQL語句,發現只須要0.2-0.3s的時間就能統計出表中的行數。
不由嚇了一跳,誤打誤撞就發現了優化的方法:在InnoDB中,除了惟一索引以外,在其餘字段添加一個普通索引(稱爲輔助索引)就可以提高count(*)函數的性能。可是這是爲何呢?
加了索引以後的表結構:
當前的索引:
Explain一下:
一樣是掃描同樣的行數,爲何添加一個普通索引就能夠提升這麼多的性能?因而便開始查找資料和閱讀文檔弄懂這個問題。
正如在不一樣的存儲引擎中,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中,我添加的惟一索引被稱爲聚簇索引,然後面添加的索引稱爲輔助索引,所以,讓咱們再來看看聚簇索引和輔助索引的區別。
每個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使用了一個文件來保存數據以及另外一個文件保存索引記錄)。
除了聚簇索引以外的全部索引都被稱爲輔助索引。在InnoDB裏,輔助索引的每一行記錄都包含每一行的主鍵列,輔助索引指向主鍵。InnoDB使用這個主鍵來查找在聚簇索引中的行。若是主鍵很長,輔助索引會使用更多的空間,所以輔助索引有利於存儲引擎擁有長度更短的主鍵。
在第一次使用了惟一索引(u_id)的時候,InnoDB使用了惟一索引做爲表的聚簇索引。而在InnoDB存儲引擎中,count(*)函數是先從磁盤中讀取表中的數據到內存緩衝區,而後掃描全表得到行記錄數的。所以,使用惟一索引做爲聚簇索引的時候,InnoDB須要先讀取110W條的數據到數據緩衝區中,這裏發生了不少次I/O,所以形成了主要的時間消耗。而添加了輔助索引後,mysql在執行查詢時會使用內部的優化機制:即便用輔助索引來統計數量。輔助索引保存的是index的值,此時只須要讀取一個字段,I/O減小了,性能就提升了。所以在InnoDB中,若是有統計整張表的數量的需求,能夠考慮增長一個輔助索引。