MySQL索引和查詢優化
對于任何DBMS,索引都是進行優化的最主要的因素。對于少量的資料,沒有合适的索引影響不是很大,但是,當随着資料量的增加,性能會急劇下降。
如果對多列進行索引(組合索引),列的順序非常重要,MySQL僅能對索引最左邊的字首進行有效的查找。
例如:
假 設存在組合索引it1c1c2(c1,c2),查詢語句select * from t1 where c1=1 and c2=2能夠使用該索引。查詢語句select * from t1 where c1=1也能夠使用該索引。但是,查詢語句select * from t1 where c2=2不能夠使用該索引,因為沒有組合索引的引導列,即,要想使用c2列進行查找,必需出現c1等于某值。
索引是快速搜尋的關鍵。MySQL索引的建立對于MySQL的高效運作是很重要的。
下面介紹幾種常見的MySQL索引類型:
在資料庫表中,對字段建立索引可以大大提高查詢速度。假如我們建立了一個 mytable表:
- CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL );
我們随機向裡面插入了10000條記錄,其中有一條:5555, admin。
在查找username="admin"的記錄
- SELECT * FROM mytable WHERE username='admin';
時,如果在username上已經建立了索引,MySQL無須任何掃描,即準确可找到該記錄。相反,MySQL會掃描所有記錄,即要查詢10000條記錄。
索引分單列索引群組合索引。單列索引,即一個索引隻包含單個列,一個表可以有多個單列索引,但這不是組合索引。組合索引,即一個索包含多個列。
MySQL索引類型包括:
(1)普通索引
這是最基本的索引,它沒有任何限制。它有以下幾種建立方式:
建立索引
- CREATE INDEX indexName ON mytable(username(length));
如果是CHAR,VARCHAR類型,length可以小于字段實際長度;如果是BLOB和TEXT類型,必須指定 length,下同。
修改表結構
- ALTER mytable ADD INDEX [indexName] ON (username(length))
建立表的時候直接指定
-
CREATE TABLE mytable( ID INT NOT NULL,
username VARCHAR(16) NOT NULL, INDEX [indexName] (username(length)) );
删除索引的文法:
- DROP INDEX [indexName] ON mytable;
(2)MySQL索引類型:唯一索引
它與前面的普通索引類似,不同的就是:索引列的值必須唯一,但允許有空值。如果是組合索引,則列值的組合必須唯一。它有以下幾種建立方式:
建立索引
- CREATE UNIQUE INDEX indexName ON mytable(username(length))
修改表結構
- ALTER mytable ADD UNIQUE [indexName] ON (username(length))
建立表的時候直接指定
-
CREATE TABLE mytable( ID INT NOT NULL,
username VARCHAR(16) NOT NULL, UNIQUE [indexName] (username(length)) );
(3)MySQL索引類型:主鍵索引
它是一種特殊的唯一索引,不允許有空值。一般是在建表的時候同時建立主鍵索引:
- CREATE TABLE mytable( ID INT NOT NULL, username VARCHAR(16) NOT NULL, PRIMARY KEY(ID) );
當然也可以用 ALTER 指令。記住:一個表隻能有一個主鍵。
(4)組合索引
為了形象地對比單列索引群組合索引,為表添加多個字段:
-
CREATE TABLE mytable( ID INT NOT NULL, username
VARCHAR(16) NOT NULL, city VARCHAR(50) NOT NULL, age INT NOT NULL );
為了進一步榨取MySQL的效率,就要考慮建立組合索引。就是将 name, city, age建到一個索引裡:
- ALTER TABLE mytable ADD INDEX name_city_age (name(10),city,age);
建表時,usernname長度為 16,這裡用 10。這是因為一般情況下名字的長度不會超過10,這樣會加速索引查詢速度,還會減少索引檔案的大小,提高INSERT的更新速度。
如果分别在 usernname,city,age上建立單列索引,讓該表有3個單列索引,查詢時和上述的組合索引效率也會大不一樣,遠遠低于我們的組合索引。雖然此時有了三個索引,但MySQL隻能用到其中的那個它認為似乎是最有效率的單列索引。
建立這樣的組合索引,其實是相當于分别建立了下面三組組合索引:
- usernname , city, age usernname , city usernname
以上的相關内容就是對MySQL索引類型的部分内容的介紹,望你能有所收獲。
使用索引的注意事項
使用索引時,有以下一些技巧和注意事項:
索引不會包含有NULL值的列
隻要列中包含有NULL值都将不會被包含在MySQL索引中,複合索引中隻要有一列含有NULL值,那麼這一列對于此複合索引就是無效的。是以我們在資料庫設計時不要讓字段的預設值為NULL。
使用短索引
對串列進行索引,如果可能應該指定一個字首長度。例如,如果有一個CHAR(255)的列,如果在前10個或20個字元内,多數值是惟一的,那麼就不要對整個列進行索引。短索引不僅可以提高查詢速度而且可以節省磁盤空間和I/O操作。
索引列排序
MySQL查詢隻使用一個索引,是以如果where子句中已經使用了索引的話,那麼order by中的列是不會使用索引的。是以資料庫預設排序可以符合要求的情況下不要使用排序操作;盡量不要包含多個列的排序,如果需要最好給這些列建立複合索引。
like語句操作
一般情況下不鼓勵使用like操作,如果非使用不可,如何使用也是一個問題。like “%aaa%” 不會使用MySQL索引而like “aaa%”可以使用索引。
不要在列上進行運算
- select * from users where YEAR(adddate)<2007;
将在每個行上進行運算,這将導緻索引失效而進行全表掃描,是以我們可以改成
- select * from users where adddate<‘2007-01-01’;
不使用NOT IN和<>操作
索引優化 http://blog.codinglabs.org/articles/theory-of-mysql-index.html

