UPDATE…JOIN是同步已有记录最常用且稳妥的方式,直接在数据库引擎内完成关联赋值,避免子查询报错;MySQL 8.0+用标准JOIN语法,5.7需用旧式多表语法,并务必加WHERE限定范围。

UPDATE … JOIN 一次性同步已有记录
跨表同步「已有数据」最常用、也最稳妥的方式是 UPDATE 配合 JOIN。它直接在数据库引擎内完成关联与赋值,不依赖子查询嵌套,执行计划清晰,大表性能稳定。
常见错误是写成子查询形式:UPDATE t1 SET col = (SELECT col FROM t2 WHERE t2.id = t1.id)——当 t2.id 不唯一或为空时,会报错 Subquery returns more than 1 row 或设为 NULL,且 MySQL 5.7+ 默认开启 sql_mode=STRICT_TRANS_TABLES,拒绝隐式失败。
- 必须显式指定连接条件,推荐用
INNER JOIN确保只更新有匹配的行 - MySQL 8.0+ 支持标准语法:
UPDATE t1 JOIN t2 ON t1.id = t2.id SET t1.name = t2.name - 老版本(如 5.7)要用旧式多表语法:
UPDATE t1, t2 SET t1.name = t2.name WHERE t1.id = t2.id - 务必加
WHERE限定范围,比如AND t1.updated_at 控制只同步变更过的记录
INSERT … SELECT ON DUPLICATE KEY UPDATE 处理新增+更新
当目标表主键/唯一键已存在时要更新、不存在时要插入,INSERT ... ON DUPLICATE KEY UPDATE 是 MySQL 原生最优解。它比先 SELECT 再判断再 INSERT/UPDATE 少一次网络往返,且原子执行,避免并发冲突。
典型坑是没建好唯一索引:该语句依赖 PRIMARY KEY 或 UNIQUE 约束触发“重复”逻辑,否则直接报错 Duplicate entry 或静默忽略更新。
- 源表字段和目标表字段类型必须兼容,比如
TINYINT赋值给INT没问题,但VARCHAR(10)插入超长值会截断或报错(取决于sql_mode) - 更新部分可用
VALUES(col_name)引用 INSERT 中的值,例如:ON DUPLICATE KEY UPDATE status = VALUES(status), updated_at = NOW() - 若需忽略重复行(只插不更),改用
INSERT IGNORE,但它不会返回影响行数,不利于监控
MERGE(SQL Server)或 UPSERT(PostgreSQL)替代方案
MySQL 没有标准 MERGE 语句,但 SQL Server 和 PostgreSQL 分别提供原生支持,语义更明确、可控性更强。
SQL Server 的 MERGE 必须配 WHEN MATCHED 和 WHEN NOT MATCHED 分支,少一个就语法报错;PostgreSQL 的 INSERT ... ON CONFLICT 则更灵活,支持按具体列冲突、指定 DO UPDATE 条件。
- SQL Server 示例:
MERGE target AS t USING source AS s ON t.id = s.id WHEN MATCHED THEN UPDATE SET t.name = s.name WHEN NOT MATCHED THEN INSERT (id, name) VALUES (s.id, s.name); - PostgreSQL 注意点:
ON CONFLICT (id)括号里必须是唯一约束名或列名,不能写表达式;DO UPDATE SET后可跟WHERE过滤,比如只更新status != 'archived'的行 - 跨库同步时,这些语句通常不支持跨库直接引用(除非配置了链接服务器或 foreign data wrapper),得先导入中间表或用应用层协调
增量同步容易被忽略的三个细节
真正上线跑批量同步时,光写对 SQL 不够,以下三点出错概率极高,但日志里往往不报明显错误:
-
UPDATE或INSERT没加WHERE条件,或条件字段没索引——几百万行全表扫描,锁表十几分钟,业务直接卡死 - 时间戳字段用
NOW()而不是CURRENT_TIMESTAMP,在复制从库上可能因时区或 binlog 格式导致值异常 - 没验证字符集和排序规则:源表
utf8mb4_unicode_ci,目标表utf8mb4_general_ci,中文排序或比较行为可能不一致,后续 where 查询结果对不上

















