上一篇已經講了索引的基本類型,這一篇主要介紹下如何選擇更高效的索引類型。
獨立的列
現在有下面一張學生成績表
CREATE TABLE `student` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`first_name` varchar(20) NOT NULL,
`last_name` varchar(20) NOT NULL,
`created_at` timestamp NOT NULL,
`updated_at` timestamp NOT NULL,
`score` int(3) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
KEY `created_at` (`created_at`)
KEY `score` (`score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
現在我們要根據學生成績查詢學生姓名,這是一個很簡單的查詢。select first_name,last_name from student where score=99;
這條sql
就使用到了索引score
。
但是我們通常會看到很多查詢不恰當的使用到索引,最後就導致mysql
沒辦法使用到索引。如果查詢中的不是獨立的,則Mysql不會使用到索引,獨立的列
是指索引列不能是表達式的一部分,也不能是函數的參數。
如select first_name,last_name from student where score+1=100;
這個查詢是不能使用到索引的。
再如:select first_name from student where TO_DAYS(NOW())-TO_DAYS(created_at)>0;
也是不能使用到索引的。
前綴索引
有時候需要索引的列是很長的字符串,如果直接創建索引會使索引變的很大這樣就變的比較慢了。改進方式現在能想到就是前面說到的哈希索引
,
但是有時候哈希索引
是不適合的。這個時候就有了前綴索引:把列開始的部分字符串作爲索引,這樣可以大大的節約索引空間,從而提高索引效率
。但這樣也會降低索引的選擇性。索引選擇性指:不重複的索引值和數據表總數的比值。索引的選擇性越高,那麼索引的查詢效率越高。唯一索引的選擇性最高爲1,性能也是最好的。
對於很長的VARCHAR
,TEXT
這樣的列,如果要作爲索引的話,那麼必須使用前綴索引。
那麼怎麼選擇合適的前綴索引呢。
訣竅在於要選擇足夠長的前綴以保證比較高的索引選擇性,同時又不能太長,因爲索引越短,索引空間越小。
如:一張訂單表,要爲聯繫人手機號做前綴索引,這個適合需要分析多少長度的前綴索引,可以查詢每個長度的
SELECT COUNT(DISTINCT LEFT(phone,3))/COUNT(*) AS pre3,
COUNT(DISTINCT LEFT(phone,4))/COUNT(*) AS pre4,
COUNT(DISTINCT LEFT(phone,5))/COUNT(*) AS pre5,
COUNT(DISTINCT LEFT(phone,6))/COUNT(*) AS pre6,
COUNT(DISTINCT LEFT(phone,7))/COUNT(*) AS pre7,
COUNT(DISTINCT LEFT(phone,8))/COUNT(*) AS pre8
FROM orders;
+--------+--------+--------+--------+--------+--------+
| pre3 | pre4 | pre5 | pre6 | pre7 | pre8 |
+--------+--------+--------+--------+--------+--------+
| 0.0026 | 0.0216 | 0.1397 | 0.3274 | 0.4533 | 0.4533 |
+--------+--------+--------+--------+--------+--------+
1 row in set (0.10 sec)
可以看到在長度爲7的時候,選擇性的提升已經很小了。這個時候,我們就可以考慮取7這個值了。當然這裏只是一個舉例,在實際場景中,手機號的長度完全可以直接作爲普通索引的。
前綴索引是比較小而且快,但是Mysql
不能用前綴索引作爲group by
和order by
,也不能做覆蓋掃描(所以查詢必須回表)。
多列索引
對於初學者,常見的錯誤就是爲每個查詢列都加一個索引,或者按照錯誤的順序創建的多列索引。網上有多說“把where 條件中的列都加上索引就好了”,這種說法是錯誤的。很多時候這樣的索引並不會提高效率。如一個訂單表,狀態有8種,如果爲訂單狀態加上索引。那麼在數據量少的情況下可能會提高效率,但是當數據量大的時候反而會影響效率。
如有下表:
CREATE TABLE `student` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`first_name` varchar(20) NOT NULL,
`last_name` varchar(20) NOT NULL,
`created_at` timestamp NOT NULL,
`score` int(3) NOT NULL DEFAULT '0',
`updated_at` timestamp NOT NULL,
PRIMARY KEY (`id`),
KEY `score` (`score`),
KEY `first_name` (`first_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULT CHARSET=utf8
現在表中有600萬數據。現在我們統計分數的出現次數SELECT COUNT(*) AS num,score FROM student GROUP BY score ORDER BY num DESC LIMIT 10;
得到結果
+-------+-------+
| num | score |
+-------+-------+
| 68607 | 13 |
| 68557 | 44 |
| 68551 | 67 |
| 68527 | 64 |
| 68490 | 35 |
| 68490 | 5 |
| 68457 | 17 |
| 68422 | 50 |
| 68415 | 95 |
| 68409 | 11 |
+-------+-------+
10 rows in set (2.35 sec)
發現分數爲13的有6萬8千個。這個時候我們查詢score
爲13,first_name
開始爲O
的,SELECT score,first_name FROM student WHERE first_name LIKE '0%' AND score=13;
可以得到結果1173 rows in set (0.76 sec)
;
分析得到
mysql> explain SELECT score,first_name FROM student WHERE first_name LIKE '0%' AND score=13 \G
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: student
partitions: NULL
type: ref
possible_keys: score,first_name
key: score
key_len: 4
ref: const
rows: 135128
filtered: 3.61
Extra: Using where
1 row in set, 1 warning (0.00 sec)
這個時候可以看到其實是只用到索引score
的,掃描了13萬行數據, 這個時候我們是單獨爲兩個字段加的索引。
但是如果我們創建一個(score,first_name)
的多列索引呢。
得到結果1173 rows in set (0.00 sec)
;可以看到多列索引的速度明顯比單獨的索引要很多.
分析
mysql> explain SELECT * FROM student WHERE first_name LIKE '0%' AND score=13 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: student
partitions: NULL
type: range
possible_keys: score_first_name
key: score_first_name
key_len: 66
ref: NULL
rows: 1173
filtered: 100.00
Extra: Using index condition
1 row in set, 1 warning (0.01 sec)
可以看到這個時候用到多列索引,掃描行數,明顯的減少了,時間也快了很多。所以說選擇合適的多列索引
選擇合適的索引順序
在創建多列索引的時候我們經常需要考慮的就是如何去選擇合適的索引順序,而不是說哪個查詢條件在前面就選擇哪個順序。而是應該根據實際情況來分析考慮,在可能是順序下還應該滿足排序分組等需求。比如說查詢SELECT score,first_name FROM student WHERE first_name LIKE '0Z%' AND score=13;
這個查詢是應該創建一個(score,first_name)的索引,還是應該將索引順序顛倒一下呢。我們可以查詢一下兩個列的分佈情況,最後根據分析結果來確認索引的順序。
mysql> SELECT SUM(score='13'),SUM(first_name LIKE '0Z%') FROM student;
+-----------------+----------------------------+
| SUM(score='13') | SUM(first_name LIKE '0Z%') |
+-----------------+----------------------------+
| 68607 | 3499 |
+-----------------+----------------------------+
1 row in set (2.59 sec)
根據前面在索引選擇性的描述,我們應該將first_name
放到前面。那我們在來看看這個情況下 score
的索引選擇性。
mysql> SELECT SUM(score='13') FROM student WHERE first_name LIKE '0Z%';
+-----------------+
| SUM(score='13') |
+-----------------+
| 39 |
+-----------------+
可以看到這把first_name
放到前面是比較符合索引選擇性的規則的。但是也不是所有的場景都是符合這種情況的。所以還是需要具體分析。綜合出比較有利的設計方案。不然反而可能造成一些不必要的麻煩。
覆蓋索引
如果一個索引的葉子節點,也就是索引中包含需要查詢行,那麼我們就稱這個索引是覆蓋索引。
索引中包含需要查詢的行,那麼這次查詢就不會需要回表去查詢數據。如果是二級索引,那麼可以減少對主鍵的二次查詢。
如果只需要讀取索引數據,Mysql將會減少很大的數據訪問量。磁盤I/O也會降低很多。如果開啓了緩存,那麼緩存中的數據也會小很多。