高频 SQL 50 题(基础版)- 06. 子查询

359 阅读11分钟

来源:高频 SQL 50 题(基础版)leetcode.cn/studyplan/s…

1978. 上级经理已离职的公司员工

查找这些员工的 id,他们的薪水严格少于 $30000 并且他们的上级经理已离职。当一个经理离开公司时,他们的信息需要从员工表中删除掉,但是表中的员工的 manager_id 这一列还是设置的离职经理的 id 。

返回的结果按照 employee_id 从小到大排序。

查询结果如下所示:

思路 + SQL

  1. 先搜索出上级经理已经离职的员工,即 manager_id 在员工表里找不到对应数据的员工:
select
  employee_id
from
  Employees
where
  manager_id not in (
    select
      employee_id
    from
      Employees
  );
  1. 组合薪水少于 30000,加上 employee_id 排序,即可得到结果:
select
  employee_id
from
  Employees
where
  salary < 30000
  -- 上司已离职的条件
  and manager_id not in (
    select
      employee_id
    from
      Employees
  )
order by
  employee_id;

626. 换座位

编写解决方案来交换每两个连续的学生的座位号。如果学生的数量是奇数,则最后一个学生的id不交换。

按 id 升序 返回结果表。

查询结果格式如下所示。

思路 + SQL

这里是相邻两个人,两两相互换座位,也就是 id 交换一下。

以 1 号和 2 号为例,1 号 + 1 变为 2 号,2 号 - 1 变为 1 号,就做好交换了。

但是在奇数数量的时候,需要判断多出来的那单独的最后一个人,那个人不需要换座位。

select
  if(
    -- 如果是偶数,直接 -1,奇数得做后续操作
    id % 2 = 0,
    id - 1,
    if(
      -- 如果该奇数是最后一位,不需要换座位,其他情况的奇数 +1
      id = (
        select
          count(id)
        from
          Seat
      ),
      id,
      id + 1
    )
  ) as id,
  student
from
  Seat
order by
  id;

1341. 电影评分

请你编写一个解决方案:

  • 查找评论电影数量最多的用户名。如果出现平局,返回字典序较小的用户名。
  • 查找在 February 2020 平均评分最高 的电影名称。如果出现平局,返回字典序较小的电影名称。

字典序 ,即按字母在字典中出现顺序对字符串排序,字典序较小则意味着排序靠前。

返回结果格式如下例所示。

示例 1:

思路 + SQL

  1. 首先,先做第一步,关联用户表和评分表:
select
  *
from
  Users u
  inner join MovieRating mr on u.user_id = mr.user_id;
  1. 对上面的结果稍作修改,并做为表,通过此表搜索出评论电影数量最多的用户名。
select
  name,
  user_id,
  count(*) as rating_count
from
  (
    select
      u.name,
      u.user_id
    from
      Users u
      inner join MovieRating mr on u.user_id = mr.user_id
  ) t
group by
  user_id
order by
  rating_count desc,
  name asc
limit
  1;
  1. 解决第二个问题,需要关联电影列表以及评分表:
select
  *
from
  Movies m
  inner join MovieRating mr on m.movie_id = mr.movie_id;
  1. 同样的,对上面的结果稍作修改,用来查询 2020 年 2 月的电影平均分中+字典序第一位的电影
select
  movie_id,
  title,
  avg(rating) as avg_rating
from
  (
    select
      m.movie_id,
      m.title,
      mr.rating
    from
      Movies m
      inner join MovieRating mr on m.movie_id = mr.movie_id
    where
      created_at like '2020-02-%'
  ) t
group by
  movie_id
order by
  avg_rating desc,
  title asc
limit
  1;
  1. 将上面的两个查询结果声明为临时表,然后用 union all 将两个查询连接起来:
-- 【with xxx as 查询】起手,说明要声明临时表
with most_rate_user as (
  -- 评论最多 + 字典序最靠前用户的查询结果
  select
    name,
    user_id,
    count(*) as rating_count
  from
    (
      select
        u.name,
        u.user_id
      from
        Users u
        inner join MovieRating mr on u.user_id = mr.user_id
    ) t
  group by
    user_id
  order by
    rating_count desc,
    name asc
  limit
    1
), highest_rate_movie as (
  -- 2022 年 2 月评分最高 + 字典序最靠前的电影查询结果
  select
    movie_id,
    title,
    avg(rating) as avg_rating
  from
    (
      select
        m.movie_id,
        m.title,
        mr.rating
      from
        Movies m
        inner join MovieRating mr on m.movie_id = mr.movie_id
      where
        created_at like '2020-02-%'
    ) t
  group by
    movie_id
  order by
    avg_rating desc,
    title asc
  limit
    1
)
-- 以上面的两个临时表为基础进行简单的查询
select
  name as results
from
  most_rate_user
-- 使用 union all,避免用户名和电影名一样导致结果被合并
union all
select
  title as results
from
  highest_rate_movie;

1321. 餐馆营业额变化增长

你是餐馆的老板,现在你想分析一下可能的营业额变化增长(每天至少有一位顾客)。

计算以 7 天(某日期 + 该日期前的 6 天)为一个时间段的顾客消费平均值。average_amount 要 保留两位小数

结果按 visited_on 升序排序

返回结果格式的例子如下。

思路 + SQL 1 (笛卡尔积 + 分组计算)

因为要计算 7 天销售额,所以需要从第 7 天开始才开始计算

所以可以先让获取有消费日期对应的日期表(distinct Customer 表中的 visited_on 就可以轻松获得),然后和 Customer 表做笛卡尔积,并做相关的数据过滤(顾客的消费日期范围在日期表指定日期的当天 + 前六天)。

同时关联用户最早消费的日期,用于判断最早日期后的第 7 天是哪一天(最早消费日期起的 7 天为第一个时间段)。

筛选后,得到左侧日期为统计日期下,对应的 7 天内销售数据。

select
  *
from
  -- 开店的所有日期表
  (
    select
      distinct visited_on
    from
      Customer
  ) t1,
  Customer t2,
  -- 第一天日期(用于计算是否是第七天开始)
  (
    select
      min(visited_on) as min_date
    from
      Customer
  ) t3
where
  -- 过滤条件 1:统计的日期 >= 消费者访问日期,但不大于 7 天
  dateDiff(t1.visited_on, t2.visited_on) <= 6
  and dateDiff(t1.visited_on, t2.visited_on) >= 0
  -- 过滤条件 2:统计日期在最早记录日期算起的第 7 天及往后
  and dateDiff(t1.visited_on, min_date) >= 6
order by
  t1.visited_on;

最后 group by 一下日期,用聚合函数计算总数和平均值即可得到答案:

select
  t1.visited_on,
  sum(t2.amount) as amount,
  round(sum(t2.amount) / 7, 2) as average_amount
from
	-- 开店的所有日期表
  (
    select
      distinct visited_on
    from
      Customer
  ) t1,
  Customer t2,
  -- 第一天日期(用于计算是否是第七天开始)
  (
    select
      min(visited_on) as min_date
    from
      Customer
  ) t3
where
	-- 过滤条件 1:当天的日期 >= 消费者访问日期,但不大于 7 天
  dateDiff(t1.visited_on, t2.visited_on) <= 6
  and dateDiff(t1.visited_on, t2.visited_on) >= 0
  -- 过滤条件 2:当天日期在最早记录日期算起的第 7 天及往后
  and dateDiff(t1.visited_on, min_date) >= 6
group by
  t1.visited_on
order by
  t1.visited_on;

思路 + SQL 2 (思路 1 的 SQL 优化)

下面写法的优化,更符合常规写法。

select
  t1.visited_on,
  sum(t2.amount) as amount,
  round(sum(t2.amount) / 7, 2) as average_amount
from
  -- 开店的所有日期表
  (
    select
      distinct visited_on
    from
      Customer
  ) t1
  -- 连接条件:当天的日期 >= 消费者访问日期,但不大于 7 天
  cross join Customer t2
  on dateDiff(t1.visited_on, t2.visited_on) between 0 and 6
group by
  t1.visited_on
having
  -- 分组后条件:此条数据的日期在最早记录日期算起的第 7 天及往后
  dateDiff(
    t1.visited_on,
    (
      select
        min(visited_on)
      from
        Customer
    )
  ) >= 6
order by
  t1.visited_on;

602. 好友申请 II :谁有最多的好友

编写解决方案,找出拥有最多的好友的人和他拥有的好友数目。

