【MySQL】為什麼SQL會這麼慢
建表
CREATE TABLE `ts_ab` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` varchar(20) CHARACTER SET utf8 DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `ind_b` (`b`) USING BTREE,
KEY `ind_a` (`a`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
CREATE TABLE `ts_ef` ( `id` int(11) NOT NULL, `e` int(11) DEFAULT NULL, `f` varchar(20) CHARACTER SET utf8 DEFAULT NULL, PRIMARY KEY (`id`), KEY `ind_e` (`e`) USING BTREE, KEY `ind_f` (`f`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
插入資料
create PROCEDURE addDataab() BEGIN DECLARE i int; set i=1; WHILE(i<=10000) DO INSERT ts_ab(id, a, b) VALUES(i, i, CONCAT('b',i)); set i= i+1; end WHILE; WHILE(i<=20000) DO INSERT ts_ab(id, a, b) VALUES(i, i, CONCAT('c',i)); set i= i+1; end WHILE; WHILE(i<=30000) DO INSERT ts_ab(id, a, b) VALUES(i, i, CONCAT('d',i)); set i= i+1; end WHILE; end; CALL addDataab();
create PROCEDURE addDataef() BEGIN DECLARE i int; DECLARE j int; set i=1; set j=1; WHILE(i<=200) DO set j=i; if i >100 and i<=105 THEN set j=105; end if; INSERT ts_ef(id, e, f) VALUES(i, j, CONCAT('b',j)); set i= i+1; end WHILE; end; CALL addDataef();
1、索引
sql執行慢,第一想法就是加個索引唄。但有時儘管加了索引了,為什麼執行還是這麼慢的呢。這就要問你真正使用對了索引沒有了。我們一般可以使用EXPLAIN
來檢視是否sql執行時是否使用了索引。對於索引還不怎麼清楚的同學,建議你自行檢視下我的上一篇文章【MySQL】索引
1.1對索引欄位進行了函式操作
a欄位上面建有索引,圖1中對a欄位進行了運算,圖2中沒有對a欄位進行運算。
從結果可知,圖1中進行了全表掃描,大概掃描了29484行,沒有使用索引快速查詢。圖2表示查詢使用了索引,掃描了1行。出現這種現場的原因是圖1中的sql對索引欄位進行的運算操作,你可以想象我們的索引是一棵B+樹,如果一張表中有日期這個欄位,並且對此欄位加了索引。現在我們需要查詢8月份的資料,如果我們sql寫成select * from cyj_test where month(createDate) = 8
。createDate這個欄位的索引是有序的,你覺得month(createDate) 還會是有序的嗎?想想都覺得不會是有序的,所以,在對索引欄位做了函式操作時,會破壞了索引的有序性,MySQL就乾脆不走索引了。或許你會說加1操作後還是有序的啊,但對於MySQL來說,做了運算操作了,不會去理會操作後是否還是跟原索引一樣的順序,這可以理解為MySQL的一個"偷懶"行為。
1.2隱式型別轉換
你會發現圖4的查詢沒有走索引,在你對照了建表sql後你發現了原來a是int型別,去跟字串做對比,而b是字串型別去跟int做對比。那麼,為什麼圖3卻可以走索引呢?
如果你用對應的sql去查詢資料,是能準確的查詢到需要的結果資料的,這時候我們猜想肯定是資料庫做了型別轉換,那麼資料庫在欄位型別不匹配時,是把字串轉換成int型別,還是把int型別轉換成字串呢?
測試sql
select '10' > 9;
如果返回結果是1,那麼是字串轉成int
如果返回結果是0,那麼是int轉成字串
從結果上來看,是把字串轉換成了int。
回到一開始的圖3和圖4。圖3中a='1'
會把字串1轉換成int型別的1,所以不影響索引。圖4中b=1
會把字串的b轉換成int型別,實際操作為CAST(b AS signed int) = 1
。對索引欄位進行了函式操作,優化器在選擇索引時會放棄走樹索引搜尋功能。
1.3隱式編碼轉換
待補充。。。(原因:查詢很多資料說兩表編輯不一樣,例如utf8與utf8mp4,聯表查詢會導致索引失效,但我不能重現啊。)
1.4掃描行數過多
使用哪個索引是在優化器時決定的,在決定因素中就有一個是sql執行需要掃描的行數,當行數過多時,優化器會決定不使用該索引,例如下面:
需要掃描的行數是一個預估值,可以使用show index from ts_ab;
進行檢視Cardinality
字值的估值,有時也會因這個預估值的不準確而導致走了全表掃描而沒走索引,這時我們可以使用analyze table ts_ab;
讓資料庫重新取樣估值,例如我執行完後重新查詢的結果便走了索引。
當然,這個辦法不可控。如果你的環境是真的要走a索引準確無誤的話,可以使用 force index(ind_a)
強行走索引。
1.5最左字首沒使用對
【MySQL】索引中介紹了最左字首的定義:最左字首原則指的是隻要sql滿足最左字首,就可以利用索引進行高效的查詢。最左字首可以是聯合索引的最左N個欄位,也可以是字串索引的最左M個字元。如果我們使用最左字首時沒理解好定義來操作,是不能使用索引的。
圖11中沒真正的使用字串索引的最左M個字元進行查詢。還有就是如果聯合索引為INDEX index_a_b (a, b)
,這時候根據最左字首原則可以對a單獨使用索引,但卻不可以對b單獨使用索引。這個可以自己嘗試下。
2、等待鎖
2.1表級別的鎖或行鎖都會使查詢處於等待狀態。
表級別鎖分為表鎖和元資料鎖MDL(meta data lock)。
表鎖使用lock tables t1 read, t2 write
。執行這個命令後,當前執行緒只能對錶t1讀,對錶t2讀書,不能對錶t1寫。其他執行緒寫t1、請寫t2都會被阻塞。
MDL是在mysql5.5時引入的,當對一個表進行增刪改查時對錶加鎖,當對一個表做資料結構變更時加鎖,在加鎖時會進行阻塞。
上述鎖可以使用show processlist
進行檢視,會提示Waiting for table metadata lock
。
2.2flush
在session1中執行select sleep(1) from ts_ef
,在session2中執行flush tables ts_ef
,在session3中執行select * from ts_ef where id = 1
。這時session3中的查詢會被卡住,使用show processlist
進行檢視,會提示Waiting for table flush
。
這個是因為在session1中的sleep(1)
是指執行1萬秒,導致session2中的flush被卡住,進而影響了session3。
2.3行鎖
java開發中會使用事務,當進行for update查詢或增改刪時會對對應行進行鎖定,這時如果事務會影響其他sql。
使用show processlist
檢視會出現State欄位為statistics
。這時可以使用select * from sys.innodb_lock_waits;
進行檢視。
圖12和圖13拼接起來看,這裡的資訊非常全面,blocking_pid
指出是119837的執行緒被卡住了,可以使用KILL 119837
結束此執行緒。
3、刷髒頁
WAL:全稱為Write Ahead Log。在資料更新的時候,InnoDB會先更新日誌(redo log)並更新記憶體,再寫磁碟。具體來說就是一條更新InnoDB會先寫入到redo log中,然後更新記憶體,那麼這條更新就算是完成了,至於什麼時候會更新到磁碟中,InnoDB會在適當的時候進行更新。
髒頁:記憶體中的資料頁跟磁碟中的資料頁不一致。
乾淨頁:記憶體中的資料刷入了磁碟後,跟磁碟的資料一致。
InnoDB什麼時候會進行刷髒頁呢?一般是出現下面四種情況的時候
1、redo log日誌被寫滿了,必須先把資料刷一部分到磁碟中。
2、記憶體不夠用時,會淘汰一部分資料頁,當淘汰的剛好是髒頁時,必須刷回磁碟。
3、mysql認為系統比較空閒的時候。
4、mysql正常關閉的時候。
4、undo log
oracle預設事務隔離級別為讀提交,mysql預設事務隔離級別為可重複讀。可重複讀是指一個事務執行過程中看到的資料,總是跟這個事務在啟動時看到的資料是一樣的。
資料庫中每一行的資料都是有多個版本的,每個版本都有自己的row trx_id。undo log(回滾日誌)會記錄每個更新的過程。在需要查詢低版本的資料時,會根據當前版本的資料與undo log進行計算得出。
根據可重複讀的定義,如果A事務啟動時表id=1的欄位num=1,此時A事務先執行別的邏輯。期間啟動B事務,進行update t set num=num+1 where id=1
,加到100000。此時資料庫中id=1的num為100000,這時A事務查詢到的結果應該是num=1才正確。mysql就會執行上面回滾版本的過程了,查詢到A事務啟動時行對應的版本號資料,這個過程會耗費一定的時間,所以此時一個簡單的查詢select num from t where id=1
都會比平時花費時間大很多。
注:
redo log通常是物理日誌,記錄的是資料頁的物理修改,而不是某一行或某幾行修改成怎樣怎樣,它用來恢復提交後的物理資料頁(恢復資料頁,且只能恢復到最後一次提交的位置)。
undo用來回滾行記錄到某個版本。undo log一般是邏輯日誌,根據每行記錄進行記錄