码迷,mamicode.com
首页 > 数据库 > 详细

SQL Server2012 T-SQL基础教程--读书笔记

时间:2016-02-22 20:50:16      阅读:385      评论:0      收藏:0      [点我收藏+]

标签:

SQL Server2012 T-SQL基础教程--读书笔记


Chapter 01 T-SQL 查询和编程背景

1.3 创建表和定义数据的完整性

1.3.1 创建表

  1. USE TSQL2012; 
  2. IF OBJECT_ID(‘dbo.Employees‘, ‘U‘) IS NOT NULL 
  3. DROP TABLE dbo.Employees; 
  4.  
  5. CREATE TABLE dbo.Employees 
  6. ( 
  7. empid INT NOT NULL, 
  8. firstname VARCHAR(30) NOT NULL, 
  9. lastname VARCHAR(30) NOT NULL, 
  10. hiredate DATE NOT NULL, 
  11. mgrid INT NULL, 
  12. ssn VARCHAR(20) NOT NULL, 
  13. salary MONEY NOT NULL 
  14. ); 

在创建对象脚本中包含USE语句是十分重要,能确保在指定的数据库中创建对象。

1.3.2 定义数据的完整性

1. 主键约束

主键约束强制行的唯一性,在约束的属性中不允许使用NULL标记。约束的属性的值必须是唯一的,每个表只能有一个主键。为了强制逻辑主键约束的一唯一性,SQL Server会在后台创建一个唯一索引。唯一索引是SQL Server为了强制唯一性所使用的一种物理机制。

  1. ALTER TABLE dbo.Employees 
  2. ADD CONSTRAINT PK_Employees 
  3. PRIMARY KEY(empid) 

2. 唯一约束

唯一约束强制行的唯一性,允许你在自己的数据库中实现关系模型的备用键概念。与主键不同,可以在同一个表内定义多个唯一约束,此外,唯一约束不限制列必须定义为NOT NULL。根据SQL标准,具有唯一约束的列应该允许重复的NULL值。但是,SQL Server则不允许重复的NULL标记

  1. ALTER TABLE dbo.Employees 
  2. ADD CONSTRAINT UNQ_Employees_ssn 
  3. UNIQUE(ssn); 

3. 外键束约

外键用于强制引用的完整性,此约束定义了引用表中的一个或多个属性指向被引用表(父表)中候选键(主键或唯一约束),引用表和被引用表可以是同一个。外键的目的是限制在外键列中允许的值要存在于那些被引用列中。

  1. IF OBJECT_ID(‘dbo.Orders‘, ‘U‘) IS NOT NULL 
  2. DROP TABLE dbo.Orders; 
  3. CREATE TABLE dbo.Orders 
  4. ( 
  5. orderid INT NOT NULL, 
  6. empid INT NOT NULL, 
  7. custid VARCHAR(10) NOT NULL, 
  8. orderts DATETIME2 NOT NULL, 
  9. qty INT NOT NULL, 
  10. CONSTRAINT PK_Orders 
  11. PRIMARY KEY(orderid) 
  12. ); 
  13.  
  14. ALTER TABLE dbo.Orders 
  15. ADD CONSTRAINT FK_Orders_Employees 
  16. FOREIGN KEY(empid) 
  17. REFERENCES dbo.Employees(empid) 

如果Orders表中的订单引用了Employees中的某一个雇员行,当尝试从Employees中删除这一雇员行时,RDBMS会拒绝删除并抛出错误。

4. CHECK约束

CHECK允许定义一个谓词,确保进入到表中的行或是被修改的行必须满足些约束。

  1. ALTER TABLE dbo.Employees 
  2. ADD CONSTRAINT CHK_Employees_salary 
  3. CHECK(salary > 0); 

5. 默认约束

如果没有在插入时指定一个显式值时,将会使用该默认值。

  1. ALTER TABLE dbo.Employees 
  2. ADD CONSTRAINT DFT_Orders_orders 
  3. DEFAULT(SYSDATETIME()) FOR orders; 

Chapter 02 单表查询

2.1 SELECT 语句元素

SELECT语句的查询顺序:

  1. FROM

  2. WHERE

  3. GROUP BY

  4. HAVING

  5. SELECT

  6. ORDER BY

2.1.7 TOP和OFFSET-FETCH

1. TOP筛选

指定10个最近的订单:

  1. SELECT TOP 10 orderid, orderdate, custid, empid  
  2. FROM Sales.Orders 
  3. ORDER BY orderdate DESC; 

可以为TOP选项指定PERCENT关键字,向上舍入。

  1. SELECT TOP (1) PERCENT orderid, orderdate, custid, empid  
  2. FROM Sales.Orders 
  3. ORDER BY orderdate DESC 

Orders中总共有830行,830 * 1%向上舍入则是9行。
我们可以发现,在前4行的数据中,orderdate是相相同的,在这种没有指定决胜属性(tiebreaker)的情况下,对具有相同值的orderdate排序是没有意义的。SQL Server返回的结果是不确定的,即哪行先被物理访问到就先返回哪行。
如果希望查询结果是确定的,则需要ORDER BY列出的数据是唯一的,即要添加一个决胜发展(tiebreaker)。如可在ORDER BY加入orderid作为tiebreaker

  1. SELECT TOP(5) orderid, orderdate, custid, empid  
  2. FROM Sales.Orders 
  3. ORDER BY orderdate DESC, orderid DESC; 

或者使用WITH TIES作为关联行,来实现tiebreaker

  1. SELECT TOP(5) WITH TIES orderid, orderdate, custid, empid  
  2. FROM Sales.Orders 
  3. ORDER BY orderdate DESC, orderid DESC; 

注意,即使指定了TOP 5,但是输出却是8行。SQL Server首先返回基于orderdate DESC排序的TOP 5行,然后是返回与检索到的5行中最后一行orderdate值相同的其他所有行。

2. OFFSET-FETCH

TOP不是标准SQL,并且不支持跳过功能。标准的SQL定义的TOP类似筛选称为为OFFSET-FETCH,支持跳过功能。SQL SERVER 2012中的OFFSET-FETCH被视为ORDER BY子句的一部分,通常用于实现按顺序
显示效果。OFFSET子句指定要跳过的行数,FETCH子句指定在跳过的行数后要筛选的行数。

  1. SELECT orderid, orderdate, custid, empid 
  2. FROM Sales.Orders 
  3. ORDER BY orderdate, orderid 
  4. OFFSET 50 ROWS FETCH NEXT 25 ROWS ONLY; 

基于排序好的结果,OFFSET子句跳过50行,由FETCH子句往后取25行。
注意,使用OFFSET-FETCH的查询必须具有ORDER BY子句。此外FETCH子句不支持没有OFFSET子句。不过,没有FETCHOFFSET是允许的,这种情况就是跳过多少行,并返回剩余的所有行。而且ROWROWS是可以互换的。

2.1.8 开窗函数速览

开窗函数的功能是:对于基本的每一行,按行的窗口(组)进行运算,并计算一个标量(单个)结果值,行的窗口使用OVER子句定义。OVER子句可以使用PARTITION BY子子句约束窗口中的行,并且可以使用ORDER BY子子名为计算结果定义排序。

  1. SELECT orderid, custid, val 
  2. , ROW_NUMBER() OVER(PARTITION BY custid ORDER BY val) AS rownum 
  3. FROM Sales.OrderValues 
  4. ORDER BY custid,val; 

