天天看點

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

作者:閃念基因

前言

資料庫是“按照資料結構來組織、存儲和管理資料的倉庫”,承接核心的業務資料,在業務中占有非常重要的地位。日常測試工作中,我們通常會将重點放在業務邏輯準确性的驗證中,進而忽略掉資料庫自身的一些問題。本文将以關系型資料庫MySQL為例,通過講解一條查詢SQL的工作過程來了解MySQL的核心子產品、執行流程以及索引的組織形式,進而從典型的資料庫慢查詢和其他踩坑經驗兩個方面分析日常測試中遇到的問題,并提供解決方案。

一條SQL查詢語句是怎麼執行的?

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

衆所周知,MySQL 可以分為 Server 層和存儲引擎層兩部分。

Server 層包括連接配接器、查詢緩存、分析器、優化器、執行器等,涵蓋 MySQL 的大多數核心服務功能,以及所有的内置函數(如日期、時間、數學和加密函數等),所有跨存儲引擎的功能都在這一層實作,比如存儲過程、觸發器、視圖等。

存儲引擎層則負責資料的存儲和提取。

接下來思考一個問題,輸入一條語句到傳回結果中間經過了哪些過程呢?

一條查詢SQL的執行過程如下圖所示:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

連接配接器:你會先連接配接到這個資料庫上,這時候接待你的就是連接配接器。連接配接器負責跟用戶端建立連接配接、擷取權限、維持和管理連接配接。

查詢緩存:連接配接建立完成後,執行邏輯就會來到查詢緩存。如果開啟來了查詢緩存,之前執行過的語句及其結果可能會以 key-value 對的形式,被直接緩存在記憶體中。如果命中,value直接傳回給用戶端。沒有命中,則繼續。執行完成後,執行結果會被存入查詢緩存中。

分析器:如果沒有命中查詢緩存,進入分析器,通過詞法分析+文法分析對 SQL 語句做解析,文法錯誤是從這個環節報出的。

優化器:優化器是為了提升SQL的執行性能。經過了分析器,MySQL 就知道要做什麼了。在開始執行之前,還要先經過優化器的處理。在表裡面有多個索引的時候,決定使用哪個索引;或者在一個語句有多表關聯(join)的時候,決定各個表的連接配接順序。

執行器:優化器優化後進入了執行器階段,執行器跟存儲層進行互動,取得執行結果并傳回

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

什麼是索引?

從上面的查詢流程可以看出,在優化器環節,會決定用什麼索引來加速SQL的執行性能,那我們再來看下MySQL的索引。

簡單來講,索引的出現其實就是為了提高資料查詢的效率,就像書的目錄一樣,如果你想快速找到其中的某一個知識點,在不借助目錄的情況下,那我估計你可得找一會兒。同樣,對于資料庫的表而言,索引其實就是它的“目錄”。

以InnoDB引擎為例,表都是根據主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。InnoDB 使用了 B+ 樹索引模型,是以資料都是存儲在 B+ 樹中的。每一個索引在 InnoDB 裡面對應一棵 B+ 樹。

索引主要包括主鍵索引和和非主鍵索引,通過一個例子看看它們的差別。

假設,我們有一個主鍵列為 ID 的表,表中有字段 k,并且在 k 上有索引。

M1

表中 R1~R5 的 (id,k) 值分别為 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹的示例示意圖如下。

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

從圖中不難看出,根據葉子節點的内容,索引類型分為主鍵索引和非主鍵索引。

●主鍵索引的葉子節點存的是整行資料。在 InnoDB 裡,主鍵索引也被稱為聚簇索引(clustered index)。

●非主鍵索引的葉子節點内容是主鍵的值。在 InnoDB 裡,非主鍵索引也被稱為二級索引(secondary index)。

基于上面的索引結構說明,我們來讨論一個問題:基于主鍵索引和普通索引的查詢有什麼差別?

如果語句是 select * from T where ID=500,即主鍵查詢方式,則隻需要搜尋 ID 這棵 B+ 樹;

如果語句是 select * from T where k=5,即普通索引查詢方式,則需要先搜尋 k 索引樹,得到 ID 的值為 500,再到 ID 索引樹搜尋一次。這個過程稱為回表。也就是說,基于非主鍵索引的查詢需要多掃描一棵索引樹。是以,我們在應用中應該盡量使用主鍵查詢。

當然,也有走了普通索引卻不需要回表的例子,上面的SQL把取全部字段改成隻取列k,因為k的值在 k 索引樹可以取到,是以不需要回表,這種通常稱為使用了覆寫索引。

