开发人员正在研发的许多项目都涉及编写由基本的 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 被查询的视图
 CREATE VIEW vwMyView AS
SELECT
EmployeeID, COUNT(*) AS NumOrders, MAX(OrderDate) AS MaxDate
FROM Orders
GROUP BY EmployeeID
GO SELECT
e.EmployeeID, oe.NumOrders, oe.MaxDate, e.ReportsTo AS ManagerID,
om.NumOrders, om.MaxDate
FROM
Employees AS e
INNER JOIN vwMyView AS oe ON e.EmployeeID = oe.EmployeeID
INNER JOIN vwMyView AS om ON e.ReportsTo = om.EmployeeID
另一种方法是创建派生表(也称为内联视图)。要创建派生表,在由括号包围的 FROM 子句中移动 SELECT 语句即可。接着就能像表或视图一样查询或者联接它。图 2 中的代码解决的查询与图 1 所解决的相同,但使用的是派生表而不是视图。尽管只能在派生表所在的语句中访问它们,但是,表通常使查询变得更难以阅读和维护。如果想要在同一个批处理中多次使用派生表,此问题会变得更加严重,因为随后必须复制和粘贴派生表才能重复使用它。
  Figure 2 使用派生表的查询 
 SELECT
e.EmployeeID, oe.NumOrders, oe.MaxDate, e.ReportsTo AS ManagerID,
om.NumOrders, om.MaxDate
FROM
Employees AS e
INNER JOIN
(SELECT EmployeeID, COUNT(*), MAX(OrderDate)
FROM Orders
GROUP BY EmployeeID) AS oe(EmployeeID, NumOrders, MaxDate)
ON e.EmployeeID = oe.EmployeeID
LEFT JOIN
(SELECT EmployeeID, COUNT(*), MAX(OrderDate)
FROM Orders
GROUP BY EmployeeID) AS om(EmployeeID, NumOrders, MaxDate)
ON e.ReportsTo = om.EmployeeID

  

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

因此,同样的情形也能用 CTE 来编写,如图 3 所示。EmpOrdersCTE 收集聚合数据,然后在紧随 CTE 之后的查询中使用该数据。使用 CTE 之后,图 3 中的代码令查询变得非常易读(就像视图一样),而且并没有创建系统对象来存储元数据。
  Figure 3 使用 CTE 查询
 ;WITH EmpOrdersCTE (EmployeeID, NumOrders, MaxDate) AS
(
SELECT EmployeeID, COUNT(*), MAX(OrderDate)
FROM Orders
GROUP BY EmployeeID
) SELECT
e.EmployeeID, oe.NumOrders, oe.MaxDate,
e.ReportsTo AS ManagerID, om.NumOrders, om.MaxDate
FROM
Employees AS e
INNER JOIN EmpOrdersCTE oe ON e.EmployeeID = oe.EmployeeID
LEFT JOIN EmpOrdersCTE om ON e.ReportsTo = om.EmployeeID
 
CTE 的结构
  现在我将用一个简单的 CTE 为例来演示如何构造 CTE。CTE 以 WITH 关键字开始。然而,如果 CTE 不是批处理中的第一个语句,则必须在 WITH 关键字前添加一个分号。作为最佳做法,我倾向于在所有的 CTE 之前都加上一个分号作为前缀,我发现这种一致的方式比起必须牢记是否需要添加分号来,要容易得多。
WITH 关键字后面是 CTE 的名称,接着是一个列别名的可选列表。列别名对应于 CTE 内的 SELECT 语句返回的列。可选列别名的后面是 AS 关键字,这是必需的。AS 关键字后面是用括号括起来、定义 CTE 的查询表达式。
请看这个示例:
 ;WITH myCTE (CustID, Co) AS
(
SELECT CustomerID, CompanyName FROM Customers
)
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 的查询。例如,以下批处理会产生错误:
 ;WITH myCTE (CustID, Co) AS
(
SELECT CustomerID, CompanyName FROM Customers
)
SELECT CompanyName FROM Customers WHERE CustomerID = 'ALFKI'
SELECT CustID, Co FROM myCTE
在本段代码中,myCTE 仅供紧随其后的第一个查询使用。当第二个查询引用 myCTE 时,CTE 不在范围之内,并且引发异常(对象名“myCTE”无效)。
另请注意,因为 CTE 预期由另一个可能随之肯定要重新处理数据的查询引用,所以 CTE 的查询不能含有 ORDER 和 COMPUTE 之类的语句。然而,诸如 FOR XML 的复杂语句仍可用来定义和运行 CTE。例如,您可以使用 FOR XML 子句来查询 CTE 并返回其结果,如下所示。 
 ;WITH myCTE AS
(
SELECT c.CustomerID, c.CompanyName, o.OrderID, o.OrderDate
FROM Customers c
INNER JOIN Orders o ON c.CustomerID = o.CustomerID
) SELECT CustomerID, CompanyName, OrderID, OrderDate
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 如下所示:
 ;WITH myRecursiveCTE(col1, col2, ... coln) AS
(
-- 定位点成员 查询
UNION ALL
-- 递归成员 查询来自CTE的自身数据
)
  当编写不涉及 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 递归收集销售人员
 ;WITH EmpCTE(EmployeeID, EmployeeFirstName,
EmployeeLastName, MgrID, SalesLevel)
AS
(
-- 定位点成员
SELECT EmployeeID, FirstName, LastName, ReportsTo, 0
FROM Employees
WHERE EmployeeID = 2 -- Start with the VP of Sales UNION ALL -- 递归成员
SELECT
e.EmployeeID, e.FirstName, e.LastName, e.ReportsTo, m.SalesLevel+1
FROM
Employees AS e
INNER JOIN EmpCTE m ON e.ReportsTo = m.EmployeeID
) -- 使用CTE
SELECT EmployeeID, EmployeeFirstName, EmployeeLastName,
MgrID, SalesLevel
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 可以帮您应对许多常见开发情况,并且可在不损害性能的情况下提升可读性。
 