ROW_NUMBER函数对于查询结果按custid进行分区,并在各个分区内按照指定的val进行排序,分配了唯一、递增、连续的整数。
注意,ROW_NUMBER函数必须在每个分区内生成唯一值 。这意味着即使排序值不增加,行号也会增加。
SELECT列表中的表达式是在DISTINCT子句之前计算的,总之,sql语句的执行顺序为:

  • FROM

  • WHERE

  • GROUP BY

  • HAVING

  • SELECT

    • 表达式

    • DISTINCT

  • ORDER BY

    • TOP/OFFSET-FETCH


2.2 谓词和运算符


2.3 CASE表达式

CASE是一个标题表达式,返回一个基于条件置逻辑的值。注意,CASE是表达式而不是语句。所以,它不支持你控制活动流或是做一些基于条件逻辑的损伤。可以在诸如:SELECTWHEREHAVINGORDER BY子句中以及在CHECK中使用。CASE具有简单和复杂格式,ELSE子句默认值为NULL
简单格式:

  1. SELECT productid,productname, categoryid 
  2. ,CASE categoryid 
  3. WHEN 1 THEN ‘Beverages‘ 
  4. WHEN 2 THEN ‘Condiments‘ 
  5. WHEN 3 THEN ‘Confections‘ 
  6. WHEN 4 THEN ‘Dairy Products‘ 
  7. ELSE ‘Others‘ 
  8. END AS categoryname 
  9. FROM Production.Products 
  10. ORDER BY categoryid 

复杂格式:

  1. SELECT orderid, custid, val, 
  2. CASE 
  3. WHEN val < 1000 THEN ‘Less than 1000‘ 
  4. WHEN val > 1000 THEN ‘More than 1000‘  
  5. ELSE ‘Others‘ 
  6. END AS category 
  7. FROM Sales.OrderValues 

T-SQL 中某些函数可以看作是CASE的表达式缩写,如:ISNULLCOALESCEIIFCHOOSE*,其中只有COALESCE是标准的,后两个仅在2012版本中可以使用


2.5 同时操作

  1. SELECT col1, col2 FROM dbo.test WHERE col1 <> 0 AND col2/col > 2 

这是一个col2/col1大于2的查询语句,很有可能假定SQL Server 会从左到右计算此表达式。从而使得col=0时会出现短路,从而不继续进行col2/col1的计算。但是,SQL Server 支持短路,这是基于SQL标准的同时操作概念。SQL Server通常基于成本估计来进行表达式的计算顺序。所以此语句有可能会查询失败。对于此种特定情况,该语句可改写为

  1. SELECT col1, col2 FROM dbo.test WHERE (col1<0 AND col2> *col1) OR (col1<0 AND col2<2*col1) 

2.6 运算符和函数##

2.6.1 字符串连接(+和CONCAT函数)

使用*+运算符作为字符连接时,如果连接符中有数字类型需要使用CONVERT*,CAST数字类型转换为字符类型,否则可能会因字符类型不能转换为数字类型而报错,这是因为SQL Server会隐式的将其他字符类型转换为数字类型

  1. SELECT CONCAT(1,‘/‘,5,‘a‘) --query success: 1/5a 
  2. SELECT 1+‘a‘ --execute fail 

2.6.2 SUBSTRING 函数

SUBSTRING(string, start, length) 在sql中,string字符串下标从1开始算起,而不是0。

  1. SELECT SUBSTRING(‘ABCD‘,1,2) --AB  

2.6.3 LEFT 和 RIGHT函数

LEFT(string, n),RIGHT(string, n),n代表从LEFTRIGHT提取的字符数

  1. SELECT LEFT(‘ABCD‘,2) --AB RIGHT(‘ABCD‘,2) --CD 

2.6.4 LEN 和 DATALENGTH 函数

LENG(string) 返回字符数(或者长度),并且会删除字符串最后的空格.
DATALENG(string) 返回的是字节数

  1. SELECT LEN(N‘ABCD‘) --4 DATALENGTH(N‘ABCD‘) --8 

2.6.5 CHARINDEX 函数

CHARINDEX(subString, string[,start_pos]) 返回字符串subStringstart_pos开始在string中第一次出现的位置。如果找不到指定的字符串,返回0

  1. SELECT CHARINDEX(‘cd‘,‘CDABCDEFG‘,2) --5 

2.6.6 PATINDEX 函数

PATINDEX(pattern,string) patternLIKE 模式相同

  1. SELECT PATINDEX(‘%[0-9]%‘,‘abcde53fg‘) --6 

2.6.7 REPLACE 函数

REPLACE(string, subString1, subString2) 使用subString2取代string中所有的subString1

  1. SELECT REPLACE(‘abcde53fg53‘,‘53‘,‘11‘) --abcde11fg11 

2.6.8 REPLICATE 函数

REPLICATE(string, n)string复制n

  1. SELECT REPLICATE(‘abc‘,3) --abcabcabc 

2.6.9 STUFF 函数

STUFF(string, pos, del_length, insertString)pos位置开始,删除指定del_length长度的字符串,并从pos处插入insertString字符串

  1. SELECT STUFF(‘abcdefg‘,3,4,1234) --ab1234g 

2.6.10 UPPER 和 LOWER 函数

返回大小写或小写字符串

2.6.11 RTRIM 和 LRITM 函数

从右边或左边删句末或句首的空格

2.6.12 FORMAT 函数

FORMAT(input, format_string, culture) 按照.NET格式和一个可选的区域参数,将input格式化成一个字符串

pos位置开始,删除指定del_length长度的字符串,并从pos处插入insertString字符串

  1. SELECT FORMAT(153,‘0000‘) --0153 

2.6.13 LIKE 谓词

  1. % 通配符,代表任意字符

  2. _ 通配符,表示单个字符

  3. [] 通配符,代表单个字符必须是指定[]内的字符之一

  4. [^] 通配符,代表指定的字符不在指定的字符列表或范围内。

  5. ESCAPE 字符,要查找上面定义的通配符时,需要使用转义字符。如要查找_col LIKE ‘%!_%‘ESCAPE‘!‘col LIKE ‘%[_]%‘


2.7 时间和日期

在筛选列上应用操作时,数据库不能以有效方式使用索引,所以如果要筛选日期范围时可使用如col >= 20160101 AND col < 20080101来代替YEAR(col) = 2007

2.7.1 当前时间

  1. SELECT 
  2. GETDATE() --2016-01-28 23:37:29.447  
  3. ,CURRENT_TIMESTAMP --2016-01-28 23:37:29.447 
  4. ,GETUTCDATE() --2016-01-28 15:37:29.447 
  5. ,SYSDATETIME() --2016-01-28 23:37:29.4810323 
  6. ,SYSUTCDATETIME() --2016-01-28 15:37:29.4810323 
  7. ,SYSDATETIMEOFFSET()--2016-01-28 23:37:29.4810323 +08:00 

2.7.2 CAST、CONVERT和PARSE函数,及其TRY_对应函数

