对于开窗函数一般的解释是"用于’分区’或’分组’计算的函数".联想到聚集函数,同样是为分组而准备的函数(与GROUP BY
子句连用),这两者有什么区别呢?
作为 ISO SQL 支持的开窗函数与聚集函数在使用聚合函数计算上其实差别不大, 其区别主要出现在两个部分:一是在结果显示上上面,聚集函数计算出的结果是按组别在一行输出的,而开窗函数是在每一行上分别输出;二是可以利用开窗,使用排名开窗函数获取行的排名值,在OLAP系统和Web应用排序上有较多的应用.另外,两者还在使用形式上有所不同,在编写语句时,开窗需要在SELECT
子句中针对列声明,聚集是在主句后使用GROUP BY
指定目标列.
使用开窗需要用到OVER
和PARTITION BY
关键字,基本使用方法是在SELECT
中使用函数名(计算列) OVER(PARTITION BY 开窗列) AS 别名
.
聚合开窗与聚合函数的使用基本一致,这里使用一个实例来说明,有一张商品表,其中的存储(商品名, 商品类别, 价格)等数值:
商品名 | 商品类别 | 价格 |
---|---|---|
a | X | 2 |
b | X | 3 |
c | X | 4 |
d | Y | 9 |
e | Y | 11 |
使用聚合函数获取同种类别商品价格平均价格,价格之和,最高价格的语句是:
SELECT 商品类别,
AVG(价格) AS 平均价格,
SUM(价格) AS 价格之和,
MAX(价格) AS 最高价格
FROM 商品表 GROUP BY 商品类别
结果是:
商品类别 | 平均价格 | 价格之和 | 最高价格 |
---|---|---|---|
X | 3 | 9 | 4 |
Y | 10 | 20 | 11 |
如果使用开窗函数查询上述数据,语句应是:
SELECT 商品类别,
AVG(价格) OVER(PARTITION BY 商品类别) AS 平均价格,
SUM(价格) OVER(PARTITION BY 商品类别) AS 价格之和,
MAX(价格) OVER(PARTITION BY 商品类别) AS 最高价格,
FROM 商品表
结果是:
商品类别 | 平均价格 | 价格之和 | 最高价格 |
---|---|---|---|
X | 3 | 9 | 4 |
X | 3 | 9 | 4 |
X | 3 | 9 | 4 |
Y | 10 | 20 | 11 |
Y | 10 | 20 | 11 |
上述这些冗余的结果十分令人费解,但我们在查询的时候将商品名标识出来,就比较好理解了:
SELECT 商品名, 商品类别,
AVG(价格) OVER(PARTITION BY 商品类别) AS 平均价格,
SUM(价格) OVER(PARTITION BY 商品类别) AS 价格之和,
MAX(价格) OVER(PARTITION BY 商品类别) AS 最高价格,
FROM 商品表
结果是:
商品名 | 商品类别 | 平均价格 | 价格之和 | 最高价格 |
---|---|---|---|---|
a | X | 3 | 9 | 4 |
b | X | 3 | 9 | 4 |
c | X | 3 | 9 | 4 |
d | Y | 10 | 20 | 11 |
e | Y | 10 | 20 | 11 |
可以看出,使用开窗函数,原来聚集函数每一组一行的数据显示方式变成了每一条记录显示一行了.
排名开窗是开窗的重要用法,主要排名函数有四个.这一部分使用一列(1, 2, 3, 1)数据来简要说明.使用排名函数,待排序的列名不是在函数的参数中指定,而是在开窗中说明.需要注意的是,开窗函数的排名方式不是在ORDER BY
子句中声明,而是在带排名列后标注.
RANK()
RANK()
排名时会考虑到值相同的情况,也就是并列,在并列后会按照绝对位置继续排名,换句话说,排名出来的数字不是连续的.
例如SELCET value, RANK() OVER(PARTITION BY value ASC) AS RANK排名 FROM Values
的结果是
value | RANK排名 |
---|---|
1 | 1 |
1 | 1 |
2 | 3 |
3 | 4 |
DENSE_RANK()
DENSE_RANK()
排名时也会考虑到并列的情况,但其返回值是连续不间断的.
例如SELCET value, DENSE_RANK() OVER(PARTITION BY value ASC) AS DENSE_RANK排名 FROM Values
的结果是
value | DENSE_RANK排名 |
---|---|
1 | 1 |
1 | 1 |
2 | 2 |
3 | 3 |
ROW_NUMBER()
此函数排名时不考虑并列的情况,其结果自然也是连续的.
例如SELCET value, ROW_NUMBER() OVER(PARTITION BY value ASC) AS ROW_NUMBER排名 FROM Values
的结果是
value | ROW_NUMBER排名 |
---|---|
1 | 1 |
1 | 2 |
2 | 3 |
3 | 4 |
NTILE()
NTILE()
函数比较特殊,是在有序分区中划分组来排名,需要在函数参数里指明分组的数目,排名结果是组所排名的结果.
例如将前面表格分为两组排名,使用SELCET value, NTILE(2) OVER(PARTITION BY value ASC) NTILE排名 FROM Values
的结果是
value | NTILE排名 |
---|---|
1 | 1 |
1 | 1 |
2 | 2 |
3 | 2 |
子查询是在SELECT
语句中使用另一个SELECT
语句,也被称作内层查询,其外层查询也被称作主查询.内外层查询没有固定的先后查询顺序,根据不同的查询语句情况(语义)有不同的查询顺序.
嵌套子查询,WHERE/HAVING
子查询,这种内层查询(子查询)先于外层(主)进行.基于集合的嵌套子查询,WHERE 表达式 [NOT] IN (子查询)
.比较嵌套子查询,WHERE 表达式 比较运算符 (使用聚合函数返回单值的子查询)
.以上查询又称作不相关子查询,测试子查询.
这种查询先进行内层查询,查询出一个集合(WHERE ... IN ...
)或者查出一个数(WHERE 比较条件
),然后再进行外部查询,外部的每一行执行WHERE
中的比较语句,检查结果是否符合条件,若符合,则该记录进入最终的结果集,不符合则弃用.
例如利用子查询方式查询和A商品同类的商品:
SELECT 商品名, 价格, 类别 FROM 商品表
WHERE 类别 IN (
SELECT 类别 FROM 商品表 WHERE 商品名 = 'A'
) AND 商品名 != 'A'
相关子查询虽然是用在WHERE/HAVING
中,但执行顺序是先执行主查询,再主查询中逐行进行子查询,根据子查询的值决定主查询中当前行是否返回在结果集合中.这个子查询的值是布尔值,形式为WHERE [NOT] EXISTS(子查询)
.此种查询也称为存在性测试子查询.
在这种查询方式中,子查询不同于嵌套子查询只执行一次,而是会执行很多次的.执行的次数根据主查询返回的原始结果集决定.子查询返回布尔值的逻辑是由子查询中的WHERE
决定的.可以理解为先产生主查询语句中查出来的行,在主查询完成后,逐行进行子查询.
例如利用相关子查询查找单次消费大于一万元的客户姓名:
SELECT DISTINCT 客户表.姓名 FROM 客户表
WHERE EXISTS (
SELECT * FROM 销售明细表
WHERE 销售明细表.销售额 > 10000
AND 销售明细表.客户编号 = 客户表.客户编号
)
替代表达式子查询(SELECT
子查询)通常利用子查询中聚合函数返回一个标量值.其作用貌似是如同相关子查询一样利用主查询的每一行进行查找,然而事实上其查询条件是在查询之前就写死再子查询语句中的,与主查询无关,通常是只查询某一个记录时使用.其查询结果将作为一列呈现在结果中.
例如查询客户编号为10086的客户名,地址和累计消费金额:
SELECT 姓名, 地址, (
SELECT SUM(销售额) FROM 销售明细表
JOIN 客户表 ON 客户表.客户编号 = 销售明细表.客户编号
WHERE 客户表.客户编号 = 10086
) FROM 客户表
WHERE 客户编号 = 10086
派生表子查询(FROM子查询
)作用类似于临时表,在主查询进行前先进性子查询,而后主查询以子查询返回的表作为数据源继续查询,可以理解为临时表的性能优化方式和一种方便的用法,其形式为FROM (子查询)
或者FROM (子查询|表) [各种连接类型] JOIN (子查询 | 表)
.其性能优化之处在于不必手动生成临时表,产生的派生表在内存中用完即焚,避免在SQL Server调用tempdb
数据库,减少I/O带来的性能损失.
例如查询客户编号为10086和10010的两个用户都购买了的商品有哪些:
SELECT 商品表.商品编号, 商品表.商品名
FROM (
SELECT * FROM 销售明细表 WHERE 客户编号 = 10086
) AS T1 JOIN (
SELECT * FROM 销售明细表 WHERE 客户编号 = 10010
) AS T2 ON T1.商品编号 = T2.商品编号
JOIN 商品表 ON T1.商品编号 = 商品表.商品编号
需要注意的是,在WHERE
子句中使用聚集函数的值不可以直接调用聚集函数,需要使用子查询的方式调用.聚合函数不能直接出现在WHERE
子句中,需要时必须先利用子查询获取结果.
读者会发现,这一部分许多的查询其实可以使用联接查询或修改WHERE
条件来实现.的确如此,但在许多复杂地查询需求中,联接查询和WHERE
条件并不能高效易懂地完成任务,还是需要子查询来帮忙的.在数据量大,事务多的情况下,查询的性能表现十分重要,子查询和联接查询等等查询方法具体用哪一个,需要结合业务逻辑,数据表结构,索引甚至是物理文件等等因素具体分析.
使用子查询无法提高连接查询的性能,放在存储过程中也无法减少运算量,也无法提升查询效率.在一定程度上可以说,只要使用到了子查询,性能一定会有折扣(联接查询同理).为了改善查询性能,就需要在进行数据库物理设计阶段,将符合3NF的关系模式进行适度的合并,人为增加一些冗余,合理地,成本最小化地用空间换时间.
[1]何玉洁, 刘乃嘉. 全国计算机等级考试三级教程-数据库技术[M]. 高等教育出版社. 2020.
[2]Ben Forta. SQL必知必会[M]. 人民邮电出版社. 2020.
[3]史嘉权. 数据库系统概论[M]. 清华大学出版社. 2006.
[4]褚华, 霍邱艳. 软件设计师教程[M]. 清华大学出版社. 2018.
[5]王珊, 陈红. 数据库系统原理教程[M]. 清华大学出版社. 1998.
[6]汤小丹, 梁红兵, 哲凤屏, 汤子瀛. 计算机操作系统[M]. 西安电子科技大学出版社. 2014.