MySQL之索引

1. 前言

1. 最佳左前綴

最左前綴原則,就是最左優先,在建立多列索引時,要根據業務需求,where 子句中使用最頻繁的一列放在最左邊。 image.png 當咱們建立一個組合索引的時候,如 (a1,a2,a3),至關於建立了(a1)、(a1,a2)和(a1,a2,a3)三個索引,這就是最左匹配原則。html

1.1 原理分析

先舉一個遵循最佳左前綴法則的例子mysql

## 假設ab是聯合索引
select * from testTable where a=1 and b=2
複製代碼
  • 首先a字段在B+樹上是有序的,因此咱們能夠經過二分查找法來定位到a=1的位置。
  • 其次在a肯定的狀況下,b是相對有序的,由於有序,因此一樣能夠經過二分查找法找到b=2的位置。

再來看看不遵循最佳左前綴的例子sql

select * from testTable where b=2
複製代碼

咱們來回想一下b有順序的前提:在a肯定的狀況下: 如今你的a都飛了,那b確定是不能肯定順序的,在一個無序的B+樹上是沒法用二分查找來定位到b字段的。 因此這個時候,是用不上索引的。你們懂了嗎?數據庫

範圍查詢右邊失效原理緩存

select * from testTable where a>1 and b=2
複製代碼
  • 首先a字段在B+樹上是有序的,因此能夠用二分查找法定位到1,而後將全部大於1的數據取出來,a能夠用到索引。
  • b有序的前提是a是肯定的值,那麼如今a的值是取大於1的,可能有10個大於1的a,也可能有一百個a。
  • 大於1的a那部分的B+樹裏,b字段是無序的(開局一張圖),因此b不能在無序的B+樹裏用二分查找來查詢,b用不到索引。

2. 索引下推

2.1 什麼是索引下推

導讀:[官網解釋索引條件下推(ICP)](dev.mysql.com/doc/refman/…bash

如今你的a都飛了,那b確定是不能肯定順序的,在一個無序的B+樹上是沒法用二分查找來定位到b字段的。服務器

因此這個時候,是用不上索引的。你們懂了嗎?-condition-pushdown-optimization.html)markdown

索引下推(index condition pushdown )簡稱ICP,在Mysql5.6的版本上推出,用於優化查詢。函數

  • 在不使用ICP的狀況下,在使用非主鍵索引(又叫普通索引或者二級索引)進行查詢時,存儲引擎經過索引檢索到數據,而後返回給MySQL服務器,服務器而後判斷數據是否符合條件 。oop

  • 在使用ICP的狀況下,若是存在某些被索引的列的判斷條件時,MySQL服務器將這一部分判斷條件傳遞給存儲引擎,而後由存儲引擎經過判斷索引是否符合MySQL服務器傳遞的條件,只有當索引符合條件時纔會將數據檢索出來返回給MySQL服務器 。

索引條件下推優化能夠減小存儲引擎查詢基礎表的次數,也能夠減小MySQL服務器從存儲引擎接收數據的次數。

2.2 案例分析

在開始以前先準備一張用戶表(user),其中主要幾個字段有:id、name、age、address。創建聯合索引(name,age)。

假設有一個需求,要求匹配姓名第一個爲陳的全部用戶,sql語句以下:

SELECT * from user where  name like '陳%'
複製代碼
  • 根據 "最佳左前綴" 的原則,這裏使用了聯合索引(name,age)進行了查詢,性能要比全表掃描確定要高。

問題來了,若是有其餘的條件呢?假設又有一個需求,要求匹配姓名第一個字爲陳,年齡爲20歲的用戶,此時的sql語句以下:

SELECT * from user where  name like '陳%' and age=20
複製代碼
  • 這條sql語句應該如何執行呢?下面對Mysql5.6以前版本和以後版本進行分析。

Mysql5.6以前的版本

  • 5.6以前的版本是沒有索引下推這個優化的,所以執行的過程以下圖:

image.png

會忽略age這個字段,直接經過name進行查詢,在(name,age)這課樹上查找到了兩個結果,id分別爲2,1,而後拿着取到的id值一次次的回表查詢,所以這個過程須要回表兩次