输入值如果转换成功,则返回目标值,否则就会查询失败。其对应的TRY_函数执行的是相同的操作,只是如果查询失败返回的是NULL**。
语法:
CAST(val AS datatype)
CONVERT(dataType, val [,sytle_number])
PARSE(val AS dataType [USING culture])

  1. SELECT PARSE(‘02/12/2016‘ AS DATETIME USING ‘en-us‘),PARSE(‘02/12/2016‘ AS DATETIME USING ‘en-gb‘) 

2.7.3 SWITCHOFFSET 函数

SEITCHOFFSET(datatimeoffset_val, time_zone) 该函数将输入的datatimeoffset_val值调整为指定的时区

  1. --将时区调整为-05:00 
  2. SELECT SYSDATETIMEOFFSET(), SWITCHOFFSET(SYSDATETIMEOFFSET(), ‘-05:00‘) 

2.7.4 TODATETIMEOFFSET 函数

TODATETIMEOFFSET(date_and_time_value,time_zone) 设置date_and_time_valuetime_zone时区,通常用于迁移非已知偏移量数据到已知偏移数据。

  1. --Sunday, January 31, 2016 8:42:00 PM +08:00 
  2. SELECT TODATETIMEOFFSET(CONVERT(DATETIME,‘20160131 20:42‘),‘+08:00‘) 

2.7.5 DATEADD 函数

DATEADD(part,n,dt_val) 指定日期的某个部分 part 增加 ndt_val

  • part:YEAR MONTH QUARTER DAY DAYOFYEAR WEEK WEEKDAY HOUR MINUTE SECOND MILLISECOND MICROSECOND NANOSECOND,也可以使用缩写形式,如yy代替year

  • n:可以是正、负数

  1. --4/30/2016 9:00:52 PM 1/31/2016 9:00:52 PM 
  2. SELECT DATEADD(QUARTER,1,SYSDATETIME()), SYSDATETIME() 

2.7.6 DATEDIFF 函数

DATEDIFF(part, dt_val1, dt_val2) 返回 dt_val1dt_val2 之间指定日期部分 part 的间隔

2.7.7 DATEPART 、YEAR、MONTH、DAY和DATENAME 函数

DATEPART(part, dt_val) 返回 dt_val 指定日期部分 part 的值。可以使用 YEAR()MONTH(),**DAY()代替
DATETIME(part, dt_val) 此函数和
DATEPART
相似,但是它返回的是对应日期部分的名称,而不是数字。如:

  1. --January  
  2. SELECT DATENAME(MONTH, SYSDATETIME()) 

2.7.7 ISDATE 函数

ISDATE(string) 判断 string 是否为日期格式可以转换为日期格式数据,可以则返回1,否则返回0

2.7.8 FORMPARTS 函数

DATEFORMPARTS(year, month, day)
DATETIME2FORMPARTS(year, month, day,hour,minute,seconds,fractions,precisions)
DATETIMEFORMPARTS(year, month, day,hour,minute,seconds.millseconds)
DATETIMEOFFSETFORMPARTS(year, month, day,hour,minute,seconds,fractions,hour_offset,minute_offset,precision)
SMALLDATETIMEFORMPARTS(year, month, day,hour,minute)
TIMEFORMPARTS(hour,minute,seconds,fractions,precisions)
这是SQL Server 2012中引入的,它们接受代表日期和时间各个部分的整数值,并根据这些值构建一个所请求类型的值。

  1. SELECT DATEFROMPARTS(2016,01,31) --2016-01-31 
  2. ,DATETIMEFROMPARTS(2016,01,31,22,30,00,00) --2016-01-31 22:30:00.000 
  3. ,DATETIMEOFFSETFROMPARTS(2016,01,31,22,30,00,1,8,4,3)  
  4. --2016-01-31 22:30:00.001 +08:04 

2.7.9 EOMONTH 函数

EOMONTH(input[,months_to_add])
这是2012引入的函数。它接受一个日期和时间值输入,并返回相应的每个月的最后一天,作为 DATE 数据类型。第2个可选参数指示要增加多少个月。

  1. SELECT EOMONTH(DATEADD(mm,1,SYSDATETIME())) --2016-02-29 

2.8 查询元数据

2.8.1 目录视图

  1. --查询所有Schema.Table 
  2. SELECT SCHEMA_NAME(SCHEMA_ID) AS tableSchemaName, name AS tableName FROM sys.tables 
  3. --查询Sales.Orders表的相关column 
  4. SELECT name AS colName, TYPE_NAME(system_type_id) AS colType, max_length AS length FROM sys.columns WHERE object_id = OBJECT_ID(‘Sales.Orders‘) 

2.8.2 信息架构视图

信息架构视图是一个视图合集,其位于 INFORMATION_SCHEMA 的架构中,并以标准方式提供元数据信息。

  1. --查询所有的基本表 
  2. SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = ‘BASE TABLE‘ 
  3.  
  4. --查询 Sales.Orders 所有的column 
  5. SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ‘Sales‘ AND TABLE_NAME = ‘orders‘  

2.8.3 系统存储过程和函数

  1. --返回当前数据库中能够查询到的对象列表(表、视图等) 
  2. EXEC sys.sp_tables 
  3. --返回orders表的详细信息 
  4. EXEC sys.sp_help @obejctName = ‘Sales.Orders‘  
  5. --返回orders表的column 
  6. EXEC sys.sp_columns @table_name = ‘orders‘, @schema_name = ‘Sales‘ 
  7. --返回orders表的约束信息,参数必须包含schema 
  8. EXEC sys.sp_helpconstraint ‘Sales.orders‘ 
  9. --返回指定数据为所请求的属性信息  
  10. SELECT DATABASEPROPERTYEX(N‘TSQL2012‘,‘Collation‘) 
  11. --返回指定对象名称的所请求属性信息 
  12. SELECT OBJECTPROPERTY(OBJECT_ID(‘Sales.Orders‘),‘TableHasPrimaryKey‘) 
  13. --返回指定column所请求的信息 
  14. SELECT COLUMNPROPERTY(OBJECT_ID(‘Sales.Orders‘),‘orderdate‘,‘AllowsNull‘) 

练习

  1. --1.查询2007年6月份订单(30 rows) 
  2. SELECT * FROM Sales.orders WHERE orderdate >= ‘20070601‘ AND orderdate < ‘20070701‘ ORDER BY orderdate 
  3.  
  4. --2.查询每月最后一天的订单(26 rows) 
  5. SELECT * FROM Sales.Orders  
  6. WHERE orderdate = EOMONTH(orderdate)  
  7. ORDER BY orderdate  
  8.  
  9. --3.查询HR.Employees表中姓氏包含‘a’两次以上的雇员(1 rows) 
  10. SELECT * FROM HR.Employees WHERE lastname LIKE ‘%a%a%‘ 
  11.  
  12. --4.查询Sales.OrderDetails,返回总价(qty*unitprice)大于10000的订单 
  13. SELECT orderid, SUM(unitprice * qty) AS total  
  14. FROM Sales.OrderDetails  
  15. GROUP BY orderid  
  16. HAVING SUM(unitprice*qty) > 10000 
  17.  
  18. --5.查询Sales.Orders,返回2007年中平均运费最高的3个国家 
  19. SELECT TOP 3 shipcountry, AVG(freight) AS avgFreight1,SUM(freight)/COUNT(*) AS avgFreight2  
  20. FROM Sales.Orders  
  21. WHERE orderdate >= ‘20070101‘ AND orderdate < ‘20080101‘  
  22. GROUP BY shipcountry ORDER BY 3 DESC 
  23.  
  24. --6.查询Sales.Orders,分别对每个客户的订单按订单日期排序(orderid作为决胜属性),计算订单编号  
  25. SELECT custid,orderid, orderdate, ROW_NUMBER() OVER (PARTITION BY custid ORDER BY orderdate,orderid ) rownum FROM Sales.orders 
  26.  
  27. --7.查询HR.Employees,推测每个雇员性别。"Ms."和"Mrs.":Female,"Mr.":male,其他:Unknown 
  28. SELECT *,  
  29. CASE  
  30. WHEN titleofcourtesy=‘Mrtitleofcourtesy‘ THEN ‘Male‘  
  31. WHEN titleofcourtesy in(‘Mrs.‘,‘Ms.‘) THEN ‘Female‘  
  32. ELSE ‘Unknown‘ END AS gender  
  33. FROM HR.Employees  
  34.  
  35. --8.查询Sales.Customers,返回客户的ID和地区,按地区排序,具有null的最后输出(null值默认是先输出) 
  36. SELECT custid, region FROM Sales.Customers  
  37. ORDER BY  
  38. CASE WHEN region IS NULL THEN 1 ELSE 0 END, region  

