Mysql查詢優化
什么是索引?
觀察下面一組數字:
如果我想查找最后一個數字,那么我付出的最大查詢成本是:查詢10次,數據越多,查詢代價越大。
如果我想查詢某個范圍的值,比如查找小於5的值,我需要從頭到尾把每個值都需要對比一遍,最終挑出小於5的值。
如果我把上面這組數字變成如下圖:有序的數據結構,這樣就可以利用二分查找法
那么此時我查找某個具體的值付出的最大查詢成本大概僅僅是3、4次。
如果我想查找某個范圍的值,比如還是小於5,因為數據有序排列的,那么等我通過二分法確定5的位置時,那么它左邊的值全部就是小於5的數據,立即就能拿到。
經過上面分析你得到什么啟示:想使用某種快速查找的算法,這前提必須是建立在某種有規律的特定的數據結構之上的。而我們創建索引的過程,就是創建為了實現快速查找算法所必須的數據結構的過程。而在mysql中,想使用索引實現快速查找,你可以簡單理解為:必須要求索引的數據是按順序排列的。
使用索引和非索引的查詢成本對比
整個stu表如下:
怎么查看分析查看查詢成本,通過explain查看執行計划。
執行某條sql語句、不使用索引:
分析:type:all 說明mysql沒有走索引,走了全表掃描,8條記錄全部取出。
IO成本: 8條記錄(全表)
算法成本:普通查找、必須一個個的去掃描對比。
使用索引:
分析:type:ref ref表示使用了普通索引。1條記錄被取出。
IO成本: 1記錄+1列索引
算法成本:二叉樹查找法,通過有規律的數據結構,快速定位到某個數據,比全表掃描快。
由此可見:使用索引之后,在一般情況下,無論是IO成本還是計算查找成本都遠低於全表掃描。
多列索引
需求:查出班級class_id為3且年齡小於25的人
Sql: select * from stu where age >20 and class_id=3;
可以把age建立index索引,查詢計划如下:
IO 成本:4條記錄
算法成本:當查找age>20走了索引。然后就把age>20的滿足條件的記錄都取了出去,然再按普通查找的方式掃描class_id=3
但是這僅僅夠么,還不夠完美:
為了盡可能的利用mysql的索引特性,我們可以建立一個多列索引。當mysql使用復合索引時,會先掃描完age列之后,然而不會再把滿足age列條件的記錄都取出來,而是再繼續利用二叉樹查找算法掃描class_id這個列,得到最終結果的索引項,取出索引中保存的地址,根據地址把表中的記錄取出。
給age和 class_id建立復合索引:alter table stu add index fuhe (class_id,age);
查詢后的執行計划如下:
分析:
IO成本:row為1,最終的IO成本變成了1
算法成本:相比上面只建立age單個索引,class_id=3這個條件的查詢也利用上了索引,利用索引之后,提高了class_id=3的查找效率,最終又降低了最終從表取出數據的IO成本。
注意:
在當前查詢語句中,建立復合索引的條件只能是(class_id,age)的順序,不能是(age,class_id)這種順序。因為這樣的話第二列class_id索引會利用不上,最終還是走了age單個列的索引查詢。
為什么?不是說復合索引中,只要使用了第一列索引,就會使用第二列索引么?
不是的,這是不對的。
試驗:以這種(age,class_id)創建索引的順序,執行下面語句,:
分析:key值為fuhe ,說明使用到了索引,看似也是使用到了索引,觀察rows的值,是4,並不是1,這說明一個什么問題?其實真實的情況還是只是使用了復合索引中 age這個一單列的索引,並沒有使用到class_id。
為什么?
因為你不了解復合索引的內部結構是怎樣的?
比如:以該表的(class_id,age)復合索引為例,它內部結構簡單說就是下面這樣排列的:
mysql創建復合索引的規則是首先會對復合索引的最左邊的,也就是第一個class_id字段的數據進行排序,在第一個字段的排序基礎上,然后再對后面第二個的age字段進行排序。其實就相當於實現了類似sql語句中,走了 order by class_id age這樣一種排序規則。
所以:第一個class_id字段索引是絕對有序的,而第二字段就是無序的了。我之前說過,想利用到索引,必須要求該列索引的數據必須是有規律的特定的數據結構,也就是在這里必須是有序的。而所以通常情況下,直接使用第二個age字段進行條件判斷是用不到索引的,這就是所謂的mysql為什么要強調最左前綴原則的原因。
那么什么時候才能用到呢?
觀察可知,當然是在class_id字段是等值匹配的情況下,cid才是有序的。發現沒有,觀察兩個class_id值為2 的age字段值是不是有序的呢。從上往下分別是15 16。
這也就是mysql索引規則中要求復合索引要想使用第二個索引,必須先使用第一個索引的原因,而且第一列索引必須是等值匹配。
多表查詢的優化
需求:查出所有在讀班級的學生
Sql: select * from stu where class_id in (select id from class);
執行計划如下:
分析:
Select_type中 DEPENDENT SUBQUERY代表這個表是子查詢出來的,而且是相關子查詢。
執行計划中,其實它並不是先執行in子查詢語句找到id,然后再去到stu中去查復合id值的。
mysql會把in子查詢轉換成exists相關子查詢,所以它實際等同於這條sql語句:select * from stu where exists(select id from class where stu.class_id=class.id );
而exists相關子查詢的執行原理是: 循環取出外表的每一條記錄與子查詢中的表進行比較,比較的條件是stu.class_id=class.id 然后看外表的每條記錄的class_id是否在內表的id字段存在,如果存在就行返回外表的這條記錄。
是不是很類似join連接查詢?
exists查詢有什么弊端?
由exists執行原理可知,外表使用不了索引,必須全表掃描,因為是拿外表的數據到內表查。而且必須得使用外表的數據到內表中查(外表到里表中),順序是固定死的。
如何優化?
建索引。但是由上面分析可知,要建索引只能在內表(class表)的id字段建,不能在外表的class_id上,因為外表是全表掃描,mysql利用不上。(當熱這里class表的id字段因為是主鍵,已經是索引了,不用咱們創建)
這樣優化夠了嗎?
引出了一個更細致的疑問:在雙方兩個表的字段上都建有索引時,到底是外表查內表的效率高,還是內表查外表的效率高?
該如何進一步優化?
把查詢修改成inner join連接查詢:select * from stu inner join class on stu.class_id=class.id; (但是僅此還不夠,接着往下看)
為什么不用left join 和 right join?
這時候表之間的連接的順序就被固定住了,比如左連接就是必須先查左表全表掃描,然后一條一條的到另外表去查詢,右連接同理。仍然不是最好的選擇。
為什么使用inner join就可以?
inner join中的兩張表,如: a inner join b,但實際執行的順序是跟寫法的順序沒有半毛錢關系的,最終執行也可能會是b連接a,順序不是固定死的。如果on條件字段有索引的情況下,同樣可以使用上索引。
那我們又怎么能知道a和b什么樣的執行順序效率更高?
答:你不知道,我也不知道。誰知道?mysql自己知道。讓mysql自己去判斷(查詢優化器)。具體表的連接順序和使用索引情況,mysql查詢優化器會對每種情況做出成本評估,最終選擇最優的那個做為執行計划。
在inner join的連接中,mysql會自己評估使用a表查b表的效率高還是b表查a表高,如果兩個表都建有索引的情況下,mysql同樣會評估使用a表條件字段上的索引效率高還是b表的。
而我們要做的就是:把兩個表的連接條件的兩個字段都各自建立上索引,然后explain 一下,查看執行計划,看mysql到底利用了哪個索引,最后再把沒有使用索引的表的字段索引給去掉就行了。
Group by 和 臨時表的優化
先觀察一個分組的sql語句的explain結果(在沒有使用索引的情況下):
extra結果: Using temporary; Using filesort 什么意思?表示查詢使用了臨時表、使用了排序。
為什么會產生這種情況?了解一下group by 執行原理:
1 首先mysql會把最終需要分組的結果集提取出來作為一個臨時的表存放到內存空間。
2 對該臨時表進行排序
3排序之后進行分組
alter table stu add index (class_id) 把class_id建立索引,利用索引:
extra結果:Using index 發現mysql直接走了索引覆蓋
僅接着對max或者min進行測試:
extra結果: Using temporary; Using filesort 發現時使用了臨時表、使用了排序。
為什么?
因為我們最終查詢的age字段並沒有在索引中,Mysql無法只通過class_id這個索引字段進行分組就能求出age這個字段的統計信息。它必須還得通過class_id這個索引上地址回去取出完整的記錄。
那這樣的話,豈不是多次一舉,所以它不會使用索引,還不如直接把記錄都取出來,使用臨時表的方式進行統計。
怎么辦?解決方案:
建立一個復合索引,把age也利用上。(class_id,age) class_id是索引的第一列,所以class_id是有序的數據結構,能被group by 利用上。如下圖:
那么此時,group by 會直接該索引進行分組,然后對索引的age列直接統計就行了。
結果:使用上了索引,並沒有產生臨時表排序。
Limit分頁的優化
分頁的原理:例。Select * from t limit 1000, 10; 通常情況是先取出前1010條數據,再舍棄前1000條,只保留最后10條。
由此可知,數據量越大,查詢的IO的成本越大。
有人說通過id主鍵優化,這樣查:select * from t where id> =1000 and id< =1010
但是有個很大的缺點:必須要求id值是連續的,否則的話就肯定不對了。實際應用中,我們經常會刪除某條數據,想要id值必須是連續的通常是一個理想化的情況。
最終怎么做?
我們先只查出id主鍵字段,再自連接查出最終的記錄:
select * from (select id from t limit 1000,10) as a inner join t on a.id = t.id
而避免了全表的IO。
如果還有時間字段參與排序的話,可以把(id,time)建立一個復合索引。
多表查詢中出現的臨時表現象
觀察下面sql結果:
為什么會產生了臨時表?
分析:mysql首先查了class表,然后再使用stu的class_id索引對stu表進行了join連接查詢。這並沒有什么問題,為什么此時產生了臨時表呢?請注意我sql語句最后我利用age字段進行了排序: order by stu.age
mysql就把join后的結果作為臨時表進行了排序。
在這里我們無法通過有效的索引來解決這個問題。
我們只能盡量保證讓它只在內存中來進行這個過程,而不是在磁盤上。
什么叫在磁盤上操作,具體內容請往下看:磁盤臨時表:
磁盤臨時表
這個磁盤臨時表跟上面講的臨時表是什么關系呢?
其實是同一個概念,無論是產生的條件還是解決的方案都是一樣的。
只不過是:在mysql使用臨時表的過程中,這個臨時表在內存中放不下時,會自動的轉換成磁盤臨時表,把結果集放到磁盤上,一點一點的回讀到內存中操作。這樣的話,就會產生磁盤IO,那么此種的臨時表效率會更加糟糕!mysql最終采用的是內存臨時表還是磁盤臨時表我們無從得知,我們仍然可以采用上面講的索引方案避免臨時表的產生。
但是,有時候情況並不是那么完美,就一定能用索引解決臨時表的產生。而此時,我們應要盡量要避免磁盤臨時表的產生,讓它在內存中操作就好。
解決方案:
在我們的mysql中有兩個參數為:
tmp_table_size (默認33.5M)
max_heap_table_size (默認16.7M)
mysql是否轉化成為磁盤臨時表的依據就是這兩個參數,mysql會取這兩個參數最小的那個作為依據,如果當前要操作的結果集超過了這個設定,就會自動轉換成磁盤臨時表,所以我們可以設置這兩個參數,把它調大。一般初始會設置成百兆,當然根據實際情況。
怎么設置,在mysql中使用以下命令動態的改變:
set @@tmp_table_size=100*1024*1024;
set @@max_heap_table_size=100*1024*1024;
查看結果:
SELECT @@tmp_table_size;
SELECT @@max_heap_table_size;
然后不斷的觀察sql語句的執行時間是否有降低。
學會查看sql語句的執行的各項性能消耗
在MySQL數據庫中,可以通過配置profiling參數來啟用SQL剖析。
但是這個功能默認是關閉的,可以使用set profiling =1 命令開啟
查看是否開啟
開啟之后,執行你要分析的sql語句,然后通過show profiles命令可以看到你執行過的所有sql語句的消耗時間
詳細查看特定的某個語句的各項執行情況:
比如 query_id = 1的sql語句
通過 show profile for query 1 命令,可以看到該sql語句執行的每一個步驟的消耗時間
還可以查看該sql語句每執行步驟的cpu、 io、 memory 等消耗情況
比如查看cpu和io的消耗時間: show profile cpu,block io for query 1
通過慢查詢日志找出需要優化的sql語句
先查看慢日志是否開啟:
slow_query_log : off表示關閉的,on表示開啟
Slow_query_log_file :慢日志的存儲位置
查看慢查詢的時間限制,默認10秒:
這些配置的修改建議直接去配置文件修改,最終我們找到慢日志的存放位置,打開查看就行了。
mysql cpu占用過高怎么解決
1先看個整體的情況
在Mysql當中,使用show [full] processlist查看當前在mysql正在執行的sql語句
User:發送sql語句到當前Mysql使用的是哪個用戶
Host: 發送sql語句到當前mysql的主機ip和端口
Db: 連接哪個數據庫
Command: 連接狀態,一般是休眠空閑sleep 查詢query 連接connect
Time: 連接連續時間
State: 當前sql語句執行到哪個狀態
列舉幾個執行狀態,可以看一下:
Checking table 正在檢查數據表
Sending data 正在處理SELECT查詢的記錄,返回數據
Sorting for group 正在為GROUP BY做排序
Sorting for order 正在為ORDER BY做排序
Updating 正在搜索匹配的記錄,並且修改它們
Locked 被其他查詢鎖住了
着重查看當前連接的執行時間和狀態情況,可以多執行幾次show processlist 看看有哪些sql語句還在執行當中
2 打開慢查詢日志
看看哪些sql語句執行時間長,尤其是那些有group by order by 等的語句,比較消耗cpu資源。針對買個sql語句,使用explain或者show profiles語句分析執行過程,逐條優化。
一個真實的sql優化案例一
公司是做汽車服務行業SCRM門店管理系統的,其中一個功能是查出該門店的所有會員與之對應的車輛。有三張表,核心字段如下:
需求:查出某個門店下的所有會員與車輛列表(會員姓名,車輛品牌,車牌號,車標logo)
比如store_id=1的本店下的所有會員車輛列表,每次只取出20條:
第一次優化
分析:需要三張表相連,其中一個會員可能有多輛車,所以車輛表必然比會員表的記錄多,而會員表可通過門店store_id只篩選出本店的會員。由此可見,先查出本門店會員再與車輛表相連,再與車品牌表相連,在會員表的storeid和車輛表的mid和車品牌表brand_code上建索引,這種連接順序,IO成本比較小,是一種不錯的方案。
比如
member_base 1000條
where store_id走索引之后只真正取出100條
連接 car_base 1500條,走mid索引真正取出120條
連接 car_brand 50條 走id索引 取出1條
總IO成本為100+120+1=230
推算其他幾種連接順序方案與這種對比,IO成本都不如這種少
故使用left join 強制左表連右表:
select name,brand_name,cpai,brand_logo
from member_base as m left join car_base as c on m.id=c.mid
left join car_brand as b on c.brand_code=b.id
where m.store_id=? limit 20 ;
(mysql查詢優化器會自動先where篩選出member_base會員表store_id本門店的會員,再join)
第二次優化
有時候為了追求更好的查詢效率,可在車輛表甚至會員表做字段冗余,減少join連表的。
比如在車輛表,添加需要查出的brand_name和brand_logo字段,這樣我們只需要會員表與車輛表總共兩張表相連即可。(sql省略)
第三次優化
由於我們每次只取出limit 20條的記錄,我們首次在查會員表的時候,一般該門店的會員不都只有20條記錄這么少,我記得系統中很多門店的會員數量都在1千條以上,那么這個時候,再與車輛表相連的時候,會把會員表每一條記錄的id都在整個車輛表查一遍,共查了1000次,這是非常不划算的。(sql語句的執行順序通常是先 join on 后 where 后 group by 后 統計函數 后 having 后 order by 最后才執行limit )
解決方案
先在會員表取出門店的20條記錄會員,再與車輛表join
select * from (select * from member_base where store_id=? limit 20) as m left join car_base as c on m.id=c.mid
最終只在車輛表查詢20次即可。
一個真實的sql優化案例二
這是阿里雲上的某個項目的表查詢,下面的sql語句,竟然耗時了1525秒
explain結果:
為了同學們便於快速觀察該sql語句,我把它簡化成如下sql語句:
select * from
a inner join b on a.uid=b.uid
inner join c on a.shopid=c.id
where a.code=0 and a.status like '%未發貨%'
and b.auto_user=0 order by a.time;
我詢問后所獲得的一些信息如下:
a表一百六十萬多條數據,數據量最大。
b表兩千多條
c 表十五萬條
解決思路:
由上面explain結果得知:查詢a表時,雖然利用上time索引,解決了排序問題,但是並不高效,為什么?,觀察type:index 說明mysql先把a表的time索引加載過來,然后倒着一個個的遍歷索引項,說明:index類型表示,這個索引的查找並不是按特定的算法“跳着”查找某個索引項,而是一個個掃描索引項,然后取出表中的記錄這個索引的查找並不是按然后一個個取出表中的記錄。其實最終仍然走了全表的掃描,而且又加上剛開始time索引的讀取成本。mysql中影響sql查詢最大的障礙其實就是磁盤IO上的操作。而a表是最大的,有一百六十多萬數據,這樣的做法是萬萬不妥的。
我們解決問題的核心思路點是應該使用索引盡量減少a表數據讀取的量。
觀察語句得知,a表能利用上的索引有以下四個:
a.uid
a.code=0
a.status like ’%未發貨%’
a.shopid
分析:
a.status是利用不上索引的,只有a.uid和a.code=0能做些文章。
到底選哪個呢?
繼續詢問得知:
code=0這個值在列中的重復度最高,大概占了90%,而uid值的重復度相對比code少很多,具體占比不詳。
a.shopid和uid的重復度差不多,並不像code=0這么明顯的高。
而c表有十五萬條記錄,比b表大很多,如果先查c表連接a表不如先查b連接a表更划算。所以最終選了個a.uid這個列建立索引,此時b表會連接a表時,會使用a.uid這個索引提取a表符合條件的記錄。
因為a表還有code=0這個條件,或者我們也可以建立一個復合索引:
(uid,code) 不過code的選擇性並不高,效果甚微,這已經不是我們的核心點了,同學們可自由發揮測試。根據實踐做決定。
這時候b表查a表的優化已經結束了,接着是c表,這個就簡單了,還是盡量利用索引么,因為c表的id字段本身就是主鍵索引,我們無需建索引,直接使用就好了。
有時候,我們的查詢優化器可能並不能按我們分析的最優順序去執行,它也有不聰明的時候,那么我們要放開思維,大膽嘗試改變語句的結構,比如inner join 不行 使用left join 強執行左連,或者加括號提高優先級。