生成的测试用例保证拥有最多好友数目的只有 1 个人。

查询结果格式如下例所示。

思路 + SQL

其实就是数 requester_id 与 accepter_id 里所有数字的重复数量,哪个重复数量多就返回哪个数字,就可以得到。

  1. 先使用 UNION ALL 将 requester_id 和 accepter_id 拼接起来:
select
  id,
  count(*) as num
from
  (
    select
      requester_id as id
    from
      RequestAccepted
    union all
    select
      accepter_id as id
    from
      RequestAccepted
  ) as t
group by
  id
order by
  num desc
limit
  1;
  1. 然后对这个查询结果作为表去分组排序,取计数次数的倒序第一位,就是最多的好友 id 对应的数量。
select
  id,
  count(*) as num
from
  (
    select
      requester_id as id
    from
      RequestAccepted
    union all
    select
      accepter_id as id
    from
      RequestAccepted
  ) as t
group by
  id
order by
  num desc
limit
  1;

585. 2016年的投资

编写解决方案报告 2016 年 (tiv_2016) 所有满足下述条件的投保人的投保金额之和:

  • 他在 2015 年的投保额 (tiv_2015) 至少跟一个其他投保人在 2015 年的投保额相同。
  • 他所在的城市必须与其他投保人都不同(也就是说 (lat, lon) 不能跟其他任何一个投保人完全相同)。

tiv_2016 四舍五入的 两位小数 。

查询结果格式如下例所示。

思路 + SQL 1(常规思路)

  1. 先内连接查询出所有存在相同的 2015 年投保额情况(需要排除自身)
select
  *
from
  Insurance i1
  inner join Insurance i2 on i1.tiv_2015 = i2.tiv_2015
  and i1.pid != i2.pid;
  1. 排除城市相同的情况,即在 lat 和 lon 分组聚合下,计数大于 1 的情况:
select
  *
from
  Insurance i1
  inner join Insurance i2 on i1.tiv_2015 = i2.tiv_2015
  and i1.pid != i2.pid
where
  (i1.lat, i1.lon) in (
    select
      lat,
      lon
    from
      Insurance
    group by
      lat,
      lon
    having
      count(*) = 1
  );
  1. 将上面的查询结果去重后进行 SUM 聚合计算,即可得到结果:
select
  round(sum(tiv_2016), 2) as tiv_2016
from
  (
    select
      -- 对符合条件的查询结果去重
      distinct i1.pid,
      i1.tiv_2016
    from
      Insurance i1
      -- 交集得到存在相同的 2015 年投保额情况(需要排除自身)
      inner join Insurance i2 on i1.tiv_2015 = i2.tiv_2015
      and i1.pid != i2.pid
    where
      -- 排除城市有重复的条件查询
      (i1.lat, i1.lon) in (
        select
          lat,
          lon
        from
          Insurance
        group by
          lat,
          lon
        having
          count(*) = 1
      )
  ) as t;

思路 + SQL 2(优化简约版)

上面的 SQL 看起来可读性还是比较差的,其实可以两个条件都放进 where 里,可读性就会好很多。

  1. 先查询出 2015 年的投保额存在相同的相关额度:
select
  tiv_2015
from
  Insurance
group by
  tiv_2015
having
  count(*) > 1;
  1. 再查询出所在的城市必须与其他投保人都不同的情况,也就是 lat 和 lon 分组后,计数只有 1 的查询结果:
select
  lat,
  lon
from
  Insurance
group by
  lat,
  lon
having
  count(*) = 1;
  1. 最后将这两个查询合并应用,即可获得答案:
select
  -- 四舍五入两位小数的总和
  round(sum(tiv_2016), 2) as tiv_2016
from
  Insurance
where
  -- 相同额度表
  tiv_2015 in (
    select
      tiv_2015
    from
      Insurance
    group by
      tiv_2015
    having
      count(*) > 1
  )
  -- 唯一城市表
  and (lat, lon) in (
    select
      lat,
      lon
    from
      Insurance
    group by
      lat,
      lon
    having
      count(*) = 1
  );

185. 部门工资前三高的所有员工

表: Employee

