詳解MySQL的主鍵查詢?yōu)槭裁催@么快
這篇文章會(huì)讓大家清楚地明白:
- 什么是InnoDB行格式?InnoDB頁(yè)是什么?
- InnoDB頁(yè)和InnoDB行格式都有哪些字段信息?
- 為什么推薦使用自增ID作為主鍵,而不推薦使用UUID?
- InnoDB設(shè)計(jì)者如何設(shè)計(jì)高效算法,快速在一個(gè)頁(yè)中搜索記錄。
正文開始!
注:我們接下來(lái)的所有描述,針對(duì)的都是InnoDB存儲(chǔ)引擎,如果涉及到其他存儲(chǔ)引擎,將會(huì)特殊說(shuō)明
1. 初探InnoDB行格式(ROW_FORMAT)
我們平時(shí)都是以記錄為單位向MySQL的表中插入數(shù)據(jù)的,這些記錄在磁盤中的存放的格式就是InnoDB的行格式。
為了證明我不是瞎說(shuō),舉個(gè)例子,我查詢一下本地?cái)?shù)據(jù)庫(kù)以forward
開頭的數(shù)據(jù)表的行格式
我們平時(shí)很少操作行格式,所以對(duì)這個(gè)概念可能不是很清楚。其實(shí)InnoDB存儲(chǔ)引擎為我們提供了4種不同的行格式
DYNAMIC
(默認(rèn)的行格式)COMPACT
REDUNDANT
COMPRESSED
我們可以在創(chuàng)建表時(shí)指定行格式(如果不指定,默認(rèn)行格式為DYNAMIC
),比如我指定row_format_table
表的行格式為COMPACT
mysql> CREATE TABLE row_format_table( -> id INT, -> c1 VARCHAR(10), -> c2 CHAR(10), -> PRIMARY KEY(id) -> ) CHARSET=utf8 ROW_FORMAT=COMPACT;
語(yǔ)法了解到這一步就可以了,接下來(lái)我們看一下4種行格式的具體表現(xiàn)形式,畫個(gè)圖就是
從圖中可以看出,一條完整的記錄可以分為「記錄的額外信息」和「真實(shí)數(shù)據(jù)信息」兩部分,4種行格式的不同也主要體現(xiàn)在「真實(shí)數(shù)據(jù)信息」這一部分。也就是說(shuō),不同的行格式采用了不同的數(shù)據(jù)格式來(lái)存儲(chǔ)我們的真實(shí)數(shù)據(jù),至于有什么具體的不同,對(duì)我們這篇文章并不重要,不需要關(guān)注。
next_record
表示下一條記錄的相對(duì)位置,有了這個(gè)字段,記錄之間可以串聯(lián)成一個(gè)單鏈表,這個(gè)比較好理解,看看圖吧。至于其他的字段信息,我們用到的時(shí)候再介紹就好了。
注意:圖中所列字段的排列順序(包括下文即將提及的)并非是InnoDB行格式在存儲(chǔ)設(shè)備上的真實(shí)存儲(chǔ)順序,為了方便說(shuō)明接下來(lái)的故事,此處我做了簡(jiǎn)化,大家理解思想即可
2. 引入InnoDB頁(yè)
對(duì)于MySQL的任何存儲(chǔ)引擎而言,數(shù)據(jù)都是存儲(chǔ)在磁盤中的,存儲(chǔ)引擎要操作數(shù)據(jù),必須先把磁盤中的數(shù)據(jù)加載到內(nèi)存中才可以。
那么問(wèn)題來(lái)了,一次性從磁盤中加載多少數(shù)據(jù)到內(nèi)存中合適呢?當(dāng)獲取記錄時(shí),InnoDB存儲(chǔ)引擎需要一條條地把記錄從磁盤中讀取出來(lái)嗎?
當(dāng)然不行!我們知道磁盤的讀寫速度和內(nèi)存讀寫速度差了幾個(gè)數(shù)量級(jí),如果我們需要讀取的數(shù)據(jù)恰好運(yùn)行在磁盤的不同位置,那就意味著會(huì)產(chǎn)生多次I/O操作。
因此,無(wú)論是操作系統(tǒng)也好,MySQL存儲(chǔ)引擎也罷,都有一個(gè)預(yù)讀取的概念。概念的依據(jù)便是統(tǒng)治計(jì)算機(jī)界的局部性原理。
空間局部性:如果當(dāng)前數(shù)據(jù)是正在被使用的,那么與該數(shù)據(jù)空間地址臨近的其他數(shù)據(jù)在未來(lái)有更大的可能性被使用到,因此可以優(yōu)先加載到寄存器或主存中提高效率
就是當(dāng)磁盤上的一塊數(shù)據(jù)被讀取的時(shí)候,我們干脆多讀一點(diǎn),而不是用多少讀多少。
InnoDB存儲(chǔ)引擎將數(shù)據(jù)劃分為若干個(gè)頁(yè)
,以頁(yè)
作為磁盤和內(nèi)存之間交互的最小單位。InnoDB中頁(yè)的大小默認(rèn)為16KB。也就是默認(rèn)情況下,一次最少?gòu)拇疟P中讀取16KB的數(shù)據(jù)到內(nèi)存中,一次最少把內(nèi)存中16KB的內(nèi)容刷新到磁盤上。
對(duì)于InnoDB存儲(chǔ)引擎而言,所有的數(shù)據(jù)(存儲(chǔ)用戶數(shù)據(jù)的索引、各種元數(shù)據(jù)、系統(tǒng)數(shù)據(jù))都是以頁(yè)的形式進(jìn)行存儲(chǔ)的。
InnoDB頁(yè)的種類很多,比如存放Insert Buffer
信息的頁(yè),存放undo日志
信息的頁(yè)等,不過(guò)我們今天不關(guān)注其他亂七八糟的頁(yè)。這篇文章的主角是存放我們表中記錄的頁(yè),姑且稱之為數(shù)據(jù)頁(yè)
吧。
3. 數(shù)據(jù)頁(yè)的結(jié)構(gòu)
很顯然,數(shù)據(jù)頁(yè)也會(huì)有自己的格式表示,像行格式一樣,我先列出兩個(gè)我們用到的字段,其他的用到再說(shuō)吧。
3.1 用戶記錄是如何存放的
我們實(shí)際存儲(chǔ)的數(shù)據(jù)表記錄會(huì)按照指定的行格式存儲(chǔ)到圖中的User Records
部分,如果當(dāng)前的數(shù)據(jù)頁(yè)是新生成的,還沒(méi)有任何記錄的話,User Records
部分其實(shí)并不會(huì)存在,而是從Free Space
部分申請(qǐng)一塊空間劃分到User Records
部分,當(dāng)Free Space
空間全部用完(或者剩余的空間已經(jīng)不足以承載新數(shù)據(jù))的時(shí)候,意味著當(dāng)前數(shù)據(jù)頁(yè)的空間被占滿了,如果繼續(xù)插入記錄,就需要申請(qǐng)新的數(shù)據(jù)頁(yè)了,示意圖如下:
要注意的是,上圖中的各條記錄之間通過(guò)next_record
字段串聯(lián)成了一個(gè)單鏈表,只不過(guò)我沒(méi)有在圖中畫出來(lái)罷了。
但是,只是串聯(lián)起來(lái)就可以了嗎?
如果讓我們來(lái)設(shè)計(jì)串聯(lián)的規(guī)則的話,我們肯定希望能夠按照某種“大小關(guān)系”來(lái)確定串聯(lián)的順序,而不是單純按照插入數(shù)據(jù)的順序,畢竟我們是學(xué)過(guò)數(shù)據(jù)結(jié)構(gòu)的人?。?/p>
可是記錄之間能比較大小嗎?能啊,這篇文章的題目就是關(guān)于主鍵啊,我們可以按照主鍵的順序,從小到大來(lái)串聯(lián)當(dāng)前數(shù)據(jù)頁(yè)中的所有記錄。事實(shí)上,MySQL的設(shè)計(jì)者也確實(shí)是這么設(shè)計(jì)的。
如果你足夠叛逆,你可能會(huì)想,你不設(shè)置主鍵的話是不是MySQL就崩了?。?/p>
當(dāng)我們沒(méi)有設(shè)置主鍵的時(shí)候,為了防止這種情況,InnoDB會(huì)優(yōu)先選取一個(gè)Unique鍵
作為主鍵,如果表中連Unique鍵
也沒(méi)有的話,就會(huì)自動(dòng)為每一條記錄添加一個(gè)叫做DB_ROW_ID
的列作為默認(rèn)主鍵,只不過(guò)這個(gè)主鍵我們看不到罷了。
下面我們補(bǔ)充一下行格式
再次強(qiáng)調(diào)
- 我畫的字段的順序并非在存儲(chǔ)設(shè)備中實(shí)際存儲(chǔ)的順序
- 只有在InnoDB實(shí)在無(wú)法確定主鍵的情況下(創(chuàng)建時(shí)不指定主鍵,同時(shí)沒(méi)有
Unique鍵
),才會(huì)添加DB_ROW_ID
列
3.2 番外:為什么推薦使用自增ID作為主鍵,而不推薦使用UUID?
說(shuō)到這,順便談一談為什么推薦使用自增ID作為主鍵,而不推薦使用UUID?
除了UUID主鍵索引占據(jù)大量空間的問(wèn)題之外,在插入數(shù)據(jù)的資源開銷上,自增ID也遠(yuǎn)小于UUID。由于數(shù)據(jù)頁(yè)中的記錄是按照主鍵從小到大進(jìn)行串聯(lián)的,自增ID決定了后來(lái)插入的記錄一定會(huì)排列在上一條記錄的后面,只需要簡(jiǎn)單添加next_record
指針就可以了;如果當(dāng)前數(shù)據(jù)頁(yè)寫滿,那就放心地直接插入新的數(shù)據(jù)頁(yè)中就可以了。
而UUID不同,它的大小順序是不確定的,后來(lái)插入的記錄有可能(而且概率相當(dāng)大)插入到上一條記錄之前(甚至是當(dāng)前數(shù)據(jù)頁(yè)之前),這就意味著需要遍歷當(dāng)前數(shù)據(jù)頁(yè)的記錄(或者先找到相關(guān)的數(shù)據(jù)頁(yè)),然后找到自己的位置進(jìn)行插入;如果當(dāng)前數(shù)據(jù)頁(yè)寫滿了,只能先找到適合自己位置的數(shù)據(jù)頁(yè),然后在數(shù)據(jù)頁(yè)中遍歷記錄找到自己的合適位置進(jìn)行插入。
因此使用UUID的方式插入記錄花費(fèi)的時(shí)間更長(zhǎng)。
3.3 數(shù)據(jù)頁(yè)自帶的兩條偽記錄
實(shí)際上,InnoDB的設(shè)計(jì)者在InnoDB頁(yè)中添加了兩條偽記錄,一條Infimum
,一條Supremum
。并且設(shè)計(jì)者規(guī)定,當(dāng)前數(shù)據(jù)頁(yè)的任何用戶記錄都比Infimum
大,任何用戶記錄都比Supremum
小。
因?yàn)槭莻斡涗洠孕枰?code>User Records中的內(nèi)容區(qū)分開,所以把這兩條偽記錄放在了一個(gè)叫做Infimum+Supremum
的部分,見下圖:
最終在數(shù)據(jù)頁(yè)中,用戶記錄的保存形式就成了這個(gè)樣子:
上圖中我把真實(shí)數(shù)據(jù)信息中的主鍵id值畫了出來(lái),方便我們后續(xù)進(jìn)行解釋。
你可能不太理解InnoDB設(shè)計(jì)者為什么要無(wú)緣無(wú)故添加這兩個(gè)字段,這倆貨對(duì)我們的搜索工作看起來(lái)沒(méi)有任何好處。
沒(méi)錯(cuò),這倆貨不是方便我們?cè)跀?shù)據(jù)頁(yè)中檢索數(shù)據(jù)而添加的,他們發(fā)揮作用的戰(zhàn)場(chǎng)是MySQL的LOCK_GAP記錄鎖。啥?不懂?沒(méi)事兒,我就是提一嘴而已,對(duì)這篇文章沒(méi)啥用,具體以后再說(shuō)。。。
3.4 數(shù)據(jù)頁(yè)中主鍵的高效查詢方案
到目前為止,我們已經(jīng)知道了在一個(gè)數(shù)據(jù)頁(yè)中,用戶記錄是按照主鍵由小到大的順序串聯(lián)而成的單向鏈表。接下來(lái)我們要解決的就是如何在一個(gè)數(shù)據(jù)頁(yè)中根據(jù)主鍵值搜索數(shù)據(jù)了。
如果我們執(zhí)行下面這條查詢語(yǔ)句
SELECT * FROM row_format_table WHERE id = 4;
最簡(jiǎn)單的辦法就是遍歷當(dāng)前頁(yè)面的所有記錄,從Infimum
記錄開始沿著單向鏈表進(jìn)行搜索,直到找到id為4的記錄為止。記錄數(shù)量少的時(shí)候還好說(shuō),這要是有成千上萬(wàn)條,那誰(shuí)能受的了。
所以InnoDb設(shè)計(jì)者想出了一種絕妙的搜索方法,把數(shù)據(jù)頁(yè)中的所有記錄(包括偽記錄)分成若干個(gè)小組,每個(gè)小組選出組內(nèi)最大的一條記錄作為“小組長(zhǎng)”,接著把所有小組長(zhǎng)的地址拿出來(lái),編成目錄。
這就好比我們?nèi)W(xué)校找人,我們只知道他是幾年級(jí)的(確定數(shù)據(jù)頁(yè)),然后再問(wèn)問(wèn)每個(gè)班主任有沒(méi)有這個(gè)人(數(shù)據(jù)頁(yè)中的小組),而不是上來(lái)就直接遍歷整個(gè)年級(jí)的所有人。
為了使這種方案最大程度上發(fā)揮它的檢索效率(不能隨便分組,畢竟一個(gè)數(shù)據(jù)頁(yè)分成一個(gè)組或者每條記錄獨(dú)占一個(gè)分組跟遍歷也沒(méi)什么區(qū)別),所以InnoDB的設(shè)計(jì)者規(guī)定了如下分組方案:
Infimum
偽記錄單獨(dú)分成一個(gè)組Supremum
偽記錄所在分組的記錄條數(shù)只能在1~8條之間- 其余分組的記錄條數(shù)只能在4~8條之間
規(guī)則是這樣,可是InnoDB怎么確定每個(gè)組內(nèi)有多少個(gè)組員呢?設(shè)計(jì)者又想了一個(gè)辦法,給“小組長(zhǎng)”添加一個(gè)屬性,記錄這個(gè)組內(nèi)一共有多少個(gè)組員(包括自己)。所以我們?cè)贁U(kuò)充一下行格式:
小組長(zhǎng)的n_owned
值是組員的個(gè)數(shù)(包括自己),組員的n_owned
值就是0。
接下來(lái)我們向表中多添加幾條數(shù)據(jù),看看分組到底是什么回事兒?需要注意的是,由于我們已經(jīng)在表中指定了主鍵id,因此DB_ROW_ID
這個(gè)參數(shù)不會(huì)再畫出來(lái)了。
上圖中的所有記錄(包括偽記錄)分成了4個(gè)小組,每個(gè)小組的“組長(zhǎng)”被單獨(dú)提拔,單獨(dú)編制成“目錄”,InnoDB官方稱之為「槽
」。槽在物理空間中是連續(xù)的,意味著通過(guò)一個(gè)槽可以很輕松地找到它的上一個(gè)和下一個(gè),這一點(diǎn)非常重要。
槽的編號(hào)從0開始,我們查找數(shù)據(jù)的時(shí)候先找到對(duì)應(yīng)的槽,然后再到小組中進(jìn)行遍歷即可,因?yàn)橐粋€(gè)小組內(nèi)的記錄數(shù)量并不多,遍歷的性能損耗可以忽略。而且每個(gè)槽代表的“組長(zhǎng)”的主鍵值也是從小到大進(jìn)行排列的,所以我們可以用二分法進(jìn)行槽的快速查找。
圖中包含4個(gè)槽,分別是0
、1
、2
、3
,二分法查找之前,最低的槽low=0
,最高的槽high=3
?,F(xiàn)在我們?cè)賮?lái)看看在這個(gè)數(shù)據(jù)頁(yè)中,我們查詢id為7的記錄,過(guò)程是怎樣的。
- 使用二分法,計(jì)算中間槽的位置,
(0+3)/2=1
,查看槽1
對(duì)應(yīng)的“組長(zhǎng)”的主鍵值為4
,因?yàn)?code>4<7,所以設(shè)置low=1
,high
保持不變; - 再次使用二分法,計(jì)算中間槽的位置,
(1+3)/2=2
,查看槽2
對(duì)應(yīng)的“組長(zhǎng)”的主鍵值為8
,因?yàn)?code>8>7,所以設(shè)置high=2
,low
保持不變; - 現(xiàn)在
high=2
,low=1
,兩者相差1,已經(jīng)沒(méi)有必要繼續(xù)進(jìn)行二分了,可以確定我們的記錄就在槽2
中,并且我們也能知道槽2
對(duì)應(yīng)的“組長(zhǎng)”的主鍵是8
,但是記錄之間是單向鏈表,我們無(wú)法向前遍歷。上文提到過(guò),我們可以通過(guò)槽2
找到槽1
,進(jìn)而找到它的“組長(zhǎng)”,然后沿著“組長(zhǎng)”向下遍歷直到找到主鍵為7的記錄就可以了。
說(shuō)到這里,我們已經(jīng)非常清楚在一個(gè)數(shù)據(jù)頁(yè)中是如何根據(jù)主鍵進(jìn)行搜索的。但是對(duì)于我們這篇文章的主題——MySQL的主鍵查詢?yōu)槭裁催@么快,只能算是回答了一半,畢竟在數(shù)據(jù)頁(yè)中進(jìn)行搜索的前提是你得先找到數(shù)據(jù)頁(yè)啊。這就是每次面試必問(wèn)的MySQL索引的知識(shí)了,下一篇文章再介紹吧。
4. 重要!數(shù)據(jù)頁(yè)的其他字段
最后再補(bǔ)充幾個(gè)知識(shí)點(diǎn),文章中有兩個(gè)問(wèn)題我并沒(méi)有講
- 槽是怎樣被存儲(chǔ)的?
- 二分查找的時(shí)候,怎么知道目前有幾個(gè)槽呢?
先回答第1個(gè)問(wèn)題,我們上文介紹過(guò)數(shù)據(jù)頁(yè)的結(jié)構(gòu),其實(shí)并不完整,下面我們?cè)僖胍粋€(gè)字段Page Directory
,槽就是保存在了這個(gè)字段信息里。
Page Directory
翻譯成中文就是「頁(yè)目錄」,這么一來(lái)是不是更加深了你對(duì)槽這種目錄的理解呢?
至于第2個(gè)問(wèn)題,其實(shí)也是關(guān)于數(shù)據(jù)頁(yè)結(jié)構(gòu)的,之前沒(méi)有一下子全畫出來(lái),因?yàn)槲矣X得需要的時(shí)候再加上更有助于記憶。
接下來(lái)我把所有之后會(huì)用到的數(shù)據(jù)頁(yè)的結(jié)構(gòu)都給大家畫出來(lái)(很簡(jiǎn)單,別害怕),暫時(shí)沒(méi)用的就屏蔽掉了,之后用到再說(shuō)吧。
FIL_PAGE_OFFSET
InnoDB頁(yè)的頁(yè)號(hào),相當(dāng)于這個(gè)頁(yè)的身份證
FIL_PAGE_PREV
,FIL_PAGE_NEXT
看圖你就明白了吧,每個(gè)頁(yè)之間都是雙向鏈表
FIL_PAGE_TYPE
InnoDB頁(yè)的種類很多,比如我們這篇文章講的數(shù)據(jù)頁(yè),還有其他的比如存放Insert Buffer
信息的頁(yè),存放undo日志
信息的頁(yè)等,這個(gè)字段就是用來(lái)標(biāo)識(shí)頁(yè)面的類型的
PAGE_N_DIR_SLOTS
這個(gè)字段保存的就是槽的個(gè)數(shù)了,二分法就是根據(jù)這個(gè)字段的值來(lái)確定high
的值
PAGE_LAST_INSERT
當(dāng)前頁(yè)面最后插入記錄的PAGE_N_RECS
位置,當(dāng)有新記錄插入的時(shí)候,直接讀取這個(gè)數(shù)據(jù),將新記錄放到相應(yīng)位置就可以了
該頁(yè)中記錄的數(shù)量(不包括最小和最大記錄)
到此這篇關(guān)于詳解MySQL的主鍵查詢?yōu)槭裁催@么快的文章就介紹到這了,更多相關(guān)MySQL的主鍵查詢內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解MySQL監(jiān)控工具 mysql-monitor
這篇文章主要介紹了解MySQL監(jiān)控工具 mysql-monitor的相關(guān)知識(shí),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2020-07-07mysql創(chuàng)建函數(shù)出現(xiàn)1418錯(cuò)誤的解決辦法
本篇文章是對(duì)在Mysql中創(chuàng)建函數(shù)報(bào)“ERROR 1418”的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-06-06mysql數(shù)據(jù)庫(kù)刪除重復(fù)數(shù)據(jù)只保留一條方法實(shí)例
這篇文章主要給大家介紹了關(guān)于mysql數(shù)據(jù)庫(kù)刪除重復(fù)數(shù)據(jù),只保留一條的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-03-03MySQL創(chuàng)建和刪除數(shù)據(jù)表的命令及語(yǔ)法詳解
這篇文章主要介紹了MySQL創(chuàng)建和刪除數(shù)據(jù)表的命令及語(yǔ)法,是MySQL入門學(xué)習(xí)中的基礎(chǔ)知識(shí),需要的朋友可以參考下2015-11-11Workbench通過(guò)遠(yuǎn)程訪問(wèn)mysql數(shù)據(jù)庫(kù)的方法詳解
這篇文章主要給大家介紹了Workbench通過(guò)遠(yuǎn)程訪問(wèn)mysql數(shù)據(jù)庫(kù)的相關(guān)資料,文中通過(guò)圖文介紹的非常詳細(xì),對(duì)大家具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起看看吧。2017-06-06MYSQL必知必會(huì)讀書筆記第七章之?dāng)?shù)據(jù)過(guò)濾
這篇文章主要介紹了MYSQL必知必會(huì)讀書筆記第七章之?dāng)?shù)據(jù)過(guò)濾的相關(guān)資料,需要的朋友可以參考下2016-05-05