数据库服务器如何进行容量规划?数据增长、索引与存储预算

"这台数据库服务器还能撑多久?"、"下次扩容要买多大?"、"磁盘又要满了怎么办?"——这些问题背后其实是同一件事:容量规划。它不像故障排查那样紧急,但做不好会带来两类代价:预估过小,三天两头扩容、业务受影响;预估过大,钱白花在闲置资源上。

很多中小团队的容量规划全靠"磁盘快满了就加一块"这种被动模式,结果要么突然告警手忙脚乱,要么长期浪费。这篇文章给一套实用的估算方法:怎么估数据量的增长、索引到底占多少空间、binlog 和备份要预留多少、QPS/TPS 和实例规格怎么对应、什么时候该扩容、以及历史数据怎么归档。所有数字都给经验量级,并说明是经验值,实际以你的真实数据和压测为准。

一、从"数数据"开始:先量化现状

容量规划的第一步不是猜未来,而是量现状。你需要四个数据:

第一,当前数据量。各库各表的大小。

-- MySQL:查看每个库的数据与索引大小
SELECT table_schema AS db,
       ROUND(SUM(data_length)/1024/1024/1024, 2)  AS data_gb,
       ROUND(SUM(index_length)/1024/1024/1024, 2) AS index_gb,
       ROUND(SUM(data_length+index_length)/1024/1024/1024, 2) AS total_gb
FROM information_schema.tables
GROUP BY table_schema
ORDER BY total_gb DESC;

-- 单表明细,找最大的表
SELECT table_schema, table_name,
       ROUND(data_length/1024/1024/1024, 2)  AS data_gb,
       ROUND(index_length/1024/1024/1024, 2) AS index_gb,
       table_rows
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql','information_schema','performance_schema','sys')
ORDER BY (data_length+index_length) DESC
LIMIT 20;
-- PostgreSQL:查看每个库大小
SELECT datname, pg_size_pretty(pg_database_size(datname)) AS size
FROM pg_database ORDER BY pg_database_size(datname) DESC;

-- 查看最大的表(含索引)
SELECT relname AS table_name,
       pg_size_pretty(pg_total_relation_size(relid)) AS total,
       pg_size_pretty(pg_relation_size(relid))       AS table_only,
       pg_size_pretty(pg_total_relation_size(relid) - pg_relation_size(relid)) AS indexes
FROM pg_catalog.pg_statio_user_tables
ORDER BY pg_total_relation_size(relid) DESC LIMIT 20;

第二,增长速率。取几个时间点的数据量,算出每天/每月增加多少。最可靠的办法是定期打点记录,比如每周记录一次各库大小,画成趋势线。至少要有最近一个月的数据才能看出规律。

第三,业务增长预期。用户量、订单量、日志量的增长计划。这块要和业务方对齐,比如"下季度预计用户翻倍"。

第四,资源使用基线。CPU 使用率、内存使用、磁盘 IO、QPS/TPS 的正常水位和峰值水位。这些决定了还剩多少余量。

二、估算数据量的增长

有了增长速率,就能做外推。基本公式:

未来数据量 = 当前数据量 + 日均增长 × 天数 × 增长系数

增长系数用来体现业务增长。如果业务稳定,系数取 1;如果预期增长,取 1.2 到 2 甚至更高。保守估计宁可偏高,因为存储不足的代价远大于稍有富余。

举例:一张订单表当前 200GB,最近三个月每月增加 20GB,也就是日均约 0.65GB,业务预期明年翻倍。

  • 一年自然增长:20GB × 12 = 240GB。
  • 考虑业务翻倍(下半年起加速),保守按 1.5 倍系数:240GB × 1.5 = 360GB。
  • 明年同一时间的数据量约 200 + 360 = 560GB。

这只是数据本身,还没算索引、binlog、备份和文件系统开销。所以实际需要的磁盘会比 560GB 大不少。下面逐项加。

三、索引占用的估算

索引是常被低估的部分。很多人只算数据大小,忘了索引也要占空间,结果磁盘总是比预期先满。

