我們的業務服務隨著功能規模擴大,使用者量擴增,流量的不斷的增長,經常會遇到一個問題,就是資料儲存服務響應變慢。
導致資料庫服務變慢的誘因很多,而RD最重要的工作之一就是找到問題並解決問題。
下面以MySQL為例子,我們從幾個角度分析可能產生原因,並討論解決的方案。
開啟SlowLog,預設是關閉的,由引數slow_query_log決定,在MySQL命令終端中輸入下面的命令:
# 是否開啟,這邊為開啟,預設情況下是off
set global slow_query_log=on;
# 設定慢查詢閾值,單位是 s,預設為10s,這邊的意思是查詢耗時超過0.5s,便會記錄到慢查詢紀錄檔裡面
set global long_query_time=0.5;
# 確定慢查詢紀錄檔的檔名和路徑
mysql> show global variables like 'slow_query_log_file';
+---------------------+-------------------------------------------------------+
| Variable_name | Value |
+---------------------+-------------------------------------------------------+
| slow_query_log_file | /usr/local/mysql/data/MacintoshdeMacBook-Pro-slow.log |
+---------------------+-------------------------------------------------------+
1 row in set (0.00 sec)
# 檢查慢查詢的詳細指標,可以看到下面 slow_query_log = ON,long_query_time = 0.5 ,都是因為我們調整過的
mysql> show global variables like '%quer%';
+----------------------------------------+-------------------------------------------------------+
| Variable_name | Value |
+----------------------------------------+-------------------------------------------------------+
| binlog_rows_query_log_events | OFF |
| ft_query_expansion_limit | 20 |
| have_query_cache | NO |
| log_queries_not_using_indexes | OFF |
| log_throttle_queries_not_using_indexes | 0 |
| long_query_time | 0.500000 |
| query_alloc_block_size | 8192 |
| query_prealloc_size | 8192 |
| slow_query_log | ON |
| slow_query_log_file | /usr/local/mysql/data/MacintoshdeMacBook-Pro-slow.log |
+----------------------------------------+-------------------------------------------------------+
10 rows in set (0.01 sec)
設定好之後,就會按照閾值預設把慢查詢紀錄檔收集下來,可以到對應的目錄下分析具體的慢請求原因。
很多時候我們在評審RD同學程式碼和SQL指令碼的時候,上下文和使用環境不瞭解,不能做出很準確的判斷。
這時候使用Explain分析SQL的執行計劃就顯得非常有用,拿到具體環境中Run一下就能看出很多問題。
舉個例子:
模擬一個千萬級別的僱員表,我們在沒有做索引的欄位上做一下查詢看看,在500W資料中查詢一個名叫LsHfFJA的員工,消耗 2.239S ,獲取到一條id為4582071的資料。
再看看他的執行計劃,掃描了4952492 條資料才找到該行資料:
mysql> explain select * from emp where empname='LsHfFJA';
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
| 1 | SIMPLE | emp | ALL | NULL | NULL | NULL | NULL | 4952492 | Using where |
+----+-------------+-------+------+---------------+------+---------+------+---------+-------------+
1 row in set
這就是無索引或者索引不合理的結果,這個時候我們就可以根據實際情況進行查詢優化了。
比較核心要關注的欄位一般有 select_type、type、possible_keys、key、rows、Extra等
我們來一個個說明:
select_type的值 | 解釋 |
---|---|
SIMPLE | 簡單查詢(不使用關聯查詢或子查詢) |
PRIMARY | 如果包含關聯查詢或者子查詢,則最外層的查詢部分標記primary |
UNION | 聯合查詢(UNION)中第二個及後面的查詢 |
DEPENDENT UNION | UNION中的第二個或後面的SELECT語句,取決於外面的查詢 |
UNION RESULT | UNION的結果,union語句中第二個select開始後面所有select |
SUBQUERY | 字查詢中的第一個擦訊 |
DEPENDENT SUBQUERY | 子查詢中的第一個查詢,並且依賴外部查詢 |
DERIVED | 派生表的SELECT, FROM子句的子查詢 |
MATERIALIZED | 被物化的子查詢 |
UNCACHEABLE SUBQUERY | 一個子查詢的結果不能被快取,必須重新評估外連線的第一行 |
type的值 | 解釋 |
---|---|
system | 查詢物件表只有一行資料,且只能用於MyISAM和Memory引擎的表,這是最好的情況 |
const | 基於主鍵或唯一索引查詢,最多返回一條結果 |
eq_ref | 類似ref,區別就在使用的索引是唯一索引,對於每個索引鍵值,表中只有一條記錄匹配,簡單來說,就是多表連線中使用primary key或者 unique key作為關聯條件 |
ref | 表示上述表的連線匹配條件,即哪些列或常數被用於查詢索引列上的值 |
fulltext | 全文檢索 |
ref_or_null | 表連線型別是ref,但進行掃描的索引列中可能包含NULL值 |
index_merge | 利用多個索引 |
unique_subquery | 子查詢中使用唯一索引 |
index_subquery | 子查詢中使用普通索引 |
range | 只檢索給定範圍的行,使用一個索引來選擇行 |
index | Full Index Scan,index與ALL區別為index型別只遍歷索引樹 |
ALL | Full Table Scan, MySQL將遍歷全表以找到匹配的行 |
表示MySQL能使用哪個索引在表中找到記錄,查詢涉及到的欄位上若存在索引,則該索引將被列出,但不一定被查詢使用。這個趨向於指導性作用。
顯示MySQL在查詢中實際使用的索引,若沒有使用索引,顯示為NULL
★ 以下是我們團隊的准入規範,也是CodeReview 標準。
-- 如下,這種查詢會導致掃描表:
select a,b,c from t_name where a like '%name';
-- 可以使用%模糊字尾查詢如:
select a,b from t_name where a like 'name%';
-- 如
select a,b,c from t1 limit 10000,20;
-- 優化為:
select a,b,c from t1 where id>10000 limit 20;
建議參考筆者這篇《構建高效能索引(策略篇)》,比較完整
索引必須建立在索引選擇性(區分度)較高的列上,選擇性的計算方式為:
selecttivity = count(distinct c_name)/count(*) ;
如果區分度結果小於0.2,則不建議在此列上建立索引,否則大概率會拖慢SQL執行
對於確定需要組成組合索引的多個欄位,設計時建議將選擇性高的欄位靠前放。使用時,組合索引的首欄位,必須在where條件中,且需要按照最左字首規則去匹配。
正確理解和計算索引欄位的區分度,文中有計算規則,區分度高的索引,可以快速得定位資料,區分度太低,無法有效的利用索引,可能需要掃描大量資料頁,和不使用索引沒什麼差別。
禁止使用外來鍵,可以在程式級別來約束完整性
varchar、text型別欄位如果需要建立索引,必須使用字首索引。
字首索引計算公式如下,calcul_len 是數位,長度為1 ~ c_name欄位的最長值,可以逐一比較,對比區分度最高的出來
正確理解和計算字首索引的欄位長度,文中有判斷規則,合適的長度要保證高的區分度和最恰當的索引儲存容量,只有達到最佳狀態,才是保證高效率的索引。
select count(distinct left(`c_name`,calcul_len))/count(*) from t_name;
-- 如果建立(depno,empname,job)順序的索引,job是用不到索引的。
depno=1 and empname>'' and job=1
PRIMARYKEY (`id`),
UNIQUEKEY `uni_brand_define` (`app_id`,`define_id`)
實際場景中,建議程式碼交叉評審,當你同事業務程式碼中的檢索語句如下的時候,應建議調整:
select brand_id,brand_name from ds_brand_system where status=? and define_id=? and app_id=?
建議改成如下:
select brand_id,brand_name from ds_brand_system where app_id=? and define_id=? and status=?
雖然說 MySQL的查詢優化器會根據實際索引情況進行順序優化,所以這邊不做強制。但是同等條件下還是按照順序進行排列,比較清晰,並且節省查詢優化器的處理。
這邊僅僅是從查詢語句的角度進行分析,實際上快取服務變慢的可能性很多,不僅僅是慢查詢怎麼分析(Slow Log、Explain命令)。還應該全面的分析原因,並給出處理方案,如 分析SQL指令碼合理性、建立索引或優化索引、讀寫分離、垂直+水平分割區)、多讀少寫/冷資料 做快取、優化資料庫的鎖競爭、資料庫設定調優、硬體資源升級 等等,後面幾篇我們慢慢說。