mysql中,innodb表裏,某一條數據刪除了之後,這條數據會被真實的擦掉嗎,還是刪除了關係?

 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_ID00 00 00 00 08 0c
隱藏列DB_TRX_ID00 00 00 03 c9 4d
隱藏列DB_ROLL_PTRb9 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_ID00 00 00 00 08 0d
隱藏列DB_TRX_ID00 00 00 03 c9 4e
隱藏列DB_ROLL_PTRba 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_ID00 00 00 00 08 0e
隱藏列DB_TRX_ID00 00 00 03 c9 51
隱藏列DB_ROLL_PTRbc 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_ID00 00 00 00 08 0f
隱藏列DB_TRX_ID00 00 00 03 c9 54
隱藏列DB_ROLL_PTRbe 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的字段contentextra,長度分別是 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_ID00 00 00 00 08 0c
隱藏列DB_TRX_ID00 00 00 03 c9 6e
隱藏列DB_ROLL_PTR4f 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 字段分別是 idcontentextralarge_content,分別是第一,第四,第五,第六列,那麼 NULL 值列表爲:00111001,也就是 0x39。在加入新字段之前NULL 字段分別是 idcontentextra,分別是第一,第四,第五列,那麼 NULL 值列表爲:00011001,也就是 0x19 針對第四行記錄,他的 NULL 字段分別是score,namelarge_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
無用位:00deleted_flag0min_rec_flag0n_owned0000heap_no0000000000010record_type000next_record00000000 01000111

第二行記錄頭信息:00 00 18 00 37
轉換爲2進制:00000000 00000000 00011000 00000000 00110111
無用位:00deleted_flag0min_rec_flag0n_owned0000heap_no0000000000010record_type000next_record00000000 01000111

對於更新後的原始第一行和第二行:

第一行記錄頭信息:20 00 10 00 47 
轉換爲2進制:00010000 00000000 00010000 00000000 01000111
無用位:00deleted_flag1min_rec_flag0n_owned0000heap_no0000000000010record_type000next_record00000000 01000111

第二行記錄頭信息:20 00 18 00 37
轉換爲2進制:00010000 00000000 00011000 00000000 00110111
無用位:00deleted_flag1min_rec_flag0n_owned0000heap_no0000000000010record_type000next_record00000000 01000111

可以看出,原有的數據 deleted_flag 變成 1,代表數據被刪除。

對於更新後的新的第一行和第二行:

第一行記錄頭信息:00 00 30 00 ca 
轉換爲2進制:00000000 00000000 00110000 00000000 11001010
無用位:00deleted_flag0min_rec_flag0n_owned0000heap_no0000000000011record_type000next_record00000000 11001010

第二行記錄頭信息:00 00 38 fe e6
轉換爲2進制:00000000 00000000 00111000 11111110 11100110
無用位:00deleted_flag0min_rec_flag0n_owned0000heap_no0000000000111record_type000next_record11111110 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源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章