SQL Server2012 T-SQL基础教程--读书笔记(1-4章)
示例数据库:点我
- Chapter 01 T-SQL 查询和编程背景
- 1.3 创建表和定义数据的完整性
- 1.3.1 创建表
- 1.3.2 定义数据的完整性
- 1. 主键约束
- 2. 唯一约束
- 3. 外键束约
- 4. CHECK约束
- 5. 默认约束
- Chapter 02 单表查询
- 2.1 SELECT 语句元素
- 2.1.7 TOP和OFFSET-FETCH
- 2.1.8 开窗函数速览
- 2.2 谓词和运算符
- 2.3 CASE表达式
- 2.5 同时操作
- 2.6 运算符和函数##
- 2.6.1 字符串连接(+和CONCAT函数)
- 2.6.2 SUBSTRING 函数
- 2.6.3 LEFT 和 RIGHT函数
- 2.6.4 LEN 和 DATALENGTH 函数
- 2.6.5 CHARINDEX 函数
- 2.6.6 PATINDEX 函数
- 2.6.7 REPLACE 函数
- 2.6.8 REPLICATE 函数
- 2.6.9 STUFF 函数
- 2.6.10 UPPER 和 LOWER 函数
- 2.6.11 RTRIM 和 LRITM 函数
- 2.6.12 FORMAT 函数
- 2.6.13 LIKE 谓词
- 2.7 时间和日期
- 2.7.1 当前时间
- 2.7.2 CAST、CONVERT和PARSE函数,及其TRY_对应函数
- 2.7.3 SWITCHOFFSET 函数
- 2.7.4 TODATETIMEOFFSET 函数
- 2.7.5 DATEADD 函数
- 2.7.6 DATEDIFF 函数
- 2.7.7 DATEPART 、YEAR、MONTH、DAY和DATENAME 函数
- 2.7.7 ISDATE 函数
- 2.7.8 FORMPARTS 函数
- 2.7.9 EOMONTH 函数
- 2.8 查询元数据
- 2.8.1 目录视图
- 2.8.2 信息架构视图
- 2.8.3 系统存储过程和函数
- 练习
- CHAPTER 03 联接
- 3.1交叉联接
- 3.1.1 ANSI SQL-92 和 89 语法
- 3.1.2 自交叉联接
- 3.1.3 生成数字表
- 3.2 内部联接
- 3.2.1 ANSI SQL-92、89 语法和安全性
- 3.3 更多联接示例
- 3.3.1 复合联接
- 3.3.2 不等联接
- 3.3.3 多联接查询
- 3.4 外部联接
- 3.4.1 外联接基础知识
- 3.4.2 其他外联接知识
- 练习
- CHAPTER 04 子查询
- 4.1 自包含子查询
- 4.1.1 自包含标量子查询示例
- 4.1.2 自包含多值子查询示例
- 4.2 相关子查询
- 4.3 额外子查询知识
- 4.3.1 返回前一个或下一个值
- 4.3.2 使用运行聚合
- 4.3.3 不当子查询处理
- 练习
Chapter 01 T-SQL 查询和编程背景
1.3 创建表和定义数据的完整性
1.3.1 创建表
- 1USE TSQL2012;
- 2IF OBJECT_ID('dbo.Employees', 'U') IS NOT NULL
- 3 DROP TABLE dbo.Employees;
- 4
- 5CREATE 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为了强制唯一性所使用的一种物理机制。
- 1ALTER TABLE dbo.Employees
- 2 ADD CONSTRAINT PK_Employees
- 3 PRIMARY KEY(empid)
2. 唯一约束
唯一约束强制行的唯一性,允许你在自己的数据库中实现关系模型的备用键概念。与主键不同,可以在同一个表内定义多个唯一约束,此外,唯一约束不限制列必须定义为NOT NULL
。根据SQL标准,具有唯一约束的列应该允许重复的NULL
值。但是,SQL Server则不允许重复的NULL
标记
- 1ALTER TABLE dbo.Employees
- 2 ADD CONSTRAINT UNQ_Employees_ssn
- 3 UNIQUE(ssn);
3. 外键束约
外键用于强制引用的完整性,此约束定义了引用表中的一个或多个属性指向被引用表(父表)中候选键(主键或唯一约束),引用表和被引用表可以是同一个。外键的目的是限制在外键列中允许的值要存在于那些被引用列中。
- 1IF OBJECT_ID('dbo.Orders', 'U') IS NOT NULL
- 2 DROP TABLE dbo.Orders;
- 3CREATE 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
- 14ALTER 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允许定义一个谓词,确保进入到表中的行或是被修改的行必须满足些约束。
- 1ALTER TABLE dbo.Employees
- 2 ADD CONSTRAINT CHK_Employees_salary
- 3 CHECK(salary > 0);
5. 默认约束
如果没有在插入时指定一个显式值时,将会使用该默认值。
- 1ALTER 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个最近的订单:
- 1SELECT TOP 10 orderid, orderdate, custid, empid
- 2FROM Sales.Orders
- 3ORDER BY orderdate DESC;
可以为TOP
选项指定PERCENT
关键字,向上舍入。
- 1SELECT TOP (1) PERCENT orderid, orderdate, custid, empid
- 2FROM Sales.Orders
- 3ORDER BY orderdate DESC
Orders中总共有830行,830 * 1%向上舍入则是9行。
我们可以发现,在前4行的数据中,orderdate是相相同的,在这种没有指定决胜属性(tiebreaker)的情况下,对具有相同值的orderdate排序是没有意义的。SQL Server返回的结果是不确定的,即哪行先被物理访问到就先返回哪行。
如果希望查询结果是确定的,则需要ORDER BY
列出的数据是唯一的,即要添加一个决胜发展(tiebreaker)。如可在ORDER BY
加入orderid作为tiebreaker
- 1SELECT TOP(5) orderid, orderdate, custid, empid
- 2FROM Sales.Orders
- 3ORDER BY orderdate DESC, orderid DESC;
或者使用WITH TIES
作为关联行,来实现tiebreaker
- 1SELECT TOP(5) WITH TIES orderid, orderdate, custid, empid
- 2FROM Sales.Orders
- 3ORDER 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
子句指定在跳过的行数后要筛选的行数。
- 1SELECT orderid, orderdate, custid, empid
- 2FROM Sales.Orders
- 3ORDER BY orderdate, orderid
- 4OFFSET 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子子名为计算结果定义排序。
- 1SELECT orderid, custid, val
- 2 , ROW_NUMBER() OVER(PARTITION BY custid ORDER BY val) AS rownum
- 3FROM Sales.OrderValues
- 4ORDER 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
简单
格式:
- 1SELECT 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
- 9FROM Production.Products
- 10ORDER BY categoryid
复杂
格式:
- 1SELECT 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
- 7FROM Sales.OrderValues
T-SQL 中某些函数可以看作是CASE的表达式缩写,如:ISNULL、COALESCE、IIF和CHOOSE*,其中只有COALESCE是标准的,后两个仅在2012版本中可以使用
2.5 同时操作
- 1SELECT 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通常基于成本估计来进行表达式的计算顺序。所以此语句有可能会查询失败。对于此种特定情况,该语句可改写为
- 1SELECT 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会隐式的将其他字符类型
转换为数字类型
。
- 1SELECT CONCAT(1,'/',5,'a')
- 2SELECT 1+'a'
2.6.2 SUBSTRING 函数
SUBSTRING(string, start, length) 在sql中,string
字符串下标从1开始算起,而不是0。
- 1SELECT 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
- 1SELECT CHARINDEX('cd','CDABCDEFG',2)
2.6.6 PATINDEX 函数
PATINDEX(pattern,string) pattern
与LIKE 模式相同
- 1SELECT PATINDEX('%[0-9]%','abcde53fg')
2.6.7 REPLACE 函数
REPLACE(string, subString1, subString2) 使用subString2
取代string
中所有的subString1
- 1SELECT REPLACE('abcde53fg53','53','11')
2.6.8 REPLICATE 函数
REPLICATE(string, n) 将string
复制n
次
- 1SELECT REPLICATE('abc',3)
2.6.9 STUFF 函数
STUFF(string, pos, del_length, insertString) 从pos
位置开始,删除指定del_length
长度的字符串,并从pos
处插入insertString
字符串
- 1SELECT 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
字符串
- 1SELECT 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 当前时间
- 1SELECT
- 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])
- 1SELECT 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
- 2SELECT SYSDATETIMEOFFSET(), SWITCHOFFSET(SYSDATETIMEOFFSET(), '-05:00')
2.7.4 TODATETIMEOFFSET 函数
TODATETIMEOFFSET(date_and_time_value,time_zone) 设置date_and_time_value为time_zone时区,通常用于迁移非已知偏移量数据到已知偏移数据。
- 1
- 2SELECT TODATETIMEOFFSET(CONVERT(DATETIME,'20160131 20:42'),'+08:00')
2.7.5 DATEADD 函数
DATEADD(part,n,dt_val) 指定日期的某个部分 part 增加 n 到 dt_val中
- 1
- 2SELECT 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
- 2SELECT 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中引入的,它们接受代表日期和时间各个部分的整数值,并根据这些值构建一个所请求类型的值。
- 1SELECT 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个可选参数指示要增加多少个月。
- 1SELECT EOMONTH(DATEADD(mm,1,SYSDATETIME()))
2.8 查询元数据
2.8.1 目录视图
- 1
- 2SELECT SCHEMA_NAME(SCHEMA_ID) AS tableSchemaName, name AS tableName FROM sys.tables
- 3
- 4SELECT 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
- 2SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE'
- 3
- 4
- 5SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = 'Sales' AND TABLE_NAME = 'orders'
2.8.3 系统存储过程和函数
- 1
- 2EXEC sys.sp_tables
- 3
- 4EXEC sys.sp_help @obejctName = 'Sales.Orders'
- 5
- 6EXEC sys.sp_columns @table_name = 'orders', @schema_name = 'Sales'
- 7
- 8EXEC sys.sp_helpconstraint 'Sales.orders'
- 9
- 10SELECT DATABASEPROPERTYEX(N'TSQL2012','Collation')
- 11
- 12SELECT OBJECTPROPERTY(OBJECT_ID('Sales.Orders'),'TableHasPrimaryKey')
- 13
- 14SELECT COLUMNPROPERTY(OBJECT_ID('Sales.Orders'),'orderdate','AllowsNull')
练习
- 1
- 2SELECT * FROM Sales.orders WHERE orderdate >= '20070601' AND orderdate < '20070701' ORDER BY orderdate
- 3
- 4
- 5SELECT * FROM Sales.Orders
- 6WHERE orderdate = EOMONTH(orderdate)
- 7ORDER BY orderdate
- 8
- 9
- 10SELECT * FROM HR.Employees WHERE lastname LIKE '%a%a%'
- 11
- 12
- 13SELECT orderid, SUM(unitprice * qty) AS total
- 14FROM Sales.OrderDetails
- 15GROUP BY orderid
- 16HAVING SUM(unitprice*qty) > 10000
- 17
- 18
- 19SELECT TOP 3 shipcountry, AVG(freight) AS avgFreight1,SUM(freight)/COUNT(*) AS avgFreight2
- 20FROM Sales.Orders
- 21WHERE orderdate >= '20070101' AND orderdate < '20080101'
- 22GROUP BY shipcountry ORDER BY 3 DESC
- 23
- 24
- 25SELECT custid,orderid, orderdate, ROW_NUMBER() OVER (PARTITION BY custid ORDER BY orderdate,orderid ) rownum FROM Sales.orders
- 26
- 27
- 28SELECT *,
- 29 CASE
- 30 WHEN titleofcourtesy='Mrtitleofcourtesy' THEN 'Male'
- 31 WHEN titleofcourtesy in('Mrs.','Ms.') THEN 'Female'
- 32 ELSE 'Unknown' END AS gender
- 33FROM HR.Employees
- 34
- 35
- 36SELECT custid, region FROM Sales.Customers
- 37ORDER BY
- 38 CASE WHEN region IS NULL THEN 1 ELSE 0 END, region
在SQL SERVER 中 NULL 默认为无穷小,所以排序时默认排在前面。
CHAPTER 03 联接
联接的3个基本类型是交叉联接、内部联接和外部联接。交叉联接仅处理阶段——笛卡尔乘积,内部联接应用两个阶段——笛卡尔乘积和筛选,外部联接应用三个阶段——笛卡尔乘积、筛选和添加外部行。
3.1交叉联接
3.1.1 ANSI SQL-92 和 89 语法
两种语法没有逻辑或性能上的差异,但推荐使用92
的SQL语法。
- 1
- 2SELECT custid, empid FROM Sales.Customers CROSS JOIN HR.Employees ORDER BY 1,2
- 3
- 4SELECT custid, empid FROM Sales.Customers ,HR.Employees ORDER BY 1,2
3.1.2 自交叉联接
可以对一个表的多个实例进行联接,支持联接的基本类型,这功能就是自联接。
- 1
- 2SELECT * FROM HR.Employees e1 CROSS JOIN HR.Employees e2
3.1.3 生成数字表
自联接生成整数数列(1,2,3....)结果集非常方便。
- 1--总共10*10*10行,生成从0-999的数
- 2SELECT 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
- 2SELECT e.empid , o.orderid
- 3FROM Sales.Orders o
- 4 INNER JOIN HR.Employees e ON o.empid = e.empid
- 5 ORDER BY 1,2
- 6
- 7SELECT e.empid , o.orderid FROM Sales.Orders o, HR.Employees e
- 8WHERE o.empid = e.empid
- 9ORDER BY 1,2
92
语法具有在联接安全性方面比较好,比如忘记写on
后面的联接条件时,SQL语句会运行异常,以便您对SQL进行修改。然而对于89
语法来说,如果忘记在WHERE
后添加条件,此时SQL也可以进行有效查询,返回了错误的结果集,并且由于查询不会失败,SQL可能会运行一段时间。而且,其他维护人员也不会知道些SQL语句是交叉联接还是内联接。所以,建议使用92
语法的SQL。
3.3 更多联接示例
3.3.1 复合联接
复合联接就是在ON
之后涉及多个属性的简单联接。
- 1FROM table1 AS t1 INNER JOIN table2 t2 ON t1.col1 = t2.col1 AND t1.col2 = t2.col2
3.3.2 不等联接
联接条件除了=
号之外还有其他任何运算符的联接即为不等联接
- 1
- 2SELECT 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
子句中出现多个表运算符时,表运算符会从左到右进行逻辑处理。也就是说,第一个运算符生成的结果集将会被视为第二个运算符的左侧驱动表,以此类推。
- 1SELECT c.custid, c.companyname, o.orderid, od.productid, od.qty
- 2FROM 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
则是两侧的行都需要保留。
- 1SELECT c.custid, c.companyname, O.orderid
- 2FROM Sales.Customers c
- 3 LEFT OUTER JOIN Sales.Orders O ON c.custid = O.custid
- 4ORDER 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
- 2SELECT empid, firstname,lastname, n.n FROM HR.Employees e CROSS JOIN Nums n WHERE n < 6
- 3
- 4
- 5SELECT 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
- 8SELECT 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
- 11SELECT c.custid, c.companyname, o.orderid, o.orderdate FROM Sales.Customers c LEFT JOIN Sales.Orders o ON c.custid = o.custid
- 12
- 13
- 14SELECT 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
- 2DECLARE @maxId INT = (SELECT MAX(orderid) FROM Sales.Orders)
- 3SELECT * FROM Sales.Orders WHERE orderid = @maxId
- 4
- 5
- 6SELECT * FROM Sales.Orders WHERE orderid = (SELECT MAX(orderid) FROM Sales.Orders)
如果子查询没有返回值,则返回 NULL 。而与NULL的都会生成 NUKNOWN, 然而查询筛选是不返回筛选表达式试算为 UNKNOWN 的行。
4.1.2 自包含多值子查询示例
多值子查询是作为单个列,返回多个值的子查询,无论子查询是否是自包含的。可使用IN谓词进行多值子查询操作。
没有明确的经验方法表示子查询比联接要好,所以,在写SQL时先以直观的形式对指定任务写一个解决方案查询,如果性能不满意,可以尝试查询调教来进行调优,如:使用子联接代替子查询或是使用子查询代替联接
。
在子查询中可以不使用 DISTINCT 子句进智能行去重从而提高性能,因为数据库引擎足够智能,会考虑删除重复项,不用显式地去告诉它这样做。
4.2 相关子查询
相关子查询引用的表属性位于外部查询中,这意味着,该子查询依赖于外部查询并且无法单独调用。
- 1SELECT * 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 是一个两值逻辑运算。
- 1SELECT 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
- 2SELECT 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
- 4SELECT 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 使用运行聚合
运行聚合是随着时间累积值的聚合。
- 1SELECT 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
- 2SELECT * FROM Sales.Customers WHERE custid NOT IN ( SELECT custid FROM Sales.Orders )
- 3
- 4SELECT TOP 1 'Have Value' FROM Sales.Customers WHERE 3 NOT IN (1,2)
- 5
- 6SELECT TOP 1 'Have Value' WHERE 3 NOT IN (1,2,null)
- 7
- 8SELECT '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
- 2SELECT * FROM Sales.Orders o1
- 3WHERE o1.orderdate = (SELECT MAX(orderdate) FROM Sales.Orders )
- 4
- 5
- 6SELECT * FROM Sales.Orders
- 7WHERE custid IN (SELECT TOP 1 WITH TIES custid FROM Sales.Orders GROUP BY custid ORDER BY COUNT(*) DESC)
- 8
- 9
- 10SELECT * FROM HR.Employees
- 11WHERE empid NOT IN (SELECT o1.empid FROM Sales.Orders o1 WHERE o1.orderdate > '20080501')
- 12
- 13
- 14SELECT DISTINCT country FROM Sales.Customers
- 15WHERE country NOT IN (SELECT country FROM HR.Employees) ORDER BY 1
- 16
- 17
- 18SELECT o1.custid,o1.orderdate,o1.empid FROM Sales.Orders o1
- 19WHERE o1.orderdate = (SELECT MAX(o2.orderdate) AS orderdate FROM Sales.Orders o2 WHERE o1.custid = o2.custid )
- 20ORDER BY 1
- 21
- 22
- 23SELECT * FROM Sales.Customers c
- 24WHERE EXISTS (SELECT o1.custid FROM Sales.Orders o1 WHERE orderdate > '20070101' AND orderdate < '20080101' AND o1.custid = c.custid )
- 25AND NOT EXISTS (SELECT o2.custid FROM Sales.Orders o2 WHERE orderdate > '20080101' AND orderdate < '20090101' AND o2.custid = c.custid)
- 26
- 27
- 28SELECT * FROM Sales.Customers c WHERE EXISTS (
- 29SELECT * FROM Sales.Orders o WHERE EXISTS(
- 30SELECT * FROM Sales.OrderDetails od WHERE productid = 12 AND od.orderid = o.orderid) AND o.custid = c.custid )
- 31
- 32
- 33SELECT *, (SELECT SUM(c2.qty) FROM Sales.CustOrders c2 WHERE c2.ordermonth <= c1.ordermonth AND c2.custid = c1.custid)
- 34FROM Sales.CustOrders c1 ORDER BY 1