分表查询前必须确认分表策略、查询参数能否唯一确定目标表、MySQL版本对UNION ALL条件下推的支持;手动生成带子表WHERE条件的UNION ALL语句最稳,避免ORM自动分表;跨表过多时应改用视图或归档表。

分表查询前必须确认的三件事
不提前理清分表规则、数据分布和查询边界,封装出来的逻辑大概率会漏数据或查错表。MySQL 分表(尤其是按时间/ID哈希)不是单纯拼表名,关键在 WHERE 条件能否下推到单表——否则就得全表扫描再聚合,性能直接崩。
- 明确分表策略:是
user_202401这类时间后缀,还是order_001这类哈希后缀?对应解析逻辑完全不同 - 确认查询参数是否能唯一确定目标表:比如查
created_at BETWEEN '2024-01-01' AND '2024-01-15',必须能映射到user_202401且仅此一张 - 检查 MySQL 版本对
UNION ALL的优化能力:8.0.19+ 支持UNION ALL下推条件下推,老版本得靠应用层控制并发和结果合并
用 Python 动态生成 UNION ALL 查询语句
别依赖 ORM 的“自动分表”(如 SQLAlchemy sharding 扩展),它们往往绕过查询优化器,且难以调试。最稳的方式是手动生成 UNION ALL SQL,再交给 MySQL 原生执行。
示例:按月分表 log_YYYYMM,查最近 3 个月日志:
from datetime import datetime, timedelta
<p>def build_monthly_union_sql(table_prefix: str, start_date: str, end_date: str) -> str:</p><p><span>立即学习</span>“<a href="https://pan.quark.cn/s/00968c3c2c15" style="text-decoration: underline !important; color: blue; font-weight: bolder;" rel="nofollow" target="_blank">Python免费学习笔记(深入)</a>”;</p><h1>生成目标月份列表,如 ['202401', '202402', '202403']</h1><pre class="brush:php;toolbar:false;">dt_start = datetime.strptime(start_date, "%Y-%m-%d")
dt_end = datetime.strptime(end_date, "%Y-%m-%d")
months = set()
while dt_start <= dt_end:
months.add(dt_start.strftime("%Y%m"))
dt_start += timedelta(days=32)
dt_start = dt_start.replace(day=1)
parts = []
for m in sorted(months):
table_name = f"{table_prefix}_{m}"
# 每张子表加 WHERE 限制范围,避免跨月数据混入
where_clause = f"created_at >= '{start_date}' AND created_at <= '{end_date}'"
parts.append(f"SELECT * FROM {table_name} WHERE {where_clause}")
return " UNION ALL ".join(parts) + " ORDER BY created_at DESC LIMIT 1000"输出类似:
SELECT * FROM log_202401 WHERE created_at >= '2024-01-01' ...
UNION ALL
SELECT * FROM log_202402 WHERE created_at >= '2024-01-01' ...
注意:WHERE 必须每张子表都写,不能只在外层加——MySQL 不保证 UNION ALL 外层条件能下推。
用 pymysql 执行并合并结果时的坑
直接 cursor.execute(sql) 可能因结果集过大触发 MySQLdb._exceptions.OperationalError: Packet too large,尤其当某张子表返回几十万行时。
- 强制设置
cursor.scroll(0, mode='absolute')无用,UNION ALL结果集是单结果集,无法分页游标 - 正确做法:用
stream=True参数逐行读取,配合生成器 yield,避免内存爆炸 - 字段顺序必须严格一致:不同子表若存在
ALTER TABLE ADD COLUMN时间差,列序可能不一致,导致cursor.description错位
推荐执行封装:
def execute_union_query(conn, sql: str):
with conn.cursor() as cursor:
cursor.execute(sql)
columns = [col[0] for col in cursor.description]
while True:
row = cursor.fetchone()
if row is None:
break
yield dict(zip(columns, row))
什么时候该放弃封装,改用视图或归档表?
如果查询经常跨 6 张以上子表,或需要 JOIN 多个分表(如 order_202401 关联 user_202401),Python 封装就变成负优化:网络往返多、结果合并逻辑复杂、错误难定位。
- 优先考虑 MySQL 8.0+ 的
CREATE VIEW聚合所有子表,让优化器自己决定是否下推 - 高频跨表分析场景,定期用
INSERT INTO archive_table SELECT ... FROM unioned_tables归档到宽表 - 绝对不要在封装里做“先查元数据
information_schema.tables再拼 SQL”——元数据锁+查询延迟会让并发掉一半
分表查询封装真正的难点不在代码,而在判断“这次查询值不值得封装”。多数时候,一张表查 3 个月,比 12 张表查 1 天更慢——因为 I/O 和连接开销被低估了。


















