MySQL索引及事務
MySQL進階
索引
索引相關的需要了解
資料結構、設計原則、優化、失效、回表、索引覆蓋、最左匹配、索引下推、聚簇索引
用途
提高查詢效率
資料和索引都是儲存在磁碟中的,在進行索引時會先將索引載入到磁碟找到對應的磁碟空間再去取資料
作業系統常識:
區域性性原理: 時間:之前被訪問過的資料很有可能再次被訪問 空間:資料和程式都有聚整合群的傾向(相同型別的資料會放一起) 磁碟預讀: 記憶體跟磁碟在進行互動的時候有一個最小的邏輯單位,這個單位稱之為頁,或者data-page,一般是4K或者8K,由作業系統決定,我們在進行資料讀取的時候,一般會讀取頁的整數倍,也就是4K,8K,16K,innodb儲存引擎在進行資料載入的時候讀取的是16KB的資料
mysql索引流程
為什麼選擇B+樹
hashmap會由於演算法的原因可能會出現雜湊碰撞或者雜湊衝突,而且要進行範圍查詢時需要挨個遍歷,效率低。
二叉樹、平衡數、紅黑樹特點
由於都是二叉樹每個節點只能存2個值導致當資料量大的時候,樹的深度增加,導致IO增加、影響查詢效率
B樹的特點
會將一些資料儲存在根節點和枝節點,如果需要id為16的資料直接從根節點獲取不會往下,如果需要id為30的資料會根據p2的指標一直往下找到30的資料。但是由於根節點和枝節點儲存了資料 會導致一旦資料量超過所能儲存的3層的極限有需要加深度,導致IO增多,影響效率,所以有了B+樹
假設一個data-page是16K,一個數據佔1K那麼排除指標和id所佔空間每個data-page最多可以存16條資料,三層最多是16 * 16 * 16 = 4096條資料 資料量大的話遠遠不夠,需要增加樹的深度
B+樹 資料全部儲存在葉子節點,根節點和枝節點全部存指標和索引,可以大大增加資料量,只是每次必須經過三層才能獲得資料。但也只需要三層,而且三層能儲存的資料量是很大的。一般情況3-4層的B+樹足以支撐千萬級別的資料量
假設一個data-page為16K,一個id+指標佔10個位元組空間,一條資料佔1K,那麼一個data-page可以存16*
1024/10 那麼三層可以存 16*1024/10 ** 2 * 16 是千萬級別的
用int做索引還是varchar做索引
看誰佔用的位元組數小int預設佔4個位元組 varchar則是根據()中的值確定所佔位元組 若所佔位元組小於4 則可以使用varchar做索引
當必須用varchar做索引時,我們可以使用字首索引,即根據前k個字元來做索引,這個k可以根據select語句查詢出來
當增加k時cnt的次數和原始資料相差不大,而且再次增加k時不會有明顯變化時,我們可以使用該欄位的前7個位元組作為索引
聚簇索引和非聚簇索引
資料和索引儲存在一起的叫做聚簇索引,沒有儲存在一起的叫做非聚簇索引
innodb儲存引擎在進行資料插入的時候,資料必須要跟某一個索引列儲存在一起,這個索引列可以是主鍵,如果沒有主鍵,選擇唯一鍵,如果沒有唯一鍵,選擇6位元組的rowid來進行儲存
此時資料必定是跟某一個索引繫結在一起的,繫結資料的索引叫做聚簇索引 其他索引的葉子節點中儲存的資料不再是整行記錄,而是聚簇索引的id值。
id name age gender
id主鍵 name普通索引
id是聚簇索引 name對應的索引的B+樹上的葉子節點儲存的就是id值
innodb中既有聚簇索引,也有非聚簇索引
myisam中由於索引和資料是分檔案儲存的 ,所以myisam只有非聚簇索引
回表
id name age gender
id主鍵 name普通索引
select * from table where name = "zhangsan";
# 索引流程
先根據name的值去name B+樹匹配到對應的葉子節點,查詢到對應記錄的id值, 再根據id去id的 B+樹中檢索整行記錄 這個過程就稱之為回表,要儘量避免回表操作
索引覆蓋
id name age gender
id主鍵 name普通索引
select id,name from table where name = "zhangsan";
# 索引流程
根據name的值去name B+樹檢索對應的記錄 能獲取到id的屬性值,索引的葉子節點中包含了查詢的所有列,此時不需要回表,這個過程叫做索引覆蓋,explain會出現 using index 的提示資訊
推薦使用 在某些場景中 可以考慮將要查詢的所有列都變成組合索引 此時會使用索引覆蓋 加快查詢效率
最左匹配
建立索引的時候可以選擇多個列來共同組成索引 此時叫做組合索引或者聯合索引 要遵循最左匹配原則
id name age gender
id主鍵 name,age聯合索引
select * from table where name="zhangsan" and age=12;
select * from table where name="zhangsan";
select * from table where age=12;
select * from table where age=12 and name="zhangsan";
# 1,2,4會走聯合索引 3不會 因為沒有滿足最左匹配原則
"""
select * from table where name="zhangsan" and age>12;
當語句變成這樣時不會影響結果
但如果 name,age,gender是聯合索引 那麼 gender的索引就會失效 導致索引失效 但是失效的也只是age後面的索引 對於name來說還是走索引的
"""
索引下推
select * from table where name="zhangsan" and age=12
# 在沒有索引下推之前
# 先根據name從儲存引擎中拉取資料到server層 然後在server層中對age進行資料過濾
# 有了索引下推之後
# 根據name和age兩個條件來做資料篩選 將篩選之後的結果返回給server層
索引下推指的時從server層推到儲存引擎層
本來只匹配name的話可能會過濾10條記錄傳送到server層 但如果索引下推 會在儲存引擎層匹配name同時匹配age會減少發生到server層的資料量
mysql是預設開啟索引下推的
優化問題
加索引
看執行計劃
優化sql語句
表結構設計
分庫分表
自己總結
特殊情況
因為建的表就四個欄位,a,b,c建聯合索引後,就意味著表裡面的所有資料都能在聯合索引裡面找到,就不需要去回表查資料,所以當然就索引生效了,但如果再加一個欄位,用b做條件查,就會發現索引無效了,這時再把查詢條件改成a=1,就會發現索引又生效了,這才是最左匹配。
還有看一下explain中的type是index,已經很明白的告訴你是去查聯合索引樹去了。而最左匹配的type一般是ref,效率比index高。
事務、鎖、MVCC
事務的四大特性:ACID
原子性、一致性、隔離性、永續性
MVCC:是多版本併發控制 為了解決在併發進行訪問時的讀寫效率 由於加鎖會影響效率
基礎知識
當前讀: 讀取得是資料的最新版本 總是讀取到最新的資料
觸發條件:
1.select ... lock in share mode # 讀鎖
2.select... for update # 寫鎖 排他鎖
3.update、delete、insert
快照讀: 讀取的是歷史版本記錄
觸發條件:
select...
事務的隔離級別
讀未提交
讀已提交:RC
可重複度:RR(是mysql的預設隔離級別)
序列化
同時開啟A、B兩個事務 A第二次select能否讀到B修改後的最新的結果?
RC:可以讀到最新的結果記錄
RR:不可以讀到最新的結果記錄
這是由事務版本可見性
MVCC及可見性演算法決定的
第一部分
隱藏欄位
每一行記錄都會包含幾個使用者不可見的欄位
DB_TRX_ID:最後一次建立或修改該記錄的事務id
DB_ROW_ID:隱藏主鍵 6個位元組
DB_ROW_PTR:回滾指標 配合undolog完成回滾
第二部分
undolog
回滾日誌
當事務2要對資料進行修改時,會先加上排他鎖 然後將當前版本的資料存到undolog中 再對資料進行修改 而且mysql也會將操作記錄記錄到binlog中 就涉及到二階段提交了。
當不同的事務對同一條記錄做修改的時候,會導致該記錄的undolog形成一個線性表,也就是連結串列,連結串列的鏈首是最新的歷史記錄,而鏈尾是最早的歷史記錄
如果有了事務4,那麼事務4讀取到的是哪一個版本的資料?還有前3個事務是否已提交?這4個事務開啟的時機?
需要按照一定的規則和演算法來進行判斷
第三部分
readview
事務在進行快照讀的時候產生的讀檢視
readview中包含的元件
trx_list:系統活躍的事務id
up_limit_id:列表中事務最小的id
low_limit_id:系統尚未分配的下一個事務id
實際場景
可見性演算法規則:
1.首先比較DB_TRX_ID<up_limit_id,如果小於,則當前事務能看到DB_TRX_ID所在的記錄,如果大於等於進入下一個判斷
2.判斷DB_TRX_ID>=low_limit_id,如果大於等於則代表DB_TRX_ID所在的記錄在ReadView生成之後才出現的,那麼對於當前事務肯定不可見,如果小於,則進入下一步判斷
3.判斷DB_TRX_ID是否在活躍事務中,如果再,則代表在ReadView生成時刻,這個事務還是活躍狀態,還沒有commit,修改的資料,當前事務也是看不到,如果不在,則說明這個事務在ReadView生成之前就已經開始commit,那麼修改的結果是能夠看見的。
產生現象的原因:
trx_list: 1,2,3
up_limit_id:1
low_limit_id:5
DB_TRX_ID:4
根據上面的可見性規則:
1.4>1——>2
2.4<5——>3
3.4不在trx_list中——>能看到修改的值
#######
而在實際中為什麼RC的隔離級別是能看到的
而RR的隔離級別是看不到的呢?
#######
因為readview生成的時機是不同的
RC:每次在進行快照讀的時候都會生成新的readview
RR:只有在第一次進行快照讀的時候才會生成readview,之後的讀操作都會用第一次生成的readview
# 這也能解釋現象2出現的原因了
因為事務4在修改之前事務2已經進行了一次快照讀,此時就已經生成了readview,所以事務2在進行第二次快照讀的用的還是第一次檢視讀的readview
# 根據readview的可見性規則
第一次快照讀時
trx_list: 1,2,3,4
up_limit_id:1
low_limit_id:5
DB_TRX_ID:0
DB_TRX_ID<up_limit_id——>事務2能看到事務0的結果也就是最初始的3條記錄
第二次快照讀時,還是用的第一次的readview,但是記錄的DB_TRX_ID已經變成4了。
trx_list: 1,2,3,4
up_limit_id:1
low_limit_id:5
DB_TRX_ID:4
4>1——>4<5——>4在trx_list中——>表示當前事務還是活躍狀態,沒有commit,所以事務2的第二次檢視讀看不到事務4修改後的記錄的。
事務四大特性是如何保證的?
Redo log二階段提交
日誌又分為隨機讀寫和順序讀寫
隨機讀寫的效率要低於順序讀寫,為了保證資料的一致性,可以先將資料通過順序讀寫的方式寫到日誌檔案中,容納後再將資料寫入到對應的磁碟檔案中,這個過程順序的效率要遠遠高於隨機的效率,換句話說,如果實際的資料沒有寫入到磁碟,只要日誌檔案儲存成功了,那麼資料就不會丟失,可以根據日誌來進行資料的恢復。
mysql的binlog和innodb的redolog這兩種日誌屬於不同的元件,所以為了保證資料的一致性,要保證binlog和redolog一致,所以有了二階段提交的概念。
先寫redolog後寫binlog:假設在redolog寫完,binlog還沒有寫完的時候,MySQL程序異常重啟,所以系統在恢復的時候會根據redolog會將c的值設為1,但是binlog中並沒有這條語句。因此,如果需要binlog來恢復臨時庫的話,這個臨時庫會少一個更新語句,恢復出來的c的值就是0,與原庫的值不同。
先寫binlog後寫redolog,也是同理。
具體流程
當redolog寫入成功後突然崩潰,那麼系統恢復時會根據redolog和binlog的比較發現binlog少一條資料,redolog處於prepare狀態,那麼redolog會將多的資料刪掉,並將prepare狀態改掉,回滾到事務發生前。
當binlog寫入成功後崩潰,恢復時比較redolog和binlog傳送兩份日誌一樣,並且redolog處於prepare狀態,則直接將事務提交即可。
幻讀
由於在一個事務中既有了快照讀,又有了當前讀,所以發生了幻讀,主要是readview傳送了變化。
解決方法:將所有的快照讀都加上讀鎖或者寫鎖改成當前讀重新生成readview。
檢視鎖
set global innodb_status_output_locks=1;
# 在事務中執行當前讀/快照讀並檢視鎖的狀態
show engine innodb status\G;
# 發生當前讀時會產生間隙鎖和意向鎖
# 當其他的事務執行插入操作時會阻塞
# 當其他事務不能對資料進行修改時就不會發生幻讀了
結論:
一個事務操作都是快照讀是不會發生幻讀的
一個事務中既有快照讀又有當前讀才會產生幻讀需要全部改成當前讀 for uodate