高效使用索引的首要條件是知道什麼樣的查詢會使用到索引,這個問題和B+Tree中的「最左前綴原理」有關,下面經過例子說明最左前綴原理。mysql
這裏先說一下聯合索引的概念。MySQL中的索引能夠以必定順序引用多個列,這種索引叫作複合(聯合)索引,通常的,一個聯合索引是一個有序元組<a1, a2, …, an>,其中各個元素均爲數據表的一列,實際上要嚴格定義索引須要用到關係代數,可是這裏我不想討論太多關係代數的話題,由於那樣會顯得很枯燥,因此這裏就再也不作嚴格定義。另外,單列索引能夠當作聯合索引元素數爲1的特例。sql
以employees.titles表爲例,下面先查看其上都有哪些索引:函數
SHOW INDEX FROM employees.titles; +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+ | Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Null | Index_type | +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+ | titles | 0 | PRIMARY | 1 | emp_no | A | NULL | | BTREE | | titles | 0 | PRIMARY | 2 | title | A | NULL | | BTREE | | titles | 0 | PRIMARY | 3 | from_date | A | 443308 | | BTREE | | titles | 1 | emp_no | 1 | emp_no | A | 443308 | | BTREE | +--------+------------+----------+--------------+-------------+-----------+-------------+------+------------+
從結果中能夠到titles表的主索引爲<emp_no, title, from_date>,還有一個輔助索引<emp_no>。爲了不多個索引使事情變複雜(MySQL的SQL優化器在多索引時行爲比較複雜),這裏咱們將輔助索引drop掉:性能
ALTER TABLE employees.titles DROP INDEX emp_no;
這樣就能夠專心分析索引PRIMARY的行爲了。優化
示例說明以下:spa
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26'; +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
很明顯,當按照索引中全部列進行精確匹配(這裏精確匹配指「=」或「IN」匹配)時,索引能夠被用到。這裏有一點須要注意,理論上索引對順序是敏感的,可是因爲MySQL的查詢優化器會自動調整where子句的條件順序以使用適合的索引,例如咱們將where中的條件順序顛倒:code
EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26' AND emp_no='10001' AND title='Senior Engineer'; +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+ | 1 | SIMPLE | titles | const | PRIMARY | PRIMARY | 59 | const,const,const | 1 | | +----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
效果是同樣的。blog
示例說明以下:索引
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------+
當查詢條件精確匹配索引的左邊連續一個或幾個列時,如<emp_no>或<emp_no, title>,因此能夠被用到,可是隻能用到一部分,即條件所組成的最左前綴。上面的查詢從分析結果看用到了PRIMARY索引,可是key_len爲4,說明只用到了索引的第一列前綴。ci
示例說明以下:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
此時索引使用狀況和狀況二相同,由於title未提供,因此查詢只用到了索引的第一列,然後面的from_date雖然也在索引中,可是因爲title不存在而沒法和左前綴鏈接,所以須要對結果進行掃描過濾from_date(這裏因爲emp_no惟一,因此不存在掃描)。若是想讓from_date也使用索引而不是where過濾,能夠增長一個輔助索引<emp_no, from_date>,此時上面的查詢會使用這個索引。除此以外,還可使用一種稱之爲「隔離列」的優化方法,將emp_no與from_date之間的「坑」填上。
首先咱們看下title一共有幾種不一樣的值:
SELECT DISTINCT(title) FROM employees.titles; +--------------------+ | title | +--------------------+ | Senior Engineer | | Staff | | Engineer | | Senior Staff | | Assistant Engineer | | Technique Leader | | Manager | +--------------------+
只有7種。在這種成爲「坑」的列值比較少的狀況下,能夠考慮用「IN」來填補這個「坑」從而造成最左前綴:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ('Senior Engineer', 'Staff', 'Engineer', 'Senior Staff', 'Assistant Engineer', 'Technique Leader', 'Manager') AND from_date='1986-06-26'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 7 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
此次key_len爲59,說明索引被用全了,可是從type和rows看出IN實際上執行了一個range查詢,這裏檢查了7個key。看下兩種查詢的性能比較:
SHOW PROFILES; +----------+------------+-------------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+-------------------------------------------------------------------------------+ | 10 | 0.00058000 | SELECT * FROM employees.titles WHERE emp_no='10001' AND from_date='1986-06-26'| | 11 | 0.00052500 | SELECT * FROM employees.titles WHERE emp_no='10001' AND title IN ... | +----------+------------+-------------------------------------------------------------------------------+
「填坑」後性能提高了一點。若是通過emp_no篩選後餘下不少數據,則後者性能優點會更加明顯。固然,若是title的值不少,用填坑就不合適了,必須創建輔助索引。
示例說明以下:
EXPLAIN SELECT * FROM employees.titles WHERE from_date='1986-06-26'; +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
因爲不是最左前綴,索引這樣的查詢顯然用不到索引。
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title LIKE 'Senior%'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 56 | NULL | 1 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
此時能夠用到索引,可是若是通配符不是隻出如今末尾,則沒法使用索引。(原文表述有誤,若是通配符%不出如今開頭,則能夠用到索引,但根據具體狀況不一樣可能只會用其中一個前綴)
對於範圍條件查詢,MYSQL沒法再使用範圍後面的其餘索引列了。但對多個等值條件查詢則沒有這樣的限制。
EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' and title='Senior Engineer'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
範圍列能夠用到索引(必須是最左前綴),可是範圍列後面的列沒法用到索引。同時,索引最多用於一個範圍列,所以若是查詢條件中有兩個範圍列則沒法全用到索引。
EXPLAIN SELECT * FROM employees.titles WHERE emp_no < '10010' AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 4 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
能夠看到索引對第二個範圍索引無能爲力。這裏特別要說明MySQL一個有意思的地方,那就是僅用explain可能沒法區分範圍索引和多值匹配,由於在type中這二者都顯示爲range。同時,用了「between」並不意味着就是範圍查詢,例以下面的查詢:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no BETWEEN '10001' AND '10010' AND title='Senior Engineer' AND from_date BETWEEN '1986-01-01' AND '1986-12-31'; +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+ | 1 | SIMPLE | titles | range | PRIMARY | PRIMARY | 59 | NULL | 16 | Using where | +----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
看起來是用了兩個範圍查詢,但做用於emp_no上的「BETWEEN」實際上至關於「IN」,也就是說emp_no實際是多值精確匹配。能夠看到這個查詢用到了索引所有三個列。所以在MySQL中要謹慎地區分多值匹配和範圍匹配,不然會對MySQL的行爲產生困惑。
很不幸,若是查詢條件中含有函數或表達式,則MySQL不會爲這列使用索引(雖然某些在數學意義上可使用)。例如:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND left(title, 6)='Senior'; +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+ | 1 | SIMPLE | titles | ref | PRIMARY | PRIMARY | 4 | const | 1 | Using where | +----+-------------+--------+------+---------------+---------+---------+-------+------+-------------+
雖然這個查詢和狀況五中功能相同,可是因爲使用了函數left,則沒法爲title列應用索引,而狀況五中用LIKE則能夠。再如:
EXPLAIN SELECT * FROM employees.titles WHERE emp_no - 1='10000'; +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | titles | ALL | NULL | NULL | NULL | NULL | 443308 | Using where | +----+-------------+--------+------+---------------+------+---------+------+--------+-------------+
顯然這個查詢等價於查詢emp_no爲10001的函數,可是因爲查詢條件是一個表達式,MySQL沒法爲其使用索引。看來MySQL尚未智能到自動優化常量表達式的程度,所以在寫查詢語句時儘可能避免表達式出如今查詢中,而是先手工私下代數運算,轉換爲無表達式的查詢語句。
既然索引能夠加快查詢速度,那麼是否是隻要是查詢語句須要,就建上索引?答案是否認的。由於索引雖然加快了查詢速度,但索引也是有代價的:索引文件自己要消耗存儲空間,同時索引會加劇插入、刪除和修改記錄時的負擔,另外,MySQL在運行時也要消耗資源維護索引,所以索引並非越多越好。通常兩種狀況下不建議建索引。
第一種狀況是表記錄比較少,例如一兩千條甚至只有幾百條記錄的表,不必建索引,讓查詢作全表掃描就行了。至於多少條記錄纔算多,這個我的有我的的見解,我我的的經驗是以2000做爲分界線,記錄數不超過 2000能夠考慮不建索引,超過2000條能夠酌情考慮索引。
另外一種不建議建索引的狀況是索引的選擇性較低。所謂索引的選擇性(Selectivity),是指不重複的索引值(也叫基數,Cardinality)與表記錄數(#T)的比值:
Index Selectivity = Cardinality / #T
顯然選擇性的取值範圍爲(0, 1],選擇性越高的索引價值越大,這是由B+Tree的性質決定的。例如,上文用到的employees.titles表,若是title字段常常被單獨查詢,是否須要建索引,咱們看一下它的選擇性:
SELECT count(DISTINCT(title))/count(*) AS Selectivity FROM employees.titles; +-------------+ | Selectivity | +-------------+ | 0.0000 | +-------------+
title的選擇性不足0.0001(精確值爲0.00001579),因此實在沒有什麼必要爲其單獨建索引。
有一種與索引選擇性有關的索引優化策略叫作前綴索引,就是用列的前綴代替整個列做爲索引key,當前綴長度合適時,能夠作到既使得前綴索引的選擇性接近全列索引,同時由於索引key變短而減小了索引文件的大小和維護開銷。下面以employees.employees表爲例介紹前綴索引的選擇和使用。
從圖12能夠看到employees表只有一個索引<emp_no>,那麼若是咱們想按名字搜索一我的,就只能全表掃描了:
EXPLAIN SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido'; +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+ | 1 | SIMPLE | employees | ALL | NULL | NULL | NULL | NULL | 300024 | Using where | +----+-------------+-----------+------+---------------+------+---------+------+--------+-------------+
若是頻繁按名字搜索員工,這樣顯然效率很低,所以咱們能夠考慮建索引。有兩種選擇,建<first_name>或<first_name, last_name>,看下兩個索引的選擇性:
SELECT count(DISTINCT(first_name))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.0042 | +-------------+ SELECT count(DISTINCT(concat(first_name, last_name)))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.9313 | +-------------+
<first_name>顯然選擇性過低,<first_name, last_name>選擇性很好,可是first_name和last_name加起來長度爲30,有沒有兼顧長度和選擇性的辦法?能夠考慮用first_name和last_name的前幾個字符創建索引,例如<first_name, left(last_name, 3)>,看看其選擇性:
SELECT count(DISTINCT(concat(first_name, left(last_name, 3))))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.7879 | +-------------+
選擇性還不錯,但離0.9313仍是有點距離,那麼把last_name前綴加到4:
SELECT count(DISTINCT(concat(first_name, left(last_name, 4))))/count(*) AS Selectivity FROM employees.employees; +-------------+ | Selectivity | +-------------+ | 0.9007 | +-------------+
這時選擇性已經很理想了,而這個索引的長度只有18,比<first_name, last_name>短了接近一半,咱們把這個前綴索引 建上:
ALTER TABLE employees.employees ADD INDEX `first_name_last_name4` (first_name, last_name(4));
此時再執行一遍按名字查詢,比較分析一下與建索引前的結果:
SHOW PROFILES; +----------+------------+---------------------------------------------------------------------------------+ | Query_ID | Duration | Query | +----------+------------+---------------------------------------------------------------------------------+ | 87 | 0.11941700 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' | | 90 | 0.00092400 | SELECT * FROM employees.employees WHERE first_name='Eric' AND last_name='Anido' | +----------+------------+---------------------------------------------------------------------------------+
性能的提高是顯著的,查詢速度提升了120多倍。
前綴索引兼顧索引大小和查詢速度,可是其缺點是不能用於ORDER BY和GROUP BY操做,也不能用於Covering index(即當索引自己包含查詢所需所有數據時,再也不訪問數據文件自己)。
----------------------------------------------------------------------------------------------
補充該節中的"範圍查詢"說明:
Mysql對於範圍查詢range分的優化爲單字段優化和多元素優化:
單元素索引範圍條件的定義以下:
· 對於BTREE和HASH索引,當使用=、<=>、
IN
、
IS
NULL
或者
IS
NOT
NULL
操做符時,關鍵元素與常量值的比較關係對應一個範圍條件,即const範圍。
· 對於BTREE索引,當使用>、<、>=、<=、
BETWEEN
、!=或者<>,或者
LIKE
'pattern'
(其中
'pattern'
不以通配符開始)操做符時,關鍵元素與常量值的比較關係對應一個範圍條件。
· 對於全部類型的索引,多個範圍條件結合
OR
或
AND
則產生一個範圍條件。
前面描述的「常量值」係指:
· 查詢字符串中的常量
· 同一聯接中的const或system表中的列
· 無關聯子查詢的結果
· 徹底從前面類型的子表達式組成的表達式
|
多元素索引的範圍條件:
1.
----------------
對於BTREE索引,區間能夠對結合
AND
的條件有用,其中每一個條件用一個常量值經過=、<=>、
IS
NULL
、>、<、>=、<=、!=、<>、
BETWEEN
或者
LIKE
'pattern'
(其中
'pattern'
不以通配符開頭)比較一個關鍵元素。區間能夠足夠長以肯定一個包含全部匹配條件(或若是使用<>或!=,爲兩個區間)的記錄的單一的關鍵元組。例如,對於條件:
key_part1 =
'foo'
AND
key_part2 >= 10
AND
key_part3 > 10
2.
-----------
對於HASH索引,可使用包含相同值的每一個區間。
key_part1 cmp const1
AND
key_part2 cmp const2
AND
...
AND
key_partN cmp constN;
這裏,const1,const2,...爲常量,cmp是=、<=>或者
IS
NULL
比較操做符之一,條件包括全部索引部分。(也就是說,有N 個條件,每個對應N-元素索引的每一個部分)。
3.
----------
若是包含區間內的一系列記錄的條件結合使用
OR
,則造成包括一系列包含在區間並集的記錄的一個條件。若是條件結合使用了
AND
,則造成包括一系列包含在區間交集內的記錄的一個條件。例如,對於兩部分索引的條件:
(key_part1 = 1
AND
key_part2 < 2)
OR
(key_part1 > 5)
區間爲:
(1, -inf) < (key_part1, key_part2) < (1, 2)
(5, -inf) < (key_part1, key_part2)
|
mysql> set profiling=1; 2 Query OK, 0 rows affected (0.00 sec) eg: mysql> select * from test_1; mysql> show profiles; +----------+------------+----------------------+ | Query_ID | Duration | Query | +----------+------------+----------------------+ | 1 | 0.84718100 | select * from test_1 | +----------+------------+----------------------+ 1 row in set (0.00 sec)