索引占用经验上可以占到数据大小的 20% 到 50%,视索引数量和字段长度而定:只建主键和少量索引,占比接近 20%;索引多、字段长(比如多个字符串列建复合索引),占比能到 50% 甚至更高。上面 MySQL 的查询里 index_gb 就是真实的索引占用,可以据此算出你自己业务的索引比例。

粗略估算:

索引空间 ≈ 数据空间 × 索引比例(经验值 0.2 ~ 0.5)

沿用上面的例子,560GB 数据,按 30% 的索引比例,索引约 168GB,数据 + 索引约 728GB。

需要注意索引还有额外开销:一是 B+ 树结构本身有填充因子和页分裂,写入过程中索引会比最终大小更占空间(可能临时膨胀到 1.5 倍);二是删除数据后,InnoDB 的表空间不会自动缩小,产生的碎片会让实际占用大于逻辑数据量,长期运行的库尤其明显。

-- MySQL 查看表碎片(Data_free 不为 0 说明有可回收空间)
SELECT table_schema, table_name, table_rows, data_free/1024/1024 AS free_mb
FROM information_schema.tables
WHERE data_free > 0
ORDER BY data_free DESC LIMIT 20;
-- 回收碎片(会锁表,务必评估:MySQL 8.0 部分场景可在线)
-- ALTER TABLE orders ENGINE=InnoDB;   -- 重建表,大表慎用,需在低峰执行

风险提示:ALTER TABLE ... ENGINE=InnoDB 或 OPTIMIZE TABLE 会重建表,在大表上可能锁表很久、消耗大量 IO,甚至短时间需要接近原表大小的额外磁盘空间(用于存放临时新表)。执行前必须确认磁盘余量、选低峰期,并做好备份。空间不足或锁表时间过长都可能引发线上故障。

四、binlog、redo log 与备份的占用

这三块经常被忽略,却是磁盘的"隐藏大户"。

Binlog:主从复制和恢复都依赖它,体积和写入量直接相关。经验上,binlog 的日增量大致是日数据变更量的 1 到 3 倍(ROW 格式下,更新一行记录可能产生前后镜像,放大明显)。binlog 保留时间越长,占用越大:

binlog 空间 ≈ binlog 日增量 × 保留天数

比如日增 binlog 30GB,保留 7 天,就是约 210GB。若为了做延迟恢复保留 14 天,则翻倍到 420GB。这也是为什么 binlog 要单独规划、单独清理。

-- MySQL 查看 binlog 使用情况
SHOW VARIABLES LIKE 'binlog_expire_logs_seconds';   -- 8.0,单位秒
-- 旧版是 expire_logs_days,单位天
SHOW BINARY LOGS;

风险提示:不要用 PURGE BINARY LOGS 或直接 rm 去删 binlog,尤其是从库还在用的日志。删掉从库尚未拉取的 binlog 会导致复制中断;删掉做点恢复需要的日志会让恢复失败。删除前务必确认主从位置和备份策略,用 PURGE BINARY LOGS BEFORE '时间' 而非手工删除文件。

Redo log / WAL:InnoDB 的 redo log 默认不大(MySQL 8.0.30 之后可动态调整 innodb_redo_log_capacity,常设 1 到 4GB),但 PostgreSQL 的 WAL 在归档开启时可能持续累积。

备份:本机至少要留一份最近备份的空间。逻辑备份(mysqldump)的文本形式往往比数据本身还大(SQL 文本膨胀),物理备份(xtrabackup)接近原数据大小。建议按"数据 + 索引"的实际大小预留至少 1 份本机备份的空间,如果保留多份则成倍增加。异地备份虽然不占本机磁盘,但传输和存储成本要单独算。

临时空间:排序、大查询、DDL 重建、导入导出都会用临时空间,tmpdir 所在分区要留足余量,否则一个 ALTER TABLE 就可能把 /tmp 撑满导致服务异常。

五、磁盘预算公式

把上面各项汇总,给出一个实用的磁盘预算公式:

所需磁盘 = (数据量 + 索引量) × (1 + 碎片与膨胀系数)
         + binlog 空间
         + 备份空间
         + 临时空间
         + 系统与预留

