一、目錄
- 理論基礎
- 優化實踐
- 常見坑
二、基礎知識
2.1 Mysql explain使用
2.2 聚合索引和非聚合索引
MySQL索引可以分爲兩類:聚合索引和非聚合索引,其中聚合索引也被稱爲一級索引,非聚合索引也被稱爲輔助索引、二級索引。
兩種索引相同點:內部都是 B+ 樹。
不同點
聚集索引的葉子節點存放是一整行的信息。
聚集索引一個表只能有一個,而非聚集索引一個表可以存在多個。
聚集索引存儲記錄是物理上連續存在,而非聚集索引是邏輯上的連續,物理存儲並不連續。
聚集索引查詢數據速度快,插入數據速度慢;非聚集索引反之。 聚集索引範圍查詢快。
聚集索引:InnoDB 存儲引擎表是索引組織表,表種數據按照主鍵順序存放,而聚集索引就是按照每張表的主鍵構造一顆B+樹,同時葉子節點中存放的就是整張表的行記錄數據,也將聚集索引的葉子節點稱爲數據頁。 每張表只能擁有一個聚集索引。 查詢優化器傾向於採用聚集索引。
非聚集索引:葉子節點不包含記錄的全部數據。 葉子節點中索引行中還包含了一個書籤,用來告訴 InnoDB 存儲引擎在哪裏可以找到與索引相應的行數據。 這個書籤就是相應的行數據的聚集索引鍵。 可以有多個非聚集索引。 使用非聚集索引來尋找數據時,通過葉級別的指針獲得指向主鍵索引的主鍵,再通過主鍵索引找到一個完整的行記錄。
三、常規SQL優化
在優化實踐之前,我們先準備下實踐的數據,便於增加理解。
案例SQL
-- 初始化表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
`age` int(11) NOT NULL,
`nick` varchar(64) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `award` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1000001 DEFAULT CHARSET=utf8mb4;
CREATE TABLE `user_award_rela` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id_bigint` bigint(20) NOT NULL,
`user_id_int` int(11) NOT NULL,
`award_id_bigint` bigint(20) NOT NULL,
`award_id_int` int(11) NOT NULL,
`status` int(11) NOT NULL,
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_user_id_award_id_status` (`user_id_bigint`,`award_id_bigint`,`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB AUTO_INCREMENT=1000003 DEFAULT CHARSET=utf8mb4;
-- 初始化數據
DROP PROCEDURE if EXISTS `init_data`;
DELIMITER //
CREATE procedure init_data() -- 創建無參存儲過程
BEGIN
DECLARE i INT; -- 申明變量
SET i = 0; -- 變量賦值
WHILE i<1000000 DO -- 結束循環的條件: 當i大於閾值時跳出while循環
-- 初始化表數據語句
INSERT INTO `user` VALUES(i+1,LEFT(RAND() * 1000000,6),LEFT(RAND() * 100,2),LEFT(RAND() * 1000000,6));
INSERT INTO `award` VALUES(i+1,LEFT(RAND() * 1000000,6));
INSERT INTO user_award_rela values(i+1,i+1,i+1,i+1,i+1,i%2,DATE_ADD(now(),interval left(rand() * 1000,3) DAY));
SET i = i+1;
END WHILE; -- 結束while循環
END
//
CALL init_data(); -- 調用存儲過程
3.1 分頁優化
大家都知道MySQL在使用limit分頁時,會一次性查詢offset+page_size條數據,然後將offset數據丟棄,導致性能較差。
原因:
- MySQL在查詢數據時先判斷是否在緩存中,如果在緩存中直接返回數據
- 不在緩存數據,判斷使用哪個索引更好,優先使用聚合索引,其次是非聚合索引。
- 假設使用非聚合索引篩選數據,根據非聚合索引上記錄的聚合索引ID,再去查詢具體數據(offset+page_size條數據)。
- 丟棄offset條數據
- 返回結果
在實際應用中都是帶有where條件的,在瞭解過程後,接下來看下如何優化Limit分頁。
-- 常規sql
select * from `user` where name like '78%' limit 10000,10;
優化思路一:基於索引在MySQL存儲時有順序的特性,我們改造原SQL增加一個排序列(一般爲索引列),並用該列作爲篩選條件,每次查詢都用上一頁中排序列的最大/小值,這樣每次查詢的時候根據篩選條件就過濾掉之前頁的數據了,從而省去offset。
-- 第一頁
select * from `user` where name like '78%' and id > 0 order by id limit 10;
-- 第二頁
select * from `user` where name like '78%' and id > 824 order by id limit 10;
-- 第三頁
select * from `user` where name like '78%' and id > 1682 order by id limit 10;
此種方式明顯的缺點:
- 業務中經常有排序要求,且允許自定義排序列,導致一些場景無法適用。
- 侷限性較大,每次都需要依賴上一分頁是最大/小值作爲下一個分頁的數據,無法適用於直接跳轉到指定頁數場景。
- 改造較大,需要web -> service -> dao依次透傳傳入上一分頁參數。
接下來我們再看下優化思路二:基於join關聯時小表驅動大表、非聚合索引不包含全部數據但包含聚合索引的索引鍵兩個特點來優化。
select * from `user` u1
inner join (select id from `user` where name like '78%' limit 10000,10) u2 on u2.id = u1.id
可能看到上面SQL心裏會有疑問了,還是用的Limit啊,怎麼可能會快呢?
本質是利用非聚合索引上存儲着查詢列,不經過查詢索引鍵(聚合索引)回表的特性。具體執行流程如下:
- 關聯的子SQL語句的查詢僅查詢了ID、並且使用的是非聚合索引
- 非聚合索引上記錄了聚合索引索引鍵,MySQL雖然也會獲取offset+page_size條數據,但這些數據都是索引數據,無需回表(快的根本原因),執行速度還是很快的。
- 通過ID與原表自關聯查詢最終數據。
3.2 批量分組查詢最新一條數據
需求:在用戶列表中需要展示最近獲取的獎勵數據。
常規實現思路:分頁獲取用戶列表後,循環用戶ID獲取最新的獎勵ID,然後根據獎勵ID批量查詢獎勵數據。但這樣會執行大量SQL導致性能較差。
-- 省略分頁獲取有用戶列表SQL,獲取最新一條獎勵數據,互聯網公司中大多不允許使用join語句。
select award_id_bigint from user_award_rela where user_id_bigint = 102342 order by id desc limit 1;
-- 批量獲取獎勵數據
select * from award where id in (xx,xx);
由於常規實現有性能問題,那該如何優化呢? 思路:單個查詢修改爲批量,但是如何批量每個用戶最近一條數據呢?答案是使用group by分組,藉助max函數來實現;max(獎勵ID),獎勵ID最大的一定是最新的了。即得到下文SQL
-- 批量獲取每個用戶最新的獎勵數據
select user_id_bigint,max(award_id_bigint) award_id from user_award_rela where user_id_bigint in (XX,XX)
group by user_id_bigint;
根據獎勵ID批量查詢獎勵數據即可。
四、常見錯誤
4.1 字段類型不一致,查詢索引失效。
MySQL在數據類型不一致時,雖然也可以進行查詢,但是索引會失效,原因是MySQL會對每行數據類型做隱形轉換。
-- 無法使用索引語句
select * from user where name = 962848;
-- 正常使用索引語句
select * from user where name = '962848';
具體效果如下圖所示:
4.2 字符編碼不一致,關聯查詢索引失效。
該問題與4.1 字段類型不一致導致索引實現原理是一樣的,MySQL在關聯的時候,會對數據類型做隱形轉換。
4.3 索引字段做運算,索引失效。
索引的大多都是基於B+Tree結構實現,可以減少IO操作次數便於快速返回查詢結果,但是如何做數據運算,B+Tree結構的存儲方式無法發揮出來,只能逐行數據進行運算纔行,從而導致速度特別慢。
-- 無法使用索引語句
select * from user_award_rela where date_format(create_time,'%Y-%m-%d') = '2023-07-18' limit 10;
-- 正常使用索引語句
select * from user_award_rela where create_time >= str_to_date('2023-07-18','%Y-%m-%d') and create_time < str_to_date('2023-07-19','%Y-%m-%d') limit 10;
4.4 delete使用in&子查詢刪除,索引失效
這裏說的delete 使用in 並不是 delete from tablename where column in (XX),而是指下面這種情況。
-- 無法使用索引語句
delete from user_award_rela where id in (
select * from (select id from user_award_rela where id = 1000000) a
);
-- 正常使用索引語句
delete r1 from user_award_rela r1
inner join user_award_rela r2 on r1.id = r2.id
where r1.id = 1000000;
原因:MySQL對select in子查詢做了優化,把子查詢改成join的方式,所以可以走索引。但是很遺憾,對於delete in子查詢,MySQL卻沒有對它做這個優化。
4.5 全文索引
不建議在MySQL innodb中使用全文索引。
- 查詢速度不穩定,有時候一個查詢及時毫秒就返回了,有時候性能還沒
like "%value%"
快 - 在使用全文索引時,很多小夥伴以爲執行完創建全文索引SQL就完事了,其實不然;可能會遇到明明有數據爲何查詢不到呢,可能就是忘記對數據做分詞了。Mysql全文索引默認使用空格分詞,對於中文這種沒有明顯分隔符的語言,就需要進行中文分詞。可使用插件或開發自己的中文分詞函數。比如使用IK Analyzer插件。
- MySQL全文索引對分詞的長度是有限制的
- 導致內存升高、進程異常退出;此部分在2021年期間使用時發現,但具體原因不清楚。