目录

题目描述

570. 至少有5名直接下属的经理

题意分析

Employee 表里每行是一名员工,含 idnamedepartmentmanagerId 四个字段,其中 managerId 指向这名员工的直接上级id。要求找出至少有 5 名直接下属的经理,输出他们的姓名。

这张表是典型的自引用结构:上下级关系没有单独的关系表,而是靠同一张表里的 managerId 指回 id 来表达。因此「某人有几名直接下属」不能在他自己那一行读到,必须反过来数有多少行的 managerId 等于他的 id。这个视角切换是整道题的核心。

「直接」两个字要读准:只统计 managerId 直接指向自己的那些人,不做任何层级递归。若题目要的是「所有下属(含下属的下属)」,就必须用递归 CTE 了,那是完全不同的一道题。

输出粒度也要先钉死:结果是每位符合条件的经理一行、只含 name。这意味着最终必须回到 Employee 表按 id 取名字——因为统计出来的只是 managerId 这个数字,姓名不在统计结果里。

边界与约束:顶层老板的 managerIdNULL,这些行既不属于任何经理的下属计数,也不该被误算成「有一个名为 NULL 的经理」;id 是主键因此唯一,回连时不会出现一对多放大;「至少 5 名」是闭区间,条件写 >= 5 而不是 > 5;数据里可能根本没有满足条件的经理,此时返回空结果集是正确行为。

解法:分组统计 + 连接查询

核心思路

Employee 自连接成两种角色:manager 是经理,report 是员工。连接条件 report.managerId = manager.id 让结果中的每一行恰好表示一条直接汇报关系。

manager.id, manager.name 分组后,每组包含该经理的全部直接下属,因此 COUNT(*) 就是直接下属人数。HAVING COUNT(*) >= 5 在聚合后保留满足门槛的经理。

循环关系的不变量是:连接后的每行只贡献给它直接指向的那位经理,不会沿层级继续传播;分组后的每行对应唯一经理,计数等于其直接下属数。于是筛选留下且只留下至少五名直接下属的经理。

顶层员工的 managerIdNULL,等值连接不会把它匹配成任何人的下属,无需额外处理。按 id 和 name 一起分组,也满足 MySQL ONLY_FULL_GROUP_BY 对输出列的要求。

解题步骤

  • Employee 分别命名为 managerreport,按 report.managerId = manager.id 自连接。
  • manager.id, manager.name 分组;同名经理仍由不同 id 分开,选出的 name 在严格分组模式下也合法。
  • HAVING COUNT(*) >= 5 筛选。聚合结果必须放在 HAVING,且恰好五人也要保留。
  • 最终只输出 manager.name;分组已经保证每位经理只有一行,不需要 DISTINCT

走一遍一份具体数据。设 Employee 为:(101, John, A, NULL)(102, Dan, A, 101)(103, James, A, 101)(104, Amy, A, 101)(105, Anne, A, 101)(106, Ron, B, 101)

自连接后,五名员工都与 id = 101 的 John 配成一行;John 自己的 managerIdNULL,不会作为 report 匹配。按 John 分组得到 COUNT(*) = 5,恰好通过门槛并输出 John

若删掉 Ron,只剩四条直接汇报关系,HAVING 会过滤 John。若某位下属自己还管理其他人,那些更下一层的行只计入该下属自己的分组,不会递归计给 John。

代码实现

SELECT manager.name
FROM Employee AS manager
JOIN Employee AS report
ON report.managerId = manager.id
GROUP BY manager.id, manager.name
HAVING COUNT(*) >= 5;

复杂度分析

  • 时间复杂度:哈希连接与哈希聚合下期望为 $O(n)$;若执行器采用排序分组,则为 $O(n \log n)$。实际代价由索引和执行计划决定。
  • 空间复杂度:最坏 $O(n)$,用于连接或聚合状态。

关键点总结

  • 自引用表的第一反应是换视角:某个实体的「下属数 / 引用数」永远要从指向它的那些行去数,而不是从它自己那一行读。看清这一点,GROUP BY managerId 就是自然而然的写法。
  • ON 负责定义哪两行构成直接汇报关系,HAVING 负责按分组后的 COUNT(*) 过滤;聚合值不能提前写进 WHERE
  • 自连接后的一行就是一条直接汇报关系;按经理主键分组,计数才与「直接下属人数」一一对应。
  • 分组键同时写 manager.id, manager.name:id 区分同名经理,name 满足严格分组模式的输出要求。
  • 「至少 / 至多 / 超过」要逐字对应到 >= / <= / >。这类差一错误在 SQL 里不会报错,只会静默返回错误结果。
  • 本题只沿一条 managerId 边连接一次,因此不会把间接下属计入。

易错点总结

  • COUNT(*) >= 5 写进 WHEREWHERE COUNT(*) >= 5 → 直接语法错误,因为 WHERE 在分组之前求值,此时聚合值尚不存在。
  • HAVING COUNT(*) > 5:某经理恰好有 5 名直接下属 → 被漏掉,「至少 5 名」被误解成「超过 5 名」。
  • report.id 分组:员工 id 是主键,每组只有一行,COUNT(*) 永远达不到 5。
  • 连接方向写成 manager.managerId = report.id:角色完全反转,统计到的是经理的上级而非经理的下属。
  • 只按 manager.name 分组:两个同名经理会被合并,人数可能相加后错误过线。
  • 误以为要统计所有层级的下属:某经理直接下属只有 2 人、但下属的下属共 6 人 → 被错误地算作符合条件,而题目只看「直接」下属。

相似题目

题目 难度 考察点
181. 超过经理收入的员工 简单 同为自引用表,但用自连接逐行比对薪水,不涉及分组聚合
182. 查找重复的电子邮箱 简单 GROUP BY + HAVING COUNT(*) > 1 的最小形态,无需回表翻译字段
184. 部门工资最高的员工 中等 聚合结果是组内最大值而非行数,回连时要匹配「部门 + 薪水」两个键
185. 部门工资前三高的所有员工 困难 组内取前 N 名,需要窗口函数 DENSE_RANK() 而不是简单的 HAVING 过滤
183. 从不订购的客户 简单 求的是「没有关联行」的实体,用 LEFT JOIN ... IS NULLNOT IN
175. 组合两个表 简单 考的是必须用 LEFT JOIN 保留无地址的员工,与本题的内连接语义正相反
197. 上升的温度 简单 同为单表自连接,但连接条件是日期相差一天,考日期函数的用法
1376. 通知所有员工所需的时间 中等 同一份上下级结构的算法版,要沿 manager 链向上或向下递归求最长路径