Mysql深入浅出学习
文章目錄
- MySQL邏輯架構(gòu)圖
- redo log
- 為什么要redo log?
- binLog
- 為什么會(huì)有兩份日志?
- 兩種日志的區(qū)別
- 簡單update語句執(zhí)行流程
- 兩階段提交
- 事務(wù)隔離
- 什么是事務(wù)?
- 隔離性與隔離級別
- 事務(wù)隔離的實(shí)現(xiàn)?
- 索引
- InnoDB的索引模型
- 覆蓋索引
- 最左前綴原則
- 索引下推
- 鎖
- 鎖的類型有哪些?
- 全局鎖
- 表級鎖
- 行鎖
- 死鎖和死鎖檢測
- 普通索引和唯一索引,怎么選?
- MySQL為什么有時(shí)候會(huì)選錯(cuò)索引?
- 怎么給字符串字段加索引?
- SQL語句突然“變慢”?
- 為什么表數(shù)據(jù)刪掉一半,表文件大小不變?
- 為什么count(*)這么慢?
- 不同的count用法
- “order by”是怎么工作的?
- 按字段排序
- rowid排序
- 查詢響應(yīng)慢排查
- MDL鎖
- flush
- 等行鎖
- 一致性讀原因?qū)е?/li>
- 什么是幻讀?
- 幻讀有什么問題?
- 如何解決幻讀?
- MySQL加鎖規(guī)則解讀
- MySQL加鎖原則
- 案例一:等值查詢間隙鎖
- 案例二:非唯一索引等值鎖
- 案例三:主鍵索引范圍鎖
- 案例四:非唯一索引范圍鎖
- 案例五:唯一索引范圍鎖bug
- 案例六:非唯一索引上存在“等值”的例子
- 案例七:非唯一索引上存在“等值”的例子
- 案例八:死鎖例子
- 索引失效反面案例
- 條件字段函數(shù)操作
- 隱式類型轉(zhuǎn)換
- 隱式字符編碼轉(zhuǎn)換
- 慢查詢處理技巧
- 索引沒有設(shè)計(jì)好
- 語句沒寫好
- MySQL選錯(cuò)了索引
學(xué)習(xí)自jdh莫老師
MySQL邏輯架構(gòu)圖
redo log
為什么要redo log?
如果每一次的更新操作都需要寫進(jìn)磁盤,然后磁盤也要找到對應(yīng)的那條記錄,然后再更新,整個(gè)過程 IO 成本、查找成本都很高。為了解決這個(gè)問題,MySQL 的設(shè)計(jì)者采用了WAL技術(shù)(Write-Ahead Logging),關(guān)鍵點(diǎn)就是先寫日志,再寫磁盤,這個(gè)日志就是redo log。redo log是InnoDB引擎特有的日志。
binLog
為什么會(huì)有兩份日志?
redo log 是 InnoDB 引擎特有的日志,而 Server 層也有自己的日志,稱為 binlog(歸檔日志)。那為什么會(huì)有兩份日志呢?因?yàn)樽铋_始 MySQL 里并沒有 InnoDB 引擎。MySQL 自帶的引擎是 MyISAM,但是 MyISAM 沒有 crash-safe 的能力,binlog 日志只能用于歸檔。而 InnoDB 是另一個(gè)公司以插件形式引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,所以 InnoDB 使用另外一套日志系統(tǒng)——也就是 redo log 來實(shí)現(xiàn) crash-safe 能力。
兩種日志的區(qū)別
redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實(shí)現(xiàn)的,所有引擎都可以使用。
redo log 是物理日志,記錄的是“在某個(gè)數(shù)據(jù)頁上做了什么修改”;binlog 是邏輯日志,記錄的是這個(gè)語句的原始邏輯,比如“給 ID=2 這一行的 c 字段加 1 ”。
redo log 是循環(huán)寫的,空間固定會(huì)用完;binlog 是可以追加寫入的。“追加寫”是指 binlog 文件寫到一定大小后會(huì)切換到下一個(gè),并不會(huì)覆蓋以前的日志。
簡單update語句執(zhí)行流程
update T set c=c+1 where ID=2
兩階段提交
由于 redo log 和 binlog 是兩個(gè)獨(dú)立的邏輯,如果不用兩階段提交,數(shù)據(jù)庫的狀態(tài)就有可能和用它的日志恢復(fù)出來的庫的狀態(tài)不一致。redo log 和 binlog 都可以用于表示事務(wù)的提交狀態(tài),而兩階段提交就是讓這兩個(gè)狀態(tài)保持邏輯上的一致。假設(shè)不用兩階段提交,會(huì)出現(xiàn)如下情況
- 先寫 redo log 后寫 binlog。假設(shè)在 redo log 寫完,binlog 還沒有寫完的時(shí)候,MySQL 進(jìn)程異常重啟。由于我們前面說過的,redo log 寫完之后,系統(tǒng)即使崩潰,仍然能夠把數(shù)據(jù)恢復(fù)回來,所以恢復(fù)后這一行 c 的值是 1。但是由于 binlog 沒寫完就 crash 了,這時(shí)候 binlog 里面就沒有記錄這個(gè)語句。因此,之后備份日志的時(shí)候,存起來的 binlog 里面就沒有這條語句。然后你會(huì)發(fā)現(xiàn),如果需要用這個(gè) binlog 來恢復(fù)臨時(shí)庫的話,由于這個(gè)語句的 binlog 丟失,這個(gè)臨時(shí)庫就會(huì)少了這一次更新,恢復(fù)出來的這一行 c 的值就是 0,與原庫的值不同。先寫 binlog 后寫 redo log。如果在 binlog 寫完之后 crash,由于 redo log 還沒寫,崩潰恢復(fù)以后這個(gè)事務(wù)無效,所以這一行 c 的值是 0。但是 binlog 里面已經(jīng)記錄了“把 c 從 0 改成 1”這個(gè)日志。所以,在之后用 binlog 來恢復(fù)的時(shí)候就多了一個(gè)事務(wù)出來,恢復(fù)出來的這一行 c 的值就是 1,與原庫的值不同。
- 先寫 binlog 后寫 redo log。如果在 binlog 寫完之后 crash,由于 redo log
還沒寫,崩潰恢復(fù)以后這個(gè)事務(wù)無效,所以這一行 c 的值是 0。但是 binlog 里面已經(jīng)記錄了“把 c 從 0 改成1”這個(gè)日志。所以,在之后用 binlog 來恢復(fù)的時(shí)候就多了一個(gè)事務(wù)出來,恢復(fù)出來的這一行 c 的值就是 1,與原庫的值不同。
事務(wù)隔離
什么是事務(wù)?
簡單來說,事務(wù)就是要保證一組數(shù)據(jù)庫操作,要么全部成功,要么全部失敗。MySQL 是一個(gè)支持多引擎的系統(tǒng),但并不是所有的引擎都支持事務(wù)。比如 MySQL 原生的 MyISAM 引擎就不支持事務(wù),這也是 MyISAM 被 InnoDB 取代的重要原因之一。
隔離性與隔離級別
隔離性是事務(wù)ACID(Atomicity、Consistency、Isolation、Durability,即原子性、一致性、隔離性、持久性)中的I,當(dāng)數(shù)據(jù)庫上有多個(gè)事務(wù)同時(shí)執(zhí)行的時(shí)候,就可能出現(xiàn)臟讀(dirty read)、不可重復(fù)讀(non-repeatable read)、幻讀(phantom read)的問題,為了解決這些問題,就有了“隔離級別”的概念。SQL 標(biāo)準(zhǔn)的事務(wù)隔離級別包括:讀未提交(read uncommitted)、讀提交(read committed)、可重復(fù)讀(repeatable read)和串行化(serializable )
讀未提交是指,一個(gè)事務(wù)還沒提交時(shí),它做的變更就能被別的事務(wù)看到。
讀提交是指,一個(gè)事務(wù)提交之后,它做的變更才會(huì)被其他事務(wù)看到。
可重復(fù)讀是指,一個(gè)事務(wù)執(zhí)行過程中看到的數(shù)據(jù),總是跟這個(gè)事務(wù)在啟動(dòng)時(shí)看到的數(shù)據(jù)是一致的。當(dāng)然在可重復(fù)讀隔離級別下,未提交變更對其他事務(wù)也是不可見的。
串行化,顧名思義是對于同一行記錄,“寫”會(huì)加“寫鎖”,“讀”會(huì)加“讀鎖”。當(dāng)出現(xiàn)讀寫鎖沖突的時(shí)候,后訪問的事務(wù)必須等前一個(gè)事務(wù)執(zhí)行完成,才能繼續(xù)執(zhí)行。
Oracle 數(shù)據(jù)庫的默認(rèn)隔離級別是“讀提交”,MySQL則是“可重復(fù)讀”
事務(wù)隔離的實(shí)現(xiàn)?
在 MySQL 中,實(shí)際上每條記錄在更新的時(shí)候都會(huì)同時(shí)記錄一條回滾操作。記錄上的最新值,通過回滾操作,都可以得到前一個(gè)狀態(tài)的值。假設(shè)一個(gè)值從 1 被按順序改成了 2、3、4,在回滾日志里面就會(huì)有類似下面的記錄。當(dāng)前值是 4,但是在查詢這條記錄的時(shí)候,不同時(shí)刻啟動(dòng)的事務(wù)會(huì)有不同的 read-view。如圖中看到的,在視圖 A、B、C 里面,這一個(gè)記錄的值分別是 1、2、4,同一條記錄在系統(tǒng)中可以存在多個(gè)版本,就是數(shù)據(jù)庫的多版本并發(fā)控制(MVCC)
索引
InnoDB的索引模型
在 MySQL 中,索引是在存儲引擎層實(shí)現(xiàn)的,所以并沒有統(tǒng)一的索引標(biāo)準(zhǔn),即不同存儲引擎的索引的工作方式并不一樣。在 InnoDB 中,表都是根據(jù)主鍵順序以索引的形式存放的,這種存儲方式的表稱為索引組織表。InnoDB 使用了 B+ 樹索引模型,所以數(shù)據(jù)都是存儲在 B+ 樹中的。每一個(gè)索引在 InnoDB 里面對應(yīng)一棵 B+ 樹。
create table T(id int primary key, k int not null, name varchar(16),index (k))engine=InnoDB;表中 R1~R5 的 (ID,k) 值分別為 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),兩棵樹的示例示意圖如下。
覆蓋索引
在一個(gè)查詢里面,索引 k 已經(jīng)“覆蓋了”我們的查詢需求,我們稱為覆蓋索引。由于覆蓋索引可以減少樹的搜索次數(shù),顯著提升查詢性能,所以使用覆蓋索引是一個(gè)常用的性能優(yōu)化手段。例如如果有一個(gè)高頻請求,要根據(jù)市民的身份證號查詢他的姓名,建立一個(gè)(身份證號、姓名)的聯(lián)合索引就有意義了,可以在這個(gè)高頻請求上用到覆蓋索引,不再需要回表查整行記錄,減少語句的執(zhí)行時(shí)間。
最左前綴原則
B+ 樹這種索引結(jié)構(gòu),可以利用索引的“最左前綴”,來定位記錄。比如說已經(jīng)有了(name,age)這個(gè)聯(lián)合索引,SQL 語句的條件是"where name like ‘張 %’"。這時(shí),你也能夠用上這個(gè)索引,查找到第一個(gè)符合條件的記錄是 ID3,然后向后遍歷,直到不滿足條件為止。這個(gè)最左前綴可以是聯(lián)合索引的最左 N 個(gè)字段,也可以是字符串索引的最左 M 個(gè)字符。已經(jīng)有了 (a,b) 這個(gè)聯(lián)合索引后,一般就不需要單獨(dú)在 a 上建立索引了
索引下推
MySQL 5.6 引入的索引下推優(yōu)化(index condition pushdown), 可以在索引遍歷過程中,對索引中包含的字段先做判斷,直接過濾掉不滿足條件的記錄,減少回表次數(shù)。
鎖
鎖的類型有哪些?
根據(jù)加鎖的范圍,MySQL 里面的鎖大致可以分成全局鎖、表級鎖和行鎖三類。
全局鎖
全局鎖就是對整個(gè)數(shù)據(jù)庫實(shí)例加鎖。MySQL 提供了一個(gè)加全局讀鎖的方法,命令是 Flush tables with read lock (FTWRL)。當(dāng)你需要讓整個(gè)庫處于只讀狀態(tài)的時(shí)候,可以使用這個(gè)命令,之后其他線程的以下語句會(huì)被阻塞:數(shù)據(jù)更新語句(數(shù)據(jù)的增刪改)、數(shù)據(jù)定義語句(包括建表、修改表結(jié)構(gòu)等)和更新類事務(wù)的提交語句。全局鎖的典型使用場景是,做全庫邏輯備份。一般只有不支持事務(wù)的數(shù)據(jù)庫引擎才會(huì)用到這種鎖,如MyISAM
表級鎖
MySQL 里面表級別的鎖有兩種:一種是表鎖,一種是元數(shù)據(jù)鎖(meta data lock,MDL)。表鎖的語法是 lock tables … read/write。在 MySQL 5.5 版本中引入了 MDL鎖,每執(zhí)行一條DML、DDL語句時(shí)都會(huì)申請MDL鎖,DML操作需要MDL讀鎖,DDL操作需要MDL寫鎖(MDL加鎖過程是系統(tǒng)自動(dòng)控制,無法直接干預(yù),讀讀共享,讀寫互斥,寫寫互斥)。MDL讀鎖之間不互斥,因此可以有多個(gè)線程同時(shí)對一張表增刪改查。MDL讀寫鎖之間、寫鎖之間是互斥的,用來保證變更表結(jié)構(gòu)操作的安全性。因此,如果有兩個(gè)線程要同時(shí)給一個(gè)表加字段,其中一個(gè)要等另一個(gè)執(zhí)行完才能開始執(zhí)行。
行鎖
MySQL 的行鎖是在引擎層由各個(gè)引擎自己實(shí)現(xiàn)的。但并不是所有的引擎都支持行鎖,比如 MyISAM 引擎就不支持行鎖。不支持行鎖意味著并發(fā)控制只能使用表鎖,對于這種引擎的表,同一張表上任何時(shí)刻只能有一個(gè)更新在執(zhí)行,這就會(huì)影響到業(yè)務(wù)并發(fā)度。InnoDB 是支持行鎖的,這也是 MyISAM 被 InnoDB 替代的重要原因之一。
在 InnoDB 事務(wù)中,行鎖是在需要的時(shí)候才加上的,但并不是不需要了就立刻釋放,而是要等到事務(wù)結(jié)束時(shí)才釋放。這個(gè)就是兩階段鎖協(xié)議。如果事務(wù)中需要鎖多個(gè)行,要把最可能造成鎖沖突、最可能影響并發(fā)度的鎖盡量往后放。
死鎖和死鎖檢測
當(dāng)并發(fā)系統(tǒng)中不同線程出現(xiàn)循環(huán)資源依賴,涉及的線程都在等待別的線程釋放資源時(shí),就會(huì)導(dǎo)致這幾個(gè)線程都進(jìn)入無限等待的狀態(tài),稱為死鎖。針對死鎖,InnoDB有兩種解決策略
一種策略是,直接進(jìn)入等待,直到超時(shí)。這個(gè)超時(shí)時(shí)間可以通過參數(shù) innodb_lock_wait_timeout 來設(shè)置。在 InnoDB 中,innodb_lock_wait_timeout 的默認(rèn)值是 50s,意味著如果采用這個(gè)策略,當(dāng)出現(xiàn)死鎖以后,第一個(gè)被鎖住的線程要過 50s 才會(huì)超時(shí)退出,然后其他線程才有可能繼續(xù)執(zhí)行。對于在線服務(wù)來說,這個(gè)等待時(shí)間往往是無法接受的。但是超時(shí)時(shí)間設(shè)置太短的話,會(huì)出現(xiàn)很多誤傷。
另一種策略是,發(fā)起死鎖檢測,發(fā)現(xiàn)死鎖后,主動(dòng)回滾死鎖鏈條中的某一個(gè)事務(wù),讓其他事務(wù)得以繼續(xù)執(zhí)行。將參數(shù) innodb_deadlock_detect 設(shè)置為 on,表示開啟這個(gè)邏輯。
普通索引和唯一索引,怎么選?
這兩類索引在查詢能力上是沒差別的,主要考慮的是對更新性能的影響。對唯一索引來說,所有的更新操作都要先判斷這個(gè)操作是否違反唯一性約束。比如,要插入 (4,400) 這個(gè)記錄,就要先判斷現(xiàn)在表中是否已經(jīng)存在 k=4 的記錄,而這必須要將數(shù)據(jù)頁讀入內(nèi)存才能判斷。如果都已經(jīng)讀入到內(nèi)存了,那直接更新內(nèi)存會(huì)更快,就沒必要使用 change buffer 了。因此,唯一索引的更新就不能使用 change buffer,實(shí)際上也只有普通索引可以使用。將數(shù)據(jù)從磁盤讀入內(nèi)存涉及隨機(jī) IO 的訪問,是數(shù)據(jù)庫里面成本最高的操作之一。
綜上所述,建議盡量選擇普通索引,如果所有的更新后面,都馬上伴隨著對這個(gè)記錄的查詢,應(yīng)該關(guān)閉 change buffer。而在其他情況下,change buffer 都能提升更新性能。在實(shí)際使用中,普通索引和 change buffer 的配合使用,對于數(shù)據(jù)量大的表的更新優(yōu)化還是很明顯的。
MySQL為什么有時(shí)候會(huì)選錯(cuò)索引?
隨著表數(shù)據(jù)的不斷增刪,可能會(huì)出現(xiàn)MySQL 選錯(cuò)索引,根本原因是MySQL沒能準(zhǔn)確地判斷出掃描行數(shù)或者錯(cuò)誤地計(jì)算了執(zhí)行成本,這種是低概率事件,但是某些場景下有可能觸發(fā)MySQL的這個(gè)bug,解決方法一般有以下幾種
由于索引統(tǒng)計(jì)信息不準(zhǔn)確導(dǎo)致的問題,可以用 analyze table 來解決
采用 force index 強(qiáng)行選擇一個(gè)索引,不過這個(gè)弊端也很明顯,一來這么寫不優(yōu)雅,二來如果索引改了名字,這個(gè)語句也得改,顯得很麻煩。而且如果以后遷移到別的數(shù)據(jù)庫的話,這個(gè)語法還可能會(huì)不兼容。
修改語句,引導(dǎo) MySQL 使用我們期望的索引。 如把select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1改成select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b,a limit 1
新建一個(gè)更合適的索引,來提供給優(yōu)化器做選擇,或刪掉誤用的索引。
怎么給字符串字段加索引?
主要看該字段的區(qū)分度是不是足夠大,建索引是否有必要。如果有必要,建索引有兩種方法,一種是整個(gè)字段索引,如果字段值普遍較長,則可能會(huì)消耗較大的存儲空間,另一種是使用前綴索引,定義好長度,就可以做到既節(jié)省空間,又不用額外增加太多的查詢成本。如:alter table SUser add index index2(email(7))。那問題來了,當(dāng)要給字符串創(chuàng)建前綴索引時(shí),有什么方法能夠確定應(yīng)該使用多長的前綴呢?
select count(distinct email) as L from SUser;
select count(distinct left(email,4))as L4, count(distinct left(email,5))as L5, count(distinct left(email,6))as L6, count(distinct left(email,7))as L7,from SUser;
但是使用前綴索引,就用不上覆蓋索引對查詢性能的優(yōu)化了
小技巧:倒序存儲和使用 hash 字段
SQL語句突然“變慢”?
InnoDB 在處理更新語句的時(shí)候,只做了寫日志這一個(gè)磁盤操作,這個(gè)日志叫作 redo log(重做日志)。平時(shí)執(zhí)行很快的更新操作,其實(shí)就是在寫內(nèi)存和日志,而 MySQL 偶爾“抖”一下的那個(gè)瞬間,可能就是在刷臟頁(flush)。MySQL在下面這些情境下會(huì)做flush操作
InnoDB 的 redo log 寫滿了。這時(shí)候系統(tǒng)會(huì)停止所有更新操作,把 checkpoint 往前推進(jìn),redo log 留出空間可以繼續(xù)寫。這種情況是 InnoDB 要盡量避免的。因?yàn)槌霈F(xiàn)這種情況的時(shí)候,整個(gè)系統(tǒng)就不能再接受更新了
系統(tǒng)內(nèi)存不足。當(dāng)需要新的內(nèi)存頁,而內(nèi)存不夠用的時(shí)候,就要淘汰一些數(shù)據(jù)頁,空出內(nèi)存給別的數(shù)據(jù)頁使用。如果淘汰的是“臟頁”,就要先將臟頁寫到磁盤。這種情況其實(shí)是常態(tài)。但是出現(xiàn)以下這兩種情況,都是會(huì)明顯影響性能的
一個(gè)查詢要淘汰的臟頁個(gè)數(shù)太多,會(huì)導(dǎo)致查詢的響應(yīng)時(shí)間明顯變長;
日志寫滿,更新全部堵住,寫性能跌為 0,這種情況對敏感業(yè)務(wù)來說,是不能接受的。
要用到 innodb_io_capacity 這個(gè)參數(shù)了,它會(huì)告訴 InnoDB 數(shù)據(jù)庫主機(jī)的磁盤能力。這個(gè)值建議設(shè)置成磁盤的 IOPS。磁盤的 IOPS 可以通過 fio 這個(gè)工具來測試。并且平時(shí)要多關(guān)注臟頁比例(innodb_max_dirty_pages_pct),不要讓它經(jīng)常接近 75%。如果是SSD盤,innodb_flush_neighbors參數(shù)設(shè)置成0
MySQL 認(rèn)為系統(tǒng)“空閑”的時(shí)候。
MySQL 正常關(guān)閉
為什么表數(shù)據(jù)刪掉一半,表文件大小不變?
delete 命令其實(shí)只是把記錄的位置,或者數(shù)據(jù)頁標(biāo)記為了“可復(fù)用”,但磁盤文件的大小是不會(huì)變的。也就是說,通過 delete 命令是不能回收表空間的。這些可以復(fù)用,而沒有被使用的空間,看起來就像是“空洞”。不止是刪除數(shù)據(jù)會(huì)造成空洞,插入數(shù)據(jù)也會(huì)。也就是說,經(jīng)過大量增刪改的表,都是可能是存在空洞的。所以,如果能夠把這些空洞去掉,就能達(dá)到收縮表空間的目的。而重建表,就可以達(dá)到這樣的目的。
MySQL 5.6 版本開始引入的 Online DDL,對表重建操作流程做了優(yōu)化,可以通過執(zhí)行alter table t engine=InnoDB對表進(jìn)行重建。通過日志文件記錄和重放操作,在重建表的過程中,允許對表做增刪改操作。重建方法都會(huì)掃描原表數(shù)據(jù)和構(gòu)建臨時(shí)文件。對于很大的表來說,這個(gè)操作是很消耗 IO 和 CPU 資源的。因此,如果是線上服務(wù),需要很小心地控制操作時(shí)間。
為什么count(*)這么慢?
為什么 InnoDB 不跟 MyISAM 一樣,也把記錄總數(shù)存起來呢,查詢的時(shí)候直接返回?這是因?yàn)榧词故窃谕粋€(gè)時(shí)刻的多個(gè)查詢,由于多版本并發(fā)控制(MVCC)的原因,InnoDB 表“應(yīng)該返回多少行”也是不確定的。對于 count(*) 這樣的操作,遍歷哪個(gè)索引樹得到的結(jié)果邏輯上都是一樣的。因此,MySQL 優(yōu)化器會(huì)找到最小的那棵樹來遍歷,但是如果表的數(shù)據(jù)十分巨大,因?yàn)槭钦脴浔闅v,所以性能會(huì)非常差。如果有高頻需要獲取大表記錄總數(shù)的場景,需要自己進(jìn)行計(jì)數(shù)。
比如可以在數(shù)據(jù)庫里面建一張表,把高頻需要獲取記錄總數(shù)的大表的記錄數(shù)自己進(jìn)行維護(hù),獲取總數(shù)時(shí)候直接從該表獲取。
不同的count用法
對于 count(主鍵 id) 來說,InnoDB 引擎會(huì)遍歷整張表,把每一行的 id 值都取出來,返回給 server 層。server 層拿到 id 后,判斷是不可能為空的,就按行累加。
對于 count(1) 來說,InnoDB 引擎遍歷整張表,但不取值。server 層對于返回的每一行,放一個(gè)數(shù)字“1”進(jìn)去,判斷是不可能為空的,按行累加。count(1) 執(zhí)行得要比 count(主鍵 id) 快。因?yàn)閺囊娣祷?id 會(huì)涉及到解析數(shù)據(jù)行,以及拷貝字段值的操作。
對于 count(字段) 來說,如果這個(gè)“字段”是定義為 not null 的話,一行行地從記錄里面讀出這個(gè)字段,判斷不能為 null,按行累加;如果這個(gè)“字段”定義允許為 null,那么執(zhí)行的時(shí)候,判斷到有可能是 null,還要把值取出來再判斷一下,不是 null 才累加。
所以結(jié)論是:按照效率排序的話,count(字段)<count(主鍵 id)<count(1)≈count(),所以建議盡量使用 count()。
“order by”是怎么工作的?
按字段排序
select city,name,age from t where city='杭州' order by name limit 1000 ;
圖中“按 name 排序”這個(gè)動(dòng)作,可能在內(nèi)存中完成,也可能需要使用外部排序,這取決于排序所需的內(nèi)存和參數(shù)sort_buffer_size。sort_buffer_size,就是 MySQL 為排序開辟的內(nèi)存(sort_buffer)的大小。如果要排序的數(shù)據(jù)量小于 sort_buffer_size,排序就在內(nèi)存中完成。但如果排序數(shù)據(jù)量太大,內(nèi)存放不下,則不得不利用磁盤臨時(shí)文件輔助排序。
rowid排序
剛說的按字段排序算法有一個(gè)問題,就是如果查詢要返回的字段很多的話,那么 sort_buffer 里面要放的字段數(shù)太多,這樣內(nèi)存里能夠同時(shí)放下的行數(shù)很少,要分成很多個(gè)臨時(shí)文件,排序的性能會(huì)很差。所以rowid排序的思想是減少查詢字段的數(shù)量,節(jié)省排序內(nèi)存,但是缺點(diǎn)是有可能需要回到原表去取數(shù)據(jù)。對于 InnoDB 表來說,rowid 排序會(huì)要求回表多造成磁盤讀,因此不會(huì)被優(yōu)先選擇。
查詢響應(yīng)慢排查
MDL鎖
使用 show processlist 命令查看 Waiting for table metadata lock 的示意圖。MySQL 啟動(dòng)時(shí)需要設(shè)置 performance_schema=on,通過查詢 sys.schema_table_lock_waits 這張表,就可以直接找出造成阻塞的 process id,把這個(gè)連接用 kill 命令斷開即可。
flush
出現(xiàn) Waiting for table flush 狀態(tài)的可能情況是:有一個(gè) flush tables 命令被別的語句堵住了,然后它又堵住了select 語句。
等行鎖
select * from t where id=1 lock in share mode;由于訪問 id=1 這個(gè)記錄時(shí)要加讀鎖,如果這時(shí)候已經(jīng)有一個(gè)事務(wù)在這行記錄上持有一個(gè)寫鎖,select 語句就會(huì)被堵住。這個(gè)問題并不難分析,但問題是怎么查出是誰占著這個(gè)寫鎖。如果是 MySQL 5.7 版本,可以通過 sys.innodb_lock_waits 表查到。查詢語句:select * from t sys.innodb_lock_waits where locked_table=‘test.t’\G
一致性讀原因?qū)е?/h2>
session A 先用 start transaction with consistent snapshot 命令啟動(dòng)了一個(gè)事務(wù),之后 session B 才開始執(zhí)行 update 語句。session B 更新完 100 萬次,生成了 100 萬個(gè)回滾日志 (undo log)。
帶 lock in share mode 的 SQL 語句,是當(dāng)前讀,因此會(huì)直接讀到 1000001 這個(gè)結(jié)果,所以速度很快;而 select * from t where id=1 這個(gè)語句,是一致性讀,因此需要從 1000001 開始,依次執(zhí)行 undo log,執(zhí)行了 100 萬次以后,才將 1 這個(gè)結(jié)果返回。所以后者的執(zhí)行時(shí)間大概是前者的4000倍
什么是幻讀?
在可重復(fù)讀隔離級別下,普通的查詢是快照讀,是不會(huì)看到別的事務(wù)插入的數(shù)據(jù)的。因此,幻讀在“當(dāng)前讀”下才會(huì)出現(xiàn)。上面 session B 的修改結(jié)果,被 session A 之后的 select 語句用“當(dāng)前讀”看到,不能稱為幻讀。幻讀僅專指“新插入的行”。
幻讀有什么問題?
- 語義被破壞
session A 在 T1 時(shí)刻就聲明了,“我要把所有 d=5 的行鎖住,不準(zhǔn)別的事務(wù)進(jìn)行讀寫操作”。而實(shí)際上,這個(gè)語義被破壞了。
- 數(shù)據(jù)一致性問題
如何解決幻讀?
產(chǎn)生幻讀的原因是,行鎖只能鎖住行,但是新插入記錄這個(gè)動(dòng)作,要更新的是記錄之間的“間隙”。因此,為了解決幻讀問題,InnoDB 只好引入新的鎖,也就是間隙鎖 (Gap Lock)。顧名思義,間隙鎖,鎖的就是兩個(gè)值之間的空隙。跟間隙鎖存在沖突關(guān)系的,是“往這個(gè)間隙中插入一個(gè)記錄”這個(gè)操作。間隙鎖之間都不存在沖突關(guān)系。間隙鎖的引入,雖然解決了幻讀的問題,但是可能會(huì)導(dǎo)致同樣的語句鎖住更大的范圍,這其實(shí)是影響了并發(fā)度的。間隙鎖是在可重復(fù)讀隔離級別下才會(huì)生效的。所以,如果把隔離級別設(shè)置為讀提交的話,就沒有間隙鎖了。但同時(shí),要解決可能出現(xiàn)的數(shù)據(jù)和日志不一致問題,需要把 binlog 格式設(shè)置為 row。這,也是現(xiàn)在不少公司使用的配置組合。
next-key lock:間隙鎖和行鎖合稱 next-key lock,每個(gè) next-key lock 是前開后閉區(qū)間。表 t(下圖上方數(shù)字是主鍵值) 初始化以后,如果用 select * from t for update 要把整個(gè)表所有記錄鎖起來,就形成了 7 個(gè) next-key lock,分別是 (-∞,0]、(0,5]、(5,10]、(10,15]、(15,20]、(20, 25]、(25, +supremum]。
MySQL加鎖規(guī)則解讀
MySQL加鎖原則
原則 1:加鎖的基本單位是 next-key lock。
原則 2:查找過程中訪問到的對象才會(huì)加鎖。
優(yōu)化 1:索引上的等值查詢,給唯一索引加鎖的時(shí)候,next-key lock 退化為行鎖。
優(yōu)化 2:索引上的等值查詢,向右遍歷時(shí)且最后一個(gè)值不滿足等值條件的時(shí)候,next-key lock 退化為間隙鎖。
一個(gè) bug:唯一索引上的范圍查詢會(huì)訪問到不滿足條件的第一個(gè)值為止。
案例一:等值查詢間隙鎖
由于表 t 中沒有 id=7 的記錄,加鎖單位是 next-key lock(),session A 加鎖范圍就是 (5,10];這是一個(gè)等值查詢 (id=7),而 id=10 不滿足查詢條件,根據(jù)優(yōu)化2,next-key lock 退化成間隙鎖,因此最終加鎖的范圍是 (5,10)。
案例二:非唯一索引等值鎖
session A 要給索引 c 上 c=5 的這一行加上讀鎖。根據(jù)原則 1,加鎖單位是 next-key lock,因此會(huì)給 (0,5]加上 next-key lock。要注意 c 是普通索引,因此僅訪問 c=5 這一條記錄是不能馬上停下來的,需要向右遍歷,查到 c=10 才放棄。根據(jù)原則 2,訪問到的都要加鎖,因此要給 (5,10]加 next-key lock。但是同時(shí)這個(gè)符合優(yōu)化 2:等值判斷,向右遍歷,最后一個(gè)值不滿足 c=5 這個(gè)等值條件,因此退化成間隙鎖 (5,10)。根據(jù)原則 2 ,只有訪問到的對象才會(huì)加鎖,這個(gè)查詢使用覆蓋索引,并不需要訪問主鍵索引,所以主鍵索引上沒有加任何鎖,這就是為什么 session B 的 update 語句可以執(zhí)行完成。但 session C 要插入一個(gè) (7,7,7) 的記錄,就會(huì)被 session A 的間隙鎖 (5,10) 鎖住。在這個(gè)例子中,lock in share mode 只鎖覆蓋索引,但是如果是 for update 就不一樣了。 執(zhí)行 for update 時(shí),系統(tǒng)會(huì)認(rèn)為接下來要更新數(shù)據(jù),因此會(huì)順便給主鍵索引上滿足條件的行加上行鎖。這個(gè)例子說明,鎖是加在索引上的。如果要用 lock in share mode 來給行加讀鎖避免數(shù)據(jù)被更新的話,就必須得繞過覆蓋索引的優(yōu)化。
案例三:主鍵索引范圍鎖
開始執(zhí)行的時(shí)候,要找到第一個(gè) id=10 的行,因此本該是 next-key lock(5,10]。 根據(jù)優(yōu)化 1, 主鍵 id 上的等值條件,退化成行鎖,只加了 id=10 這一行的行鎖。范圍查找就往后繼續(xù)找,找到 id=15 這一行停下來,因此需要加 next-key lock(10,15]。所以,session A 這時(shí)候鎖的范圍就是主鍵索引上,行鎖 id=10 和 next-key lock(10,15]。這樣,session B 和 session C 的結(jié)果就能理解了。
案例四:非唯一索引范圍鎖
session A 用字段 c 來判斷,加鎖規(guī)則跟案例三唯一的不同是:在第一次用 c=10 定位記錄的時(shí)候,索引 c 上加了 (5,10]這個(gè) next-key lock 后,由于索引 c 是非唯一索引,沒有優(yōu)化規(guī)則,也就是說不會(huì)蛻變?yōu)樾墟i,因此最終 sesion A 加的鎖是,索引 c 上的 (5,10] 和 (10,15] 這兩個(gè) next-key lock。
案例五:唯一索引范圍鎖bug
session A 是一個(gè)范圍查詢,按照原則 1 的話,應(yīng)該是索引 id 上只加 (10,15]這個(gè) next-key lock,并且因?yàn)?id 是唯一鍵,所以循環(huán)判斷到 id=15 這一行就應(yīng)該停止了。但是實(shí)現(xiàn)上,InnoDB 會(huì)往前掃描到第一個(gè)不滿足條件的行為止,也就是 id=20。而且由于這是個(gè)范圍掃描,因此索引 id 上的 (15,20]這個(gè) next-key lock 也會(huì)被鎖上。
案例六:非唯一索引上存在“等值”的例子
session A 在遍歷的時(shí)候,先訪問第一個(gè) c=10 的記錄。同樣地,根據(jù)原則 1,這里加的是 (c=5,id=5) 到 (c=10,id=10) 這個(gè) next-key lock。然后,session A 向右查找,直到碰到 (c=15,id=15) 這一行,循環(huán)才結(jié)束。根據(jù)優(yōu)化 2,這是一個(gè)等值查詢,向右查找到了不滿足條件的行,所以會(huì)退化成 (c=10,id=10) 到 (c=15,id=15) 的間隙鎖。也就是說,這個(gè) delete 語句在索引 c 上的加鎖范圍,就是下圖中藍(lán)色區(qū)域覆蓋的部分。
案例七:非唯一索引上存在“等值”的例子
session A 的 delete 語句加了 limit 2。表 t 里 c=10 的記錄其實(shí)只有兩條,因此加不加 limit 2,刪除的效果都是一樣的,但是加鎖的效果卻不同。可以看到,session B 的 insert 語句執(zhí)行通過了,跟案例六的結(jié)果不同。這是因?yàn)?#xff0c;案例七里的 delete 語句明確加了 limit 2 的限制,因此在遍歷到 (c=10, id=30) 這一行之后,滿足條件的語句已經(jīng)有兩條,循環(huán)就結(jié)束了。因此,索引 c 上的加鎖范圍就變成了從(c=5,id=5) 到(c=10,id=30) 這個(gè)前開后閉區(qū)間,如下圖所示:
這個(gè)例子的指導(dǎo)意義就是,在刪除數(shù)據(jù)的時(shí)候盡量加 limit。這樣不僅可以控制刪除數(shù)據(jù)的條數(shù),讓操作更安全,還可以減小加鎖的范圍。
案例八:死鎖例子
session A 啟動(dòng)事務(wù)后執(zhí)行查詢語句加 lock in share mode,在索引 c 上加了 next-key lock(5,10] 和間隙鎖 (10,15);session B 的 update 語句也要在索引 c 上加 next-key lock(5,10] ,進(jìn)入鎖等待;然后 session A 要再插入 (8,8,8) 這一行,被 session B 的間隙鎖鎖住。由于出現(xiàn)了死鎖,InnoDB 讓 session B 回滾。你可能會(huì)問,session B 的 next-key lock 不是還沒申請成功嗎? 其實(shí)是這樣的,session B 的“加 next-key lock(5,10] ”操作,實(shí)際上分成了兩步,先是加 (5,10) 的間隙鎖,加鎖成功;然后加 c=10 的行鎖,這時(shí)候才被鎖住的。
索引失效反面案例
條件字段函數(shù)操作
select count(*) from tradelog where month(t_modified)=7;
對索引字段做函數(shù)操作,可能會(huì)破壞索引值的有序性,因此優(yōu)化器就決定放棄走樹搜索功能而遍歷整個(gè)索引的所有值。需要注意的是,即使是對于不改變有序性的函數(shù),也不會(huì)考慮使用索引。比如,對于 select * from tradelog where id + 1 = 10000 這個(gè) SQL 語句,這個(gè)加 1 操作并不會(huì)改變有序性,但是 MySQL 優(yōu)化器還是不能用 id 索引快速定位到 9999 這一行。所以,需要在寫 SQL 語句的時(shí)候,手動(dòng)改寫成 where id = 10000 -1 才可以。
隱式類型轉(zhuǎn)換
select * from tradelog where tradeid=110717;
tradeid 的字段類型是 varchar(32),而輸入的參數(shù)卻是整型,所以需要做類型轉(zhuǎn)換,導(dǎo)致走了全表掃描。MySQL默認(rèn)是將字符串轉(zhuǎn)換成數(shù)字進(jìn)行比較,所以上面的語句等價(jià)于select * from tradelog where CAST(tradid AS signed int) = 110717;觸發(fā)了上面說到的規(guī)則:對索引字段做函數(shù)操作,優(yōu)化器會(huì)放棄走樹搜索功能。
隱式字符編碼轉(zhuǎn)換
select d.* from tradelog l, trade_detail d where d.tradeid=l.tradeid and l.id=2; 由于tradelog表的tradeid字符集編碼是utf8mb4,而trade_detail表的tradeid是utf8,utf8mb4是utf8的超集,所以發(fā)生了隱式轉(zhuǎn)換,該語句等價(jià)于select d.* from tradelog l, trade_detail d where d.tradeid=CONVERT(d.traideid USING utf8mb4) and l.id=2; 觸發(fā)了上面說到的規(guī)則:對被驅(qū)動(dòng)表索引字段做函數(shù)操作,優(yōu)化器會(huì)放棄走樹搜索功能。
思考題:不調(diào)整字段編碼的情況下怎么改才能走對索引?
慢查詢處理技巧
慢查詢導(dǎo)致性能問題的三種可能情況如下
索引沒有設(shè)計(jì)好
這種場景一般就是通過緊急創(chuàng)建索引來解決。MySQL 5.6 版本以后,創(chuàng)建索引都支持 Online DDL 了,對于那種高峰期數(shù)據(jù)庫已經(jīng)被這個(gè)語句打掛了的情況,最高效的做法就是直接執(zhí)行 alter table 語句。比較理想的是能夠在備庫先執(zhí)行。假設(shè)你現(xiàn)在的服務(wù)是一主一備,主庫 A、備庫 B,這個(gè)方案的大致流程是這樣的:在備庫 B 上執(zhí)行 set sql_log_bin=off,也就是不寫 binlog,然后執(zhí)行 alter table 語句加上索引;執(zhí)行主備切換;這時(shí)候主庫是 B,備庫是 A。在 A 上執(zhí)行 set sql_log_bin=off,然后執(zhí)行 alter table 語句加上索引。
語句沒寫好
這種就要通過分析explain命令分析語句的執(zhí)行計(jì)劃,并進(jìn)行優(yōu)化,典型反面案例參考上一頁的索引失效反面案例。MySQL 5.7 提供了 query_rewrite 功能,可以把輸入的一種語句改寫成另外一種模式,當(dāng)出現(xiàn)線上業(yè)務(wù)bug導(dǎo)致QPS暴漲,數(shù)據(jù)庫也面臨宕機(jī)風(fēng)險(xiǎn)的時(shí)候,可以用這個(gè)把壓力最大的SQL直接重寫為“select 1”返回,以解燃眉之急
MySQL選錯(cuò)了索引
這種是低概率事件,具體解決方案參考上面的“MySQL為什么有時(shí)候會(huì)選錯(cuò)索引”這節(jié)
慢查詢導(dǎo)致性能問題的三種可能情況,實(shí)際上出現(xiàn)最多的是前兩種,一般在測試環(huán)境可以通過以下手段盡量規(guī)避
上線前,在測試環(huán)境,把慢查詢?nèi)罩?#xff08;slow log)打開,并且把 long_query_time 設(shè)置成 0,確保每個(gè)語句都會(huì)被記錄入慢查詢?nèi)罩?#xff1b;
在測試表里插入模擬線上的數(shù)據(jù),做一遍回歸測試;觀察慢查詢?nèi)罩纠锩款愓Z句的輸出,特別留意 Rows_examined 字段是否與預(yù)期一致。
總結(jié)
以上是生活随笔為你收集整理的Mysql深入浅出学习的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux下通过ssh上传、下载文件或者
- 下一篇: ASP.net防止SQL注入方法