Mysql5.6及以後版本

  • 5.6版本添加了索引下推這個優化,執行的過程以下圖:

image.png

InnoDB並無忽略age這個字段,而是在索引內部就判斷了age是否等於20,對於不等於20的記錄直接跳過,所以在(name,age)這棵索引樹中只匹配到了一個記錄,此時拿着這個id去主鍵索引樹中回表查詢所有數據,這個過程只須要回表一次。

2.3 實踐

固然上述的分析只是原理上的,咱們能夠實戰分析一下,所以陳某裝了Mysql5.6版本的Mysql,解析了上述的語句,以下圖 image.png

  • 根據explain解析結果能夠看出Extra的值爲Using index condition,表示已經使用了索引下推。

2.4 總結

  • 索引下推在非主鍵索引上的優化,能夠有效減小回表的次數,大大提高了查詢的效率。
  • 關閉索引下推可使用以下命令,配置文件的修改再也不講述了,畢竟這麼優秀的功能幹嗎關閉呢:
set optimizer_switch='index_condition_pushdown=off';
複製代碼

4. 前綴索引

4.1 什麼是前綴索引

前綴索引也叫局部索引,好比給身份證的前 10 位添加索引,相似這種給某列部分信息添加索引的方式叫作前綴索引。

4.2 爲何要用前綴索引

前綴索引能有效減少索引文件的大小,讓每一個索引頁能夠保存更多的索引值,從而提升了索引查詢的速度。但前綴索引也有它的缺點,不能在 order by 或者 group by 中觸發前綴索引,也不能把它們用於覆蓋索引。

4.3 什麼狀況下適合使用前綴索引?

當字符串自己可能比較長,並且前幾個字符就開始不相同,適合使用前綴索引;相反狀況下不適合使用前綴索引,好比,整個字段的長度爲 20,索引選擇性爲 0.9,而咱們對前 10 個字符創建前綴索引其選擇性也只有 0.5,那麼咱們須要繼續加大前綴字符的長度,可是這個時候前綴索引的優點已經不明顯,就沒有建立前綴索引的必要了

舉例說明

當要索引的列字符不少時 索引則會很大且變慢,能夠只索引列開始的部分字符串 節約索引空間 從而提升索引效率

  • 原則:下降重複的索引值

例如:如今有一個地區表,發現 area 字段不少都是以 china 開頭的,那麼若是之前1-5位字符作前綴索引就會出現大量索引值重複的狀況,索引值重複性越低 查詢效率也就越高

前綴索引測試

// 建立一個測試表\
CREATE TABLE `x_test` (\
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,\
  `x_name` varchar(255) NOT NULL,\
  `x_time` int(10) NOT NULL,\
  PRIMARY KEY (`id`)\
) ENGINE=InnoDB AUTO_INCREMENT=4145025 DEFAULT CHARSET=utf8mb4\
\
// 添加200萬條測試數據\
INSERT INTO x_test(x_name,x_time) 
    SELECT CONCAT(rand()*3300102,x_name),x_time
    FROM x_test 
    WHERE id < 30000;
複製代碼

image.png

200萬 測試數據

  • 在無任何索引的狀況下隨便查詢一條
SELECT * FROM x_test WHERE 
x_name = '1892008.205824857823401.800099203178258.8904820949682635656.62526521254';

----
查詢時間:2.253s
複製代碼
  • 添加前綴索引 ( 以第一位字符建立前綴索引 )
alter table x_test add index(x_name(1))

再次查詢相同sql語句

SELECT * FROM x_test WHERE 
x_name = '1892008.205824857823401.800099203178258.8904820949682635656.62526521254';

---
查詢時間:3.291s
複製代碼

當使用第一位字符建立前綴索引後 貌似查詢的時間更長了,由於只第一位字符而言索引值的重讀性太大了。200萬條數據全以數字開頭那麼平均20萬條的數據都是相同的索引值。

  • 從新創建前綴索引 此次之前4位字符來建立
alter table x_test add index(x_name(4));