其中碎片与膨胀系数经验取 0.2 到 0.5(长期运行的库取大值);临时空间取数据量的 10% 到 20%(做重建类操作时需要接近原表大小,建议单独挂盘给 tmpdir);系统与预留至少留 15% 到 20%,因为磁盘使用率过高不仅性能下降,还会触发各种保护和告警(比如 Elasticsearch 的水位、MySQL 写满导致服务异常)。

沿用前面的例子,做个完整估算:

项目 估算 说明
数据 560GB 一年后
索引 168GB 按 30%
小计 728GB 数据 + 索引
膨胀系数 +145GB 按 20%
binlog 210GB 日增 30GB × 7 天
本机备份 728GB 预留 1 份
临时空间 100GB 单独盘更好
系统与预留 200GB 约 15%
合计 约 2111GB 约需 2.1TB

所以这台库一年后需要约 2.1TB 可用磁盘。规划时按"能撑到下次扩容"的时间窗预留,比如要求一次配置支撑 18 个月,那就要再往前推一档。

六、QPS/TPS 与实例规格

存储算完了,还要算计算能力。核心是 QPS(每秒查询)和 TPS(每秒事务),以及它们跟 CPU、内存的对应关系。

先把业务的 QPS/TPS 峰值算出来(注意是峰值,不是平均值,扩容要按峰值留余量)。然后对照经验量级:

实例规格 单机 QPS 量级(简单查询) 说明
2 核 4G 数百到上千 开发、小型站点
4 核 8G 数千 中小业务
8 核 16G 数千到上万 中等业务
16 核 32G 上万到数万 较大业务
32 核以上 数万以上 需要分区/分库分担

这些是经验量级,强烈依赖 SQL 复杂度、数据量、命中率和硬件。复杂查询、全表扫描会大幅拉低这个量级;缓存命中率高、索引良好的简单查询则能到上限。必须用压测在自己的数据和 SQL 上验证,不要照搬。

内存方面,MySQL 的 innodb_buffer_pool_size 希望把热数据装下,所以内存容量的目标是让工作集(经常访问的数据+索引)能放进缓冲池。如果缓冲池装不下热数据,磁盘 IO 会成为瓶颈。一个粗略判断:如果缓冲池大小明显小于实际活跃数据量,就该考虑加内存了。

-- MySQL 缓冲池命中率,理想情况应在 99% 以上(经验值)
SHOW STATUS LIKE 'Innodb_buffer_pool_read_requests';
SHOW STATUS LIKE 'Innodb_buffer_pool_reads';
-- 命中率 = 1 - reads / read_requests

冷数据多、热数据少时(比如几十 GB 的表只有几 GB 被频繁访问),缓冲池不必装下全表,装下热数据即可。所以内存规划要基于工作集而非总数据量。

七、扩容触发阈值

光会算还不够,要提前定好"什么时候必须扩容",把被动救火变成主动规划。

建议设置几个触发点:

  • 磁盘使用率超过 70% 开始评估,超过 80% 立即准备扩容,超过 90% 是高危状态。留出 20% 以上缓冲,是为了应对突发写入、备份、临时文件。触发值因环境而异,但绝不该等到 95% 才动手。
  • 缓冲池命中率持续低于 99%,且 IO 等待上升,考虑加内存。
  • CPU 使用率峰值持续超过 70%,或 Threads_running 频繁触顶,考虑加 CPU 或优化。
  • 平均响应时间较基线明显上升,是最直接的体感指标。
  • 单表数据量超过一定规模(如几千万到上亿行,视行大小而定)时,评估是否需要归档或分表。

配合监控打点,把数据量和资源使用率画成趋势图,能提前看到"还有多久到阈值",在到达前安排扩容窗口。

八、历史数据归档

当数据只增不减时,任何容量规划都会被时间击穿。归档是让小库保持健康的关键手段。

常见做法:

冷热分离:把超过一定时间的冷数据(如一年前的订单、半年前的日志)迁移到归档库或对象存储,主库只保留近期热数据。这样主库体积可控,缓冲池能装下热数据,性能稳定。

按时间分区:用分区表(MySQL 的 PARTITION BY RANGE、PostgreSQL 的声明式分区),把数据按月/按天分区。好处是删除旧数据只需 DROP PARTITION(秒级,且不产生大事务和碎片),比 DELETE 快得多,也不会造成主从延迟。

