T-SQL基础:不当子查询处理(二)
。
DELETE FROM Sales.OrdersWHERE custid IS NULL;
2.子查询列名称中的替换错误
代码中的逻辑bug有时非常难以捉摸。本节中,我会描述一个难以捉摸的bug,即子查询列名称中一个幼稚的替换错误。在解释bug之后,我会提供最佳做法,有助于避免未来出现这样的错误。
本节中的示例查询Sales架构中的一个称为MyShippers的表。运行下面的代码创建并填充该表。
IF OBJECT_ID('Sales.MyShippers', 'U') IS NOT NULL
DROP TABLESales.MyShippers;
CREATE TABLE Sales.MyShippers
(
shipper_id INT NOT NULL,
companynameNVARCHAR(40) NOT NULL,
phone NVARCHAR(24) NOT NULL,
CONSTRAINTPK_MyShippers PRIMARY KEY(shipper_id)
);
INSERT INTO Sales.MyShippers(shipper_id,companyname, phone)
VALUES(1,N'Shipper GVSUA', N'(503) 555-0137'),
(2,N'Shipper ETYNR', N'(425) 555-0136'),
(3,N'Shipper ZHISN', N'(415) 555-0138');
请考虑以下查询,应当返回发货订单给客户43的发货人。
SELECT shipper_id, companyname
FROM Sales.MyShippers
WHERE shipper_id IN
(SELECTshipper_id
FROMSales.Orders
WHEREcustid = 43);
此查询生成下列输出。.
shipper_id companyname
----------- ---------------
1 Shipper GVSUA
2 Shipper ETYNR
3 Shipper ZHISN
显然,只有发货人2和3发货给客户43,但由于某些原因,此查询从MyShippers表返回了所有发货人。仔细检查查询以及表涉及的架构,看看你能这是解释为什么吗。
原来,Orders表中的存储发货人ID的列名称不是shipper_id,而是shipperid(无下划线)。MyShippers表中的列叫做shipper_id,具有下划线。子查询上下文中的无前缀列名称可以从当前/内部范围之外解析。在我们的示例中,SQL Server首先在Orders表中查找shipper_id列,如果没有找到,SQL Server在查询的外部表MyShipper中查找。因为找到了shipper_id,所以查询中就使用该列。
你可以看到本应当是一个自包含子查询,无意中变成了一个相关子查询。只要订单表至少有一行,当将外部发货人与查询比较时,会对Orders表中的每行返回很多相同的外部发货人ID,这样MyShippers表中的所有行都会找到一个匹配行。
有些人可能认为,此行为是标准SQL的设计缺陷。不过,ANSISQL委员会中此行为的设计师不认为这是难以察觉的“错误”,相反,引用外部表中的列名而无需为其前缀表名,这是一种有意义的行为,只要这些列名称是明确的(即只要它们仅出现在一个表中)。
在跨表之间未使用一致的属性名称,这种问题是比较常见的。有时名称只是略有不同,如在这个问题中,一个表中是shipperid,另一个是shipper_id。这就足以表明这是一个bug。
要避免此类问题可以遵循两种最佳做法,一个需要从长远计议,另一个在短期内就可以实现。
从长远来看,你的组织应当作为一种策略,不要低估在跨表间使用一致属性名称的重要性。在短期内,你当然不想改变现有的列名称,因为这有可能破坏应用程序代码。
短期内,你可以采用一种非常简单的做法——以源表的别名作为子查询中列名称的前缀。这样,解析过程仅查找指定表中的列,如果没有这样的列,会收到一个解析错误。例如,尝试运行以下代码。
SELECT shipper_id,companyname
FROM Sales.MyShippers
WHERE shipper_id IN
(SELECT O.shipper_id
FROMSales.Orders AS O
WHERE O.custid = 43);
将出现下面的解析错误。
消息 207,级别 16,状态 1,第 4 行
列名 'shipper_id' 无效。
在得到此错误之后,你当然可以识别问题并修正查询。
SELECT shipper_id,companyname
FROM Sales.MyShippers
WHERE shipper_id IN
(SELECT O.shipperid
FROMSales.Orders AS O
WHERE O.custid = 43);
这一次,查询返回了预期的结果。
shipper_id companyname
----------- ---------------
2 Shipper ETYNR
3 Shipper ZHISN
完成后,请运行以下清理代码。
IFOBJECT_ID('Sales.MyShippers', 'U') IS NOT NULL
DROP TABLE Sales.MyShippers;