SQL - 查询层次结构以确定职责
SQL - querying a heirachy to determine responsibility
假设我有一个由四个级别组成的组织层次结构:
- 部门,有
- 有
的部门
- 地区,有
- 团队
这在数据库中由四个 table 表示,其中记录 link 由其 ID 自动生成,每个 table 从 1 开始。例如:
DEPARTMENTS
departmentID departmentName divisionID
1 Finance 1
2 IT 1
3 HR 2
...
AREAS
areaID areaName departmentID
1 Accounts 1
2 Payroll 1
3 Collections 1
4 Development 2
5 Tech Support 2
...
等等。
还有一个 UserRoles table,其中用户 link 被分配到这四个级别之一的角色。但是,此 table 并未明确 link 任何层次结构 table。相反,每个 UserRole 记录都由四个部分组成 - 一个 userID、一个角色、一个 unitID 和一个 unitType。例如:
userID role unitID unitType
smithj Manager 1 Division
doej Manager 1 Department
simpsonh Manager 1 Area
cashj Supervisor 1 Team
nelsonw Supervisor 2 Team
kristofk Supervisor 3 Team
talbots Manager 2 Division
abbets Manager 2 Department
lowmany Manager 3 Department
...
等等。每个 ID 号都可以(并且将会)重复多次,代表它用于识别的各种不同类型的单位。需要 ID 号和单位类型才能将 user/role 组合绑定到组织的特定部分。
业务规则是,在分部、部门或区域级别担任角色的用户被视为负责其级别下的所有组织单位。例如,doej 将负责其部门下的区域,以及这些区域下的团队,但不负责其部门以上的部门。
如何查询此 table 结构以找出给定单位类型和 ID 的用户负责该结构?
这将是一个很长的答案来阐明我们在做什么。
TL;DR 版本
将递归 CTE 与自下而上的策略和呈现角色层次结构的视图结合使用。转到最后一个代码片段以获取解决方案。
完整版
您可以使用递归 CTE 自下而上查询分层数据 - 从叶子到节点(这里是从区域到部门,但在您的真实示例中只是添加团队)。此外,由于每个级别都有单独的 table,我们将使用一个视图来简化它,该视图将保留整个树并允许我们实际构建递归 CTE 查询。稍后会详细介绍。开始吧。
正在准备简化的数据样本
CREATE TABLE divisions ( divisionid int );
CREATE TABLE departments ( departmentid int, divisionid int);
CREATE TABLE areas ( areaid int, departmentid int);
CREATE TABLE userroles ( userid varchar(255), unitid int, unittype varchar(255));
INSERT INTO divisions VALUES (1),(2);
INSERT INTO departments VALUES (1,1),(2,2);
INSERT INTO areas VALUES (1,1),(2,1),(3,3),(4,2);
INSERT INTO userroles VALUES ('smithj',1,'Division'),('doej',1,'Department'),('simpsonh',2,'Area'),('anyother',1,'Area'),('smhg',1,'Division');
让我们看看 userroles
table 长什么样:
+----------+--------+------------+
| userid | unitid | unittype |
+----------+--------+------------+
| smithj | 1 | Division |
| doej | 1 | Department |
| simpsonh | 2 | Area |
| anyother | 1 | Area |
| smhg | 1 | Division |
+----------+--------+------------+
现在我们实际上向前迈进了一步 CREATE VIEW
,因为由于我们在不同的 table 中有不同的级别,因此逻辑需要使用几个 LEFT JOINs
和 SQL 服务器我们不能在递归 CTE 查询中使用这些。 会弹出这个错误:
Outer join is not allowed in the recursive part of a recursive common
table expression
所以我们正在创建角色层次结构视图以将左连接逻辑转换为一个 table 我们可以 INNER JOIN
到:
CREATE VIEW roles AS (
SELECT 'Division' AS unittype, divisionid as unitid, cast(null as int) as parentid, null as parenttype FROM divisions
UNION ALL
SELECT 'Department', departmentid, divisionid, 'Division' FROM departments
UNION ALL
SELECT 'Area', areaid, departmentid, 'Department' FROM areas
);
让我们看看我们的观点 roles
呈现了什么:
+------------+--------+----------+------------+
| unittype | unitid | parentid | parenttype |
+------------+--------+----------+------------+
| Division | 1 | NULL | NULL |
| Division | 2 | NULL | NULL |
| Department | 1 | 1 | Division |
| Department | 2 | 2 | Division |
| Area | 1 | 1 | Department |
| Area | 2 | 1 | Department |
| Area | 3 | 3 | Department |
| Area | 4 | 2 | Department |
+------------+--------+----------+------------+
到目前为止一切顺利。让我们为搜索条件设置两个变量并编写我们的最终查询。我选择 unittype = 'Area'
和 unitid = 2
。这将使预期的层次结构如下所示:
(Area: 2) -> (Department: 1) -> (Division: 1)
哪些用户应该为这些单位类型分配责任:
simpsonh <- from Area 2
doej <- from Department 1
smhg <- from Division 1
smithj <- from Division 1
现在开始查询。首先,我们在 roles
视图中选择我们的单元类型和选择的 id(通过设置变量),这样我们就已经有了第一个开始遍历层次结构的父级。我们还加入了 userroles
table 以查找可能对我们正在调查的单位直接负责的任何用户。我们使用 LEFT JOIN
因为可能没有人在这个级别(在示例中:Area)有直接责任。然后我们通过将 roles
和 userroles
连接回我们之前查询 (cte
) 的结果来执行递归部分,以查找对直接父级负责的用户。在我们的 Area 案例中,这将寻找 Department,然后寻找 Division。
最后,我们丢弃了 userid IS NULL
的任何行,因为如果没有人直接负责,我们不想显示从第一层出现的行。在我们的例子中他们这样做了,但是如果你删除分配给 (Area, 2)
的用户,你会看到它是如何工作的:
SQL解码
-- If you already have data in your tables and created view, proceed with this code:
DECLARE @unittype varchar(max);
DECLARE @unitid int;
SET @unittype = 'Area'; -- change it to look for what you want
SET @unitid = 2; -- change it to look for what you want
WITH cte AS (
SELECT ur.userid, r.unittype, r.unitid, r.parenttype, r.parentid
FROM roles r
LEFT JOIN userroles ur ON ur.unitid = r.unitid AND ur.unittype = r.unittype
WHERE r.unittype = @unittype AND r.unitid = @unitid
UNION ALL
SELECT ur.userid, r.unittype, r.unitid, r.parenttype, r.parentid
FROM roles r
INNER JOIN cte c ON r.unitid = c.parentid AND r.unittype = c.parenttype
INNER JOIN userroles ur ON ur.unittype = r.unittype AND ur.unitid = r.unitid
)
SELECT userid
FROM cte
WHERE userid IS NOT NULL
假设我有一个由四个级别组成的组织层次结构:
- 部门,有
- 有 的部门
- 地区,有
- 团队
这在数据库中由四个 table 表示,其中记录 link 由其 ID 自动生成,每个 table 从 1 开始。例如:
DEPARTMENTS
departmentID departmentName divisionID
1 Finance 1
2 IT 1
3 HR 2
...
AREAS
areaID areaName departmentID
1 Accounts 1
2 Payroll 1
3 Collections 1
4 Development 2
5 Tech Support 2
...
等等。
还有一个 UserRoles table,其中用户 link 被分配到这四个级别之一的角色。但是,此 table 并未明确 link 任何层次结构 table。相反,每个 UserRole 记录都由四个部分组成 - 一个 userID、一个角色、一个 unitID 和一个 unitType。例如:
userID role unitID unitType
smithj Manager 1 Division
doej Manager 1 Department
simpsonh Manager 1 Area
cashj Supervisor 1 Team
nelsonw Supervisor 2 Team
kristofk Supervisor 3 Team
talbots Manager 2 Division
abbets Manager 2 Department
lowmany Manager 3 Department
...
等等。每个 ID 号都可以(并且将会)重复多次,代表它用于识别的各种不同类型的单位。需要 ID 号和单位类型才能将 user/role 组合绑定到组织的特定部分。
业务规则是,在分部、部门或区域级别担任角色的用户被视为负责其级别下的所有组织单位。例如,doej 将负责其部门下的区域,以及这些区域下的团队,但不负责其部门以上的部门。
如何查询此 table 结构以找出给定单位类型和 ID 的用户负责该结构?
这将是一个很长的答案来阐明我们在做什么。
TL;DR 版本
将递归 CTE 与自下而上的策略和呈现角色层次结构的视图结合使用。转到最后一个代码片段以获取解决方案。
完整版
您可以使用递归 CTE 自下而上查询分层数据 - 从叶子到节点(这里是从区域到部门,但在您的真实示例中只是添加团队)。此外,由于每个级别都有单独的 table,我们将使用一个视图来简化它,该视图将保留整个树并允许我们实际构建递归 CTE 查询。稍后会详细介绍。开始吧。
正在准备简化的数据样本
CREATE TABLE divisions ( divisionid int );
CREATE TABLE departments ( departmentid int, divisionid int);
CREATE TABLE areas ( areaid int, departmentid int);
CREATE TABLE userroles ( userid varchar(255), unitid int, unittype varchar(255));
INSERT INTO divisions VALUES (1),(2);
INSERT INTO departments VALUES (1,1),(2,2);
INSERT INTO areas VALUES (1,1),(2,1),(3,3),(4,2);
INSERT INTO userroles VALUES ('smithj',1,'Division'),('doej',1,'Department'),('simpsonh',2,'Area'),('anyother',1,'Area'),('smhg',1,'Division');
让我们看看 userroles
table 长什么样:
+----------+--------+------------+
| userid | unitid | unittype |
+----------+--------+------------+
| smithj | 1 | Division |
| doej | 1 | Department |
| simpsonh | 2 | Area |
| anyother | 1 | Area |
| smhg | 1 | Division |
+----------+--------+------------+
现在我们实际上向前迈进了一步 CREATE VIEW
,因为由于我们在不同的 table 中有不同的级别,因此逻辑需要使用几个 LEFT JOINs
和 SQL 服务器我们不能在递归 CTE 查询中使用这些。 会弹出这个错误:
Outer join is not allowed in the recursive part of a recursive common table expression
所以我们正在创建角色层次结构视图以将左连接逻辑转换为一个 table 我们可以 INNER JOIN
到:
CREATE VIEW roles AS (
SELECT 'Division' AS unittype, divisionid as unitid, cast(null as int) as parentid, null as parenttype FROM divisions
UNION ALL
SELECT 'Department', departmentid, divisionid, 'Division' FROM departments
UNION ALL
SELECT 'Area', areaid, departmentid, 'Department' FROM areas
);
让我们看看我们的观点 roles
呈现了什么:
+------------+--------+----------+------------+
| unittype | unitid | parentid | parenttype |
+------------+--------+----------+------------+
| Division | 1 | NULL | NULL |
| Division | 2 | NULL | NULL |
| Department | 1 | 1 | Division |
| Department | 2 | 2 | Division |
| Area | 1 | 1 | Department |
| Area | 2 | 1 | Department |
| Area | 3 | 3 | Department |
| Area | 4 | 2 | Department |
+------------+--------+----------+------------+
到目前为止一切顺利。让我们为搜索条件设置两个变量并编写我们的最终查询。我选择 unittype = 'Area'
和 unitid = 2
。这将使预期的层次结构如下所示:
(Area: 2) -> (Department: 1) -> (Division: 1)
哪些用户应该为这些单位类型分配责任:
simpsonh <- from Area 2
doej <- from Department 1
smhg <- from Division 1
smithj <- from Division 1
现在开始查询。首先,我们在 roles
视图中选择我们的单元类型和选择的 id(通过设置变量),这样我们就已经有了第一个开始遍历层次结构的父级。我们还加入了 userroles
table 以查找可能对我们正在调查的单位直接负责的任何用户。我们使用 LEFT JOIN
因为可能没有人在这个级别(在示例中:Area)有直接责任。然后我们通过将 roles
和 userroles
连接回我们之前查询 (cte
) 的结果来执行递归部分,以查找对直接父级负责的用户。在我们的 Area 案例中,这将寻找 Department,然后寻找 Division。
最后,我们丢弃了 userid IS NULL
的任何行,因为如果没有人直接负责,我们不想显示从第一层出现的行。在我们的例子中他们这样做了,但是如果你删除分配给 (Area, 2)
的用户,你会看到它是如何工作的:
SQL解码
-- If you already have data in your tables and created view, proceed with this code:
DECLARE @unittype varchar(max);
DECLARE @unitid int;
SET @unittype = 'Area'; -- change it to look for what you want
SET @unitid = 2; -- change it to look for what you want
WITH cte AS (
SELECT ur.userid, r.unittype, r.unitid, r.parenttype, r.parentid
FROM roles r
LEFT JOIN userroles ur ON ur.unitid = r.unitid AND ur.unittype = r.unittype
WHERE r.unittype = @unittype AND r.unitid = @unitid
UNION ALL
SELECT ur.userid, r.unittype, r.unitid, r.parenttype, r.parentid
FROM roles r
INNER JOIN cte c ON r.unitid = c.parentid AND r.unittype = c.parenttype
INNER JOIN userroles ur ON ur.unittype = r.unittype AND ur.unitid = r.unitid
)
SELECT userid
FROM cte
WHERE userid IS NOT NULL