根本原因是model::upsert()在hyperf中仍走模型生命周期、未合并sql且默认查库判断冲突;应改用db::insert()配合on duplicate key update原生语句,确保唯一索引、字段一致、分片控制,并手动包裹事务。

Hyperf模型批量更新慢,根本原因不是数据库本身,而是Model::update()或Model::upsert()这类方法在内部仍走完整模型生命周期——每条数据都实例化、校验、触发事件、拼SQL、再单条执行。哪怕你传入1000条,它默认仍是N次查询或N次独立UPDATE。要真正提速,必须绕过模型层,直连Db::insert()配合ON DUPLICATE KEY UPDATE语句。
为什么Model::upsert()在Hyperf里还是慢
Model::upsert()看似是批量接口,但在Hyperf 3.x中它底层仍会尝试先SELECT查是否存在(尤其没指定$uniqueBy时),再决定INSERT或UPDATE;更关键的是,它不合并成一条SQL,而是拆成多条单独语句执行。实测万级数据下,耗时可能比手写原生语句高5–8倍。
- 它会为每条数据创建
Model实例,触发booting、setAttribute、casts等开销 - 字段映射依赖
$fillable和$casts,动态处理进一步拖慢速度 - 若没显式传
$uniqueBy参数,Hyperf会尝试读取主键+唯一索引,额外查information_schema - 并发环境下,多个协程共用同一连接池但未复用PDO预编译句柄,导致prepare重复开销
怎么用Db::insert() + ON DUPLICATE KEY UPDATE写批量更新
核心是把更新逻辑“反向”写成「插入语句 + 冲突时更新」,MySQL原生支持,一条SQL搞定多行。前提是表必须有PRIMARY KEY或UNIQUE索引(比如id或user_id)。
- 构造二维数组,确保每项键名与数据库字段完全一致:
['id' => 1, 'sort' => 7],不能是['user_id' => 1]而字段叫id - 调用
Db::table('your_table')->insert($batch)不行——它不支持ON DUPLICATE KEY UPDATE;必须用Db::insert()并手动拼SQL - 正确写法是:
Db::insert("INSERT INTO your_table (id, sort, updated_at) VALUES ? ON DUPLICATE KEY UPDATE sort = VALUES(sort), updated_at = VALUES(updated_at)", [$values]),其中$values是array_merge(...$batch)展平后的索引数组 - 注意
VALUES(sort)引用的是当前这一行的输入值,不是原表值;如需保留原值(比如只更新sort不碰updated_at),就写updated_at = updated_at
容易踩的三个坑
这三个问题不解决,哪怕语句写对了,照样报错或静默失败。
-
SQLSTATE[HY093]: Invalid parameter number:常见于$batch里混用关联数组和索引数组,或字段顺序不统一。必须保证所有子数组键名一致、顺序一致,且不含空值或null字段(除非对应列允许NULL) - “更新没生效”,实际是唯一索引没建对:检查是否真在目标字段(如
id)上建了UNIQUE或PRIMARY KEY;如果用user_id做冲突判断,但表只有id主键,那ON DUPLICATE KEY UPDATE永远不触发 - 超长报错
Packets larger than max_allowed_packet are not allowed:单次最多塞1000行左右。分片用array_chunk($data, 500),别硬刚2000+;同时确认MySQL配置max_allowed_packet≥ 64M
最常被忽略的一点:Hyperf里Db::insert()不自动开启事务。如果你的批量更新需要原子性,必须手动Db::transaction()包裹,否则某一批失败,前面成功的不会回滚。另外,VALUES()函数在MySQL 5.7+才完全稳定,线上用8.0+更稳妥。











