SQL中的CTE通用表表达式

  开发人员正在研发的许多项目都涉及编写由基本的 SELECT/FROM/WHERE 类型的语句派生而来的复杂 SQL 语句。其中一种情形是需要编写在 FROM 子句内使用派生表(也称为内联视图)的 Transact-SQL (T-SQL) 查询。这一常规使开发人员能获取一个行集,并立即将该行集加入到 SELECT 语句中的其他表、视图和用户定义函数中。另一种方案是使用视图而不是派生表。这两种方案都有其各自的优势和劣势。

当使用 SQL Server™ 2005 时,我更倾向于第三种方案,就是使用通用表表达式 (CTE)。CTE 能改善代码的可读性(以及可维护性),且不会有损其性能。此外,与早期版本的 SQL Server 相比,它们使得用 T-SQL 编写递归代码简单了许多。

  首先,我将介绍 CTE 的工作原理以及可用它们来应对的情况。接着我将讨论使用 CTE 相对于使用传统的 T-SQL 构造的优势,如派生表、视图和自定义过程。在本期专栏中,我将给出示例并解释它们的使用方法和适用情况。我还将演示 CTE 是如何处理递归逻辑并定义递归 CTE 的运行方式的。请注意,我在本专栏中讨论的所有代码都可从《MSDN® 杂志》网站下载获得,而且它们还使用 SQL Server 2005 附带的 Northwind 和 AdventureWorks 数据库。

视图、派生表和 CTE

  如果查询需要在一组数据中进行选取,而这些数据在数据库中并不是以表的形式存在,则 CTE 可能非常有用。例如,您可能想要编写一个针对一组聚合数据的查询,该聚合数据基于客户及其订单来计算值。这些聚合数据可能会将 Customers、Orders 和 Order Details 表联接在一起,以计算订单的总和以及平均值。此外,您可能想要查询聚合的行集。一个方法是创建一个视图,首先收集聚合数据,然后针对该视图编写一个查询。另一个方法是使用派生表针对聚合数据编写一个查询 通过将 SQL 语句移到 FROM 子句中并对其进行查询,可实现这一点。

视图通常用来分解大型的查询,以便用更易读的方式来查询它们。例如,一个视图可以表示一个 SELECT 语句,该语句会将 10 个表联接起来,选择许多列,然后根据涉及的一组逻辑来过滤行。接着,可以通过其他 SELECT 语句在整个数据库中查询该视图。此抽象使由该视图表征的行集更容易访问,而且无需在临时表中复制或存储数据。

  假定权限许可,这个视图还能在整个数据库中被重复使用。例如,在图 1 中,已经创建了一个视图,并为另一个 T-SQL 语句所使用。然而,当您想要收集数据并且只使用一次的时候,视图未必是最佳解决方案。由于视图是存在于数据库中、适用于所有批处理的数据库对象,那么创建仅用于单个 T-SQL 批处理的视图就有些多余。

  Figure 1 被查询的视图

 1
 2 CREATE VIEW vwMyView AS
 3 SELECT
 4     EmployeeID, COUNT(*) AS NumOrders,  MAX(OrderDate) AS MaxDate
 5 FROM Orders
 6 GROUP BY EmployeeID
 7 GO
 8
 9 SELECT
10     e.EmployeeID, oe.NumOrders, oe.MaxDate, e.ReportsTo AS ManagerID,
11     om.NumOrders, om.MaxDate
12 FROM
13     Employees AS e
14     INNER JOIN vwMyView AS oe ON e.EmployeeID = oe.EmployeeID
15     INNER JOIN vwMyView AS om ON e.ReportsTo = om.EmployeeID

另一种方法是创建派生表(也称为内联视图)。要创建派生表,在由括号包围的 FROM 子句中移动 SELECT 语句即可。接着就能像表或视图一样查询或者联接它。图 2 中的代码解决的查询与图 1 所解决的相同,但使用的是派生表而不是视图。尽管只能在派生表所在的语句中访问它们,但是,表通常使查询变得更难以阅读和维护。如果想要在同一个批处理中多次使用派生表,此问题会变得更加严重,因为随后必须复制和粘贴派生表才能重复使用它。

  Figure 2 使用派生表的查询

 1 SELECT
 2     e.EmployeeID, oe.NumOrders, oe.MaxDate,  e.ReportsTo AS ManagerID,
 3     om.NumOrders, om.MaxDate
 4 FROM
 5     Employees AS e
 6     INNER JOIN
 7         (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
 8          FROM Orders
 9          GROUP BY EmployeeID) AS oe(EmployeeID, NumOrders, MaxDate)