最左字首原理與相關優化
高效使用索引的首要條件是知道什麼樣的查詢會使用到索引,這個問題和B+Tree中的“最左字首原理”有關,下面通過例子說明最左字首原理。
這裡先說一下聯合索引的概念。在上文中,我們都是假設索引隻引用了單個的列,實際上,MySQL中的索引可以以一定順序引用多個列,這種索引叫做聯合索引,一般的,一個聯合索引是一個有序元組<a1, a2, …, an>,其中各個元素均為資料表的一列,實際上要嚴格定義索引需要用到關系代數,但是這裡我不想讨論太多關系代數的話題,因為那樣會顯得很枯燥,是以這裡就不再做嚴格定義。另外,單列索引可以看成聯合索引元素數為1的特例。
以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的行為了。
情況一:全列比對。
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中的條件順序颠倒:
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 | |
+----+-------------+--------+-------+---------------+---------+---------+-------------------+------+-------+
效果是一樣的。
情況二:最左字首比對。
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,說明隻用到了索引的第一列字首。
情況三:查詢條件用到了索引中列的精确比對,但是中間某個條件未提供。
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 |
+----+-------------+--------+-------+---------------+---------+---------+------+------+-------------+
此時可以用到索引,但是如果通配符不是隻出現在末尾,則無法使用索引。(原文表述有誤,如果通配符%不出現在開頭,則可以用到索引,但根據具體情況不同可能隻會用其中一個字首)
情況六:範圍查詢。
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分的優化為單字段優化和多元素優化:
|
|
Mysql檢索時間查詢 (版本要求: 5.0.37或以上)

開啟profile
1 mysql> set profiling=1;
2 Query OK, 0 rows affected (0.00 sec)
eg:
1 mysql> select * from test_1;
2 mysql> show profiles;
3 +----------+------------+----------------------+
4 | Query_ID | Duration | Query |
5 +----------+------------+----------------------+
6 | 1 | 0.84718100 | select * from test_1 |
7 +----------+------------+----------------------+
8 1 row in set (0.00 sec)