以 Compact 行格式爲例:
mysql
刪除一條記錄,數據原有的被廢棄,記錄頭髮生變化,主要是打上了刪除標記。也就是原有的數據 deleted_flag 變成 1,表明數據被刪除。可是數據沒有被清空,在新一行數據大小小於這一行的時候,可能會佔用這一行。這樣其實就是存儲碎片,要想減小存儲碎片,能夠經過重建表來實現(例如對於高併發大數據量表,除了歸檔,還能夠經過利用無鎖算法Alter修改字段來重建表增長表性能)。算法
咱們來建立一個包含幾乎全部基本數據類型的表,其餘的例如 geometry,timestamp 等等,也是基於 double 還有 bigint 而來的, text、json、blob等類型,通常不與行數據一塊兒存儲,咱們以後再說:sql
create table record_test_1 ( id bigint, score double, name char(4), content varchar(8), extra varchar(16) )row_format=compact;
插入以下幾條記錄:json
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 生成的行數據不同,可是咱們建立的行數據內容應該是同樣的,並且數據長度應該是一摸同樣的,能夠搜索其中的字符找到這些數據):編輯器
咱們這裏先直接給出這些數據表明的意義,讓你們直觀感覺下:ide
變長字段長度列表: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 記錄,其結構以下圖所示:高併發
對於像 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`;
這時候行數據部分並無變化。
問題一:那麼爲何用 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個字節。
某些字段可能能夠爲 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 行格式存儲,記錄頭固定爲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
這些信息的其餘字段,在咱們以後用到的時候,會詳細說明。
隱藏列包含三個:
列名 | 大小(字節) | 描述 |
---|---|---|
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 中把原始值讀取出來再放到記錄中去 |
這裏咱們先不詳細展開這些列的說明,只是先知道這些列便可,只會會在聚簇索引說明以及多版本控制分析的章節中詳細說明。
對於 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 就是顯示長度的設定。
double 的存儲對於非 NULL 的列,符合 IEEE 754 floating-point "double format" bit layout 這個統一標準:
同時,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,也是相同的格式,只是長度減半。
對於定長字段,不須要存長度信息直接存儲數據便可,若是不足設定的長度則補充。對於char類型,補充 0x20, 對應的就是空格。
例如:
第一行列數據name(hash):68 61 73 68 第二行列數據name(zhx):7a 68 78 20 第三行列數據name(aa):61 61 20 20 第四行列數據name(NULL):空
對於相似的 binary 類型,補充 0x00。
由於數據開頭有可變長度字段長度列表,因此 varchar 只須要保存實際的數據便可,不須要填充額外的數據。
正是因爲這個特性,對於可變長度字段的更新,通常都是將老記錄標記爲刪除,在記錄末尾添加新的一條記錄填充更新後的記錄。這樣提升了更新速度,可是增長了存儲碎片。
每日一刷,輕鬆提高技術,斬獲各類offer: