算法工程师-SQL进阶:集合之间的较量

时间:2022-07-22
本文章向大家介绍算法工程师-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 = SS 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完成,或者将除法运算转换为减法运算来实现。
  • 判断两个集合是否相等时,可以通过幂等性等方法。