丁奇45總結
查詢語句執行流程:聯結器-(快取)-分析器-優化器-執行器-儲存引擎
快取缺點:對一個表執行一次更新操作後,整個表的快取會被清空,8.0已棄用快取功能
----02-------------------------------------------
更新語句執行流程:聯結器-(快取)-分析器-優化器-執行器-innodb-redolog-更新快取-返回客戶端......系統空閒時:將redolog-》磁碟
redolog(粉板):innodb引擎特有日誌
如果redolog滿了-》將redolog寫入磁碟騰出空間,涉及redolog兩個座標 writepos和checkpoint,checkpoint指哪擦哪,writepos指哪寫哪,writepos追著checkpoint跑
WAL:write-ahead-logging,粉板與賬本配合,先寫日誌再寫磁碟的技術
crash-safe:有了redo log,InnoDB就可以保證即使資料庫發生異常重啟,之前提交的記錄都不會丟失,這個能力稱為crash-safe。
binlog:MySQL的Server層實現的,所有引擎都可以使用
redo log是物理日誌,記錄的是“在某個資料頁上做了什麼修改”;binlog是邏輯日誌,記錄的是這個語句的原始邏輯,比如“給ID=2這一行的c欄位加1 ”。
redo log是迴圈寫的,空間固定會用完;binlog是可以追加寫入的。“追加寫”是指binlog檔案寫到一定大小後會切換到下一個,並不會覆蓋以前的日誌。
資料庫備份是通過binlog,當從備份資料庫恢復資料時用的是binlog
binlog和redolog的兩階段提交:
執行器呼叫innodb引擎查詢待更新資料-innodb檢視資料是否在快取頁,在則返回,不在則從磁碟讀取到記憶體再返回-
執行器執行欄位操作-寫入儲存引擎快取並寫入redolog(狀態preparing)-告訴執行器可以提交了-執行器寫binlog-執行器傳送命令給innodb將redolog狀態改為commit
如果不用兩階段提交會有什麼問題?
先寫binlog後寫redolog:binlog中有,redolog中沒有(磁碟中也沒有),從備庫恢復資料時磁碟中由沒有變成有了
先寫redolog後寫binlog:redolog中有(磁碟中有),binlog中沒有,從備庫恢復資料時磁碟中由有變成沒有了
----03-------------------------------------------
隔離級別
讀未提交、讀提交、可重複讀、序列化
事務檢視、不同隔離級別下讀資料結果不一樣、事務回滾、長事務
MVCC:多版本併發控制,用於事務回滾。
假設一個值從1被按順序改成了2、3、4,不同時刻啟動的事務會有不同的read-view(一致性讀檢視),其中記錄的值分別是1、2、4,同一條記錄在系統中可以存在多個版本。
-------04----------------------------------------
索引模型:hash表、陣列、搜尋樹,各自場景及優缺點
innodb索引模型為B+Tree,聚集索引、二級索引
索引維護:B+樹為了維護索引有序性,在插入新值的時候需要做必要的維護,涉及申請資料頁、頁分裂、頁合併
哪些場景要使用自增主鍵?
考慮因素:
1.缺點:二級索引的葉節點儲存的是主鍵的值,要考慮主鍵過長時對儲存空間的影響
2.優點:使用自增主鍵插入資料時,不會涉及聚簇索引維護的問題,因為是有序插入
為什麼要重建索引?有什麼好處?使索引檔案更緊湊節省空間
---------05--------------------------------------
innodb回表查詢、覆蓋索引不回表
最左索引原則:聯合索引的最左M個欄位 / 字串索引的最左N個字元
當建立聯合索引(a,b)後,還需要建立一個(b)索引,這時需要考慮ab欄位所佔的空間,如果b佔空間更大,可以考慮將索引換成(b,a)(a)
索引下推(5.6之後才有):索引(a,b)-》select x from y where a like "z%" and b="k" and c="m"; 在聯合索引中先匹配到a,然後再下推比較b是否滿足,然後再回表查c是否滿足。減少回表次數。
---------06--------------------------------------
全域性鎖:
使整個資料庫處於只讀狀態,所有更新、建表、更改表結構操作都會被阻塞,適用於對資料庫做邏輯備份(binlog)時使用
FTWRL:mysql提供的加全域性鎖的命令Flush tables with read lock
表級鎖:
表鎖:一般是在資料庫引擎不支援行鎖的時候才會被用到
元資料鎖(MDL):當對一個表增加列、刪除列等修改元資料的操作(DDL)時,會自動加上MDL寫鎖,如果同時有兩個增加列的操作,則要排隊獲取鎖;
當對錶進行增刪改查操作(DML)時要加上MDL讀鎖,這時修改元資料的操作由於讀鎖被未釋放所以獲取不到寫鎖,所以操作被阻塞,保證使用者在增刪改查時表結構不會被更改
為表增加/修改/刪除欄位、增加索引,這些操作會掃描全表,大表儘量不要輕易做這些操作,比如卡表。
session A先啟動,這時候會對錶t加一個MDL讀鎖。由於session B需要的也是MDL讀鎖,因此可以正常執行。
事務中的MDL鎖,在語句執行開始時申請,等到整個事務提交後再釋放。
之後session C會被blocked,是因為session A的MDL讀鎖還沒有釋放,而session C需要MDL寫鎖,因此只能被阻塞。
如果只有session C自己被阻塞還沒什麼關係,但是之後所有要在表t上新申請MDL讀鎖的請求也會被session C阻塞。前面我們說了,所有對錶的增刪改查操作都需要先申請MDL讀鎖,就都被鎖住,等於這個表現在完全不可讀寫了。
如果某個表上的查詢語句頻繁,而且客戶端有重試機制,也就是說超時後會再起一個新session再請求的話,這個庫的執行緒很快就會爆滿。
小表增加欄位導致資料庫掛掉
如何安全為小表增加欄位? kill長事務、為DDL命令增加等待時間
解決長事務,事務不提交,就會一直佔著MDL鎖。
在MySQL的information_schema 庫的 innodb_trx 表中,你可以查到當前執行中的事務。如果你要做DDL變更的表剛好有長事務在執行,要考慮先暫停DDL,或者kill掉這個長事務。
但考慮一下這個場景。如果你要變更的表是一個熱點表,雖然資料量不大,但是上面的請求很頻繁,而你不得不加個欄位,你該怎麼做呢?
這時候kill可能未必管用,因為新的請求馬上就來了。比較理想的機制是,在alter table語句裡面設定等待時間,如果在這個指定的等待時間裡面能夠拿到MDL寫鎖最好,拿不到也不要阻塞後面的業務語句,先放棄。之後開發人員或者DBA再通過重試命令重複這個過程。
MariaDB已經合併了AliSQL的這個功能,所以這兩個開源分支目前都支援DDL NOWAIT/WAIT n這個語法。
ALTER TABLE tbl_name NOWAIT add column ...
ALTER TABLE tbl_name WAIT N add column ...
如何安全為小表增加欄位
-----07------------------------------------------
行鎖:
innodb支援,myisam不支援
兩階段鎖協議:行鎖是在需要的時候才加上,但要等到事務結束時才釋放。
如果你的事務中需要鎖多個行,要把最可能造成鎖衝突、最可能影響併發度的鎖儘量往後放
死鎖:
當併發系統中不同執行緒出現迴圈資源依賴,涉及的執行緒都在等待別的執行緒釋放資源時,就會導致這幾個執行緒都進入無限等待的狀態,稱為死鎖
應對策略:
1.直接進入等待,直到超時,預設50s
2.主動死鎖檢測(預設開啟):每個新來的被堵住的執行緒,都要判斷會不會由於自己的加入導致了死鎖,這期間要消耗大量的CPU資源。
怎麼解決在 熱點行更新時進行大量死鎖檢測,導致的效能問題呢?
考慮將熱點行擴充套件為n行,每次選一行更新,衝突概率變成原來的1/n,可以減少鎖等待個數,也就減少了死鎖檢測的CPU消耗。
---08事務隔離性----知識點較多----------------------------------------
事務陣列、低水位高水位、行鎖
---09--------------------------------------------
普通索引與唯一索引對比
主鍵是一種約束,唯一索引是一種索引,兩者在本質上是不同的。
主鍵建立後一定包含一個唯一性索引,唯一性索引並不一定就是主鍵。
唯一性索引列允許空值,而主鍵列不允許為空值。
主鍵列在建立時,已經預設為非空值 + 唯一索引了。
主鍵可以被其他表引用為外來鍵,而唯一索引不能。
一個表最多隻能建立一個主鍵,但可以建立多個唯一索引。
主鍵和唯一索引都可以有多列。
主鍵更適合那些不容易更改的唯一標識,如自動遞增列、身份證號等。
在 RBO 模式下,主鍵的執行計劃優先順序要高於唯一索引。 兩者可以提高查詢的速度。
主鍵和唯一索引的區別
1.查詢對比:效能差距不大。在普通索引中查到第一條滿足條件的記錄後還要繼續向後查(如果查到資料頁末尾,需要繼續查下個數據頁),直到查到不滿足條件的記錄為止;唯一索引查到第一條記錄後就結束了。
用於被更新的資料頁不在快取中時,不將磁碟資料頁讀入快取,而是將更新操作寫入changeBuffer,減少磁碟IO和記憶體佔用,提高更新效率
當下次有查詢查到這個資料頁時,再將它讀到快取,然後將change buffer中的更新操作merge到資料快取頁中,從而保證一致性。
後臺執行緒會定期merge,正常關閉資料庫也會merge
changeBuffer
2.更新對比:
2.1 更新的資料頁在記憶體中:插入資料時,唯一索引需要先判斷唯一性,其他方面兩種索引沒啥區別。
2.2更新的資料頁不在記憶體中:
唯一索引:需要先判斷保證資料在表中不存在,這個操作必須要將資料頁讀入快取進行判斷。資料頁都已經在記憶體中了,changebuffer優化機制就用不上了。
普通索引:寫入changebuffer就結束了。
changebuffer使用場景:
1.適用寫多讀少場景,如果更新完資料馬上就要讀,會頻繁觸發changebuffer的merge操作將資料頁讀入快取,changebuffer機制反而成了負擔
2.只對普通索引的更新操作有優化作用,對唯一索引沒用
有個DBA的同學跟我反饋說,他負責的某個業務的庫記憶體命中率突然從99%降低到了75%,整個系統處於阻塞狀態,更新語句全部堵住。而探究其原因後,我發現這個業務有大量插入資料的操作,而他在前一天把其中的某個普通索引改成了唯一索引,大量的插入操作都要先查詢資料頁是否在記憶體中,結果大量不在,都需要先將資料頁讀入記憶體,記憶體命中率降低,bufferpool中記憶體資料量暴漲導致更新停滯,必須進行清除記憶體資料頁以騰出記憶體的操作。
將 普通索引 改成 唯一索引 導致的線上問題
changebuffer對比redolog:
redo log 主要節省的是隨機寫磁碟的IO消耗(隨機寫磁碟表資料 轉成 順序寫redolog日誌)
change buffer主要節省的則是隨機讀磁碟的IO消耗(當需要查詢的時候才將資料頁讀入快取)
現在要插入兩條記錄 insert into t(id,k) values(id1,k1),(id2,k2);
(id1,k1)
所在資料頁在快取中,(id2,k2)
所在資料頁不在快取中
1.(id1,k1)
插入快取頁,結束。(id2,k2)
插入操作寫入bufferpool中的changebuffer中,結束
2.兩個插入操作均寫入redolog
3.後臺執行緒將changebuffer記憶體與磁碟中內容的同步、後臺執行緒將bufferpool中快取頁與磁碟儲存同步
現在要查詢兩條記錄select * from t where k in (k1, k2);
1.(id1,k1)
所在資料頁在快取中,直接拿到
2.(id2,k2)
所在資料頁不在快取中,需將資料頁讀入快取,取changebuffer中更新操作更新快取頁,返回正確結果
---10--------------------------------------------
explain rows欄位是預估待掃描行數,是根據 n個數據頁中的平均條目數*資料頁總數 得到的近似值
優化器選錯索引,有時是由於rows預估錯誤
1.可使用analyze table xxx 重新統計索引資訊
2.可在sql中使用force index("idx_xxx")強制指定索引
3.修改sql引導優化器使用目標索引
4.刪除無用索引
----11-------------------------------------------
字串字首索引
使用字串前n個字元建立索引,而不是整個字串建立索引
優點:索引檔案儲存內容變少,節省空間
缺點:與全字串索引相比可能會損失索引區分度、增加回表次數、使覆蓋索引失效,所以需合理決定字首長度
使用字首索引,定義好長度,就可以做到既節省空間,又不用額外增加太多的查詢成本
如何決定字首長度?
對比不同字首長度的去重記錄數
select
count(distinct email) as L,
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6
from SUser
SQL
如何優化字首索引?
比如身份證號倒序儲存,使用倒序索引增加區分度
使用hash索引,但會消耗CPU資源
---12--------------------------------------------
sql語句變慢,可能原因:
1.innodb記憶體爆滿-》更新停滯-》淘汰資料頁騰出記憶體-》如果資料頁是髒葉則要進行刷盤同步操作(刷盤後操作對應的redolog就可以擦除了)
2.redolog中writepos追上checkpoint-》更新停滯-》將部分redolog日誌刷盤同步到磁碟
InnoDB刷髒頁的控制策略:
innodb_max_dirty_pages_pct 是髒頁比例上限,預設值是75%
innodb_io_capacity 引數告訴Innodb磁碟的IO處理能力,建議設成磁碟的IOPS值,這個引數決定innodb將一個髒頁刷盤時的速率【實戰場景:MySQL的寫入速度很慢,TPS很低,但是資料庫主機的IO壓力並不大。經過一番排查,發現罪魁禍首就是這個引數的設定出了問題。】
innodb_flush_neighbors 引數決定刷髒頁時是否將相鄰的髒頁一起刷盤,優點是減少了多次刷盤時的隨機IO消耗,缺點是如果是業務操作觸發的刷盤,業務響應時間會變長。機械磁碟適合開啟連坐刷盤,固態硬碟沒必要開啟。8.0中已預設關閉連坐刷盤
InnoDB怎麼計算刷髒頁的速度?
0.InnoDB全力刷髒頁的速度 = innodb_io_capacity引數值(記為S)
1. InnoDB根據當前的髒頁比例(髒頁/總頁數,假設為M),算出一個範圍在0到100之間的數字F1,M與F1成正比,即髒頁越多刷盤速度越快。
2.InnoDB根據redolog當前writepos和checkpoint對應日誌序號之間的差值(假設為N,N越大說明兩個指標之間的日誌範圍越大,又因為跨度是環形的,代表馬上就要追上,需要更快的速率刷盤才行),也算出一個範圍在0到100之間的數字F2。N與F2成正比。
3. InnoDB刷髒頁的速度 = max(F1,F2)% * S
---14--------------------------------------------
Innodb中的count(*)需要全表掃描:
需要判斷每行是不是對當前事務可見,以保證innodb預設的可重複讀隔離級別。(比如啟動事務A執行count(*),執行完之前事務B插入了一條記錄,那為了保證可重複讀,這條記錄對事務A是不可見的)
Innodb中對count(*)的優化:
對於count(*)這樣的操作,遍歷哪個索引樹得到的結果邏輯上都是一樣的。主鍵索引樹的葉子節點是資料,而普通索引樹的葉子節點是主鍵值。因此,MySQL優化器會優先使用普通索引樹來遍歷,但是如果有where語句,且where中的條件不在普通索引中,沒法利用索引下推,還是他要回表,這種情況下innodb還是會走全表掃描(專案中count全表時帶了delstate=“01”導致走全表掃描)
---15--------------------------------------------
正常執行中的例項,資料寫入後的最終落盤,是從redo log更新過來的還是從buffer pool更新過來的呢?
redo log更新快取頁,快取頁變髒頁,髒頁最終寫入磁碟
---16--------------------------------------------
select city,name,age from t where city='杭州' order by name limit 1000;
orderby排序原理:
order_buffer 資料庫分配的用於排序的記憶體空間
1.如果索引是聯合索引(city,name),那麼order by name可以去掉,因為name本身就是有序儲存
2.如果索引是(city),有兩種排序方式:
max_length_for_sort_data 引數:控制使用哪種排序方式,比如city、name、age 這三個欄位的定義總長度是36,此引數>=36使用2.1,此引數<36使用2.2
2.1 全欄位排序:通過city索引拿到主鍵後回表,取需返回的三個欄位的值放到order_buffer中,
如果order_buffer記憶體夠用,直接在記憶體中進行快排後返回。
如果order_buffer記憶體不夠,需使用若干個臨時檔案一起排序,各自有序後通過歸併演算法排出最終結果後返回。
如果記憶體大,優先選此方式,效率高。
2.2 rowid排序:通過city索引拿到主鍵後回表,只取id和name放到order_buffer中,
如果order_buffer記憶體夠用,直接在記憶體中進行快排後需要再通過id回表查city和age的值。
如果order_buffer記憶體不夠,需使用若干個臨時檔案一起排序,各自有序後通過歸併演算法排出最終結果後,需要再通過id回表查city和age的值。
此方式節約記憶體,但是增加了一次回表查詢,效率不如2.1。
---34--------------------------------------------
join的兩種演算法:
Index Nested-Loop Join:join欄位是“被驅動表”的索引,驅動表全表掃描,每次拿一條資料-》取出join欄位的值-》在被驅動表的索引中找-》回被驅動表取資料-》合併資料
Block Nested-Loop Join:join欄位不是“被驅動表”的索引,驅動表將整表的資料讀入join_buffer,如果join_buffer_size不足容納整個表,就分多次讀入,每次讀入一批後-》對被驅動表全表掃描對比join_buffer中匹配-》每次取被驅動表的一條資料,拿到這條資料中join欄位的值-》去join_buffer中匹配-》直到被驅動表全部匹配完,將join_buffer清空-》將驅動表剩餘資料讀入join_buffer-》再進行一遍被驅動表全表匹配
join優化點:
使用小表做驅動表
被驅動表的join欄位要有索引
被驅動表的join欄位沒有索引只能使用Block Nested-Loop Join演算法,很危險,如果硬要使用,調大join_buffer_size
explain中extra欄位中若有 using join buffer(Block Nested Loop),這種join語句會佔用大量記憶體,而且被驅動表可能會進行多次全表掃描,儘量不要用