2. 先说结论没有绝对的行数红线很多人习惯背一个数字比如“单表别超过2000万行”“超过500万就卡”我见过不少团队拿着这些经验值开会拍板但实际一测根本不是那么回事。MySQL单表能存多少数据从来不是单纯由“行数”决定的。有的表几百万行就慢得让人抓狂有的表上亿行依然跑得飞快。差别在哪三个方面数据行的真实大小、查询模式是否命中索引、写入并发压力有多大。行数只是一个最容易挂在嘴边的表象真正该关注的是这三件事的综合结果。先说数据行大小。一张表如果全是短整型、定长字符一行可能只有几十字节2000万行算下来也就几个GB。可如果一张表里塞了多个TEXT字段、JSON字段一行动辄几KB甚至几十KB500万行就是几十GB。同样是“几百万行”物理体积天差地别InnoDB缓冲池要扛的压力也完全不同。再说查询模式。如果你的查询条件走的是主键或者唯一索引单点查询在小数据量和大数据量下差距很小因为B树的层数只跟数据页数量相关不跟行数线性相关。反过来如果查询经常要全表扫、或者走一个选择性很差的索引那数据量一大延迟直接起飞。最后是写入并发。高并发写入下即使表里数据量不大锁竞争、磁盘IO、redo log的刷盘压力也会先一步让系统变慢这时候问题根本不在于“你存了多少行”而在于“你每秒钟来了多少写”。所以我在评估一张表的数据量是否合适时不会先问“多少行”而是会按下面的顺序问自己几个问题这张表一行的平均长度是多少查询有没有稳定用上索引写入TPS是多少只有把这些问题理清了才能给出一个真正有用的建议。2.1 不同数据量级下的真实表现差异为了让你有个直观感知我用一个常见的业务表举例订单表包含订单号、用户ID、商品ID、金额、状态、创建时间等字段一行的平均长度大概在200字节左右。这个表在三个数据量级下的表现差异我实测下来大概是这样的情况数据量表体积估算单行主键查询范围查询走索引常见问题100万行约200MB1ms以内5ms左右无感1000万行约2GB1-2ms20-50ms偶尔慢查询1亿行约20GB2-5ms100ms以上索引膨胀、写入变慢这套数据有两个背景条件要说明一是数据能完全放进InnoDB缓冲池buffer pool里没有明显的磁盘IO瓶颈二是表上的索引控制在两三个以内没有过度索引。如果你的环境跟这个差异很大数值也会差很多。从表体积上算200字节一行1亿行就是20GB。如果服务器的内存只有32GBInnoDB buffer pool按默认比例大概只能分到20GB不到意味着缓冲池刚够放热数据冷热交替稍微一多磁盘读就成为常态。这时候你再去跑一个大范围查询性能就不是增长而是断崖。这里多说一句很多人在做容量规划时只看磁盘剩余空间却忘了内存才是InnoDB真正的“性能放大器”。磁盘能存下不代表数据库扛得住buffer pool装不下热数据才是大多数MySQL性能问题的根源。3. 判断单表数据量是否合适关键要看四个维度很多人把“单表能存多少”当成一个静态边界这其实是个误区。同一个数量级下不同业务场景的体感完全不同。我习惯用四个维度来综合判断行数、存储容量、索引深度、写入压力。这四个维度任何一个冲到顶都可能成为压垮系统的最后一根稻草。3.1 行数维度为什么2000万行是个常被提起的数字“单表2000万行”这个说法流传很广因为它背后确实有一个计算逻辑InnoDB的主键索引是B树结构当主键是BIGINT时一个数据页默认16KB非叶子节点大概能存几百个索引项算下来三层B树大概能支撑两千万到三千万条记录。三层的B树意味着每次查询最多只需要三次磁盘IO就能定位到叶子节点这在机械硬盘时代是一个很舒服的深度。但注意这个2000万行的估算有两个前提一是主键是递增的BIGINT二是数据行比较短小单页能装下足够多的行。如果你的主键是UUID字符串或者行宽很大这个数字会往下掉。反过来如果你的行非常短、或者一页能塞更多数据实际支撑量还能再往上走。我自己做判断的时候不会死盯2000万这个数而是看主键索引的深度是否稳定在3层以内。层数一旦到了4层每次查询多一次磁盘IO热点场景下延迟抖动就会明显变大。层数怎么查可以用下面这条SQL看索引的根页信息或者直接通过information_schema.tables统计估算-- 查看 innodb 索引页相关信息 SHOW TABLE STATUS LIKE your_table_name;不过这条命令只能看到行数和数据长度不能直接看到层数。要精确看B树层数可以用工具解析物理文件或者用innodb_ruby这类分析工具。生产环境一般不需要搞得这么重通过表体积和主键大小估算就够了如果主键是BIGINT数据量在3000万行以内几乎肯定还在三层以内。3.2 存储容量维度磁盘便宜但别忽视内存有些团队觉得反正现在SSD便宜几TB磁盘随便上表大一点无所谓。磁盘确实便宜但InnoDB的缓存结构决定了数据量一旦超过buffer pool的承载能力性能就会掉一个量级。假设你的机器内存是64GBInnoDB buffer pool设置默认按内存的75%来分大概48GB。一张表20GB看起来装得下但如果同一实例上还有其他热表、还有排序和join的临时结果占用缓冲池实际能给这张表用的空间可能只剩一半。这时候如果查询触发的数据页不在缓存里就必须走磁盘读SSD的随机读延迟大概是内存的几十倍到上百倍量大之后慢查询自然就来了。所以我在评估一张表时会先看一个比值表的活跃数据体积 / buffer pool可用空间。如果这个比值大于0.5我就开始警觉大于1的话就要认真考虑冷热分离或者归档了。有一个细节很多人会忽略表体积是持续增长的今天比值0.3三个月后可能就0.8了。所以评估的时候不要只看当前值要看业务增速和未来半年的余量。数据量评估从来不是一个时间点的判断题而是一条趋势线的预测题。3.3 索引深度与维护成本维度数据行多了以后不仅数据页变多索引占用的空间和更新成本也在涨。每加一个二级索引就相当于给表多加了一棵B树每次INSERT都要同步更新所有索引。如果一张表有5个二级索引每次插入的成本就差不多是“1次主键索引写入5次二级索引写入”这还没算随机IO的影响。有一个很典型的坑我见过不止一次业务初期为了查询方便给表加了四五个单列索引数据量小的时候没感觉等到了千万级写入直接变慢了一半。后来排查发现二级索引的维护成本已经把写入拖垮了删除冗余索引之后TPS立刻恢复。所以在评估表的大小时不光要看查询还要看这张表的写入频率。如果写入和更新很频繁二级索引的膨胀会先于数据行本身成为瓶颈。那些“单表几百万行但是写入很慢”的案例十有八九是索引太多而不是数据量本身太大。3.4 写入并发与锁竞争维度最后再补一个维度并发写入。很多表数据量并不大比如只有几十万行但每秒写入请求几百上千这个时候表内热点行、间隙锁、自增锁都会成为瓶颈。数据量本身不是问题并发才是。这种情况的判断方式很简单看数据库的Threads_running、Innodb_row_lock_waits和Innodb_row_lock_time指标。如果锁等待次数和等待时间持续走高说明问题不在“存了多大”而在“写得多猛”。结论就是单表数据量没有一块铁板边界行数、容量、索引深度、写入压力这四个维度加权起来看才能真正判断一张表的状态是否健康。4. 超了怎么办分区、归档和冷热分离的实际做法不管你怎么规划业务总有超出预期的时候。接下来这部分是重点我会把“数据量已经超标”的几种应对方案讲透包括它们各自的适用边界和实操中的坑。4.1 分区表能救你吗还是坑你一说表太大很多人第一反应是做分区。MySQL的分区表确实能把一张逻辑表拆成多个物理分区但我不建议把它当成“数据量超标”的首选方案原因有三个。第一分区表并不能减少IO总量。按范围分区比如按月分区之后虽然查询可以利用分区裁剪只扫一个分区的数据但如果你有一个查询没法用分区键过滤它依然会扫描所有分区性能甚至比普通表还差因为多个分区的文件碎片增加了随机IO。第二分区键必须非常稳定。一旦业务SQL里没有带上分区键或者分区键的选择跟实际查询模式不匹配你就把一个简单问题变成了复杂问题。我有一个真实的教训某系统按“创建时间”做了RANGE分区但业务里最频繁的查询是按“订单号”查详情结果每次查询都要扫全部历史分区还不如不做分区时用一个联合索引来得快。第三MySQL分区表在DDL和运维上有很多坑。比如分区表的全局唯一索引支持有限制二级索引的分区键必须包含在主键里8.0之前这会导致表结构设计被分区方案绑架。如果版本还没升到8.0分区的收益和代价往往不成正比。所以我对分区的建议很直接如果业务上有明确的时间范围查询并且90%以上的查询都会带上分区键你可以用分区否则不要为了拆表而拆表优先考虑下面两种方案。4.2 归档最朴素也最有效的手段归档听起来不够“高级”但在大部分业务场景下它是性价比最高的方案。核心思路很简单把“很久没被访问”的历史数据从热表中搬出去。具体怎么做我常用的方式是在业务表旁边建一张同样的历史表字段结构保持一致然后按时间维度把超过半年或一年的明细数据批量INSERT到历史表再从主表DELETE。整个过程用脚本跑在凌晨低峰期执行跑完后对主表做ANALYZE TABLE更新统计信息。这里有一个关键细节归档不是只考虑磁盘空间更重要的是缩短主表的行数和索引体积。很多团队归档之后发现查询没变快就是因为归档条件选得太宽比如只删了三五个月的数据对主表整体体量没有实质影响。归档的周期和保留时长要根据你的buffer pool大小来反推理想状态是主表活跃数据不超过buffer pool的三分之一。还有一个容易踩的坑DELETE大批量数据时会导致主键索引的碎片化如果一直只删不整理表空间不回收索引扫描效率还会下降。所以归档后一定要做表空间整理也就是重建表MySQL 8.0里可以直接用ALTER TABLE ... ENGINEInnoDB来在线重建不会阻塞读写太多。4.3 冷热分离上升到架构层面的解法当数据量增长不是一次性冲高而是持续性的趋势归档只能治标。这个时候要上升到架构层面做氢热分离。冷热分离的核心思想是业务查询天然有“热点窗口”比如只有最近30天的订单会被频繁查询三个月前的订单几乎无人问津。那就可以把热数据留在MySQL在线库冷数据落到另一个存储层比如归档库、对象存储或分析型存储系统。查询入口做路由先查热库查不到再查冷存储。这套方案比单纯归档更进一步因为冷数据的查询路径是专门设计的不会影响在线MySQL实例的性能。我在实际项目中是这么落地主库保留最近6个月的订单六个月前的数据每天定时同步到归档库。运营和分析类查询直接走归档库用户端的订单查询先查Redis缓存缓存没有再看主库。这样主库的表体量基本稳定在几千万行的量级写性能和读性能都稳定可控。冷热分离有个前提就是业务上要能清楚定义“冷”和“热”的边界并且产品上允许冷数据的查询有一点点额外延迟。如果每一笔数据用户都可能随时高频访问就谈不上冷热分离那时候要做的就是靠真正的分布式方案来支撑了。4.4 真正的分布式到这一步才需要分库分表我一直强调单表数据量过大时先做归档和冷热分离实在不行再考虑分库分表。因为分库分表引入的复杂度是最高的分布式事务、全局ID生成、跨库查询、动态扩容、数据迁移每一个都是实打实的工程挑战。什么样的场景才必须走上这条路我的判断标准是单表数据量级到了亿级别同时写入和查询都是高并发高频且数据没有明显的“冷热边界”可以直接分离。换句话说业务的每一行数据都可能是热的这时候靠单实例已经扛不住物理资源的极限才需要把数据分散到多个实例上。分库分表的方案选型上如果团队有自研能力可以做中间件层如果没有就用现成的分布式数据库方案尽量少自己造轮子。这里不展开讲具体产品但有一条经验可以肯定能晚点引入就晚点引入因为一旦上了分布式所有简单的SQL和事务都要重新审视开发效率和排查问题的难度会明显增加。5. 具体操作一张表从一千万到一亿行的扩容实录说了这么多判断和方案我拿一个模拟项目X的实际操作过程举例尽量把过程写具体方便你照着排查自己的系统。这个系统的核心表是“交易流水表”初始设计时有三个二级索引用户ID、交易状态、创建时间。上线一年后数据量到了8000万行当时已经出现几个问题按时间范围查询月报要跑几秒批量写入时偶尔报锁等待超时磁盘空间也只剩一个月余量。5.1 第一步先查现状别急着动表结构接到这个任务我先没急着改表而是把现状指标拉出来看了一遍-- 查看表基本信息 SELECT table_name, table_rows, ROUND(data_length / 1024 / 1024, 2) AS data_mb, ROUND(index_length / 1024 / 1024, 2) AS index_mb, ROUND((data_length index_length) / 1024 / 1024, 2) AS total_mb FROM information_schema.tables WHERE table_schema your_db AND table_name trade_flow;同时看了CPU、内存、磁盘IO几个维度。排查结论是数据量8000万行数据文件12GB索引文件5GBbuffer pool当时是24GB理论上还装得下但月报查询每次都要扫四五千万行加上并发写入磁盘IO经常跑到接近上限。这个阶段最关键的判断就是主表压力主要来自大范围查询和频繁写入之间的碰撞而不是单纯的数据总量。所以直接分库分表属于过度设计正确路径是“冷热分离索引优化”。5.2 第二步做归档任务把主表瘦身然后我设计了一个归档脚本核心逻辑如下-- 创建历史表 CREATE TABLE trade_flow_history LIKE trade_flow; -- 批量插入历史数据每次一千万行以内分多次执行 INSERT INTO trade_flow_history SELECT * FROM trade_flow WHERE create_time DATE_SUB(NOW(), INTERVAL 6 MONTH) LIMIT 1000000; -- 确认插入成功后删除主表相应数据 DELETE FROM trade_flow WHERE create_time DATE_SUB(NOW(), INTERVAL 6 MONTH) LIMIT 1000000;这里有个非常重要的问题DELETE不能一次性删太多建议单批5万到10万行并且在业务低峰期执行。批次太大容易导致锁范围过大主从延迟飙升。我在实操中会控制每批8万行执行完一批后等待3到5秒再执行下一批避免对在线业务造成明显影响。归档完成后数据量从8000万降到了3500万主表体积从17GB降到8GB月报查询时间从几次秒降到了1秒以内。这个效果非常立竿见影但我知道这只是治标如果继续跑半年数据量又会回来。5.3 第三步调整索引结构减少不必要的索引开销归档之外我还顺手把表上的索引做了一次减法。原来的三个二级索引里“交易状态”这个索引选择性非常差状态只有四五种取值大量查询用不上它但每次INSERT都要维护它收益很低。我确认了业务上没有“只用交易状态作为唯一查询条件”的场景后直接把这个索引删了。这里给一个索引设计的体会二级索引的价值取决于查询能不能用它完成“索引覆盖”或者“迅速缩小范围”。如果一个索引区分度低、又没办法覆盖查询需要的字段那它大概率是在帮倒忙。百余万行的时候不明显到了千万级多余的索引就是实打实的写入成本。5.4 第四步配置层配合临时保命在归档脚本还没全量跑完之前我先做了一步临时配置调整把innodb_io_capacity和innodb_buffer_pool_size适当调大。这一步不是长久之计但能缓解写入高峰期的问题给归档争取时间。调整buffer pool要小心不是在配置文件里改完就生效8.0里可以动态调整但调大后也会吃内存别把操作系统内存挤爆。SET GLOBAL innodb_buffer_pool_size 64 * 1024 * 1024 * 1024;这个必须按你实际内存来定别照抄。我的建议是buffer pool上限不要超过物理内存的80%同时留出足够的内存放操作系统、连接线程和排序缓冲否则就会频繁触发内存交换比不调还要糟。6. 监控这套方案是否生效需要长期盯的四个指标做完上述改造不代表事情就结束了。数据量问题是一点一点积累的你必须把监控做成常态化才能在下一次“超标”之前提前介入。我长期盯的指标主要有四个你可以直接加进监控系统指标含义健康阈值参考buffer pool命中率数据页在内存中命中的比例持续低于95%要警觉慢查询数量超过阈值时间的SQL数量每天超过几十条就要分析磁盘IO利用率数据读写压力长时间超过70%要看IO瓶颈主从延迟从库落后主库的时间超过业务容忍底线就要处理buffer pool命中率这条特别值得展开说。如果你发现命中率持续低于90%说明很多查询都在走磁盘IO那就不再是SQL本身的问题了大概率是热数据总量已经超出内存承载能力。这时候你能做的选择只有两个加内存或者给表减负。再优秀的SQL优化都救不了“内存装不下所有热点数据”这个结构性问题。另外慢查询日志一定要开并且要有日常分析习惯。我在排查很多数据量问题时发现所谓的“表太大变慢”根因常常是某个查询没走索引或者索引选择失败。数据量放大之后本来可以忍的坏查询会变得完全不能忍慢查询日志会非常清楚地帮你定位到罪魁祸首。7. 从容量规划角度设计阶段就该考虑的长期策略最后聊点设计阶段的建议。很多人等表数据量真的上来了才到处找方案其实很多痛苦在设计期是可以避免的。第一审表的时候就想清楚保留周期。有些数据天生就有生命周期比如日志、流水、状态流转记录。设计表结构时就应该配套设计归档计划数据保留多久、归档到哪、谁来触发归档。不要等运营来问“为什么库这么大”才补作业。第二索引要克制。建立索引的唯一标准是“有真实查询需要”而不是“这个字段以后可能有用”。多加一个索引INSERT和UPDATE的成本就多一分。到了千万级再来删索引虽然能救回一些性能但已经白白付出了大量写入成本。第三容量评估要结合增长速度。如果你的业务月增长是1000万行那你现在只有3000万行不代表能高枕无忧因为三个月后就是6000万半年后就是9000万。评估时要把业务增速算进去提前设计好数据生命周期策略。第四不要神话任何数字。我看到过很多团队拿“2000万行”作为硬性标准把还没到量的表强行拆成好几个表反而导致一堆跨表查询和应用层代码改动。这类所谓“最佳实践”带来的额外复杂度往往比收益大得多。数据量是否合适永远要结合实例规格、查询模式、写入压力、增长趋势综合判断。我自己在实际项目里踩过最深的坑就是只看“行数”做方案。有一次一张表到了8000万行我看了看行数就建议分库分表后来仔细一测才发现其中超过60%是半年前没人访问的冷数据。先用两周做了归档主表降到3000万行性能就恢复正常了完全不需要动分库分表方案。那次之后我再判断表容量永远先问自己一句这里面到底有多少是热数据所以回到开头那个问题——MySQL单表存多大的数据量比较合适我的答案很简单能把“热数据”装进内存、能把“索引深度”控制在三层以内、能让“写入并发”不被锁和IO拖垮在这个范围内行数多一点少一点都不是核心问题。超过这个范围优先做归档和冷热分离最后才考虑分库分表。工程上少折腾就是最大的效率给自己留出可维护的余地比追求一个看似光鲜的“最优数字”重要得多。
阅读完成 · 觉得有帮助?