單條記錄大小增長倍數和ibd檔案大小的增長倍數不成正比
環境資訊
資料庫版本: GreatSQL 8.0.25
字符集:utf8mb4
innodb_default_row_format: dynamic
innodb_page_size: 16384
問題描述
表資料為新insert資料,無delete、無update
GreatSQL 一個資料量為1萬的A表,有100個varchar欄位,每個欄位存10位元組,ibd大小為21M
GreatSQL 一個資料量為1萬的B表,有100個varchar欄位,每個欄位存100位元組,ibd大小為4.7G
問題:相同資料量,相同資料量,B表的每行比A表大10倍,磁碟使用大小不是10倍,而是200多倍?
greatsql> show create table t_user_100_1000_100 \G;
*************************** 1. row ***************************
Table: t_user_100_1000_100
Create Table: CREATE TABLE `t_user_100_1000_100` (
`id` int NOT NULL AUTO_INCREMENT,
`c_name1` varchar(10) NOT NULL DEFAULT '',
。。。
`c_name100` varchar(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
greatsql> show create table t_user_100_10000_100 \G;
*************************** 1. row ***************************
Table: t_user_100_10000_100
Create Table: CREATE TABLE `t_user_100_10000_100` (
`id` int NOT NULL AUTO_INCREMENT,
`c_name1` varchar(100) NOT NULL DEFAULT '',
。。。
`c_name100` varchar(100) NOT NULL DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=10001 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci
1 row in set (0.00 sec)
greatsql> select count() from t_user_100_10000_100 ;
+----------+
| count() |
+----------+
| 10000 |
+----------+
1 row in set (0.06 sec)
greatsql> select count() from t_user_100_1000_100 ;
+----------+
| count() |
+----------+
| 10000 |
+----------+
1 row in set (0.18 sec)
#os ibd 檔案大小
ll
total 4313096
-rw-r----- 1 mysql mysql 5016387584 Apr 9 18:52 t_user_100_10000_100.ibd
-rw-r----- 1 mysql mysql 20971520 Apr 9 18:40 t_user_100_1000_100.ibd
greatsql> select 5016387584/20971520 from dual;
+---------------------+
| 5016387584/20971520 |
+---------------------+
| 239.2000 |
+---------------------+
1 row in set (0.00 sec)
問題分析
多出來的24倍難道是碎片導致的?
使用optimize table重整表後,幾乎沒有任何最佳化,檢視系統檢視,也沒有多少空洞。
使用官方工具innochecksum檢視錶空間檔案PAGE型別分佈,可以看到,B表相對A表多了大量的Other type of page。看來主要的空間消耗是在這個“Other”上。
- INNODB的行溢位
INNODB預設下每個PAGE的大小為16K。B表每行10K,每個PAGE只能存放1行記錄,餘下的6K就浪費了。但即使按照這個演算法,也只浪費了37%的空間。
實際上,INNODB在這裡有個處理,當記錄過大,會將最大的列使用一個指標替代,指標指向一個新的PAGE,在該PAGE上存放實際資料。
由函式page_zip_rec_needs_ext()判斷是否需要溢位。判斷方式是該記錄長度是否大於空PAGE的可用空間。
GDB執行一下,可以看到一個16k的PAGE實際可用的空間為16252位元組(頁頭等佔用了小量位元組)。一半粗略算作是8k。
如果行長度大於8K,會將最長的列存放到新的PAGE,原位置使用20位元組的指標代替。如果處理後,行長度依然大於8K,則選擇當前最長的列進一步處理,不斷迴圈。如果列長度無法進一步縮少,仍然大於8K,則丟擲DB_TOO_BIG_RECORD錯誤,即“row size too large”。dtuple_convert_big_rec()函式上可以看到更多的執行細節。
- 小量資料溢位的情況
以下堆疊展示把溢位資料寫入"Off Page"。主要函式為lob::insert()。
log::insert()會申請一個新的16K大小的PAGE,並將資料寫入新的page。
dberr_t insert(){
......
//分配一個新的16KB的PAGE
first_page_t first(mtr, index);
buf_block_t *first_block = first.alloc(mtr, ctx->is_bulk());
......
//將100位元組寫入寫入
ulint to_write = first.write(trxid, ptr, len);
......
}
以下是B表插入資料,往innodb"Off page"寫入資料時候的斷點,可以看到只寫了B表單列100位元組資料。16KB的容量只寫100位元組的資料,剩餘99%的空間用不上,實在太浪費了。
B表有100個varchar列,每個列100位元組。如果需要滿足列長少於8K,需要25個列以上進行溢位(一個列還有隱藏列和其它資料,實際需要溢位的列略多於25)。使用innochecksum查詢到“OTHER” page 有29萬,B表有一萬行,平均每行29個“OTHER” page。看來這個“OTHER” page基本都是這種“Off page”了。
問題總結
GreatSQL 白白浪費了95%的磁碟空間,是因為大量的列溢位了小量的資料。INNODB存放每個溢位列的資料的最小分配單元大小是16KB。原本10KB的行長度,需要多佔N倍的儲存空間。
最佳化建議
表設計時,要注意控制行長度小於8k,避免小量列資料溢位,導致磁碟容量和效能問題。
延伸閱讀
- https://dev.mysql.com/doc/refman/8.0/en/out-of-range-and-overflow.html
- https://mp.weixin.qq.com/s/AjGrQONFPJgVAqy_qtoiYA
- https://mp.weixin.qq.com/s/QMZ7O0gfs81zXo69F1bHdQ
Enjoy GreatSQL 😃
關於 GreatSQL
GreatSQL是適用於金融級應用的國內自主開源資料庫,具備高效能、高可靠、高易用性、高安全等多個核心特性,可以作為MySQL或Percona Server的可選替換,用於線上生產環境,且完全免費併相容MySQL或Percona Server。
相關連結: GreatSQL社群 Gitee GitHub Bilibili
GreatSQL社群:
社群部落格有獎徵稿詳情:https://greatsql.cn/thread-100-1-1.html
技術交流群:
微信:掃碼新增
GreatSQL社群助手
微信好友,傳送驗證資訊加群
。