select count(*)检查主键冲突在高并发下失效,因其非原子操作:两个事务同时查到“不存在”后插入,触发error 1062;安全做法是用insert ... on duplicate key update等数据库原生命令原子执行。

为什么直接用 SELECT COUNT(*) 检查主键冲突在高并发下会失效
因为这不是原子操作:先查再插之间存在时间窗口,两个事务同时查到“不存在”,然后都执行 INSERT,最终触发唯一键冲突(ERROR 1062: Duplicate entry)。这种写法在 Navicat 中跑单条 SQL 看似没问题,但压测或上线后立刻暴露。真正安全的做法是让数据库自己判断并拒绝重复,而不是靠应用层“预判”。
Navicat 中应优先使用 INSERT ... ON DUPLICATE KEY UPDATE 或 REPLACE INTO
MySQL 场景下,把“检查 + 插入”合并为一条语句,由存储引擎在索引层面原子性完成冲突判定:
-
INSERT INTO users (id, name) VALUES (123, 'Alice') ON DUPLICATE KEY UPDATE name = VALUES(name);—— 冲突时更新,不报错 -
REPLACE INTO users (id, name) VALUES (123, 'Alice');—— 冲突时先删后插(注意:会触发DELETE和INSERT两条 binlog,且自增 ID 可能跳变) - PostgreSQL 对应的是
INSERT ... ON CONFLICT DO UPDATE,语法更严谨,推荐优先迁移至此
在 Navicat 查询编辑器中直接执行这类语句,执行计划里不会出现 SELECT 扫描步骤,自然避开竞争窗口。
如果必须用 SELECT 预检,请强制走唯一索引并加 FOR UPDATE
仅当业务逻辑确实需要“查出旧值再决定是否插入”时才考虑此路径。此时必须满足两个条件,否则执行计划仍会显示 ALL 或 index 全扫:
- WHERE 条件字段必须有唯一索引(如
UNIQUE KEY (user_id)),不能只是普通索引 - SQL 必须显式加锁:
SELECT * FROM users WHERE user_id = 123 FOR UPDATE; - 该语句需和后续
INSERT在同一个事务内,且事务隔离级别不低于READ COMMITTED
在 Navicat 中点击“解释”按钮,确认执行计划的 type 是 const 或 eq_ref,且 key 列明确显示索引名;若出现 ALL 或 range,说明索引未命中,预检就失去意义。
Navicat 执行计划里最该盯住的三列: type、key、rows
高并发场景下,这三列直接决定你那条“检查 SQL”会不会成为瓶颈:
-
type = const:表示通过主键或唯一索引精确匹配,最快,可接受 -
key为空或显示NULL:索引完全没用上,哪怕表只有 1 万行也会卡住连接池 -
rows值远大于 1(比如几百或几千):说明优化器误判了选择性,可能因统计信息过期,需手动执行ANALYZE TABLE users;
特别注意:Navicat 的“可视化解释”在 MariaDB 上默认读取 performance_schema 聚合数据,反映的是历史平均行为。刚加的索引或刚改的查询,首次执行可能看不到真实优化效果,要多跑几次或手动用 EXPLAIN FORMAT=TREE 对比。











