使用自包含或关联子查询
以前,我们将自包含子查询看作是内部查询不依赖于外部查询,执行一次,并向外部查询返回结果。 T-SQL 还支持关联子查询,其中内部查询会引用外部查询中的列,理论上每行执行一次。
使用关联子查询
同自包含子查询一样,关联子查询也是嵌套在外部查询中的 SELECT 语句。 关联子查询也可以是标量子查询或多值子查询。 它们通常用于内部查询需要引用外部查询中值的情况。
但是,与自包含子查询不同的是,使用关联子查询时有一些特殊注意事项:
- 关联子查询不能与外部查询分开运行。 此限制会使测试和调试变得有些复杂。
- 与自包含子查询处理一次不同,关联子查询将运行多次。 逻辑上,外部查询先运行,对于返回的每一行,内部查询都会处理。
以下示例将使用关联子查询返回每个客户的最近订单。 子查询引用外部查询,并在 WHERE 子句中引用其“CustomerID”值。 对于外部查询中的每一行,子查询都会查找该行中所引用客户的最大订单 ID,而外部查询将检查其正在查看的行是否是包含该订单 ID 的行。
SELECT SalesOrderID, CustomerID, OrderDate
FROM SalesLT.SalesOrderHeader AS o1
WHERE SalesOrderID =
(SELECT MAX(SalesOrderID)
FROM SalesLT.SalesOrderHeader AS o2
WHERE o2.CustomerID = o1.CustomerID)
ORDER BY CustomerID, OrderDate;
写入关联子查询
要写入关联子查询,请考虑以下准则:
- 写入外部查询以接受来自内部查询的适当返回结果。 如果内部查询是标量查询,可以在 WHERE 子句中使用等于和比较运算符,例如 =、<、> 和 <>。 如果内部查询可能返回多个值,则使用 IN 谓词。 计划处理 NULL 结果。
- 标识关联子查询将会引用的外部查询中的列。 对作为外部查询中列源的表声明一个别名。
- 标识内部表中要与外部表中列进行比较的列。 为源表创建一个别名,方法与针对外部查询的处理方式相同。
- 写入内部查询,以根据外部查询的输入值从其源中检索值。 例如,在内部查询的 WHERE 子句中使用外部列。
当内部查询引用外部值进行比较时,内外查询之间将会发生关联。 正是这种关联,让该子查询名副其实。
使用 EXISTS
除从子查询中检索值之外,T-SQL 还提供了一种检查查询是否会返回任何结果的机制。 EXISTS 谓词可确定是否存在满足指定条件的任何行,但不会返回这些行,而是返回 TRUE 或 FALSE。 这种方法对于验证数据非常有用,不会产生检索和处理结果的开销。
当子查询与使用 EXISTS 谓词的外部查询关联时,SQL Server 将以特殊方式来处理子查询的结果。 EXISTS 只需检查结果中是否存在任何行,而不是从子查询中检索标量值或多值列表。
理论上,EXISTS 谓词相当于检索结果,对返回的行计数,再将计数与零进行比较。 比较以下查询,这些查询将返回已下单客户的详细信息:
第一个示例查询在子查询中使用 COUNT:
SELECT CustomerID, CompanyName, EmailAddress
FROM Sales.Customer AS c
WHERE
(SELECT COUNT(*)
FROM Sales.SalesOrderHeader AS o
WHERE o.CustomerID = c.CustomerID) > 0;
第二个查询使用 EXISTS 返回相同的结果:
SELECT CustomerID, CompanyName, EmailAddress
FROM Sales.Customer AS c
WHERE EXISTS
(SELECT *
FROM Sales.SalesOrderHeader AS o
WHERE o.CustomerID = c.CustomerID);
在第一个示例中,子查询必须对在“Sales.SalesOrderHeader”表中找到的每个“custid”匹配项计数,并将计数结果与零比较,这样才能指示该客户已下单。
在第二个查询中,只要在“Sales.SalesOrderHeader”表中找到相关订单,EXISTS 就会立即对该“custid”返回 TRUE。 不需要完整计算每个匹配项。 另请注意,使用 EXISTS 形式时,子查询并不局限于返回单列。 在此,我们使用 SELECT *。 返回的列无关紧要,因为我们只是检查有无返回任何行,而不是这些行中的值。
从逻辑处理角度来看,这两种查询形式效果相当。 从性能角度来看,数据库引擎对查询的处理可能有所不同,因为它会优化查询后执行处理。 请针对自己的使用情况测试每种查询。
注意
如果要将使用 COUNT(*) 的子查询转换为使用 EXISTS 的子查询,请确保子查询使用 SELECT *,而不是 SELECT COUNT(*)。 SELECT COUNT(*) 始终返回一行,因此 EXISTS 将始终返回 TRUE。
EXISTS 的另一种有用情形是使用 NOT 对子查询求反,如以下示例所示,结果将返回从未下过订单的所有客户:
SELECT CustomerID, CompanyName, EmailAddress
FROM SalesLT.Customer AS c
WHERE NOT EXISTS
(SELECT *
FROM SalesLT.SalesOrderHeader AS o
WHERE o.CustomerID = c.CustomerID);
SQL Server不必一定返回已下过订单客户的相关订单数据。 如果在“Sales.SalesOrderHeader”表中找到“custid”,则 NOT EXISTS 计算为 FALSE,并将很快完成计算。
要对使用 EXISTS 的查询写入子查询,请考虑以下准则:
- 关键字 EXISTS 直接位于 WHERE 后。 除非也使用 NOT,否则前面不加任何列名称(或其他表达式)。
- 在子查询中使用 SELECT *。 子查询不返回任何行,因此无需指定任何列。