MySQL单表数据数据最大两千万?
相比大家也有听说过MySQL
单表数据数据最大两千万,如果超过两千万数据库性能就会下降的说法。
其实这种说法的背后是索引高度的问题,MySQL
索引内部使用的是B+
树,叶子节点存具体的数据,非叶子节点是为了去叶子节点寻址。MySQL
的数据是按页存储,每一页的大小是16k,读取的时候也是按页读取,也就是说就算你的语句里有limit 1
,但是实际上也是将整页都加载到内存中。
这个16k大小的页包含有页头(页号,寻址指针等内容)、页尾(检验码等)、页目录(这一页内的数据目录,用于快速找到页内数据),剩下的空间就是存放具体的数据记录record
。结合B+
树的特性,非叶子节点内具体的数据记录record
实际上存储的某一页的页号以及这一页的最小记录的id
值。叶子节点内具体的数据记录record
实际上存储的是行信息(再具体到存储的是行记录的多少内容,要看是什么索引)。
所以想查询某一行数据的主要过程:从索引的根往下找,先根据非叶子节点的id
值匹配,匹配到对应的id
值之后根据页号,找到对应的叶子节点,从而获取到整行数据。
当数据庞大时,树的叶子节点如果想要容纳所有数据就需要增加高度,高度越高,能容纳的数据量也就越大。同时也伴随着更多的磁盘io,一般3层索引树,会有3次磁盘io,并且是随机io。
这里有一个计算公式:(x ^ (z-1)) * y
- 非叶子结点内指向其他内存页的指针数量为
x
- 叶子节点内能容纳的
record
数量为y
B+
树的层数为z
x的值:主键假设是bigint(8Byte),而页号在源码里叫FIL_PAGE_OFFSET(4Byte),那么非叶子节点里的一条数据是12Byte左右,整个数据页16k, 页头页尾那部分数据全加起来大概128Byte,加上页目录毛估占1k吧。那剩下的15k除以12Byte,等于1280,也就是可以指向x=1280页。
y的值:叶子节点和非叶子节点结构是一致的,这里也假设记录可用空间为15k,假设一行数据需要占用1k空间,那么叶子节点可用容纳15行的数据。
根据公式:
若z=2,则(1280 ^ (2-1)) * 15 ≈ 2w
若z=3,则(1280 ^ (3-1)) * 15 ≈ 2.5kw
这里的2.5kw
也就是通常说的单表数据不要超过2kw的估值,如果高度再加一层,那么所能容纳的数据量就更加庞大。但是你有没有发现,我假设了单行数据的空间是1k,如果表字段不多,单行数据仅0.25k呢,那么这个计算结果就是1亿,也就是在三层高度的情况下,数据量也有可能达到1亿。
随机IO/顺序IO
顺序IO是指读写操作的访问地址连续。随机IO是指读写操作的时间连续,但访问的地址不连续。
深度分页
深度分页涉及到的问题通常是使用limit offset, size
,对于limit 10, 10
和limit 10000, 10
,性能上前者是快于后者的,这是因为后者也是需要查出10000条数据,然后丢弃掉,再往后获取10条数据,也就是说前者总共获取了20条数据,后者获取了10010条数据。
这种情况其实是无解的,因为这是MySQL
的内部逻辑,我们根本就触及不到,但是在一定条件下,可以通过一定的手段进行优化。假设有语句:
select * from page order by id limit 10000, 10;
这条查询语句还是有优化空间的,由于这里需要查询整行数据,也就导致了丢弃的10000行数据也去查询了整行数据,实际上这是没有必要的。
优化如下,虽然它也是会获取那丢弃的10000条数据,但是这10000条数据仅获取了id值,并没有获取整行数据,当行数据比较长时,还是会有一定的性能提升。
select * from page where id >=(select id from page order by id limit 10000, 1) order by id limit 10;
再看在非主键索引上的操作:
select * from page order by user_name limit 10000, 10;
这里由于是非主键索引,所以会对这丢弃的10000条数据都进行一次回表操作,影响性能。可以通过连接来优化,减少回表次数:
select * from page t1, (select id from page order by user_name limit 10000, 10) t2 WHERE t1.id = t2.id;
前面的优化实际上也都会查询丢弃的10000条数据,所以这里再怎么优化都是治标不治本的优化手段。所以只能通过一些特殊手段控制,比如百度、淘宝的搜索页只会固定页码,到一定页码之后就不允许往后页码查。再比如抖音这种这种滚动下只有翻页没有跳页,可以将当前页设置一个最小值,下一页在这个最小值的基础上查询。这也是同步大表时的一个手段。
如果同步一个大表,你使用的是select * from tableName
,同步到异构系统,比如es等,因为数据量比较大,数据库无法一次性获取到所有数据而报错,如果仅仅通过limit offset, size
就有可能出现深度分页的情况,所以一般来说可以通过id
排序,将id
对应的记录分块,当前块的最大id
值,作为下一块id
的查询条件。
select * from tableName where id > 上一块id的最大值 order by id limit 100