更新查询在FROM子句中使用了错误的表
我在生产环境中误执行了一条错误且不完整的查询,如下所示。
#TempDiff 是一个包含两列的临时表,如下所示。对于每个产品,存在一个或多个 [Pallet Contents]。每个SKU在 #TempDiff 中都是唯一的,在 products 表中存在,并且恰好存在一条 Pallet Content 满足 P2CLocation = 1。还有一些其他产品不在 #TempDiff 范围内。
我的问题:有没有办法看到这条查询更新了哪些行?
#TempDiff 里只有69行,但实际更新了60,000行……
根据我的观察,所有非空的 [pallet contents] 在执行完这条查询后似乎都被减去了3。3确实是 #tempDiff 中的一个 diff 值,但它是第二条(插入的)行,而不是第一条。
这台服务器的版本是SQL Server 2017,兼容性级别为100。
我尝试创建一个最小示例,请看这个 db fiddle,但它的行为有所不同(尽管结果是一致的)。也许结果是不可确定的?
以下是原始查询:
CREATE TABLE #TempDiff
(
SKU VARCHAR(50),
Diff INT
);
/* Here is the logic that add rows into #temp diff */
with productPickLocation as
(
select [Pallet Contents].*
from [Pallet Contents]
left join pallets on [Pallet Contents].[Pallet ID] = pallets.[Pallet ID]
where P2CPullLocation = 1 and pallets.WarehouseID = 3
)
update [Pallet Contents]
set [Pallet Contents].Qty = [Pallet Contents].Qty - diff
from #TempDiff
inner join products on #TempDiff.SKU = products.ItemNumber
left join productPickLocation on products.[PRODUCT KEY] = productPickLocation.[Product key]
解决方案
问题基本在于 update [Pallet Contents] 指向对 [Pallet Contents] 表的新引用。它看不到 productPickLocation 中的引用,因为它处于不同的作用域。
所以你先有:
with productPickLocation as (
select [Pallet Contents].*
from [Pallet Contents]
left join pallets on [Pallet Contents].[Pallet ID] = pallets.[Pallet ID]
where P2CPullLocation = 1 and pallets.WarehouseID = 3
)
这实际上因为 where 子句而构成一个内连接,而不是左连接。
你随后在主段中执行
from #TempDiff
inner join products on #TempDiff.SKU = products.ItemNumber
left join productPickLocation on products.[PRODUCT KEY] = productPickLocation.[Product key]
这又带有左连接,导致选出所有 products,并忽略了 productPickLocation,因为它在逻辑上未被使用。
接着你得到
update [Pallet Contents]
它在自身的作用域中查找引用,找不到,因此再次对整张表进行笛卡尔积连接。这个隐式笛卡尔积没有其他连接条件,因此所有行都被更新。
更有意思的是:这看起来像是在试图让每一行被多次更新。其实不会发生。SQL Server检测到 update 是非确定性的,于是只选择一个值来更新。该行在一条语句中最多只会被更新一次。
并且因为 set 引用了一个可为空的列
set [Pallet Contents].Qty = [Pallet Contents].Qty - diff
null - value 的结果是 null,因此它保持不变,只有其他行受到影响。
你的查询本来应该长成这样:
with productPickLocation as (
select pc.*
from [Pallet Contents] pc
inner join pallets pt on pc.[Pallet ID] = pt.[Pallet ID]
where pc.P2CPullLocation = 1
and pt.WarehouseID = 3
)
update ppl -- NOTE THIS LINE
set Qty -= t.diff
from productPickLocation ppl
inner join products p on p.[PRODUCT KEY] = ppl.[Product key]
inner join #TempDiff t on t.SKU = p.ItemNumber;
或者更简单地:
update pc -- NOTE THIS LINE
set Qty -= t.diff
from [Pallet Contents] pc
inner join products p on p.[PRODUCT KEY] = pc.[Product key]
inner join pallets pt on pc.[Pallet ID] = pt.[Pallet ID]
inner join #TempDiff t on t.SKU = p.ItemNumber
where pc.P2CPullLocation = 1
and pt.WarehouseID = 3;
如果临时表实际上并不唯一,那么你需要先对它进行聚合
with AggDiff as (
select
t.SKU,
TotalDiff = SUM(t.diff)
from #TempDiff t
group by
t.SKU
)
update pc -- NOTE THIS LINE
set Qty -= t.TotalDiff
from [Pallet Contents] pc
inner join products p on p.[PRODUCT KEY] = pc.[Product key]
inner join pallets pt on pc.[Pallet ID] = pt.[Pallet ID]
inner join AggDiff t on t.SKU = products.ItemNumber
where pc.P2CPullLocation = 1
and pt.WarehouseID = 3;
TL;DR;
在编写带连接的更新时,
- 始终为每个表使用唯一的别名,第一行的
update指的是别名而不是表名。 - 除非你确定逻辑正确,否则不要使用左连接。
- 为了清晰起见,将你要更新的表放在
from行中,然后确保每个连接返回一个唯一的行(要么通过连接条件,要么通过子聚合)。