0%

mysql问题汇总

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, 10limit 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

-------------本文结束感谢您的阅读-------------