CHAPTER 03 联接

联接的3个基本类型是交叉联接内部联接外部联接交叉联接仅处理阶段——笛卡尔乘积,内部联接应用两个阶段——笛卡尔乘积和筛选,外部联接应用三个阶段——笛卡尔乘积、筛选和添加外部行。

3.1交叉联接

3.1.1 ANSI SQL-92 和 89 语法

两种语法没有逻辑或性能上的差异,但推荐使用92的SQL语法。

  1. --92 
  2. SELECT custid, empid FROM Sales.Customers CROSS JOIN HR.Employees ORDER BY 1,2 
  3. --89 
  4. SELECT custid, empid FROM Sales.Customers ,HR.Employees ORDER BY 1,2 

3.1.2 自交叉联接

可以对一个表的多个实例进行联接,支持联接的基本类型,这功能就是自联接

  1. -- 9*9 得到81行纪录 
  2. SELECT * FROM HR.Employees e1 CROSS JOIN HR.Employees e2 

3.1.3 生成数字表

自联接生成整数数列(1,2,3....)结果集非常方便。

  1. --总共10*10*10行,生成从0-999的数 
  2. SELECT d3.digit * 100 + d1.digit * 10 + d2.digit FROM digits d1 CROSS JOIN digits d2 CROSS JOIN digits d3 ORDER BY 1 

3.2 内部联接

内部联接是默认联接,所以INNER是可选的。

3.2.1 ANSI SQL-92、89 语法和安全性

  1. --92 
  2. SELECT e.empid , o.orderid 
  3. FROM Sales.Orders o  
  4. INNER JOIN HR.Employees e ON o.empid = e.empid  
  5. ORDER BY 1,2 
  6. --89 
  7. SELECT e.empid , o.orderid FROM Sales.Orders o, HR.Employees e 
  8. WHERE o.empid = e.empid 
  9. ORDER BY 1,2 

92语法具有在联接安全性方面比较好,比如忘记写on后面的联接条件时,SQL语句会运行异常,以便您对SQL进行修改。然而对于89语法来说,如果忘记在WHERE后添加条件,此时SQL也可以进行有效查询,返回了错误的结果集,并且由于查询不会失败,SQL可能会运行一段时间。而且,其他维护人员也不会知道些SQL语句是交叉联接还是内联接。所以,建议使用92语法的SQL。

3.3 更多联接示例

3.3.1 复合联接

复合联接就是在ON之后涉及多个属性的简单联接。

  1. FROM table1 AS t1 INNER JOIN table2 t2 ON t1.col1 = t2.col1 AND t1.col2 = t2.col2 

3.3.2 不等联接

联接条件除了=号之外还有其他任何运算符的联接即为不等联接

  1. --除自己以外的行就是9 * 9 - 9 
  2. SELECT e1.empid, e2.empid FROM HR.Employees e1 JOIN HR.Employees e2 ON e1.empid <> e2.empid ORDER BY 1,2 

3.3.3 多联接查询

一般来说,当FROM子句中出现多个表运算符时,表运算符会从左到右进行逻辑处理。也就是说,第一个运算符生成的结果集将会被视为第二个运算符的左侧驱动表,以此类推。

  1. SELECT c.custid, c.companyname, o.orderid, od.productid, od.qty 
  2. FROM Sales.Customers c  
  3. JOIN Sales.Orders o ON c.custid = o.custid 
  4. JOIN Sales.OrderDetails od ON o.orderid = od.orderid 

3.4 外部联接

与其它联接相比,外部联接更难把握,因为你需要清楚知道哪个表的数据应该保留还是去掉。

3.4.1 外联接基础知识

外联接的语法是LEFT OUTER JOIN RIGHT OUTER JOINFULL OUTER JOIN, OUTER关键字可选,LEFT保留左侧表的行,RIGHT则相反,FULL则是两侧的行都需要保留。

  1. SELECT c.custid, c.companyname, O.orderid 
  2. FROM Sales.Customers c  
  3. LEFT OUTER JOIN Sales.Orders O ON c.custid = O.custid 
  4. ORDER BY 3 

Customers表中有两位客户(22、75)是没有orderid的,在Orders返回的orderid属性中是NULL。逻辑上,这两个客户会被联接的第二阶段(基于ON的筛选)过滤掉,但是在第三个阶段将其作为外部行添加回了结果集。而在内联接中,这两行是不会返回的。这两行是在保留左侧表中所有行时添加的。
所以在外联接的的结果中,保留的行有两种:内部行外部行。内部行是基于ON与另一侧匹配的行,外部行则是未匹配的行。
外部联接的一个混乱问题是:是在ON子句还是WHERE子句进行筛选。ON子句并不确定行是否会最终显示在输出的结果集中,只是判断行是否与另一侧是否匹配。所以,当需要表达的谓词不是最终结果时,在ON子句中进行指定谓词。当需要在外部行生成后再筛选,则应当使用WHERE子句。WHERE子句是在FROM子句之后进行处理的,即外部行生成后。

3.4.2 其他外联接知识

  1. 包含缺失值

  2. 从外联接的非保留侧筛选属性
    当需要检查涉及外部联接的代码查找bug时,要查的项目之一就是WHERE子句,查看是否引用了非保留侧的属性。这是因为联接非保留侧的属性在输出行中是NULL标记,通常NULLoperator value 形式的表达式会生成UNKNOWN值。WHERE子句通常会过滤掉UNKNOWN值,这样的谓词会导致所有外部行被过滤掉,实际上就抵消了外联接 。逻辑上就是实现了一个内联接