10         ON e.EmployeeID = oe.EmployeeID
11     LEFT JOIN
12         (SELECT EmployeeID, COUNT(*), MAX(OrderDate)
13          FROM Orders
14          GROUP BY EmployeeID) AS om(EmployeeID, NumOrders, MaxDate)
15         ON e.ReportsTo = om.EmployeeID

  

  CTE 非常适用于此类情形,因为它提升了 T-SQL 的可读性(就像视图一样),而且能在同一个批处理后紧跟的查询中多次使用。当然,超出该范围它就不适用了。另外,CTE 是语言级别的构造,也就是说 SQL Server 不会在内部创建临时表或虚拟表。每次在紧随其后的查询中引用 CTE 的底层查询时都会调用它。

因此,同样的情形也能用 CTE 来编写,如图 3 所示。EmpOrdersCTE 收集聚合数据,然后在紧随 CTE 之后的查询中使用该数据。使用 CTE 之后,图 3 中的代码令查询变得非常易读(就像视图一样),而且并没有创建系统对象来存储元数据。

  Figure 3 使用 CTE 查询

 1 ;WITH EmpOrdersCTE (EmployeeID, NumOrders, MaxDate) AS
 2 (
 3   SELECT EmployeeID, COUNT(*), MAX(OrderDate)
 4   FROM Orders
 5   GROUP BY EmployeeID
 6 )
 7
 8 SELECT
 9     e.EmployeeID,  oe.NumOrders, oe.MaxDate,
10     e.ReportsTo AS ManagerID,  om.NumOrders, om.MaxDate
11 FROM
12     Employees AS e
13     INNER JOIN EmpOrdersCTE oe ON e.EmployeeID = oe.EmployeeID
14     LEFT JOIN EmpOrdersCTE om ON e.ReportsTo = om.EmployeeID

CTE 的结构

  现在我将用一个简单的 CTE 为例来演示如何构造 CTE。CTE 以 WITH 关键字开始。然而,如果 CTE 不是批处理中的第一个语句,则必须在 WITH 关键字前添加一个分号。作为最佳做法,我倾向于在所有的 CTE 之前都加上一个分号作为前缀,我发现这种一致的方式比起必须牢记是否需要添加分号来,要容易得多。

WITH 关键字后面是 CTE 的名称,接着是一个列别名的可选列表。列别名对应于 CTE 内的 SELECT 语句返回的列。可选列别名的后面是 AS 关键字,这是必需的。AS 关键字后面是用括号括起来、定义 CTE 的查询表达式。

请看这个示例:

1 ;WITH myCTE (CustID, Co) AS
2 (
3   SELECT CustomerID, CompanyName FROM Customers
4 )
5 SELECT CustID, Co FROM myCTE

CustomerID 和 CompanyName 列的别名为 CustID 和 Co。接着跟随 CTE 其后的是通过列别名引用 CTE 的 SELECT 语句。

理解 CTE

  在设计 CTE 之前,必须理解它的工作原理和遵循的规则。本章节描述了 CTE 的适用情况,以及在 CTE 内什么是可以使用的,什么是不可以使用的。对于初学者来说,可以在 T-SQL 批处理、用户自定义函数、存储过程、触发器或视图中创建并使用 CTE。

CTE 仅能被紧随其后的语句所引用。这意味着如果要使用 CTE,则必须紧随 T-SQL 批处理中的 CTE 之后编写引用 CTE 的查询。例如,以下批处理会产生错误:

1 ;WITH myCTE (CustID, Co) AS
2 (
3   SELECT CustomerID, CompanyName FROM Customers
4 )
5 SELECT CompanyName FROM Customers WHERE CustomerID = ‘ALFKI‘
6 SELECT CustID, Co FROM myCTE

在本段代码中,myCTE 仅供紧随其后的第一个查询使用。当第二个查询引用 myCTE 时,CTE 不在范围之内,并且引发异常(对象名“myCTE”无效)。

另请注意,因为 CTE 预期由另一个可能随之肯定要重新处理数据的查询引用,所以 CTE 的查询不能含有 ORDER 和 COMPUTE 之类的语句。然而,诸如 FOR XML 的复杂语句仍可用来定义和运行 CTE。例如,您可以使用 FOR XML 子句来查询 CTE 并返回其结果,如下所示。

