表表达式(Table Expression)是一个命名的查询表达式,代表一个有效的关系表。Microsoft SQL Server支持4种类型的表表达式:派生表、公用表表达式(CTE)、视图和内嵌表值函数(内嵌TVF)。
有效定义任何类型表表达式的查询必须满足3个要求:
1. 无法保证顺序。表表达式用于代表一个关系表,并且关系表中的行无法保证顺序。因此,标准 SQL 不允许 ORDER BY 子句出现在定义表表达式的查询中,除非 ORDER BY 用于展示之外的其他目的。例如使用具有 TOP 或 OFFSET-FETCH 与 ORDER BY 的查询定义一个表表达式, ORDER BY 仅是用于与筛选相关的目的,而不是通常的展示目的。如果表表达式的外部查询没有用于展示的 ORDER BY,则输出无法保证按特定顺序返回。
2. 所有列都必须具有名称。在定义表表达式的查询中,必须为 SELECT 列表中的所有表达式分配列别名。
3. 所有列名都必须是唯一的。多个列具有相同名称的表表达式是无效的。
注:所有这3个要求的事实是,表表达式应当表示一个关系。所有关系属性必须具有名称,所有属性名称必须唯一,并且关系的主体是一个无序的元组集合。
派生表(也称子查询表)是在外部查询的 FROM 子句中定义的,它们存在的范围是外部查询,一旦外部查询完成,派生表就消失了。定义派生表的查询应包含在括号内,需要指定派生表的名称。
使用表表达式的好处之一就是,在外部查询的任何子句中,可以引用内部查询的 SELECT 子句中分配的列别名。这可以帮助你绕开在 SELECT 子句逻辑处理之前的查询子句中(如 WHERE、GROUP BY)无法引用在 SELECT 子句中分配的列别名的实际问题。
SELECT OrderYear=YEAR(A.orderdate), NumCusts=COUNT(DISTINCT A.custid)
FROM Sales.Orders A GROUP BY YEAR(A.orderdate)
因为在 GROUP BY 中无法使用 SELECT 中的别名,所以只能在 GROUP BY 子句中使用同样的表达式,这样会损害代码的可读性和可维护性,容易出错。可以通过表表达式避免这种问题:
SELECT B.OrderYear, NumCusts=COUNT(DISTINCT B.custid)
FROM
(SELECT OrderYear=YEAR(A.orderdate), A.custid FROM Sales.Orders A) B
GROUP BY B.OrderYear
在定义派生表的查询中可以引用参数,参数可以是例行的能够用于存储过程或函数等的变量或输入参数。
DECLARE @Empid INT = 3;
SELECT B.OrderYear, NumCusts=COUNT(DISTINCT B.custid)
FROM
(SELECT OrderYear=YEAR(A.orderdate), A.custid FROM Sales.Orders A WHERE A.empid=@Empid) B
GROUP BY B.OrderYear
如果定义派生表的查询需要引用一个派生表,则这将是一个嵌套派生表。派生表嵌套的实际结果是派生表定义不在是单独存在的外部查询 FROM 子句中。嵌套是编程中一个不确定的方面,因为它会使代码复杂化并降低其可读性。
SELECT C.OrderYear, C.NumCusts
FROM (
SELECT B.OrderYear, NumCusts=COUNT(DISTINCT B.custid)
FROM (
SELECT OrderYear=YEAR(A.orderdate), A.custid FROM Sales.Orders A) B
GROUP BY B.OrderYear) C
WHERE C.NumCusts>70
此例使用表表达式的整体目的是,通过重复使用列别名代替重复表达式来简化解决方案。但派生表的嵌套增加了复杂性,不如不用:
SELECT YEAR(A.orderdate), COUNT(DISTINCT A.custid)
FROM Sales.Orders A GROUP BY YEAR(A.orderdate) HAVING COUNT(DISTINCT A.custid)>70
公用表表达式(CTE)是表表达式的另一种标准形式,定义 CTE 的内部查询同样必须遵循有效定义表表达式的所有要求。CTE 通过 WITH 语句定义:
WITH [(target_column_list)]
AS
(
)
CTE 支持两种列别名命名方式——内嵌方式和外部方式:
WITH CTE AS
(
SELECT OrderYear=YEAR(A.orderdate), A.custid FROM Sales.Orders A
)
SELECT B.OrderYear, NumCusts=COUNT(DISTINCT B.custid) FROM CTE B GROUP BY B.OrderYear
WITH CTE(OrderYear, custid) AS
(
SELECT YEAR(A.orderdate), A.custid FROM Sales.Orders A
)
SELECT B.OrderYear, NumCusts=COUNT(DISTINCT B.custid) FROM CTE B GROUP BY B.OrderYear
DECLARE @Empid INT = 3;
WITH CTE AS
(
SELECT OrderYear=YEAR(A.orderdate), A.custid
FROM Sales.Orders A WHERE A.empid=@Empid
)
SELECT B.OrderYear, NumCusts=COUNT(DISTINCT B.custid) FROM CTE B GROUP BY B.OrderYear
可以同时定义多个 CTE,在同一个 WITH 语句下使用逗号进行分割,每个 CTE 可以引用所有前面定义的 CTE,并且外部查询可以引用所有的 CTE。
WITH CTE1 AS
(
SELECT OrderYear=YEAR(A.orderdate), A.custid FROM Sales.Orders A
), CTE2 AS
(
SELECT B.OrderYear, NumCusts=COUNT(DISTINCT B.custid) FROM CTE1 B GROUP BY B.OrderYear
)
SELECT C.OrderYear, C.NumCusts FROM CTE2 C WHERE C.NumCusts>70
就外部查询的 FROM 子句而言,CTE 在其之前已经存在了,因此可以引用同一个 CTE 的多个实例:
WITH CTE AS
(
SELECT OrderYear=YEAR(A.orderdate), NumCusts=COUNT(DISTINCT A.custid)
FROM Sales.Orders A GROUP BY YEAR(A.orderdate)
)
SELECT
Cur.OrderYear, CurNumCusts=Cur.NumCusts, PrvNumCusts=Prv.NumCusts,
Growth=Cur.NumCusts-Prv.NumCusts
FROM
CTE Cur LEFT JOIN
CTE Prv ON Cur.OrderYear=Prv.OrderYear+1
递归 CTE 至少由两个查询(可能更多)定义,至少一个查询作为定位点成员,一个查询作为递归成员。基本递归 CTE 的一般形式:
WITH [(target_column_list)]
AS
(
UNION ALL
)
定位点成员是一个返回有效关系结果表的查询,定位成员查询仅调用一次。递归成员是一个引用 CTE 名称的查询,递归成员没有显示递归终止检查——终止检查是隐式的。递归多次调用,直到它返回一个空集合或超过某些限制为止。
WITH CTE_Emps AS
(
SELECT empid, mgrid, firstname, lastname FROM HR.Employees WHERE empid=2
UNION ALL
SELECT B.empid, B.mgrid, B.firstname, B.lastname
FROM CTE_Emps A JOIN HR.Employees B ON A.empid=B.mgrid
)
SELECT empid, mgrid, firstname, lastname FROM CTE_Emps OPTION(MAXRECURSION 2);
SQL Server 默认限制递归成员可以被调用的次数为100,递归成员第101次调用代码会失败。可以在外部查询的尾部指定 OPTION(MAXRECURSION n) 提示来更改默认的最大递归限制,n是一个0~32767范围的整数,表示要设置的最大递归限制。可以指定 MAXRECURSION 0 完全取消限制。
视图和内嵌表值函数(内嵌TVF)是两张可重复使用的表表达式类型,其定义被存储为数据库对象。创建之后,这些对象是数据库的永久部分,只有在显示删除它们是才会从数据库中移除。由于视图是数据库中的对象,你可以控制视图的访问权限(包括 SELECT、INSERT、UPDATE 和 DELETE权限)。
应避免在具有特定相关的视图上下文中使用 SELECT *,在视图编译中列是枚举的,新的表列不会自动添加到视图。可以使用存储过程 sp_refreshview 或 sp_refreshsqlmodule 来刷新视图的元数据,但为了避免混淆,最好在视图定义中显示地列出所需要的列名称。
用于定义视图的查询必须满足有效定义表表达式的所有要求。视图无法保证行的顺序,所有视图列都必须具有名称,并且所有列名称必须唯一。
ORDER BY 子句不允许出现在定义表表达式的查询中,因为关系表的行之间没有顺序可言。试图创建一个有序试图是荒谬的,因为它违反了关系模型定义的基本特性。除非 ORDER BY 用于展示之外的其他目的(如 TOP、OFFSET-FETCH 筛选)。
可以通过带有 0 ROWS 的 OFFSET 子句,并且没有 FETCH 子句尝试获取有序试图:
IF OBJECT_ID('Sales.USACusts') IS NOT NULL
DROP VIEW Sales.USACusts;
GO
CREATE VIEW Sales.USACusts
AS
SELECT custid, companyname, contactname, contacttitle, address
FROM Sales.Customers A WHERE A.country=N'USA'
ORDER BY A.custid
OFFSET 0 ROWS;
GO
创建或更改视图时,可以指定作为视图定义一部分都视图属性和选项。
ENCRYPTION 选项
在创建或更改视图、存储过程、触发器和用户定义函数(UDF)时,ENCRYPTION 选项是可用的。ENCRYPTION 指示 SQL Server在内部以代码混淆方式存储对象定义文本。代码混淆文本对通过任何目录对象的用户不直接可见,仅对通过特定方法的特权用户可见。
当视图没有使用 ENCRYPTION 时,视图定义文本是可用的,可以通过 OBJECT_DEFINITION 函数获得视图的定义。
SELECT OBJECT_DEFINITION(OBJECT_ID('Sales.USACusts'))
通过 ALTER 改变视图的定义,并包含 ENCRYPTION 选项,再次尝试获取视图定义文本,通过 OBJECT_DEFINITION 方式会返回 NULL。可以使用 sp_helptext 存储过程获取对象的定义,但是因为这里视图以 ENCRYPTION 选项创建,所有无法得到对象的定义,而是会得到消息:The text for object 'Sales.USACusts' is encrypted.
。
ALTER VIEW Sales.USACusts WITH ENCRYPTION
AS
SELECT custid, companyname, contactname, contacttitle, address
FROM Sales.Customers A WHERE A.country=N'USA'
GO
SELECT OBJECT_DEFINITION(OBJECT_ID('Sales.USACusts')) -- NULL
EXEC sp_helptext 'Sales.USACusts' -- The text for object 'Sales.USACusts' is encrypted.
SCHEMABINDING选项
SCHEMABINDING 选项对视图和 UDF 可用,它将被引用对象的架构和列绑定到引用对象的架构中,它指示不能删除被引用对象,也不能删除或修改被引用的列。
ALTER VIEW Sales.USACusts WITH SCHEMABINDING
AS
SELECT custid, companyname, contactname, contacttitle, address
FROM Sales.Customers A WHERE A.country=N'USA'
GO
ALTER TABLE Sales.Customers DROP COLUMN address;
/*
消息 5074,级别 16,状态 1,第 14 行
The object 'USACusts' is dependent on column 'address'.
消息 4922,级别 16,状态 9,第 14 行
ALTER TABLE DROP COLUMN address failed because one or more objects access this column.
*/
要支持 SCHEMABINDING 选项,查询中不允许在 SELECT 子句中使用 *,而是必须显式地列出列名称,并在引用对象时,必须使用架构限定的两部分名称。
CHECK OPTION 选项
CHECK OPTION 的目的是防止出现视图修改与视图筛选的冲突,(个人理解:简而言之,就是防止通过视图修改基础对象的数据之后,导致视图筛选的结果和修改之前的结果不一致的问题,可能的检测方式是通过视图中的 WHERE 条件)。
ALTER VIEW Sales.USACusts WITH SCHEMABINDING
AS
SELECT custid, companyname, contactname, contacttitle, address
FROM Sales.Customers A WHERE A.country=N'USA'
WITH CHECK OPTION;
GO
内嵌 TVF 是支持输入参数的可重复使用的表表达式。可以使用 DML(Data Manipulation Language:数据操纵语言)语句查询内嵌 TVF。
IF OBJECT_ID('dbo.GetUser') IS NOT NULL
DROP FUNCTION dbo.GetUser;
GO
CREATE FUNCTION dbo.GetUser (@UserID INT)
RETURNS TABLE
AS
RETURN
SELECT A.UserID, A.UserName, A.PKCity FROM Users A WHERE A.UserID=@UserID;
GO
SELECT OBJECT_DEFINITION(OBJECT_ID('dbo.GetUser'))
EXEC sp_helptext 'dbo.GetUser'
内嵌 TVF 与其他表一样可以作为联接的一部分:
SELECT A.CityID, A.City, B.UserID, B.UserName
FROM Cities A JOIN GetUser(1) B ON A.PKCity=B.PKCity;
APPLY 运算符是一个非常强大的表运算符,该运算符用于查询的 FROM 子句中。APPLY 运算符对两个输入表进行操作,第二个表可以是一个表表达式。APPLY 运算符支持 CROSS APPLY 和 OUTER APPLY 两种类型。 CROSS APPLY 仅实施一个逻辑查询处理阶段,而 OUTER APPLY 实施两个阶段。
CROSS APPLY 运算符实施一个逻辑查询处理阶段——将右侧表表达式应用到左侧的每一行,并生成一个组合结果集的结果表。CROSS APPLY 非常类似 CROSS JOIN,但是 CROSS APPLY 运算符右侧的表表达式可以对来自左侧的每一行表示一个不同的行集。
SELECT A.custid, C.orderid, C.orderdate
FROM
Sales.Customers A CROSS APPLY
(
SELECT TOP(3) B.orderid, B.empid, B.orderdate, B.requireddate
FROM Sales.Orders B
WHERE B.custid=A.custid ORDER BY B.orderdate DESC, B.orderid DESC
) C
如果右侧的表表达式返回一个空集合,CROSS APPLY 运算符不会返回相应的左侧行。如果要返回右侧表表达式为空集的左侧表中的行,可以使用 OUTERE APPLY 运算符代替 CROSS APPLY。OUTER APPLY 运算符增加了第二个逻辑阶段,即标识右侧表表达式为空集的左侧行,并作为输出行添加到结果中,在右侧的属性中以 NULL 标记作为占位符(类似左外联接)。
-- OUTER APPLY 会保留左侧表,右侧表属性以 NULL 标记作为占位符
SELECT A.custid, C.orderid, C.orderdate
FROM
Sales.Customers A OUTER APPLY
(
SELECT TOP(3) B.orderid, B.empid, B.orderdate, B.requireddate
FROM Sales.Orders B
WHERE B.custid=A.custid ORDER BY B.orderdate DESC, B.orderid DESC
) C