在企业的组织架构中,部门通常以树形层级形式存在,比如总公司下分多个分公司,分公司下又设不同部门,需要统计每一层级对应的员工薪资、业绩等汇总数据时,单纯使用GROUP BY只能完成单层分组统计,无法适配层级结构。结合递归查询和GROUP BY可以优雅解决这个问题,下面以常见的关系型数据库为例讲解具体实现思路。

一、场景与表结构准备
首先假设我们有一个部门表dept和一个员工表emp,表结构如下:
-- 部门表,id为部门ID,parent_id为上级部门ID,name为部门名称
CREATE TABLE dept (
id INT PRIMARY KEY,
parent_id INT,
name VARCHAR(50)
);
-- 员工表,id为员工ID,dept_id为所属部门ID,salary为员工薪资
CREATE TABLE emp (
id INT PRIMARY KEY,
dept_id INT,
name VARCHAR(50),
salary DECIMAL(10,2)
);
部门表的数据示例如下,形成总公司-分公司-部门的层级结构:
| id | parent_id | name |
|---|---|---|
| 1 | NULL | 总公司 |
| 2 | 1 | 北京分公司 |
| 3 | 1 | 上海分公司 |
| 4 | 2 | 北京研发部 |
| 5 | 2 | 北京市场部 |
二、递归查询构建部门层级关系
递归查询的作用是找到每个部门的所有下级部门,包括自身,这样后续汇总时就能把下级部门的数据归到上级部门中。不同数据库的递归语法略有差异,下面分别给出MySQL 8+、PostgreSQL和SQL Server的实现示例。
1. MySQL 8+ 递归实现
MySQL 8+支持WITH RECURSIVE语法,我们可以先生成每个部门对应的所有下属部门ID列表:
WITH RECURSIVE dept_tree AS (
-- 锚点查询:取每个部门自身作为初始节点
SELECT
id AS root_dept_id, -- 根部门ID,也就是我们要汇总的上级部门
id AS sub_dept_id -- 下属部门ID,初始为自身
FROM dept
UNION ALL
-- 递归查询:关联上级部门和下级部门
SELECT
dt.root_dept_id,
d.id AS sub_dept_id
FROM dept_tree dt
JOIN dept d ON d.parent_id = dt.sub_dept_id
)
SELECT * FROM dept_tree ORDER BY root_dept_id, sub_dept_id;
2. PostgreSQL 递归实现
PostgreSQL的递归语法和MySQL类似,同样使用WITH RECURSIVE:
WITH RECURSIVE dept_tree AS (
SELECT
id AS root_dept_id,
id AS sub_dept_id
FROM dept
UNION ALL
SELECT
dt.root_dept_id,
d.id AS sub_dept_id
FROM dept_tree dt
JOIN dept d ON d.parent_id = dt.sub_dept_id
)
SELECT * FROM dept_tree ORDER BY root_dept_id, sub_dept_id;
3. SQL Server 递归实现
SQL Server使用WITH关键字加递归查询,语法略有不同:
WITH dept_tree AS (
SELECT
id AS root_dept_id,
id AS sub_dept_id
FROM dept
UNION ALL
SELECT
dt.root_dept_id,
d.id AS sub_dept_id
FROM dept_tree dt
JOIN dept d ON d.parent_id = dt.sub_dept_id
)
SELECT * FROM dept_tree ORDER BY root_dept_id, sub_dept_id;
三、结合GROUP BY完成层级汇总
得到每个根部门对应的所有下属部门ID后,我们可以把这个结果集和员工表关联,再通过GROUP BY按照根部门分组,就能得到每个层级的汇总数据。
1. 汇总每个部门的员工总薪资
以MySQL 8+为例,完整实现代码如下:
WITH RECURSIVE dept_tree AS (
SELECT
id AS root_dept_id,
id AS sub_dept_id
FROM dept
UNION ALL
SELECT
dt.root_dept_id,
d.id AS sub_dept_id
FROM dept_tree dt
JOIN dept d ON d.parent_id = dt.sub_dept_id
)
SELECT
dt.root_dept_id AS dept_id,
d.name AS dept_name,
SUM(e.salary) AS total_salary,
COUNT(e.id) AS emp_count
FROM dept_tree dt
JOIN dept d ON dt.root_dept_id = d.id
LEFT JOIN emp e ON dt.sub_dept_id = e.dept_id
GROUP BY dt.root_dept_id, d.name
ORDER BY dt.root_dept_id;
代码逻辑说明:
- 递归部分
dept_tree生成每个部门对应的所有下属部门ID,包括自身 - 关联部门表获取根部门的名称
- 左关联员工表,匹配下属部门的员工数据
- 按照根部门ID和名称分组,使用SUM汇总薪资,COUNT统计员工数量
2. 适配不同层级的汇总需求
如果只需要汇总某一层级的部门,比如只汇总一级分公司的数据,可以在递归的锚点查询中增加过滤条件,只取parent_id为总公司的部门作为根部门:
WITH RECURSIVE dept_tree AS (
-- 只取一级分公司作为根部门
SELECT
id AS root_dept_id,
id AS sub_dept_id
FROM dept
WHERE parent_id = 1 -- 假设总公司ID为1
UNION ALL
SELECT
dt.root_dept_id,
d.id AS sub_dept_id
FROM dept_tree dt
JOIN dept d ON d.parent_id = dt.sub_dept_id
)
SELECT
dt.root_dept_id AS dept_id,
d.name AS dept_name,
SUM(e.salary) AS total_salary,
COUNT(e.id) AS emp_count
FROM dept_tree dt
JOIN dept d ON dt.root_dept_id = d.id
LEFT JOIN emp e ON dt.sub_dept_id = e.dept_id
GROUP BY dt.root_dept_id, d.name
ORDER BY dt.root_dept_id;
四、注意事项
1. 递归查询需要避免循环引用,比如部门表的parent_id指向自身的场景,会导致递归无限循环,需要在数据入库时做好校验。
2. 如果部门层级很深,递归查询可能会有性能问题,可以考虑给dept表的parent_id字段增加索引提升关联效率。
3. 低版本MySQL(8.0以下)不支持递归语法,可以通过存储过程或者自连接多次查询的方式实现,但是代码复杂度会明显提升。
通过上述递归和GROUP BY结合的方式,就可以灵活实现任意部门层级的汇总需求,适配不同的业务统计场景。