1 ;WITH myCTE AS
2 (
3   SELECT c.CustomerID, c.CompanyName, o.OrderID, o.OrderDate
4   FROM Customers c
5   INNER JOIN Orders o ON c.CustomerID = o.CustomerID
6 )
7
8 SELECT CustomerID, CompanyName, OrderID, OrderDate
9 FROM myCTE FOR XML AUTO

一旦定义了 CTE,跟随其后的首个查询便能多次引用它。这一功能在某个查询需要多次引用 CTE 时尤为有用。图 3 中的代码示例演示了查询如何引用 EmpOrdersCTE 两次,以便能获取员工和主管的信息。当需要多次引用同一行集时,这非常有用;引用 CTE 两次比复制该查询要简单得多。

  CTE 并不一定由 SELECT 语句使用;任何引用 CTE 所生成行集的语句都可使用它。这意味着 CTE 后面可以跟随使用 CTE 的 SELECT、INSERT、UPDATE 或 DELETE 语句。您也可以在使用 CTE 的查询中使用只进和快照光标。

此外,CTE 后面也可以跟随另一个 CTE。在想要把中间结果聚集到行集时,可使用这种技术从其他 CTE 构建 CTE。当创建从其他 CTE 构建的 CTE 时,请用逗号分隔 CTE 的定义。

图 4 所示的示例定义了 EmpOrdersCTE,它收集了一个员工列表和每个员工的订单总数。第二个 CTE 的名称是 MinMaxOrdersCTE,它查询第一个 EmpOrdersCTE 并在行集上执行聚合函数来确定员工订单的平均数、最小数和最大数。

  Figure 4 引用另一个 CTE 的 CTE

;WITH
EmpOrdersCTE (EmployeeID, NumOrders)
AS
(
  SELECT EmployeeID, COUNT(*)
  FROM Orders
  GROUP BY EmployeeID
),
MinMaxOrdersCTE (Mn, Mx, Diff)
AS
(
  SELECT MIN(NumOrders), MAX(NumOrders), AVG(NumOrders)
  FROM EmpOrdersCTE
)
SELECT Mn, Mx,    Diff
FROM MinMaxOrdersCTE

通过用逗号来分隔,在 WITH 关键字后面可以定义多个 CTE。每个 CTE 都可以被紧随其后的 CTE 所引用,形成层接的构建关系。CTE 定义后面的数据操作语言 (DML) 语句也可引用 WITH 子句中定义的任何 CTE。

递归规则

  CTE 还可用于实现递归算法。在需要编写调用其本身的算法时,递归逻辑很有用——这通常用来遍历一组嵌套的数据。编写递归逻辑可能很复杂,特别是使用 T-SQL 之类的语言的时候。然而,这正是 CTE 旨在解决的特别问题之一。创建递归 CTE 的基本公式如下所示:

  1. 创建一个返回顶层(这是定位点成员)的查询。
  2. 编写一个递归查询(这是递归成员)。
  3. 通过 UNION 将第一个查询与递归查询结合起来。
  4. 确保存在没有行会被返回的情况(这是终止检查)。

您的递归 CTE 如下所示:

1 ;WITH myRecursiveCTE(col1, col2, ... coln) AS
2 (
3   -- 定位点成员 查询
4   UNION ALL
5   -- 递归成员 查询来自CTE的自身数据
6 )

  当编写不涉及 CTE 的自定义递归过程时,必须包含一个显式终止子句。这个终止子句负责确保递归算法最后将终止,并弹出了递归调用堆栈。若无此子句,您的代码最终将无限循环下去。

  CTE 可从两个方面帮助处理终止子句。首先是一个隐式终止子句,当递归成员返回零记录时出现。此时,递归成员查询不会递归调用 CTE,取而代之的弹出了递归调用堆栈。其次是能显式设置 MAXRECURSION 层。

MAXRECURSION 层可以在含有 CTE 的批处理中或通过服务器端设置(服务器范围的设置默认值为 100,除非您更改它)显式设置。这个设置限制了 CTE 可递归调用其本身的次数。如果达到限制次数,则将引发异常。设置 MAXRECURSION 层的语法是在 SELECT 语句中的 CTE 后面使用 OPTION 子句,如下所示:

-- DEFINE YOUR CTE HERE
SELECT * FROM EmpCTE
OPTION (MAXRECURSION 7)

