官方定義:索引是幫助 MySQL 高效獲取資料的資料結構
從上面定義中我們可以分析出索引本質是一個資料結構,他的作用是幫助我們高效獲取資料,在正式介紹索引前,我們先來了解一下基本的資料結構
Hash 索引是比較常見的一種索引,他是通過計算出記錄對應的 hash 值,然後根據計算結果,儲存在對應位置。查詢的時候也是根據 hash 值快速找到位置。他的單條記錄查詢的效率很高,時間複雜度為1。但是,Hash索引並不是最常用的資料庫索引型別,尤其是我們常用的Mysql Innodb引擎就是不支援hash索引的。
hash 索引在等值查詢時速度很快,但是有以下兩個問題
1、一個節點只能有兩個子節點
2、左子節點的值小於父親節點值,右子節點的值大於父親節點的值,採用二分查詢,速度較快
經典二元樹會出現一個極端例子,就是連結串列,節點資料越來越大。這種情況下,二元樹搜尋效能就會降低
平衡二元樹又稱AVL樹。它可以是一顆空樹,或者具有以下性質的二叉排序樹:
數位 1-6 在平衡二元樹中圖示如下:
B樹屬於多叉樹又名平衡多路查詢樹,可以有多叉,有如下特點
(1)排序方式:所有節點關鍵字是按遞增次序排列,並遵循左小右大原則;
(2)子節點數:非葉節點(根節點和枝節點)的子節點數 >1、且子節點數量<=M 、且M>=2,空樹除外(注:M階代表一個樹節點最多有多少個查詢路徑,M=M路,當M=2則是2叉樹,M=3則是3叉);
(3)關鍵字數:枝節點的關鍵字數量大於等於ceil(m/2)-1個且小於等於M-1個(注:ceil()是個朝正無窮方向取整的函數 如ceil(1.1)結果為2);
(4)所有葉子節點均在同一層、葉子節點除了包含了關鍵字 和 關鍵字記錄的指標外,也有指向其子節點的指標只不過其指標地址都為null對應下圖最後一層節點的空格子;
MySQL 中 B 樹儲存結構如下:
B+樹是在B樹的基礎上又一次的改進,其主要對兩個方面進行了提升,一方面是查詢的穩定性,另外一方面是在資料排序方面更友好。MySQL 索引的底層資料結構採用的就是 B+ 樹
(1)B+樹的非葉子節點不儲存具體的資料,而只儲存關鍵字的索引,而所有的資料最終都會儲存到葉子節點。因為所有資料必須要到葉子節點才能獲取到,所以每次資料查詢的次數都一樣,這樣一來B+樹的查詢速度也就會比較穩定,而B樹的查詢過程中,不同的關鍵字查詢的次數很有可能都是不同的(有的資料可能在根節點,有的資料可能在最下層的葉節點),所以在資料庫的應用層面,B+樹就顯得更合適。
(2)B+樹葉子節點的關鍵字從小到大有序排列,左邊結尾資料都會儲存右邊節點開始資料的指標。因為葉子節點都是有序排列的,所以B+樹對於資料的排序有著更好的支援。
B樹是B+樹一種變形,它是在B+樹的基礎上,將索引層以指標連線起來(B+ 樹只是將資料層用指標連線起來),使搜尋取值更加快捷
select * from user where age = 20 order by age
這條查詢語句則符合一星和二星
三星
select * from user where age = 20
這條語句不符合三星,因為索引列中只有 id 和 age,沒有 name
select age from user where age = 20
這條語句則符合三星,因為只查詢了 age,age 在索引中存在,不需要回表
上面三星索引提到了一個次回表,那麼回表是什麼?
簡單點說,就是查詢語句中需要的列,在索引中不包含,需要根據主鍵 id 再查詢一次才能獲取到。回表相當於多查詢一次,再查詢時我們要儘量避免回表查詢。
因為普通索引中只包含了對應列和主鍵的值,比如 age 索引,那麼 age 索引中包含的資料有 age,id。此時如果需要 name 的話,需要先通過 age 索引找到對應的 id,然後再去主鍵索引上找到 name,主鍵索引包含了一行所有記錄的值。這裡回答了上面的問題,為什麼 MySQL 一定要有主鍵索引,因為主鍵索引子節點中包含了全部資料
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) NOT NULL,
`age` int(1) DEFAULT NULL,
`sex` varchar(2) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name_age` (`name`,`age`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;
select name,age from user where name = "張三"
-- 這條語句就使用了索引覆蓋,因為 name 和 age 再 idx_name_age 索引中都有,不需要回表查詢
select name,age,sex from user where name = "張三"
-- 如果加上了 sex,那麼就需要回表查詢了,因為索引中不存在 sex 欄位
慢查詢紀錄檔是 MySQL 提供的紀錄檔記錄,用來記錄所有的慢 SQL 語句,我們可以通過設定慢查詢的時間閾值 long_query_time,來定義什麼樣的 SQL 是慢 SQL。通過慢查詢紀錄檔我們可以找出需要優化的 SQL,下一步就是進行 SQL 優化
第一步:我們可以通過 show variables like 'slow_query_log' 語句查詢慢查詢是否開啟,預設是關閉(OFF)
slow_query_log_file 是慢查詢紀錄檔存放的位置,如果是 window 的話,通常在你的安裝資料夾 Data 目錄下
第二步:開啟慢查詢
set global slow_query_log = 1;
第三步:設定慢查詢閾值
什麼樣的查詢叫做慢查詢呢?1s,5s 還是 10s,這點 MySQL 不知道,所以需要我們通過設定去設定 long_query_time 引數
通過命令 show variables like '%long_query_time%' 檢視慢查詢時間,預設是 10 s
如果需要修改,可以通過命令 set global long_query_time = 5
來設定
注意:這裡通過 set global long_query_time = 5
設定完慢查詢時間後,再次查詢發現慢查詢時間依然是 10s,難道是設定沒生效?
使用此命令修改後,需要重新連線或者新開啟一個對談就可以看到修改後的設定
或者通過 show global variables like '%long_query_time%'
命令檢視
我們剛才已經將慢查詢閾值設定為 5s,現在我們執行一條這樣的 sql 語句
select sleep(6);
這條語句執行時間為 6s,我們開啟慢查詢紀錄檔可以發現增加了一些資料
# Time: 2022-10-02T09:16:23.194396Z
# User@Host: root[root] @ localhost [::1] Id: 6
# Query_time: 6.011569 Lock_time: 0.000000 Rows_sent: 1 Rows_examined: 0
SET timestamp=1664675770;
select sleep(6);
我們來逐個分析一下每行代表什麼含義:
User@Host:執行該 SQL 的使用者和慢查詢 IP 地址
Query_time:語句執行時長
Lock_time:獲取鎖的時長
Rows_sent:MySQL 返回給使用者端的行數
Rows_examined:MySQL 掃描行數
timestamp:表示慢 SQL 記錄時的時間戳
select sleep(6):則是慢查詢 SQL
下面我們來分析一條真實的慢查詢 SQL,之前測試時的一條 SQL 語句
# Time: 2022-07-27T09:26:44.440318Z
# User@Host: root[root] @ localhost [127.0.0.1] Id: 249
# Query_time: 68.461112 Lock_time: 0.000938 Rows_sent: 877281 Rows_examined: 877303
SET timestamp=1658914004;
SELECT id,prd_line_id,shift_name,shift_id,app_id,weight,upload_time,operator,status,prd_line_name FROM prd_weight
WHERE (upload_time > '2022-07-27 00:00' AND upload_time < '2022-07-27 17:24');
Query_time:總查詢時長 68.461112s
Lock_time:0.000938s
Rows_examined:掃描行 877281
Rows_sent:返回了 877303
當然了,這是測試用的,生產上一般不會出現這麼離譜的 SQL 語句
通過上面的慢查詢紀錄檔分析,我們可以知道有哪些慢 SQL 語句。但是這些 SQL 具體慢在哪裡,需要如何優化,我們還需要更詳細的分析計劃,這裡 MySQL 給我們提供了 Explain 關鍵字,通過該關鍵字我們可以分析出 SQL 語句的詳細執行資訊。
我們在資料庫中建立一張 user 表用於測試
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`sex` varchar(2) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`phone` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`dept_id` int(10) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_name`(`name`) USING BTREE,
INDEX `idx_dept_id`(`dept_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, '張三', '123', '男', '12323432', 1);
INSERT INTO `user` VALUES (2, '李四', '456', '男', '178873937', 1);
INSERT INTO `user` VALUES (3, '小花', '123', '女', '1988334554', 2);
INSERT INTO `user` VALUES (4, '小芳', '334', '女', '18765287937', 2);
INSERT INTO `user` VALUES (5, NULL, '122', NULL, NULL, NULL);
DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dept_name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of dept
-- ----------------------------
INSERT INTO `dept` VALUES (1, '開發部');
INSERT INTO `dept` VALUES (2, '銷售部');
explain 使用也很簡單,直接在查詢語句前面加上 explain 關鍵字即可:
EXPLAIN SELECT * FROM user where id = 1;
從圖中我們看到 MySQL 返回了一行記錄,下面我們一起來分析每個欄位代表什麼含義
欄位 | 含義 |
---|---|
id | 一次查詢過程中該條 select 語句的唯一標識 |
select_type | 查詢型別,共包含四種 simple、primary、subquery、derived |
table | 查詢的是哪張表 |
partitions | 表的分割區資訊 |
type | 存取型別,分析效能主要通過該欄位 |
possible_keys | 可能會用到的索引 |
key | 實際用到的索引 |
key_len | 索引裡使用的位元組數 |
ref | 這一列顯示了在key列記錄的索引中,表查詢值所用到的列或常數 |
rows | MySQL 預估的掃描行 |
filtered | MySQL 過濾後,滿足條件記錄數的比例 |
Extra | 展示了一些額外資訊 |
1、id
id 是查詢語句中的唯一標識,id 的值越大,該 id 對應的 sql 語句越先執行
explain select * from dept where id = (select dept_id from user where id = 1);
從執行計劃來看,select dept_id from user where id = 1 這條語句先執行,因為外層查詢需要藉助這條查詢語句的結果
2、select_type
查詢型別,共包含四種
simple:簡單查詢。查詢不包含子查詢和union
primary:複雜查詢中最外層的 select
subquery:包含在 select 中的子查詢(不在 from 子句中)
derived:包含在 from 子句中的子查詢。MySQL會將結果存放在一個臨時表中,也稱為派生表(derived的英文含義)
3、table
查詢的是哪張表,比較好理解
4、partitions
查詢時匹配到的分割區資訊,對於非分割區表值為 NULL,當查詢的是分割區表時,partitions 顯示分割區表命中的分割區情況。
5、type
type:查詢使用了何種型別,它在 SQL優化中是一個非常重要的指標,以下效能從好到壞依次是:system > const > eq_ref > ref > ref_or_null > range > index > ALL
system 是 const 的特例,也就是當表中只存在一條記錄時,type 為 system
const,常數查詢,id 是主鍵,通過 id 可以查詢到所有資訊
eq_ref,連線查詢中,primary key 或 unique key 索引的所有部分被連線使用
注意:這裡 dept 的 id 和 user 的 id 並無關聯關係,只是為了演示該查詢型別
user 的 id 和 dept 的 id 都是主鍵,在連線查詢中,兩個主鍵都被使用到
6、possible_keys
MySQL 分析此次查詢可能會用到的索引,但是實際查詢中不一定會用到
分析可能會用到 idx_name 這個索引,實際查詢中沒有用到索引,走的全表掃描
7、key
查詢時真正用到的 key
查詢中實際上用到了 idx_name 這個索引
8、ken_len
表示查詢用到的索引列長度
我們用這個索引來分析,key_len 為 63 是怎麼來的?
建立 user 表的時候,不知道大家有沒有注意到,name 的字元集為 utf8
MySQL 5.0 版本以上,utf8 字元集下每個字元佔用 3 個位元組,varchar(20) 則佔用 60 個位元組,同時因為 varchar 是變長字串,需要額外地位元組存放字元長度,共兩個位元組,此外,name 欄位可以為 null 值,null 值單獨佔用一個位元組,加在一起一共 63 個位元組
9、ref
當使用索引列等值匹配的條件去執行查詢時,也就是在存取方法是const
、eq_ref
、ref
、ref_or_null
、unique_subquery
、index_subquery
其中之一時,ref
列展示的就是與索引列作等值匹配的具體資訊,比如只是一個常數或者是某個列。
10、rows
預計需要掃描的函數
11、filtered
filtered 這個是一個百分比的值,表裡符合條件的記錄數的百分比。簡單點說,這個欄位表示儲存引擎返回的資料在經過過濾後,剩下滿足條件的記錄數量的比例。
12、Extra
Extra
是用來說明一些額資訊的,從而幫助我們更加準確的理解查詢
explain select * from user where left(name,2) = '小芳';
這段 sql 對 name 欄位做了函數操作,導致索引失效
在使用聯合索引查詢時,應該遵循最左字首原則,指的是查詢從索引的最左前列開始並且不跳過索引中的列。
建立一張 goods 表,有一個聯合索引包含了 name,price、mark 三個欄位
DROP TABLE IF EXISTS `goods`;
CREATE TABLE `goods` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`price` int(10) NULL DEFAULT NULL,
`mark` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_all`(`name`, `price`, `mark`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of goods
-- ----------------------------
INSERT INTO `goods` VALUES (1, '手機', 5678, '華為手機');
INSERT INTO `goods` VALUES (2, '電腦', 9888, '蘋果電腦');
INSERT INTO `goods` VALUES (3, '衣服', 199, '好看的衣服');
執行下面的查詢語句:
explain select * from goods where name = '手機' and price = 5678 and mark = '華為手機';
從上圖可以看到 type 為 ref。
現在我們不從最左側開始查詢,直接跳過 name 欄位
explain select * from goods where price = 5678 and mark = '華為手機';
type 從 ref 變成了 index,這是因為 MySQL 建立索引時是按照組合索引中的欄位順序來排序的,如果跳過中間某個欄位,則不一定是有序的了。
覆蓋索引,需要查詢的欄位全部包含在索引列中,不需要回表查詢