做游戏模板下载网站有哪些内容,学做美食看哪个网站,校园跑腿小程序源码,处方药可以做网站吗一直有传言说#xff0c;MySQL 表的数据只要超过 2000 万行#xff0c;其性能就会下降。而本文作者用实验分析证明#xff1a;至少在 2023 年#xff0c;这已不再是 MySQL 表的有效软限制。
传言 互联网上有一则传言说#xff0c;我们应该避免单个 MySQL 表中的数据超过 …一直有传言说MySQL 表的数据只要超过 2000 万行其性能就会下降。而本文作者用实验分析证明至少在 2023 年这已不再是 MySQL 表的有效软限制。
传言 互联网上有一则传言说我们应该避免单个 MySQL 表中的数据超过 2000 万行否则表的性能就会下降——当数据量超过这个软限制时你就会发现 SQL 的查询速度会比平时慢很多。这是多年前针对 HDD 做出的判断。我想知道时至 2023 年SSD 上的 MySQL 是否仍然有此限制。如果真的有那么原因是什么呢
环境 数据库
▶ MySQL 版本: 8.0.25
▶ 实例类型AWS db.r5.large2vCPUs, 16GiB RAM
▶ EBS 存储类型General Purpose SSDgp2
测试客户端
▶ Linux 内核版本6.1
▶ 实例类型AWS t2.micro1 vCPU, 1GiB RAM
实验设计 创建具有相同结构、但大小不同的表。我一共创建了 9 个表数据行数分别为10 万、20 万、50 万、100 万、200 万、500 万、1000 万、2000 万、3000 万、5000 万和 6000 万。
创建几个具有相同结构的表
CREATE TABLE row_test(
id int NOT NULL AUTO_INCREMENT,
person_id int NOT NULL,
person_name VARCHAR(200),
insert_time int,
update_time int,
PRIMARY KEY (id),
KEY query_by_update_time (update_time),
KEY query_by_insert_time (insert_time)
);插入不同的数据。我使用了测试客户端和表复制的方式创建了这些表。脚本可参考https://github.com/gongyisheng/playground/blob/main/mysql/row_test/insert_data.py。
# test client
INSERT INTO {table} (person_id, person_name, insert_time, update_time) VALUES ({person_id}, {person_name}, {insert_time}, {update_time})
# copy
create table like table
insert into (person_id, person_name, insert_time, update_time)
select person_id, person_name, insert_time, update_time fromperson_id、person_name、insert_time 和 update_time 的值是随机的。
使用测试客户端执行以下 sql 查询来测试性能。脚本可参考https://github.com/gongyisheng/playground/blob/main/mysql/row_test/select_test.py。
select count(*) from table -- full table scan
select count(*) from table where id 12345 -- query by primary key
select count(*) from table where insert_time 12345 -- query by index
select * from table where insert_time 12345 -- query by index, but cause 2-times index tree lookup查看 innodb 缓冲池状态。
SHOW ENGINE INNODB STATUS
SHOW STATUS LIKE innodb_buffer_pool_page%结果 查询1select count(*) from
这种查询会执行全表扫描MySQL 并不擅长这种工作。
▶ 第一轮没有缓存。第一次执行查询时缓冲池中没有缓存数据。
▶ 第二轮有缓存。当缓冲池中已经有数据缓存时执行查询通常在第一次查询执行完之后。
观察结果
1. 第一轮查询的执行时间超出了后面几次。 原因是 MySQL 使用了 innodb_buffer_pool 来缓存数据页。在第一次执行查询之前缓冲池是空的所以 MySQL 必须进行大量的磁盘 I/O 才能从 .idb 文件加载表。但在第一次执行结束后缓冲池中存储了数据后续查询可以直接读取内存避免磁盘 I/O因此速度更快。该过程称为 MySQL 缓冲池预热。
2. select count(*) from table 会设法将整个表加载到缓冲池。 我比较了实验前后 innodb_buffer_pool 的统计数据。运行查询后如果缓冲池足够大则其使用量变化等于表的大小。否则只有部分表会缓存在缓冲池中。原因是查询 select count(*) from table 会做全表扫描并做逐行统计。如果没有缓存就需要将完整的表加载到内存中。为什么因为 Innodb 支持事务它不能保证事务在不同时间看到同一张表。全表扫描是获得准确行数的唯一安全方法。
3. 如果缓冲池不能容纳全表则会爆发查询延迟。 我注意到 innodb_buffer_pool 的大小会极大地影响查询性能因此我尝试在不同的配置下运行查询。当使用 11G 缓冲区而表的大小达到 5000 万行时就会爆发查询延迟。接着我将缓冲区缩减到 7G当表的大小达到 3000 万行时爆发了查询延迟。最后我将缓冲区缩减到 3G当表的大小仅为 2000 万行时就爆发了查询延迟。很明显如果表中的数据无法缓存在缓冲池中则 select count(*) from 必须执行昂贵的磁盘 I/O这会导致查询运行时间直线上升。 4. 对于没有缓存的查询查询花费的时间与表的大小呈线性关系与缓冲池大小无关。 当没有缓存时查询花费的时间由磁盘 I/O 决定与缓冲池大小无关。在 IOPS 相同的情况下是否使用 select count(*) 预热缓冲池并没有区别。
5. 如果无法完整地缓存整个表则有无缓存的查询运行时间差异是恒定的。
另请注意如果无法完整地缓存整个表虽然查询运行时会突然上升但运行时是可预测的。无论表的大小如何有无缓存的时间差异是恒定的。原因是表的部分数据缓存在缓冲区中这里的时间差异来自从缓冲区读取数据节省的时间。
查询23select count(*) from where 12345 这个查询使用了索引。由于不是范围查询MySQL 只需要利用 B 树的路径从上到下查找页面并将这些页面缓存到 innodb 缓冲池中即可。
我创建的表的 B 树的深度都是 3因此前面的 3~4 次 I/O 都被拿来预热缓冲区平均耗时 4~6 毫秒。之后再次运行相同的查询MySQL 就会直接从内存中查找结果耗时为 0.5 毫秒约等于网络 RTT。如果缓存页面长时间未命中并从缓冲池中逐出则必须再次从磁盘加载该页面这样就需要磁盘 I/O最多 4 次。
查询4select * from where 12345 这个查询涉及两次索引查找。由于 select * 需要查询获取的 person_name、person_id 字段并不在索引中因此在查询执行期间数据库引擎必须查找 2 个 B 树。它首先查找 insert_time B 树获取目标行的主键然后查找主键 B 树获取该行的完整数据如下图所示 这就是我们应该在生产中避免 select * 的原因。此次实验证实此查询加载的页面块比查询 2 或 3 多出了 2 倍且最高可达 8 倍。查询的平均运行时间为 6~10 毫秒也是查询 2 或 3 的 1.5~2 倍。
传言是怎么来的 首先我们需要知道 innodb 索引页的物理结构。默认页面大小为 16k由页眉、系统记录、用户记录、页面导向器和尾部组成。只有剩下的 14~15k 用来存储数据。
假设你使用 INT 作为主键4 字节每行 1KB 的有效负载。每个叶页可以存储 15 行一个指向该页的指针需要 4812 字节。因此每个非叶页最多可以容纳 15k / 12 字节 1280 个指针。如果你有一个 4 层的 B 树它最多可以容纳 1280128015 24.6M 行数据。
回到 HDD 占据市场主导地位且 SSD 对于数据库而言过于昂贵的时代4 次随机 I/O 可能是我们可以容忍的最坏情况而使用 2 次索引树查找的查询甚至会使情况变得更糟。当时的工程师想要控制索引树的深度不希望它们太深。而如今 SSD 越来越流行随机 I/O 比以前便宜了因此我们应该反思一下 10 年前的规则。
顺便说一句5 层 B 树可以容纳 128012801280*15 31.4B 行数据超过了 INT 所能容纳的最大数据量。对每行大小的不同假设将导致不同的软限制或小于或大于 2000 万行。例如在我的实验中每一行大约是 816 字节我使用 utf8mb4 字符集所以每个字符占用 4 个字节4 层 B 树可以容纳的软限制是 29.5M。
结论 ▶ Innodb 缓存池的大小、表的大小决定了是否会出现性能降级。
▶ 判断是否需要拆分 MySQL 表的一个更有意义的指标是查询运行时/缓冲池命中率。如果查询总是命中缓冲区则不会有任何性能问题。2000 万行只是一个经验值。
▶ 除了拆分 MySQL 表之外增加 Innodb 缓存池的大小和数据库的内存也是一个选择。
▶ 如果可能请避免在生产中使用 select *这类语句在最坏的情况下会导致 2 次索引树查找。
▶ 我个人的意见考虑到 SSD 现在越来越流行2000 万行不再是 MySQL 表的有效软限制。