在设计递归 CTE 时,还有一些其他规则也得铭记在心。递归 CTE 必须包含定位点成员和递归成员。这两种成员必须拥有相同数量的列,而且同属于这两种成员的列必须具有匹配的数据类型。递归成员只能引用 CTE 一次,并且成员不能使用下列子句或关键字:

  • SELECT DISTINCT
  • GROUP BY
  • HAVING
  • TOP
  • LEFT/RIGHT OUTER JOIN

递归偏移

  在数据和行集方面,递归用于解决您需要在不同情况下针对同一组数据重复执行同一逻辑时所遇到的问题。例如,假设您需要找出所有销售人员以及他们的上级,然后以分层顺序返回数据。图 5 演示了一个使用 CTE 的解决方案,该方案通过递归来收集销售副总裁下属的员工的列表。

  Figure 5 递归收集销售人员

 1 ;WITH EmpCTE(EmployeeID, EmployeeFirstName,
 2  EmployeeLastName, MgrID, SalesLevel)
 3 AS
 4 (
 5   -- 定位点成员
 6   SELECT EmployeeID, FirstName, LastName, ReportsTo, 0
 7   FROM Employees
 8   WHERE EmployeeID = 2 -- Start with the VP of Sales
 9
10   UNION ALL
11
12   -- 递归成员
13   SELECT
14     e.EmployeeID, e.FirstName, e.LastName, e.ReportsTo, m.SalesLevel+1
15   FROM
16     Employees AS e
17     INNER JOIN EmpCTE m ON e.ReportsTo = m.EmployeeID
18 )
19
20  -- 使用CTE
21 SELECT EmployeeID, EmployeeFirstName, EmployeeLastName,
22        MgrID, SalesLevel
23 FROM EmpCTE

除了少数其他方面,图 5 中所示的递归 CTE 看起来很像标准 CTE。其中标准 CTE 包含一个定义行集的查询,递归 CTE 定义两个查询定义。第一个查询定义(即定位点成员)定义了将在调用 CTE 时执行的查询。第二个查询定义,即递归成员,定义了一个返回与定位点成员相同的列和数据类型的查询。递归成员还检索接下来将被用于递归回调到 CTE 的值。查询的结果通过 UNION 语句结合在一起。

图 5 中的 EmpCTE 显示了收集销售副总裁的员工记录的定位点成员 (EmployeeID = 2)。定位点成员查询的最后一列返回 0 值,这表示分层顺序的第 0 层,也就是最顶层。递归成员查询收集向其各自上级汇报的员工的列表。这通过联接 Employees 表和 EmpCTE 来实现。

从递归成员中可以检索相同的列,但是 SalesLevel 列的计算方式是:取当前员工的主管,收集主管的 SalesLevel,然后在其基础上增加 1。表达式 m.SalesLevel+1 为所有直接向销售副总裁汇报的员工(从定位点成员中检索到)的 SalesLevel 赋值 1。然后,所有向那些员工汇报的员工的 SalesLevel 值变为 2。对随后销售机构层次结构中的每一后续层次,其 SalesLevel 都以此方式递增。

补充: 目前仅有Sql Server 2005、2008、2012、Oracle11g、IBM DB2和PostegreSQL8.4支持CTE;

MySQL、SQLite和Infomix暂时不支持。

结束语

  比起那些在查询中使用复杂的派生表或引用那些在 T-SQL 批处理外部定义的视图的方案,CTE 使得编写 T-SQL 更具可读性。此外,CTE 还为解决使用递归算法的过程中遇到的难题提供了一个更先进的工具。不管您使用的是非递归 CTE 还是递归 CTE,您都会发现 CTE 可以帮您应对许多常见开发情况,并且可在不损害性能的情况下提升可读性。

时间: 2024-10-12 12:02:59

SQL中的CTE通用表表达式的相关文章

Sql Server系列:通用表表达式CTE

1 CTE语法WITH关键字 通用表表达式(Common Table Express, CTE),将派生表定义在查询的最前面.要使用CTE开始创建一个查询,可以使用WITH关键字. CTE语法: WITH <expression_name> [(column_name [,...n])] AS ( CTE_query_definition) [, <another_expression>] <query> 首先为CTE提供一个名称,该名称类似于用于派生表的别名.然后可以

SQL中查看数据库各表的大小

SQL中查看数据库各表的大小 编写人:CC阿爸 2014-6-17 在日常SQL数据库的操作中,如何快速的查询数据库中各表中数据的大小. 以下有两种方法供参考: 第一种: create table #t(name varchar(255), rows bigint, reserved varchar(20), data varchar(20), index_size varchar(20), unused varchar(20)) exec sp_MSforeachtable "insert i

