大表优化
问题
单表数据量达到千万甚至亿级别,查询和写入越来越慢,如何优化?
答案
一、大表问题诊断
| 症状 | 阈值 | 影响 |
|---|---|---|
| 查询变慢 | 单表 > 2000 万行 | B+ 树层级增加,IO 增大 |
| DDL 锁表 | 数据量大时 ALTER TABLE 耗时 | 阻塞业务写入 |
| 备份慢 | 表数据 > 50GB | mysqldump 耗时过长 |
| InnoDB Buffer Pool 不足 | 热数据 > 内存 | 缓存命中率下降 |
-- 查看表行数和大小
SELECT
table_name,
table_rows,
ROUND(data_length / 1024 / 1024, 2) AS data_mb,
ROUND(index_length / 1024 / 1024, 2) AS index_mb
FROM information_schema.TABLES
WHERE table_schema = 'mydb'
ORDER BY data_length DESC;
二、优化方案全景
三、SQL 层面优化
-- 1. 覆盖索引:避免回表
-- ❌ SELECT * 导致回表
SELECT * FROM orders WHERE user_id = 100 ORDER BY created_at DESC LIMIT 20;
-- ✅ 只查需要的列,建联合索引覆盖
CREATE INDEX idx_user_time ON orders(user_id, created_at, order_no, amount);
SELECT order_no, amount, created_at FROM orders
WHERE user_id = 100 ORDER BY created_at DESC LIMIT 20;
-- 2. 深分页优化(延迟关联)
-- ❌ OFFSET 100 万
SELECT * FROM orders ORDER BY id LIMIT 1000000, 20;
-- ✅ 延迟关联
SELECT o.* FROM orders o
JOIN (SELECT id FROM orders ORDER BY id LIMIT 1000000, 20) t
ON o.id = t.id;
-- 3. 范围查询加分区条件
-- ✅ 查询时带上分区键(时间),减少扫描范围
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-02-01'
AND status = 'completed';
四、分库分表
分表时机
- 单表行数 > 2000 万~5000 万
- 单表数据量 > 10GB
- 单表并发写入 > 2000 QPS
| 拆分方式 | 策略 | 适用场景 |
|---|---|---|
| 水平分表 | 按 ID 取模、按时间范围 | 数据量大,结构相同 |
| 垂直分表 | 常用字段与不常用字段拆开 | 宽表,列多 |
| 水平分库 | 按用户 ID 分片到不同库 | 单库性能瓶颈 |
// 水平分表:按 user_id 分 16 张表
function getTableName(userId: number): string {
const shard = userId % 16;
return `orders_${shard.toString().padStart(2, '0')}`;
}
// 插入数据
async function createOrder(userId: number, order: Order) {
const table = getTableName(userId);
await db.query(`INSERT INTO ${table} (user_id, ...) VALUES (?, ...)`, [userId]);
}
分表带来的挑战:
| 问题 | 解决方案 |
|---|---|
| 跨表查询 | 聚合层/中间件(ShardingSphere) |
| 全局排序 | 各分片排序后归并 |
| 全局 COUNT | 维护计数表 |
| 分布式 ID | 雪花算法 / 号段模式 |
| 扩容 | 一致性哈希 / 翻倍扩容 |
五、冷热分离与数据归档
-- 归档脚本:将 3 个月前的数据移到归档表
-- 分批操作,避免大事务
INSERT INTO orders_archive
SELECT * FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL 3 MONTH)
LIMIT 10000;
DELETE FROM orders
WHERE id IN (
SELECT id FROM orders_archive
WHERE created_at < DATE_SUB(NOW(), INTERVAL 3 MONTH)
LIMIT 10000
);
-- 循环执行直到完成
六、Online DDL
对大表做 DDL(加列、加索引)必须使用在线方案:
| 工具 | 原理 | 推荐度 |
|---|---|---|
| MySQL Online DDL | ALGORITHM=INPLACE | 小表可用 |
| gh-ost | 影子表 + binlog 同步 | 推荐 |
| pt-osc | 影子表 + 触发器同步 | 可用 |
# gh-ost 添加索引
gh-ost \
--host=master \
--database=mydb \
--table=orders \
--alter="ADD INDEX idx_status(status)" \
--allow-on-master \
--execute
常见面试问题
Q1: 单表多大时需要分表?
答案:
没有绝对标准,一般参考:
- InnoDB:2000 万~5000 万行(取决于行大小)
- 核心依据:B+ 树 3~4 层时 IO 次数可控
- 假设每页 16KB,主键 8B + 指针 6B → 每页约 1170 个指针
- 3 层:1170 × 1170 × 16 ≈ 2190 万行
- 4 层会增加一次磁盘 IO
更重要的是看实际查询 RT,如果 P99 < 50ms 且 Buffer Pool 够用,2 亿行也可以不分。
Q2: 分表后如何处理跨表查询?
答案:
| 场景 | 方案 |
|---|---|
| 带分片键查询 | 路由到指定表 |
| 不带分片键查询 | 所有分表查询后合并(scatter-gather) |
| 全局排序 | 每张表 Top N → 归并排序 |
| 聚合统计 | 异步汇总到统计表 / OLAP 引擎 |
推荐使用中间件(ShardingSphere、Vitess)屏蔽分片细节。
Q3: 如何进行大表的不停机迁移?
答案:
- 双写阶段:新旧表同时写入
- 历史数据迁移:分批将旧表数据同步到新表
- 数据校验:
pt-table-checksum对比数据一致性 - 切读:逐步将读流量切到新表
- 停旧写:确认无误后停止旧表写入
- 清理:下线旧表
关键:全程有回滚方案,灰度切换。