1. 为什么需要冷热分层
一句话总结: 90% 的访问集中在新近 10% 的数据上,让热数据与冷数据共用一套昂贵存储是巨大浪费——冷热分层把"热"留在高性能层,“冷"迁到低成本层。
数据库成本随数据量线性增长,但访问热度是幂律分布:新写入的数据被高频查询,几个月前的历史数据几乎无人问津。把所有数据都放在同一套热存储上,等于为"几乎不读的老数据"持续支付高昂的存储与索引成本。
访问热度(典型幂律):
最近 7 天数据 → 80%+ 的查询流量
最近 90 天数据 → 15% 的查询流量
一年前数据 → 5% 的查询流量(多为审计/合规)
存储成本(SSD vs 归档):
热层 SSD :每 GB 成本高、性能好
冷层归档/对象存储:每 GB 成本可低一个数量级
冷热分层带来的收益不仅是省钱,还让热表的索引更小、锁竞争更少、备份更快。
2. 冷热识别:谁该进冷层
2.1 冷热的判断维度
| 维度 | 热数据 | 冷数据 |
|---|---|---|
| 访问频率 | 秒级/分钟级被查 | 几乎不查 / 偶发 |
| 时间窗 | 最近 7~90 天 | 超过保留窗口 |
| 变更特性 | 可能更新 | 只读,永不改 |
| 价值 | 支撑线上业务 | 合规、审计、分析 |
2.2 冷热识别的工程化做法
不要靠感觉,用访问统计驱动:
-- 用慢日志/审计日志统计:不同时间窗数据的查询占比
-- 例如统计表 orders 中,按 created_at 时间窗的查询命中分布
SELECT DATE_FORMAT(created_at, '%Y-%m') AS month,
COUNT(*) AS row_count
FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
GROUP BY month ORDER BY month;
识别口诀:
1. 以业务保留期为准(合规要求 3 年,就必须留 3 年)
2. 以访问统计为准(连续 90 天无查询,进冷层)
3. 以运维窗口为准(每晚 2 点做归档任务)
一句话: 冷热分界不是一个固定天数,而是"业务保留期 + 访问频率 + 存储成本"三个约束的交点。先定保留期,再按访问统计划冷热线。
2.3 热表与冷表的物理差异
热与冷不只是逻辑概念,它们在物理层有截然不同的形态:
| 维度 | 热表 | 冷表/归档表 |
|---|---|---|
| 存储介质 | SSD/NVMe | HDD/对象存储/廉价 SSD |
| 索引 | 完整,按查询路径建 | 精简,只保审计与检索 |
| 压缩 | 少压缩,保随机读 | 高压缩(列式/页压缩) |
| 备份 | 高频率 | 低频甚至免备份 |
| 锁 | 行级,最小化 | 只读,无并发写 |
冷层该「牺牲什么」:
✅ 牺牲随机读性能(老数据访问极少)
✅ 牺牲更新能力(冷数据只读)
❌ 不能牺牲:可检索、可恢复、可审计
冷热分层不是「把热表复制一份到便宜机器」,而是按访问模式重新设计存储形态:热表为点查优化,冷表为压缩与低成本优化。
3. 归档策略设计
3.1 归档的目标与原则
归档三个目标:
1. 热表瘦身:表变小 → 索引小、锁少、查询快
2. 成本控制:冷数据进低成本存储
3. 合规保留:老数据仍可查询/审计
归档五个原则:
□ 可回溯:归档数据必须可查询、可校验
□ 可逆:迁移出错的冷数据能回退到热层
□ 定时:归档任务可编排、可重试、可监控
□ 无感:归档过程不阻塞线上读写
□ 一致:归档后主键/约束/统计信息不丢
3.2 归档策略矩阵
| 维度 | 策略选项 |
|---|---|
| 归档对象 | 全表归档 / 按时间窗分批 |
| 归档去向 | 同实例冷表 / 独立历史库 / 对象存储 |
| 归档频率 | 每日 / 每周 / 每月 |
| 保留期 | 热层 N 天 / 冷层 M 年 |
| 触发方式 | 定时任务 / binlog 订阅触发 |
一个典型策略:
订单表 orders
热层保留:最近 90 天(SSD,主库)
冷层保留:90 天 ~ 5 年(历史库 orders_archive)
过期清理:超过 5 年 → 导出到对象存储 → 删除
4. 分区归档:最优雅的路径
4.1 时间分区 + 秒级搬迁
如果表按时间做了 RANGE 分区,归档就可以用分区操作实现"秒级搬移”:
-- 按天分区的订单表
CREATE TABLE orders (
id BIGINT,
created_at DATETIME,
amount DECIMAL(10,2),
PRIMARY KEY (id, created_at)
) PARTITION BY RANGE (TO_DAYS(created_at)) (
PARTITION p20260629 VALUES LESS THAN (TO_DAYS('2026-06-30')),
PARTITION p20260630 VALUES LESS THAN (TO_DAYS('2026-07-01')),
PARTITION pFuture VALUES LESS THAN MAXVALUE
);
-- 归档 30 天前的分区:先交换到归档表,再 DROP
ALTER TABLE orders EXCHANGE PARTITION p20260530 WITH TABLE orders_archive;
ALTER TABLE orders DROP PARTITION p20260530;
对比:
DELETE 1000 万行老数据 → 分钟级,产生大量 redo/binlog
DROP 一个分区 → 秒级,直接释放数据文件
所以:能用分区,就绝不用 DELETE 归档。
4.2 分区归档的自动化流程
每日归档流水线:
1. 识别到期分区(created_at < 今天 - 90 天)
2. EXCHANGE 到归档表(同库,结构一致)
3. 校验归档表行数 = 原分区行数
4. DROP 原分区,回收热层空间
5. 归档表导出 → 历史库/对象存储 → 本地归档表清空
一句话总结: 时间分区让归档从"逐行 DELETE"进化为"分区级秒搬",代价是建表时就要按时间设计分区并保证分区键进主键。
4.3 非分区表的归档兜底方案
历史遗留表没有分区怎么办?不要急着 DELETE,用「时间窗分批 + 分批删除」:
-- 分批归档:每次取 1 万条老数据搬到归档表
INSERT INTO orders_archive (id, created_at, amount)
SELECT id, created_at, amount FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
LIMIT 10000;
-- 确认归档成功后,分批删除原表(加 LIMIT 防大锁)
DELETE FROM orders
WHERE created_at < DATE_SUB(NOW(), INTERVAL 90 DAY)
LIMIT 10000;
分批规则:
□ 每批 5000~10000 行,批间 sleep 0.1~1s
□ 跑在业务低峰期,观察主从延迟与磁盘 IO
□ 用主键排序保证分批可断点续跑
这是「没有分区时的妥协方案」:分批 + LIMIT 控制锁与复制延迟。治本方案仍是先给大表补上时间分区,之后归档走 EXCHANGE/DROP 秒级完成。
5. 历史库与查询路由
5.1 归档数据放哪:三种形态
| 形态 | 位置 | 查询方式 | 适用 |
|---|---|---|---|
| 同库冷表 | 同实例归档表 | 直接 SQL | 数据量适中 |
| 独立历史库 | 单独实例/低配机器 | 应用路由 | 量大、隔离热库 |
| 对象存储 | S3/OSS + 分析引擎 | 湖仓查询 | 海量、低频分析 |
推荐演进:
初期(量小) → 同库冷表,SQL 无感
中期(量大) → 独立历史库,应用层路由
远期(海量) → 对象存储 + 数仓,走分析链路
5.2 查询路由设计
归档后热表查不到老数据,必须让查询能"路由"到正确的层:
统一查询入口 QueryRouter:
SELECT ... FROM orders WHERE created_at >= 90 天内
→ 路由到热表 orders
SELECT ... FROM orders WHERE created_at < 90 天前
→ 路由到历史库 orders_archive
SELECT ... 不按时间过滤
→ 先热表,miss 再查冷层(或提示改查历史)
# 应用层按时间路由(伪代码)
def query_orders(start, end):
if start >= ARCHIVE_CUTOFF: # 全热窗
return query_hot(start, end)
if end < ARCHIVE_CUTOFF: # 全冷窗
return query_archive(start, end)
hot = query_hot(start, ARCHIVE_CUTOFF) # 跨界:两段都查
old = query_archive(ARCHIVE_CUTOFF, end)
return merge(hot, old)
一句话: 路由是归档后必须补的一课。设计上优先"按时间窗双表查询",业务无感知;实在做不到,再提供显式的历史查询接口。
6. 归档的数据校验与可回溯性
归档最怕"数据悄悄丢"。每次归档都要做三重校验:
-- 1. 行数校验:归档前后行数必须一致
SELECT COUNT(*) FROM orders
WHERE created_at < '2026-06-30'; -- 迁移前
SELECT COUNT(*) FROM orders_archive; -- 迁移后(应相等)
-- 2. 主键校验:两边主键集合一致,无缺失无重复
SELECT COUNT(*) FROM (
SELECT id FROM orders
WHERE created_at < '2026-06-30'
EXCEPT
SELECT id FROM orders_archive
) AS missing; -- 期望 0 行
-- 3. 抽样校验:抽查若干行字段值一致
SELECT * FROM orders_archive
WHERE created_at >= '2026-06-01' AND created_at < '2026-06-30'
LIMIT 100;
可回溯保障:
□ 每次归档写审计表:归档时间、表名、时间窗、行数、MD5
□ 归档数据设只读权限,防误改
□ 冷层保留"回迁脚本":出错时可反向 EXCHANGE 回热表
□ 定期做归档恢复演练(从对象存储拉回并校验)
一句话总结: 归档动作本身是"高风险数据搬迁",必须把校验(行数、主键、抽样)与审计固化到流程里,并保证冷层数据可回迁、可恢复演练。
6.1 归档审计表设计
每轮归档都应落一条审计记录,保证「可追溯、可对账」:
CREATE TABLE archive_audit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
archive_batch VARCHAR(64) NOT NULL, -- 批次号
src_table VARCHAR(128) NOT NULL, -- 源表
dst_table VARCHAR(128) NOT NULL, -- 目标
time_start DATETIME NOT NULL, -- 归档时间窗起点
time_end DATETIME NOT NULL, -- 归档时间窗终点
rows_moved BIGINT NOT NULL, -- 迁移行数
checksum_md5 VARCHAR(64), -- 全量行指纹
status ENUM('ok','failed','rollback') NOT NULL,
finished_at DATETIME NOT NULL,
KEY idx_batch (archive_batch)
);
-- 对账:本批迁移行数与源表应删行数相等
SELECT batch, rows_moved, time_start, time_end
FROM archive_audit WHERE status = 'ok' ORDER BY id DESC LIMIT 10;
审计的价值:
1. 归档出错时可定位到具体批次回滚
2. 审计/合规审查时能证明数据去向
3. 每月对账:所有已归档批次与历史库总量吻合
归档是「不可见的数据搬移」,审计表就是让搬运过程「可见」的账本。没有审计的归档,等于裸奔。
7. 避坑清单
| 坑 | 表现 | 对策 |
|---|---|---|
| 没做分区就归档 | DELETE 大表归档,锁库半天 | 建表就按时间 RANGE 分区 |
| 冷热线拍脑袋 | 归档过早影响查询,过晚成本不减 | 访问统计 + 保留期双驱动 |
| 归档只搬不校验 | 数据悄悄丢失 | 行数/主键/抽样三重校验 |
| 归档后查不到老数据 | 业务线报错"数据不见了" | 提前设计查询路由/双表查询 |
| 归档表无索引 | 冷层查询全表扫 | 按查询路径重建冷层索引 |
| 冷数据永不回迁 | 误归档无法恢复 | 保留回迁脚本 + 恢复演练 |
| 忘了保留期合规 | 提前删了审计所需数据 | 先确认法律/合规保留期 |
8. 总结
| 环节 | 要点 |
|---|---|
| 识别 | 访问统计 + 业务保留期,划出冷热分界线 |
| 策略 | 定归档对象、去向、频率、保留期 |
| 归档 | 时间分区 + EXCHANGE/DROP,秒级搬移 |
| 历史库 | 同库冷表 → 独立历史库 → 对象存储渐进 |
| 路由 | 按时间窗双表查询,业务无感 |
| 校验 | 行数 + 主键 + 抽样 + 审计表 + 恢复演练 |
冷热分层与归档是一场**“用时间换空间、用低频换低成本”的治理。它的价值不止省钱:热表更瘦、索引更小、备份更快、查询更稳。真正要做好,核心是把分区设计、访问统计、三重校验、查询路由**这四件事在一开始就纳入表结构设计——而不是等数据膨胀到不可收拾再补救。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。