通用表表达式和递归查询
通用表表达式 (CTE) 允许您编写只在查询期间持续存储的命名表表达式。它们的简单形式提供了视图和派生表的混合功能。与视图类似,CTE 可以在外部查询中被引用多次,而与派生表类似,它只在查询期间持续存储。采用更复杂的形式,您可以编写递归的 CTE,从而更加容易和高效地操作树和图。
定义一个 CTE 时,要使用一个 WITH 子句后面紧跟 CTE 的名称,并可选地在括号中提供一个结果列别名的列表。后面是 AS 子句和包含 CTE 查询表达式的括号。最后是提供一个引用 CTE 结果的外部查询。在 CTE 的查询表达式内,您可以按自己的意愿引用变量。
sqlserver/art/TSQLSyntaxBoost06.htm" target="_blank">图 6 中的代码给出了一个简单示例,编写一个非递归的 CTE 返回每年客户的销售定单值。显然,不使用 CTE 您也可以获得同样的结果。但是设想一下:如果您还希望每一行都返回前一年的总值以及与本年的差值,那又会怎么样呢。如果您选择使用派生表,就必须在一个派生表中指定本年的查询,而在另一个中指定前一年的查询,并用外部查询联接二者。凭借 CTE,您可以编写一个查询返回每年的总值,并用外部查询引用它两次(参见sqlserver/art/TSQLSyntaxBoost07.htm" target="_blank">图 7)。
但是 CTE 的真正强大之处是它们的递归形式。在 CTE 的括号内,您可以定义独立的或者向回引用 CTE 的查询。独立的查询(那些不引用 CTE 名称的查询)称为固定成员,只能调用一次。向回引用 CTE 名称的查询称为递归成员,可以重复调用,直到查询不再返回行。固定成员可以使用 UNION 或者 UNION ALL 运算符互相追加,具体取决于是否愿意消除重复项。而递归成员必须使用 UNION ALL 运算符追加。
举一个说明递归 CTE 用途的示例场景,考虑 AdventureWorks 数据库中的 BillOfMaterials 表。这个表代表一个典型的材料帐单,其中产品的组装形成了一个非循环的有向图。每个产品都是用其他产品组装的,而其他产品又是用另一些产品组装的,因此没有循环关系。这种组装产品包含的产品关系用 AssemblyID 和 ComponentID 列表示。PerAssemblyQty 包含 AssemblyID 所表示的每个产品的组件产品(用 ComponentID 表示)的数量。已经过时的关系在 ObsoleteDate 列中指定了一个日期。如果您只对非过时数据感兴趣,应该测试这个列是否为 NULL。表中还有其他有用的信息,包括度量单位,但是就我们要说明的意图而言,所有其他列都可以忽略。
sqlserver/art/TSQLSyntaxBoost08.htm" target="_blank">图 8 中的代码生成了 ProductID 210 的分解图数据。sqlserver/art/TSQLSyntaxBoost09.htm" target="_blank">图 9 给出了这种视图的一部分;描述了产品之间的包含关系。在 CTE 的主体内,第一个查询没有引用 CTE 的名称,因此它是一个固定成员,并且只能调用一次。请注意查询将查找组件 ID 为 210 而组装 ID 为 NULL 的行,这意味着它是一个顶层产品。查询确保此关系没有过时,并返回组件 ID 和数量。递归成员返回组装(通过在 CTE 的名称和 BillOfMaterials 表之间联接从前面的步骤返回)内包含的产品。第一次调用递归成员的时候,以前的步骤是固定成员返回的结果。第二次调用的时候,以前的步骤是第一次调用递归成员返回的结果,以此类推,直到递归成员返回一个空的集合。
递归成员通过用前一步骤的数量乘上组件的数量计算组件的累积数量。外部查询引用 CTE 的名称,获得对固定成员和递归成员所有调用的统一结果。外部查询将 CTE 与 Products 表联接,以获得产品名称,生成sqlserver/art/TSQLSyntaxBoost10.htm" target="_blank">图 10 中的 90 行(有删节)。每个组件在输出中都可多次出现,例如产品 835,因为它可以参与不同的组装。可以修改外部查询按产品的 ID 和名称将结果分组,获得每个产品的总数量。代码如sqlserver/art/TSQLSyntaxBoost08.htm" target="_blank">图 8 所示,而外部查询如下所示:
SELECT B.ProductID, P.Name,SUM(B.Qty) AS TotalQtyFROM BOMCTE AS BJOIN Product AS PON P.ProductID = B.ProductIDGROUP BY B.ProductID, P.NameORDER BY B.ProductID;
如果您怀疑其中存在循环,想要限制递归调用的数量,可以在外部查询之后马上指定 MAXRECURSION 选项:
WITH...outer_queryOPTION(MAXRECURSION 30)
此选项将在 CTE 超过指定限制的时候,使 SQL Server 引发一个错误。如果没有指定这个选项,SQL Server 中的默认值是 100。如果不想有限制的话,必须指定 0。请注意您可以编写自定义代码检测循环关系,但是这超出了本文的范围。