应根据语义需求选择:单纯追加用 insert,去重更新用 upsert;但 upsert 依赖数据库唯一索引与驱动支持,mysql 8.0.19+、postgresql 可用,sqlite 部分支持,SQL Server 不支持。

用 upsert 还是 insert,取决于你是否需要「存在则更新、不存在则插入」的语义。如果只是单纯追加数据,insert 更快更轻量;如果要避免重复、保持唯一性且批量处理,upsert 是 laravel 9+ 提供的正确选择——但它的行为和底层 SQL 有强绑定,不注意会 silently 失败。
upsert 的实际行为依赖数据库驱动支持
upsert 在 MySQL 和 PostgreSQL 中表现不同,SQLite 仅部分支持,SQL Server 完全不支持(Laravel 会退化为逐条 insert + update)。调用前必须确认你的数据库版本和驱动配置:
- MySQL 8.0.19+ 支持
INSERT ... ON DUPLICATE KEY UPDATE,Laravel 会自动映射 - PostgreSQL 需要
ON CONFLICT子句,且$uniqueBy参数必须对应表中已存在的UNIQUE或PRIMARY KEY约束字段 - 若指定的
$uniqueBy字段没有对应唯一索引,MySQL 会报错1062 Duplicate entry,PostgreSQL 直接抛出SQLSTATE[42703]: undefined column
DB::table('users')->upsert( [ ['email' => 'a@example.com', 'name' => 'Alice', 'score' => 100], ['email' => 'b@example.com', 'name' => 'Bob', 'score' => 85], ], ['email'], // $uniqueBy:必须是数据库中已有 UNIQUE 约束的字段 ['name', 'score'] // $updateColumns:只更新这些字段,其他字段(如 created_at)不会被 touch );
insert 不做冲突检测,但大数据量时要注意内存与事务
insert 只负责批量写入,不查重、不更新。适合日志、埋点、导入原始数据等场景。问题在于:一次塞几万条容易 OOM 或触发 MySQL max_allowed_packet 限制。
- 建议按
1000行分批,用循环 +DB::transaction()包裹 - 不要把整个 csv 解析后一次性
array_chunk再传给insert,而应在文件流读取过程中边解析边插入 - 使用
DB::statement()手动拼接多值 INSERT(如INSERT INTO t VALUES (),(),()...)比 Eloquent 的insert()快 3–5 倍,但需自己处理 SQL 注入(务必用DB::raw()和参数绑定)
foreach (array_chunk($data, 1000) as $chunk) { DB::table('events')->insert($chunk); }
upsert 的 $updateColumns 参数不能包含主键或唯一键
这是最容易踩的坑:$updateColumns 列表里如果误写了 id 或 email(而 email 又在 $uniqueBy 里),PostgreSQL 会报 ON CONFLICT DO UPDATE command cannot affect row a second time,MySQL 则可能静默忽略更新或报错。Laravel 不校验这个逻辑,全靠开发者自查。
- 正确做法:$updateColumns 只放业务字段,比如
['status', 'updated_at'] - 时间戳字段要显式写进 $updateColumns,否则
updated_at不会自动更新 - 如果想让
updated_at自动设为当前时间,得配合DB::raw('NOW()')或在模型中启用timestamps并手动赋值
真正麻烦的不是语法,而是 upsert 背后那层数据库约束——没建好唯一索引,它就不是 upsert,只是个会崩的 insert。别跳过 php artisan migrate 里的 Schema::unique() 步骤。