SQLServer中的CTE通用表表达式的更多相关文章

  1. SQLServer中的CTE(Common Table Expression)通用表表达式使用详解

    概述 我们经常会编写由基本的 SELECT/FROM/WHERE 类型的语句派生而来的复杂 SQL 语句.其中一种方案是需要编写在 FROM 子句内使用派生表(也称为内联视图)的 Transact-S ...

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

    1 CTE语法WITH关键字 通用表表达式(Common Table Express, CTE),将派生表定义在查询的最前面.要使用CTE开始创建一个查询,可以使用WITH关键字. CTE语法: WI ...

  3. sql中with的用法(CTE公用表表达式):应用子查询嵌套,提高sql性能

    一.WITH AS的含义 WITH AS短语,也叫子查询部分(subquery factoring),定义一个SQL片断,该片断会被整个SQL语句所用到. 有时是为了让SQL语句的可读性更高些,也可能 ...

  4. SQL Server CET 通用表表达式 之 精典递归

    SQL2005 Common Table Expressions(CET)即通用表表达式. SQLSERVER CET递归使用案例: 1.普通案例 表结构如下:   ;WITH cet_depart ...

  5. 关于使用CTE(公用表表达式)的递归查询

    --关于使用CTE(公用表表达式)的递归查询 --CTE 的基本语法结构如下: WITH expression_name [ ( column_name [,...n] ) ] AS ( CTE_qu ...

  6. mysql 和 sqlserver中备份一张表的区别

    sqlserver中备份一张表 SELECT * into qa_buglist_bak FROM qa_buglist 不管表的数据有多大,速度很快: mysql中上述语句就无效了,须得新建一张表, ...

  7. SQLServer中查询表结构(表主键 、列说明、列数据类型、所有表名)的Sql语句

    SQLServer中查询表结构(表主键 .列说明.列数据类型.所有表名)的Sql语句 1.查询数据库中的所有表名称: SELECT name FROM SysObjects Where XType=' ...

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

    CTE(Common Table Expression) 公用表表达式,它是在单个语句的执行范围内定义的临时结果集,只在查询期间有效.它可以自引用,也可在同一查询中多次引用,实现了代码段的重复利用. ...

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

    本文转自:爽朗的微笑  http://www.cnblogs.com/shuangnet/archive/2013/03/22/2975929.html 公用表表达式 (CTE) 具有一个重要的优点, ...

随机推荐

  1. 【JVM】JVM垃圾收集器、垃圾收集算法、无用对象

    Java 常见的垃圾收集器有哪些 实际上,垃圾收集器(GC,Garbage Collector)是和具体 JVM 实现紧密相关的,不同厂商(IBM.Oracle),不同版本的JVM,提供的选择也不同. ...

  2. 「线性基」学习笔记and乱口胡总结

    还以为是什么非常高大上的东西花了1h不到就学好了 线性基 线性基可以在\(O(nlogx)\)的时间内计算出\(n\)个数的最大异或和(不需要相邻). 上述中\(x\)表示的最大的数. 如何实现 定义 ...

  3. apache beam ElasticSearchIO 遇到异常后job中断执行 自己定制beam IO

    可以将 ElasticSearchIO.java 源码拷贝至自己的项目.改名为MyElasticSearchIO.java.自己定制实现.这时编译会遇到auto.value失败.在pom.xml中加入 ...

  4. 大数质因解:浅谈Miller-Rabin和Pollard-Rho算法

    2017-07-19 08:54 Amphetamine:能发一下代码吗? 应我那位谜一样好友的邀约,我打算好好看一看Miller-Rabin和Pollard-Rho算法.很奇怪,各种地方有很多代码描 ...

  5. Sobel 边缘检测算子

    转自:http://blog.csdn.net/xiaqunfeng123/article/details/17302003 Sobel 算子是一个离散微分算子 (discrete different ...

  6. MySQL学习笔记(四)悲观锁与乐观锁

    恼骚 最近在搞并发的问题,订单的异步通知和主动查询会存在并发的问题,用到了Mysql数据库的 for update 锁 在TP5直接通过lock(true),用于数据库的锁机制 Db::name('p ...

  7. topshelf 服务启动运行

    topshlf 服务启动运行方式 安装:servicename.exe install 启动:servicename.exe start 卸载:servicename.exe uninstall

  8. 开发portlet中的一些问题记录,portlet:resourceURL用法,portlet中通过processAction方法传值

    在portlet页面中引入js或者css,通过c或者s标签 <!--jquery实际放的地方:/MyTask/WebContent/scripts/jquery-1.8.3.min.js--&g ...

  9. 移动端调用电话、短信、唤起QQ和使用百度地图

    H5能很方便地实现这些功能,都是一句代码搞定 调用电话 <a href="tel:12345678"> 短信 <a href='sms:12345678'> ...

  10. Easyui datalist 使用记录

    仅简单记录下,资料相对比较少 官方给了一个很简单的例子,没啥用处,文档:http://www.jeasyui.com/documentation/datalist.php 学习要点: 1.追加行 $( ...