百度後端二面有哪些內容,萬字總結

前言

這是最近一位老朋友去百度面試,應該是面試資深工程師崗位,他跟我講被問到mysql索引知識點?其實面試官主要還是考察對mysql的性能調優相關,問理論知識其實也是想知道你對原理的認知,從而確認你是否有相關的調優經驗。朋友說他回答的還行,然後很順利進行了三面四面。那麼本文將跟大家一起來聊一聊這個如何回答面試官的這個問題!

以下是自己的理解,如果以下有不對的地方,請你來噴我鴨!

聊聊索引分類

數據結構分類可分爲:B+TREE(樹)索引HASH索引FULLTEXT索引;按索引種類可以分爲:普通索引主鍵索引唯一索引全文索引組合索引一級索引二級索引

這兩個有什麼區別嘛?肯定有:一個是索引實現類型;一個是創建索引用到的類型

  • 普通索引:(INDEX)建立在普通字段上的索引被稱爲普通索引
ALTER TABLE `table_name` ADD INDEX idx_name ( `user_name` ) 
  • 主鍵索引:(PRIMARY KEY)建立在主鍵上的索引被稱爲主鍵索引,一張數據表只能有一個主鍵索引,索引列值不允許有空值
ALTER TABLE `table_name` ADD PRIMARY KEY ( `user_id` ) 
  • 唯一索引:(UNIQUE)建立在 unique 字段上的索引被稱爲唯一索引,一張表可以有多個唯一索引,索引列值允許爲空,列值中出現多個空值不會發生重複衝突
ALTER TABLE `table_name` ADD UNIQUE (`user_name`)
  • 全文索引:(FULLTEXT)建立在 varcharchartext列上的全文索引;配合 match against 使用,類似一個搜索引擎,數據大時,很佔用空間且耗時
ALTER TABLE `table_name` ADD FULLTEXT ( `user_desc` )
  • 組合索引:建立在多列上的索引叫組合索引,遵循”最左前綴“原則
ALTER TABLE `table_name` ADD INDEX idx_name_age ( `user_name`, `user_age` )
  • 一級索引:索引和數據存儲在一起,都存儲在同一個B+tree中的葉子節點。一般主鍵索引都是一級索引
  • 二級索引:二級索引樹的葉子節點存儲的是主鍵而不是數據。也就是說,在找到索引後,得到對應的主鍵,再回到一級索引中找主鍵對應的數據記錄

注意點:切不可濫用索引;切不可建立太多的索引;切不可建立重複索引

① 索引雖然提高查詢速度,但同時會降低更新表的速度

② 建立索引會佔用磁盤空間的索引文件;儘量減少在大表上建立過多的組合索引;

上面圖帶上一級二級索引是爲了讓大家更加了解索引結構 B+ Tree 的結構圖可以很清楚索引是如何存儲構建的且分層

索引覆蓋

顧名思義:覆蓋索引就是查詢的數據列只需要從索引中就可以獲取到不用再讀取數據行;再通俗易懂的講,我們sql查詢的數據要被所建的索引能覆蓋

Mysql中只能使用 B+Tree 索引做覆蓋索引;想必大家都知道 B+Tree 的原理吧?這裏不再贅述。說下用處:

  • 無需回表,查詢速度快

  • 減少系統調用和數據拷貝到緩存區等待時間

看到這這裏知道爲啥很多大廠不建議使用 select * from xxx 查詢了(千萬不要聽別人說 可以使用select * 我待了幾個大廠從來不建議這樣操作),目的就是:儘量能避免回表和減少IO的大小

怎麼確認sql觸發索引覆蓋

觸發索引覆蓋:我們可以通過 explain sql 語句 輸出結果爲 Using Index 時,就能夠觸發索引覆蓋。

① 那麼我們看下 Explain 關鍵詞分析:

常見類型 描述 備註說明
Using Index 使用了索引覆蓋,不需要回表查詢
Using Index Condition 使用了索引下推(5.6+版本)
Using Where 使用where條件再Sever層過濾數據 並非最佳
Using Filesort 不能利用索引樹而採取了額外的排序操作 需要優化
Using Temporary 使用了臨時表保存了中間結果集 需要優化
Using Join Buffer 連表查詢時使用了循環嵌套掃描 需要優化

② 索引覆蓋例子

