Django处理海量历史数据的冷热分离方法
真正的冷热分离核心是热数据留主库、冷数据移出主库以降低索引压力、减少备份体积并避免拖慢查询计划器;否则跨分区查询仍会全表扫描。
真正的冷热分离核心是热数据留主库、冷数据移出主库以降低索引压力、减少备份体积并避免拖慢查询计划器;否则跨分区查询仍会全表扫描。

冷热分离不是加个路由就能解决的
直接在 Django 的 urls.py 里按时间分路由(比如把 /archive/2020/ 指向另一个 view)只是表层分流,不碰数据存储结构。真正的冷热分离核心是:**热数据留在主库高频读写,冷数据移出主库、降低索引压力、减少备份体积、避免拖慢查询计划器**。否则哪怕你路由分开了,Article.objects.filter(pub_date__year=2018) 这类跨分区查询仍会扫全表或触发低效索引,PostgreSQL 或 MySQL 都扛不住千万级历史记录。
用 Django ORM 做归档迁移容易踩的三个坑
别用 QuerySet.delete() + bulk_create() 直接搬——它不保留自增 ID、不处理外键约束、不兼容事务回滚边界。生产环境必须保证原子性与一致性:
- 用
django.db.transaction.atomic包裹整个迁移批次,单次操作控制在 5000 条以内(避免长事务锁表) - 归档前先禁用相关模型的
post_save信号(否则每条都触发缓存更新或日志写入) - 目标表(如
article_archive)字段定义必须和原表完全一致,包括db_column、default、null,否则bulk_insert会静默丢字段
示例关键逻辑:
with transaction.atomic():
qs = Article.objects.filter(pub_date__lt='2020-01-01').select_related('author')
records = [ArticleArchive.from_article(a) for a in qs.iterator(chunk_size=2000)]
ArticleArchive.objects.bulk_create(records, batch_size=1000)
qs.delete() # 真删除,非软删
定时脚本别依赖 Django shell 或 manage.py runscript
用 manage.py runscript 跑归档,一旦脚本卡住或被 kill,没有 checkpoint 机制,下次得重头来;用 shell 交互式执行更不可控。正确做法是写独立 Python 脚本,通过系统 cron 或 APScheduler 触发,并自带断点续传:
- 每次运行前查
ArchiveLog.objects.filter(status='done').order_by('-end_time').first(),取上次归档截止时间作为本次起点 - 把当前批次的
min_id和max_id记进ArchiveLog表,失败时可精准重试 - 脚本开头加
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings'),确保能加载 settings 和数据库路由
别把脚本放在 management/commands/ 下假装是命令——它需要稳定运行数小时,而 Django 命令默认无超时保护、无日志轮转、无资源隔离。
数据库路由配置要区分「读」和「写」两个维度
只靠 db_for_read 把历史查询导到从库没用,因为归档后的热查询(如后台搜索)仍会命中主库。必须配合模型层面的路由控制:
- 在
settings.DATABASES中为归档库单独配一个 alias,比如'archive' - 定义路由类,对
ArticleArchive模型强制所有读写走archive,但对Article模型的get_queryset方法做条件判断:if pub_date < timezone.now() - timedelta(days=730): return using('archive') - 注意
select_related和prefetch_related会忽略路由——跨库关联必须手动拆解,比如先查ArticleArchive,再用author_id单独查主库User
路由不是开关,是细粒度的流量染色。没做写的分离,归档库就只是个备份盘;没做读的动态判定,冷数据查询照样压垮主库连接池。
最常被忽略的是外键引用完整性:归档后,Comment.article_id 仍指向主库 Article.id,但对应记录已不存在。要么改外键为 IntegerField 并加业务层校验,要么用视图做跨库联合查询——后者在 PostgreSQL 里可用 postgres_fdw,MySQL 则基本只能妥协为应用层双查。


































