算法工程师-SQL进阶:集合之间的较量
集合在数据库领域表示记录的集合。SQL是一门面向集合的语言,四则运算里的和、差、积已经加入到标准SQL,但由于其标准化进程比较缓慢,一些集合运算在主流的数据库如MySQL、HiveSQL中还未实现。
本节给大家介绍,SQL中集合运算的使用方法及其在使用中需要注意的地方,分为两个部分:
第一部分,讲解基础概念,熟悉的同学可以跳过;
第二部分,结合实际案例,介绍集合运算的SQL解法和实现思路;
目录:
一、概念篇
所谓集合运算,就是对满足同一规则的记录进行的加减等四则运算。通过集合运算,可以得到两张表中记录的集合或者公共记录的集合,又或者其中某张表中的记录的集合。
1、并集
并集
指的是两个集合a与b的加法运算,结果是:既包含集合a中所有元素又包含集合b所有元素的集合。
并集
运算可以用UNION实现,比如求两个表的并集:
SELECT *
FROM table_a
UNION
SELECT *
FROM table_b;
UNION运算会将两个集合的结果进行排序并去重,当表比较大时,会有性能损耗。
如果用UNION ALL代替UNION,它便不会对并集进行排序和去重操作,也就是说,UNION ALL的结果会保留重复的元素(记录)。
用集合运算符时的注意事项:
- 作为运算对象的记录的列数必须相同;
- 作为运算对象的记录中列的类型必须一致;
注:这些注意事项不仅限于 UNION,之后将要学习的所有集合运算符都要遵守这些注意事项。
2、差集
差集
指的是两个集合a与b的减法运算,如果是a-b,含义就是:在集合a中剔除掉同时出现在集合b中的元素。
差集
运算可以用EXCEPT实现,比如求两个表的差集:
SELECT *
FROM table_a EXCEPT
SELECT *
FROM table_b;
遗憾的是,MySQL和Hive SQL中都不支持EXCEPT,但是,我们可以用左外连接/右外连接
来代替。
-- 用左连接实现a-b
SELECT *
FROM table_a
LEFT OUTER JOIN table_b ON table_a.id = table_b.id
WHERE table_b.id IS NULL;
-- 用右连接实现b-a
SELECT *
FROM table_a
RIGHT OUTER JOIN table_b ON table_a.id = table_b.id
WHERE table_a.id IS NULL;
3、交集
交集
指的是,同时出现在两个集合a与b的元素集合。
交集
运算可以用INTERSECT实现,比如求两个表的交集:
SELECT *
FROM table_a INTERSECT
SELECT *
FROM table_b;
遗憾的是,MySQL和Hive SQL中都不支持INTERSECT,但是,我们可以用内连接
来代替。
SELECT *
FROM table_a
INNER JOIN table_b ON table_a.id = table_b.id;
4、异或集
异或集
指的是,集合a与集合b的交集的补集,即,排除掉同时出现在集合a和集合b的元素后,其他所有元素的集合。
异或集
没有特定的关键字来实现,全外连接结合判空逻辑可以实现该功能。
SELECT *
FROM table_a
FULL OUTER JOIN table_b ON table_a.id = table_b.id
WHERE table_a.id IS NULL
OR table_b.id IS NULL;
6、乘积运算:笛卡尔积
乘积运算
,指的是两个集合的笛卡尔积
(CROSS JOIN
)。如果一个集合a有10个元素,另一个集合b有5个元素,那么通过笛卡尔积
可以得到10*5=50个元素,每个元素都是(a_item,b_item)这种pair形式。
笛卡尔积通过CROSS JOIN实现:
SELECT *
FROM table_a
CROSS JOIN table_b;
5、关系除法运算
关系除法运算
,属于中级内容,集合a/集合b的含义大概是,筛选出集合a中包含集合b所有元素的组,即商。
关系除法运算
是本文的重点,在第二部分【实战篇】中结合具体实例进行深入讲解。
二、实战篇
案例:
有如下两张候选人技能管理表,skills
是公司规定的候选人必会技能表,candidate_skills
是2021秋招候选人的技能明细表。
< skills >
+------------+
| skill_name |
+------------+
| python |
| spark |
| hive |
+------------+
< candidate_skills >
+---------+-----------+------------+
| user_id | user_name | skill_name |
+---------+-----------+------------+
| 1 | 刘红 | python |
| 1 | 刘红 | spark |
| 1 | 刘红 | hive |
| 1 | 刘红 | c++ |
| 2 | 肖磊 | python |
| 2 | 肖磊 | tensorflow |
| 2 | 肖磊 | spark |
| 2 | 肖磊 | hive |
| 3 | 王芳 | tensorflow |
| 3 | 王芳 | python |
| 3 | 王芳 | spark |
| 4 | 张阳 | python |
| 4 | 张阳 | c++ |
| 4 | 张阳 | spark |
| 5 | 李鑫 | spark |
| 5 | 李鑫 | python |
| 5 | 李鑫 | tensorflow |
+---------+-----------+------------+
1、简单集合运算
Eg1: 请筛选出会使用大数据开发工具(掌握hive或spark),但不会tensorflow的候选人集合。
分析: 可以先通过UNION运算
求出掌握大数据技能的全部候选人,然后进行INTERSECT
运算:减去掌握tensorflow的候选人。
参考SQL:
SELECT BigData.user_name
FROM
(SELECT user_name
FROM candidate_skills
WHERE skill_name="spark"
UNION SELECT user_name
FROM candidate_skills
WHERE skill_name = 'hive') BigData -- 掌握大数据技术的后续人集合
LEFT JOIN -- 利用left join求差集
( SELECT user_name
FROM candidate_skills
WHERE skill_name="tensorflow") Tensorflow ON Tensorflow.user_name = BigData.user_name
WHERE Tensorflow.user_name IS NULL;
结果:
+-----------+
| user_name |
+-----------+
| 刘红 |
| 张阳 |
+-----------+
2、集合是否相等
想一想,我们一般通过什么原则判断两个集合相等呢?
首先,如果有一个集合S,则S UNION S = S
和 S INTERSECT S = S
都是成立的,类似这种S * S = S
的性质,我们称之为幂等性
。UNION和INTERSECT都具有幂等性。
如果集合 A 和集合 B 相等,那么 A = B = A UNION B = A INTERSECT B,这个条件是A=B的充分必要条件
。因此,可以通过检测两个集合的并集和交集数量是否一致来检测它们是否相等。
Eg2: 请检查:掌握spark的候选人和掌握hive的候选人是不是同一批人?
分析: 先分别求出掌握两个技能的候选人集合s1、s2,然后分别求出s1与s2的交集和并集,如果交集和并集的数量相等,则s1和s2相等。
SQL参考:
SELECT
(SELECT count(*) from
(SELECT user_name
FROM candidate_skills
WHERE candidate_skills.skill_name ='spark'
UNION SELECT user_name
FROM candidate_skills
WHERE candidate_skills.skill_name ='hive') U) AS union_num,
(SELECT count(*)
FROM
(SELECT DISTINCT A.user_name
FROM candidate_skills A
INNER JOIN candidate_skills B ON A.user_name = B.user_name
WHERE A.skill_name='spark'
AND B.skill_name='hive') I) AS inter_num;
结果:
+-----------+-----------+
| union_num | inter_num |
+-----------+-----------+
| 5 | 2 |
+-----------+-----------+
解析:
上面的SQL用了两个标量子查询,分别求出s1和s2的并集元素数量和交集元素数量,很显然,4 != 2,可知掌握这两种技能的人并不是相同一批人(两个集合不同)。值得注意的是,求s1和s2的交集时,并没有采用不通用的intersect
,而是使用了inner join
实现,其中也运用了自连接
方法,思路比较巧妙,这种方法Hive和MySQL都能支持。
3、关系除法
Eg3: 请筛选出符合该公司技能要求
的全部候选人。
分析:
针对每个候选人逐一筛选,从规定的技术集合中减去该候选人自己的技术的集合,如果结果是空集
,则说明该候选人具备所要求的全部技术,否则说明该候选人不具备某些要求的技术。
参考SQL:
-- 用求差集的方法进行关系除法运算(有余数)
SELECT DISTINCT user_name
FROM candidate_skills S1
WHERE NOT EXISTS
(SELECT skill_name
FROM skills -- 所要求的全部技术
EXCEPT --相减,如果结果不为空,则存在没有掌握的必备技术
SELECT skill_name
FROM candidate_skills S2 -- 该候选人自己具备的全部技术
WHERE S1.user_name = S2.user_name);
结果:
+-----------+
| user_name |
+-----------+
| 刘红 |
| 肖磊 |
+-----------+
解析:
上面这段SQL,采用减法的思想去做除法,即让除数集合减去个人技能集合,如果剩余为空,则这个人掌握全部要求的技能(除数中的技能)。有个缺点就是,上面的SQL用到了expect这个不通用的减法运算符,如果改写成left join的减法形式,需要在exists() 中既要嵌套一层关联子查询,又要通过left join skills实现减法逻辑,实现起来比较复杂。其实,还有下面的方法可以实现关系除法运算。
【扩展】使用 HAVING
子句转换成一对一关系
SELECT user_name
FROM candidate_skills A
INNER JOIN skills ON A.skill_name = skills.skill_name -- 通过内连接,只保留公司要求的技能项
GROUP BY user_name HAVING count(*) = -- 按用户分组,检查每个用户在公司要求的技能范围内的技能数目是否达标
(SELECT count(*)
FROM skills);
4、寻找相等的子集
Eg4: 请筛选出技能栈完全相同的候选人。
分析:
我们可以将此问题拆解为两步。
第一步:求出有公共技能的候选人组合(pair);
第二步:验证左右两位候选人的公共技能,是否是其全部掌握的技能,如果都满足,则这两个候选人的技能栈是完全相同的。
参考SQL:
SELECT A.user_name,
B.user_name
FROM candidate_skills A,
candidate_skills B
WHERE A.user_id < B.user_id
AND A.skill_name = B.skill_name
GROUP BY A.user_name,
B.user_name -- 到这里,完成第一步:求出有公共技能的候选人组合(pair)
HAVING count(*) = -- 通过having 对每组匹配候选人依次判断
(SELECT count(*)
FROM candidate_skills C
WHERE C.user_name = A.user_name) -- 这里检查第一个候选人的全部技能数量是否等于两人功能的技能数
AND count(*) =
(SELECT count(*)
FROM candidate_skills D
WHERE D.user_name = B.user_name); -- 这里检查第二个候选人的全部技能数量是否等于两人功能的技能数
结果:
+-----------+-----------+
| user_name | user_name |
+-----------+-----------+
| 王芳 | 李鑫 |
+-----------+-----------+
解析:
通过候选人两两配对后,筛出共同技能,然后在分组筛选逻辑having中,通过关联子查询依次判断每个人是否符合条件,最终得到技能栈完全相同的候选人pair。这种方法通用且灵活,可以在实际中尝试使用并根据具体情景加以扩展。
三、总结
本节,我们学习了SQL中集合运算
的使用方法。关于集合运算
,SQL 的标准化进行得比较缓慢,所以尽管集合运算可以用来解决很多问题,但是很多人并不知道。
- 在集合运算方面,SQL 的标准化进行得比较缓慢,直到现在也是实现状况因数据库不同而参差不齐,因此使用的时候需要注意。
- 差集和交集运算可以用
内连接
和左(右)外连接
来间接实现。 - 如果UNION不指定 ALL 可选项,重复行会被排除掉,而且,这种情况下还会发生排序,所以性能方面不够好。
- 标准 SQL 没有
关系除法
的运算符,需要自己实现,可以通过having完成,或者将除法运算转换为减法运算来实现。 - 判断两个集合是否相等时,可以通过
幂等性
等方法。
- 浅谈用Python计算文本BLEU分数
- Fourinone如何实现并行计算和数据库引擎
- 在Python中用一个长短期记忆网络来演示记忆
- CDA数据分析师学习之路第3期 | Spark RDD的转换操作举例
- 通过Temboo实现从Arduino获取雅虎天气信息
- 自动化模式中的MySQL
- 通过Pandas实现快速别致的数据分析
- R语言中的非线性分类
- 用SPSS做数据分析?先弄懂SPSS的基础知识吧
- 学习笔记CB001:NLTK库、语料库、词概率、双连词、词典
- 时序列数据库武斗大会之 OpenTSDB 篇
- 应当使用 SQLite 的五个原因
- Apache Spark作为编译器:深入介绍新的Tungsten执行引擎
- DC/OS 的安装与部署
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 各类SQL日期时间处理方法
- 这可能是最全最实用的Vim操作集合
- 个人珍藏的80道多线程并发面试题(1-10答案解析)
- 理解DOM Diff算法
- 腾讯云ES集群通过COS实现跨地域备份与恢复
- Thymeleaf一篇就够了
- python scipy.stats实现各种常见的统计分布
- 操作系统基础 - 文件系统
- Linux升级OpenSSH修复高危漏洞
- Pytest之收集用例及命令行参数
- CVE-2020-14645:Weblogic远程代码执行复现
- 从0到1开发测试平台(七)后端服务添加swagger第三方类库
- leetcode - 分割数组的最大值
- leetcode - 除数博弈
- leetcode - 最小路径和