## 創建一張測試索引覆蓋的臨時表,並對暱稱  user_name 創建了索引

CREATE TABLE `user` (
  `user_id`   int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '用戶id',
  `user_name` varchar(125)     NOT NULL DEFAULT ''     COMMENT '用戶暱稱',
  `user_pwd`  varchar(64)      NOT NULL DEFAULT ''     COMMENT '用戶密碼',
  `user_sex`  tinyint(1)       NOT NULL DEFAULT '0'    COMMENT '用戶性別 0-保密;1-男;2-女',
  `create_at` int(10)          NOT NULL DEFAULT '0'    COMMENT '創建時間',
  PRIMARY KEY (`user_id`),
  KEY `idx_name` (`user_name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';

來看下第一個sql語句

mysql> explain select user_id,user_name from user where user_name = '李阿沐' limit 1;
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key      | key_len | ref   | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name      | idx_name | 377     | const |    1 |   100.00 | Using index |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.01 sec)

我們從上面執行的結果可以看到 Extra = 'Using index',使用了 idx_name 普通索引項。我們知道 B+Tree 葉子節點中 索引也會作爲數據頁,存放的是普通目錄項記錄;idx_name的索引樹裏面存儲了主鍵ID和user_name,這樣就完全不需要回表操作,查詢效率比較高。

再來看一個sql語句

## 例如有時我們會通過用戶暱稱查詢用戶的pwd
mysql> explain select user_pwd from user where user_name = '李阿沐';
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key      | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name      | idx_name | 377     | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.01 sec)

我們來看下流程走向:

  • 通過 idx_name 索引樹,從樹上找到 user_name = '李阿沐' 對應的主鍵id
  • 通過 回表操作 在主鍵索引樹上找到滿足條件的數據,返回

雖然sql語句命中了idx_name索引,儘管索引葉子節點存儲了主鍵user_id,很遺憾並沒有存儲 user_pwd 字段,所以需要回表查詢纔可以拿到這個值;那麼這種操作就不符合索引覆蓋原理,因爲經過了回表,從而影響了查詢效率。跟索引覆蓋理念完全不合。

在這裏有必要順便解釋一下 explain 結果集個字段的意思,加深下印象(簡單掃一眼,看看下,很少人問):

字段 描述說明
id SELECT識別符,每個SELECT子句的標識id
select_type SELECT類型;例如:① simple簡單select(不使用UNION或子查詢);② primary最外面的select查詢;③ union中的第二個或後面的select查詢語句;④ dependent union的第二個或後面的查詢語句,取決於外面的查詢;⑤ union result的結果集;⑥ subquery子查詢中的第一個select查詢;⑦ dependent subquery子查詢中的第一個select查詢,取決於外面的查詢;⑧ derived導出表的select查詢(from子句的子查詢)
table 當前表名
partitions 顯示查詢訪問的分區
type 當前表內的聯接類型;例如:① system表僅有一行;② const表最多有一個匹配行;③ eq_ref對於每個來自於前面的表的行組合,從該表中讀取一行;④ ref:對於每個來自於前面的表的行組合,所有有匹配索引值的行將從這張表中讀取;⑤ ref_or_null同ref,但添加了mysql可以專門搜索包含NULL值的行;⑥ index_merge使用了索引合併優化方法;⑦ range只檢索給定範圍的行,使用一個索引來選擇行;⑧ index該聯接類型與ALL相同,除了只有索引樹被掃描。這通常比ALL快,因爲索引文件通常比數據文件小;⑨ all對於每個來自於先前的表的行組合,進行完整的表掃描
possible_keys 顯示可能使用到的索引
key 顯示mysql經過優化器評估最終使用的索引,如果沒有選擇索引,鍵是null
key_len 使用到的索引長度,若鍵爲null則爲null
ref 引用到上個表的列
rows 得到結果集需要掃描的記錄數
filtered 存儲引擎返回數據在server層過濾後, 剩下多少滿足查詢的記錄數據比例
Extra 查詢額外信息

索引下推

在介紹索引下推之前,我們對上面的數據表增加一個 user_age 字段:

增加字段 user_age

alter table `user` add column `user_age` smallint(5) not null default '0' comment '用戶年齡';

## 執行結果

mysql> alter table `user` add column `user_age` smallint(5) not null default '0' comment '用戶年齡';
Query OK, 0 rows affected, 1 warning (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 1

增加組合索引 name + age

alter table `user` add index idx_name_age (`user_name`, `user_age`);

## 執行結果

mysql> alter table `user` add index idx_name_age (`user_name`, `user_age`);
Query OK, 0 rows affected (0.03 sec)
Records: 0  Duplicates: 0  Warnings: 0

查看錶結構

## 再看下錶結構 已經新增了  user_age 字段 和 idx_name_age索引
mysql> show create table user\G
*************************** 1. row ***************************
       Table: user
Create Table: CREATE TABLE `user` (
  `user_id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '用戶id',
  `user_name` varchar(125) NOT NULL DEFAULT '' COMMENT '用戶暱稱',
  `user_pwd` varchar(64) NOT NULL DEFAULT '' COMMENT '用戶密碼',
  `user_sex` tinyint(1) NOT NULL DEFAULT '0' COMMENT '用戶性別 0-保密;1-男;2-女',
  `create_at` int NOT NULL DEFAULT '0' COMMENT '創建時間',
  `user_age` smallint NOT NULL DEFAULT '0' COMMENT '用戶年齡',
  PRIMARY KEY (`user_id`),
  KEY `idx_name_age` (`user_name`,`user_age`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表'
1 row in set (0.00 sec)

索引下推介紹

索引下推(index condition pushdown )簡稱ICP,在Mysql5.6+的版本上推出,用於優化查詢。索引下推在 非主鍵索引 上做合理的優化,可以有效減少回表的次數,同時可減少mysql服務器從存儲引擎接收數據的次數,大大提升了查詢的效率

未出現索引下推流程

在沒有索引下推之前,執行的過程是這樣,例如執行以下sql:

## 根據用戶暱稱 + 年齡查詢 相匹配的用戶

select * from user where `user_name` like '李%' and `age` = 26;

因爲上面已經建立了聯合索引idx_name_age,所以要根據 最左匹配原則 優先匹配name索引進行查詢,不然就會造成不走索引導致全表掃描
複製代碼

先根據name索引從存儲引擎中拉取數據,存儲引擎通過索引檢索到數據之後,通過不斷一個個的回表到主鍵索引找出符合的數據記錄,然後數據加載到 server 層,開始通過 user_age 條件過濾符合要求的數據。

未使用索引下推圖3-1

從圖中我們可以看出來:① 在查詢數據時存儲引擎會忽略age這個字段;② 直接通過 name 索引在 idx_name_age 這顆索引樹上查詢到3條複合要求的結果;③ 開始根據主鍵 user_id 回表查詢age對應的值,共回表3次;④ 在server層通過age條件進行過濾,得到最終符合要求的結果集;

出現索引下推流程

跟上圖進行對比發現:在通過索引樹拿到數據之後,就進行了索引下推操作,索引內部條件 過濾 符合數據結果。

使用索引下推圖3-2

mysql5.6版本之後,增加索引下推,流程走向:① 存儲引擎通過 idx_name_age 查詢,索引內部直接檢測判斷age值是否等於26,否則直接跳過;② 通過索引樹查到匹配記錄,通過主鍵id去主鍵索引樹中回表查詢對應字段值;③並不需要從存儲引擎拉到數據在server層做過濾操作

實踐操作

實踐前先插入幾條數據:

## 插入數據

mysql> insert into `user`(`user_name`,`user_pwd`,`user_sex`,`user_age`,`create_at`) VALUES('李阿沐', '123', 1, 26, 1624182989),('李子柒', '123', 1, 31, 1624182989),('李佳琦', '123', 1, 29, 1624182989),('高火火', '123', 1, 25, 1624182989);
Query OK, 4 rows affected (0.01 sec)
Records: 4  Duplicates: 0  Warnings: 0

## 查看下錶數據 

mysql> select * from user;
+---------+-----------+----------+----------+------------+----------+
| user_id | user_name | user_pwd | user_sex | create_at  | user_age |
+---------+-----------+----------+----------+------------+----------+
|       1 | 李阿沐    | 123      |        1 | 1624182989 |       26 |
|       2 | 李子柒    | 123      |        1 | 1624182989 |       31 |
|       3 | 李佳琦    | 123      |        1 | 1624182989 |       29 |
|       4 | 高火火    | 123      |        1 | 1624182989 |       25 |
+---------+-----------+----------+----------+------------+----------+
4 rows in set (0.00 sec)

mysql版本5.5下sql執行情況:

## 查看版本 5.5 小於5.6版本

mysql> select version();
+------------+
| version()  |
+------------+
| 5.5.19-log |
+------------+
1 row in set (0.00 sec)

## 查看執行explain結果集 圖 1

mysql> explain select * from user where `user_name` like '李%' and `user_age` = 26;
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | user  | ALL  | idx_name_age  | NULL | NULL    | NULL |    4 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

## 另一個執行結果  圖 2
mysql> explain select user_id,user_name from user where `user_name` like '李%' and `user_age` = 26;
+----+-------------+-------+-------+---------------+--------------+---------+------+------+--------------------------+
| id | select_type | table | type  | possible_keys | key          | key_len | ref  | rows | Extra                    |
+----+-------------+-------+-------+---------------+--------------+---------+------+------+--------------------------+
|  1 | SIMPLE      | user  | range | idx_name_age  | idx_name_age | 379     | NULL |    3 | Using where; Using index |
+----+-------------+-------+-------+---------------+--------------+---------+------+------+--------------------------+
1 row in set (0.00 sec)

不知道大家從上面是否看出來一個問題:它並不是走的索引下推而是 Using where;圖1和圖2存儲引擎都是顯示可能使用到的索引,但是圖1並沒有走索引並且全表掃描;而圖2走了索引只掃描其中幾條;所以可以得到一個結論:like查詢百分號前置並不是100%不會走索引。① 據量少直接回全表掃描;② 若只select索引字段,或者select索引字段和主鍵,會走索引的

mysql版本5.6下sql執行情況:

## 一樣先查看下mysql版本 8.0

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.19    |
+-----------+
1 row in set (0.00 sec)

## 查看執行explain結果集

mysql> explain select * from user where `user_name` like '李%' and `user_age` = 26;
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key          | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | idx_name_age  | idx_name_age | 379     | NULL |    3 |    25.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+--------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)

小夥伴們是不是可以很清晰的看到: Extra = Using index condition 使用了索引下推。所以大家以後再寫sql語句的時候,需要適當的根據 where條件 做合理的索引,儘量的使我們sql語句是最優狀態;當然這也是我們公司經常要求的,而且發現不好的sql語句,會被拿出來做案例。

小小的總結下索引下推

  • 未使用索引下推優化:先根據索引查詢記錄,回表再根據where條件過濾
  • 使用索引下推優化:根據索引樹獲取記錄的時,檢測是否可以用where條件過濾數據在回表查詢

最左匹配原則

面試中經常會被問到:假如表中設置了 (a, b, c)聯合索引,那麼你在sql查詢使用 (a, c, b) 或者 (b, a, c)會不會繼續走索引項呢?至少我面試中基本都會被問到,尤其是某些大廠!其實他們主要是想考察你最 最左匹配原則 是否理解原理。從而看出來你平常是否會對sql進行調優。

爲什麼要使用聯合索引

我們先修改表的聯合索引字段:

## mysql沒有提供修改索引的指令,可先刪除原索引,新增一個新的索引,變相實現修改索引

alter table `user` drop index idx_name_age;

alter table `user` add index idx_name_age_sex ( `user_name`, `user_age`, `user_sex` );

## 執行結果

mysql> alter table `user` drop index idx_name_age;
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0

mysql> alter table `user` add index idx_name_age_sex ( `user_name`, `user_age`, `user_sex` );
Query OK, 0 rows affected (0.02 sec)
Records: 0  Duplicates: 0  Warnings: 0
  • idx_name_age_sex聯合索引,相當於三個索引:idx_nameidx_name_ageidx_age_sex,節省磁盤空間的開銷和寫操作開銷
  • 若出現覆蓋索引,select user_name,user_age,user_sex from xxx where xxx,則直接通過索引遍歷獲取數據,無序回表查詢數據,減少IO操作和回表次數

最左匹配原則:mysql創建聯合索引時總會遵守最左匹配原則;從最左邊爲起點任何連續的索引都會被匹配成功;但是若查詢時出現範圍查詢(like、>、<、between)會停止索引匹配。

全值匹配查詢時

name_age_sex 索引順序

mysql> explain select * from user where `user_name` = "李阿沐" and `user_age` = 26 and `user_sex` = 1;
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys    | key              | key_len | ref               | rows | filtered | Extra |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name_age_sex | idx_name_age_sex | 380     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

name_sex_age 索引順序

mysql> explain select * from user where `user_name` = "李阿沐" and `user_sex` = 1 and `user_age` = 26;
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys    | key              | key_len | ref               | rows | filtered | Extra |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name_age_sex | idx_name_age_sex | 380     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

sex_age_name 索引順序

mysql> explain select * from user where `user_sex` = 1 and `user_age` = 26 and `user_name` = "李阿沐";
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys    | key              | key_len | ref               | rows | filtered | Extra |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name_age_sex | idx_name_age_sex | 380     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

從上面看出來:三個查詢 全部走了 idx_name_age_sex 聯合索引。可能不能更好的看出到底走的是哪一種索引,我們可以通過觀察 key_len 和 ref 這兩個跟第一個是完全一致的。

可能有小夥伴會有疑惑:“臥槽,不應該走最左匹配嘛?怎麼下面兩個命名沒有按照最左匹配卻都走了索引?”

其實我們不能忽略mysql本身的查詢優化器啊,我們可以不需要規規矩矩的按照順序去寫where條件,因爲查詢優化器會自動檢測這條sql,它以哪一種方式執行效率最高,最後才生成了真正的執行計劃。

匹配左邊的列時

① 依次匹配 name

mysql> explain select * from user where `user_name` = "李阿沐";
+----+-------------+-------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys    | key              | key_len | ref   | rows | filtered | Extra |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name_age_sex | idx_name_age_sex | 377     | const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

從上圖我們呢可以很清楚看到是遵循 最左匹配原則 使用了聯合索引且使用的是其中的 name 索引,沒有其他索引;看下key_len長度爲(單位字節):

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mn>377</mn><mo>=</mo><mn>125</mn><mo>∗</mo><mn>3</mn><mo>+</mo><mn>2</mn></mrow><annotation encoding="application/x-tex">377 = 125*3 + 2</annotation></semantics></math>377=125∗3+2

剛剛好是 name 索引長度值。

② 依次匹配 name_age

mysql> explain select * from user where `user_name` = "李阿沐" and `user_age` = 26;
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys    | key              | key_len | ref         | rows | filtered | Extra |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name_age_sex | idx_name_age_sex | 379     | const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

從上圖我們呢可以很清楚看到是遵循 從左往右依次匹配原則 使用了聯合索引且使用的是其中的 name + age 索引,沒有其他索引;看下key_len長度爲(單位字節):

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mn>379</mn><mo>=</mo><mn>125</mn><mo>∗</mo><mn>3</mn><mo>+</mo><mn>2</mn><mo>+</mo><mn>2</mn><mo stretchy="false">(</mo><mi>s</mi><mi>m</mi><mi>a</mi><mi>l</mi><mi>l</mi><mi>i</mi><mi>n</mi><mi>t</mi><mtext>長度</mtext><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">379 = 125*3 + 2 + 2(smallint長度)</annotation></semantics></math>379=125∗3+2+2(smallint長度)

剛剛好是 name + age 索引長度值。

③ 依次匹配 name_age_sex

mysql> explain select * from user where `user_name` = "李阿沐" and `user_age` = 26 and `user_sex` = 1;
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys    | key              | key_len | ref               | rows | filtered | Extra |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | user  | NULL       | ref  | idx_name_age_sex | idx_name_age_sex | 380     | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+------------------+------------------+---------+-------------------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

從上圖我們呢可以很清楚看到是遵循 從左往右依次匹配原則 使用了聯合索引且使用的是其中的 name + age 索引,沒有其他索引;看下key_len長度爲(單位字節):

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mn>380</mn><mo>=</mo><mn>125</mn><mo>∗</mo><mn>3</mn><mo>+</mo><mn>2</mn><mo>+</mo><mn>2</mn><mo stretchy="false">(</mo><mi>s</mi><mi>m</mi><mi>a</mi><mi>l</mi><mi>l</mi><mi>i</mi><mi>n</mi><mi>t</mi><mtext>長度</mtext><mo stretchy="false">)</mo><mo>+</mo><mn>1</mn><mo stretchy="false">(</mo><mi>t</mi><mi>i</mi><mi>n</mi><mi>y</mi><mi>i</mi><mi>n</mi><mi>t</mi><mtext>長度</mtext><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">380 = 125*3 + 2 + 2(smallint長度) + 1(tinyint長度)</annotation></semantics></math>380=125∗3+2+2(smallint長度)+1(tinyint長度)

剛剛好是 name + age + age 索引長度值。

④ 若不是依次匹配

mysql> explain select * from user where `user_age` = 26;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |    25.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

由於上面都沒有從最左邊開始匹配,所以沒有用到聯合索引,使用的都是index全索引掃描。

⑤ 補充下 索引長度計算方式

- 1.所有的索引字段,如果沒有設置not null,則需要加一個字節 

- 2.定長字段,int佔四個字節、date佔三個字節、char(n)佔n個字符

- 3.對於變成字段varchar(n),則有n個字符+兩個字節

- 4.字符集不同則一個字符佔用的字節數也不同;
    utf8mb4 - 1字符佔用4字節
    utf8    - 1字符佔用3字節
    gbk     - 1字符佔用2字節
    latin1  - 1字符佔用1字節

這樣是不是很清楚通過索引長度看出到底使用了那幾個索引:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>k</mi><mi>e</mi><msub><mi>y</mi><mi>l</mi></msub><mi>e</mi><mi>n</mi><mo>=</mo><mn>125</mn><mo stretchy="false">(</mo><mi>v</mi><mi>a</mi><mi>r</mi><mi>c</mi><mi>h</mi><mi>a</mi><mi>r</mi><mtext>類型</mtext><mn>3</mn><mtext>字節</mtext><mo stretchy="false">)</mo><mo>∗</mo><mn>3</mn><mo>+</mo><mn>2</mn><mo stretchy="false">(</mo><mtext>額外加</mtext><mn>2</mn><mtext>字節</mtext><mo stretchy="false">)</mo><mo>+</mo><mn>2</mn><mo stretchy="false">(</mo><mi>s</mi><mi>m</mi><mi>a</mi><mi>l</mi><mi>l</mi><mi>i</mi><mi>n</mi><mi>t</mi><mtext>類型</mtext><mn>2</mn><mtext>字節</mtext><mo stretchy="false">)</mo><mo>+</mo><mn>1</mn><mo stretchy="false">(</mo><mi>t</mi><mi>i</mi><mi>n</mi><mi>y</mi><mi>i</mi><mi>n</mi><mi>t</mi><mtext>類型</mtext><mn>1</mn><mtext>個字節</mtext><mo stretchy="false">)</mo><mo>=</mo><mn>380</mn><mo stretchy="false">(</mo><mtext>字節</mtext><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">key_len = 125(varchar類型3字節)*3 + 2(額外加2字節) + 2(smallint類型2字節) + 1(tinyint類型1個字節) = 380(字節)</annotation></semantics></math>keylen=125(varchar類型3字節)∗3+2(額外加2字節)+2(smallint類型2字節)+1(tinyint類型1個字節)=380(字節)

匹配列前綴

我們先看下下面三個sql語句最終的執行結果:

① 前綴匹配

mysql> explain select * from user where `user_name` like '李%';
+----+-------------+-------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys    | key              | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | idx_name_age_sex | idx_name_age_sex | 377     | NULL |    3 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+------------------+------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

② 後綴匹配

mysql> explain select * from user where `user_name` like '%李';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |    25.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

③ 中綴匹配

mysql> explain select * from user where `user_name` like '%李%';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | user  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    4 |    25.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

從上面執行結果可以看出:第一個前綴已經排好序的,所以走索引查詢;第二個第三個則是全表掃描查詢

注意:若列是字符串則比較規則是這樣的;先比較字符串第一個字符,若相同則繼續比較第二個字符,以此類推下去。

匹配範圍值
## 最左查詢 這裏是新增了一列聯合索引 刪除之前的那個 便於測試
mysql> explain select * from user where `user_age` > 1 and `user_age` < 30;
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key         | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | idx_age_sex   | idx_age_sex | 2       | NULL |    3 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

## 在user_age>1 and user_age < 30的範圍中,sex是有序的,所以使用的是rang範圍查詢
mysql> explain select * from user where `user_age` > 1 and `user_age` < 30 and `user_sex` > 0;
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys | key         | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | idx_age_sex   | idx_age_sex | 2       | NULL |    3 |    33.33 | Using index condition |
+----+-------------+-------+------------+-------+---------------+-------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

結論:多個列進行範圍查詢時,只有最左邊列查詢時才使用了索引;若在 1<age<20 範圍中sex是有序的,則使用的是rang範圍查詢;否則無序只能等 age 查詢到記錄後 通過 sex > 0 逐條過濾。

精確匹配某一列並範圍匹配另外一列
## 如果左邊的列是精確查找的,右邊的列可以進行範圍查找
mysql> explain select * from user where `user_name` = "李阿沐" and `user_age` < 29;
+----+-------------+-------+------------+-------+------------------------------+------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type  | possible_keys                | key              | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+-------+------------+-------+------------------------------+------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | user  | NULL       | range | idx_name_age_sex,idx_age_sex | idx_name_age_sex | 379     | NULL |    1 |   100.00 | Using index condition |
+----+-------------+-------+------------+-------+------------------------------+------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.01 sec)

通過上面可以看出,左列值精確查找,左邊值是有序的進行範圍查找會走聯合索引。當然可以通過key_len長度可以看出來:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>k</mi><mi>e</mi><msub><mi>y</mi><mi>l</mi></msub><mi>e</mi><mi>n</mi><mo>=</mo><mn>125</mn><mo>∗</mo><mn>3</mn><mo>+</mo><mn>2</mn><mo>+</mo><mn>2</mn><mo>=</mo><mn>377</mn><mo>+</mo><mn>2</mn><mo>=</mo><mn>399</mn></mrow><annotation encoding="application/x-tex">key_len = 125*3 + 2 + 2 = 377 + 2 = 399</annotation></semantics></math>keylen=125∗3+2+2=377+2=399

回表

什麼是回表查詢:簡單來說就是查詢時獲取的列有大量的非索引列,這個時候根據主鍵索引樹去表中知道相關列的值信息,而這個操作就叫做 回表

我們看下下面兩個sql語句:

## 無需回表,因爲select列全部是索引列

select user_id, user_name from user where `user_name` = "李阿沐";

或者

select * from user where `user_id` = 1;
不回表原因:根據主鍵ID查詢,只需要在主鍵b+tree上搜索數據即可。

## 需要回表,因爲select列出現非索引列,需要根據主鍵索引到表中查詢信息;實際上使用了兩次索引查詢

select user_id, user_name, user_pwd from user where `user_name` = "李阿沐";

爲什麼要儘量減少回表操作,尤其是當表的數據量越來越大的時候?

例如:現在我們通過 idx_name_age 索引掃描到了 10w 條數據,通過索引查詢到主鍵索引在回表去查詢相關列信息;mysql會認爲每一次的回表都需要一次單獨的 I/O 操作成本.

索引查詢成本
  • CPU操作成本

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>r</mi><mi>e</mi><mi>s</mi><mi>u</mi><mi>l</mi><mi>t</mi><mo>=</mo><mn>100000</mn><mo>∗</mo><mn>0.2</mn><mo>=</mo><mn>20000</mn></mrow><annotation encoding="application/x-tex">result = 100000 * 0.2 = 20000</annotation></semantics></math>result=100000∗0.2=20000

  • I/O操作成本

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>r</mi><mi>e</mi><mi>s</mi><mi>u</mi><mi>l</mi><mi>t</mi><mo>=</mo><mn>100000</mn><mo>∗</mo><mn>1</mn><mo>=</mo><mn>100000</mn></mrow><annotation encoding="application/x-tex">result = 100000 * 1 = 100000</annotation></semantics></math>result=100000∗1=100000

  • 兩次操作的成本數

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>r</mi><mi>e</mi><mi>s</mi><mi>u</mi><mi>l</mi><mi>t</mi><mo>=</mo><mn>20000</mn><mo>+</mo><mn>100000</mn><mo>=</mo><mn>120000</mn></mrow><annotation encoding="application/x-tex">result = 20000 + 100000 = 120000</annotation></semantics></math>result=20000+100000=120000

全表掃描查詢成本
mysql> SELECT TABLE_NAME,DATA_LENGTH,INDEX_LENGTH,(DATA_LENGTH+INDEX_LENGTH) as length,TABLE_ROWS,concat(round((DATA_LENGTH+INDEX_LENGTH)/1024/1024,3), 'MB') as total_size FROM information_schema.TABLES WHERE TABLE_SCHEMA='test' order by length desc;
+------------+-------------+--------------+--------+------------+------------+
| TABLE_NAME | DATA_LENGTH | INDEX_LENGTH | length | TABLE_ROWS | total_size |
+------------+-------------+--------------+--------+------------+------------+
| test       |       16384 |        16384 |  32768 |          3 | 0.031MB    |
| user       |       16384 |        16384 |  32768 |          4 | 0.031MB    |
+------------+-------------+--------------+--------+------------+------------+
2 rows in set (0.01 sec)

通過 命令 查看全表的字節數 user表 Data_length = 16384 

從上面可以計算出全表掃描需要讀取多少記錄頁:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>p</mi><mi>a</mi><mi>g</mi><mi>e</mi><mo>=</mo><mn>16384</mn><mi mathvariant="normal">/</mi><mn>1024</mn><mi mathvariant="normal">/</mi><mn>16</mn><mo>=</mo><mn>1</mn></mrow><annotation encoding="application/x-tex">page = 16384 / 1024 / 16 = 1</annotation></semantics></math>page=16384/1024/16=1

可以看出我本地的記錄頁太少了;假設本地全表有 16384000 個字節,表內有 300000 條記錄;那麼我們需要掃描1000個記錄頁;現在算下成本:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mi>r</mi><mi>e</mi><mi>s</mi><mi>u</mi><mi>l</mi><mi>t</mi><mo>=</mo><mi>C</mi><mi>P</mi><mi>U</mi><mtext>成本</mtext><mo stretchy="false">(</mo><mn>300000</mn><mo>∗</mo><mn>0.2</mn><mo>=</mo><mn>60000</mn><mo stretchy="false">)</mo><mo>+</mo><mi>I</mi><mi mathvariant="normal">/</mi><mi>O</mi><mtext>成本</mtext><mo stretchy="false">(</mo><mn>1000</mn><mo>∗</mo><mn>1</mn><mo>=</mo><mn>1000</mn><mo stretchy="false">)</mo><mo>=</mo><mn>61000</mn></mrow><annotation encoding="application/x-tex">result = CPU成本(3000000.2=60000) + I/O成本(10001 = 1000) = 61000</annotation></semantics></math>result=CPU成本(300000∗0.2=60000)+I/O成本(1000∗1=1000)=61000

這樣看起來有時候會出現 回表操作查詢比全表掃描更消耗性能;所以我們在做業務需求時,先預估表的量,再合理的構建索引,並且通過explain解析看下具體性能。

查詢成本組成與計算
  • CPU成本
  • I/O成本
  • 要清楚mysql規定讀取一個頁面的成本是 1.0 並不是隨意寫的;讀取和檢測記錄是否使用索引的成本是 0.2
  • 注意:不管記錄是否檢測滿足索引條件,成本都是 0.2;記住就行了

所以上面大家可以看清楚回表的計算邏輯了,如下:

<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><semantics><mrow><mtext>回表成本</mtext><mo>=</mo><mi>C</mi><mi>P</mi><mi>U</mi><mtext>成本</mtext><mo stretchy="false">(</mo><mtext>記錄數</mtext><mo>∗</mo><mn>0.2</mn><mo stretchy="false">)</mo><mo>+</mo><mi>I</mi><mi mathvariant="normal">/</mi><mi>O</mi><mtext>成本</mtext><mo stretchy="false">(</mo><mtext>記錄頁數</mtext><mo>∗</mo><mn>1</mn><mo stretchy="false">)</mo></mrow><annotation encoding="application/x-tex">回表成本 = CPU成本(記錄數 * 0.2) + I/O成本(記錄頁數 * 1)</annotation></semantics></math>回表成本=CPU成本(記錄數∗0.2)+I/O成本(記錄頁數∗1)

小小總結

使用聚集索引(主鍵或第一個唯一索引)就不會回表,普通索引就會回表。

儘量減少回表查詢降低查詢成本:① 能用主鍵索引或唯一索引的就不用輔助索引;② 可以使用覆蓋索引

作者:我是阿沐

鏈接:https://juejin.cn/post/6976421675260706852
來源:掘金

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章