SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
示例数据库:点我
Chapter 05 表表达式
表表达式(Table Expression)是一个命名的查询表达式,代表一个有效的关系表。表表达式没有任何的物理实例化,在查询表表达式时它们是虚拟的,内部查询是非嵌套的。即外部查询和内部查询直接合并到一个对底层对象的查询中。
5.1 派生表
派生表(也称子查询表)是在外部查询的FROM 子句中定义的,它们存在的范围是外部查询。一旦外部查询完成后,派生表就消失了。
--语法
SELECT * FROM (SELECT * FROM Sales.Customers WHERE country = 'USA') AS USACusts
有效定义的表表达式的查询必须满足3个要求:
-
无法保证顺序。标准SQL是不允许 ORDER BY 子句出现在定义的表表达式查询中的,除非 ORDER BY 用于展示之外的其他目的。如:使用 OFFSET-FETCH 或 TOP 筛选。
-
所有列必须具有名称。必须为所有列分配列别名。
-
所有列名必须是唯一的。
5.1.1 分配列别名
--1.内嵌方式
SELECT * FROM (SELECT YEAR(orderdate) AS orderyear,custid FROM Sales.Orders) AS D
--2.外部形式
SELECT * FROM (SELECT YEAR(orderdate),custid FROM Sales.Orders) AS D(orderyear, custid)
通常建议使用内嵌别名形式,这样调试代码时可以直接选定定义表表达式来直接运行,在结果中就可以直观的别名显示出来。如果不打算再进行任何进一步的修改的话,并且希望将其看作一个“黑匣子”时使用外部形式分配列别名更好点。
5.1.2 使用参数
5.1.3 嵌套
5.1.4 多个引用
5.2 公用表表达式
公用表表达式(CTE)是表表达式的另一种标准形式,与派生表非常相似。
语法:
;WITH CTE_NAME AS ( inner_query ) outer_query
注意,T-SQL中的 WITH 子句可以用于不同的目的,为避免报错,建议在使用CTE时,要在 WITH 前加分别(;)
5.2.1 分别列别名
CTE中也是有两种方式分配列别名
--外部方式
;WITH CTE_NAME(col1,col2) AS( inner_query ) outer_query
5.2.2 使用参数
5.2.3 定义多个CTE
;WITH c1 AS (SELECT YEAR(orderdate) AS orderyear FROM Sales.Orders)
,c2 AS ( SELECT count(*) total FROM c1 )
SELECT * FROM c2
5.2.4 CTE的多次引用
就外部查询的 FROM 子句而言, CTE在其之前已经存在了,因此可以对同一个CTE进行多次引用。
5.2.5 递归CTE
递归CTE至少由两个查询定义,至少一个查询作为定位点成员,一个查询作为递归成员。基本递归CTE的一般形式如下:
;WITH <CTE_name>[<targe_column_list>]
AS
(
<anchor_member>
UNION ALL
<recursive_member>
)
<outer_query_against_CTE>
定位点成员是一个返回有效关系结果表的查询,就像一个用于定义非递归表表达式的查询。定点成员查询仅调用一次。
递归成员是一个引用CTE名称的查询。递归成员多次调用,直到它返回一个空集合或超过某些限制为止。
在外部查询中引用CTE名称代表的是定位点成员调用和所有递归成员调用的组合结果集。
--利用递归CTE返回某个雇员和其各级下属雇员(直接或间接)
;WITH EmpsCTE AS
(
--定位点成员
SELECT empid, mgrid, firstname, lastname
FROM HR.Employees
WHERE empid = 2
UNION ALL
--递归成员
SELECT e.empid, e.mgrid, e.firstname, e.lastname
FROM EmpsCTE p
INNER JOIN HR.Employees AS e ON p.empid = e.mgrid
)
SELECT * FROM EmpsCTE
递归成员联接CTE代表的是上一个结果集。 然后从 Employees 表检索由上一个结果集中返回的直接下属。
在出现递归成员的联接谓词逻辑错误或是数据的循环结果错误,递归成员可能会调用无数次。作为一项安全措施,SQL SERVER默认情况下限制递归成员可以被调用的次数为100。可以在外部查询的尾部指定
OPTION(MAXRECURSION n)
提示来更改默认的最大递归限制,n范围为0-32767。
5.3 视图
表表达式的范围只是在单查询语句之中,视图 和 内嵌表值函数(内嵌TVF) 是两种可重复使用的表表达式类型,其定义被存储为数据库对象。只有在显式删除它们时才从数据库中移除掉。
语法:
IF OBJECT_ID('Sales.USACusts') IS NOT NULL
DROP VIEW Sales.USACusts
GO
CREATE VIEW Sales.USACusts
AS
SELECT custid,companyname ROM Sales.Customers WHERE country = 'USA'
--查询
SELECT * FROM Sales.USACusts
注意,不建议使用 SELECT *
,因为当TABLE的添加或删除列时,VIEW 的元数据并不会跟着改变,可以使用 sp_refreshview
或 sp_refreshsqlmodule
来刷新 VIEW 的元数据,但是为了避免混淆,最好是通过 ALTER VIEW 来进行显式的添加或删除 TABLE 对应的列。
5.3.1 视图和ORDER BY 子句
用于展示的 ORDER BY
子句不允许出现在定义表表达式的查询中,因为关系表的行之间没有顺序可言。试图创建一个有序的VIEW是荒谬的,因为违反了关系模型定义的基本特性。
当然你可以通过 TOP(100)
和 OFFSET 0 ROWS
与 ORDER BY
子句来创建VIEW。当查询VIEW时得到的结果可能会是有序的,但是这个结果是不确定的,这种情况是数据库优化造成的。所以,不要混淆用于定义表表达式和非定义表表达式查询的行为。
5.3.2 视图选项
当创建或更改视图时,可以指定作为视图定义一部分的视图属性和选项。在视图的头部,在 WITH 子句下面可以指定如ENCRYPTION和SCHEMABINDING属性,可以在查询的尾部指定WITH CHECK OPTION。
1.ENCRYPTION选项
ENCRYPTION 可用于创建或更改 VIEW、Stored Procedure、Trigger 或 用户定义函数(UDF user define function) 时。ENCRYPTION选项指示SQL SERVER在内部以代码混淆方式存储对象定义文本。
--由于创建视图时没有使用ENCRYPTION,可以得到创建视图的定义语句
SELECT OBJECT_DEFINITION(OBJECT_ID('Sales.USACusts'))
--使用ENCRYPTION,偷懒使用了 * 创建。再使用上面这个语句得到的是NULL
CREATE VIEW Sales.USACusts WITH ENCRYPTION
AS
SELECT * FROM Sales.Customers
/*
作为OBJECT_DEFINITION函数的替代方法,可以执行下面这个存储过程来获取对象的定义,但是你发现返回的是“2 Procedure sp_helptext. The text for object 'Sales.USACusts' is encrypted. SQL.sql 126 25 ”
*/
EXECUTE sys.sp_helptext 'Sales.USACusts'
2.SCHEMABINDING
可对VIEW和UDF使用,它将被引用对象的架构和列绑定到引用对象的架构中。它指示不能删除被引用对象,也不能删除或修改被引用的列。
CREATE VIEW Sales.USACusts WITH SCHEMABINDING
AS
SELECT custid,companyname FROM Sales.Customers WHERE country = 'USA'
/*
尝试从Customers表中删除companyname,报“ALTER TABLE DROP COLUMN companyname failed because one or more objects access this column. SQL.sql 14 1 ”
*/
ALTER TABLE Sales.Customers DROP COLUMN companyname
如果使用SCHEMABINDING选项,可以避免被引用对象或列的改变或删除导致的运行时错误,其实有点像外键约束一样。
注意,使用SCHEMABINDING选项时SELECT语句不能使用星号(*)查询,否则报错。Procedure USACusts. Syntax '*' is not allowed in schema-bound objects. SQL.sql 12 8
此外,在引用对象时,必须使用架构限定的两部分名称。
3.CHECK OPTION选项
使用此选项的目的是防止出现视图修改与视图筛选的冲突。假如定义了一个视图 USACusts,用于筛选国家为'USA'的客户,而没有使用CHECK OPTION选项,那么其它国家的客户也是可以成功插入到此视图中。如果你想防止出现此种冲突,那么可以在定义视图查询的尾部添加WITH CHECK OPTION来实现。这与检查约束类似。
CREATE VIEW Sales.USACusts WITH SCHEMABINDING
AS
SELECT custid,companyname,country FROM Sales.Customers WHERE country = 'USA'
WITH CHECK OPTION
/*
插入国家为'UK'的数据,报“An explicit value for the identity column in table 'Sales.USACusts' can only be specified when a column list is used and IDENTITY_INSERT is ON. SQL.sql 15 13 ”
*/
INSERT INTO Sales.USACusts VALUES (32,'Customer TEST','UK')
5.4 内嵌表值函数(TVF)
内嵌TVF(Table-valued Functions) 是支持输入参数的可重复使用的表表达式。除了支持输入参数之外,其他方面基本与视图类似。可以看作是参数化视图
语法:
--创建TVF
CREATE FUNCTION dbo.GetCustOrders
(@cid AS INT) RETURNS TABLE
AS
RETURN
SELECT *
FROM Sales.Orders
WHERE custid = @cid
--使用
SELECT c.* FROM dbo.GetCustOrders(1) AS c
5.5 APPLY 运算符
APPLY 运算符支持 CROSS APPLY 和 OUTER APPLY,前者仅实施一个逻辑查询处理阶段,而后者实施了两个阶段。
注:标准SQL叫做LATERAL,APPLY不是标准SQL
。
APLLY运算符对两个输入表进行操作,第二个表可以是一个表表达式(通常为派生表
或内联TVF
)。 CROSS APPLY 运算符的逻辑查询处理阶段是:它将右侧的表表达式
应用到左侧表的每一行,并生成一个组合结果集的结果表。与交叉联接非常类似。
SELECT s.shipperid,e.empid
FROM Sales.Shippers s
CROSS JOIN HR.Employees e
SELECT s.shipperid,e.empid
FROM Sales.Shippers s
CROSS APPLY HR.Employees e
以上两个SQL语句运行的结果是一致的。但是,CROSS APPLY 运算符右侧的
表表达式
可以对来自左侧表的每一行表示一个不同的行集,这是与联接不同的。可以在右侧表(派生表或内嵌TVF)中引用(传递)左侧表的属性。
--返回每个客户的最近3个订单
SELECT c.custid, A.orderid, A.orderdate
FROM Sales.Customers c
CROSS APPLY
( SELECT TOP 3 o.orderid, o.empid, o.orderdate, o.requireddate
FROM Sales.Orders o
WHERE o.custid = c.custid
ORDER BY o.orderdate DESC, o.orderid DESC) A
可以将表表达式A看作是一个相关子查询。
CROSS APPLY 运算符类似于内联接,若右侧表中没有对应的结果,则左侧的行也不会返回。如果想返回左侧的行,则可使用 OUTER APPLY 。
出于封装的目的,可以使用内嵌TVF代替派生表,这样代码更容易维护和跟踪,可读性更高。
--创建TVF
CREATE FUNCTION TopOrders
(@cid INT, @n INT) RETURNS TABLE
AS
RETURN
SELECT TOP (@n) orderid, empid, orderdate, requireddate
FROM Sales.Orders
WHERE custid = @cid
ORDER BY orderdate DESC, orderid DESC
--代替派生表
SELECT c.custid, A.orderid, A.orderdate
FROM Sales.Customers c
OUTER APPLY dbo.TopOrders(c.custid, 3) A
运行结果:
练习
--1.1 返回每个雇员在orderdate列中的最大值,表:Sales.Orders
SELECT empid, MAX(orderdate) AS maxorderdate
FROM Sales.Orders
GROUP BY empid
--1.2 根据1.1的派生表和Orders表之间的关联查询,返回每个雇员最大订单日期的订单。
SELECT o1.empid, o1.orderdate, o1.orderid, o1.custid
FROM Sales.Orders o1
INNER JOIN (
SELECT empid, MAX(orderdate) AS maxorderdate
FROM Sales.Orders
GROUP BY empid ) AS o2
ON o1.empid = o2.empid AND o1.orderdate = o2.maxorderdate
--2.1 计算orderdate、orderid排序的每个订单的行号,表:orders
SELECT ROW_NUMBER() OVER (ORDER BY orderdate, orderid) AS rownum
,orderid, orderdate, custid, empid
FROM Sales.Orders
--2.2 返回2.1行号为11-12的行。使用CTE封装2.1的代码
;WITH fetchOrdersCTE AS
(
SELECT ROW_NUMBER() OVER (ORDER BY orderdate, orderid) AS rownum
,orderid, orderdate, custid, empid
FROM Sales.Orders
)
SELECT *
FROM fetchOrdersCTE
ORDER BY 1
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY
--3 使用CTE,返回Zoya Dologopyatova(empid 9)的领导管理链,表:Employees
;WITH empsCTE AS
(
--锚点成员
SELECT empid,mgrid,lastname,firstname FROM HR.Employees
WHERE firstname = 'zoya'
UNION ALL
--递归成员
SELECT e.empid,e.mgrid,e.lastname, e.firstname
FROM HR.Employees e
INNER JOIN empsCTE cte ON e.empid = cte.mgrid
)
SELECT * FROM empsCTE
--4.1 创建一个视图,返回每位雇员每年的总销量,表:orders,orderdetails
CREATE VIEW Sales.VEmpOrders
AS
SELECT o.empid, YEAR(o.orderdate) AS orderyear, SUM(od.qty) AS qty
FROM Sales.Orders o
INNER JOIN Sales.OrderDetails od ON o.orderid = od.orderid
GROUP BY o.empid, YEAR(O.orderdate)
SELECT * FROM Sales.VEmpOrders ORDER BY 1, 2
--4.2 使用4.1的视图,返回每个雇员每年的运行总销量
SELECT * ,(SELECT SUM(qty) FROM Sales.VempOrders v2 WHERE v2.orderyear <= v1.orderyear AND v2.empid = v1.empid) as runqty
FROM Sales.VEmpOrders v1
GROUP BY empid, orderyear,qty
ORDER BY 1,2
/*
5.1 创建一个TVF,参数为供应商ID(@supid AS INT)和请求的产品数量(@n AS INT)。返回指定的供应商ID供应的@n个最高单价产品。表:products
*/
CREATE FUNCTION Production.TopProducts
(@supid INT, @n INT) RETURNS TABLE
AS
RETURN
SELECT TOP (@n) productid, productname, unitprice #
FROM Production.Products
WHERE supplierid = @supid
ORDER BY unitprice
SELECT * FROM Production.TopProducts(5,2)
--5.2 使用CROSS APPLY运算符和5.1中的TVF,为每个供应商返回两个最贵产品。
SELECT s.supplierid, s.companyname, t.productid, t.productname, t.unitprice
FROM Production.Suppliers s
CROSS APPLY Production.TopProducts(s.supplierid,2) t
3. 5.2
CHAPTER 06 集合运算符
集合运算符是应用于两个输入集合之间的运算符,或者说是“多元集合(multisets)”,其结果来自于两个输入查询。
T-SQL 支持UNITON、INTERSECT、EXCEPT 集合运算符。ORDER BY可以随意应用于运算符的结果中。
集合运算符涉及的两个查询必须具有相同的列数,而且对应的类型必须兼容(数据类型可以根据优先级转换) 。列名(类型)由第一个查询来确定。
标准的SQL对每个运算符支持两种行为:DISTINCT(默认)和ALL,即不加ALL的查询语句默认都是去重的
集合运算符中认为两个NULL 值是相等的。
6.1 UNION运算符(并集)
如果后面有ALL则两个查询结果的重复项都会返回到最终的结果中去。
如何确定使用哪种情况?当需要使用重复的数据时就使用ALL了,当然如果确定不会有重复的数据时,建议使用UNION ALL,这样避免数据库检查重复项所导致的开销。
6.2 INTERSECT运算符(交集)
仅返回两个查询结果中同时出现的行。 INTERSECT 运算符可以使用内部联接(INNER JOIN)和 EXISTS 谓词来替代。在这两种情况下,两个查询中的 NULL
标记的比较的结果是 UNKONW ,所以带有 NULL
的行被过滤掉。所以如果有 NULL
标记时就需要注意了。
在标准SQL中是支持 INTERSECT ALL 这个运算行为的,但是在SQL SERVER 2012中尚未实现。INTERSECT ALL 即是说R行数据在第一个查询集合中出现x次,在第二个中出现的次数为y次,则最终返回的结果应该是min(x,y)次。我们可以通过 ROW_NUMBER 函数生成每个查询生成的次数,在 PARTITION BY 子句指定所有参与的属性,并在 ORDER BY 子句中使用 SELECT <CONSTANT> 指示顺序(其实排序序在这里没有什么卵用,SQL SERVER 会进行识别优化,不会进行相应的排序,所以也不会造成相关开销)。
--INTERSECT ALL 不应该返回rownum的,可以再查询一次去年这一列
SELECT ROW_NUMBER() OVER (PARTITION BY country, region, city ORDER BY (SELECT 0)) AS rownum
,country, region, city
FROM HR.Employees
INTERSECT
SELECT ROW_NUMBER() OVER (PARTITION BY country, region, city ORDER BY (SELECT 0)) AS rownum
,country, region, city
FROM Sales.Customers
6.3 EXCEPT 运算符(差集)
返回第一个查询集合中没有出现在第二个查询集合中的结果行。
EXCEPT 集合运算符在逻辑上首先消除两个查询集合中的重复行,再进行差值运算。可以使用仅筛选外部行的外联接和NOT EXISTS 谓词来替代 EXCEPT(有NULL
标记时就要注意了)。
EXCEPT ALL的定义:R行在第一个查询集合中出现x次,在第二个出现y次,并且x>y,则R在 EXCEPT ALL 后出现x-y次。T-SQL也没有实现这一功能,这个也可以通过ROW_NUMBER来实现。
SELECT ROW_NUMBER() OVER (PARTITION BY country, region,city ORDER BY (SELECT 0)) AS rownum
,country, region, city
FROM HR.Employees
EXCEPT
SELECT ROW_NUMBER() OVER (PARTITION BY country, region,city ORDER BY (SELECT 0)) AS rownum
,country, region, city
FROM Sales.Customers
6.4 优先级
集合运算符的优先级是: INTERSECT > UNION = EXCEPT,但使用括号能够使得代码阅读性更佳。
6.5 规避不支持的逻辑阶段
用于集合运算符输入的独立查询支持除 ORDER BY 之外的所有逻辑查询处理阶段(如表运算符,WHERE, GROUP BY, HAVING)。但是,仅有 ORDER BY 阶段允许用于运算符的结果,如果需要其他逻辑运算可以通过表表达式绕过此限制。定义一个基于使用集合运算符的查询的表表达式,可以在对表表达式的外部查询中应用任何所需的逻辑查询处理阶段。
/*
此查询演示了UNION的运行结果应用GROUP BY的逻辑处理阶段。同样,其他的逻辑查询处理阶段都可以在外部查询中应用
*/
SELECT u.country, COUNT(*) AS toatl
FROM (
SELECT country, region, city FROM HR.Employees
UNION
SELECT country, region, city FROM Sales.Customers
) u
GROUP BY u.country
/*
如果需要一个带有TOP(OFFSET-FETCH)的查询参与集合运算符中,那么需要定义一个表表达式,并指定一个对此表表达式的的外部查询参与到集合运算符中即可。
*/
SELECT *
FROM (
SELECT empid,orderid,orderdate
FROM Sales.Orders
WHERE empid = 5
ORDER BY orderdate DESC, orderid DESC
OFFSET 0 ROWS FETCH FIRST 2 ROWS ONLY
) o1
UNION ALL
SELECT * FROM(
SELECT empid,orderid,orderdate
FROM Sales.Orders
WHERE empid = 3
ORDER BY orderdate DESC, orderid DESC
OFFSET 0 ROWS FETCH FIRST 2 ROWS ONLY
) o2
练习
- --1. 不使用循环结构生成一个1-10范围的10个数字虚拟辅助表,不需要保证顺序。
- --2. 返回订单在200801而不是100802的客户和雇员,表Orders
- SELECT o.custid, o.empid
- FROM Sales.Orders o
- WHERE o.orderdate >= '20080101' AND o.orderdate < '20080201'
- EXCEPT
- SELECT o.custid, o.empid
- FROM Sales.Orders o
- WHERE o.orderdate >= '20080201' AND o.orderdate < '20080301'
-
- --3. 返回20080101和20080201均有订单的客户和雇员id
- SELECT o.custid, o.empid
- FROM Sales.Orders o
- WHERE o.orderdate >= '20080101' AND o.orderdate < '20080201'
- INTERSECT
- SELECT o.custid, o.empid
- FROM Sales.Orders o
- WHERE o.orderdate >= '20080201' AND o.orderdate < '20080301'
-
- --4. 返回订单活动在200801和200802,但是不在2007年的客户和雇员id对
- (
- SELECT o.custid, o.empid
- FROM Sales.Orders o
- WHERE o.orderdate >= '20080101' AND o.orderdate < '20080201'
- INTERSECT
- SELECT o.custid, o.empid
- FROM Sales.Orders o
- WHERE o.orderdate >= '20080201' AND o.orderdate < '20080301'
- )
- EXCEPT
- SELECT custid, orderid
- FROM Sales.Orders
- WHERE orderdate >= '20070101' AND orderdate < '20080101'
-
- --5.给出以下查询:
- SELECT country, region, city, 1 AS sortNum
- FROM HR.Employees
- UNION ALL
- SELECT country, region, city, 1 AS sortNum
- FROM Production.Suppliers
- --向查询中添加逻辑,保证输出中Employees返回的行在Suppliers返回的行之前,此外,在每一部分中,行按照country,region和city排序
- WITH tmpCTE AS
- (
- SELECT country, region, city, 1 AS sortNum
- FROM HR.Employees
- UNION ALL
- SELECT country, region, city, 0 AS sortNum
- FROM Production.Suppliers
- )
- SELECT country, region, city
- FROM tmpCTE
- ORDER BY sortNum DESC,country, region, city
5.
CHAPTER 07 查询
7.1 开窗函数
开窗函数对基础函数行子集的计算,为子集中的每行计算一个标题结果值。行子集被称为“窗口”,它是基于与当前行相关的窗口描述符。开窗函数使用 OVER 子句指定窗口的规范。
- --返回每个雇员每月的运行总值
- SELECT empid, ordermonth, val
- ,SUM(val) OVER(PARTITION BY empid ORDER BY ordermonth ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS runVal
- FROM Sales.EmpOrders
在 OVER 子句中指定的窗口规范有3个主要部分:分区(PARTITION BY)、排序(ORDER BY)和框架。空OVER()子句会为空函数公开一个来自一个基础查询结果集合所有行的窗口,然后添加到窗口规范的元素会从根本上进一步限制窗口。
分区子句PARTITION BY子句限定窗口为来自基础查询结果集合的行子集,共享分区列中相同值的行作为当前行。在些示例中,窗口以 empid 分区。
排序子句ORDER BY子句定义窗口中的行排序,只针对窗口框架的排序,不要与显示排序的混淆。
框架子句(ROWS BETWEEN <top delimiter> AND <bottom delimmiter>)筛选一个框架或一个子集,来自于窗口分区的两个指定分隔符之间的行。
7.1.1 排名开窗函数
开窗函数允许以多种不同的方式对行进行排序。SQL SERVER 支持4种排名函数:ROW_NUMBER、RANK、DENSE_RANK和NTILE。
- SELECT
- ROW_NUMBER() OVER ( ORDER BY val) AS rowNumber
- ,RANK() OVER ( ORDER BY val) AS rank
- ,DENSE_RANK() OVER ( ORDER BY val) AS denseRank
- ,NTILE(100) OVER ( ORDER BY val) AS ntile
- ,orderid,custid,val
- FROM Sales.OrderValues
使用分区子句(PARTITION BY)
- SELECT orderid,custid,val
- ,ROW_NUMBER() OVER (PARTITION BY custid ORDER BY val) AS rowNumber
- FROM Sales.OrderValues
- ORDER BY 2,3
窗口排序不是用于展示的,并且不会改变结果的关系的本质。如果需要保证展示排序,则必须添加一个展示用的 ORDER BY 子句。
如CHAPTER 02 所示, SELECT 的开窗函数的计算是在 DISTINCT 子句之前的。在以上的OrderValues的830行中有795行是不重复的,如果需要直接使用 DISTINCT 和 ROW_NUMBER,则不可能去掉重复项,因为ROW_NUMBER函数是在 DISTINCT之前处理的,所以可以考虑在 GROUP BY 阶段进行去重处理。
- --1.结果返回830行
- SELECT val
- ,ROW_NUMBER() OVER (ORDER BY val) AS rowNumber
- FROM Sales.OrderValues
-
- --2.
- SELECT val
- ,ROW_NUMBER() OVER (ORDER BY val) AS rowNumber
- FROM Sales.OrderValues
- GROUP BY val
- --或使用DENSE_RANK来处理
- SELECT DISTINCT val
- ,DENSE_RANK() OVER (ORDER BY val) AS rowNumber
- FROM Sales.OrderValues
- ORDER BY 1
以上SQL中,GROUP BY 阶段为795个唯一值生成了795个组,然后 SELECT 为每个 val 组生成唯一行和基于 val 排序的行号。
更多详情请看这里
7.1.2 偏移开窗函数
偏移开窗函数允许从当前行的某个偏移量或者一个窗口框架的开关或结尾的行返回一个元素。SQL SERVER 212 支持4个偏移函数:LAG和LEAD、FRISRT_VALUE和LAST_VALUE。
LAG(英文指:后移) 和LEAD(英文指:前移) 函数支持窗口分区和窗口排序子句,这些与窗口框架没有相关性。允许基于指定排序,从分区内当前行的某个偏移量行获得一个元素。LAG 函数是在当前行之前查找,LEAD 则是之后查找。函数第1个
参数是要返回的元素,第2个
参数是偏移量(可选,默认为1),第3个
参数是在请求的偏移量没有行返回的情况下的默认值(如果没指定,则为NULL)
- /*
- 查询OrderValues视图返回订单住处,对于每个客户的订单,使用>>**LAG**<<函数返回前一个客户订购值,使用>>**LEAD**<<返回下一个客户的订单值。
- */
- SELECT custid, orderid, val
- , LAG(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid) AS preVal
- , LEAD(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid) AS nextVal
- FROM Sales.OrderValues
FIRST_VALUE和LAST_VALUE 函数分别允许从窗口框架的第一行和最后一行返回元素。返回第一行使用的窗口框架应为:ROWS BETWWEEN UNBOUNDED PRECEDING AND CURRENT ROW
的 FIRST_VALUE,返回最后一行为:ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING
的 LAST_VALUE。
- SELECT custid, orderid, val
- ,FIRST_VALUE(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS firstVal
- ,LAST_VALUE(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS lastVal
- FROM Sales.OrderValues
- ORDER BY custid, orderdate, orderid
7.1.3 聚合开窗函数
在SQL SERVER 2012之前,窗口聚合函数仅支持窗口分区子句。在这版本之后,T-SQL支持窗口排序和框架子句。
使用OVER
子句会为函数公开一个基于查询结果集合所有行的窗口。SUM(val) OVER()
会返回所有值的总值。如果添加了窗口分区子句,那么就会为函数公开一个限定窗口,只有那些基础查询结果集合中共享分区元素中相同的值的行作为当前行。举例来说:SUM(val) OVER(PARTITION BY custid)
会返回当前客户的总值。
下面的查询不涉及排序和框架。
- --1
- SELECT orderid, custid, val
- ,SUM(val) OVER() AS totalVal
- ,SUM(val) OVER(PARTITION BY custid) AS custTotalVal
- FROM Sales.OrderValues
- GROUP BY orderid, custid, val
- -------------------------------------------------------------------
- --2 以下查询可以使用SUM(val) OVER()在SELECT中直接使用而不必先查询出总值赋值给变量,再进行使用
- SELECT SUM(val) AS total FROM Sales.OrderValues
SQL SERVER 2012的窗口聚合函数现在也支持窗口排序和框架子句,这能够允许进行像运动和移运动聚合、YTD计算等更复杂的计算。
- SELECT empid, ordermonth, val
- ,SUM(val) OVER( PARTITION BY empid
- ORDER BY ordermonth
- ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS runval
- FROM Sales.EmpOrders
SUM
返回雇员自活动开始到当前月份的val
总值。要对每个雇员单独计算,需要按empid
分区,然后按ordermonth
定义排序,ROWS BETWEEN UNBOUND PRECEDING AND CURRENT ROW
意味着“从分区开始当当前月份的所有活动”。对于
ROWS
窗口框架,SQL SERVER还支持其他定界符,可以指示一个当前行的后移偏移量或前移偏移量。如,计算当前行之前两行到后面一行之间的所有行:ROWS BETWEEN 2 PRECEDING AND 1 FOLLOWING
,没有上界则为 UNBOUND FOLLOWING
7.2 透视数据
透视数据可将行
转换为列
,转换过程中可能会聚合值。
每个透视请求都要涉及到3个逻辑处理阶段以及相关的元素:一个与分组相关的分组阶段或是行元素(分组):一个与扩展相关的扩展阶段或是列元素(扩展):以一个聚合元素和聚合函数相关的聚合阶段(聚合)。
- --创建并插入值
- CREATE TABLE dbo.Orders
- (
- orderid INT NOT NULL,
- orderdate DATE NOT NULL,
- empid INT NOT NULL,
- custid VARCHAR(5) NOT NULL,
- qty INT NOT NULL,
- CONSTRAINT PK_Orders PRIMARY KEY(orderid)
- );
-
- INSERT INTO dbo.Orders(orderid, orderdate, empid, custid, qty)
- VALUES
- (30001, '20070802', 3, 'A', 10),
- (10001, '20071224', 2, 'A', 12),
- (10005, '20071224', 1, 'B', 20),
- (40001, '20080109', 2, 'A', 40),
- (10006, '20080118', 1, 'C', 14),
- (20001, '20080212', 2, 'B', 12),
- (40005, '20090212', 3, 'A', 10),
- (20002, '20090216', 1, 'C', 20),
- (30003, '20090418', 2, 'B', 15),
- (30004, '20070418', 3, 'C', 22),
- (30007, '20090907', 3, 'D', 30);
- /*
- 查询返回每个雇员的每个客户生成总订单数量。
- */
- SELECT empid, custid, SUM(qty) AS sumqty
- FROM dbo.Orders
- GROUP BY empid, custid
- ORDER BY empid, custid
- --或使用SUM() OVER()分区计算总值后去重
- SELECT DISTINCT empid, custid, SUM(qty) OVER(PARTITION BY empid, custid)sumqty
- FROM dbo.Orders
但是现在想将以上结果进行旋转,即实现的查询输出结果如下图:
如上图所示,将dbo.Orders表中的数据聚合后旋转和透视视图,生成该数据视图的技术叫做透视。
每个透视请求涉及
3
个逻辑处理阶段以及与之相关的元素:一个与分组相关的分组阶段或是行元素,一个与扩展相关的扩展阶段或是列元素,以及一个与聚合元素和聚合函数相关的聚合阶段。最后,由于透视涉及分组,需要聚合数据生成分组和扩展元素“交叉口”的结果值,需要确定聚合函数和聚合元素。
7.2.1 使用标准SQL透视
-
分组阶段使用
GROUP BY
子句实现 -
扩展阶段在
SELECT
子句中使用CASE
表达式实现,这需要提前知道扩展元素并为每个元素指定单独的表达式。 -
聚合阶段是通过对每个
CASE
表达式应用相关的聚合函数实现。
- /*
- CASE后不指定ELSE时,默认返回 ELSE NULL
- */
- SELECT empid
- ,SUM(CASE WHEN custid = 'A' THEN qty END) AS A
- ,SUM(CASE WHEN custid = 'B' THEN qty END) AS B
- ,SUM(CASE WHEN custid = 'C' THEN qty END) AS C
- ,SUM(CASE WHEN custid = 'D' THEN qty END) AS D
- FROM orders
- GROUP BY empid
如果你不知道需要扩展的值,并且要从数据中查询它们,你需要动态SQL来构建查询字符串并执行它。第10章会涉及到这方面。
7.2.2 使用T-SQL PIVOT运算符透视
PIVOT 是T-SQL特有的表运算符,在查询 FROM
子句上下文中操作(像JOIN
一样)。它对一个源表或是表表达式进行操作,透视数据并返回一个结果表。
PIVOT 也涉及3
个逻辑处理阶段。但它的语法有所不同:
- --语法
- SELECT ...
- FROM soureTable
- PIVOT( <agg_func>(<aggregation_element>)
- FOR <spreading_element> IN (<list_of_target_columns>)) AS <result_table_alias>
在PIVOT 的括号中,可以指定聚合函数(此为SUM
)、聚合元素(qty
)、扩展元素(custid
)和目标名称列表(A,B,C,D
)。然后,需要指定一个别名,否则报错。
PIVOT运算符没有显式指定分组元素(移除了GROUP BY
子句)。PIVOT 根据源表(或表表达式)中未指定
的为扩展元素
或聚合元素
的其他元素进行隐式地分组
,所以源表的属性不应包含除扩展元素
、聚合元素
和分组元素
之外的属性。
- --1.custid作为扩展元素
- SELECT empid,A,B,C,D
- FROM (SELECT empid, custid, qty FROM orders) d
- PIVOT(SUM(qty)FOR custid IN(A, B, C, D)) AS p
-
- --2.empid作为扩展元素
- SELECT [1],[2],[3]
- FROM (SELECT empid, custid, qty FROM orders) d
- PIVOT(SUM(d.qty) FOR empid IN([1],[2],[3])) d
建议不要直接操作基表,即使表中仅包含用途透视的列,因为当需求改变时你的基表可能会添加新列,造成透视结果不符合预期。所以推荐使用表表达式
7.3 逆透视数据
逆透视就是将数据从列
状态旋转成为行
状态。从每个源行生成多个结果行,每行具有一个不同的源列值。
- CREATE TABLE dbo.EmpCustOrders
- (
- empid INT NOT NULL
- CONSTRAINT PK_EmpCustOrders PRIMARY KEY,
- A VARCHAR(5) NULL,
- B VARCHAR(5) NULL,
- C VARCHAR(5) NULL,
- D VARCHAR(5) NULL
- );
- INSERT INTO dbo.EmpCustOrders(empid, A, B, C, D)
- SELECT empid, A, B, C, D
- FROM (SELECT empid, custid, qty
- FROM dbo.Orders) AS D
- PIVOT(SUM(qty) FOR custid IN(A, B, C, D)) AS P;
-
- SELECT * FROM dbo.EmpCustOrders;
现在要求每个雇员的每个客户返回一行,并具有相应的订单数量。即将7.2的结果反过来实现
7.3.1 使用标准SQL实现逆透视
逆透视的标准解决方案包括3个逻辑处理阶段:生成副本、提取元素和消除不相关的交叉点
第1步
为需要逆透视的每列生成对应副本。示例为代表客户ID的A,B,C,D列都生成对应的一个副本。在关系代数和SQL中,用于生成每行多个副本的运算是笛卡尔积(交叉联接)。需要在EmpCustOrders表和一个具有每个客户行的表之间应用交叉联接。
第2步
是生成一列(示例为qty),值从当前副本所代表客户的相应列返回。
第3步
消除不相关的交叉点。源表中是没有 NULL 标记的,所以可以第2步过滤掉包含NULL 标记的行。
- --1.第1步,下面每个源行分别生成4个副本,A,B,C,D分别对应一个副本
- SELECT empid, Custs.custid FROM dbo.EmpCustOrders
- CROSS JOIN (VALUES('A'),('B'),('C'),('D')) AS Custs(custid)
- --2.第2步
- SELECT empid, Custs.custid
- , CASE Custs.custid
- WHEN 'A' THEN A
- WHEN 'B' THEN B
- WHEN 'C' THEN C
- WHEN 'D' THEN D
- END AS qty
- FROM dbo.EmpCustOrders
- CROSS JOIN (VALUES('A'),('B'),('C'),('D')) AS Custs(custid)
- --3.第3步在源表中>>**NULL**<<代表的是不相关的交叉点。所以只要在第2步中添加 >>**WHERE qty IS NOT NULL**<< 子句就可以消除。
7.3.2 使用T-SQL UNPIVOT实现逆透视
T-SQL的 UNPIVOT 可完成实现逆透视的功能。与 PIVOT 相似,它操作一个源表(表表达式),为将要存储源列的值
的列分配一个名称(此处为qty
),为将要存储源列名称
分配一个名称(此为custid
),以及源列名称列表(A,B,C,D
作为custid
列的值)
- --语法:
- SELECT *
- FROM <source_table or table_expression>
- UNPIVOT (<target_col_tohold_source_col_values>
- FOR <target_col_to_hold_source_col_names> IN (<list_of_source_columns>)
- ) AS <alias>
-
- SELECT empid, custid, qty
- FROM EmpCustOrders
- UNPIVOT( qty FOR custid IN (A, B, C, D)) AS U
7.4 分组表
分组表就是用户所以分组的一个属性集。
--1.定义了分组集(empid,custid)
SELECT empid, custid, SUM(qty) AS sumqty
FROM dbo.Orders
GROUP BY empid, custid;
--2.分组集(empid)
SELECT empid, SUM(qty) AS sumqty
FROM dbo.Orders
GROUP BY empid;
--3.分组集(custid)
SELECT custid, SUM(qty) AS sumqty
FROM dbo.Orders
GROUP BY custid;
--4.空分组集
SELECT SUM(qty) AS sumqty
FROM dbo.Orders;
如果想要要统一这4个分组集,则可使用 UNION ALL
组合运算组合这4个结果集,但是由于集合运算符要求所有的结果集具有相同的列数。所以,为实现这个要求则需要有 null
替代缺少的列
。
SQL SERVER的GROUP BY
子句 GROUPING SETS
,CUBE
,ROLLUP
以及 GROUPING
和 GROUPING_ID
函数可以在同一查询中定义多个分组集。
7.4.1 GROUPING SETS 从属子句(GROUP BY)
主要用于报表和数据仓库。通过GROUPING SETS 从属子句可以在同一查询中定义多个分组集。只需在子句的括号内以逗号隔开,并且在每个分组集列出的成员在其括号内也要以逗号隔开。
SELECT empid, custid, SUM(qty) AS sumqty
FROM Orders
GROUP BY GROUPING SETS ((empid, custid),(empid),(custid),())
此查询在逻辑上等效于前面使用 UNION ALL
集合运算符的查询结果。与 UNION ALL
相比,使用GROUPING SETS
代码更简洁,而且SQL SERVER 会优化扫描源表的次数,而不是每使用一次UNION ALL
扫描一次源表,所以性能上应该有一定的提升。
7.4.2 CUBE从属子句(GROUP BY)
在 CUBE 从属子句的括号内,提供了一个以逗号分隔的成员列表后,会得到基于所定义的输入成员的所有可能分组集。例:CUBE(A,B)
等效于 GROUPING SETS((A,B),(A),(B),())
。 在集合理论中,能够从一个特定集合生成所有的元素子集的集合,称为幂集
SELECT empid, custid, SUM(qty) AS sumqty
FROM Orders
GROUP BY CUBE(empid, custid)
7.4.3 ROLLUP从属子句(GROUP BY)
与CUBE的不同,ROLLUP 假定输入成员之间是一个层次结构,并生成鉴于层次结构意义的所有分组集。即 CUBE(A,B,C)
根据3
个成员生成所有可能的8
个分组集,而ROLLUP(A,B,C)
仅生成4个分组集,其会假定A>B>c
,等效于 GROUPING SETS((A,B,C),(A,B),(A),())
7.4.4 GROUPING 和 GROUPING_ID 函数
当有一个定义了多个分组集的效查询时,如果想确定与每个结果行相关的分组集时,只要所有分组元素定义为 NOT NULL
就可以区分了。
如7.4.2使用CUBE(empid,custid)
得到的结果集。因为empid
和custid
列在表中定义成NOT NULL
,所以当这两列的值为NULL
时,说明该列没有参与当前分组集。如empid
和custid
都不为NULL
时是与分组集(empid,custid) 相关联,依次可以类推。
但是如果分组列在表中可以允许为NULL
时,那么就不可以直接根据NULL
来确定结果中的行是来源于源表还是仅仅分组集的NULL
占位符。
-
SQL SERVER 提供了GROUPING函数,函数的参数是传入一个分组列,如果该列是分组集成员返回
0
,否则返回1
SELECT
GROUPING(empid) AS grpemp
,GROUPING(custid) AS grpcust
,empid, custid, SUM(qty) AS sumqty
FROM Orders
GROUP BY CUBE(empid, custid)
运行结果:
若grpemp和grpcust都为0的所有行与*分组集(empid,custid)*相关联,类推。
-
SQL SERVER 提供的另一个GROUPING_ID函数,可以进一步简化结果行与分组集的关联处理。
如:GROUPING_ID(A,B,C,D)
,返回的结果像二进制的计算一样(0
代表是分组集成员,1
相反),分组集(A,B,C,D)
返回的值是0 (0*8+0*4+0*2+0*1=0)
,分组集(A,C) 则是5 (0*8+1*4+0*2+1*1)
SELECT
GROUPING_ID(empid, custid) AS grpid
,empid, custid, SUM(qty) AS sumqty
FROM Orders
GROUP BY CUBE(empid, custid)
运行结果:
练习
--1. 计算dbo.orders表每个客户订单的排名和密集排名,近custid,orderid,qty排序
SELECT custid, orderid, qty
,RANK() OVER (PARTITION BY custid ORDER BY qty) AS rnk
,DENSE_RANK() OVER (PARTITION BY custid ORDER BY qty) AS drnk
FROM dbo.Orders
/*
2. 计算每个客户订单的当前订单数量与该客户之前订单数量之间的差异,以及该客户下一个订单之间的差异
,表dbo.orders
*/
SELECT custid, orderid, qty
,qty - LAG(qty) OVER(PARTITION BY custid ORDER BY orderdate, orderid) AS diffprev
,qty - LEAD(qty) OVER (PARTITION BY custid ORDER BY orderdate, qty) AS diffnext
FROM dbo.Orders
/*
3. 返回每个雇员行,每个订单年度一列,以及每个雇员每年的订单数量
*/
--3.1 标准SQL
SELECT empid
,SUM(CASE WHEN YEAR(orderdate) = 2007 THEN 1 ELSE 0 END) cnt2007
,SUM(CASE WHEN YEAR(orderdate) = 2008 THEN 1 ELSE 0 END) cnt2008
,SUM(CASE WHEN YEAR(orderdate) = 2009 THEN 1 ELSE 0 END) cnt2009
FROM dbo.Orders
GROUP BY empid
--3.2 T-SQL
SELECT p.*
FROM (SELECT empid, YEAR(orderdate) AS orderyear FROM dbo.Orders) o
PIVOT(COUNT(o.orderyear)FOR orderyear IN([2007], [2008], [2009])) AS p
/*
4.先创建EmpYearOrders,初始化数据;将数据逆透视,每个雇员和订单年度一行,并带有订单数量。消除订单数量为0的行。(empid=3的2008年订单数为0)
*/
CREATE TABLE dbo.EmpYearOrders
(
empid INT NOT NULL
CONSTRAINT PK_EmpYearOrders PRIMARY KEY,
cnt2007 INT NULL,
cnt2008 INT NULL,
cnt2009 INT NULL
);
INSERT INTO dbo.EmpYearOrders(empid, cnt2007, cnt2008, cnt2009)
SELECT empid, [2007] AS cnt2007, [2008] AS cnt2008, [2009] AS cnt2009
FROM (SELECT empid, YEAR(orderdate) AS orderyear
FROM dbo.Orders) AS D
PIVOT(COUNT(orderyear)
FOR orderyear IN([2007], [2008], [2009])) AS P;
SELECT * FROM dbo.EmpYearOrders;
--------
SELECT empid, ordernums, RIGHT(orderyear,4) AS orderyear
FROM dbo.EmpYearOrders
UNPIVOT(ordernums FOR orderyear IN ([cnt2007],[cnt2008],[cnt2009])) AS up
WHERE ordernums > 0
/*
编写一个dbo.orders表的分组查询。分组集:(雇员,客户,订单年度),(雇员,订单年度),(客户,订单年度),返回总计订购数量。在结果中包含一列,标识当前行与相关联的分组集的关系
*/
SELECT
GROUPING_ID(empid, custid, YEAR(Orderdate)) AS groupingset,
empid, custid, YEAR(Orderdate) AS orderyear, SUM(qty) AS sumqty
FROM dbo.Orders
GROUP BY
GROUPING SETS
((empid, custid, YEAR(orderdate)), (empid, YEAR(orderdate)), (custid, YEAR(orderdate)));
2.
3.
4.
5.
返回顶部