一文讀懂MySQL的索引結構及查詢優化

回顧前文: 一文學會MySQL的explain工具html

(同時再次強調,這幾篇關於MySQL的探究都是基於5.7版本,相關總結與結論不必定適用於其餘版本)java

MySQL官方文檔中(https://dev.mysql.com/doc/refman/5.7/en/optimization-indexes.html)有這樣一段描述:mysql

The best way to improve the performance of SELECT operations is to create indexes on one or more of the columns that are tested in the query. But unnecessary indexes waste space and waste time for MySQL to determine which indexes to use. Indexes also add to the cost of inserts, updates, and deletes because each index must be updated. You must find the right balance to achieve fast queries using the optimal set of indexes.算法

就是說提升查詢性能最直接有效的方法就是創建索引,可是沒必要要的索引會浪費空間,同時也增長了額外的時間成本去判斷應該走哪一個索引,此外,索引還會增長插入、更新、刪除數據的成本,由於作這些操做的同時還要去維護(更新)索引樹。所以,應該學會使用最佳索引集來優化查詢。sql

索引結構

參考:數據庫

  1. 《MySQL索引背後的數據結構及算法原理》http://blog.codinglabs.org/articles/theory-of-mysql-index.htmljson

  2. 《Mysql BTree和B+Tree詳解》https://www.cnblogs.com/Transkai/p/11595405.htmlsession

  3. 《爲何MySQL使用B+樹》https://draveness.me/whys-the-design-mysql-b-plus-tree/數據結構

  4. 《淺入淺出MySQL和InnoDB》https://draveness.me/mysql-innodb/app

  5. 《漫畫:什麼是B樹?》https://mp.weixin.qq.com/s/rDCEFzoKHIjyHfI_bsz5Rw

什麼是索引

在MySQL中,索引(Index)是幫助高效獲取數據的數據結構。這種數據結構MySQL中最經常使用的就是B+樹(B+Tree)。

Indexes are used to find rows with specific column values quickly. Without an index, MySQL must begin with the first row and then read through the entire table to find the relevant rows.

就比如給你一本書和一篇文章標題,若是沒有目錄,讓你找此標題對應的文章,可能須要從第一頁翻到最後一頁;若是有目錄大綱,你可能只須要在目錄頁尋找此標題,而後迅速定位文章。

這裏咱們能夠把書(book)當作是MySQL中的table,把文章(article)當作是table中的一行記錄,即row文章標題(title)當作row中的一列column目錄天然就是對title列創建的索引index了,這樣根據文章標題從書中檢索文章就對應sql語句select * from book where title = ?,相應的,書中每增長一篇文章(即insert into book (title, ...) values ('華山論劍', ...)),都須要維護一下目錄,這樣才能從目錄中找到新增的文章華山論劍,這一操做對應的是MySQL中每插入(insert)一條記錄須要維護title列的索引樹(B+Tree)。

爲何使用B+Tree

首先須要澄清的一點是,MySQL跟B+樹沒有直接的關係,真正與B+樹有關係的是MySQL的默認存儲引擎InnoDB,MySQL中存儲引擎的主要做用是負責數據的存儲和提取,除了InnoDB以外,MySQL中也支持好比MyISAM等其餘存儲引擎(詳情見https://dev.mysql.com/doc/refman/5.7/en/storage-engine-setting.html)做爲表的底層存儲引擎。

mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine             | Support | Comment                                                        | Transactions | XA   | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| MRG_MYISAM         | YES     | Collection of identical MyISAM tables                          | NO           | NO   | NO         |
| CSV                | YES     | CSV storage engine                                             | NO           | NO   | NO         |
| PERFORMANCE_SCHEMA | YES     | Performance Schema                                             | NO           | NO   | NO         |
| BLACKHOLE          | YES     | /dev/null storage engine (anything you write to it disappears) | NO           | NO   | NO         |
| InnoDB             | DEFAULT | Supports transactions, row-level locking, and foreign keys     | YES          | YES  | YES        |
| MyISAM             | YES     | MyISAM storage engine                                          | NO           | NO   | NO         |
| ARCHIVE            | YES     | Archive storage engine                                         | NO           | NO   | NO         |
| MEMORY             | YES     | Hash based, stored in memory, useful for temporary tables      | NO           | NO   | NO         |
| FEDERATED          | NO      | Federated MySQL storage engine                                 | NULL         | NULL | NULL       |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+

提到索引,咱們可能會立馬想到下面幾種數據結構來實現。

(1) 哈希表
哈希雖然可以提供O(1)的單數據行的查詢性能,可是對於範圍查詢排序卻沒法很好支持,需全表掃描。

(2) 紅黑樹
紅黑樹(Red Black Tree)是一種自平衡二叉查找樹,在進行插入和刪除操做時經過特定操做保持二叉查找樹的平衡,從而得到較高的查找性能。

通常來講,索引自己也很大,每每不可能所有存儲在內存中,所以索引每每以索引文件的形式存儲的磁盤上。這樣的話,索引查找過程當中就要產生磁盤I/O消耗,相對於內存存取,I/O存取的消耗遠遠高於內存,因此評價一個數據結構做爲索引的優劣最重要的指標就是查找過程當中磁盤I/O次數。換句話說,索引的結構組織要儘可能減小查找過程當中磁盤I/O的次數。

在這裏,磁盤I/O的次數取決於樹的高度,因此,在數據量較大時,紅黑樹會因樹的高度較大而形成磁盤IO較多,從而影響查詢效率。

(3) B-Tree
B樹中的B表明平衡(Balance),而不是二叉(Binary),B樹是從平衡二叉樹演化而來的。

爲了下降樹的高度(也就是減小磁盤I/O次數),把原來瘦高的樹結構變得矮胖,B樹會在每一個節點存儲多個元素(紅黑樹每一個節點只會存儲一個元素),而且節點中的元素從左到右遞增排列。以下圖所示:

B-Tree結構圖

B-Tree在查詢的時候比較次數其實不比二叉查找樹少,但在內存中的大小比較、二分查找的耗時相比磁盤IO耗時幾乎能夠忽略。 B-Tree大大下降了樹的高度,因此也就極大地提高了查找性能。

(4) B+Tree
B+Tree是在B-Tree基礎上進一步優化,使其更適合實現存儲索引結構。InnoDB存儲引擎就是用B+Tree實現其索引結構。

B-Tree結構圖中能夠看到每一個節點中不只包含數據的key值,還有data值。而每個節點的存儲空間是有限的,若是data值較大時將會致使每一個節點能存儲的key的數量很小,這樣會致使B-Tree的高度變大,增長了查詢時的磁盤I/O次數,進而影響查詢性能。在B+Tree中,全部data值都是按照鍵值大小順序存放在同一層的葉子節點上,而非葉子節點上只存儲key值信息,這樣能夠增大每一個非葉子節點存儲的key值數量,下降B+Tree的高度,提升效率。

B+Tree結構圖

這裏補充一點相關知識 在計算機中,磁盤每每不是嚴格按需讀取,而是每次都會預讀,即便只須要一個字節,磁盤也會從這個位置開始,順序向後讀取必定長度的數據放入內存。這樣作的理論依據是計算機科學中著名的局部性原理

當一個數據被用到時,其附近的數據也一般會立刻被使用。

因爲磁盤順序讀取的效率很高(不須要尋道時間,只需不多的旋轉時間),所以對於具備局部性的程序來講,預讀能夠提升I/O效率。預讀的長度通常爲頁(page)的整數倍。

是計算機管理存儲器的邏輯塊,硬件及操做系統每每將主存和磁盤存儲區分割爲連續的大小相等的塊,每一個存儲塊稱爲一頁(許多操做系統的頁默認大小爲4KB),主存和磁盤以頁爲單位交換數據。當程序要讀取的數據不在主存中時,會觸發一個缺頁異常,此時操做系統會向磁盤發出讀盤信號,磁盤會找到數據的起始位置並向後連續讀取一頁或幾頁載入內存中,而後異常返回,程序繼續運行。(以下命令能夠查看操做系統的默認頁大小)

$ getconf PAGE_SIZE
4096

數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設爲操做系統的頁大小的整數倍,這樣每一個節點只須要一次I/O就能夠徹底載入。

InnoDB存儲引擎中也有頁(Page)的概念,頁是其磁盤管理的最小單位。InnoDB存儲引擎中默認每一個頁的大小爲16KB。

mysql> show variables like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| innodb_page_size | 16384 |
+------------------+-------+
1 row in set (0.01 sec)

通常表的主鍵類型爲INT(佔4個字節)或BIGINT(佔8個字節),指針類型也通常爲4或8個字節,也就是說一個頁(B+Tree中的一個節點)中大概存儲16KB/(8B+8B)=1K個鍵值(由於是估值,爲方便計算,這裏的K取值爲10^3)。也就是說一個深度爲3的B+Tree索引能夠維護10^3 * 10^3 * 10^3 = 10億條記錄。

B+Tree的高度通常都在2到4層。mysql的InnoDB存儲引擎在設計時是將根節點常駐內存的,也就是說查找某一鍵值的行記錄時最多隻須要1到3次磁盤I/O操做。

隨機I/O對於MySQL的查詢性能影響會很是大,而順序讀取磁盤中的數據會很快,由此咱們也應該儘可能減小隨機I/O的次數,這樣才能提升性能。在B-Tree中因爲全部的節點均可能包含目標數據,咱們老是要從根節點向下遍歷子樹查找知足條件的數據行,這會帶來大量的隨機I/O,而B+Tree全部的數據行都存儲在葉子節點中,而這些葉子節點經過雙向鏈表依次按順序鏈接,當咱們在B+樹遍歷數據(好比說範圍查詢)時能夠直接在多個葉子節點之間進行跳轉,保證順序倒序遍歷的性能。

另外,對以上提到的數據結構不熟悉的朋友,這裏推薦一個在線數據結構可視化演示工具,有助於快速理解這些數據結構的機制:https://www.cs.usfca.edu/~galles/visualization/Algorithms.html

主鍵索引

上面也有說起,在MySQL中,索引屬於存儲引擎級別的概念。不一樣存儲引擎對索引的實現方式是不一樣的,這裏主要看下MyISAMInnoDB兩種存儲引擎的索引實現方式。

MyISAM索引實現

MyISAM引擎使用B+Tree做爲索引結構時葉子節點的data域存放的是數據記錄的地址。以下圖所示:

MyISAM主鍵索引原理圖

由上圖能夠看出:MyISAM索引文件和數據文件是分離的,索引文件僅保存數據記錄的地址,所以MyISAM的索引方式也叫作非彙集的,之因此這麼稱呼是爲了與InnoDB的彙集索引區分。

InnoDB索引實現

InnoDB主鍵索引也使用B+Tree做爲索引結構時的實現方式卻與MyISAM大相徑庭。InnoDB的數據文件自己就是索引文件。在InnoDB中,表數據文件自己就是按B+Tree組織的一個索引結構,這棵樹的葉子節點data域保存了完整的數據記錄,這個索引的key是數據表的主鍵,所以InnoDB表數據文件自己就是主索引。

InnoDB主鍵索引原理圖

InnoDB存儲引擎中的主鍵索引(primary key)又叫作彙集索引(clustered index)。由於InnoDB的數據文件自己要按主鍵彙集,因此InnoDB要求表必須有主鍵(MyISAM能夠沒有),若是沒有顯式指定,則MySQL系統會自動選擇一個能夠惟一標識數據記錄的列做爲主鍵,若是不存在這種列,則MySQL自動爲InnoDB表生成一個隱含字段做爲主鍵,這個字段長度爲6個字節,類型爲長整形。(詳情見官方文檔:https://dev.mysql.com/doc/refman/5.7/en/innodb-index-types.html)

彙集索引這種實現方式使得按主鍵搜索十分高效,直接能查出整行數據。

在InnoDB中,用非單調遞增的字段做爲主鍵不是個好主意,由於InnoDB數據文件自己是一棵B+Tree,非單增的主鍵會形成在插入新記錄時數據文件爲了維持B+Tree的特性而頻繁的分裂調整,十分低效,於是使用遞增字段做爲主鍵則是一個很好的選擇。

非主鍵索引

MyISAM索引實現

MyISAM中,主鍵索引和非主鍵索引(Secondary key,也有人叫作輔助索引)在結構上沒有任何區別,只是主鍵索引要求key是惟一的,而輔助索引的key能夠重複。這裏再也不多加敘述。

InnoDB索引實現

InnoDB的非主鍵索引data域存儲相應記錄主鍵的值。換句話說,InnoDB的全部非主鍵索引都引用主鍵的值做爲data域。以下圖所示:

InnoDB非主鍵索引原理圖

由上圖可知:使用非主鍵索引搜索時須要檢索兩遍索引,首先檢索非主鍵索引得到主鍵(primary key),而後用主鍵到主鍵索引樹中檢索得到完整記錄。

那麼爲何非主鍵索引結構葉子節點存儲的是主鍵值,而不像主鍵索引那樣直接存儲完整的一行數據,這樣就能避免回表二次檢索?顯然,這樣作一方面節省了大量的存儲空間,另外一方面多份冗餘數據,更新數據的效率確定低下,另外保證數據的一致性是個麻煩事。

到了這裏,也很容易明白爲何不建議使用過長的字段做爲主鍵,由於全部的非主鍵索引都引用主鍵值,過長的主鍵值會讓非主鍵索引變得過大。

聯合索引

官方文檔:https://dev.mysql.com/doc/refman/5.7/en/multiple-column-indexes.html

好比INDEX idx_book_id_hero_name (book_id, hero_name) USING BTREE,即對book_id, hero_name兩列創建了一個聯合索引。

A multiple-column index can be considered a sorted array, the rows of which contain values that are created by concatenating the values of the indexed columns.

聯合索引是多列按照次序一列一列比較大小,拿idx_book_id_hero_name這個聯合索引來講,先比較book_id,book_id小的排在左邊,book_id大的排在右邊,book_id相同時再比較hero_name。以下圖所示:

InnoDB聯合索引原理圖

瞭解了聯合索引的結構,就能引入最左前綴法則

If the table has a multiple-column index, any leftmost prefix of the index can be used by the optimizer to look up rows. For example, if you have a three-column index on (col1, col2, col3), you have indexed search capabilities on (col1), (col1, col2), and (col1, col2, col3).

就是說聯合索引中的多列是按照列的次序排列的,若是查詢的時候不能知足列的次序,好比說where條件中缺乏col1 = ?,直接就是col2 = ? and col3 = ?,那麼就走不了聯合索引,從上面聯合索引的結構圖應該能明顯看出,只有col2列沒法經過索引樹檢索符合條件的數據。

根據最左前綴法則,咱們知道對INDEX idx_book_id_hero_name (book_id, hero_name)來講,where book_id = ? and hero_name = ?的查詢來講,確定能夠走索引,可是若是是where hero_name = ? and book_id = ?呢,表面上看起來不符合最左前綴法則啊,但MySQL優化器會根據已有的索引,調整查詢條件中這兩列的順序,讓它符合最左前綴法則,走索引,這裏也就回答了上篇《一文學會MySQL的explain工具》中爲何用show warnings命令查看時,where中的兩個過濾條件hero_namebook_id前後順序被調換了。

至於對聯合索引中的列進行範圍查詢等各類狀況,均可以先想聯合索引的結構是如何建立出來的,而後看過濾條件是否知足最左前綴法則。好比說範圍查詢時,範圍列能夠用到索引(必須是最左前綴),可是範圍列後面的列沒法用到索引。同時,索引最多用於一個範圍列,所以若是查詢條件中有兩個範圍列則沒法全用到索引。

優化建議

主鍵的選擇

在使用InnoDB存儲引擎時,若是沒有特別的須要,儘可能使用一個與業務無關的遞增字段做爲主鍵,主鍵字段不宜過長。緣由上面在講索引結構時已提過。好比說經常使用雪花算法生成64bit大小的整數(佔8個字節,用BIGINT類型)做爲主鍵就是一個不錯的選擇。

索引的選擇

(1) 表記錄比較少的時候,好比說只有幾百條記錄的表,對一些列創建索引的意義可能並不大,因此表記錄不大時酌情考慮索引。可是業務上具備惟一特性的字段,即便是多個字段的組合,也建議使用惟一索引(UNIQUE KEY)。

(2) 當索引的選擇性很是低時,索引的意義可能也不大。所謂索引的選擇性(Selectivity),是指不重複的索引值(也叫基數Cardinality)與表記錄數的比值,即count(distinct 列名)/count(*),常見的場景就是有一列status標識數據行的狀態,可能status非0即1,總數據100萬行有50萬行status爲0,50萬行status爲1,那麼是否有必要對這一列單獨創建索引呢?

An index is best used when you need to select a small number of rows in comparison to the total rows.

這句話我摘自stackoverflow上《MySQL: low selectivity columns = how to index?》下面一我的的回答。(詳情見:https://stackoverflow.com/questions/2386852/mysql-low-cardinality-selectivity-columns-how-to-index)

對於上面說的status非0即1,並且這兩種狀況分佈比較均勻的狀況,索引可能並無實際意義,實際查詢時,MySQL優化器在計算全表掃描和索引樹掃描代價後,可能會放棄走索引,由於先從status索引樹中遍歷出來主鍵值,再去主鍵索引樹中查最終數據,代價可能比全表掃描還高。

可是若是對於status爲1的數據只有1萬行,其餘99萬行數據status爲0的狀況呢,你怎麼看?歡迎有興趣的朋友在文章下面留言討論!

補充: 關於MySQL如何選擇走不走索引或者選擇走哪一個最佳索引,可使用MySQL自帶的trace工具一探究竟。具體使用見下面的官方文檔。
https://dev.mysql.com/doc/internals/en/optimizer-tracing.html
https://dev.mysql.com/doc/refman/5.7/en/information-schema-optimizer-trace-table.html

使用方法:

mysql> set session optimizer_trace="enabled=on",end_markers_in_json=on;
mysql> select * from tb_hero where hero_id = 1;
mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;

注意:開啓trace工具會影響MySQL性能,因此只能臨時分析sql使用,用完以後應當當即關閉

mysql> set session optimizer_trace="enabled=off";

(3) 在varchar類型字段上創建索引時,建議指定索引長度,有些時候可能不必對全字段創建索引,根據實際文本區分度決定索引長度便可【說明:索引的長度與區分度是一對矛盾體,通常對字符串類型數據,長度爲20的索引,區分度會高達90%以上,可使用count(distinct left(列名, 索引長度))/count(*)來肯定區分度】。

這種指定索引長度的索引叫作前綴索引(詳情見https://dev.mysql.com/doc/refman/5.7/en/column-indexes.html#column-indexes-prefix)。

With col_name(N) syntax in an index specification for a string column, you can create an index that uses only the first N characters of the column. Indexing only a prefix of column values in this way can make the index file much smaller. When you index a BLOB or TEXT column, you must specify a prefix length for the index.

前綴索引語法以下:

mysql> alter table tb_hero add index idx_hero_name_skill2 (hero_name, skill(2));

前綴索引兼顧索引大小和查詢速度,可是其缺點是不能用於group byorder by操做,也不能用於covering index(即當索引自己包含查詢所需所有數據時,再也不訪問數據文件自己)。

(4) 當查詢語句的where條件或group byorder by含多列時,可根據實際狀況優先考慮聯合索引(multiple-column index),這樣能夠減小單列索引(single-column index)的個數,有助於高效查詢。

If you specify the columns in the right order in the index definition, a single composite index can speed up several kinds of queries on the same table.

創建聯合索引時要特別注意column的次序,應結合上面提到的最左前綴法則以及實際的過濾、分組、排序需求。區分度最高的建議放最左邊

說明:

  • order by的字段能夠做爲聯合索引的一部分,而且放在最後,避免出現file_sort的狀況,影響查詢性能。正例:where a=? and b=? order by c會走索引idx_a_b_c,可是WHERE a>10 order by b卻沒法徹底使用上索引idx_a_b,只會使用上聯合索引的第一列a

  • 存在非等號和等號混合時,在建聯合索引時,應該把等號條件的列前置。如:where c>? and d=?那麼即便c的區分度更高,也應該把d放在索引的最前列,即索引idx_d_c

  • 若是where a=? and b=?,若是a列的幾乎接近於惟一值,那麼只須要創建單列索引idx_a便可

order by與group by

儘可能在索引列上完成分組、排序,遵循索引最左前綴法則,若是order by的條件不在索引列上,就會產生Using filesort,下降查詢性能。

分頁查詢

MySQL分頁查詢大多數寫法可能以下:

mysql> select * from tb_hero limit offset,N;

MySQL並非跳過offset行,而是取offset+N行,而後返回放棄前offset行,返回N行,那當offset特別大的時候,效率就很是的低下。

能夠對超過特定閾值的頁數進行SQL改寫以下:

先快速定位須要獲取的id段,而後再關聯

mysql> select a.* from tb_hero a, (select hero_id from tb_hero where 條件 limit 100000,20 ) b where a.hero_id = b.hero_id;

或者這種寫法

mysql> select a.* from tb_hero a inner join (select hero_id from tb_hero where 條件 limit 100000,20) b on a.hero_id = b.hero_id;

多表join

(1) 須要join的字段,數據類型必須絕對一致;
(2) 多表join時,保證被關聯的字段有索引

覆蓋索引

利用覆蓋索引(covering index)來進行查詢操做,避免回表,從而增長磁盤I/O。換句話說就是,儘量避免select *語句,只選擇必要的列,去除無用的列。

An index that includes all the columns retrieved by a query. Instead of using the index values as pointers to find the full table rows, the query returns values from the index structure, saving disk I/O. InnoDB can apply this optimization technique to more indexes than MyISAM can, because InnoDB secondary indexes also include the primary key columns. InnoDB cannot apply this technique for queries against tables modified by a transaction, until that transaction ends.

Any column index or composite index could act as a covering index, given the right query. Design your indexes and queries to take advantage of this optimization technique wherever possible.

當索引自己包含查詢所需所有列時,無需回表查詢完整的行記錄。對於InnoDB來講,非主鍵索引中包含了全部的索引列以及主鍵值,查詢的時候儘可能用這種特性避免回表操做,數據量很大時,查詢性能提高很明顯。

in和exsits

原則:小表驅動大表,即小的數據集驅動大的數據集

(1) 當A表的數據集大於B表的數據集時,in優於exists

mysql> select * from A where id in (select id from B)

(2) 當A表的數據集小於B表的數據集時,exists優於in

mysql> select * from A where exists (select 1 from B where B.id = A.id)

like

索引文件具備B+Tree最左前綴匹配特性,若是左邊的值未肯定,那麼沒法使用索引,因此應儘可能避免左模糊(即%xxx)或者全模糊(即%xxx%)。

mysql> select * from tb_hero where hero_name like '%無%';
+---------+-----------+--------------+---------+
| hero_id | hero_name | skill        | book_id |
+---------+-----------+--------------+---------+
|       3 | 張無忌    | 九陽神功     |       3 |
|       5 | 花完好    | 移花接玉     |       5 |
+---------+-----------+--------------+---------+
2 rows in set (0.00 sec)

mysql> explain select * from tb_hero where hero_name like '%無%';
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table   | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_hero | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    6 |    16.67 | Using where |
+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

能夠看出全模糊查詢時全表掃了,這個時候使用覆蓋索引的特性,只選擇索引字段能夠有所優化。以下:

mysql> explain select book_id, hero_name from tb_hero where hero_name like '%無%';
+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+
| id | select_type | table   | partitions | type  | possible_keys | key                   | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | tb_hero | NULL       | index | NULL          | idx_book_id_hero_name | 136     | NULL |    6 |    16.67 | Using where; Using index |
+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)

count(*)

阿里巴巴Java開發手冊中有這樣的規約:

不要使用count(列名)count(常量)來替代count(*)count(*)是SQL92定義的標準統計行數的語法,跟數據庫無關,跟NULL非NULL無關【說明:count(*)會統計值爲NULL的行,而count(列名)不會統計此列爲NULL值的行】。
count(distinct col)計算該列除NULL以外的不重複行數,注意count(distinct col1, col2)若是其中一列全爲NULL,那麼即便另外一列有不一樣的值,也返回爲0

截取一段官方文檔對count的描述(具體見:https://dev.mysql.com/doc/refman/5.7/en/aggregate-functions.html#function_count)

COUNT(expr): Returns a count of the number of non-NULL values of expr in the rows.The result is a BIGINT value.If there are no matching rows, COUNT(expr) returns 0.

COUNT(*) is somewhat different in that it returns a count of the number of rows, whether or not they contain NULL values.

Prior to MySQL 5.7.18, InnoDB processes SELECT COUNT(*) statements by scanning the clustered index. As of MySQL 5.7.18, InnoDB processes SELECT COUNT(*) statements by traversing the smallest available secondary index unless an index or optimizer hint directs the optimizer to use a different index. If a secondary index is not present, the clustered index is scanned.

可見5.7.18以前,MySQL處理count(*)會掃描主鍵索引,5.7.18以後從非主鍵索引中選擇較小的合適的索引掃描。能夠用explain看下執行計劃。

mysql> select version();
+-----------+
| version() |
+-----------+
| 5.7.18    |
+-----------+
1 row in set (0.00 sec)

mysql> explain select count(*) from tb_hero;
+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+
| id | select_type | table   | partitions | type  | possible_keys | key       | key_len | ref  | rows | filtered | Extra       |
+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_hero | NULL       | index | NULL          | idx_skill | 15      | NULL |    6 |   100.00 | Using index |
+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql> explain select count(1) from tb_hero;
+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+
| id | select_type | table   | partitions | type  | possible_keys | key       | key_len | ref  | rows | filtered | Extra       |
+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | tb_hero | NULL       | index | NULL          | idx_skill | 15      | NULL |    6 |   100.00 | Using index |
+----+-------------+---------+------------+-------+---------------+-----------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

有人糾結count(*)count(1)到底哪一種寫法更高效,從上面的執行計劃來看都同樣,若是你還不放心的話,官方文檔中也明確指明瞭InnoDBcount(*)count(1)的處理徹底一致。

InnoDB handles SELECT COUNT(*) and SELECT COUNT(1) operations in the same way. There is no performance difference.

其餘

索引列上作任何操做(表達式函數計算類型轉換等)時沒法使用索引會致使全表掃描

實戰

前幾周測試同事對公司的某產品進行壓測,某單表寫入了近2億條數據,過程當中發現配的報表有幾個數據查詢時間太長,因此重點看了幾個慢查詢SQL。避免敏感信息,這裏對其提取簡化作個記錄。

mysql> select count(*) from tb_alert;
+-----------+
| count(*)  |
+-----------+
| 198101877 |
+-----------+

表join慢

表join後,取前10條數據就花了15秒,看了下SQL執行計劃,以下:

mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 10;
10 rows in set (15.46 sec)

mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 10;
+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+
| id | select_type | table              | partitions | type | possible_keys | key  | key_len | ref  | rows      | filtered | Extra                                              |
+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+
|  1 | SIMPLE      | tb_alert           | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 190097118 |   100.00 | NULL                                               |
|  1 | SIMPLE      | tb_situation_alert | NULL       | ALL  | NULL          | NULL | NULL    | NULL |   8026988 |   100.00 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+
2 rows in set, 1 warning (0.00 sec)

能夠看出join的時候沒有用上索引,tb_situation_alert表上聯合主鍵是這樣的PRIMARY KEY (situation_id, alert_id),參與表join字段是alert_id,原來是不符合聯合索引的最左前綴法則,僅從這條sql看,解決方案有兩種,一種是對tb_situation_alert表上的alert_id單獨創建索引,另一種是調換聯合主鍵的列的次序,改成PRIMARY KEY (alert_id, situation_id)。固然不能由於多配一張報表,就改其餘產線的表的主鍵索引,這並不合理。在這裏,應該對alert_id列單獨創建索引。

mysql> create index idx_alert_id on tb_situation_alert (alert_id);

mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 100;
100 rows in set (0.01 sec)

mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_id limit 100;
+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+
| id | select_type | table              | partitions | type | possible_keys | key          | key_len | ref                             | rows      | filtered | Extra |
+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+
|  1 | SIMPLE      | tb_alert           | NULL       | ALL  | NULL          | NULL         | NULL    | NULL                            | 190097118 |   100.00 | NULL  |
|  1 | SIMPLE      | tb_situation_alert | NULL       | ref  | idx_alert_id  | idx_alert_id | 8       | tb_alert.alert_id |         2 |   100.00 | NULL  |
+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

優化後,執行計劃能夠看出join的時候走了索引,查詢前100條0.01秒,和以前的取前10條數據就花了15秒天壤之別。

分頁查詢慢

從第10000000條數據日後翻頁時,25秒才能出結果,這裏就能使用上面的分頁查詢優化技巧了。上面講優化建議時,沒看執行計劃,這裏正好看一下。

mysql> select * from tb_alert limit 10000000, 10;
10 rows in set (25.23 sec)

mysql> explain select * from tb_alert limit 10000000, 10;
+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+
| id | select_type | table    | partitions | type | possible_keys | key  | key_len | ref  | rows      | filtered | Extra |
+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+
|  1 | SIMPLE      | tb_alert | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 190097118 |   100.00 | NULL  |
+----+-------------+----------+------------+------+---------------+------+---------+------+-----------+----------+-------+
1 row in set, 1 warning (0.00 sec)

再看下使用上分頁查詢優化技巧的sql的執行計劃

mysql> select * from tb_alert a inner join (select alert_id from tb_alert limit 10000000, 10) b on a.alert_id = b.alert_id;
10 rows in set (2.29 sec)

mysql> explain select * from tb_alert a inner join (select alert_id from tb_alert a2 limit 10000000, 10) b on a.alert_id = b.alert_id;
+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+
| id | select_type | table      | partitions | type   | possible_keys | key           | key_len | ref       | rows      | filtered | Extra       |
+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+
|  1 | PRIMARY     | <derived2> | NULL       | ALL    | NULL          | NULL          | NULL    | NULL      |  10000010 |   100.00 | NULL        |
|  1 | PRIMARY     | a          | NULL       | eq_ref | PRIMARY       | PRIMARY       | 8       | b.alert_id |         1 |   100.00 | NULL        |
|  2 | DERIVED     | a2         | NULL       | index  | NULL          | idx_processed | 5       | NULL      | 190097118 |   100.00 | Using index |
+----+-------------+------------+------------+--------+---------------+---------------+---------+-----------+-----------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)

分組聚合慢

分析SQL後,發現根本上並不是分組聚合慢,而是掃描聯合索引後,回表致使性能低下,去除沒必要要的字段,使用覆蓋索引。

這裏避免敏感信息,只演示分組聚合前的簡化SQL,主要問題也是在這。
表上有聯合索引KEY idx_alert_start_host_template_id ( alert_start, alert_host, template_id),優化前的sql爲

mysql> select alert_start, alert_host, template_id, alert_service from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000;
10000 rows in set (1 min 5.22 sec)

使用覆蓋索引,去掉template_id列,就能避免回表,查詢時間從1min多變爲0.03秒,以下:

mysql> select alert_start, alert_host, template_id from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000;
10000 rows in set (0.03 sec)

mysql> explain select alert_start, alert_host, template_id from tb_alert where alert_start > {ts '2019-06-05 00:00:10.0'} limit 10000;
+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+
| id | select_type | table    | partitions | type  | possible_keys                      | key                                | key_len | ref  | rows     | filtered | Extra                    |
+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+
|  1 | SIMPLE      | tb_alert | NULL       | range | idx_alert_start_host_template_id   | idx_alert_start_host_template_id   | 9       | NULL | 95048559 |   100.00 | Using where; Using index |
+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+
1 row in set, 1 warning (0.01 sec)

總結

任何不考慮應用場景的設計都不是最好的設計,就好比說表結構的設計、索引的建立,都應該權衡數據量大小、查詢需求、數據更新頻率等。
另外正如《阿里巴巴java開發手冊》中提到的索引規約(詳情見:《Java開發手冊》之"異常處理、MySQL 數據庫"): 建立索引時避免有以下極端誤解:

1)寧濫勿缺。認爲一個查詢就須要建一個索引 2)寧缺勿濫。認爲索引會消耗空間、嚴重拖慢記錄的更新以及行的新增速度

相關文章
相關標籤/搜索