Django的DatabaseRouter仅支持分库,无法自动分表,因其只在查询前选择数据库别名,不干预SQL生成与表名替换,分表需手动处理建表、查询、写入及迁移。

分库分表不能靠 Django 的 DatabaseRouter 完全实现,它只负责“路由到库”,不处理“表拆分逻辑”;真正要分表(比如按时间/用户ID哈希),必须手动干预查询、写入和迁移流程。
为什么 DatabaseRouter 只能分库,不能自动分表
Django 的路由机制在 db_for_read() 和 db_for_write() 阶段只决定用哪个数据库别名(如 'shard_01'),此时 SQL 还没生成,更没机会改表名。Django ORM 不支持运行时动态替换 Model._meta.db_table,所有查询仍指向原始定义的表名。
- 你配置了 4 个分片库,但所有
User模型操作仍默认查user表 —— 如果每个库没手动建好对应表,直接报Table 'user' doesn't exist -
makemigrations生成的迁移文件也只针对单库结构,不会自动为每个分片生成带后缀的表(如user_2024) - 跨分片的
SELECT ... JOIN、aggregate()、distinct()等操作天然不支持,Router 不会帮你合并结果
分库:正确实现 DatabaseRouter 的三个关键点
核心是让读写请求落到正确的数据库别名上,且避免路由冲突或漏判。
- 路由函数必须返回
str(数据库别名),不能返回None或空字符串,否则 Django 回退到'default'—— 这在多租户场景下极易引发数据混写 - 对
instance(模型实例)做路由时,必须确保instance已赋值分片键(如tenant_id或user_id),否则instance.tenant_id是None,导致误路由 - 涉及多模型关联的操作(如
ForeignKey查询),需在allow_relation()中显式放行同分片内的关联,否则save()时可能抛Exception: Relation not allowed
示例:db_for_write() 中按用户 ID 哈希选库:
立即学习“Python免费学习笔记(深入)”;
def db_for_write(self, model, **hints):
if model._meta.app_label == 'orders':
instance = hints.get('instance')
if instance and hasattr(instance, 'user_id'):
shard_id = instance.user_id % 4 # 假设 4 个分片
return f'shard_{shard_id:02d}'
return None分表:必须自己接管建表、查询和写入路径
没有魔法开关,只有三类手动控制点:
-
建表:不用
python manage.py migrate直接建表,改用脚本遍历分片库,执行CREATE TABLE user_001 (...)等语句;或者用schema_editor+ 自定义迁移类,在migrate时动态生成表名 -
查询:避免直接
User.objects.all();改用原生 SQL 或封装查询函数,例如User.objects.using('shard_01').raw('SELECT * FROM user_2024'),表名由业务逻辑拼接 -
写入:重写模型
save()方法,在存入前根据分片键计算目标表名,并临时修改self._meta.db_table(注意:仅对当前实例有效,且不能用于批量bulk_create)
常见坑:bulk_create 绕过 save(),所以必须配合 using='shard_x' + 手动指定 table_name 参数(Django 不提供该参数),最终往往得退回到 connection.cursor().execute()。
实际项目中更可行的折中方案
硬刚全量分库分表成本高、维护难,多数团队选择分层收敛:
- 核心交易类模型(如
Order)按user_id % N分库 + 按月分表,其余模型(如Product、Category)保留在'default'库只读同步 - 用中间件或上下文管理器提前绑定当前请求的
shard_key(如从 URL 或 JWT 解析),避免每次都要从 instance 取值 - 放弃 ORM 跨分片聚合,把统计类需求下沉到 ClickHouse 或预聚合表,Django 层只做单分片 CRUD
最易被忽略的是迁移一致性:新增字段时,必须确保所有分片库的对应表(order_202401, order_202402…)同时执行 ALTER TABLE,否则后续写入会因字段缺失失败 —— 这没法靠 Router 自动完成。



















