暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

[ibd2sql] myisam MYD文件存储格式

原创 大大刺猬 2024-05-31
487

导读

现在基本上已经没有使用myisam存储引擎的了, 毕竟它已经没啥优势了… 那为啥还要来看这个存储引擎的存储格式呢? 闲? 只是最近再看MySQL Internals Manual, 虽然官网已经无法打开这个链接了. 而且该手册基于mysql 5.0 比较老(5.7都停止更新了). 但也有不少能用的东西的.

MYISAM 存储引擎

mysql5.5 之前默认存储引擎就是myisam. 但该存储引擎不支持事务. 这也就注定他要被替代了, 当然它的存储结构也注定了会被取代(不像innodb/bdb 使用page).

我们随便创建一个myisam存储引擎的表, 并插入几行数据(我这里直接引用官方的例子).
注:我这里测试环境是 5.7.38

CREATE TABLE Table1 (column1 CHAR(1), column2 CHAR(1), column3 CHAR(1)) engine=myisam; INSERT INTO Table1 VALUES ('a', 'b', 'c'); INSERT INTO Table1 VALUES ('d', NULL, 'e');

然后我们就可以在@@datadir/DBNAME目录下面找到对应的文件

SHELL> ls -l /data/mysql_3308/mysqldata/ibd2sql_myisam/table1* -rw-r----- 1 mysql mysql 8638 May 31 09:41 /data/mysql_3308/mysqldata/ibd2sql_myisam/table1.frm -rw-r----- 1 mysql mysql 20 May 31 09:50 /data/mysql_3308/mysqldata/ibd2sql_myisam/table1.MYD -rw-r----- 1 mysql mysql 1024 May 31 09:50 /data/mysql_3308/mysqldata/ibd2sql_myisam/table1.MYI

frm 是记录元数据信息的, 之前有讲过, 感兴趣的可以看 https://cloud.tencent.com/developer/article/2409341
MYD 是数据文件, 本文的重点
MYI 是索引文件, 和innodb不同,它索引是单独的文件. 本文不讲.

MYD 又分为3种格式 fixed, dynamic, packed. 我们就看前2种, 第三种涉及到哈夫曼,比较复杂.

注: 所有整型或浮点数都使用小端字节序.

fixed

看名字就知道这种是固定格式的. 也就是只有定长字符时才使用这种格式. 格式也很简单. 就是header+data, header只有nullbitmask. data都是定长的. 读起来也是嘎嘎快. 要注意的是header的第一bit是表示这行数据是否被删除.
删除标记: 0 表示被删除 , 1表示未被删除
nullbitmask: 0 表示不为空, 1表示为空.
不足的补1
所以header的大小就是 int((1+column_no+7)/8)
直接演示吧

00000000  f1 61 20 20 62 20 20 63  20 20 f5 64 20 20 20 20  |.a  b  c  .d    |
00000010  20 65 20 20                                       | e  |
00000014

image.png
第一行数据
我们只有3个字段, 所以header只有1字节, 即为 b’0xf1’ 转为binary就是11110001
第1bit是 1 表示该行数据未被删除.
第2,3,4bit均为0 表示 1,2,3 列数据都不为空.
由于是 char(1), 使用的utf8mb3编码, 所以实际上是占用的3字节.
即第一行数据为
61 20 20 --> a
62 20 20 --> b
63 20 20 --> c

第二行数据
header是 b’0xf5’ 转为binary就是 11110101
第1bit 是1, 表示数据未被删除
第2,4bit为0, 表示第1,3列都不为空, 第3bit是1 表示 第二列是空值.
所以第二行数据是
64 20 20 --> d
20 20 20 --> ’ ’ 这里虽然已经记录是空值了, 但还是占用了3个字节
65 20 20 --> e

我们再来删除第一行数据看看

00000000  00 ff ff ff ff ff ff 63  20 20 f5 64 20 20 20 20  |.......c  .d    |
00000010  20 65 20 20                                       | e  |
00000014

