每日一面 - mysql中,innodb表裏,某一條數據刪除了以後,這條數據會被真實的擦掉嗎,仍是刪除了關係?

以 Compact 行格式爲例:
mysql

總結

刪除一條記錄,數據原有的被廢棄,記錄頭髮生變化,主要是打上了刪除標記。也就是原有的數據 deleted_flag 變成 1,表明數據被刪除。可是數據沒有被清空,在新一行數據大小小於這一行的時候,可能會佔用這一行。這樣其實就是存儲碎片,要想減小存儲碎片,能夠經過重建表來實現(例如對於高併發大數據量表,除了歸檔,還能夠經過利用無鎖算法Alter修改字段來重建表增長表性能)。算法

Compact 行格式存儲

咱們來建立一個包含幾乎全部基本數據類型的表,其餘的例如 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 生成的行數據不同,可是咱們建立的行數據內容應該是同樣的,並且數據長度應該是一摸同樣的,能夠搜索其中的字符找到這些數據):編輯器

image

咱們這裏先直接給出這些數據表明的意義,讓你們直觀感覺下: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 記錄,其結構以下圖所示:高併發

image

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;

查看數據:

image

發現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 只須要保存實際的數據便可,不須要填充額外的數據。

正是因爲這個特性,對於可變長度字段的更新,通常都是將老記錄標記爲刪除,在記錄末尾添加新的一條記錄填充更新後的記錄。這樣提升了更新速度,可是增長了存儲碎片。

每日一刷,輕鬆提高技術,斬獲各類offer:

image

相關文章
相關標籤/搜索