原创

MySQL UPDATE JOIN 多重匹配:先确定唯一来源再写入

盘点表里同一个 SKU 有两次结果,UPDATE JOIN 没报错,也只改了一条库存记录。这不代表数据库替你选了“最新一次盘点”。一条目标记录匹配多个来源时,必须由 SQL 或数据约束明确选择规则,不能把某次执行结果当成稳定顺序。

MySQL 8.4 的多表 UPDATE 对同一匹配目标行只更新一次,即使它匹配多次。这个行为解决的不是“多份来源该用哪一份”的业务判断。若来源值不同,依赖偶然访问顺序就是错误的同步协议。

先把来源粒度摆出来

CREATE DATABASE update_source_lab;
USE update_source_lab;
CREATE TABLE inventory (
    id BIGINT NOT NULL PRIMARY KEY,
    sku VARCHAR(30) NOT NULL,
    quantity INT NOT NULL,
    UNIQUE KEY uk_inventory_sku (sku)
) ENGINE=InnoDB;
CREATE TABLE recount_result (
    id BIGINT NOT NULL PRIMARY KEY,
    batch_id BIGINT NOT NULL,
    sku VARCHAR(30) NOT NULL,
    counted_quantity INT NOT NULL,
    observed_at DATETIME(6) NOT NULL,
    KEY idx_batch_sku_time (batch_id, sku, observed_at, id)
) ENGINE=InnoDB;

INSERT INTO inventory VALUES (1, 'A', 100), (2, 'B', 50);
INSERT INTO recount_result VALUES
    (11, 101, 'A', 20, '2026-10-01 09:00:00'),
    (12, 101, 'A', 12, '2026-10-02 09:00:00'),
    (21, 101, 'B', 40, '2026-10-02 10:00:00');

目标库存按 SKU 唯一,来源却按盘点记录唯一,两张表的粒度不同。先查询实际连接结果,A 会出现两行:

SELECT i.id, i.sku, r.id AS source_id,
       r.counted_quantity, r.observed_at
FROM inventory AS i
JOIN recount_result AS r ON r.sku = i.sku
WHERE r.batch_id = 101
ORDER BY i.id, r.observed_at, r.id;

直接把这个连接换成 UPDATE,SQL 文本里没有任何“按时间取最新”的含义。即使来源索引刚好按时间排列,优化器也没有义务把它当成业务裁决。加一个没有明确来源选择作用的 ORDER BY,也不是多表更新的通用补救办法。

先选出每个 SKU 唯一的一份来源,再更新

本例规则是同一批次里时间最新,时间相同则取来源 id 最大的一条。窗口排序把规则写在产生来源的地方:

SELECT sku, counted_quantity, id AS source_id
FROM (
    SELECT id, sku, counted_quantity,
           ROW_NUMBER() OVER (
               PARTITION BY sku
               ORDER BY observed_at DESC, id DESC
           ) AS rn
    FROM recount_result
    WHERE batch_id = 101
) AS ranked
WHERE rn = 1
ORDER BY sku;

确认这份结果确实每个 SKU 只有一行,再用于更新:

UPDATE inventory AS i
JOIN (
    SELECT sku, counted_quantity,
           ROW_NUMBER() OVER (
               PARTITION BY sku
               ORDER BY observed_at DESC, id DESC
           ) AS rn
    FROM recount_result
    WHERE batch_id = 101
) AS r ON r.sku = i.sku AND r.rn = 1
SET i.quantity = r.counted_quantity;

样本里 A 应变成 12,B 变成 40。来源中的旧值 20 不会因为数值更大就胜出,因此 MAX(counted_quantity) 也不是相同规则的短写法。

如果盘点时间允许为空,或者同一时间需要按设备优先级裁决,应把这份规则继续展开。当前表把 observed_at 设为非空,并用 id 打破并列,只保证示例的确定顺序,不替代真实业务对冲突来源的判断。

来源是增量时,应该聚合,而不是取最后一条

盘点结果代表某个时刻的绝对库存,所以选一条;出入库流水若表示数量增减,则可能需要汇总。两种数据用同一条 UPDATE JOIN,会产生完全不同的结果。

不能看到来源有重复就统一 DISTINCT。来源两条记录的值不同,DISTINCT 不会变成一条;即使字段恰好相同,也可能代表两次独立的增量事件。去重必须依据事件身份,聚合必须依据量的含义。

更不能关闭严格分组模式,让 GROUP BY sku 随意取一个未聚合的数量。那只是把不确定性从 UPDATE JOIN 移到子查询里,来源粒度仍没有被定义。

验证更新结果,别只看受影响行数

受影响行数描述的是直接更新情况,不是来源记录总数。库存原本已经等于盘点结果时,匹配行与实际改变行还可能不同,驱动的 matched/changed 口径也需要核对。

批次导入最好明确何时封存。先 SELECT 预览,过一会儿来源又增加一条更晚结果,再 UPDATE,预览与实际使用的来源就不一定相同。要么选择不可变的已封存批次,要么保存经审核的具体来源集合,而不是只在日志里记录一个总行数。

目标库存若同时接受在线扣减,也不能直接用旧盘点值覆盖当前库存。这个实验只解释如何消除来源多义性;盘点生效时点、在线变动衔接和差额调整,需要单独的业务写入边界。

最后核对实际值:

SELECT id, sku, quantity FROM inventory ORDER BY id;
正文到此结束
评论插件初始化中...
Loading...