慢查詢

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

什麼是慢查詢?

上面了解了SQL查詢語句的執行過程,以及索引,那麼什麼是慢查詢呢?

慢查詢定義:慢查詢就是那些執行慢的SQL語句,包括crud(建立(Create)、更新(Update)、讀取(Read)和删除(Delete)操作),一般是查詢,是以稱為慢查詢。

慢查詢的出現會給服務帶來非常不好的影響,輕則影響使用者體驗,服務響應變慢、無傳回等,重則會出現因慢查詢占用大量記憶體導緻MySQL服務直接挂掉,進而導緻整個系統癱瘓;是以,我們應該重視慢查詢問題,盡早發現,盡早解決。

産生慢查詢的大部分原因是未命中索引 ,也會有少部分因為網絡抖動或者刷髒頁問題 ,本文主要讨論索引未命中的場景

發現慢查詢

為了優化慢查詢的問題,先要發現慢查詢,主要有以下兩種手段:

間接發現:添加接口響應時間的監控

慢查詢的出現會讓系統響應變慢,比如接口傳回逾時,可以通過添加接口監控來監測,出現逾時的場景就去排查是否存在慢查詢。

但是這種方式不夠直接,因為接口響應慢的因素有很多,可能是網絡原因、測試伺服器資源等問題,無法快速定位是否是慢查詢;

主動發現:監控慢查詢日志

資料庫開啟慢查詢日志(通常會存儲在檔案中,可自行設定),監控慢查詢日志,如果新增慢查詢便立即發送通知。這種方式我個人比較推崇,可以第一時間發現并着手分析優化。

分析慢查詢

發現慢查詢以後,需要着手優化,通常有以下幾種情況:

情況1:通過explain你可能會發現,SQL壓根沒走任何索引,而且現在表中的資料量巨大無比。

解決:建合适索引

情況2:通過explain檢視SQL執行計劃中的key字段。如果發現優化器選擇的Key和你預期的Key不一樣。那顯然是優化器選錯了索引。

解決:最快的解決方案就是:force index ,強制指定索引,當然還有其他方案,後面實踐會展開說明。當你發現即使你使用了force index之後,查詢依然很慢。這就意味着,你需要設計一個更好的索引。

此外:也可能是資料量太大了,即使走了索引依然超過了門檻值。

這種情況其實挺糟糕的,DBA同學能做的依然隻能是kill這些出事的SQL。最好的解決方案其實是分表,比如将大表拆分成128張小表。如果來不急做分表,可能這條SQL面臨被下線的風險。

分析優化實踐

接下面來看兩個分析優化的例子。

分析優化1-正确添加索引:

資料庫版本:5.7.13

我們有個日志表,資料的量級200w+

目标比較簡單,查詢某個腳本的曆史執行日志,通過腳本id(script_id)去查詢,優化之前的SQL執行情況,時間大概1.8s,如下圖所示:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

檢視執行計劃:

EXPLAIN SELECT * FROM `prepare_data_log` WHERE script_id = 24;

結果如下圖所示:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

從圖中可以看出,type列的值是ALL,走的全表掃描;key字段是NULL,沒有使用任何索引;

優化方案是加索引:在script_id上添加二級索引:ALTER TABLE prepare_data_log ADD INDEX script_id (script_id);

加索引之後,執行時間從1.8s->0s,詳見下圖,可見,合理的添加索引可以顯著的提升查詢性能。

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

再看下執行計劃:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

從圖中可以看出,type列的值是ref,表示走了索引;key字段是索引的名字script_id,使用了剛建立的索引,符合預期;

分析優化2-選擇正确的索引

上面的例子添加了索引,執行器也選擇了期望的索引,那MySQL會一直選擇期望的索引麼?答案是:不一定

看一個例子:資料表資料有10W,包含id(自增主鍵)、a、b、c三列,其中a=b=c=id,a和b兩列都有二級索引。查詢語句如下:

M2

從條件上看,這個查詢沒有符合條件的記錄,是以會傳回空集合。執行之前,可以先設想一下,如果你來選擇索引,會選擇哪一個呢?為了便于分析,我們先來看一下 a、b 這兩個索引的結構圖。

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

如果使用索引 a 進行查詢,那麼就是掃描索引 a 的前 1000 個值,然後取到對應的 id,再到主鍵索引上去查出每一行,然後根據字段 b 來過濾。顯然這樣需要掃描 1000 行。

