一、理解MySQL的Query Optimizer
MySQL Optimizer是一個專門負責優化SELECT 語句的優化器模塊,它主要的功能就是通過計算分析系統中收集的各種統計信息,為客戶端請求的Query 給出他認為最優的執行計划,也就是他認為最優的數據檢索方式。
二、Query 語句優化基本思路和原則
在分析如何優化MySQL Query 之前,我們需要先了解一下Query 語句優化的基本思路和原則。一般來說,Query 語句的優化思路和原則主要體現在以下幾個方面:
重中之重,需熟背
1. 優化更需要優化的Query;
2. 定位優化對象的性能瓶頸;
3. 明確的優化目標;
4. 從Explain 入手;
5. 多使用profile
6. 永遠用小結果集驅動大的結果集;
7. 盡可能在索引中完成排序;
8. 只取出自己需要的Columns;
9. 僅僅使用最有效的過濾條件;
10. 盡可能避免復雜的Join 和子查詢;
上面所列的幾點信息,前面4 點可以理解為Query優化的一個基本思路,后面部分則是我們優化中的基本原則。
下面我們先針對Query 優化的基本思路做一些簡單的分析,理解為什么我們的Query 優化到底該如何進行。
1)優化更需要優化的Query
為什么我們需要優化更需要優化的Query?這個地球人都知道的“並不能成為問題的問題”我想就並不需要我過多解釋吧,哈哈。
那什么樣的Query 是更需要優化呢?對於這個問題我們需要從對整個系統的影響來考慮。什么Query 的優化能給系統整體帶來更大的收益,就更需要優化。一般來說,高並發低消耗(相對)的Query 對整個系統的影響遠比低並發高消耗的Query 大。我們可以通過以下一個非常簡單的案例分析來充分說明問題。
假設有一個Query 每小時執行10000 次,每次需要20 個IO。另外一個Query 每小時執行10 次,每次需要20000 個IO。我們先通過IO 消耗方面來分析。可以看出,兩個Query 每小時所消耗的IO 總數目是一樣的,都是200000 IO/小時。假設我們優化第一個Query,從20 個IO 降低到18 個IO,也就是僅僅降低了2 個IO,則我們節省了2 * 10000 = 20000 (IO/小時)。而如果希望通過優化第二個Query 達到相同的效果,我們必須要讓每個Query 減少20000 / 10 = 2000 IO。我想大家都會相信讓第一個Query 節省2 個IO遠比第二個Query 節省2000 個IO 來的容易。
其次,如果通過CPU 方面消耗的比較,原理和上面的完全一樣。只要讓第一個Query 稍微節省一小塊資源,就可以讓整個系統節省出一大塊資源,尤其是在排序,分組這些對CPU 消耗比較多的操作中尤其突出。
最后,我們從對整個系統的影響來分析。一個頻繁執行的高並發Query 的危險性比一個低並發的Query 要大很多。當一個低並發的Query 走錯執行計划,所帶來的影響主要只是該Query 的請求者的體驗會變差,對整體系統的影響並不會特別的突出,之少還屬於可控范圍。但是,如果我們一個高並發的Query 走錯了執行計划,那所帶來的后果很可能就是災難性的,很多時候可能連自救的機會都不給你就會讓整個系統Crash 掉。曾經我就遇到這樣一個案例,系統中一個並發度較高的Query 語句走錯執行計划,系統頃刻間Crash,甚至我都還沒有反應過來是怎么回事。當重新啟動數據庫提供服務后,系統負載立刻直線飆升,甚至都來不及登錄數據庫查看當時有哪些Active 的線程在執行哪些Query。如果是遇到一個並發並不太高的Query 走錯執行計划,至少我們還可以控制整個系統不至於系統被直接壓跨,甚至連問題根源都難以抓到。
總體來說就是優先優化並發高的query,高並發的query一定要想辦法優化到最優。
2)定位優化對象的性能瓶頸
當我們拿到一條需要優化的Query 之后,第一件事情是什么?是反問自己,這條Query 有什么問題?我為什么要優化他?只有明白了這些問題,我們才知道我們需要做什么,才能夠找到問題的關鍵。而不能就只是覺得某個Query 好像有點慢,需要優化一下,然后就開始一個一個優化方法去輪番嘗試。這樣很可能整個優化過程會消耗大量的人力和時間成本,甚至可能到最后還是得不到一個好的優化結果。這就像看病一樣,醫生必須要清楚的知道我們病的根源才能對症下葯。如果只是知道我們什么地方不舒服,然后就開始通過各種葯物嘗試治療,那這樣所帶來的后果可能就非常嚴重了。
所以,在拿到一條需要優化的Query 之后,我們首先要判斷出這個Query 的瓶頸到底是IO 還是CPU。到底是因為在數據訪問消耗了太多的時間,還是在數據的運算(如分組排序等)方面花費了太多資源?
3)明確的優化目標
當我們定位到了一條Query 的性能瓶頸之后,就需要通過分析該Query 所完成的功能和Query 對系統的整體影響制訂出一個明確的優化目標。
如何設定優化目標?
一般來說,我們首先需要清楚的了解數據庫目前的整體狀態,同時也要清楚的知道數據庫中與該Query 相關的數據庫對象的各種信息,而且還要了解該Query 在整個應用系統中所實現的功能。了解了數據庫整體狀態,我們就能知道數據庫所能承受的最大壓力,也就清楚了我們能夠接受的最悲觀情況。把握了該Query 相關數據庫對象的信息,我們就應該知道實現該Query 的消耗最理想情況下需要消耗多少資源,最糟糕又需要消耗多少資源。最后,通過該Query 所實現的功能點在整個應用系統中的重要地位,我們可以大概的分析出該Query 可以占用的系統資源比例,而且我們也能夠知道該Query 的效率給客戶帶來的體驗影響到底有多大。
當我們清楚了這些信息之后,我們基本可以得出該Query 應該滿足的一個性能范圍是怎樣的,這也就是我們的優化目標范圍,然后就是通過尋找相應的優化手段來解決問題了。如果該Query 實現的應用系統功能比較重要,我們就必須讓目標更偏向於理想值一些,即使在其他某些方面作出一些讓步與犧牲,比如調整schema 設計,調整索引組成等,可能都是需要的。而如果該Query 所實現的是一些並不是太關鍵的功能,那我們可以讓目標更偏向悲觀值一些,而盡量保證其他更重要的Query 的性能。這種時候,即使需要調整商業需求,減少功能實現,也不得不應該作出讓步。
4)從Explain 入手
為什么從Explain 入手?因為只有Explain 才能告訴你,這個Query 在數據庫中是以一個什么樣的執行計划來實現的。
5)永遠用小結果集驅動大的結果集
很多人喜歡在優化SQL 的時候說用小表驅動大表,個人認為這樣的說法不太嚴謹。為什么?因為大表經過WHERE 條件過濾之后所返回的結果集並不一定就比小表所返回的結果集大,可能反而更小。
在這種情況下如果仍然采用小表驅動大表,就會得到相反的性能效果。其實這樣的結果也非常容易理解,在MySQL 中的Join,只有Nested Loop 一種Join 方式,也就是MySQL 的Join 都是通過嵌套循環來實現的。驅動結果集越大,所需要循環的此時就越多,那么被驅動表的訪問次數自然也就越多,而每次訪問被驅動表,即使需要的邏輯IO 很少,循環次數多了,總量自然也不可能很小,而且每次循環都不能避免的需要消耗CPU ,所以CPU 運算量也會跟着增加。所以,如果我們僅僅以表的大小來作為驅動表的判斷依據,假若小表過濾后所剩下的結果集比大表多很多,結果就是需要的嵌套循環中帶來更多的循環次數,反之,所需要的循環次數就會更少,總體IO 量和CPU 運算量也會少。而且,就算是非Nested Loop 的Join 算法,如Oracle 中的Hash Join,同樣是小結果集驅動大的結果集是最優的選擇。
所以,在優化Join Query 的時候,最基本的原則就是“小結果集驅動大結果集”,通過這個原則來減少嵌套循環中的循環次數,達到減少IO 總量以及CPU 運算的次數。
可以看看這篇:聯表查詢時始終以小結果集驅動大結果集
6)盡可能在索引中完成排序
7)只取出自己需要的Columns
這個很簡單,不要所有的查詢都直接 select *,而是只取自己需要的列。
8)僅僅使用最有效的過濾條件
很多人在優化Query 語句的時候很容易進入一個誤區,那就是覺得WHERE 子句中的過濾條件越多越好,實際上這並不是一個非常正確的選擇。
為什么說過濾條件多不一定是好事呢?請看下面示例:
需求: 查找某個用戶在所有group 中所發的討論message 基本信息。
場景:
1、知道用戶ID 和用戶nick_name
2、信息所在表為group_message
3、group_message 中存在用戶ID(user_id)和nick_name(author)兩個索引
方案一:將用戶ID 和用戶nick_name 兩者都作為過濾條件放在WHERE 子句中來查詢,Query 的執行計划如下:
sky@localhost : example 11:29:37> EXPLAIN SELECT * FROM group_message -> WHERE user_id = 1 AND author='1111111111'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: group_message type: ref possible_keys: group_message_author_ind,group_message_uid_ind key: group_message_author_ind key_len: 98 ref: const rows: 1 Extra: Using where 1 row in set (0.00 sec)
方案二:僅僅將用戶ID 作為過濾條件放在WHERE 子句中來查詢,Query 的執行計划如下:
sky@localhost : example 11:30:45> EXPLAIN SELECT * FROM group_message -> WHERE user_id = 1\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: group_message type: ref possible_keys: group_message_uid_ind key: group_message_uid_ind key_len: 4 ref: const rows: 1 Extra: 1 row in set (0.00 sec)
方案三:僅將用戶nick_name 作為過濾條件放在WHERE 子句中來查詢,Query 的執行計划如下:
sky@localhost : example 11:38:45> EXPLAIN SELECT * FROM group_message -> WHERE author = '1111111111'\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: group_message type: ref possible_keys: group_message_author_ind key: group_message_author_ind key_len: 98 ref: const rows: 1 Extra: Using where 1 row in set (0.00 sec)
初略一看三個執行計划好像都挺好的啊,每一個Query 的執行類型都利用到了索引,而且都是“ref”類型。可是仔細一分析,就會發現,group_message_uid_ind 索引的索引鍵長度為4(key_len:4),由於user_id 字段類型為int,所以我們可以判定出Query Optimizer 給出的這個索引鍵長度是完全准確的。而group_message_author_ind 索引的索引鍵長度為98(key_len: 98),因為author 字段定義為varchar(32) ,而所使用的字符集是utf8,32 * 3 + 2 = 98。而且,由於user_id 與author(來源於nick_name)全部都是一一對應的,所以同一個user_id 有哪些記錄,那么所對應的author 也會有完全相同的記錄。所以,同樣的數據在group_message_author_ind 索引中所占用的存儲空間要遠遠大於group_message_uid_ind 索引所占用的空間。占用空間更大,代表我們訪問該索引所需要讀取的數據量就會更多。所以,選擇group_message_uid_ind 的執行計划才是最有的執行計划。也就是說,上面的方案二才是最有方案,而使用了更多的WHERE 條件的方案一反而沒有僅僅使用user_id一個過濾條件的方案一優。
可能有些人會說,那如果將user_id 和author 兩者建立聯合索引呢?告訴你,效果可能比沒有這個索引的時候更差,因為這個聯合索引的索引鍵更長,索引占用的空間將會更大。
這個示例並不一定能代表所有場景,僅僅是希望讓大家明白,並不是任何時候都是使用的過濾條件越多性能會越好。在實際應用場景中,肯定會存在更多更復雜的情形,怎樣使我們的Query 有一個更優化的執行計划,更高效的性能,還需要靠大家仔細分析各種執行計划的具體差別,才能選擇出更優化的Query。
9)盡可能避免復雜的Join 和子查詢
我們都知道,MySQL 在並發這一塊做的並不是太好,當並發量太高的時候,系統整體性能可能會急劇下降,尤其是遇到一些較為復雜的Query 的時候更是如此。這主要與MySQL 內部資源的爭用鎖定控制有關,如讀寫相斥等等。對於Innodb 存儲引擎由於實現了行級鎖定可能還要稍微好一些,如果使用的MyISAM 存儲引擎,並發一旦較高的時候,性能下降非常明顯。所以,我們的Query 語句所涉及到的表越多,所需要鎖定的資源就越多。也就是說,越復雜的Join 語句,所需要鎖定的資源也就越多,所阻塞的其他線程也就越多。相反,如果我們將比較復雜的Query 語句分拆成多個較為簡單的Query 語句分步執行,每次鎖定的資源也就會少很多,所阻塞的其他線程也要少一些。
可能很多讀者會有疑問,將復雜Join 語句分拆成多個簡單的Query 語句之后,那不是我們的網絡交互就會更多了嗎?網絡延時方面的總體消耗也就更大了啊,完成整個查詢的時間不是反而更長了嗎?是的,這種情況是可能存在,但也並不是肯定就會如此。我們可以再分析一下,一個復雜的Join Query語句在執行的時候,所需要鎖定的資源比較多,可能被別人阻塞的概率也就更大,如果是一個簡單的Query,由於需要鎖定的資源較少,被阻塞的概率也會小很多。所以較為復雜的Join Query 也有可能在執行之前被阻塞而浪費更多的時間。而且,我們的數據庫所服務的並不是單單這一個Query 請求,還有很多很多其他的請求,在高並發的系統中,犧牲單個Query 的短暫響應時間而提高整體處理能力也是非常值得的。優化本身就是一門平衡與取舍的藝術,只有懂得取舍,平衡整體,才能讓系統更優。
對於子查詢,可能不需要我多說很多人就明白為什么會不被推薦使用。在MySQL 中,子查詢的實現目前還比較差,很難得到一個很好的執行計划,很多時候明明有索引可以利用,可Query Optimizer 就是不用。從MySQL 官方給出的信息說,這一問題將在MySQL6.0 中得到較好的解決,將會引入SemiJoin 的執行計划,可MySQL6.0 離我們投入生產環境使用恐怕還有很遙遠的一段時間。所以,在Query 優化的過程中,能不用子查詢的時候就盡量不要使用子查詢。
10)充分利用 Explain 和 Profi l ing
Explain的使用
參閱:
11)Profiling 的使用
MySQL 的Query Profiler 是一個使用非常方便的Query 診斷分析工具,通過該工具可以獲取一條Query 在整個執行過程中多種資源的消耗情況,如CPU,IO,IPC,SWAP 等,以及發生的PAGE FAULTS,CONTEXT SWITCHE 等等,同時還能得到該Query 執行過程中MySQL 所調用的各個函數在源文件中的位置。下面我們看看Query Profiler 的具體用法。
1.開啟profiling 參數
root@localhost : (none) 10:53:11> set profiling=1; Query OK, 0 rows affected (0.00 sec)
通過執行“set profiling”命令,可以開啟關閉Query Profiler 功能。
2.執行Query
... ... root@localhost : test 07:43:18> select status,count(*) -> from test_profiling group by status; +----------------+----------+ | status | count(*) | +----------------+----------+ | st_xxx1 | 27 | | st_xxx2 | 6666 | | st_xxx3 | 292887 | | st_xxx4 | 15 | +----------------+----------+ 5 rows in set (1.11 sec) ... ...
在開啟Query Profiler 功能之后,MySQL 就會自動記錄所有執行的Query 的profile 信息了。
3.獲取系統中保存的所有Query 的profile 概要信息
root@localhost : test 07:47:35> show profiles; +----------+------------+------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+------------------------------------------------------------+ | 1 | 0.00183100 | show databases | | 2 | 0.00007000 | SELECT DATABASE() | | 3 | 0.00099300 | desc test | | 4 | 0.00048800 | show tables | | 5 | 0.00430400 | desc test_profiling | | 6 | 1.90115800 | select status,count(*) from test_profiling group by status | +----------+------------+------------------------------------------------------------+ 3 rows in set (0.00 sec)
通過執行“SHOW PROFILE” 命令獲取當前系統中保存的多個Query 的profile 的概要信息。
4.針對單個Query 獲取詳細的profile 信息
在獲取到概要信息之后,我們就可以根據概要信息中的Query_ID 來獲取某個Query 在執行過程中詳細的profile 信息了,具體操作如下:
root@localhost : test 07:49:24> show profile cpu, block io for query 6; +----------------------+----------+----------+------------+--------------+---------------+ | Status | Duration | CPU_user | CPU_system | Block_ops_in | Block_ops_out | +----------------------+----------+----------+------------+--------------+---------------+ | starting | 0.000349 | 0.000000 | 0.000000 | 0 | 0 | | Opening tables | 0.000012 | 0.000000 | 0.000000 | 0 | 0 | | System lock | 0.000004 | 0.000000 | 0.000000 | 0 | 0 | | Table lock | 0.000006 | 0.000000 | 0.000000 | 0 | 0 | | init | 0.000023 | 0.000000 | 0.000000 | 0 | 0 | | optimizing | 0.000002 | 0.000000 | 0.000000 | 0 | 0 | | statistics | 0.000007 | 0.000000 | 0.000000 | 0 | 0 | | preparing | 0.000007 | 0.000000 | 0.000000 | 0 | 0 | | Creating tmp table | 0.000035 | 0.000999 | 0.000000 | 0 | 0 | | executing | 0.000002 | 0.000000 | 0.000000 | 0 | 0 | | Copying to tmp table | 1.900619 | 1.030844 | 0.197970 | 347 | 347 | | Sorting result | 0.000027 | 0.000000 | 0.000000 | 0 | 0 | | Sending data | 0.000017 | 0.000000 | 0.000000 | 0 | 0 | | end | 0.000002 | 0.000000 | 0.000000 | 0 | 0 | | removing tmp table | 0.000007 | 0.000000 | 0.000000 | 0 | 0 | | end | 0.000002 | 0.000000 | 0.000000 | 0 | 0 | | query end | 0.000003 | 0.000000 | 0.000000 | 0 | 0 | | freeing items | 0.000029 | 0.000000 | 0.000000 | 0 | 0 | | logging slow query | 0.000001 | 0.000000 | 0.000000 | 0 | 0 | | logging slow query | 0.000002 | 0.000000 | 0.000000 | 0 | 0 | | cleaning up | 0.000002 | 0.000000 | 0.000000 | 0 | 0 | +----------------------+----------+----------+------------+--------------+---------------+
上面的例子中是獲取CPU 和Block IO 的消耗,非常清晰,對於定位性能瓶頸非常適用。希望得到取其他的信息,都可以通過執行“SHOW PROFILE *** FOR QUERY n” 來獲取,各位讀者朋友可以自行測試熟悉。
11)合理設計並利用索引
因為索引對我們的Query 性能影響很大,所以我們更應該深入理解MySQL 中索引的基本實現,以及不同索引之間的區別,才能分析出如何設計出最優的索引來最大幅度的提升Query 的執行效率。
在MySQL 中,主要有四種類型的索引,分別為:B-Tree 索引,Hash 索引,Fulltext 索引和RTree索引,下面針對這四種索引的基本實現方式及存儲結構做一個大概的分析。
1.B-Tree 索引
B-Tree 索引是MySQL 數據庫中使用最為頻繁的索引類型,除了Archive 存儲引擎之外的其他所有的存儲引擎都支持B-Tree 索引。不僅僅在MySQL 中是如此,實際上在其他的很多數據庫管理系統中B-Tree 索引也同樣是作為最主要的索引類型,這主要是因為B-Tree 索引的存儲結構在數據庫的數據檢索中有非常優異的表現。
一般來說,MySQL 中的B-Tree 索引的物理文件大多都是以Balance Tree 的結構來存儲的,也就是所有實際需要的數據都存放於Tree 的Leaf Node,而且到任何一個Leaf Node 的最短路徑的長度都是完全相同的,所以我們大家都稱之為B-Tree 索引當然,可能各種數據庫(或MySQL 的各種存儲引擎)在存放自己的B-Tree 索引的時候會對存儲結構稍作改造。如Innodb 存儲引擎的B-Tree 索引實際使用的存儲結構實際上是B+Tree,也就是在B-Tree 數據結構的基礎上做了很小的改造,在每一個Leaf Node 上面除了存放索引鍵的相關信息之外,還存儲了指向與該Leaf Node 相鄰的后一個LeafNode 的指針信息,這主要是為了加快檢索多個相鄰Leaf Node 的效率考慮。
在Innodb 存儲引擎中,存在兩種不同形式的索引,一種是Cluster 形式的主鍵索引(Primary Key),另外一種則是和其他存儲引擎(如MyISAM 存儲引擎)存放形式基本相同的普通B-Tree 索引,這種索引在Innodb 存儲引擎中被稱為Secondary Index。下面我們通過圖示來針對這兩種索引的存放形式做一個比較。
圖示中左邊為Clustered 形式存放的Primary Key,右側則為普通的B-Tree 索引。兩種索引在Root Node 和Branch Nodes 方面都還是完全一樣的。而Leaf Nodes 就出現差異了。在Primary Key中,Leaf Nodes 存放的是表的實際數據,不僅僅包括主鍵字段的數據,還包括其他字段的數據,整個數據以主鍵值有序的排列。而Secondary Index 則和其他普通的B-Tree 索引沒有太大的差異,只是在Leaf Nodes 除了存放索引鍵的相關信息外,還存放了Innodb 的主鍵值。
所以,在Innodb 中如果通過主鍵來訪問數據效率是非常高的,而如果是通過Secondary Index 來訪問數據的話,Innodb 首先通過Secondary Index 的相關信息,通過相應的索引鍵檢索到Leaf Node之后,需要再通過Leaf Node 中存放的主鍵值再通過主鍵索引來獲取相應的數據行。
MyISAM 存儲引擎的主鍵索引和非主鍵索引差別很小,只不過是主鍵索引的索引鍵是一個唯一且非空的鍵而已。而且MyISAM 存儲引擎的索引和Innodb 的Secondary Index 的存儲結構也基本相同,主要的區別只是MyISAM 存儲引擎在Leaf Nodes 上面出了存放索引鍵信息之外,再存放能直接定位到MyISAM 數據文件中相應的數據行的信息(如Row Number),但並不會存放主鍵的鍵值信息。
2.Hash 索引
Hash 索引在MySQL 中使用的並不是很多,目前主要是Memory 存儲引擎使用,而且在Memory 存儲引擎中將Hash 索引作為默認的索引類型。所謂Hash 索引,實際上就是通過一定的Hash 算法,將需要索引的鍵值進行Hash 運算,然后將得到的Hash 值存入一個Hash 表中。然后每次需要檢索的時候,都會將檢索條件進行相同算法的Hash 運算,然后再和Hash 表中的Hash 值進行比較並得出相應的信息。
在Memory 存儲引擎中,MySQL 還支持非唯一的Hash 索引。可能很多人會比較驚訝,如果是非唯一的Hash 索引,那相同的值該如何處理呢?在Memory 存儲引擎的Hash 索引中,如果遇到非唯一值,存儲引擎會將他們鏈接到同一個hash 鍵值下以一個鏈表的形式存在,然后在取得實際鍵值的時候時候再過濾不符合的鍵。
由於Hash 索引結構的特殊性,其檢索效率非常的高,索引的檢索可以一次定位,而不需要像BTree索引需要從根節點再到枝節點最后才能訪問到頁節點這樣多次IO 訪問,所以Hash 索引的效率要遠高於B-Tree 索引。
可能很多人又會有疑問了,既然Hash 索引的效率要比B-Tree 高很多,為什么大家不都用Hash索引而還要使用B-Tree 索引呢?任何事物都是有兩面性的,,Hash 索引也一樣,雖然Hash 索引檢索效率非常之高,但是Hash 索引本身由於其實的特殊性也帶來了很多限制和弊端,主要有以下這些:
1. Hash 索引僅僅只能滿足“=”,“IN”和“<=>”查詢,不能使用范圍查詢;
由於Hash 索引所比較的是進行Hash 運算之后的Hash 值,所以Hash 索引只能用於等值的過濾,而不能用於基於范圍的過濾,因為經過相應的Hash 算法處理之后的Hash 值的大小關系,並不能保證還和Hash 運算之前完全一樣。
2. Hash 索引無法被利用來避免數據的排序操作;
由於Hash 索引中存放的是經過Hash 計算之后的Hash 值,而且Hash 值的大小關系並不一定和Hash 運算前的鍵值的完全一樣,所以數據庫無法利用索引的數據來避免任何和排序運算;
3. Hash 索引不能利用部分索引鍵查詢;
對於組合索引,Hash 索引在計算Hash 值的時候是組合索引鍵合並之后再一起計算Hash 值,而不是單獨計算Hash 值,所以當我們通過組合索引的前面一個或幾個索引鍵進行查詢的時候,Hash 索引也無法被利用到;
4. Hash 索引在任何時候都不能避免表掃面;
前面我們已經知道,Hash 索引是將索引鍵通過Hash 運算之后,將Hash 運算結果的Hash 值和所對應的行指針信息存放於一個Hash 表中,而且由於存在不同索引鍵存在相同Hash 值的可能,所以即使我們僅僅取滿足某個Hash 鍵值的數據的記錄條數,都無法直接從Hash 索引中直接完成查詢,還是要通過訪問表中的實際數據進行相應的比較而得到相應的結果。
5. Hash 索引遇到大量Hash 值相等的情況后性能並不一定就會比B-Tree 索引高;
對於選擇性比較低的索引鍵,如果我們創建Hash 索引,那么我們將會存在大量記錄指針信息存與同一個Hash 值相關連。這樣要定位某一條記錄的時候就會非常的麻煩,可能會浪費非常多次表數據的訪問,而造成整體性能的地下。
3.Full-text 索引
Full-text 索引也就是我們常說的全文索引,目前在MySQL 中僅有MyISAM 存儲引擎支持,而且也並不是所有的數據類型都支持全文索引。目前來說,僅有CHAR,VARCHAR 和TEXT 這三種數據類型的列可以建Full-text 索引。
一般來說,Fulltext 索引主要用來替代效率低下的LIKE '%***%' 操作。實際上,Full-text 索引並不只是能簡單的替代傳統的全模糊LIKE 操作,而且能通過多字段組合的Full-text 索引一次全模糊匹配多個字段。
Full-text 索引和普通的B-Tree 索引的實現區別較大,雖然他同樣是以B-Tree 形式來存放索引數據,但是他並不是通過字段內容的完整匹配,而是通過特定的算法,將字段數據進行分隔后再進行的索引。一般來說MySQL 系統會按照四個字節來分隔。在整個Full-text 索引中,存儲內容被分為兩部分,一部分是分隔前的索引字符串數據集合,另一部分是分隔后的詞(或者詞組)的索引信息。所以,Full-text 索引中,真正在B-Tree 索引細細中的並不是我們表中的原始數據,而是分詞之后的索引數據。在B-Tree 索引的節點信息中,存放了各個分隔后的詞信息,以及指向包含該詞的分隔前字符串信息在索引數據集合中的位置信息。
Full-text 索引不僅僅能實現模糊匹配查找,在實現了基於自然語言的的匹配度查找。當然,這個匹配讀到底有多准確就需要讀者朋友去自行驗證了。Full-text 通過一些特定的語法信息,針對自然語言做了各種相應規則的匹配,最后給出非負的匹配值。
此外,有一點是需要大家注意的,MySQL 目前的Full-text 索引在中文支持方面還不太好,需要借助第三方的補丁或者插件來完成。而且Full-text 的創建所消耗的資源也是比較大的,所以在應用於實際生產環境之前還是盡量做好評估。
關於Full-text 的實際使用方法由於不是本書的重點,感興趣的讀者朋友可以自行參閱MySQL 關於Full-text 相關的使用手冊來了解更為詳盡的信息。
4.R-Tree 索引
R-Tree 索引可能是我們在其他數據庫中很少見到的一種索引類型,主要用來解決空間數據檢索的問題。
在MySQL 中,支持一種用來存放空間信息的數據類型GEOMETRY,且基於OpenGIS 規范。在MySQL5.0.16 之前的版本中,僅僅MyISAM 存儲引擎支持該數據類型,但是從MySQL5.0.16 版本開始,BDB,Innodb,NDBCluster 和Archive 存儲引擎也開始支持該數據類型。當然,雖然多種存儲引擎都開始支持GEOMETRY 數據類型,但是僅僅之后MyISAM 存儲引擎支持R-Tree 索引。
在MySQL 中采用了具有二次分裂特性的R-Tree 來索引空間數據信息,然后通過幾何對象(MRB)信息來創建索引。
雖然僅僅只有MyISAM 存儲引擎支持空間索引(R-Tree Index),但是如果我們是精確的等值匹配,創建在空間數據上面的B-Tree 索引同樣可以起到優化檢索的效果,空間索引的主要優勢在於當我們使用范圍查找的時候,可以利用到R-Tree 索引,而這時候,B-Tree 索引就無能為力了。
對於R-Tree 索引的詳細介紹和使用信息清參閱MySQL 使用手冊。
5.索引的利弊與如何判定是否需要索引
索引能夠極大的提高我們數據檢索的效率,讓我們的Query 執行的更快,但是索引在極大提高檢索效率的同時,也給我們的數據庫帶來了一些負面的影響。下面我們就分別對MySQL 中索引的利與弊做一個簡單的分析。
5-1.索引的利處
索引除了能夠提高數據檢索的效率,降低數據庫的IO 成本外,還有一個非常重要的用途,那就是降低數據的排序成本。
我們知道,每個索引中索引數據都是按照索引鍵鍵值進行排序后存放的,所以,當我們的Query 語句中包含排序分組操作的時候,如果我們的排序字段和索引鍵字段剛好一致,MySQL Query Optimizer就會告訴mysqld 在取得數據之后不用排序了,因為根據索引取得的數據已經是滿足客戶的排序要求。
那如果是分組操作呢?分組操作沒辦法直接利用索引完成。但是分組操作是需要先進行排序然后才分組的,所以當我們的Query 語句中包含分組操作,而且分組字段也剛好和索引鍵字段一致,那么mysqld 同樣可以利用到索引已經排好序的這個特性而省略掉分組中的排序操作。
排序分組操作主要消耗的是我們的內存和CPU 資源,如果我們能夠在進行排序分組操作中利用好索引,將會極大的降低CPU 資源的消耗。
5-2.索引的弊端
雖然,索引能夠極大的提高數據檢索效率,也能夠改善排序分組操作的性能,但是我們不能忽略的一個問題就是索引是完全獨立於基礎數據之外的一部分數據。
假設我們在Table ta 中的Column ca 創建了索引idx_ta_ca,那么任何更新Column ca 的操作,MySQL 都需要在更新表中Column ca 的同時,也更新Column ca 的索引數據,調整因為更新所帶來鍵值變化后的索引信息。而如果我們沒有對Column ca 進行索引的話,MySQL 所需要做的僅僅只是更新表中Column ca 的信息。這樣,所帶來的最明顯的資源消耗就是增加了更新所帶來的IO 量和調整索引所致的計算量。此外,Column ca 的索引idx_ta_ca 是需要占用存儲空間的,而且隨着Table ta 數據量的增長,idx_ta_ca 所占用的空間也會不斷增長。所以索引還會帶來存儲空間資源消耗的增長。
歸納總結下索引的優缺點
重點,需要熟背
優點:
a.提高數據檢索效率
b.降低數據庫的IO成本
c.降低數據的排序成本
缺點:
a.數據更新時需要額外更新相關索引信息,增加了了數據庫IO和計算量
b.需要占用存儲空間
6.如何判定是否需要創建索引
下面列出一些基本的判定策略來幫助我們分析是否需要創建索引。
6-1.較頻繁的作為查詢條件的字段應該創建索引;
提高數據查詢檢索的效率最有效的辦法就是減少需要訪問的數據量,從上面所了解到的索引的益處中我們知道了,索引正是我們減少通過索引鍵字段作為查詢條件的Query 的IO 量的最有效手段。所以一般來說我們應該為較為頻繁的查詢條件字段創建索引。
6-2.唯一性太差的字段不適合單獨創建索引,即使頻繁作為查詢條件;
唯一性太差的字段主要是指哪些呢?如狀態字段,類型字段等等這些字段中存方的數據可能總共就是那么幾個幾十個值重復使用,每個值都會存在於成千上萬或是更多的記錄中。對於這類字段,我們完全沒有必要創建單獨的索引的。因為即使我們創建了索引,MySQL Query Optimizer 大多數時候也不會去選擇使用,如果什么時候MySQL Query Optimizer 抽了一下風選擇了這種索引,那么非常遺憾的告訴你,這可能會帶來極大的性能問題。由於索引字段中每個值都含有大量的記錄,那么存儲引擎在根據索引訪問數據的時候會帶來大量的隨機IO,甚至有些時候可能還會出現大量的重復IO。
6-3.更新非常頻繁的字段不適合創建索引;
上面在索引的弊端中我們已經分析過了,索引中的字段被更新的時候,不僅僅需要更新表中的數據,同時還要更新索引數據,以確保索引信息是准確的。這個問題所帶來的是IO 訪問量的較大增加,不僅僅影響更新Query 的響應時間,還會影響整個存儲系統的資源消耗,加大整個存儲系統的負載。
那如何定義“非常頻繁”呢?每秒,每分鍾,還是每小時呢?說實話,這個還真挺難定義的。很多時候還是通過比較同一時間段內被更新的次數和利用該字段作為條件的查詢次數來判斷。
6-4.不會出現在WHERE 子句中的字段不該創建索引;
不會還有人會問為什么吧?自己也覺得這是廢話了,哈哈!
7.單鍵索引還是組合索引
對於這個問題,很難有一個絕對的定論,我們需要從多方面來分析考慮,平衡兩種方案各自的優劣,然后選擇一種最佳的方案來解決。因為從上一節中我們了解到了索引在提高某些查詢的性能的同時,也會讓某些更新的效率下降。而組合索引中因為有多個字段的存在,理論上被更新的可能性肯定比單鍵索引要大很多,這樣可能帶來的附加成本也就比單鍵索引要高。但是,當我們的WHERE 子句中的查詢條件含有多個字段的時候,通過這多個字段共同組成的組合索引的查詢效率肯定比僅僅只用過濾條件中的某一個字段創建的索引要高。因為通過單鍵索引所能過濾的數據並不完整,和通過組合索引相比,存儲引擎需要訪問更多的記錄數,自然就會訪問更多的數據量,也就是說需要更高的IO 成本。
可能有些朋友會說,那我們可以通過創建多個單鍵索引啊。確實,我們可以將WHERE 子句中的每一個字段都創建一個單鍵索引。但是這樣真的有效嗎?在這樣的情況下,MySQL Query Optimizer 大多數時候都只會選擇其中的一個索引,然后放棄其他的索引。即使他選擇了同時利用兩個或者更多的索引通過INDEX_MERGE 來優化查詢,可能所收到的效果並不會比選擇其中某一個單鍵索引更高效。因為如果選擇通過INDEX_MERGE 來優化查詢,就需要訪問多個索引,同時還要將通過訪問到的幾個索引進行merge操作,所帶來的成本可能反而會比選擇其中一個最有效的索引來完成查詢更高。
在一般的應用場景中,只要不是其中某個過濾字段在大多數場景下都能過濾出90%以上的數據,而且其他的過濾字段會存在頻繁的更新,我一般更傾向於創建組合索引,尤其是在並發量較高的場景下更是應該如此。因為當我們的並發量較高的時候,即使我們為每個Query 節省很少的IO 消耗,但因為執行量非常大,所節省的資源總量仍然是非常可觀的。
當然,我們創建組合索引並不是說就需要將查詢條件中的所有字段都放在一個索引中,我們還應該盡量讓一個索引被多個Query 語句所利用,盡量減少同一個表上面索引的數量,減少因為數據更新所帶來的索引更新成本,同時還可以減少因為索引所消耗的存儲空間。
此外,MySQL 還為我們提供了一個減少優化索引自身的功能,那就是前綴索引。在MySQL 中,我們可以僅僅使用某個字段的前面部分內容做為索引鍵來索引該字段,來達到減小索引占用的存儲空間和提高索引訪問的效率。當然,前綴索引的功能僅僅適用於字段前綴比較隨機重復性很小的字段。如果我們需要索引的字段的前綴內容有較多的重復,索引的過濾性自然也會隨之降低,通過索引所訪問的數據量就會增加,這時候前綴索引雖然能夠減少存儲空間消耗,但是可能會造成Query 訪問效率的極大降低,反而得不償失。
8.Query 的索引選擇
在有些場景下,我們的Query 由於存在多個過濾條件,而這多個過濾條件可能會存在於兩個或者更多的索引中。在這種場景下,MySQL Query Optimizer 一般情況下都能夠根據系統的統計信息選擇出一個針對該Query 最優的索引完成查詢,但是在有些情況下,可能是由於我們的系統統計信息的不夠准確完整,也可能是MySQL Query Optimizer 自身功能的缺陷,會造成他並沒有選擇一個真正最優的索引而選擇了其他查詢效率較低的索引。在這種時候,我們就不得不通過人為干預,在Query 中增加Hint 提示MySQL Query Optimizer 告訴他該使用哪個索引而不該使用哪個索引,或者通過調整查詢條件來達到相同的目的。
SELECT * FROM group_message force index(group_message_author_subject) WHERE author = '3' subject LIKE 'weiurazs%'
下面是我對於選擇合適索引的幾點建議,並不一定在任何場景下都合適,但在大多數場景下還是比較適用的。
1. 對於單鍵索引,盡量選擇針對當前Query 過濾性更好的索引;
2. 在選擇組合索引的時候,當前Query 中過濾性最好的字段在索引字段順序中排列越靠前越好;
3. 在選擇組合索引的時候,盡量選擇可以能夠包含當前Query 的WHERE 子句中更多字段的索引;
4. 盡可能通過分析統計信息和調整Query 的寫法來達到選擇合適索引的目的而減少通過使用Hint 人為控制索引的選擇,因為這會使后期的維護成本增加,同時增加維護所帶來的潛在風險。
5. 只要列中包含有NULL值都將不會被包含在索引中,復合索引中只要有一列含有NULL值,那么這一列對於此復合索引就是無效的。所以我們在數據庫設計時不要讓字段的默認值為NULL。
9.MySQL 中索引的限制
在使用索引的同時,我們還應該了解在MySQL 中索引存在的限制,以便在索引應用中盡可能的避開限制所帶來的問題。下面列出了目前MySQL 中索引使用相關的限制。
1. MyISAM 存儲引擎索引鍵長度總和不能超過1000 字節;
2. BLOB 和TEXT 類型的列只能創建前綴索引;
3. MySQL 目前不支持函數索引;
4. 使用不等於(!= 或者<>)的時候MySQL 無法使用索引;
5. 過濾字段使用了函數運算后(如abs(column)),MySQL 無法使用索引;
6. Join 語句中Join 條件字段類型不一致的時候MySQL 無法使用索引;
7. 使用LIKE 操作的時候如果條件以通配符開始( '%abc...')MySQL 無法使用索引;
8. 使用非等值查詢的時候MySQL 無法使用Hash 索引;
在我們使用索引的時候,需要注意上面的這些限制,尤其是要注意無法使用索引的情況,因為這很容易讓我們因為疏忽而造成極大的性能隱患。
10.索引的一些使用技巧
1.只要列中包含有NULL值都將不會被包含在索引中,復合索引中只要有一列含有NULL值,那么這一列對於此復合索引就是無效的。所以我們在數據庫設計時不要讓字段的默認值為NULL。
2.對串列進行索引,如果可能應該指定一個前綴長度。例如,如果有一個CHAR(255)的列,如果在前10個或20個字符內,多數值是惟一的,那么就不要對整個列進行索引。短索引不僅可以提高查詢速度而且可以節省磁盤空間和I/O操作。
3.MySQL查詢只使用一個索引,因此如果where子句中已經使用了索引的話,那么order by中的列是不會使用索引的。因此數據庫默認排序可以符合要求的情況下不要使用排序操作;盡量不要包含多個列的排序,如果需要最好給這些列創建復合索引。
4.一般情況下不鼓勵使用like操作,如果非使用不可,如何使用也是一個問題。like “%aaa” 不會使用索引而like “aaa%”可以使用索引。
12)Join 的實現原理及優化思路
在MySQL 中,只有一種Join 算法,就是大名鼎鼎的Nested Loop Join。顧名思義,Nested Loop Join 實際上就是通過驅動表的結果集作為循環基礎數據,然后一條一條的通過該結果集中的數據作為過濾條件到下一個表中查詢數據,然后合並結果。如果還有第三個參與Join,則再通過前兩個表的Join 結果集作為循環基礎數據,再一次通過循環查詢條件到第三個表中查詢數據,如此往復。
三表Join示例
select m.subject msg_subject, c.content msg_content from user_group g,group_message m,group_message_content c where g.user_id = 1 and m.group_id = g.group_id and c.group_msg_id = m.id
為group_message 表增加了一個group_id 的索引:
create index idx_group_message_gid_uid on group_message(group_id);
查看Query 的執行計划:
sky@localhost : example 11:17:04> explain select m.subject msg_subject, c.content msg_content -> from user_group g,group_message m,group_message_content c -> where g.user_id = 1 -> and m.group_id = g.group_id -> and c.group_msg_id = m.id\G *************************** 1. row *************************** id: 1 select_type: SIMPLE table: g type: ref possible_keys: user_group_gid_ind,user_group_uid_ind,user_group_gid_uid_ind key: user_group_uid_ind key_len: 4 ref: const rows: 2 Extra: *************************** 2. row *************************** id: 1 select_type: SIMPLE table: m type: ref possible_keys: PRIMARY,idx_group_message_gid_uid key: idx_group_message_gid_uid key_len: 4 ref: example.g.group_id rows: 3 Extra: *************************** 3. row *************************** id: 1 select_type: SIMPLE table: c type: ref possible_keys: idx_group_message_content_msg_id key: idx_group_message_content_msg_id key_len: 4 ref: example.m.id rows: 2 Extra:
這個過程可以通過如下表達式來表示:
for each record g_rec in table user_group that g_rec.user_id=1{ for each record m_rec in group_message that m_rec.group_id=g_rec.group_id{ for each record c_rec in group_message_content that c_rec.group_msg_id=m_rec.id pass the (g_rec.user_id, m_rec.subject, c_rec.content) row combination to output; } }
Join 語句的優化
1. 盡可能減少Join 語句中的Nested Loop 的循環總次數;
如何減少Nested Loop 的循環總次數?最有效的辦法只有一個,那就是讓驅動表的結果集盡可能的小(★★★★★需牢記,這是第一核心★★★★★)
2. 優先優化Nested Loop 的內層循環;
3.保證Join 語句中被驅動表上Join 條件字段已經被索引;
4. 當無法保證被驅動表的Join 條件字段被索引且內存資源充足的前提下,不要太吝惜Join Buffer 的設置;
13)ORDER BY,GROUP BY 和 DI STI NCT 優化
ORDER BY,GROUP BY 以及DISTINCT 這三類查詢。考慮到這三類查詢都涉及到數據的排序等操作,所以我將他們放在了一起。
ORDER BY 的實現與優化
在MySQL 中,ORDER BY 的實現有如下兩種類型:
◆ 一種是通過有序索引而直接取得有序的數據,這樣不用進行任何排序操作即可得到滿足客戶端要求的有序數據返回給客戶端;
◆ 另外一種則需要通過MySQL 的排序算法將存儲引擎中返回的數據進行排序然后再將排序后的數據返回給客戶端。
1. 加大max_length_for_sort_data 參數的設置;
2. 去掉不必要的返回字段;
3. 增大sort_buffer_size 參數設置;
GROUP BY 的實現與優化
在MySQL 中,GROUP BY 的實現同樣有多種(三種)方式,其中有兩種方式會利用現有的索引信息來完成GROUP BY,另外一種為完全無法使用索引的場景下使用。下面我們分別針對這三種實現方式做一個分析。
1. 使用松散(Loose)索引掃描實現GROUP BY
何謂松散索引掃描實現GROUP BY 呢?實際上就是當MySQL 完全利用索引掃描來實現GROUP BY 的時候,並不需要掃描所有滿足條件的索引鍵即可完成操作得出結果。
2. 使用緊湊(Tight)索引掃描實現GROUP BY
緊湊索引掃描實現GROUP BY 和松散索引掃描的區別主要在於他需要在掃描索引的時候,讀取所有滿足條件的索引鍵,然后再根據讀取的數據來完成GROUP BY 操作得到相應結果。
3. 使用臨時表實現GROUP BY
MySQL 在進行GROUP BY 操作的時候要想利用所有,必須滿足GROUP BY 的字段必須同時存放於同一個索引中,且該索引是一個有序索引(如Hash 索引就不能滿足要求)。而且,並不只是如此,是否能夠利用索引來實現GROUP BY 還與使用的聚合函數也有關系。
對於上面三種MySQL 處理GROUP BY 的方式,我們可以針對性的得出如下兩種優化思路:
1. 盡可能讓MySQL 可以利用索引來完成GROUP BY 操作,當然最好是松散索引掃描的方式最佳。在系統允許的情況下,我們可以通過調整索引或者調整Query 這兩種方式來達到目的;
2. 當無法使用索引完成GROUP BY 的時候,由於要使用到臨時表且需要filesort,所以我們必須要有足夠的sort_buffer_size 來供MySQL 排序的時候使用,而且盡量不要進行大結果集的GROUP BY 操作,因為如果超出系統設置的臨時表大小的時候會出現將臨時表數據copy 到磁盤上面再進行操作,這時候的排序分組操作性能將是成數量級的下降;
更多參閱:MySQL松散索引掃描與緊湊索引掃描
DISTINCT 的實現與優化
DISTINCT 實際上和GROUP BY 的操作非常相似,只不過是在GROUP BY 之后的每組中只取出一條記錄而已。