建表
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一般是邏輯日志,根據每行記錄進行記錄。