SQL Server2012 T-SQL基础教程--读书笔记(5-7章)

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)

SqlServer
T-SQL

示例数据库:点我



Chapter 05 表表达式

表表达式(Table Expression)是一个命名的查询表达式,代表一个有效的关系表。表表达式没有任何的物理实例化,在查询表表达式时它们是虚拟的,内部查询是非嵌套的。即外部查询内部查询直接合并到一个对底层对象的查询中。


5.1 派生表

派生表(也称子查询表)是在外部查询的FROM 子句中定义的,它们存在的范围是外部查询。一旦外部查询完成后,派生表就消失了。

--语法
SELECT * FROM (SELECT * FROM Sales.Customers WHERE country = 'USA') AS USACusts

有效定义的表表达式的查询必须满足3个要求:

  1. 无法保证顺序。标准SQL是不允许 ORDER BY 子句出现在定义的表表达式查询中的,除非 ORDER BY 用于展示之外的其他目的。如:使用 OFFSET-FETCHTOP 筛选。

  2. 所有列必须具有名称。必须为所有列分配列别名。

  3. 所有列名必须是唯一的

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
SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1460552464640.jpg

递归成员联接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_refreshviewsp_refreshsqlmodule 来刷新 VIEW 的元数据,但是为了避免混淆,最好是通过 ALTER VIEW 来进行显式的添加或删除 TABLE 对应的列。

5.3.1 视图和ORDER BY 子句

用于展示的 ORDER BY 子句不允许出现在定义表表达式的查询中,因为关系表的行之间没有顺序可言。试图创建一个有序的VIEW是荒谬的,因为违反了关系模型定义的基本特性。
当然你可以通过 TOP(100)OFFSET 0 ROWSORDER BY 子句来创建VIEW。当查询VIEW时得到的结果可能会是有序的,但是这个结果是不确定的,这种情况是数据库优化造成的。所以,不要混淆用于定义表表达式和非定义表表达式查询的行为。

5.3.2 视图选项

当创建或更改视图时,可以指定作为视图定义一部分的视图属性和选项。在视图的头部,在 WITH 子句下面可以指定如ENCRYPTIONSCHEMABINDING属性,可以在查询的尾部指定WITH CHECK OPTION