SELECT c.custid, c.companyname,o.orderid,o.orderdate FROM Sales.Customers c LEFT JOIN Sales.Orders o ON c.custid = o.custid WHERE o.orderdate > ‘20070101‘

  1. 在多联接查询中使用外联接
    2.5中“同时操作”的概念不适用于 FROM 阶段中表运算符的处理,表运算符是从左到右进行逻辑计算的,重新排列在外联接中的顺序可能会生成不同的输出结果。
    下面这个bug是上面的bug的变异,第一个联接返回的是客户及其订单,以及没有订单的客户(即外部行,orderid为NULL标记)。第二个 o.orderid = od.orderid 作为内联接的联接条件,生成的结果将会是左右两侧表都有相应 orderid 记录的结果集。因此,第一个联接(外联接)得到的外部行将不会作为结果输出。所以,外联接后跟一个内联接或是右外联接查询都会使得外部行被删除。当然这是假设联接条件是来自左侧的NULL标记和右侧的任意值进行比较。
    解决方法:

    • 第二个联接使用LEFT JOIN

    • 内联接先进行查询,再使用一个RIGHT JOIN

    • 使用()内联接括起来,使得内联接变成一个独立的逻辑阶段

    SELECT c.custid,o.orderid,od.qty FROM Sales.Customers c LEFT JOIN Sales.Orders o ON c.custid = o.custid INNER JOIN Sales.OrderDetails od ON o.orderid = od.orderid

  2. 外部联接使用COUNT聚合
    通常,不应该将外部行作为计数目标。进行COUN计算时不要使用 COUNT(*) 来进行计数,应当使用COUNT(col)代替。


练习

  1. --1.为每个雇员行生成5个副本,表 HR.Employees 和 dbo.Nums 
  2. SELECT empid, firstname,lastname, n.n FROM HR.Employees e CROSS JOIN Nums n WHERE n < 6 
  3.  
  4. --2. 编写一个查询,每个雇员返回一行,并且日期在20090612-20090616范围内,表HR.Employees, dbo.Nums 
  5. SELECT e.empid, x.orderDate FROM HR.Employees e CROSS JOIN (SELECT DATEADD(DAY,n.n,‘20090611‘) AS orderDate FROM Nums n WHERE n.n <= DATEDIFF(DAY, ‘20090612‘,‘20090616‘) + 1 ) x ORDER BY 1 
  6.  
  7. --3. 返回美国客户,并为每个客户返回订单总数和总数量。表customers、orders、orderDetails 
  8. SELECT o.custid, COUNT(DISTINCT od.orderid) AS ordersCount, SUM(od.qty) AS totalQty FROM Sales.Customers c INNER JOIN Sales.Orders o ON c.custid = o.custid INNER JOIN Sales.OrderDetails od ON o.orderid = od.orderid GROUP BY o.custid ORDER BY 1 
  9.  
  10. --4. 返回客户及其订单,包括没有下订单的客户,customers、orders 
  11. SELECT c.custid, c.companyname, o.orderid, o.orderdate FROM Sales.Customers c LEFT JOIN Sales.Orders o ON c.custid = o.custid 
  12.  
  13. --5. 返回20070212下订单的客户以及他们的订单。此外,还返回20070212没有下订单的客户 
  14. SELECT c.custid, c.companyname, o.orderid, o.orderdate FROM Sales.Customers c LEFT JOIN Sales.Orders o ON c.custid = o.custid AND o.orderdate = ‘20070212‘ 

CHAPTER 04 子查询

最外面查询的查询结果集返回给调用者,被称为外部查询。内部查询的查询结果被用于外部查询,称为内部查询

4.1 自包含子查询

4.1.1 自包含标量子查询示例

  1. --1. 将值赋给一个变量 
  2. DECLARE @maxId INT = (SELECT MAX(orderid) FROM Sales.Orders) 
  3. SELECT * FROM Sales.Orders WHERE orderid = @maxId 
  4.  
  5. --2. 使用子查询代替变量。“=”号右边只允许返回一个单值,不可返回多个,否则报错 
  6. SELECT * FROM Sales.Orders WHERE orderid = (SELECT MAX(orderid) FROM Sales.Orders) 

如果子查询没有返回值,则返回 NULL 。而与NULL的都会生成 NUKNOWN, 然而查询筛选是不返回筛选表达式试算为 UNKNOWN 的行。

4.1.2 自包含多值子查询示例

多值子查询是作为单个列,返回多个值的子查询,无论子查询是否是自包含的。可使用IN谓词进行多值子查询操作。
没有明确的经验方法表示子查询比联接要好,所以,在写SQL时先以直观的形式对指定任务写一个解决方案查询,如果性能不满意,可以尝试查询调教来进行调优,如:使用子联接代替子查询或是使用子查询代替联接
在子查询中可以不使用 DISTINCT 子句进智能行去重从而提高性能,因为数据库引擎足够智能,会考虑删除重复项,不用显式地去告诉它这样做。

4.2 相关子查询

相关子查询引用的表属性位于外部查询中,这意味着,该子查询依赖于外部查询并且无法单独调用。

  1. SELECT * FROM Sales.Orders AS o1 WHERE o1.orderid = (SELECT MAX(o2.orderid) FROM Sales.Orders AS o2 WHERE o2.custid = o1.custid ) 

4.2.1 EXISTS 谓词

使用 EXISTS 可作为一个子查询作为输入,如果子查询返回任意行,EXISTS 返回TRUE,否则返回FALSE,所以 EXISTS 是一个两值逻辑运算。

  1. SELECT c.* FROM Sales.Customers c WHERE c.country = ‘Spain‘ AND EXISTS ( SELECT * FROM Sales.Orders o WHERE c.custid = o.custid ) 

EXISTS 相当于短路计算功能,即SQL Server 只要知道子查询返回了一行或者没有数据返回就足够了, 它不需要处理所有符合条件的行。
与其他大多数情况不同,在 EXISTS的上下文子查询中使用星号()并无不妥之处,EXISTS只关心匹配行的存在,并不关心SELECT*列表中指定的列。数据库会自动忽略查询的 SELECT 列表, 所以并不会带来大的性能损耗。相比于SELECT 1,使用SELECT *更让人容易理解,更加直观。

4.3 额外子查询知识

4.3.1 返回前一个或下一个值

  1. --返回当前orderid的前一个orderid 
  2. SELECT o1.custid,o1.orderdate,o1.orderid,(SELECT MAX(o2.orderid) FROM Sales.Orders o2 WHERE o2.orderid < o1.orderid ) AS preId FROM Sales.Orders o1  
  3. --返回当前orderid的下一个orderid 
  4. SELECT o1.custid,o1.orderdate,o1.orderid,(SELECT MIN(o2.orderid) FROM Sales.Orders o2 WHERE o2.orderid > o1.orderid ) AS nextId FROM Sales.Orders o1  

PS:在SQL2012中引入了 LAGLEAD两个新开窗函数,允许按照指定排序从“前一个”或“后一个”返回一个元素。

4.3.2 使用运行聚合

运行聚合是随着时间累积值的聚合。

  1. SELECT orderyear,o1.qty,(SELECT SUM(o2.qty) FROM Sales.OrderTotalsByYear o2 WHERE o2.orderyear <= o1.orderyear) FROM Sales.OrderTotalsByYear o1 ORDER BY 1 

4.3.3 不当子查询处理

4.3.3.1 NULL值

  1. --返回没有订单的客户 
  2. SELECT * FROM Sales.Customers WHERE custid NOT IN ( SELECT custid FROM Sales.Orders ) 
  3. --可以查到记录 
  4. SELECT TOP 1 ‘Have Value‘ FROM Sales.Customers WHERE 3 NOT IN (1,2)  
  5. --没有返回数据 
  6. SELECT TOP 1 ‘Have Value‘ WHERE 3 NOT IN (1,2,null) 
  7. --等价于 
  8. SELECT ‘Have Value‘ WHERE 3 <> 1 AND 3 <> 2 AND 3 <> NULL 