+--------------+---------+
| Column Name  | Type    |
+--------------+---------+
| id           | int     |
| name         | varchar |
| salary       | int     |
| departmentId | int     |
+--------------+---------+
id 是该表的主键列(具有唯一值的列)。
departmentId 是 Department 表中 ID 的外键(reference 列)。
该表的每一行都表示员工的ID、姓名和工资。它还包含了他们部门的ID。

表: Department

+-------------+---------+
| Column Name | Type    |
+-------------+---------+
| id          | int     |
| name        | varchar |
+-------------+---------+
id 是该表的主键列(具有唯一值的列)。
该表的每一行表示部门ID和部门名。

公司的主管们感兴趣的是公司每个部门中谁赚的钱最多。一个部门的 高收入者 是指一个员工的工资在该部门的 不同 工资中 排名前三 。

编写解决方案,找出每个部门中 收入高的员工 。

以 任意顺序 返回结果表。

返回结果格式如下所示。

示例 1:

输入: 
Employee 表:
+----+-------+--------+--------------+
| id | name  | salary | departmentId |
+----+-------+--------+--------------+
| 1  | Joe   | 85000  | 1            |
| 2  | Henry | 80000  | 2            |
| 3  | Sam   | 60000  | 2            |
| 4  | Max   | 90000  | 1            |
| 5  | Janet | 69000  | 1            |
| 6  | Randy | 85000  | 1            |
| 7  | Will  | 70000  | 1            |
+----+-------+--------+--------------+
Department  表:
+----+-------+
| id | name  |
+----+-------+
| 1  | IT    |
| 2  | Sales |
+----+-------+
输出: 
+------------+----------+--------+
| Department | Employee | Salary |
+------------+----------+--------+
| IT         | Max      | 90000  |
| IT         | Joe      | 85000  |
| IT         | Randy    | 85000  |
| IT         | Will     | 70000  |
| Sales      | Henry    | 80000  |
| Sales      | Sam      | 60000  |
+------------+----------+--------+
解释:
在IT部门:
- Max的工资最高
- 兰迪和乔都赚取第二高的独特的薪水
- 威尔的薪水是第三高的

在销售部:
- 亨利的工资最高
- 山姆的薪水第二高
- 没有第三高的工资,因为只有两名员工

思路 + SQL 1(窗口函数)

看到了排序规则,就很容易想到窗口函数的 dense_rank,用 dense_rank得到薪资排名后,用 where 获取即可。

  1. 关联 Employee和 Department表:
select
  *
from
  Employee as e
  inner join Department as d on e.departmentId = d.id;
  1. 用关联表做窗口函数查询,拓展得到薪水排名:
select
  *,
  dense_rank() over (
    partition by d.id
    order by
      e.salary desc
  ) as salaryRank
from
  (
    Employee as e
    inner join Department as d on e.departmentId = d.id
  );
  1. 最后做 where 筛选出每个部门的前三名即可。
select
  Department,
  Employee,
  Salary
from
  (
    select
      d.name as department,
      e.name as employee,
      e.salary,
      -- 部门分组,并用 dense_rank 进行薪资排名
      dense_rank() over (
        partition by d.id
        order by
          e.salary desc
      ) as salaryRank
    from
      (
        Employee as e
        inner join Department as d on e.departmentId = d.id
      )
  ) as t
where
  salaryRank <= 3;

思路 + SQL 2 (子查询土办法)

官方提供的方法,看起来很拧巴,但多一种方法也好。

  1. 同样先取关联员工和部门表:
select
  d.Name as 'Department',
  e1.Name as 'Employee',
  e1.Salary
from
  Employee e1
  inner join Department d on e1.DepartmentId = d.Id
  1. 然后在 where 里做查询,这里需要判断每个员工的当前薪水是否是每个部门里前三高的:
select
  d.Name as 'Department',
  e1.Name as 'Employee',
  e1.Salary
from
  Employee e1
  inner join Department d on e1.DepartmentId = d.Id
where
  (
    select
      -- 这里 count 时候需要 distinct,因为排序方法是 dense_rank
      count(distinct e2.Salary)
    from
      Employee e2
    where
      -- 查询同部门下薪水比自己高的同事数量的对应条件
      e2.Salary > e1.Salary
      and e1.DepartmentId = e2.DepartmentId
  -- count 查询结果即为比自己薪资高的位数,即 (count + 1) 为自身的排名
  ) < 3;