ThinkPHP数据迁移需用PHP类描述结构变更而非SQL脚本,TP8中禁用模型和Db::name(),应使用$this->schema()或显式连接;chunk分批须按主键范围分片、禁用日志与自动提交,并优先选用cursor()降低内存占用。

ThinkPHP 数据迁移不是写 SQL 脚本,而是用 PHP 类描述结构变更;Chunk 分批处理不是加个循环就行,核心是避免内存堆积和游标失效。
迁移文件里不能直接调用模型或 Db::name()
TP6 的迁移类里用 $this->table('user')->insert() 是可行的,但 TP8 已废弃该写法。TP8 迁移阶段不加载应用配置、中间件和模型,Db::name('user') 可能因连接未初始化而报错,更别说调用 UserModel::create() 这类依赖完整框架上下文的操作。
- 结构变更(建表/加字段)必须用
$this->schema()或原生Db::execute() - 数据填充(如初始化管理员)应改用
Db::connect('mysql')->name('user')->insert($data),显式指定连接 - 所有数据库操作必须包裹在
Db::transaction()中,否则单条失败会导致迁移状态不一致 - 不要在
up()里查外部 API 或读大文件——迁移应只做数据库层动作
chunk() 的 where 条件必须稳定,不能依赖时间字段
用 Db::name('log')->chunk(500, function ($rows) { ... }) 很方便,但若表里有高频写入或软删除逻辑,created_at 或 updated_at 在分批过程中可能被更新,导致漏数据或重复处理。
- 优先按主键范围分片:
where('id', 'between', [$start, $end]),主键不会变 - 若无自增主键,可用唯一且单调递增的字段(如
order_no字符串前缀+时间戳) - chunk() 内部默认用
limit/offset,高偏移时性能陡降,务必配合索引 - 每次 chunk 执行完,手动调用
gc_collect_cycles()防止 PDOStatement 持有大量资源
迁移中执行大批量数据同步,必须禁用日志和事务自动提交
在 up() 方法里插入 10 万行用户数据,如果每条都走事务提交,MySQL redo log 会暴涨,甚至触发锁等待超时。ThinkPHP 默认开启事务,但迁移器本身不控制底层 autocommit 状态。
立即学习“PHP免费学习笔记(深入)”;
- 先关自动提交:
Db::execute('SET autocommit = 0'); - 用
insertAll()批量写入,每 2000 行手动Db::commit()一次 - 执行前临时关闭 MySQL 日志:
Db::execute('SET sql_log_bin = 0');(仅限从库或离线环境) - 结束后恢复:
Db::execute('SET autocommit = 1; SET sql_log_bin = 1');
chunk() 和 cursor() 的内存表现差异很大
cursor() 返回原始 PDOStatement,逐行 fetch,内存恒定在几百 KB;chunk() 虽分块,但每块仍把整批结果转成数组 + Collection 对象,10 万行下内存可能飙到 200MB 以上。
- 纯导出或只读遍历,无条件选
cursor():Db::name('big_table')->cursor()->each(function ($row) { ... }); - 需要对每块做聚合计算(如统计、去重),才用
chunk(),且块大小压到 500 以内 - chunk() 回调函数内禁止再调
Db::table()->find()—— 单次查询变成 N² 次 IO - cursor() 不支持
count()或sum(),需另起 COUNT 查询获取总数
真正难的不是写对语法,而是判断哪一步该用 schema、哪一步该切主键范围、哪一步必须关 binlog——这些边界没卡准,跑一半就 OOM 或死锁。



















