此前整個公司生態開展安全生產的應用治理活動,以故障爲鏡,能夠守安全,這其中的慢sql治理是系統高可用治理的重要一環。慢sql的產生能夠從宏觀和微觀兩個角度去看:SQL產生的微觀方面的緣由包括:DB表數據量大,索引不合理,SQL語句調優等等。此外在高併發、高流量下,數據庫所在機器的負載load太高也會致使SQL總體執行時間過長,這時可能須要從機器和實例的分配,分佈式部署,分庫分表,讀寫分離等宏觀角度進行優化。結合倉儲技術部安全生產治理過程實踐和相關資料整理,本文主要從微觀角度對慢sql的發現、分析、解決三個step,逐漸闡明治理慢sql的系統化思路。html
本文閱讀時間10~15min左右。前端
首先須要知道如何發現慢sql,簡要介紹以下幾種方法:mysql
**集團生態統一提供idb數據庫管理平臺提供mysql監控,進入idb應用系統。選擇須要治理的數據庫,點擊【性能】菜單進入cloudDBA界面查看慢sql。idb中默認執行時間超過1s的SQL爲慢SQL。算法
針對**集團,能夠訪問菜鳥本身的慢sql解決方案sql
固然通常性地還能夠MySQL的慢查詢日誌是MySQL提供的一種日誌記錄,它用來記錄在MySQL中響應時間超過閥值的語句,具體指運行時間超過long_query_time值(默認值爲10)的SQL,則會被記錄到慢查詢日誌中。數據庫
本文對慢sql的發現不作詳細贅述,非本文重點,主要下面介紹如何分析和解決慢sql。後端
在發現找到慢sql後,就是要分析這條慢sql的前因後果了,能夠分別從sql語句結構、使用場景、執行計劃三個方面逐一剖析。緩存
分析的最開始階段很直觀地便是對sql語句自己表象結構的分析,對sql進行結構拆解分析。須要理清以下三點:安全
sql的結構特色。如使用的簡單單一查詢? join關聯查詢?仍是子查詢?等等性能優化
sql語句關鍵字可能帶來的典型問題。如like模糊語句、order by/group by、join使用的驅動表大小、limit高起點的深翻頁問題、not in帶來的全表掃描等問題等等。(此處提到的典型問題會在下面小節具體展開)
相關表創建的索引狀況。
通常一個SQL的主要結構包含在以下圖所示的結構。能夠是其中的某種單一結構,也能夠是這些結構的混合形式。
進一步地,定位sql的運行使用場景,即站在sql語句語法自己以外的角度分析sql的使用方式上,包括但不限以下幾點:
使用的業務場景:
須要支持模糊關鍵詞搜索
須要多條件的複雜的在線實時查詢
定時任務(如補數據/刪除數據)
頁面查詢or系統調用
運行的環境
產生慢sql的應用機器/DB實例:預發or線上機器產生的,以前遇到過預發環境工具致使的慢sql問題;是不是同一個DB實例產生慢sql,可能實例磁盤問題或數據傾斜等問題。
sql運行的週期/頻率/時間點:根據週期運行規律能夠判斷是否爲定時任務產生,定時任務有時會撈取大量數據掃全表致使慢sql;其次針對某一時間點的某個特定DB實例形成的慢sql,能夠經過
此階段須要透過現象看本質,須要分析sql的執行細節信息。Mysql提供Explain命令直觀反映sql的執行計劃,即sql是如何執行的。而SQL 性能優化的目標:type至少要達到 range 級別,要求是 ref 級別,若是能夠是 consts 最好。
1) consts 單表中最多隻有一個匹配行(主鍵或者惟一索引),在優化階段便可讀取到數據。
2) ref 指的是使用普通的索引(normal index)。
3) range 對索引進行範圍檢索。
反例:explain 表的結果,type=index,索引物理文件全掃描,速度很是慢,這個 index 級別比較 range 還低,與全表掃描是小巫見大巫。
Explain語句執行後的各個輸出的字段以下表所示:
字段名稱 | 含義 |
---|---|
id | 查詢的惟一標識(The SELECT identifier) |
select_type | 查詢類型(The SELECT type) |
table | 數據表名稱(The table for the output row) |
partitions | 匹配到的分區(The matching partitions) |
type | 關聯類型(The join type/access type) |
possible_keys | 可能使用到的索引(The possible indexes to choose) |
key | 實際使用到的索引(The index actually chosen) |
key_len | 被選中的索引字段長度(The length of the chosen key) |
ref | 顯示索引的哪一列被使用了,若是可能的話,是一個常數;即哪些列或常量被用於查詢索引列上的值(The columns compared to the index), |
rows | 預估要掃描的行數(Estimate of rows to be examined) |
filtered | 根據查詢條件過濾行數的百分比(Percentage of rows filtered by table condition) |
Extra | 額外信息(Additional information) |
下面對其中咱們平常關心的字段進行一些簡要介紹,更全面具體的能夠參考
表示關聯類型(join type)或訪問類型(access type),是一個很是重要的字段,是咱們判斷一個SQL執行效率的主要依據,執行效率依次從最優-->最差分別爲:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > ALL
ref
不使用惟一索引,而是使用普通索引或者惟一性索引的部分前綴,索引要和某個值相比較,可能會找到多個符合條件的行。
ref
是咱們平常開發中較爲常見的狀況,也是原則上指望要達到的級別,查詢命中到索引。
# 根據索引(非主鍵,非惟一索引),匹配到多行 SELECT * FROM ref_table WHERE key_column=expr; # 多表關聯查詢,單個索引,多行匹配 SELECT * FROM ref_table,other_table WHERE ref_table.key_column=other_table.column; # 多表關聯查詢,聯合索引,多行匹配 SELECT * FROM ref_table,other_table WHERE ref_table.key_column_part1=other_table.column AND ref_table.key_column_part2=1;
range
索引範圍掃描。常見於當使用<>、>、>=、<、<=、IS NULL、<=>、BETWEEN、IN
等操做符,用常量比較關鍵字列時
# 常量比較,可能多行 SELECT * FROM tbl_name WHERE key_column > 10 and key_column < 20; # 範圍查找 SELECT * FROM tbl_name WHERE key_column BETWEEN 10 and 20; # 範圍查找 SELECT * FROM tbl_name WHERE key_column IN (10,20,30); # 多條件加範圍查找 SELECT * FROM tbl_name WHERE key_part1 = 10 AND key_part2 IN (10,20,30);
index
索引全掃描。index
類型和ALL
類型相似,區別就是index
類型是掃描的索引樹,即MYSQL遍歷整個索引樹,經過讀取索引(若是非覆蓋索引場景,須要再回表查詢)來掃描全錶行。如下兩種狀況會觸發:
若是索引是查詢的覆蓋索引,即索引查詢的數據能夠知足查詢中所需的全部數據,則只掃描索引樹,不須要回表查詢。在這種狀況下,explain 的 Extra
列的結果是 Using index
。索引掃描一般比ALL快,由於索引的大小一般小於表數據。
# 即只select索引字段 SELECT key_column FROM tbl_name
按照索引掃描全表的數據是有序的,即全表掃描會按索引的順序來查找數據行;使用索引不會出如今Extra
列中。會避免排序,但也會掃描整表數據
# key_column爲索引字段 select * from tbl_name order by key_column
ALL
全表掃描,沒有任何索引可使用時。這是最差的狀況,應該避免。
這一列顯示查詢可能使用哪些索引來查找。 explain
時可能出現 possible_keys
有值,而 key
顯示 NULL
的狀況,這種狀況是由於表中數據很少,MySQL認爲索引對此查詢幫助不大,選擇了全表查詢。
若是該列是NULL
,則沒有相關的索引。在這種狀況下,能夠經過檢查 where
子句考慮創建合適的索引
這一列顯示MySQL真正使用的索引是什麼。也有可能key
的值不存在於 possible_keys
中,這種狀況多是possible_keys
中沒有特別合適的索引,MySQL選擇了其餘的索引進行查詢。
該列代表MySQL估計要讀取並檢查的行數,注意不是結果集裏的行數。
代表返回結果的行佔須要讀到的行(rows列的值)的百分比。當咱們執行一個查詢語句時,MySQL首先會根據索引去掃描出一批數據行,而後再在這些數據行中,根據查詢條件進行過濾,實際返回的行數 / 掃描出的結果行的百分比,即爲filter
的值。
該列代表了一些額外的信息來講明MySQL如何解析查詢的。對於判斷一個SQL的執行性能,也是很是重要的判斷依據。對其中咱們可能常遇到的進行下介紹:
Using filesort:說明mysql會對數據須要進行排序,而不是按照表內的索引順序進行讀取。代表SQL可能須要進行必定的優化。
Using index:這個值重點強調了只須要使用索引就能夠知足查詢表的要求,不須要回表查詢了,通常表示使用了覆蓋索引。此類sql性能較好。
Using temporary:這個值表示使用了內部臨時表。這種狀況一般發生在查詢時包含了group by、union等子句時。每每須要優化sql。
Using where:where條件查詢,一般using where表示優化器須要經過索引回表查詢數據。
Using join buffer (Block Nested Loop):使用join buffer(BNL算法)進行關聯執行。每每須要優化sql
Using MRR(Multi-Range Read ) :使用輔助索引進行多範圍讀。
能夠從四個維度或角度對慢sql進行解決,相輔相成,協力擊破慢sql,以下圖所示:
SQL優化:從sql自己出發。僅僅對SQL自己進行優化,包括索引優化、SQL語句改寫等。
業務改造:從業務使用角度觸發。在業務場景層面進行改造和「妥協」,避免產生慢SQL。好比:改成分頁查詢、限制查詢條件、實時性的妥協等等。
源頭替換:從數據自身特性和使用角度出發。如數據生命週期的冷熱程度、複雜查詢or模糊查詢等特性替換爲不一樣的數據源。如緩存、搜索引擎、OLAP數據庫等。
數據減小:從數據庫容量和性能角度出發。如分庫分表,定時歷史數據清理等。
此處的SQL優化時普遍的SQL概念,即指SQL語句自己優化,以及SQL相關的索引優化問題,具體以下展開。
最基本可是在業務中佔比很高的case,即sql未命中索引。
優化建議:增長索引
特別地,對應order by 和group by場景:
對於order by a語句,儘可能要在列a上創建索引 或是組合索引的一部分,而且放在索引組合順序的最後,避免出現 file_sort 的狀況,利用索引有序性,能夠避免排序和臨時表創建(具體見下方order by語句問題分析)
對於group by a語句,儘可能要在列a上創建索引,利用索引有序性,能夠避免排序
一般是聯合索引的字段設置不合理,explain以後看上去有索引命中,可是並不是是最合理最優化的索引設計。
優化建議:索引中添加須要的字段,創建合理聯合索引。
創建組合索引時,區分度最高的在最左邊;對於多列的組合索引,如分別是warehouse_id, user_id, item_id, inventory_status。考慮到最左前綴匹配規則,有了這個組合索引,就至關於有了單列索引(warehouse_id),組合索引(warehouse_id, user_id),組合索引(warehouse_id, user_id, item_id)。因此在索引創建的時候,把查詢時候經常使用的(區分度高的)字段要放到索引排序的左邊。
存在非等號和等號混合判斷條件時,在建索引時,請把等號條件的列前置。如:where c>? and d=? 那麼即便 c 的區分度更高,也必須把 d 放在索引的最前列,即創建組合索引 idx_d_c。對於範圍查詢,MySQL索引會一直向右匹配直到遇到(> < between like)就中止,好比a = 1 and b = 2 and c > 3 and d = 4 ,若是創建(a,b,c,d)順序的索引,d是用不到索引的。所以若是字段在多數語句中都以範圍查詢的形式出現,能夠考慮把索引的字段作調整,將其後置,增長索引被命中率。
=和in能夠亂序,好比a = 1 and b = 2 and c = 3 創建(a,b,c)索引能夠任意順序,mysql的查詢優化器會幫你優化成索引能夠識別的形式
索引可選擇性(區分度)差
查詢的條件區分度高不高。區分度的公式是count(distinct col)/count(*),表示字段不重複的比例,比例越大掃描的記錄數越少,因此儘可能選擇區分度高的列做爲索引;惟一鍵的區分度是1,而一些狀態、性別字段可能在大數據面前區分度就是0(實際爲0.00003,考慮精度可認爲=0)。通常對於區分度大於0.1的查詢字段都要創建索引。
若是字段的可選擇性很是差,使用索引比全表掃描還慢。由於要先跑一遍索引,而後根據沒有消除幾個記錄的索引再回表跑差很少大半個的全表,結果還不如直接跑全表。
對於查詢來說,最好的索引就是惟一性,一次便可定位,對於重複數據不少的列不適合創建索引,由於過濾後數據量仍然會很大,先走索引在走表,因此很慢。
避免類型隱式轉換。索引字段的數據類型和查詢的數據類型必定要匹配上。
如查看該字段是int類型,可是查詢條件值是字符串: sql SELECT * FROM t WHERE c = 'aa'
,會致使SQL不走索引,而致使全表掃描。
經過在explain語句後增長extended即explain extended 'sql語句'
,再執行show warnings
查看是否存在隱式轉換以及哪一個字段存在隱式轉換。
使用"非/不等於"( <>,!=,not in )查詢時會致使索引失效(可是知足覆蓋索引Covering Index使用條件的sql,"!="和"not in"也能夠走索引)。儘量使用等值查詢,即全值匹配查詢
LIKE
語句不容許使用 %
開頭,不然索引會失效;即未遵循最左前綴原則致使
IS NOT NULL 或 IS NULL條件查詢也可能致使索引失效。
當索引字段不能夠爲空(null)時,is null 不會使用索引;只有使用is not null 返回的結果集中只包含索引字段時,才使用索引(即覆蓋索引)
當索引字段能夠爲空(null)時,使用 is null 會使用索引(不影響覆蓋索引);但使用 is not null 返回的結果集中只包含索引字段時,纔會使用索引(即覆蓋索引,同上)
索引列不能參與計算、函數,保持列「乾淨」。好比from_unixtime(create_time) = ’2019-12-01’就不能使用到索引:須要先作一次全表掃描,將字段上的全部值使用表達式做用後再進行匹配,從而會致使Mysql放棄走索引。因此語句應該寫成create_time = unix_timestamp(’2019-12-01’);
可能不會命中索引!!mysql自身優化時除了考慮利用索引提高查詢速度,還會考慮數據io消耗等多方面的因素,最終選取一種最合適的方案,即會綜合查詢效率和io效率的比拼:
使用索引查詢時查詢效率高,io效率低
使用全表掃描時查詢效率低,io效率高
字段按需查詢,儘量使用覆蓋索引,即查詢字段爲對應的索引列,則能夠直接從索引取出值,而不用回表再次查詢。
隨着limit offsett, n語句中offset的增大,性能愈來愈差。主要緣由爲如limit 10000,10的語法其實是mysql查找到前10010條數據,以後丟棄前面的10000行後再返回。同時因爲此類問題出如今分頁場景下,翻頁到很深度的頁數時會暴露出來,所以也常稱爲「深翻頁」問題。即MySQL 並非跳過 offset 行,而是取 offset+N 行,而後返回放棄前 offset 行,返回 N 行,那當 offset 特別大的時候,效率就很是的低下,要麼控制返回的總頁數,要麼對超過特定閾值的頁數進行 SQL 改寫。
優化建議:
使用id界限判斷優化。where id>offset limit n
來代替使用 limit offset, n;
-- 一樣的效果 select * from notes limit 1000000,3; select * from notes where id>1000000 limit 3;
用id的覆蓋索引優化。能夠先利用覆蓋索引查出ID字段,而後根據id再獲取數據(即先快速定位須要獲取的 id 段,而後再關聯);缺點是須要id必須是單調有序的 (推薦)
select * from
(select ID from job limit 1000000,100) as a join job as b
on a.ID = b.id;
強制走某些索引:Mysql優化器並不老是能作出最好的索引選擇。有些狀況下使用另外的索引有更好的性能,可是並無沒優化器所採用。則可使用force index
強制要求走某個索引,固然,必須保證這個索引之後不能被刪除,否則就是個BUG。
in和exists
in語句執行流程:查詢子查詢的表且內外表有關聯時,先執行內層表的子查詢,而後將內表和外表作一個笛卡爾積,而後按照條件進行篩選,獲得結果集。因此相對內表比較小的時候,in的速度較快。
exists語句執行流程:指定一個子查詢,檢測行的存在。遍歷循環外表,而後看外表中的記錄有沒有和內表的數據同樣的,匹配上就將結果放入結果集中。
優化建議:in和exists主要是形成了驅動順序的改變,exists是之外層表爲驅動表、IN是先執行內層表的子查詢。所以若是子查詢得出的結果集記錄較少,主查詢中的表較大且又有索引時應該用in(主要要仔細評估 in 後邊的集合元素數量,控制在 1000 個以內,也是爲了不in大結果集後致使JVM內存產生fgc);反之若是外層的主查詢記錄較少,子查詢中的表大且又有索引時使用exists。
not in和not exists
not in使用的是全表掃描沒有用到索引;而not exists在子查詢依然能用到表上的索引。
優化建議:用not exists都比not in要快。
sql示例:select * from t1 straight_join t2 on (t1.a=t2.a)
, 其中驅動表爲t1,被驅動表t2。
當關聯被驅動表上使用到索引時(即t2的字段a有索引),會使用 Index Nested-Loop Join (NLJ)算法,沒有問題。
當關聯被驅動表上沒有使用到索引時(即t2的字段a無索引),會使用 Block Nested-Loop Join(BNL)算法。會把表 t1 的數據讀入內存 join_buffer 中;掃描表 t2,把表 t2 中的每一行取出來,跟 join_buffer 中的數據作對比,知足 join 條件的,做爲結果集的一部分返回。整個過程掃描行數就會過多,尤爲是在大表上的 join 操做,這樣可能要掃描被驅動表不少次,會佔用大量的系統資源。因此這種 join 儘可能不要用。確認方法爲 explain的Extra結果有沒有Using join buffer (Block Nested Loop)
老是應該使用小表作驅動表:在決定哪一個表作驅動表的時候,應該是兩個表按照各自的條件過濾,過濾完成以後,計算參與 join 的各個字段的總數據量,數據量小的那個表,就是「小表」,應該做爲驅動表。
MySQL 作排序是一個成本比較高的操做:全字段排序在sort_buffer中會創建臨時表進行排序、另外一種基於rowid排序不只須要創建臨時表還須要涉及回表查詢等操做。然而並非全部的 order by 語句,都須要排序操做,須要排序是由於原來的數據都是無序的。判斷是否須要排序經過explain 的Extra結果裏有沒有Using filesort。
優化建議:order by 字段上創建索引,從而自然支持排序。若是order by 最後的字段是組合索引的一部分,須要把放在索引組合順序的最後
order by 最後的字段是組合索引的一部分,而且放在索引組合順序的最後,避免出現 file_sort 的狀況,影響查詢性能。 正例: where a =? and b =? order by c; 索引: a _ b _ c 反例:索引中有範圍查找,那麼索引自身的有序性沒法利用,如: WHERE a >10 ORDER BY b; 索引 a_b 沒法排序。
group by語句因爲可能會創建內部臨時表,用於保存和統計中間結果。首先會使用內存臨時表,可是內存臨時表的大小是有限制的,由參數 tmp_table_size 控制,當超過此限制時會把內存臨時錶轉成磁盤臨時表。所以內部臨時表的存在會影響內存和磁盤的空間,且須要構造的是一個帶惟一索引的表,執行代價都是比較高的。所以須要儘可能避免內部臨時表的創建。
額外排序:group by column默認會根據column排序,所以還會觸發排序開銷問題。
優化建議:
儘可能讓 group by 字段用上表的索引,確認方法是 explain 的Extra結果裏有沒有 Using temporary 和 Using filesort;經過索引創建,只須要順序掃描到數據結束,就能夠拿到 group by 的結果,不須要臨時表,也不須要再額外排序。
若是對 group by 語句的結果沒有排序要求,要在語句後面加 order by null;
若是 group by 須要統計的數據量不大,儘可能只使用內存臨時表;能夠經過適當調大tmp_table_size 參數,來避免用到磁盤臨時表;
若是數據量實在太大,使用 SQL_BIG_RESULT 這個hint,來告訴優化器直接使用排序算法獲得 group by 的結果。
有時技術的複雜度或難點可能隨着業務的玩法的調整就能夠迎刃而解。從業務服務使用的角度出發,可否進行一些trade off或是變通,包括但不限於如下幾種方式:
是否是真的須要所有查出來,仍是取其中的top N就可以知足需求了
查詢條件過多的狀況下,可否前端頁面提示限制過多的查詢條件的使用。
針對實時導出的數據,涉及到實時查DB導出大量數據時,限制導出數據量 or 走T+1的離線導出是否是也是能夠的。
如今業務上須要作數據搜索,使用了 LIKE "%關鍵詞%" 作全模糊查詢,從而致使了慢SQL。是否是可讓業務方妥協下,作右模糊匹配,這樣就能夠利用上索引了。
Mysql並非任何的查詢場景都是適合的,如須要支持全模糊搜索時,全模糊的like是沒法走到索引的。同時結合數據自己的生命週期,對於熱點數據,能夠考慮存儲到tair等緩存解決。所以針對不適合mysql數據源的狀況,咱們須要替代新的存儲介質。現梳理以下幾種case:
有like的全模糊的查詢,好比基於文本內容去查訂單信息,須要接搜索引擎openSearch的解決。
有熱點數據的查詢,考慮是否要接Tair等緩存解決。
針對複雜條件的海量數據查詢,能夠考慮切換到OLAP(Online Analytical Processing),能夠考慮接Hybrid DB或ADB通道。
有些場景Mysql不適用,須要用K-V的數據庫,HBASE等列式存儲的存儲引擎。
SQL自己的性能已經到達極限了,可是耗時仍然很長,可能因爲數據量或索引數據都比較大了。所以須要從數據量級減小的角度去處理。
使用分庫分表。因爲單表的數據量過大,例如達到千萬級別的數據了,須要使用分庫分表技術拆分後減輕單庫單表的單點壓力。
定時清理終態數據。針對已經狀態爲終態的業務單據或明顯信息,可使用idb歷史數據清理的方式配置定時自動清理。如針對咱們的倉儲庫存操做明細爲完結狀態的數據,咱們只保留最近1天的數據在db中,其餘直接刪除,減小db查詢壓力。
統計類查詢能夠單獨維護彙總數據表。參考數據倉庫中的數據分層設計,基於明細數據,抽出一張指標彙總表,或7天/15天等的視圖數據進行預計算。此類彙總表數據量級相比明細表降低不少,從而避免直接根據大量明細查詢聚合形成慢sql。
基於上面的思路,能夠對一個個sql逐個擊破,下面就簡單列舉幾個在治理過程當中的實踐示例。
sql預計索引分析。前端頁面跳轉到庫存操做明細頁面時觸發頁面查詢,但owner_id沒待到後端,致使未走到合理的索引
分析sql時間點發現固定db某個示例會致使RT尖峯抖動,發現磁盤也有相應問題。懷疑DB某些庫磁盤問題致使,聯繫DBA確認後進行主備切換解決
覈銷慢sql查詢遲遲難以解決。發現庫存覈銷記錄天天增量數據達到百萬級別,可是覈銷建立狀態記錄只有20%~30%左右,所以對完結狀態的核銷記錄idb配置定時清理,由15天縮短到2天,減小db數據量。
庫存sn查詢涉及複雜查詢,採用切換到OLAP鏈路,經過數據同步中間件
做爲開發人員,須要在平時將sql的常見問題和「坑點」內化到平常開發中;內化於心,敬畏線上,讓慢sql「清零」成爲常態化,而不要等到每一年大促前又要費時費人力的集中治理或等線上問題暴露出來時可能爲時已晚。但願本文能多少帶給你們一些系統性地思路和思考。水平有限,若有不對之處,歡迎指正討論~~