再次查詢相同sql語句
SELECT * FROM x_test WHERE 
x_name = '1892008.205824857823401.800099203178258.8904820949682635656.62526521254';

---
查詢時間:0.703s
複製代碼

此次之前4位建立索引 大大減小了索引值的重複性 查詢速度從3秒提高到0.7秒

  • 200萬條數據都以數字開頭 而0-9排列組合7位則可達到千萬種組合

也就是之前7位來作索引則不會出現重複索引值的狀況了

alter table x_test add index(x_name(7));

再次查詢相同sql語句

SELECT * FROM x_test WHERE 
x_name = '1892008.205824857823401.800099203178258.8904820949682635656.62526521254';
----
查詢時間:0.014s ( 首次執行無緩存狀態下 )
複製代碼

4.2 最佳實踐

有時候須要索引很長的字符列,這會讓索引變得大且慢。一般能夠索引開始的部分字符,這樣能夠大大節約索引空間,從而提升索引效率。但這樣也會下降索引的選擇性。索引的選擇性是指不重複的索引值(也稱爲基數,cardinality)和數據表的記錄總數的比值,範圍從1/#T到1之間。索引的選擇性越高則查詢效率越高,由於選擇性高的索引可讓MySQL在查找時過濾掉更多的行。惟一索引的選擇性是1,這是最好的索引選擇性,性能也是最好的。

通常狀況下某個前綴的選擇性也是足夠高的,足以知足查詢性能。對於BLOB,TEXT,或者很長的VARCHAR類型的列,必須使用前綴索引,由於MySQL不容許索引這些列的完整長度。

訣竅在於要選擇足夠長的前綴以保證較高的選擇性,同時又不能太長(以便節約空間)。前綴應該足夠長,以使得前綴索引的選擇性接近於索引的整個列。換句話說,前綴的」基數「應該接近於完整的列的」基數「。

爲了決定前綴的合適長度,須要找到最多見的值的列表,而後和最多見的前綴列表進行比較。下面的示例是mysql官方提供的示例數據庫

在示例數據庫sakila中並無合適的例子,因此從表city中生成一個示例表,這樣就有足夠數據進行演示:

mysql> select database();                                                           
+------------+
| database() |
+------------+
| sakila     |
+------------+
1 row in set (0.00 sec)

mysql> create table city_demo (city varchar(50) not null);                          
Query OK, 0 rows affected (0.02 sec)

mysql> insert into city_demo (city) select city from city;                          
Query OK, 600 rows affected (0.08 sec)
Records: 600  Duplicates: 0  Warnings: 0

mysql> insert into city_demo (city) select city from city_demo;
Query OK, 600 rows affected (0.07 sec)
Records: 600  Duplicates: 0  Warnings: 0

mysql> update city_demo set city = ( select city from city order by rand() limit 1);
Query OK, 1199 rows affected (0.95 sec)
Rows matched: 1200  Changed: 1199  Warnings: 0

mysql>

複製代碼

由於這裏使用了rand()函數,因此你的數據會與個人不一樣,固然那不影響聰明的你。

首先找到最多見的城市列表:

mysql> select count(*) as cnt, city from city_demo group by city order by cnt desc limit 10;               
+-----+--------------+
| cnt | city         |
+-----+--------------+
|   8 | Garden Grove |
|   7 | Escobar      |
|   7 | Emeishan     |
|   6 | Amroha       |
|   6 | Tegal        |
|   6 | Lancaster    |
|   6 | Jelets       |
|   6 | Ambattur     |
|   6 | Yingkou      |
|   6 | Monclova     |
+-----+--------------+
rows in set (0.01 sec)

mysql>

複製代碼

注意到查詢結果,上面每一個值都出現了6-8次。如今查找到頻繁出現的城市前綴。先從3個前綴字母開始,而後4個,5個,6個:

mysql> select count(*) as cnt,left(city,3) as pref from city_demo group by pref order by cnt desc limit 10;
+-----+------+
| cnt | pref |
+-----+------+
|  25 | San  |
|  15 | Cha  |
|  12 | Bat  |
|  12 | Tan  |
|  11 | al-  |
|  11 | Gar  |
|  11 | Yin  |
|  10 | Kan  |
|  10 | Sou  |
|  10 | Bra  |
+-----+------+
10 rows in set (0.00 sec)

