以 Compact 行格式爲例:
總結
刪除一條記錄,數據原有的被廢棄,記錄頭髮生變化,主要是打上了刪除標記。也就是原有的數據 deleted_flag 變成 1,代表數據被刪除。但是數據沒有被清空,在新一行數據大小小於這一行的時候,可能會佔用這一行。這樣其實就是存儲碎片,要想減少存儲碎片,可以通過重建表來實現(例如對於高併發大數據量表,除了歸檔,還可以通過利用無鎖算法Alter
修改字段來重建表增加表性能)。
Compact 行格式存儲
我們來創建一個包含幾乎所有基本數據類型的表,其他的例如 geometry,timestamp 等等,也是基於 double 還有 bigint 而來的, text、json、blob等類型,一般不與行數據一起存儲,我們之後再說:
create table record_test_1 (
id bigint,
score double,
name char(4),
content varchar(8),
extra varchar(16)
)row_format=compact;
插入如下幾條記錄:
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (1, 78.5, 'hash', 'wodetian', 'nidetiantadetian');
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (65536, 17983.9812, 'zhx', 'shin', 'nosuke');
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (NULL, -669.996, 'aa', NULL, NULL);
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (2048, NULL, NULL, 'c', 'jun');
目前表結構:
+-------+------------+------+----------+------------------+
| id | score | name | content | extra |
+-------+------------+------+----------+------------------+
| 1 | 78.5 | hash | wodetian | nidetiantadetian |
| 65536 | 17983.9812 | zhx | shin | nosuke |
| NULL | -669.996 | aa | NULL | NULL |
| 2048 | NULL | NULL | c | jun |
+-------+------------+------+----------+------------------+
查看底層存儲文件:record_test_1.ibd
,用16進制編輯器打開,我這裏使用的是Notepad++
和他的HEX-Editor
插件。可以找到如下的數據域(可能會有其中 mysql 生成的行數據不一樣,但是我們創建的行數據內容應該是一樣的,而且數據長度應該是一摸一樣的,可以搜索其中的字符找到這些數據):
我們這裏先直接給出這些數據代表的意義,讓大家直觀感受下:
變長字段長度列表:10 08
Null值列表:00
記錄頭信息:00 00 10 00 47
隱藏列DB_ROW_ID:00 00 00 00 08 0c
隱藏列DB_TRX_ID:00 00 00 03 c9 4d
隱藏列DB_ROLL_PTR:b9 00 00 01 2d 01 10
列數據id(1):80 00 00 00 00 00 00 01
列數據score(78.5):00 00 00 00 00 a0 53 40
列數據name(hash):68 61 73 68
列數據content(wodetian):77 6f 64 65 74 69 61 6e
列數據extra(nidetiantadetian):6e 69 64 65 74 69 61 6e 74 61 64 65 74 69 61 6e
變長字段長度列表:06 04
Null值列表:00
記錄頭信息:00 00 18 00 37
隱藏列DB_ROW_ID:00 00 00 00 08 0d
隱藏列DB_TRX_ID:00 00 00 03 c9 4e
隱藏列DB_ROLL_PTR:ba 00 00 01 2f 01 10
列數據id(65536):80 00 00 00 00 01 00 00
列數據score(17983.9812):b5 15 fb cb fe 8f d1 40
列數據name(zhx):7a 68 78 20
列數據content(shin):73 68 69 6e
列數據extra(nosuke):6e 6f 73 75 6b 65
Null值列表:19
記錄頭信息:00 00 00 00 27
隱藏列DB_ROW_ID:00 00 00 00 08 0e
隱藏列DB_TRX_ID:00 00 00 03 c9 51
隱藏列DB_ROLL_PTR:bc 00 00 01 33 01 10
列數據score(-669.996):87 16 d9 ce f7 ef 84 c0
列數據name(aa):61 61 20 20
變長字段長度列表:03 01
Null值列表:06
記錄頭信息:00 00 28 ff 4b
隱藏列DB_ROW_ID:00 00 00 00 08 0f
隱藏列DB_TRX_ID:00 00 00 03 c9 54
隱藏列DB_ROLL_PTR:be 00 00 01 3d 01 10
列數據id(2048):80 00 00 00 00 00 08 00
列數據content(c):63
列數據extra(jun):6a 75 6e
可以看出,在 Compact 行記錄格式下,一條 InnoDB 記錄,其結構如下圖所示:
Compact 行格式存儲 - 變長字段長度列表
對於像 varchar, varbinary,text,blob,json以及他們的各種類型的可變長度字段,需要將他們到底佔用多少字節存儲起來,這樣就省去了列數據之間的邊界定義,MySQL 就可以分清楚哪些數據屬於這一列,那些不屬於。Compact行格式存儲,開頭就是變長字段長度列表,這個列表包括數據不爲NULL的每個可變長度字段的長度,並按照列的順序逆序排列。
例如上面的第一條數據:
+-------+------------+------+----------+------------------+
| id | score | name | content | extra |
+-------+------------+------+----------+------------------+
| 1 | 78.5 | hash | wodetian | nidetiantadetian |
+-------+------------+------+----------+------------------+
有兩個數據不爲NULL的字段content
和extra
,長度分別是 8 和 16,轉換爲 16 進制分別是:0x08,0x10。倒序的順序排列就是10 08
這是對於長度比較短的情況,用一字節表示長度即可。如果變長列的內容佔用的字節數比較多,可能就需要用2個字節來表示。那麼什麼時候用一個字節,什麼時候用兩個字節呢?
我們給這張表加一列來測試下:
alter table `record_test_1`
add column `large_content` varchar(1024) null after `extra`;
這時候行數據部分並沒有變化。
如果 字符集的最大字節長度(我們這裏字符集是latin,所以長度就是1)乘以 字段最大字符個數(就是varchar裏面的參數,我們這裏的
large_content
就是1024) < 255,那麼就用一個字節表示。這裏對於large_content
,已經超過了255.如果超過255,那麼:
如果 字段真正佔用字節數 < 128,就用一個字節
如果 字段真正佔用字節數 >= 128,就用兩個字節
問題一:那麼爲什麼用 128 作爲分界線呢?一個字節可以最多表示255,但是 MySQL 設計長度表示時,爲了區分是否是一個字節表示長度,規定,如果最高位爲1,那麼就是兩個字節表示長度,否則就是一個字節。例如,01111111,這個就代表長度爲 127,而如果長度是 128,就需要兩個字節,就是 10000000 10000000,首個字節的最高位爲1,那麼這就是兩個字節表示長度的開頭,第二個字節可以用所有位表示長度,並且需要注意的是,MySQL採取 Little Endian 的計數方式,低位在前,高位在後,所以 129 就是 10000001 10000000。同時,這種標識方式,最大長度就是 2^15 - 1 = 32767,也就是32 KB。
問題二:如果兩個字節也不夠表示的長度,該怎麼辦?innoDB 頁大小默認爲 16KB,對於一些佔用字節數非常多的字段,比方說某個字段長度大於了16KB,那麼如果該記錄在單個頁面中無法存儲時,InnoDB會把一部分數據存放到所謂的溢出頁中,在變長字段長度列表處只存儲留在本頁面中的長度,所以使用兩個字節也可以存放下來。這個溢出頁機制,我們後面和Text字段一起再說。
然後對第一行數據填充large_content
字段,對於第二行,將新字段更新爲空字符串。
update `record_test_1` set `large_content` = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz' where id = 1;
update `record_test_1` set `large_content` = '' where id = 1;
查看數據:
發現COMPACT
行記錄格式下,對於變長字段的更新,會使原有數據失效,產生一條新的數據在末尾。
第一行數據原有的被廢棄,記錄頭髮生變化,主要是打上了刪除標記,這個稍後我們就會提到。第一行新數據:
變長字段長度列表:82 80 10 08
Null值列表:00
記錄頭信息:00 00 30 01 04
隱藏列DB_ROW_ID:00 00 00 00 08 0c
隱藏列DB_TRX_ID:00 00 00 03 c9 6e
隱藏列DB_ROLL_PTR:4f 00 00 01 89 1c 51
列數據id(1):80 00 00 00 00 00 00 01
列數據score(78.5):00 00 00 00 00 a0 53 40
列數據name(hash):68 61 73 68
列數據content(wodetian):77 6f 64 65 74 69 61 6e
列數據extra(nidetiantadetian):6e 69 64 65 74 69 61 6e 74 61 64 65 74 69 61 6e
列數據large_content(abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz):61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a
可以看到,變長字段長度列表變成了82 80 10 08
,這裏的large_content
字符編碼最大字節大小爲1,字段字符最大個數爲1024,這裏第一行記錄這個字段字符數量是130,所以應該用兩個字節。130*1轉換成16進製爲 0x82 也就是 0x02 + 0x80,最高位標識1之後,就是 0x82 + 0x80,對應咱們的變長字段長度列表的開頭。
而新的第二行,變長字段長度列表變成了00 06 04
,因爲實際large_content
佔用了0個字節。
Compact 行格式存儲 - NULL 值列表
某些字段可能可以爲 NULL,如果對於 NULL 還單獨存儲,是一種浪費空間的行爲,和 Compact 行格式存儲的理念相悖。採用 BitMap 的思想,標記這些字段,可以節省空間。Null值列表就是這樣的一個 BitMap。
NULL 值列表僅僅針對可以爲 NULL 的字段,如果一個字段標記了not null
,那麼這個字段不會進入這個 NUll 值列表的 BitMap 中。
NULL值列表佔用幾個字節呢?每個不爲 NULL 的字段,佔用一位,每超過八個字段,就是 8 位,就多一個字節,不足一個字節,高位補0。假如一個表所有字段都是not null
,那麼就沒有NULL 值列表,也就佔用 0 個字節。並且,每個字段在這個 bitmap 中,類似於變長字段長度列表,是逆序排列的。
+-------+------------+------+----------+------------------+------------------------------------------------------------------------------------------------------------------------------------+
| id | score | name | content | extra | large_content |
+-------+------------+------+----------+------------------+------------------------------------------------------------------------------------------------------------------------------------+
| 1 | 78.5 | hash | wodetian | nidetiantadetian | abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz |
| 65536 | 17983.9812 | zhx | shin | nosuke | lex |
| NULL | -669.996 | aa | NULL | NULL | NULL |
| 2048 | NULL | NULL | c | jun | NULL |
+-------+------------+------+----------+------------------+------------------------------------------------------------------------------------------------------------------------------------+
針對第一第二行記錄,由於沒有爲 NULL 的字段,所以他們的 NULL 值列表爲00. 針對第三行記錄,他的 NULL 字段分別是 id
,content
,extra
,large_content
,分別是第一,第四,第五,第六列,那麼 NULL 值列表爲:00111001,也就是 0x39。在加入新字段之前NULL 字段分別是 id
,content
,extra
,分別是第一,第四,第五列,那麼 NULL 值列表爲:00011001,也就是 0x19 針對第四行記錄,他的 NULL 字段分別是score
,name
,large_content
,分別是第二,第三,第六列,那麼 NULL 值列表爲:00100110,也就是 0x26。在加入新字段之前NULL 字段分別是score
,name
,分別是第二,第三列,那麼 NULL 值列表爲:00000110,也就是 0x06。
Compact 行格式存儲 - 記錄頭信息
對於Compact 行格式存儲,記錄頭固定爲5字節大小:
名稱 | 大小(bits) | 描述 |
---|---|---|
無用位 | 2 | 目前沒用到 |
deleted_flag | 1 | 記錄是否被刪除 |
min_rec_flag | 1 | B+樹中非葉子節點最小記錄標記 |
n_owned | 4 | 該記錄對應槽所擁有記錄數量 |
heap_no | 13 | 該記錄在堆中的序號,也可以理解爲在堆中的位置信息 |
record_type | 3 | 記錄類型,普通數據記錄爲000,節點指針類型爲001,僞記錄首記錄 infimum 行爲010,僞記錄最後一個記錄 supremum 行爲011,1xx的爲保留的 |
next_record pointer | 16 | 頁中下一條記錄的相對位置 |
對於更新前的第一行和第二行:
第一行記錄頭信息:00 00 10 00 47
轉換爲2進制:00000000 00000000 00010000 00000000 01000111
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
第二行記錄頭信息:00 00 18 00 37
轉換爲2進制:00000000 00000000 00011000 00000000 00110111
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
對於更新後的原始第一行和第二行:
第一行記錄頭信息:20 00 10 00 47
轉換爲2進制:00010000 00000000 00010000 00000000 01000111
無用位:00,deleted_flag:1,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
第二行記錄頭信息:20 00 18 00 37
轉換爲2進制:00010000 00000000 00011000 00000000 00110111
無用位:00,deleted_flag:1,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
可以看出,原有的數據 deleted_flag 變成 1,代表數據被刪除。
對於更新後的新的第一行和第二行:
第一行記錄頭信息:00 00 30 00 ca
轉換爲2進制:00000000 00000000 00110000 00000000 11001010
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000011,record_type:000,next_record:00000000 11001010
第二行記錄頭信息:00 00 38 fe e6
轉換爲2進制:00000000 00000000 00111000 11111110 11100110
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000111,record_type:000,next_record:11111110 11100110
這些信息的其他字段,在我們之後用到的時候,會詳細說明。
Compact 行格式存儲 - 隱藏列
隱藏列包含三個:
列名 | 大小(字節) | 描述 |
---|---|---|
DB_ROW_ID | 6 | 主鍵ID,這個列不一定會生成。優先使用用戶自定義主鍵作爲主鍵,如果用戶沒有定義主鍵,則選取一個 Unique 鍵作爲主鍵,如果表中連 Unique 鍵都沒有定義的話,則會爲表默認添加一個名爲 DB_ROW_ID 的隱藏列作爲主鍵 |
DB_TRX_ID | 6 | 產生當前記錄項的事務id,每開始一個新的事務時,系統版本號會自動遞增,而事務開始時刻的系統版本號會作爲事務id,事務 commit 的話,就會更新這裏的 DB_TRX_ID |
DB_ROLL_PTR | 7 | undo log 指針,指向當前記錄項的 undo log,找之前版本的數據需通過此指針。如果事務回滾的話,則從 undo Log 中把原始值讀取出來再放到記錄中去 |
這裏我們先不詳細展開這些列的說明,只是先知道這些列即可,只會會在聚簇索引說明以及多版本控制分析的章節中詳細說明。
Compact 行格式存儲 - 數據列 bigint 存儲
對於 bigint 類型,如果不爲 NULL,則佔用8字節,首位爲符號位,剩餘位存儲數字,數字範圍是 -2^63 ~ 2^63 - 1 = -9223372036854775808 ~ 9223372036854775807。如果爲 NULL,則不佔用任何存儲空間。
存儲時,如果爲正數,則首位 bit 爲1,如果爲負數,則首位爲 0 並用補碼的形式存儲。
對於我們的四行數據:
第一行列數據id(1):80 00 00 00 00 00 00 01
第二行列數據id(65536):80 00 00 00 00 01 00 00
第三行行列數據id(NULL):空
第四行列數據id(2048):80 00 00 00 00 00 08 00
其他的類似的整數存儲,tinyint(1字節),smallint(2字節),mediumint(3字節),int(4字節)等,只是字節長度上面有區別。對應的無符號類型,tinyint unsigned,smallint unsigned, mediumint unsigned,int unsigned,bigint unsigned等等,僅僅是是否有符號位的區別。
同時,這裏提一下 bigint(20)
裏面這個 20 的作用。他只是限制顯示,和底層存儲沒有任何關係。整型字段有個 zerofill 屬性,設置後(例如 bigint(20) zerofill
),在數字長度不夠 20 的數據前面填充0,以達到設定的長度。這個 20 就是顯示長度的設定。
Compact 行格式存儲 - 數據列 double 存儲
double 的存儲對於非 NULL 的列,符合 IEEE 754 floating-point "double format" bit layout 這個統一標準:
最高位 bit 表示符號位(0x8000000000000000)
第二到第十二的 bit 表示指數(0x7ff0000000000000)
剩下的 bit 表示浮點數真正的數字(0x000fffffffffffffL)
同時,Innodb存儲在數據文件上的格式爲 Little Edian,需要進行反轉後,才能取得字段的真實值。同樣的,如果爲 NULL, 則不佔用空間。
例如:
第一行列數據score(78.5):00 00 00 00 00 a0 53 40
翻轉: 40 53 a0 00 00 00 00 00
二進制: 01000000 01010011 10100000 00000000 00000000 00000000 00000000 00000000
符號位:0,指數位10000000101 = 1029,減去階數 1023 = 實際指數 6,小數部分0.0011101000000000000000000000000000000000000000000000,轉換爲十進制爲0.125 + 0.0625 + 0.03125 + 0.0078125 = 0.2265625, 加上隱含數字 1 爲 1.2265625, 之後乘以 2 的 6 次方就是 1.2265625 * 64 = 78.5
計算過程較爲複雜,可以利用 Java 的 Double.longBitsToDouble()
轉換:
public static void main(String[] args) {
System.out.println(Double.longBitsToDouble(0x4053a00000000000L));
}
輸出爲 78.5
類似的類型,float,也是相同的格式,只是長度減半。
Compact 行格式存儲 - 數據列 char 存儲
對於定長字段,不需要存長度信息直接存儲數據即可,如果不足設定的長度則補充。對於char類型,補充 0x20, 對應的就是空格。
例如:
第一行列數據name(hash):68 61 73 68
第二行列數據name(zhx):7a 68 78 20
第三行列數據name(aa):61 61 20 20
第四行列數據name(NULL):空
對於類似的 binary 類型,補充 0x00。
Compact 行格式存儲 - 數據列 varchar 存儲
因爲數據開頭有可變長度字段長度列表,所以 varchar 只需要保存實際的數據即可,不需要填充額外的數據。
正是由於這個特性,對於可變長度字段的更新,一般都是將老記錄標記爲刪除,在記錄末尾添加新的一條記錄填充更新後的記錄。這樣提高了更新速度,但是增加了存儲碎片。
本文分享自微信公衆號 - 我的編程喵(MyProCat)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。