ThinkPHP分表必须手动控制表名,不能依赖自动路由;需在查询前通过getTableName()、initialize()或Db::name()指定物理表,严格校验后缀格式与时间对齐,并避免跨表UNION,改用并发查询合并结果。

ThinkPHP模型层必须主动控制分表,不能依赖自动路由
框架不会根据时间或ID自动跳转到user_202401这类物理表——查不到数据、报Table 'db.user' doesn't exist,八成是因为还在用默认表名发起查询。
所有分表逻辑必须在查询发起前完成:要么重写getTableName(),要么在initialize()里设$this->table,或者调用Db::name($tableName)显式指定。
- 别在
where()里写create_time >= '2024-03-01'后指望框架“感知”并切表——它完全不看这个条件 -
partition()方法只做一件事:把模型当前$name替换成$name . '_' . $suffix,不推导月份、不加WHERE、不校验合法性 - 用
scope封装分表?注意它不影响getTableName(),表名切换仍需提前完成
按月分表时,表名生成和时间字段必须严格对齐
常见错误是PHP用date('Y_m')生成log_2024_05,但建表时用了log_202405,或数据库时区比PHP快1小时,导致3月31日23点的数据写进log_202404却查log_202403。
- 统一用
date('Ym', $timestamp)(6位纯数字),拒绝Y_m、Y-m等含符号格式,避免解析歧义 -
$timestamp必须是int型时间戳,别传字符串如'2024-05-10'——strtotime()失败会返回false,再date()就变成log_197001 -
create_time字段类型必须是DATETIME或TIMESTAMP,且每张分表都单独建索引;VARCHAR存时间会导致MySQL无法做分区裁剪,EXPLAIN显示扫全表
用户输入的分表参数必须白名单校验,严禁直接拼接
写Db::name('order_' . input('month'))等于把表名控制权交给前端——攻击者传month=202401; DROP TABLE order_202401虽因PDO预处理不执行SQL,但可能触发异常泄露表结构,或绕过后续逻辑。
立即学习“PHP免费学习笔记(深入)”;
- 校验正则只允许
^\d{6}$(如202401),拒绝202413、202400、2024_01 - 月份有效性检查:提取
substr($suffix, 4, 2),确保在01–12范围内 - 合法后缀建议缓存在Redis或静态数组,运行时只做
in_array()匹配,不走正则+业务逻辑双重校验 - 绝对不要用
str_replace('__TABLE__', $input, 'log__TABLE__')这类占位符方案——就是自己造SQL注入入口
跨月查询只能靠PHP合并,别信UNION ALL自动优化
想查“最近三个月订单总数”,写SELECT COUNT(*) FROM (SELECT * FROM order_202401 UNION ALL SELECT * FROM order_202402 ...)不仅ThinkPHP的union()不支持跨物理表嵌套,MySQL对UNION的索引利用也极差,实际性能往往比三次并发查询慢2–5倍。
- 手动循环:构建
['202401', '202402', '202403']数组,对每个后缀调用一次Db::name("order_$suffix")->where(...)->count(),再array_sum() - 并发查更优:用
Swoole\Coroutine\Channel或pcntl_fork并行拉三张表,PHP层合计,便于加缓存 - 注意
where条件中时间范围要随表名动态调整,比如查order_202402时,WHERE必须是create_time >= '2024-02-01' AND create_time ,否则可能漏数据或跨月混查
最易被忽略的是时区与字段类型的隐性耦合:PHP用date('Ym')生成表名,但数据库create_time用的是UTC存储、而PHP默认时区是Asia/Shanghai,差8小时就会让临界时间点错表。这个问题在线上压测时才暴露,但修复成本极高。



