1.ENCRYPTION选项
ENCRYPTION 可用于创建或更改 VIEWStored ProcedureTrigger用户定义函数(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'
SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1460552606921.jpg

2.SCHEMABINDING
可对VIEWUDF使用,它将被引用对象的架构和列绑定到引用对象的架构中。它指示不能删除被引用对象,也不能删除或修改被引用的列。

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
SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1460552677115.jpg

5.5 APPLY 运算符

APPLY 运算符支持 CROSS APPLYOUTER 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 Server2012 T-SQL基础教程--读书笔记(5-7章)
1460552692005.jpg

以上两个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
SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1460552719107.jpg

可以将表表达式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

运行结果:SQL Server2012 T-SQL基础教程--读书笔记(5-7章)


练习

--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.SQL Server2012 T-SQL基础教程--读书笔记(5-7章) 5.2SQL Server2012 T-SQL基础教程--读书笔记(5-7章)


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
SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456670207290.jpg

练习

  1. --1. 不使用循环结构生成一个1-10范围的10个数字虚拟辅助表,不需要保证顺序。 

  2. --2. 返回订单在200801而不是100802的客户和雇员,表Orders 

  3. SELECT o.custid, o.empid 

  4. FROM Sales.Orders o 

  5. WHERE o.orderdate >= '20080101' AND o.orderdate < '20080201' 

  6. EXCEPT 

  7. SELECT o.custid, o.empid 

  8. FROM Sales.Orders o 

  9. WHERE o.orderdate >= '20080201' AND o.orderdate < '20080301' 


  10. --3. 返回20080101和20080201均有订单的客户和雇员id 

  11. SELECT o.custid, o.empid 

  12. FROM Sales.Orders o 

  13. WHERE o.orderdate >= '20080101' AND o.orderdate < '20080201' 

  14. INTERSECT 

  15. SELECT o.custid, o.empid 

  16. FROM Sales.Orders o 

  17. WHERE o.orderdate >= '20080201' AND o.orderdate < '20080301' 


  18. --4. 返回订单活动在200801和200802,但是不在2007年的客户和雇员id对 



  19. SELECT o.custid, o.empid 

  20. FROM Sales.Orders o 

  21. WHERE o.orderdate >= '20080101' AND o.orderdate < '20080201' 

  22. INTERSECT 

  23. SELECT o.custid, o.empid 

  24. FROM Sales.Orders o 

  25. WHERE o.orderdate >= '20080201' AND o.orderdate < '20080301' 



  26. EXCEPT 

  27. SELECT custid, orderid 

  28. FROM Sales.Orders 

  29. WHERE orderdate >= '20070101' AND orderdate < '20080101' 


  30. --5.给出以下查询: 

  31. SELECT country, region, city, 1 AS sortNum 

  32. FROM HR.Employees 

  33. UNION ALL 

  34. SELECT country, region, city, 1 AS sortNum 

  35. FROM Production.Suppliers 

  36. --向查询中添加逻辑,保证输出中Employees返回的行在Suppliers返回的行之前,此外,在每一部分中,行按照country,region和city排序 

  37. WITH tmpCTE AS 



  38. SELECT country, region, city, 1 AS sortNum 

  39. FROM HR.Employees 

  40. UNION ALL 

  41. SELECT country, region, city, 0 AS sortNum 

  42. FROM Production.Suppliers 



  43. SELECT country, region, city 

  44. FROM tmpCTE 

  45. ORDER BY sortNum DESC,country, region, city 

5.SQL Server2012 T-SQL基础教程--读书笔记(5-7章)


CHAPTER 07 查询


7.1 开窗函数

开窗函数对基础函数行子集的计算,为子集中的每行计算一个标题结果值。行子集被称为“窗口”,它是基于与当前行相关的窗口描述符。开窗函数使用 OVER 子句指定窗口的规范。

  1. --返回每个雇员每月的运行总值 

  2. SELECT empid, ordermonth, val 

  3. ,SUM(val) OVER(PARTITION BY empid ORDER BY ordermonth ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS runVal 

  4. 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_NUMBERRANKDENSE_RANKNTILE

  1. SELECT  

  2. ROW_NUMBER() OVER ( ORDER BY val) AS rowNumber 

  3. ,RANK() OVER ( ORDER BY val) AS rank 

  4. ,DENSE_RANK() OVER ( ORDER BY val) AS denseRank 

  5. ,NTILE(100) OVER ( ORDER BY val) AS ntile 

  6. ,orderid,custid,val 

  7. FROM Sales.OrderValues 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456671190089.jpg

使用分区子句(PARTITION BY

  1. SELECT orderid,custid,val 

  2. ,ROW_NUMBER() OVER (PARTITION BY custid ORDER BY val) AS rowNumber 

  3. FROM Sales.OrderValues 

  4. ORDER BY 2,3 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456671237332.jpg

窗口排序不是用于展示的,并且不会改变结果的关系的本质。如果需要保证展示排序,则必须添加一个展示用的 ORDER BY 子句。
如CHAPTER 02 所示, SELECT 的开窗函数的计算是在 DISTINCT 子句之前的。在以上的OrderValues的830行中有795行是不重复的,如果需要直接使用 DISTINCTROW_NUMBER,则不可能去掉重复项,因为ROW_NUMBER函数是在 DISTINCT之前处理的,所以可以考虑在 GROUP BY 阶段进行去重处理。

  1. --1.结果返回830行 

  2. SELECT val  

  3. ,ROW_NUMBER() OVER (ORDER BY val) AS rowNumber  

  4. FROM Sales.OrderValues  


  5. --2. 

  6. SELECT val 

  7. ,ROW_NUMBER() OVER (ORDER BY val) AS rowNumber 

  8. FROM Sales.OrderValues 

  9. GROUP BY val 

  10. --或使用DENSE_RANK来处理 

  11. SELECT DISTINCT val 

  12. ,DENSE_RANK() OVER (ORDER BY val) AS rowNumber 

  13. FROM Sales.OrderValues 

  14. ORDER BY 1 

  1. SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
    1456671557234.jpg

    2. SQL Server2012 T-SQL基础教程--读书笔记(5-7章)

以上SQL中,GROUP BY 阶段为795个唯一值生成了795个组,然后 SELECT 为每个 val 组生成唯一行和基于 val 排序的行号。

更多详情请看这里

7.1.2 偏移开窗函数

偏移开窗函数允许从当前行的某个偏移量或者一个窗口框架的开关或结尾的行返回一个元素。SQL SERVER 212 支持4个偏移函数:LAGLEADFRISRT_VALUELAST_VALUE
LAG(英文指:后移)LEAD(英文指:前移) 函数支持窗口分区和窗口排序子句,这些与窗口框架没有相关性。允许基于指定排序,从分区内当前行的某个偏移量行获得一个元素。LAG 函数是在当前行之前查找,LEAD 则是之后查找。函数第1个参数是要返回的元素,第2个参数是偏移量(可选,默认为1),第3个参数是在请求的偏移量没有行返回的情况下的默认值(如果没指定,则为NULL

  1. /* 

  2. 查询OrderValues视图返回订单住处,对于每个客户的订单,使用>>**LAG**<<函数返回前一个客户订购值,使用>>**LEAD**<<返回下一个客户的订单值。 

  3. */ 

  4. SELECT custid, orderid, val 

  5. , LAG(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid) AS preVal 

  6. , LEAD(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid) AS nextVal 

  7. FROM Sales.OrderValues 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456672526211.jpg

FIRST_VALUELAST_VALUE 函数分别允许从窗口框架的第一行和最后一行返回元素。返回第一行使用的窗口框架应为:ROWS BETWWEEN UNBOUNDED PRECEDING AND CURRENT ROWFIRST_VALUE,返回最后一行为:ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWINGLAST_VALUE

  1. SELECT custid, orderid, val 

  2. ,FIRST_VALUE(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS firstVal 

  3. ,LAST_VALUE(val) OVER(PARTITION BY custid ORDER BY orderdate, orderid ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS lastVal 

  4. FROM Sales.OrderValues 

  5. ORDER BY custid, orderdate, orderid 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456673016525.jpg

7.1.3 聚合开窗函数

在SQL SERVER 2012之前,窗口聚合函数仅支持窗口分区子句。在这版本之后,T-SQL支持窗口排序框架子句。
使用OVER子句会为函数公开一个基于查询结果集合所有行的窗口。SUM(val) OVER()会返回所有值的总值。如果添加了窗口分区子句,那么就会为函数公开一个限定窗口,只有那些基础查询结果集合*享分区元素中相同的值的行作为当前行。举例来说:SUM(val) OVER(PARTITION BY custid)会返回当前客户的总值。
下面的查询不涉及排序和框架。

  1. --1 

  2. SELECT orderid, custid, val 

  3. ,SUM(val) OVER() AS totalVal  

  4. ,SUM(val) OVER(PARTITION BY custid) AS custTotalVal 

  5. FROM Sales.OrderValues 

  6. GROUP BY orderid, custid, val 

  7. ------------------------------------------------------------------- 

  8. --2 以下查询可以使用SUM(val) OVER()在SELECT中直接使用而不必先查询出总值赋值给变量,再进行使用 

  9. SELECT SUM(val) AS total FROM Sales.OrderValues 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456673942296.jpg

SQL SERVER 2012的窗口聚合函数现在也支持窗口排序和框架子句,这能够允许进行像运动和移运动聚合、YTD计算等更复杂的计算。

  1. SELECT empid, ordermonth, val 

  2. ,SUM(val) OVER( PARTITION BY empid  

  3. ORDER BY ordermonth  

  4. ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS runval 

  5. FROM Sales.EmpOrders 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456674628226.jpg

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个逻辑处理阶段以及相关的元素:一个与分组相关的分组阶段或是行元素(分组):一个与扩展相关的扩展阶段或是列元素(扩展):以一个聚合元素和聚合函数相关的聚合阶段(聚合)。

  1. --创建并插入值 

  2. CREATE TABLE dbo.Orders 



  3. orderid INT NOT NULL, 

  4. orderdate DATE NOT NULL, 

  5. empid INT NOT NULL, 

  6. custid VARCHAR(5) NOT NULL, 

  7. qty INT NOT NULL, 

  8. CONSTRAINT PK_Orders PRIMARY KEY(orderid) 

  9. ); 


  10. INSERT INTO dbo.Orders(orderid, orderdate, empid, custid, qty) 

  11. VALUES 

  12. (30001, '20070802', 3, 'A', 10), 

  13. (10001, '20071224', 2, 'A', 12), 

  14. (10005, '20071224', 1, 'B', 20), 

  15. (40001, '20080109', 2, 'A', 40), 

  16. (10006, '20080118', 1, 'C', 14), 

  17. (20001, '20080212', 2, 'B', 12), 

  18. (40005, '20090212', 3, 'A', 10), 

  19. (20002, '20090216', 1, 'C', 20), 

  20. (30003, '20090418', 2, 'B', 15), 

  21. (30004, '20070418', 3, 'C', 22), 

  22. (30007, '20090907', 3, 'D', 30); 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456674996765.jpg
  1. /* 

  2. 查询返回每个雇员的每个客户生成总订单数量。 

  3. */ 

  4. SELECT empid, custid, SUM(qty) AS sumqty 

  5. FROM dbo.Orders 

  6. GROUP BY empid, custid 

  7. ORDER BY empid, custid 

  8. --或使用SUM() OVER()分区计算总值后去重 

  9. SELECT DISTINCT empid, custid, SUM(qty) OVER(PARTITION BY empid, custid)sumqty 

  10. FROM dbo.Orders 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456675474436.jpg

但是现在想将以上结果进行旋转,即实现的查询输出结果如下图:
SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
如上图所示,将dbo.Orders表中的数据聚合后旋转和透视视图,生成该数据视图的技术叫做透视
每个透视请求涉及3个逻辑处理阶段以及与之相关的元素:一个与分组相关的分组阶段或是行元素,一个与扩展相关的扩展阶段或是列元素,以及一个与聚合元素和聚合函数相关的聚合阶段。
最后,由于透视涉及分组,需要聚合数据生成分组和扩展元素“交叉口”的结果值,需要确定聚合函数和聚合元素。

7.2.1 使用标准SQL透视

  1. 分组阶段使用 GROUP BY 子句实现

  2. 扩展阶段在 SELECT 子句中使用 CASE 表达式实现,这需要提前知道扩展元素并为每个元素指定单独的表达式。

  3. 聚合阶段是通过对每个 CASE 表达式应用相关的聚合函数实现。

  1. /* 

  2. CASE后不指定ELSE时,默认返回 ELSE NULL 

  3. */ 

  4. SELECT empid 

  5. ,SUM(CASE WHEN custid = 'A' THEN qty END) AS A 

  6. ,SUM(CASE WHEN custid = 'B' THEN qty END) AS B 

  7. ,SUM(CASE WHEN custid = 'C' THEN qty END) AS C 

  8. ,SUM(CASE WHEN custid = 'D' THEN qty END) AS D 

  9. FROM orders 

  10. GROUP BY empid 

如果你不知道需要扩展的值,并且要从数据中查询它们,你需要动态SQL来构建查询字符串并执行它。第10章会涉及到这方面。

7.2.2 使用T-SQL PIVOT运算符透视

PIVOT 是T-SQL特有的表运算符,在查询 FROM 子句上下文中操作(像JOIN一样)。它对一个源表或是表表达式进行操作,透视数据并返回一个结果表。
PIVOT 也涉及3个逻辑处理阶段。但它的语法有所不同:

  1. --语法 

  2. SELECT ... 

  3. FROM soureTable 

  4. PIVOT( <agg_func>(<aggregation_element>) 

  5. 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. --1.custid作为扩展元素 

  2. SELECT empid,A,B,C,D 

  3. FROM (SELECT empid, custid, qty FROM orders) d 

  4. PIVOT(SUM(qty)FOR custid IN(A, B, C, D)) AS p 


  5. --2.empid作为扩展元素  

  6. SELECT [1],[2],[3] 

  7. FROM (SELECT empid, custid, qty FROM orders) d 

  8. PIVOT(SUM(d.qty) FOR empid IN([1],[2],[3])) d 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456676974170.jpg

建议不要直接操作基表,即使表中仅包含用途透视的列,因为当需求改变时你的基表可能会添加新列,造成透视结果不符合预期。所以推荐使用表表达式

7.3 逆透视数据

逆透视就是将数据从状态旋转成为状态。从每个源行生成多个结果行,每行具有一个不同的源列值。

  1. CREATE TABLE dbo.EmpCustOrders 



  2. empid INT NOT NULL 

  3. CONSTRAINT PK_EmpCustOrders PRIMARY KEY, 

  4. A VARCHAR(5) NULL, 

  5. B VARCHAR(5) NULL, 

  6. C VARCHAR(5) NULL, 

  7. D VARCHAR(5) NULL 

  8. ); 

  9. INSERT INTO dbo.EmpCustOrders(empid, A, B, C, D) 

  10. SELECT empid, A, B, C, D 

  11. FROM (SELECT empid, custid, qty 

  12. FROM dbo.Orders) AS D 

  13. PIVOT(SUM(qty) FOR custid IN(A, B, C, D)) AS P; 


  14. SELECT * FROM dbo.EmpCustOrders; 

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456758014167.jpg

现在要求每个雇员的每个客户返回一行,并具有相应的订单数量。即将7.2的结果反过来实现

7.3.1 使用标准SQL实现逆透视

逆透视的标准解决方案包括3个逻辑处理阶段:生成副本、提取元素和消除不相关的交叉点
第1步为需要逆透视的每列生成对应副本。示例为代表客户ID的A,B,C,D列都生成对应的一个副本。在关系代数和SQL中,用于生成每行多个副本的运算是笛卡尔积(交叉联接)。需要在EmpCustOrders表和一个具有每个客户行的表之间应用交叉联接。
第2步是生成一列(示例为qty),值从当前副本所代表客户的相应列返回。
第3步消除不相关的交叉点。源表中是没有 NULL 标记的,所以可以第2步过滤掉包含NULL 标记的行。

  1. --1.第1步,下面每个源行分别生成4个副本,A,B,C,D分别对应一个副本 

  2. SELECT empid, Custs.custid FROM dbo.EmpCustOrders  

  3. CROSS JOIN (VALUES('A'),('B'),('C'),('D')) AS Custs(custid) 

  4. --2.第2步 

  5. SELECT empid, Custs.custid 

  6. , CASE Custs.custid 

  7. WHEN 'A' THEN A 

  8. WHEN 'B' THEN B 

  9. WHEN 'C' THEN C 

  10. WHEN 'D' THEN D 

  11. END AS qty 

  12. FROM dbo.EmpCustOrders  

  13. CROSS JOIN (VALUES('A'),('B'),('C'),('D')) AS Custs(custid) 

  14. --3.第3步在源表中>>**NULL**<<代表的是不相关的交叉点。所以只要在第2步中添加 >>**WHERE qty IS NOT NULL**<< 子句就可以消除。 

  1. SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
    1456758154188.jpg

    2. SQL Server2012 T-SQL基础教程--读书笔记(5-7章)

7.3.2 使用T-SQL UNPIVOT实现逆透视

T-SQL的 UNPIVOT 可完成实现逆透视的功能。与 PIVOT 相似,它操作一个源表(表表达式),为将要存储源列的的列分配一个名称(此处为qty),为将要存储源列名称分配一个名称(此为custid),以及源列名称列表(A,B,C,D作为custid列的值)

  1. --语法: 

  2. SELECT * 

  3. FROM <source_table or table_expression>  

  4. UNPIVOT (<target_col_tohold_source_col_values> 

  5. FOR <target_col_to_hold_source_col_names> IN (<list_of_source_columns>) 

  6. ) AS <alias> 


  7. SELECT empid, custid, qty 

  8. FROM EmpCustOrders 

  9. UNPIVOT( qty FOR custid IN (A, B, C, D)) AS U  

SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456759016501.jpg

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 Server2012 T-SQL基础教程--读书笔记(5-7章)
SQL SERVER的GROUP BY子句 GROUPING SETS,CUBE,ROLLUP 以及 GROUPINGGROUPING_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)
SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
1456636457506.jpg

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)得到的结果集。因为empidcustid列在表中定义成NOT NULL,所以当这两列的值为NULL时,说明该列没有参与当前分组集。如empidcustid都不为NULL时是与分组集(empid,custid) 相关联,依次可以类推。
但是如果分组列在表中可以允许为NULL时,那么就不可以直接根据NULL来确定结果中的行是来源于源表还是仅仅分组集的NULL占位符。

  1. 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)

运行结果:SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
grpempgrpcust都为0的所有行与*分组集(empid,custid)*相关联,类推。

  1. 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)

运行结果:SQL Server2012 T-SQL基础教程--读书笔记(5-7章)


练习

--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.SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
3.SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
4.SQL Server2012 T-SQL基础教程--读书笔记(5-7章)
5.SQL Server2012 T-SQL基础教程--读书笔记(5-7章)


返回顶部
上一篇:mysql优化技巧《转》


下一篇:Python基础知识总结笔记(四)函数