image.png
我们发现第一字节变为了00 即第1bit也为0, 表示这行数据被删除了. 由于是定长的, 我们只需要后移 1*3 + 1*3 + 1*3 字节就能到第二行数据.

dynamic

这是一种动态格式, 就是有varchar(有时候会被视为char), blob之类的类型时,使用的的格式.
也是header+data, 只不过header部分比较麻烦

start of header  起始头
actual length    实际长度
unused length    未使用的长度(留一部分给update?)
flags 
overflow pointer 溢出指针:这段空间不够的时候,指向剩余的数据位置
data             数据行
unused bytes     未使用的空间(通常填充\x00)
next row starts  下一行数据

我们建一个带有varchar字段的表

create table t20240531(id int, aa varchar(200), bb datetime) engine=myisam; insert into t20240531 values(1,'ddcw',now()); insert into t20240531 values(2,'https://github.com/ddcw',now());
SHELL> hexdump -C /data/mysql_3308/mysqldata/ibd2sql_myisam/t20240531.MYD 00000000 03 00 10 00 00 f8 01 00 00 00 04 64 64 63 77 99 |...........ddcw.| 00000010 b3 7e ab 58 03 00 23 01 00 f8 02 00 00 00 17 68 |.~.X..#........h| 00000020 74 74 70 73 3a 2f 2f 67 69 74 68 75 62 2e 63 6f |ttps://github.co| 00000030 6d 2f 64 64 63 77 99 b3 7e ab 58 00 |m/ddcw..~.X.| 0000003c

字符串类型的基本上直接就能看出来了.
第一行:
03 00 --> start of header
10 --> 16 这行数据16字节
00 --> 无 未使用的空间(即和下一行数据连在一起的)
00 --> flag
f8 --> 溢出指针
01 00 00 00 --> 第一列, int, 小端字节序, 即值为 1
04 --> varchar的大小为4字节
64 64 63 77 —> 第二列: ddcw
99 b3 73 ab 58 --> 第三列datetime类型(5字节), 计算方式参考ibd2sql 这里为: 2024-5-31 10:45:24
image.png
4 + 1 + 4 + 5

第二行
03 00 --> start of header
23 --> 35字节
01 --> 空了1字节出来
00 --> flag
f8 --> 溢出指针
02 00 00 00 --> 第一列 2
17 --> varcahr大小为23字节
68 74 74 70 73 3a 2f 2f 67 69 74 68 75 62 2e 63 6f 6d 2f 64 64 63 77 第二列: https://github.com/ddcw
99 b3 7e ab 58 --> 第三列 2024-5-31 10:45:24
00 --> 预留的1字节

演示

人工解析太慢了, 我们可以写个脚本来解析, 还能解析更多的数类型. 数据类型和innodb是差不多的, 这里就不讲了.
构造数据:

CREATE TABLE `ddcw_alltype_table` ( `id` int(11) NOT NULL AUTO_INCREMENT, `int_col` int(11) DEFAULT NULL, `tinyint_col` tinyint(4) DEFAULT '1', `smallint_col` smallint(6) DEFAULT NULL, `mediumint_col` mediumint(9) DEFAULT NULL, `bigint_col` bigint(20) DEFAULT NULL, `float_col` float DEFAULT NULL, `double_col` double DEFAULT NULL, `date_col` date DEFAULT NULL, `datetime_col` datetime DEFAULT NULL, `timestamp_col` timestamp NULL DEFAULT NULL, `time_col` time DEFAULT NULL, `year_col` year(4) DEFAULT NULL, `char_col` char(100) DEFAULT NULL, `varchar_col` varchar(100) DEFAULT NULL, `binary_col` binary(10) DEFAULT NULL, `varbinary_col` varbinary(20) DEFAULT NULL, `enum_col` enum('A','B','C') DEFAULT NULL, `set_col` set('X','Y','Z') DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM; INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (1, 5245664, -11, -2411, -239, -4662, 8269.0, -9274.0, '2019-9-23', '1995-3-19 18:4:32', '2003-10-25 6:18:37.', '7:53:17', 1995, 'rXRdHKnyQJgZqZqAIOAGZlTmYDdCAyBxUQcAQYFUwB', 'kiCeqGmIqeiCrOCTGJMNUWOz', 0x33340000000000000000, '80', 'B', 'Y'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (2, 6509620, -35, 1401, -1595, 8053, -3272.0, -3689.0, '2013-1-31', '2007-12-30 8:28:20', '2009-2-27 16:37:2.', '23:28:16', 1996, 'hivYGHpSQmcUcnVeigncbeqrStDoXjTxGUsmNTpysYDCFMiSbxmdZymOIkJAJjsdzIlKFSBfPhfKRGLVV', 'InmDSmiAdWvEENQLbuEZjWtuzGMieiGWeRWAww', 0x35360000000000000000, '14', 'A', 'Y'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (3, 730742, -40, -6559, 2987, 7571, 2274.0, 6372.0, '1995-5-16', '1995-8-18 6:6:32', '2001-9-25 7:33:15.', '17:39:45', 2008, 'TEjBXuuaJOibTcvXaMQZJlEBbUZQNTljmYrhquwpEWGTDXRhhqOmKZJIFKIVWqczpWhxOIpBwFjvupWAyYlidHxFfkzTpBHZ', 'NBGJ', 0x35380000000000000000, '12', 'B', 'X'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (4, 6583237, -116, 3525, -3783, -8103, -2000.0, 567.0, '2022-6-23', '2001-1-1 23:46:15', '2010-3-19 3:50:20.', '12:52:56', 2018, 'sUhhjcFeeFBUCwnIYKJggNJmRYVwDYBRqq', 'bOzyzYMmpnzAmXqJoJbgkyycXwildpWGVvYkXcWS', 0x31380000000000000000, '19', 'A', 'Y'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (5, 954597, -16, 1904, -311, 8410, -4793.0, 2966.0, '1996-12-4', '1997-9-15 18:55:56', '1997-4-1 18:21:59.', '12:33:1', 2004, 'SlNRuZbmtDDZXThHpcacBJSNbDjKLRwzZwjcB', 'QMycyeGpthagILOdvKfGHAYSZjbVLZOIdIeKxBIGnXmJARhfltZSSoiOzlIpkHfThHWjokZxXCIFzyKGgUYRiUTx', 0x37310000000000000000, '14', 'B', 'Y'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (6, 8816683, -20, 8707, -8865, -588, 4467.0, 3013.0, '2020-10-18', '2020-3-18 1:56:5', '2022-2-3 6:19:12.', '17:49:14', 1996, 'UjKtNLgzncwAzHyRjmiqfvfZTOesAZRruGSpypMLlGXQTFmnmLeuHLOKaxphU', 'hswBYpimEAaKPymzZwOPZXMczfLyemBPinDjLVInIKMXURKOyiLcTyuNUmPgdjOxkRnvuTmaiAwlSwkxseil', 0x38340000000000000000, '40', 'A', 'X'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (7, 122873, -15, -9256, -9442, 1426, 1652.0, -665.0, '2005-11-1', '2000-3-25 16:40:14', '2013-1-30 14:31:57.', '7:28:42', 1993, 'EiCVuCSdNfPfhedmprzXxJWxwbJU', 'RWJnkCwuthxWrQgcyexiAMJMtLvalOQzhUn', 0x32390000000000000000, '23', 'A', 'X'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (8, 7959183, 87, 3501, -8353, 7698, -3902.0, 4404.0, '2019-9-14', '2004-5-10 17:15:7', '1999-7-26 18:19:43.', '21:17:2', 1993, 'NMJdGpvUgHVJQSNneTuMbPmnhHHYvXRcTVoRqOnoEURWRmtozgDyaFmMvHlpWICfMCYXdrIgC', 'VotvlNpisoCTmUpjjASfXhsUOznKaODVSTpRhMlcHshFPlbhVJC', 0x31340000000000000000, '35', 'B', 'Y'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (9, 4854976, -4, -2950, 5690, 189, 5695.0, 1029.0, '2000-6-2', '2022-1-17 18:32:21', '1995-8-15 1:34:55.', '23:25:30', 2025, 'mAmxIfxF', 'yJkOtszJcKBbu', 0x38360000000000000000, '84', 'B', 'Y'); INSERT INTO `ibd2sql_myisam`.`ddcw_alltype_table` VALUES (10, 6262780, 119, -5660, -2595, 7833, 1764.0, -3861.0, '2003-10-7', '2020-10-5 15:29:59', '1996-10-10 21:23:6.', '21:15:52', 1990, 'TqZzrwDgQDp', 'MPksKiHyaQgzRUdkSSjVzKhFFV', 0x38380000000000000000, '38', 'B', 'X');

解析MYD文件

from myisam_myd_reader import MYD_READER
aa = MYD_READER('/data/mysql_3308/mysqldata/ibd2sql_myisam/ddcw_alltype_table.MYD')
data = aa.read_header(9)  # header
aa.read_int(4)            # int
aa.read_int(4)            # int
aa.read_int(1)            # tinyint
aa.read_int(2)            # smallint
aa.read_int(3)            # mediumint
aa.read_int(8)            # bigint
aa.read_float()           # float
aa.read_double()          # double
aa.read_date()            # date
aa.read_datetime()        # datetime
aa.read_timestamp()       # timestamp
aa.read_time()            # time
aa.read_year()            # year
aa.read_varchar()         # char 100
aa.read_varchar()         # varchar
aa.read_binary(10)        # binary
aa.read_varbinary()       # varbinary
aa.read_int(1)            # enum
aa.read_int(1)            # set

image.png

总结

myisam存储引擎使用的很少, 就不细究了. 各数据类型存储基本上和innodb也一样. 也没啥好说的.

附脚本

myisam_myd_reader.py

#!/usr/bin/env python3 # write by ddcw @https://github.com/ddcw # 读mysql myisam的myd文件的. 简单的读, 所以不考虑很多信息 import struct class MYD_READER(object): def __init__(self,filename): self.f = open(filename,'rb') def read_int(self,n,signed=True): return int.from_bytes(self.f.read(n),'little',signed=signed) def read_header(self,n): return self.f.read(n) def read_varchar(self): return self.f.read(self.read_int(1)).decode() def read_blob(self,): return self.f.read(self.read_int(2)).decode() def read_float(self,): return struct.unpack('f',self.f.read(4))[0] def read_double(self,): return struct.unpack('d',self.f.read(8))[0] def read_date(self): data = self.read_int(3,False) year = int(data/(16*32)) month = int((data-year*16*32)/32) day = data - (year*16*32) - (month*32) return f"{year}-{month}-{day}" def read_datetime(self,): """同ibd里面的datetime, 详情见ibd2sql/innodb_page.py""" bdata = self.f.read(5) idata = int.from_bytes(bdata[:5],'big') year_month = ((idata & ((1 << 17) - 1) << 22) >> 22) year = int(year_month/13) month = int(year_month%13) day = ((idata & ((1 << 5) - 1) << 17) >> 17) hour = ((idata & ((1 << 5) - 1) << 12) >> 12) minute = ((idata & ((1 << 6) - 1) << 6) >> 6) second = (idata& ((1 << 6) - 1)) return f'{year}-{month}-{day} {hour}:{minute}:{second}' def read_time(self,): bdata = self.f.read(3) idata = int.from_bytes(bdata[:3],'big') hour = ((idata & ((1 << 10) - 1) << 12) >> 12) minute = (idata & ((1 << 6) - 1) << 6) >> 6 second = (idata& ((1 << 6) - 1)) return f'{hour}:{minute}:{second}' def read_timestamp(self): return self.read_int(4) def read_year(self): return 1900 + self.read_int(1) def read_enum(self): pass def read_set(self): pass def read_decimal(self,t1,t2): pass # 只做简单的数据类型解析, 因为要精简. def read_binary(self,n): return "0x"+"".join([ hex(x).split('0x')[-1] for x in self.f.read(n) ]) def read_varbinary(self): return "0x"+"".join([ hex(x).split('0x')[-1] for x in self.f.read(self.read_int(1,False)) ])
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论