-- MySQL:按时间分区的示意(按月分区)
-- 注意:分区表的主键必须包含分区键
CREATE TABLE orders (
  id BIGINT NOT NULL,
  created_at DATETIME NOT NULL,
  amount DECIMAL(10,2),
  PRIMARY KEY (id, created_at),
  KEY idx_created (created_at)
) PARTITION BY RANGE (TO_DAYS(created_at)) (
  PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')),
  PARTITION p202602 VALUES LESS THAN (TO_DAYS('2026-03-01')),
  PARTITION pmax    VALUES LESS THAN MAXVALUE
);

-- 删除旧分区(比 DELETE 几百万行安全高效得多)
ALTER TABLE orders DROP PARTITION p202601;

风险提示:DROP PARTITION 会直接删除该分区内所有数据且不可恢复,且不写 binlog 的逐行记录(对主从来讲是元数据操作,但数据确实没了)。执行前必须确认该分区数据确实不再需要、且有可用备份。分区表的 MAXVALUE 分区若忘记维护会导致新数据插入失败,要建立定期增加分区的流程。

归档到对象存储:更冷的冷数据可以导出成文件(如 Parquet、压缩 CSV)放到对象存储,成本低、容量近乎无限,需要时再导入分析库。这也是备份异地化的一部分。

归档要有节奏和监控:定期(如每月)执行归档任务,归档后确认主库体积下降、备份时间缩短。

九、把容量规划做成例行工作

容量规划不是一次性的,而是持续过程。建议:

  • 每周记录数据量、磁盘使用率、QPS/TPS、缓冲池命中率等关键指标,形成趋势。
  • 每月评审一次增长速率和趋势,对照扩容阈值。
  • 每季度结合业务规划更新一次容量预测,提前安排扩容预算。
  • 每次大版本上线或大促前,做专项容量评估和压测。

把数据沉淀下来,扩容决策就从"拍脑袋"变成"看数据"。

常见问题

Q:索引真的会占那么多空间吗?
视情况。经验上占数据的 20% 到 50%。索引多、字段长占比高。用 information_schema.tables 的 index_length 能查到真实值,据此算你业务的索引比例。

Q:为什么数据删了,磁盘没变小?
InnoDB 删除数据只是标记可复用,表空间不会自动缩小,会留下碎片。需要重建表(OPTIMIZE TABLE 或 ALTER TABLE ... ENGINE=InnoDB)才能回收,但大表操作有风险,要评估后低峰执行。

Q:binlog 要保留多久合适?
取决于恢复需求。一般 7 天是常见值,需要做延迟恢复或从库较多时保留更久。保留越久占盘越多,要平衡恢复能力和磁盘成本。

Q:备份要占多少空间?
按"数据 + 索引"的实际大小预留至少一份,若保留多份则叠加。逻辑备份文本可能比物理备份更大。异地备份不占本机空间但要算传输和存储成本。

Q:什么时候该分库分表?
单表数据量大到影响查询和维护(常见参考是几千万到上亿行,视行大小而定),且索引优化已经无法解决时。先考虑归档和分区,分库分表复杂度高,是最后手段。

Q:扩容选纵向(升配)还是横向(加节点)?
小规模优先纵向升配,简单、无需改架构。到达单机上限或需要高可用时转向横向(读写分离、分库分表、集群)。

总结

  • 容量规划先量化现状(数据量、增长速率、业务预期、资源基线),再做外推,别凭感觉。
  • 磁盘预算要算全:数据 + 索引(经验占数据 20%~50%)+ 膨胀碎片 + binlog + 备份 + 临时空间 + 系统预留,缺一项就会提前爆盘。
  • QPS/TPS 与实例规格大致对应,但必须以压测和真实工作集为准;内存规划的目标是把热数据放进缓冲池。
  • 提前设好扩容阈值(磁盘 70%/80%/90%、缓冲池命中率、CPU 峰值、响应时间),把被动救火变成主动规划。
  • 归档和按时间分区是控制数据体积的关键,DROP PARTITION 比 DELETE 安全高效,但不可恢复,务必确认备份。
  • 把容量规划变成每周打点、每月评审、每季预测的例行工作,扩容决策靠数据而不是拍脑袋。