我们知道IN返回的是TRUENOT IN则为FASLE。 如果Orders表中存在有一条custid为NULL的记录,Customers表的custid与该记录进行比较时产生的是UNKNOWN,所以整个NOT IN 的返回结果是UNKNOWN。这意味着,这是一种不知道custid是否出现在集合中,也不知道其是否求出现在集合中的情况。总之,当对一个至少返回一个NULL的子查询使用谓词时,外部查询总是返回空集合。
解决方案:

  1. 列不允许NULL标记,将其定义为NOT NULL是什么必要的。

  2. 编写所有查询时,应考虑三值逻辑(TRUE,FALSE和UNKNNOW)的所有可能的三种真值。

  3. 考虑使用EXISTS代替IN

练习

  1. --1.返回Orders表中可以查到的活动最后一天的所有订单。表:Sales.Orders 
  2. SELECT * FROM Sales.Orders o1  
  3. WHERE o1.orderdate = (SELECT MAX(orderdate) FROM Sales.Orders ) 
  4.  
  5. --2.返回订单数量最多的客户的所有订单,表:Sales.Orders。注意:可能有多个用户具有相同的订单,可使用 TOP WIT TIES 来作为关联行绑定,详情参见2.1.7 
  6. SELECT * FROM Sales.Orders  
  7. WHERE custid IN (SELECT TOP 1 WITH TIES custid FROM Sales.Orders GROUP BY custid ORDER BY COUNT(*) DESC) --可使用联接 
  8.  
  9. --3.返回20080501或之后没有下订单的雇员,表:HR.Employees,Sales.Orders,关联条件empid 
  10. SELECT * FROM HR.Employees  
  11. WHERE empid NOT IN (SELECT o1.empid FROM Sales.Orders o1 WHERE o1.orderdate > ‘20080501‘) --可使用EXISTS 
  12.  
  13. --4.返回有客户但是没有雇员的国家,表:Sales.Customers,HR.Employees 
  14. SELECT DISTINCT country FROM Sales.Customers  
  15. WHERE country NOT IN (SELECT country FROM HR.Employees) ORDER BY 1 
  16.  
  17. --5.返回每个客户活动最后一天下的所有订单,表:Orders 
  18. SELECT o1.custid,o1.orderdate,o1.empid FROM Sales.Orders o1  
  19. WHERE o1.orderdate = (SELECT MAX(o2.orderdate) AS orderdate FROM Sales.Orders o2 WHERE o1.custid = o2.custid )  
  20. ORDER BY 1 
  21.  
  22. --6.返回2007年下了订单但是2008年没有下订单的客户,表:Orders,Customers 
  23. SELECT * FROM Sales.Customers c  
  24. WHERE EXISTS (SELECT o1.custid FROM Sales.Orders o1 WHERE orderdate > ‘20070101‘ AND orderdate < ‘20080101‘ AND o1.custid = c.custid )  
  25. AND NOT EXISTS (SELECT o2.custid FROM Sales.Orders o2 WHERE orderdate > ‘20080101‘ AND orderdate < ‘20090101‘ AND o2.custid = c.custid)--EXCEPT 
  26.  
  27. --7.返回订购产品12的客户,表:customers,orders,orderdetails 
  28. SELECT * FROM Sales.Customers c WHERE EXISTS ( 
  29. SELECT * FROM Sales.Orders o WHERE EXISTS( 
  30. SELECT * FROM Sales.OrderDetails od WHERE productid = 12 AND od.orderid = o.orderid) AND o.custid = c.custid )  
  31.  
  32. --8. 计算每个客户及其月度的采购总量,表CustOrders 
  33. SELECT *, (SELECT SUM(c2.qty) FROM Sales.CustOrders c2 WHERE c2.ordermonth <= c1.ordermonth AND c2.custid = c1.custid)  
  34. FROM Sales.CustOrders c1 ORDER BY 1 

Chapter 05 表表达式

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


5.1 派生表

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

  1. --语法 
  2. 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. --1.内嵌方式 
  2. SELECT * FROM (SELECT YEAR(orderdate) AS orderyear,custid FROM Sales.Orders) AS D 
  3. --2.外部形式 
  4. 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)是表表达式的另一种标准形式,与派生表非常相似。
语法:

  1. ;WITH CTE_NAME AS( inner_query ) outer_query 

注意,T-SQL中的 WITH 子句可以用于不同的目的,为避免报错,建议在使用CTE时,要在 WITH 前加分别(;)

5.2.1 分别列别名

CTE中也是有两种方式分配列别名

  1. --外部方式 
  2. ;WITH CTE_NAME(col1,col2) AS( inner_query ) outer_query 

5.2.2 使用参数

5.2.3 定义多个CTE

  1. ;WITH c1 AS (SELECT YEAR(orderdate) AS orderyear FROM Sales.Orders) 
  2. ,c2 AS ( SELECT count(*) total FROM c1 ) 
  3. SELECT * FROM c2 

5.2.4 CTE的多次引用

就外部查询的 FROM 子句而言, CTE在其之前已经存在了,因此可以对同一个CTE进行多次引用。

5.2.5 递归CTE

递归CTE至少由两个查询定义,至少一个查询作为定位点成员,一个查询作为递归成员。基本递归CTE的一般形式如下:

  1. ;WITH <CTE_name>[<targe_column_list>] 
  2. AS 

  3. <anchor_member>  
  4. UNION ALL 
  5. <recursive_member> 

  6. <outer_query_against_CTE> 

定位点成员是一个返回有效关系结果表的查询,就像一个用于定义非递归表表达式的查询。定点成员查询仅调用一次。
递归成员是一个引用CTE名称的查询。递归成员多次调用,直到它返回一个空集合或超过某些限制为止。
在外部查询中引用CTE名称代表的是定位点成员调用和所有递归成员调用的组合结果集。

  1. --利用递归CTE返回某个雇员和其各级下属雇员(直接或间接) 
  2.  
  3. ;WITH EmpsCTE AS 

  4. --定位点成员 
  5. SELECT empid, mgrid, firstname, lastname 
  6. FROM HR.Employees  
  7. WHERE empid = 2 
  8. UNION ALL 
  9. --递归成员 
  10. SELECT e.empid, e.mgrid, e.firstname, e.lastname  
  11. FROM EmpsCTE p 
  12. INNER JOIN HR.Employees AS e ON p.empid = e.mgrid 
  13. )  
  14. SELECT * FROM EmpsCTE 

递归成员联接CTE代表的是上一个结果集。 然后从 Employees 表检索由上一个结果集中返回的直接下属。
在出现递归成员的联接谓词逻辑错误或是数据的循环结果错误,递归成员可能会调用无数次。作为一项安全措施,SQL SERVER默认情况下限制递归成员可以被调用的次数为100。可以在外部查询的尾部指定 OPTION(MAXRECURSION n) 提示来更改默认的最大递归限制,n范围为0-32767。


5.3 视图

