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语句的查询顺序:
FROM
WHERE
GROUP BY
HAVING
SELECT
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 子句。不过,没有FETCH 的OFFSET 是允许的,这种情况就是跳过多少行,并返回剩余的所有行。而且ROW 和ROWS 是可以互换的。
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
ORDER BY
2.2 谓词和运算符
2.3 CASE 表达式
CASE 是一个标题表达式,返回一个基于条件置逻辑的值。注意,CASE 是表达式而不是语句。所以,它不支持你控制活动流或是做一些基于条件逻辑的损伤。可以在诸如:SELECT 、WHERE 、HAVING 和ORDER 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 的表达式缩写,如:ISNULL 、COALESCE 、IIF 和 CHOOSE*,其中只有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‘ ) 2 SELECT 1 +‘a‘
2.6.2 SUBSTRING 函数
SUBSTRING(string, start, length) 在sql中,string
字符串下标从1开始算起,而不是0。
1 SELECT SUBSTRING (‘ABCD‘ ,1 ,2 )
2.6.3 LEFT 和 RIGHT函数
LEFT(string, n),RIGHT(string, n),n
代表从LEFT 或RIGHT
提取的字符数
2.6.4 LEN 和 DATALENGTH 函数
LENG(string) 返回字符数(或者长度),并且会删除字符串最后的空格.
DATALENG(string) 返回的是字节数
2.6.5 CHARINDEX 函数
CHARINDEX(subString, string[,start_pos]) 返回字符串subString
从start_pos
开始在string
中第一次出现的位置。如果找不到指定的字符串,返回0
1 SELECT CHARINDEX (‘cd‘ ,‘CDABCDEFG‘ ,2 )
2.6.6 PATINDEX 函数
PATINDEX(pattern,string) pattern
与LIKE 模式相同
1 SELECT PATINDEX (‘%[0-9]%‘ ,‘abcde53fg‘ )
2.6.7 REPLACE 函数
REPLACE(string, subString1, subString2) 使用subString2
取代string
中所有的subString1
1 SELECT REPLACE (‘abcde53fg53‘ ,‘53‘ ,‘11‘ )
2.6.8 REPLICATE 函数
REPLICATE(string, n) 将string
复制n
次
1 SELECT REPLICATE (‘abc‘ ,3 )
2.6.9 STUFF 函数
STUFF(string, pos, del_length, insertString) 从pos
位置开始,删除指定del_length
长度的字符串,并从pos
处插入insertString
字符串
1 SELECT STUFF (‘abcdefg‘ ,3 ,4 ,1234 )
2.6.10 UPPER 和 LOWER 函数
返回大小写或小写字符串
2.6.11 RTRIM 和 LRITM 函数
从右边或左边删句末或句首的空格
FORMAT(input, format_string, culture) 按照.NET格式和一个可选的区域参数,将input
格式化成一个字符串
从pos
位置开始,删除指定del_length
长度的字符串,并从pos
处插入insertString
字符串
1 SELECT FORMAT (153 ,‘0000‘ )
2.6.13 LIKE 谓词
% 通配符,代表任意字符
_ 通配符,表示单个字符
[] 通配符,代表单个字符必须是指定[]内的字符之一
通配符,代表单个字符必须在指定的范围内
[^] 通配符,代表指定的字符不在指定的字符列表或范围内。
ESCAPE 字符,要查找上面定义的通配符时,需要使用转义字符。如要查找_
:col LIKE ‘%!_%‘ESCAPE‘!‘ 或 col LIKE ‘%[_]%‘
2.7 时间和日期
在筛选列上应用操作时,数据库不能以有效方式使用索引,所以如果要筛选日期范围时可使用如col >= 20160101 AND col < 20080101
来代替YEAR(col) = 2007
2.7.1 当前时间
1 SELECT 2 GETDATE () 3 ,CURRENT_TIMESTAMP 4 ,GETUTCDATE () 5 ,SYSDATETIME () 6 ,SYSUTCDATETIME () 7 ,SYSDATETIMEOFFSET ()
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 2 SELECT SYSDATETIMEOFFSET (), SWITCHOFFSET (SYSDATETIMEOFFSET (), ‘-05:00‘ )
2.7.4 TODATETIMEOFFSET 函数
TODATETIMEOFFSET(date_and_time_value,time_zone) 设置date_and_time_value 为time_zone 时区,通常用于迁移非已知偏移量数据到已知偏移数据。
1 2 SELECT TODATETIMEOFFSET (CONVERT (DATETIME,‘20160131 20:42‘ ),‘+08:00‘ )
2.7.5 DATEADD 函数
DATEADD(part,n,dt_val) 指定日期的某个部分 part 增加 n 到 dt_val 中
1 2 SELECT DATEADD (QUARTER ,1 ,SYSDATETIME ()), SYSDATETIME ()
2.7.6 DATEDIFF 函数
DATEDIFF(part, dt_val1, dt_val2) 返回 dt_val1 到 dt_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 2 SELECT DATENAME (MONTH , SYSDATETIME ())
2.7.7 ISDATE 函数
ISDATE(string) 判断 string 是否为日期格式可以转换为日期格式数据,可以则返回1
,否则返回0
。
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 ) 2 ,DATETIMEFROMPARTS(2016 ,01 ,31 ,22 ,30 ,00 ,00 ) 3 ,DATETIMEOFFSETFROMPARTS (2016 ,01 ,31 ,22 ,30 ,00 ,1 ,8 ,4 ,3 ) 4
2.7.9 EOMONTH 函数
EOMONTH(input[,months_to_add])
这是2012引入的函数。它接受一个日期和时间值输入,并返回相应的每个月的最后一天,作为 DATE 数据类型。第2个可选参数指示要增加多少个月。
1 SELECT EOMONTH (DATEADD (mm,1 ,SYSDATETIME ()))
2.8 查询元数据
2.8.1 目录视图
1 2 SELECT SCHEMA_NAME(SCHEMA_ID) AS tableSchemaName, name AS tableName FROM sys.tables 3 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 5 SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = ‘Sales‘ AND TABLE_NAME = ‘orders‘
2.8.3 系统存储过程和函数
1 2 EXEC sys.sp_tables 3 4 EXEC sys.sp_help @obejctName = ‘Sales.Orders‘ 5 6 EXEC sys.sp_columns @table_name = ‘orders‘, @schema_name = ‘Sales‘ 7 8 EXEC sys.sp_helpconstraint ‘Sales.orders‘ 9 10 SELECT DATABASEPROPERTYEX(N‘TSQL2012‘ ,‘Collation‘ ) 11 12 SELECT OBJECTPROPERTY(OBJECT_ID(‘Sales.Orders‘ ),‘TableHasPrimaryKey‘ ) 13 14 SELECT COLUMNPROPERTY(OBJECT_ID(‘Sales.Orders‘ ),‘orderdate‘ ,‘AllowsNull‘ )
练习
1 2 SELECT * FROM Sales.orders WHERE orderdate >= ‘20070601‘ AND orderdate < ‘20070701‘ ORDER BY orderdate 3 4 5 SELECT * FROM Sales.Orders 6 WHERE orderdate = EOMONTH (orderdate) 7 ORDER BY orderdate 8 9 10 SELECT * FROM HR.Employees WHERE lastname LIKE ‘%a%a%‘ 11 12 13 SELECT orderid, SUM (unitprice * qty) AS total 14 FROM Sales.OrderDetails 15 GROUP BY orderid 16 HAVING SUM (unitprice*qty) > 10000 17 18 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 25 SELECT custid,orderid, orderdate, ROW_NUMBER() OVER (PARTITION BY custid ORDER BY orderdate,orderid ) rownum FROM Sales.orders 26 27 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 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 2 SELECT custid, empid FROM Sales.Customers CROSS JOIN HR.Employees ORDER BY 1 ,2 3 4 SELECT custid, empid FROM Sales.Customers ,HR.Employees ORDER BY 1 ,2
3.1.2 自交叉联接
可以对一个表的多个实例进行联接,支持联接的基本类型,这功能就是自联接 。
1 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 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 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 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 JOIN
和 FULL 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 其他外联接知识
包含缺失值
从外联接的非保留侧筛选属性
当需要检查涉及外部联接的代码查找bug
时,要查的项目之一就是WHERE 子句,查看是否引用了非保留侧的属性。这是因为联接非保留侧的属性在输出行中是NULL 标记,通常NULL 的operator 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‘
在多联接查询中使用外联接
在2.5 中“同时操作”的概念不适用于 FROM 阶段中表运算符的处理,表运算符是从左到右 进行逻辑计算的,重新排列在外联接 中的顺序可能会生成不同的输出结果。
下面这个bug
是上面的bug
的变异,第一个联接返回的是客户及其订单,以及没有订单的客户(即外部行,orderid为NULL 标记)。第二个 o.orderid = od.orderid 作为内联接 的联接条件,生成的结果将会是左右两侧表都有相应 orderid 记录的结果集。因此,第一个联接(外联接 )得到的外部行
将不会作为结果输出。所以,外联接 后跟一个内联接 或是右外联接 查询都会使得外部行
被删除。当然这是假设联接条件是来自左侧的NULL 标记和右侧的任意值进行比较。
解决方法:
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
外部联接使用COUNT 聚合
通常,不应该将外部行作为计数目标。进行COUN 计算时不要使用 COUNT(*)
来进行计数,应当使用COUNT(col)
代替。
练习
1 2 SELECT empid, firstname,lastname, n.n FROM HR.Employees e CROSS JOIN Nums n WHERE n < 6 3 4 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 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 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 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 2 DECLARE @maxId INT = (SELECT MAX (orderid) FROM Sales.Orders) 3 SELECT * FROM Sales.Orders WHERE orderid = @maxId 4 5 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 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 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中引入了 LAG 和LEAD 两个新开窗函数,允许按照指定排序从“前一个”或“后一个”返回一个元素。
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 返回的是TRUE ,NOT IN 则为FASLE 。 如果Orders表中存在有一条custid为NULL 的记录,Customers表的custid与该记录进行比较时产生的是UNKNOWN ,所以整个NOT IN
的返回结果是UNKNOWN 。这意味着,这是一种不知道custid是否出现在集合中,也不知道其是否求出现在集合中的情况。总之,当对一个至少返回一个NULL 的子查询使用谓词时,外部查询总是返回空集合。
解决方案:
列不允许NULL 标记,将其定义为NOT NULL 是什么必要的。
编写所有查询时,应考虑三值逻辑(TRUE,FALSE和UNKNNOW)的所有可能的三种真值。
考虑使用EXISTS 代替IN
练习
1 2 SELECT * FROM Sales.Orders o1 3 WHERE o1.orderdate = (SELECT MAX (orderdate) FROM Sales.Orders ) 4 5 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 10 SELECT * FROM HR.Employees 11 WHERE empid NOT IN (SELECT o1.empid FROM Sales.Orders o1 WHERE o1.orderdate > ‘20080501‘ ) 12 13 14 SELECT DISTINCT country FROM Sales.Customers 15 WHERE country NOT IN (SELECT country FROM HR.Employees) ORDER BY 1 16 17 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 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) 26 27 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 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个要求:
无法保证顺序 。标准SQL是不允许 ORDER BY 子句出现在定义的表表达式 查询中的,除非 ORDER BY 用于展示之外的其他目的。如:使用 OFFSET-FETCH 或 TOP 筛选。
所有列必须具有名称 。必须为所有列分配列别名。
所有列名必须是唯一的 。
5.1.1 分配列别名
1 2 SELECT * FROM (SELECT YEAR (orderdate) AS orderyear,custid FROM Sales.Orders) AS D 3 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 ( 4 <anchor_member> 5 UNION ALL 6 <recursive_member> 7 ) 8 <outer_query_against_CTE>
定位点成员是一个返回有效关系结果表的查询,就像一个用于定义非递归表表达式的查询。定点成员查询仅调用一次。
递归成员是一个引用CTE 名称的查询。递归成员多次调用,直到它返回一个空集合或超过某些限制为止。
在外部查询中引用CTE 名称代表的是定位点成员调用和所有递归成员调用的组合结果集。
1 2 3 ;WITH EmpsCTE AS 4 ( 5 6 SELECT empid, mgrid, firstname, lastname 7 FROM HR.Employees 8 WHERE empid = 2 9 UNION ALL 10 11 SELECT e.empid, e.mgrid, e.firstname, e.lastname 12 FROM EmpsCTE p 13 INNER JOIN HR.Employees AS e ON p.empid = e.mgrid 14 ) 15 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_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在内部以代码混淆方式存储对象定义文本。
1 2 SELECT OBJECT_DEFINITION(OBJECT_ID(‘Sales.USACusts‘ )) 3 4 5 CREATE VIEW Sales.USACusts WITH ENCRYPTION 6 AS 7 SELECT * FROM Sales.Customers 8 9 10 11 12 EXECUTE sys.sp_helptext ‘Sales.USACusts‘
2.SCHEMABINDING
可对VIEW 和UDF 可用,它将被引用对象的架构和列绑定到引用对象的架构中。它指示不能删除被引用对象,也不能删除或修改被引用的列。
1 CREATE VIEW Sales.USACusts WITH SCHEMABINDING 2 AS 3 SELECT custid,companyname FROM Sales.Customers WHERE country = ‘USA‘ 4 5 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 7 8 INSERT INTO Sales.USACusts VALUES (32 ,‘Customer TEST‘ ,‘UK‘ )
5.4 内嵌表值函数(TVF)
内嵌TVF(Table-valued Functions) 是支持输入参数的可重复使用的表表达式。除了支持输入参数之外,其他方面基本与视图类似。可以看作是参数化视图
语法:
1 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 APPLY 和 OUTER 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 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 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 2 SELECT empid, MAX (orderdate) AS maxorderdate 3 FROM Sales.Orders 4 GROUP BY empid 5 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 15 SELECT ROW_NUMBER() OVER (ORDER BY orderdate, orderid) AS rownum 16 ,orderid, orderdate, custid, empid 17 FROM Sales.Orders 18 19 ; WITH fetchOrdersCTE AS 20 ( 21 SELECT ROW_NUMBER() OVER (ORDER BY orderdate, orderid) AS rownum 22 ,orderid, orderdate, custid, empid 23 FROM Sales.Orders 24 ) 25 SELECT * 26 FROM fetchOrdersCTE 27 ORDER BY 1 28 OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY 29 30 31 ; WITH empsCTE AS 32 ( 33 34 SELECT empid,mgrid,lastname,firstname FROM HR.Employees 35 WHERE firstname = ‘zoya‘ 36 UNION ALL 37 38 SELECT e.empid,e.mgrid,e.lastname, e.firstname 39 FROM HR.Employees e 40 INNER JOIN empsCTE cte ON e.empid = cte.mgrid 41 ) 42 SELECT * FROM empsCTE 43 44 45 CREATE VIEW Sales.VEmpOrders 46 AS 47 SELECT o.empid, YEAR (o.orderdate) AS orderyear, SUM (od.qty) AS qty 48 FROM Sales.Orders o 49 INNER JOIN Sales.OrderDetails od ON o.orderid = od.orderid 50 GROUP BY o.empid, YEAR (O.orderdate) 51 SELECT * FROM Sales.VEmpOrders ORDER BY 1 , 2 52 53 54 SELECT * ,(SELECT SUM (qty) FROM Sales.VempOrders v2 WHERE v2.orderyear <= v1.orderyear AND v2.empid = v1.empid) as runqty 55 FROM Sales.VEmpOrders v1 56 GROUP BY empid, orderyear,qty 57 ORDER BY 1 ,2 58 59 60 61 62 CREATE FUNCTION Production.TopProducts 63 (@supid INT , @n INT ) RETURNS TABLE 64 AS 65 RETURN 66 SELECT TOP (@n) productid, productname, unitprice # 67 FROM Production.Products 68 WHERE supplierid = @supid 69 ORDER BY unitprice 70 71 SELECT * FROM Production.TopProducts(5 ,2 ) 72 73 74 SELECT s.supplierid, s.companyname, t.productid, t.productname, t.unitprice 75 FROM Production.Suppliers s 76 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 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 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 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 2 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 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 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 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