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

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

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

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.

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

什么是索引#

在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)作為表的底層存儲引擎。

Copy

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, storedinmemory, usefulfortemporary 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在查詢的時候比較次數其實不比二叉查找樹少,但在內存中的大小比較、二分查找的耗時相比磁盤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的高度,提高效率。

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

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

由于磁盤順序讀取的效率很高(不需要尋道時間,只需很少的旋轉時間),因此對于具有局部性的程序來說,預讀可以提高I/O效率。預讀的長度一般為頁(page)的整數倍。

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

Copy

$ getconf PAGE_SIZE

4096

數據庫系統的設計者巧妙利用了磁盤預讀原理,將一個節點的大小設為操作系統的頁大小的整數倍,這樣每個節點只需要一次I/O就可以完全載入。

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

Copy

mysql> show variables like'innodb_page_size';+------------------+-------+| Variable_name? ? | Value |+------------------+-------+| innodb_page_size | 16384 |+------------------+-------+1 rowinset(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中,索引屬于存儲引擎級別的概念。不同存儲引擎對索引的實現方式是不同的,這里主要看下MyISAM和InnoDB兩種存儲引擎的索引實現方式。

MyISAM索引實現#

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

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

InnoDB索引實現#

InnoDB的主鍵索引也使用B+Tree作為索引結構時的實現方式卻與MyISAM截然不同。InnoDB的數據文件本身就是索引文件。在InnoDB中,表數據文件本身就是按B+Tree組織的一個索引結構,這棵樹的葉子節點data域保存了完整的數據記錄,這個索引的key是數據表的主鍵,因此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域。如下圖所示:

由上圖可知:使用非主鍵索引搜索時需要檢索兩遍索引,首先檢索非主鍵索引獲得主鍵(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。如下圖所示:

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

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_name、book_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

使用方法:

Copy

mysql>setsession optimizer_trace="enabled=on",end_markers_in_json=on;mysql> select * from tb_herowherehero_id = 1;mysql> SELECT * FROM information_schema.OPTIMIZER_TRACE;

注意:開啟trace工具會影響MySQL性能,所以只能臨時分析sql使用,用完之后應當立即關閉

Copy

mysql>setsession 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.

前綴索引語法如下:

Copy

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

前綴索引兼顧索引大小和查詢速度,但是其缺點是不能用于group by和order by操作,也不能用于covering index(即當索引本身包含查詢所需全部數據時,不再訪問數據文件本身)。

(4) 當查詢語句的where條件或group by、order 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分頁查詢大多數寫法可能如下:

Copy

mysql> select * from tb_herolimitoffset,N;

MySQL并不是跳過offset行,而是取offset+N行,然后返回放棄前offset行,返回N行,那當offset特別大的時候,效率就非常的低下。

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

先快速定位需要獲取的id段,然后再關聯

Copy

mysql> select a.* from tb_hero a, (select hero_id from tb_herowhere條件limit100000,20 ) bwherea.hero_id = b.hero_id;

或者這種寫法

Copy

mysql> select a.* from tb_hero a inner join (select hero_id from tb_herowhere條件limit100000,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

Copy

mysql> select * from Awhereidin(select id from B)

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

Copy

mysql> select * from Awhereexists (select 1 from BwhereB.id = A.id)

like#

索引文件具有B+Tree最左前綴匹配特性,如果左邊的值未確定,那么無法使用索引,所以應盡量避免左模糊(即%xxx)或者全模糊(即%xxx%)。

Copy

mysql> select * from tb_herowherehero_name like'%無%';+---------+-----------+--------------+---------+| hero_id | hero_name | skill? ? ? ? | book_id |+---------+-----------+--------------+---------+|? ? ? 3 | 張無忌? ? | 九陽神功? ? |? ? ? 3 ||? ? ? 5 | 花無缺? ? | 移花接玉? ? |? ? ? 5 |+---------+-----------+--------------+---------+2 rowsinset(0.00 sec)mysql> explain select * from tb_herowherehero_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 | Usingwhere|+----+-------------+---------+------------+------+---------------+------+---------+------+------+----------+-------------+1 rowinset, 1 warning (0.00 sec)

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

Copy

mysql> explain select book_id, hero_name from tb_herowherehero_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 | Usingwhere; Using index |+----+-------------+---------+------------+-------+---------------+-----------------------+---------+------+------+----------+--------------------------+1 rowinset, 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看下執行計劃。

Copy

mysql> select version();+-----------+| version() |+-----------+| 5.7.18? ? |+-----------+1 rowinset(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 rowinset, 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 rowinset, 1 warning (0.00 sec)

有人糾結count(*)、count(1)到底哪種寫法更高效,從上面的執行計劃來看都一樣,如果你還不放心的話,官方文檔中也明確指明了InnoDB對count(*)、count(1)的處理完全一致。

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

其他#

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

實戰#

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

Copy

mysql> select count(*) from tb_alert;

+-----------+

| count(*)? |

+-----------+

| 198101877 |

+-----------+

表join慢#

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

Copy

mysql> select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_idlimit10;10 rowsinset(15.46 sec)mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_idlimit10;+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+| 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 | Usingwhere; Using join buffer (Block Nested Loop) |+----+-------------+--------------------+------------+------+---------------+------+---------+------+-----------+----------+----------------------------------------------------+2 rowsinset, 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列單獨建立索引。

Copy

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_idlimit100;100 rowsinset(0.01 sec)mysql> explain select * from tb_alert left join tb_situation_alert on tb_alert.alert_id = tb_situation_alert.alert_idlimit100;+----+-------------+--------------------+------------+------+---------------+--------------+---------+---------------------------------+-----------+----------+-------+| 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 rowsinset, 1 warning (0.00 sec)

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

分頁查詢慢#

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

Copy

mysql> select * from tb_alertlimit10000000, 10;10 rowsinset(25.23 sec)mysql> explain select * from tb_alertlimit10000000, 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 rowinset, 1 warning (0.00 sec)

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

Copy

mysql> select * from tb_alert a inner join (select alert_id from tb_alertlimit10000000, 10) b on a.alert_id = b.alert_id;10 rowsinset(2.29 sec)mysql> explain select * from tb_alert a inner join (select alert_id from tb_alert a2limit10000000, 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? ? | | 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 rowsinset, 1 warning (0.00 sec)

分組聚合慢#

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

這里避免敏感信息,只演示分組聚合前的簡化SQL,主要問題也是在這。

表上有聯合索引KEY idx_alert_start_host_template_id ( alert_start, alert_host, template_id),優化前的sql為

Copy

mysql> select alert_start, alert_host, template_id, alert_service from tb_alertwherealert_start > {ts'2019-06-05 00:00:10.0'}limit10000;10000 rowsinset(1 min 5.22 sec)

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

Copy

mysql> select alert_start, alert_host, template_id from tb_alertwherealert_start > {ts'2019-06-05 00:00:10.0'}limit10000;10000 rowsinset(0.03 sec)mysql> explain select alert_start, alert_host, template_id from tb_alertwherealert_start > {ts'2019-06-05 00:00:10.0'}limit10000;+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+| 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 | Usingwhere; Using index |+----+-------------+----------+------------+-------+------------------------------------+------------------------------------+---------+------+----------+----------+--------------------------+1 rowinset, 1 warning (0.01 sec)

總結#

任何不考慮應用場景的設計都不是最好的設計,就比如說表結構的設計、索引的創建,都應該權衡數據量大小、查詢需求、數據更新頻率等。

1)寧濫勿缺。認為一個查詢就需要建一個索引

2)寧缺勿濫。認為索引會消耗空間、嚴重拖慢記錄的更新以及行的新增速度

最后,小編想說:我是一名python開發工程師, 整理了一套最新的python系統學習教程, 想要這些資料的可以關注私信小編“01”即可(免費分享哦)希望能 對你有所幫助.

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371