表表达式的范围只是在单查询语句之中,视图和**内嵌表值函数(内嵌TVF)**是两种可重复使用的表表达式类型,其定义被存储为数据库对象。只有在显式删除它们时才从数据库中移除掉。
语法:

  1. IF OBJECT_ID(‘Sales.USACusts‘) IS NOT NULL 
  2. DROP VIEW Sales.USACusts 
  3. GO 
  4. CREATE VIEW Sales.USACusts 
  5. AS  
  6. SELECT custid,companyname ROM Sales.Customers WHERE country = ‘USA‘ 
  7. --查询 
  8. 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在内部以代码混淆方式存储对象定义文本。

  1. --由于创建视图时没有使用ENCRYPTION,可以得到创建视图的定义语句 
  2. SELECT OBJECT_DEFINITION(OBJECT_ID(‘Sales.USACusts‘)) 
  3.  
  4. --使用ENCRYPTION,偷懒使用了 * 创建。再使用上面这个语句得到的是NULL 
  5. CREATE VIEW Sales.USACusts WITH ENCRYPTION 
  6. AS 
  7. SELECT * FROM Sales.Customers 
  8.  
  9. /* 
  10. 作为OBJECT_DEFINITION函数的替代方法,可以执行下面这个存储过程来获取对象的定义,但是你发现返回的是“2 Procedure sp_helptext. The text for object ‘Sales.USACusts‘ is encrypted. SQL.sql 126 25 ”  
  11. */ 
  12. EXECUTE sys.sp_helptext ‘Sales.USACusts‘ 

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

  1. CREATE VIEW Sales.USACusts WITH SCHEMABINDING 
  2. AS 
  3. SELECT custid,companyname FROM Sales.Customers WHERE country = ‘USA‘ 
  4. /* 
  5. 尝试从Customers表中删除companyname,报“ALTER TABLE DROP COLUMN companyname failed because one or more objects access this column. SQL.sql 14 1 ” 
  6. */ 
  7. 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来实现。这与检查约束类似。

  1. CREATE VIEW Sales.USACusts WITH SCHEMABINDING 
  2. AS 
  3. SELECT custid,companyname,country FROM Sales.Customers WHERE country = ‘USA‘ 
  4. WITH CHECK OPTION 
  5. /* 
  6. 插入国家为‘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 ” 
  7. */ 
  8. INSERT INTO Sales.USACusts VALUES (32,‘Customer TEST‘,‘UK‘) 

5.4 内嵌表值函数(TVF)

内嵌TVF(Table-valued Functions) 是支持输入参数的可重复使用的表表达式。除了支持输入参数之外,其他方面基本与视图类似。可以看作是参数化视图
语法:

  1. --创建TVF 
  2. CREATE FUNCTION dbo.GetCustOrders 
  3. (@cid AS INT) RETURNS TABLE 
  4. AS 
  5. RETURN 
  6. SELECT * 
  7. FROM Sales.Orders 
  8. WHERE custid = @cid 
  9.  
  10. --使用 
  11. SELECT c.* FROM dbo.GetCustOrders(1) AS c 

5.5 APPLY 运算符

APPLY 运算符支持 CROSS APPLYOUTER APPLY,前者仅实施一个逻辑查询处理阶段,而后者实施了两个阶段。
注:标准SQL叫做LATERAL,APPLY不是标准的
APLLY运算符对两个输入表进行操作,第二个表可以是一个表表达式(通常为派生表或内联TVF)。 CROSS APPLY 运算符的逻辑查询处理阶段是:它将右侧的表表达式应用到左侧表的每一行,并生成一个组合结果集的结果表。与交叉联接非常类似。

  1. SELECT s.shipperid,e.empid 
  2. FROM Sales.Shippers s  
  3. CROSS JOIN HR.Employees e 
  4.  
  5. SELECT s.shipperid,e.empid 
  6. FROM Sales.Shippers s  
  7. CROSS APPLY HR.Employees e 

但是,CROSS APPLY 运算符右侧的表表达式可以对来自左侧表的每一行表示一个不同的行集,这是与联接不同的。可以在右侧表中引用(传递)左侧的属性表(派生表或内嵌TVF)来实现此目标。

  1. --返回每个客户的3个最近订单 
  2. SELECT c.custid, A.orderid, A.orderdate 
  3. FROM Sales.Customers c  
  4. CROSS APPLY  
  5. ( SELECT TOP 3 o.orderid, o.empid, o.orderdate, o.requireddate  
  6. FROM Sales.Orders o  
  7. WHERE o.custid = c.custid  
  8. ORDER BY o.orderdate DESC, o.orderid DESC) A 

CROSS APPLY 运算符类似于内联接,若右侧表中没有对应的结果,则左侧的行也不会返回。如果想返回左侧的行,则可使用 OUTER APPLY
出于封装的目的,可以发现使用内嵌TVF代替派生表会更方便。这样代码更容易维护和跟踪。

  1. --创建TVF 
  2. CREATE FUNCTION TopOrders 
  3. (@cid INT, @n INT) RETURNS TABLE 
  4. AS 
  5. RETURN 
  6. SELECT TOP (@n) orderid, empid, orderdate, requireddate 
  7. FROM Sales.Orders  
  8. WHERE custid = @cid 
  9. ORDER BY orderdate DESC, orderid DESC 
  10.  
  11. --代替派生表 
  12. SELECT c.custid, A.orderid, A.orderdate 
  13. FROM Sales.Customers c 
  14. OUTER APPLY dbo.TopOrders(c.custid, 3) A 