如何在SQL中产生交叉式数据表(枢纽分析表)Part 2(PIVOT,UNPIVOT)

之前小喵使用SQL2000的时候,为了产生交叉数据表(Excel中称为枢纽分析表),小喵用了以下这篇的方式来处理 http://www.dotblogs.com.tw/topcat/archive/2008/04/14/2909.aspx 这样的方式在SQL 2005里面一样可以用,不过2005又新增了一个更方便的东西,可以更简洁的来处理.这个东西就是[PIVOT],神奇的是,除了可以透过PIVOT产生交叉数据表,还可以透过UNPIVOT将交叉数据表转回原来的方式(这对于有些数据表的正规化很有帮

Sql — CTE公用表表达式和With用法总结

CTE(Common Table Expression) 公用表表达式,它是在单个语句的执行范围内定义的临时结果集,只在查询期间有效.它可以自引用,也可在同一查询中多次引用,实现了代码段的重复利用. CTE最大的好处是提升T-Sql代码的可读性,可以更加优雅简洁的方式实现递归等复杂的查询. CTE可用于: ⒈ 创建递归查询,这个应该是CTE最好用的地方 ⒉ 在同一语句中多次引用生成的表 3. 减少子查询和表变量,提高执行效率 CTE优点: 1. 使用 CTE 可以获得提高可读性和轻松维护复杂查询

【转】CTE(公用表表达式)

本文转自:爽朗的微笑  http://www.cnblogs.com/shuangnet/archive/2013/03/22/2975929.html 公用表表达式 (CTE) 具有一个重要的优点,那就是能够引用其自身,从而创建递归 CTE.递归 CTE 是一个重复执行初始 CTE 以返回数据子集直到获取完整结果集的公用表表达式. 当某个查询引用递归 CTE 时,它即被称为递归查询.递归查询通常用于返回分层数据,例如:显示某个组织图中的雇员或物料清单方案(其中父级产品有一个或多个组件,而那些组

ORA-03113 SQL中select语句全表扫描带来的异常

今天在ERP系统的维护过程中,业务人员反馈了一个问题过来,是ERP系统生产单模块的预览打印报表出错,看到后我逐步做了以下的排查: 1.尝试其他单据是否存在相同问题 2.直接打开水晶报表,将参数代入看看是否是报表问题 排查之后逐渐发现,问题出在数据源身上,找到返回数据集的存储过程,进入测试窗口检查是否运行正常,结果发现运行即进入卡死状态,进程无法中断,只好强行退出PL/SQL,这时候我估计到问题出在SQL语句上,因此将SQL语句复制到新的窗口,代入参数,如下: SELECT WO_NBR,WO_L

SQL中使用关键词创建表或字段

有时候我们给表或者字段命名时,会无意中选择了一个SQL中的关键字进行命名,然后就报错了: ERROR:  syntax error at or near "limit" MySQL解决方法: 在MySQL中需要添加 `` create table `order` (id int, `limit` int); # 由于order和limit都是MySQL中的关键字,必须加上``才能使用 PostgreSQL解决方法: 在PostgreSQL中需要添加 "" creat

通用表表达式(Common Table Expression)

问题:编写由基本的 SELECT/FROM/WHERE 类型的语句派生而来的复杂 SQL 语句. 方案1:编写在From子句内使用派生表(内联视图)的T-SQL查询语句. 方案2:使用视图 方案3:使用通用表达式(CTE) 比较: 视图:通常用于分解大型查询,无需在临时表中复制或存储数据,数据库中可以重复使用. 1 CREATE VIEW vwMyView AS 2 SELECT 3 EmployeeID, COUNT(*) AS NumOrders, MAX(OrderDate) AS Max

SQL 中不同类型的表连接

http://www.linuxidc.com/Linux/2012-08/68035.htm 1.简介 在关系型数据库中,join操作是将不同的表中的数据联合在一起时非常通用的一种做法.首先让我们看看join是如何操作的,然后我们探索一下当join和where语句同时存在的时候的执行顺序问题,最后来谈一谈不同类型的join的顺序问题. 2.建立初始的测试表结构(建表语句到这里下载) 表建立完之后,将会看到如下三个表. 我们将通过以上三个表来演示join操作.这三个表都是用来做演示的,所以我并没