mysql> select count(*) as cnt,left(city,4) as pref from city_demo group by pref order by cnt desc limit 10; 
+-----+------+
| cnt | pref |
+-----+------+
|  12 | San  |
|  10 | Sout |
|   8 | Chan |
|   8 | Sant |
|   8 | Gard |
|   7 | Emei |
|   7 | Esco |
|   6 | Ying |
|   6 | Amro |
|   6 | Lanc |
+-----+------+
10 rows in set (0.01 sec)

mysql> select count(*) as cnt,left(city,5) as pref from city_demo group by pref order by cnt desc limit 10; 
+-----+-------+
| cnt | pref  |
+-----+-------+
|  10 | South |
|   8 | Garde |
|   7 | Emeis |
|   7 | Escob |
|   6 | Amroh |
|   6 | Yingk |
|   6 | Moncl |
|   6 | Lanca |
|   6 | Jelet |
|   6 | Tegal |
+-----+-------+
10 rows in set (0.01 sec)
複製代碼
mysql> select count(*) as cnt,left(city,6) as pref from city_demo group by pref order by cnt desc limit 10; 
+-----+--------+
| cnt | pref   |
+-----+--------+
|   8 | Garden |
|   7 | Emeish |
|   7 | Escoba |
|   6 | Amroha |
|   6 | Yingko |
|   6 | Lancas |
|   6 | Jelets |
|   6 | Tegal  |
|   6 | Monclo |
|   6 | Ambatt |
+-----+--------+
rows in set (0.00 sec)

mysql>
複製代碼

經過上面改變不一樣前綴長度發現,當前綴長度爲6時,這個前綴的選擇性就接近完整列的選擇性了。甚至是同樣的。

固然還有另外更方便的方法,那就是計算完整列的選擇性,並使其前綴的選擇性接近於完整列的選擇性。下面顯示如何計算完整列的選擇性:

mysql> select count(distinct city) / count(*) from city_demo;
+---------------------------------+
| count(distinct city) / count(*) |
+---------------------------------+
|                          0.4283 |
+---------------------------------+
row in set (0.05 sec)

mysql>
複製代碼

能夠在一個查詢中針對不一樣前綴長度的選擇性進行計算,這對於大表很是有用,下面給出如何在同一個查詢中計算不一樣前綴長度的選擇性:

mysql> select count(distinct left(city,3))/count(*) as sel3,
    -> count(distinct left(city,4))/count(*) as sel4,
    -> count(distinct left(city,5))/count(*) as sel5, 
    -> count(distinct left(city,6))/count(*) as sel6 
    -> from city_demo;
+--------+--------+--------+--------+
| sel3   | sel4   | sel5   | sel6   |
+--------+--------+--------+--------+
| 0.3367 | 0.4075 | 0.4208 | 0.4267 |
+--------+--------+--------+--------+
1 row in set (0.01 sec)

mysql>
複製代碼

能夠看見當索引前綴爲6時的基數是0.4267,已經接近完整列選擇性0.4283。

在上面的示例中,已經找到了合適的前綴長度,下面建立前綴索引:

mysql> alter table city_demo add key (city(6));
Query OK, 0 rows affected (0.19 sec)
Records: 0  Duplicates: 0  Warnings: 0
複製代碼
mysql> explain select * from city_demo where city like 'Jinch%';
+----+-------------+-----------+-------+---------------+------+---------+------+------+-------------+
| id | select_type | table     | type  | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-----------+-------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | city_demo | range | city          | city | 20      | NULL |    2 | Using where |
+----+-------------+-----------+-------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)
複製代碼

能夠看見正確使用剛建立的索引。

前綴索引是一種能使索引更小,更快的有效辦法,但另外一方面也有其缺點:

mysql沒法使用其前綴索引作ORDER BY和GROUP BY,也沒法使用前綴索引作覆蓋掃描。

相關文章
相關標籤/搜索