实验
建一张表:
CREATE TABLE person(
id int NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键',
person_id tinyint not null comment '用户id',
person_name varchar(200) comment '用户名称',
gmt_create datetime comment '创建时间',
gmt_modified datetime comment '修改时间'
) comment '人员信息表'; 插入一条数据:
insert into person values(1, 1,'user_1', NOW(), now()); 利用 MySQL 伪列 rownum 设置伪列起始点为 1
select (@i:=@i+1) as rownum, person_name from person, (select @i:=100) as init;
set @i=1; 运行下面的 sql,连续执行 20 次,就是 2 的 20 次方约等于 100w 的数据;执行 23 次就是 2 的 23 次方约等于 800w , 如此下去即可实现千万测试数据的插入。
如果不想翻倍翻倍的增加数据,而是想少量,少量的增加,有个技巧,就是在 SQL 的后面增加 where 条件,如 id > 某一个值去控制增加的数据量即可。
insert into person(id, person_id, person_name, gmt_create, gmt_modified)
select @i:=@i+1,
left(rand()*10,10) as person_id,
concat('user_',@i%2048),
date_add(gmt_create,interval + @i*cast(rand()*100 as signed) SECOND),
date_add(date_add(gmt_modified,interval +@i*cast(rand()*100 as signed) SECOND), interval + cast(rand()*1000000 as signed) SECOND)
from person; 此处需要注意的是,也许你在执行到近 800w 或者 1000w 数据的时候,会报错:The total number of locks exceeds the lock table size。
这是由于你的临时表内存设置的不够大,只需要扩大一下设置参数即可。
SET GLOBAL tmp_table_size =512*1024*1024; (512M)
SET global innodb_buffer_pool_size= 1*1024*1024*1024 (1G); 先来看一组测试数据,这组数据是在 MySQL 8.0 的版本,并且是在我本机上,由于本机还跑着 idea , 浏览器等各种工具,所以并不是机器配置就是用于数据库配置,所以测试数据只限于参考。


看到这组数据似乎好像真的和标题对应,当数据达到 2000W 以后,查询时长急剧上升,难道这就是铁律吗?
那下面我们就来看看这个建议值 2000W 是怎么来的?
单表数量限制
首先我们先想想数据库单表中行数最大是多大?
CREATE TABLE person(
id int(10) NOT NULL AUTO_INCREMENT PRIMARY KEY comment '主键',
person_id tinyint not null comment '用户id',
person_name VARCHAR(200) comment '用户名称',
gmt_create datetime comment '创建时间',
gmt_modified datetime comment '修改时间'
) comment '人员信息表';看看上面的建表,其中 id 是主键,本身就是唯一的,也就是说主键的大小可以限制表的上限:
- 如果主键声明
int类型,也就是32位,那么支持2^32-1~~21 亿; - 如果主键声明
bigint类型,那就是2^62-1(36893488147419103232),难以想象这个的多大了,一般还没有到这个限制之前,可能数据库已经爆满了!!
有人统计过,如果建表的时候,自增字段选择无符号的 bigint , 那么自增长最大值是 18446744073709551615,按照一秒新增一条记录的速度,大约什么时候能用完?

表空间
下面我们再来看看索引的结构,我们下面讲内容都是基于 Innodb 引擎的,大家都知道 Innodb 的索引内部用的是 B+ 树。

这张表数据,在硬盘上存储也是类似如此的,它实际是放在一个叫 person.ibd (innodb data)的文件中,也叫做表空间;虽然数据表中,他们看起来是一条连着一条,但是实际上在文件中它被分成很多小份的数据页,而且每一份都是 16KB。
大概就像下面这样,当然这只是我们抽象出来的,在表空间中还有段、区、组等很多概念,但是我们需要跳出来看。

页的数据结构
从图中可以看出,一个 InnoDB 数据页的存储空间大致被划分成了七个部分,有的部分占用的字节数是确定的,有的部分占用的字节数是不确定的。
在页的七个组成部分中,我们自己存储的记录会按照我们指定的行格式存储到 User Records 部分。
但是在一开始生成页的时候,其实并没有 User Records 这个部分,每当我们插入一条记录,都会从 Free Space 部分,也就是尚未使用的存储空间中申请一个记录大小的空间划分到 User Records 部分。
当 Free Space 部分的空间全部被 User Records 部分替代掉之后,也就意味着这个页使用完了,如果还有新的记录插入的话,就需要去申请新的页了。
这个过程的图示如下:
刚刚上面说到了数据的新增的过程。那下面就来说说,数据的查找过程,假如我们需要查找一条记录,我们可以把表空间中的每一页都加载到内存中,然后对记录挨个判断是不是我们想要的。
在数据量小的时候,没啥问题,内存也可以撑。但是现实就是这么残酷,不会给你这个局面。
为了解决这问题,MySQL 中就有了索引的概念,大家都知道索引能够加快数据的查询,那到底是怎么个回事呢?下面我就来看看。
索引的数据结构
在 MySQL 中索引的数据结构和刚刚描述的页几乎是一模一样的,而且大小也是 16KB,。
但是在索引页中记录的是页(数据页,索引页)的最小主键 id 和页号,以及在索引页中增加了层级的信息,从 0 开始往上算,所以页与页之间就有了上下层级的概念。