如果使用索引 b 進行查詢,那麼就是掃描索引 b 的最後 50001 個值,與上面的執行過程相同,也是需要回到主鍵索引上取值再判斷,是以需要掃描 50001 行。是以你一定會想,如果使用索引 a 的話,執行速度明顯會快很多。那麼,執行器會按照我們期望的選擇麼?

下圖是執行 explain 的結果:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

可以看到,傳回結果中 key 字段顯示,這次優化器選擇了索引 b,而 rows 字段顯示需要掃描的行數是 50210。從這個結果中可見 MySQL “選錯了”索引。

在日常工作中也遇到過幾次選錯索引的例子,通常開發的做法是采用 force index 強行選擇一個索引。

方案一:force index 強行選擇一個索引。剛開始分析時,我們認為選擇索引 a 會更好。現在,我們就來看看執行效果:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

從使用不同索引的語句執行耗時可以看到,原本語句需要執行 0.06 秒,而當你使用 force index(a) 的時候,隻用了 0.01 秒,比優化器的選擇快了 6倍。也就是說,優化器沒有選擇正确的索引,force index 起到了“矯正”的作用。

這個方案并不優雅,一來這麼寫不優美,二來如果索引改了名字,這個語句也得改,顯得很麻煩。而且如果以後遷移到别的資料庫的話,這個文法還可能會不相容。是以,資料庫的問題最好還是在資料庫内部來解決。

方案二:修改SQL,引導走期望索引。既然優化器放棄了使用索引 a,說明 a 還不夠合适,是以第二種方法就是,我們可以考慮修改語句,引導 MySQL 使用我們期望的索引。比如,在這個例子裡,顯然把“order by b limit 1” 改成 “order by b,a limit 1” ,語義的邏輯是相同的。我們來看看改之後的效果:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

之前優化器選擇使用索引 b,是因為它認為使用索引 b 可以避免排序(b 本身是索引,已經是有序的了,如果選擇索引 b 的話,不需要再做排序,隻需要周遊),是以即使掃描行數多,也判定為代價更小。現在 order by b,a 這種寫法,要求按照 b,a 排序,就意味着使用這兩個索引都需要排序。是以,掃描行數成了影響決策的主要條件,于是此時優化器選了隻需要掃描 1000 行的索引 a。

當然,這種修改并不是通用的優化手段,隻是剛好在這個語句裡面有 limit 1,是以如果有滿足條件的記錄, order by b limit 1 和 order by b,a limit 1 都會傳回 b 是最小的那一行,邏輯上一緻,才可以這麼做。

方案三:在有些場景下,我們可以建立一個更合适的索引,來提供給優化器做選擇,或删掉誤用的索引。

在日常工作中遇到慢查詢,可以根據實際的應用場景靈活的選擇合适的解決辦法。當然,優化的前提是先要清楚影響優化器選擇索引的因素。

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

其他踩坑經驗

除了慢查詢之外,我們還常遇到一些因不了解MySQL配置而導緻的bug。

group_concat截斷問題

在MySQL中,有個函數叫“group_concat”,平常使用可能發現不了問題,在處理大資料的時候,會發現内容被截取了。如下圖例子,當需要拼接的資料量大出現了因截斷導緻資料缺失的問題:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

可以通過指令查group_concat允許的最大長度,如下圖所示,預設長度是1024:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

group_concat被截斷可能會導緻資料損失和錯誤,是以在進行字元串拼接時應格外注意。建議使用其他字元串處理函數(例如SUBSTRING)和适當的資料類型(例如TEXT和BLOB)來避免這種情況的發生。

MySQL資料類型溢出

MySQL中某些字段出現溢出時并不會報錯,而是截斷處理,導緻資料達到最大範圍時會發生溢出,進而導緻資料錯誤或丢失。遇到過INT類型溢出的case,如圖所示:

QA視角的MySQL探險之旅:穿越慢查詢沼澤,揭示實戰避坑指南

為了避免和解決MySQL 類型溢出的發生,如下建議可參考:

1.使用更大範圍的資料類型,如INT換成BIGINT,可以存儲更大的整數範圍。

2.對于存儲非負整數的字段可以使用U。

3.使用程式代碼對輸入的數字進行有效性檢查,避免使用者輸入超出定義範圍的數字。

以上就是MySQL測試方面的一些經驗之談,主要針對慢查詢和一些不易察覺的參數設定引發的相關問題,希望能給你在日常工作中有所指引~

來源:微信公衆号:360品質效能

出處:https://mp.weixin.qq.com/s/Fjnmnle5pGKMDexTFTgTZg

繼續閱讀