练习

  1. --1.1 返回每个雇员在orderdate列中的最大值,表:Sales.Orders 
  2. SELECT empid, MAX(orderdate) AS maxorderdate  
  3. FROM Sales.Orders 
  4. GROUP BY empid  
  5. --1.2 根据1.1的派生表和Orders表之间的关联查询,返回每个雇员最大订单日期的订单。 
  6. SELECT o1.empid, o1.orderdate, o1.orderid, o1.custid 
  7. FROM Sales.Orders o1 
  8. INNER JOIN ( 
  9. SELECT empid, MAX(orderdate) AS maxorderdate  
  10. FROM Sales.Orders 
  11. GROUP BY empid ) AS o2  
  12. ON o1.empid = o2.empid AND o1.orderdate = o2.maxorderdate 
  13.  
  14. --2.1 计算orderdate、orderid排序的每个订单的行号,表:orders 
  15. SELECT ROW_NUMBER() OVER (ORDER BY orderdate, orderid) AS rownum 
  16. ,orderid, orderdate, custid, empid 
  17. FROM Sales.Orders 
  18. --2.2 返回2.1行号为11-12的行。使用CTE封装2.1的代码 
  19. ;WITH fetchOrdersCTE AS 

  20. SELECT ROW_NUMBER() OVER (ORDER BY orderdate, orderid) AS rownum 
  21. ,orderid, orderdate, custid, empid 
  22. FROM Sales.Orders 
  23. ) 
  24. SELECT *  
  25. FROM fetchOrdersCTE  
  26. ORDER BY 1 
  27. OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY 
  28.  
  29. --3 使用CTE,返回Zoya Dologopyatova(empid 9)的领导管理链,表:Employees 
  30. ;WITH empsCTE AS 

  31. --锚点成员 
  32. SELECT empid,mgrid,lastname,firstname FROM HR.Employees  
  33. WHERE firstname = ‘zoya‘ 
  34. UNION ALL 
  35. --递归成员 
  36. SELECT e.empid,e.mgrid,e.lastname, e.firstname 
  37. FROM HR.Employees e 
  38. INNER JOIN empsCTE cte ON e.empid = cte.mgrid 
  39. ) 
  40. SELECT * FROM empsCTE 
  41.  
  42. --4.1 创建一个视图,返回每位雇员每年的总销量,表:orders,orderdetails 
  43. CREATE VIEW Sales.VEmpOrders 
  44. AS 
  45. SELECT o.empid, YEAR(o.orderdate) AS orderyear, SUM(od.qty) AS qty 
  46. FROM Sales.Orders o 
  47. INNER JOIN Sales.OrderDetails od ON o.orderid = od.orderid 
  48. GROUP BY o.empid, YEAR(O.orderdate) 
  49. SELECT * FROM Sales.VEmpOrders ORDER BY 1, 2 
  50.  
  51. --4.2 使用4.1的视图,返回每个雇员每年的运行总销量 
  52. SELECT * ,(SELECT SUM(qty) FROM Sales.VempOrders v2 WHERE v2.orderyear <= v1.orderyear AND v2.empid = v1.empid) as runqty 
  53. FROM Sales.VEmpOrders v1 
  54. GROUP BY empid, orderyear,qty 
  55. ORDER BY 1,2 
  56.  
  57. /* 
  58. 5.1 创建一个TVF,参数为供应商ID(@supid AS INT)和请求的产品数量(@n AS INT)。返回指定的供应商ID供应的@n个最高单价产品。表:products  
  59. */ 
  60. CREATE FUNCTION Production.TopProducts 
  61. (@supid INT, @n INT) RETURNS TABLE 
  62. AS 
  63. RETURN  
  64. SELECT TOP (@n) productid, productname, unitprice # 
  65. FROM Production.Products 
  66. WHERE supplierid = @supid  
  67. ORDER BY unitprice 
  68.  
  69. SELECT * FROM Production.TopProducts(5,2) 
  70.  
  71. --5.2 使用CROSS APPLY运算符和5.1中的TVF,为每个供应商返回两个最贵产品。 
  72. SELECT s.supplierid, s.companyname, t.productid, t.productname, t.unitprice 
  73. FROM Production.Suppliers s 
  74. CROSS APPLY Production.TopProducts(s.supplierid,2) t 

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 指示顺序(其实排序序在这里没有什么卵用,SQL SERVER 会进行识别优化, 不会进行相应的排序,所以也不会造成相关开销)。

  1. --INTERSECT ALL 不应该返回rownum的,可以再查询一次去年这一列 
  2. SELECT ROW_NUMBER() OVER (PARTITION BY country, region, city ORDER BY (SELECT 0)) AS rownum 
  3. ,country, region, city 
  4. FROM HR.Employees 
  5. INTERSECT 
  6. SELECT ROW_NUMBER() OVER (PARTITION BY country, region, city ORDER BY (SELECT 0)) AS rownum 
  7. ,country, region, city 
  8. 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来实现。

  1. SELECT ROW_NUMBER() OVER (PARTITION BY country, region,city ORDER BY (SELECT 0)) AS rownum 
  2. ,country, region, city 
  3. FROM HR.Employees 
  4. EXCEPT  
  5. SELECT ROW_NUMBER() OVER (PARTITION BY country, region,city ORDER BY (SELECT 0)) AS rownum 
  6. ,country, region, city 
  7. FROM Sales.Customers 

6.4 优先级

集合运算符的优先级是: INTERSECT > UNION = EXCEPT


6.5 规避不支持的逻辑阶段

用途集合运算符输入的独立查询支持队 ORDER BY 之外的所有逻辑查询处理阶段(如表运算符,WHERE, GROUP BY, HAVING)。但是,仅有 ORDER BY 阶段允许用于运算符的结果,如果需要其他逻辑运算可以通过表表达式绕过此限制。定义一个基于使用集合运算符的查询的表表达式,可以在对表表达式的外部查询中应用任何所需的逻辑查询处理阶段。

  1. /* 
  2. 此查询演示了UNION的运行结果应用GROUP BY的逻辑处理阶段。同样,其他的逻辑查询处理阶段都可以在外部查询中应用 
  3. */ 
  4. SELECT u.country, COUNT(*) AS toatl 
  5. FROM ( 
  6. SELECT country, region, city FROM HR.Employees  
  7. UNION 
  8. SELECT country, region, city FROM Sales.Customers  
  9. ) u 
  10. GROUP BY u.country 
  11.  
  12. /* 
  13. 如果压根一个带有TOP(OFFSET-FETCH)的查询参与集合运算符中,那么需要定义一个表表达式,并对表表达式的外部查询参与集合运算符中就可以了 
  14. */ 
  15. SELECT *  
  16. FROM ( 
  17. SELECT *  
  18. FROM Sales.Orders 
  19. WHERE empid = 5 
  20. ORDER BY orderdate DESC, orderid DESC 
  21. OFFSET 0 ROWS FETCH FIRST 2 ROWS ONLY  
  22. ) o1 
  23. UNION ALL 
  24. SELECT * FROM( 
  25. SELECT *  
  26. FROM Sales.Orders 
  27. WHERE empid = 3 
  28. ORDER BY orderdate DESC, orderid DESC 
  29. OFFSET 0 ROWS FETCH FIRST 2 ROWS ONLY  
  30. ) o2 

练习

  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.  
  11. --3. 返回20080101和20080201均有订单的客户和雇员id 
  12. SELECT o.custid, o.empid 
  13. FROM Sales.Orders o 
  14. WHERE o.orderdate >= ‘20080101‘ AND o.orderdate < ‘20080201‘ 
  15. INTERSECT 
  16. SELECT o.custid, o.empid 
  17. FROM Sales.Orders o 
  18. WHERE o.orderdate >= ‘20080201‘ AND o.orderdate < ‘20080301‘ 
  19.  
  20. --4. 返回订单活动在20080101和20080201,但是不在2007年的客户和雇员id 
  21. ( 
  22. SELECT o.custid, o.empid 
  23. FROM Sales.Orders o 
  24. WHERE o.orderdate >= ‘20080101‘ AND o.orderdate < ‘20080201‘ 
  25. INTERSECT 
  26. SELECT o.custid, o.empid 
  27. FROM Sales.Orders o 
  28. WHERE o.orderdate >= ‘20080201‘ AND o.orderdate < ‘20080301‘ 
  29. ) 
  30. EXCEPT 
  31. SELECT custid, orderid 
  32. FROM Sales.Orders 
  33. WHERE orderdate >= ‘20070101‘ AND orderdate < ‘20080101‘ 
  34.  
  35. --5.  
  36. WITH tmpCTE AS 
  37. ( 
  38. SELECT country, region, city, 1 AS sortNum 
  39. FROM HR.Employees 
  40. UNION ALL 
  41. SELECT country, region, city, 1 AS sortNum 
  42. FROM Production.Suppliers 
  43. ) 
  44. SELECT country, region, city 
  45. FROM tmpCTE 
  46. ORDER BY sortNum DESC,country, region, city 

SQL Server2012 T-SQL基础教程--读书笔记

标签:

原文地址:http://www.cnblogs.com/leongfeng/p/5208080.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!