看到这个图之后,是不是有点似曾相似的感觉,是不是像一棵二叉树啊,对,没错!它就是一棵树。
只不过我们在这里只是简单画了三个节点,两层结构的而已,如果数据多了,可能就会扩展到三层的树,这个就是我们常说的 B+ 树,最下面那一层的 page level =0,也就是叶子节点,其余都是非叶子节点。

看上图中,我们是单拿一个节点来看,首先它是一个非叶子节点(索引页),在它的内容区中有 id 和 页号地址两部分:
id:对应页中记录的最小记录id值;- 页号:地址是指向对应页的指针;
而数据页与此几乎大同小异,区别在于数据页记录的是真实的行数据而不是页地址,而且 id 的也是顺序的。
单表建议值
下面我们就以三层,两分叉(实际中是 M 分叉)的图例来说明一下查找一个行数据的过程。
比如说我们需要查找一个 id = 6 的行数据,我们为了查找 id = 6 的数据,总共查询了三个页,如果三个页都在磁盘中(未提前加载至内存),那么最多需要经历三次的磁盘 IO。
需要注意的是,图中的页号只是个示例,实际情况下并不是连续的,在磁盘中存储也不一定是顺序的。
至此,我们大概已经了解了表的数据是怎么个结构了,也大概知道查询数据是个怎么的过程了,这样我们也就能大概估算这样的结构能存放多少数据了。
假设:
- 非叶子节点内指向其他页的数量为
x - 叶子节点内能容纳的数据行数为
y B+数的层数为z
如下图中所示可以得到,Total = x^(z-1) *y。

X =?
在文章的开头已经介绍了页的结构,索引也也不例外,都会有 File Header (38 byte)、Page Header (56 Byte)、Infimum + Supermum(26 byte)、File Trailer(8byte), 再加上页目录,大概 1kB 左右。
我们就当做它就是 1KB,那整个页的大小是 16KB,剩下 15kB 用于存数据,在索引页中主要记录的是主键与页号,主键我们假设数据类型是 Bigint (8B),而页号也是固定的(4B), 那么索引页中的一条数据也就是 12B。
所以 x=15*1024/12 ≈ 1280 行。
Y=?
叶子节点和非叶子节点的结构是一样的,同理,能放数据的空间也是 15kb。
但是叶子节点中存放的是真正的行数据,这个影响的因素就会多很多,比如,字段的类型,字段的数量。每行数据占用空间越大,页中所放的行数量就会越少。
这边我们暂时按一条行数据 1kb 来算,那一页就能存下 15 条,则 Y = 15*1024/1000 ≈ 15。
算到这边了,是不是心里已经有谱了啊。
根据上述的公式,Total =x^(z-1) *y,已知 x=1280,y=15:
- 假设
B+树是两层,那就是 z = 2, Total = (1280 ^1 )*15 =19200- 假设
B+树是三层,那就是 z = 3, Total = (1280 ^2) *15 =24576000(约 2.45kw)
哎呀,妈呀!这不是正好就是文章开头说的最大行数建议值 2000W 嘛!对的,一般 B+ 数的层级最多也就是 3 层。
你试想一下,如果是 4 层,除了查询的时候磁盘 IO 次数会增加,而且这个 Total 值会是多少,大概应该是 3 百多亿吧,也不太合理,所以,三层应该是比较合理的一个值。
并且在保持相同的层级(相似查询性能)的情况下,在行数据大小不同的情况下,其实这个最大建议值也是不同的,而且影响查询性能的还有很多其他因素,比如,数据库版本,服务器配置,sql 的编写等等。
MySQL 为了提高性能,会将表的索引装载到内存中,在 InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。
但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降,所以增加硬件配置(比如把内存当磁盘使),可能会带来立竿见影的性能提升哈。
总结
2000W 确实是建议值!
因为如果要保证 B+ 树是三层的话,那就只能是 2000W 条左右,但是实际生产环境中,面对的场景不同,对于单表存储上限的要求也是不同的,比如如果是用于集团内部的系统,且对于响应时长要求也不高(5s~10s)的话,那么单表 20 亿条数据(数据(600GB)+索引(300GB)大概在1TB左右)也是可以的,但这种情况下大概率 DBA 就会来找负责人要求分库分表或者数据定期转存到 hive 表了,因为会影响集群整体性能。