开发人员正在研发的许多项目都涉及编写由基本的 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. BZOJ 3653: 谈笑风生(离线, 长链剖分, 后缀和)

    题意 给你一颗有 \(n\) 个点并且以 \(1\) 为根的树.共有 \(q\) 次询问,每次询问两个参数 \(p, k\) .询问有多少对点 \((p, a, b)\) 满足 \(p,a,b\) 为 ...

  2. Entity Framework入门教程(12)--- EF进行批量添加/删除

    EF6添加了批量添加/删除实体集合的方法,我们可以使用DbSet.AddRange()方法将实体集合添加到上下文,同时实体集合中的每一个实体的状态都标记为Added,在执行SaveChange()方法 ...

  3. 腾讯云服务器tomcat端口无法访问

    第一种情况: 如题:https://console.cloud.tencent.com/cvm/securitygroup 需要去这个地址设置安全组. 说实话,一句mmp不知当讲不当讲.使用说明这块太 ...

  4. python下调用pytesseract识别某网站验证码

    一.pytesseract介绍 1.pytesseract说明 pytesseract最新版本0.1.6,网址:https://pypi.python.org/pypi/pytesseract Pyt ...

  5. Pytorch中的自动求导函数backward()所需参数含义

    摘要:一个神经网络有N个样本,经过这个网络把N个样本分为M类,那么此时backward参数的维度应该是[N X M] 正常来说backward()函数是要传入参数的,一直没弄明白backward需要传 ...

  6. Android app中的so库和CPU架构

    一.android目前有几种cpu架构? 早期的Android系统几乎只支持ARMv5的CPU架构,目前支持七种CPU架构:ARMv5,ARMv7 (从2010年起),x86 (从2011年起),MI ...

  7. Django 实现list页面检索

    在list.html写入from表单 在views渲染list方法写入,从前台获取的searchtitle根据name实现检索

  8. Synchronized 和 Volatile

    Synchronized : 称为重量级锁,经过优化后,也没那么重了 一.CAS 1.CAS:Compare and Swap, 翻译成比较并交换. 2.java.util.concurrent包中借 ...

  9. PHP 【五】

    函数 如要在页面加载时执行脚本,可以把它放到函数里 函数是通过调用函数来执行的 可在页面的任何位置调用函数 <?phpfunction functionName(){    // 要执行的代码} ...

  10. POJ 1410 Intersection (线段和矩形相交)

    题目: Description You are to write a program that has to decide whether a given line segment intersect ...