Laravel大表迁移卡住的根本原因是数据库DDL操作阻塞,而非PHP超时;应拆分迁移、显式使用ALGORITHM=INPLACE和LOCK=NONE,并禁用migrate:fresh。

大表迁移(尤其是带外键、索引或百万级数据的表)在 Laravel 中极易触发超时,根本原因不是 PHP 脚本超时,而是数据库 DDL 操作本身阻塞、锁表、I/O 密集——php artisan migrate 卡住时,往往 MySQL 或 PostgreSQL 正在执行 ALTER TABLE,PHP 只是被动等待。
为什么 migrate 会卡在 ALTER TABLE 上
当迁移包含以下任一操作时,风险陡增:
-
Schema::table('orders')->string('tracking_number')—— 为千万行表加字段,MySQL 8.0+ 默认需拷贝全表(除非显式指定ALGORITHM=INPLACE) -
$table->foreignId('user_id')->constrained()—— 添加外键会触发全表扫描验证一致性 -
$table->index(['status', 'created_at'])—— 为大表建复合索引,I/O 和排序开销巨大 - 迁移文件中混合了
create+alter+insert—— 顺序执行放大锁持有时间
此时即使你调高 max_execution_time 或 set_time_limit(0),也无济于事:数据库层已锁死,PHP 只能干等。
绕过阻塞:用原生 SQL + ALGORITHM=INPLACE(MySQL)
对 MySQL 用户,关键不是“让 Laravel 做得更快”,而是“不让 Laravel 做”。直接在迁移中执行带优化提示的原生语句:
public function up(MigrationBuilder $builder)
{
DB::unprepared('ALTER TABLE orders ADD COLUMN tracking_number VARCHAR(255) AFTER id, ALGORITHM=INPLACE, LOCK=NONE');
}
注意三点:
-
ALGORITHM=INPLACE告诉 MySQL 尽量避免表拷贝(仅适用于支持的操作,如加字段、加索引) -
LOCK=NONE显式要求不锁表(若操作不支持,MySQL 会报错而非降级为 LOCK=SHARED) - 务必先在测试库执行
EXPLAIN FORMAT=JSON ALTER TABLE ...验证是否真走 INPLACE —— 不同 MySQL 版本和存储引擎行为差异极大
拆分迁移:建表 / 加索引 / 加外键分三步走
Laravel 默认按文件名顺序执行迁移,但外键和索引不是原子操作。强行在一个迁移里写完,等于把所有风险打包引爆。应主动拆解:
- 第一步迁移:
create_orders_table.php—— 只建表,不加任何索引或外键 - 第二步迁移:
add_indexes_to_orders.php—— 仅添加查询常用索引(避开created_at这类高基数字段的单列索引) - 第三步迁移:
add_foreign_keys_to_orders.php—— 最后加外键,且确保父表(如users)已存在并有主键索引
这样做的好处:每步失败可单独回滚;DBA 可对第二、三步迁移手动加 pt-online-schema-change 或 gh-ost;上线窗口可控。
生产环境必须禁用 migrate:fresh
php artisan migrate:fresh 在大项目中是定时炸弹。它会清空所有表再逐条重跑数百个迁移——其中可能包含几十个针对大表的 ALTER。哪怕你用了 Schema Dump,一旦某张大表的初始结构定义里就含了外键或全文索引,dump 导入阶段仍会卡住。
真正安全的做法是:
- 开发/测试环境:用
schema:dump+db:import快速拉起干净库(但 dump 文件本身要剔除大表的INSERT数据) - 生产环境:永远只用
migrate,且每次上线前人工 Review 新增迁移是否含高危 DDL;配合DB::listen()记录每条 SQL 执行耗时,建立慢迁移基线 - 严禁在生产部署脚本里出现
migrate:fresh或migrate:reset—— 它们不是“重置”,是“自毁”
最常被忽略的一点:外键约束检查(FOREIGN_KEY_CHECKS=0)不能乱关。有些团队为加速迁移临时关掉它,结果导致后续数据写入破坏引用完整性——这比慢几分钟更难修复。


















