You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用INSERT ... ON DUPLICATE KEY时如何避免自增ID跳号?

如何避免ON DUPLICATE KEY UPDATE导致自增ID跳号?

问题背景

我现在使用以下SQL语句,通过ON DUPLICATE KEY UPDATE实现重复键时更新行:

$stmt = $dbCon->prepare("INSERT INTO videos_rating (videos_rating_video_fk, " . 
                        " videos_rating_user_fk, " . 
                        " videos_rating_rating) " . 
                        " VALUES (:video_id, " . 
                        " :user_id, " . 
                        " :video_rating) " . 
                        " ON DUPLICATE KEY UPDATE videos_rating_rating = :video_rating");

脚本功能正常,但遇到了自增列id跳号的问题:

  • 初始表为空,给某视频评分后生成ID为1的行;
  • 再次给同一视频评分时触发更新,此时无问题;
  • 但其他用户给新视频评分时,新行的ID会从3开始而非2,最终表数据类似:
id | videos_rating_user_fk | videos_rating_rating
1  | 1                     | 4
3  | 2                     | 5

我知道ID不需要“美观”,但跳号(比如30→51→82这类)让人困扰,还担心最终会达到UNSIGNED BIGINT的上限。没找到同类问题,若有相关讨论也烦请告知。


原因分析

这是MySQL(假设你使用的是MySQL)的正常设计行为:当执行INSERT ... ON DUPLICATE KEY UPDATE时,MySQL会先尝试插入新行,此时会为自增列预分配一个值;之后发现唯一键冲突,转而执行更新操作,但已经预分配的自增值不会被回滚——这就导致后续插入新行时,自增值会跳过已经预分配但未使用的数字,出现跳号。

解决方案

1. 先查询再执行对应操作(彻底解决跳号)

这是最直接的方案,虽然多了一次查询,但能完全避免自增跳号问题:

// 先检查当前用户是否已对该视频评过分
$checkStmt = $dbCon->prepare("SELECT 1 FROM videos_rating WHERE videos_rating_video_fk = :video_id AND videos_rating_user_fk = :user_id");
$checkStmt->bindParam(':video_id', $videoId);
$checkStmt->bindParam(':user_id', $userId);
$checkStmt->execute();

if ($checkStmt->rowCount() > 0) {
    // 已存在评分,执行更新
    $updateStmt = $dbCon->prepare("UPDATE videos_rating SET videos_rating_rating = :video_rating WHERE videos_rating_video_fk = :video_id AND videos_rating_user_fk = :user_id");
    $updateStmt->bindParam(':video_id', $videoId);
    $updateStmt->bindParam(':user_id', $userId);
    $updateStmt->bindParam(':video_rating', $videoRating);
    $updateStmt->execute();
} else {
    // 不存在评分,执行插入
    $insertStmt = $dbCon->prepare("INSERT INTO videos_rating (videos_rating_video_fk, videos_rating_user_fk, videos_rating_rating) VALUES (:video_id, :user_id, :video_rating)");
    $insertStmt->bindParam(':video_id', $videoId);
    $insertStmt->bindParam(':user_id', $userId);
    $insertStmt->bindParam(':video_rating', $videoRating);
    $insertStmt->execute();
}

⚠️ 注意:如果你的系统处于高并发场景,一定要在查询和更新/插入操作之间添加事务或者行级锁,避免出现竞态条件(比如两个请求同时检测到不存在评分,都执行插入导致唯一键冲突)。

2. 调整InnoDB自增锁模式(效果有限)

MySQL的innodb_autoinc_lock_mode参数控制自增锁的行为,默认值是2(交错模式),这种模式为了提升插入性能,会批量预分配自增值,容易加剧跳号。你可以将其修改为0(传统模式):

-- 临时生效,重启MySQL后失效
SET GLOBAL innodb_autoinc_lock_mode = 0;

或者在my.cnf/my.ini中添加以下配置实现永久生效:

innodb_autoinc_lock_mode = 0

不过这个方案有明显缺点:传统模式会使用表级自增锁,会降低批量插入的性能,而且即使修改了这个参数,INSERT ... ON DUPLICATE KEY UPDATE的插入尝试仍然会预分配自增值后回滚,所以不一定能彻底解决你的问题,更适合批量插入导致的跳号场景。

3. 接受跳号(成本最低的长期方案)

虽然跳号看起来不够“美观”,但UNSIGNED BIGINT的上限是18446744073709551615——即使每次更新都跳一个号,要达到这个上限需要的操作次数是天文数字,几乎不可能在实际业务中发生。而且自增ID的核心作用是作为主键唯一标识行,连续与否完全不影响业务逻辑。如果没有强制要求,这其实是最省心的方案。


内容的提问来源于stack exchange,提问作者ii iml0sto1

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 04:07:16