0

Assume I have an organisation hierarchy comprised of four levels:

  • Divisions, that have
  • Departments, that have
  • Areas, that have
  • Teams

This is represented in the database by four tables, with records linked by their IDs, which are autogenerated and start from 1 for each table. For example:

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
...

And so on.

Also present is a UserRoles table, where users are linked to a role for one of those four levels. This table does not explicitly link to any of the hierarchy tables, however. Instead, every UserRole record is comprised of four parts - a userID, a role, a unitID and a unitType. For example:

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
...

And so on. Every ID number can (and will) be repeated several times, representing the various different types of unit it is used to identify. Both the ID number and the unit type is needed to tie the user/role combo to a particular part of the organisation.

A business rule is that a user who has a role at Division, Department or Area level is treated as being responsible for all of the organisational units under their level. For example, doej would be responsible for the Areas under their Department, and the Teams under those Areas, but not for the Division above their Department.

How can I query this table structure to find out, given a Unit Type and an ID, which users are responsible for it?

Damien H
  • 174
  • 11
  • In your description you are talking about displaying responsibility of one user, and then at the end your question asks about showing who is responsible for a given unit bottom-up in the hierarchy. Which one is it that you actually want? This is ambiguous. – Kamil Gosciminski Aug 17 '18 at 05:44
  • @KamilG. The UserRoles table holds information about the responsibility of individuals - I would like to find out which individuals have responsibility for a given unitID/unitType combo. Is there anything I can clarify further? – Damien H Aug 17 '18 at 06:00
  • I'm preparing my answer, I think this is sufficient. – Kamil Gosciminski Aug 17 '18 at 06:20
  • Please clarify via edits, not comments. – philipxy Aug 17 '18 at 06:48

1 Answers1

1

This is going to be a long answer to clarify what we're doing.

TL;DR version

Use recursive CTE with bottom-up strategy and view that presents hierarchy of roles. Go to the last code snippet for solution.

LIVE DEMO - click here


FULL VERSION

You could use a recursive CTE to query hierarchical data bottom-up - from the leaf to the node (here from areas to divisions, but in your real example just add teams). Moreover, since you have separate tables for each level we're going to simplify it using a view that will keep the entire tree and will allow us to actually build a recursive CTE query. More on that later. Let's start.

Preparing simplified data sample

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');

Let's see how userroles table looks like:

+----------+--------+------------+
| userid   | unitid | unittype   |
+----------+--------+------------+
| smithj   | 1      | Division   |
| doej     | 1      | Department |
| simpsonh | 2      | Area       |
| anyother | 1      | Area       |
| smhg     | 1      | Division   |
+----------+--------+------------+

Now we actually go a step ahead to CREATE VIEW, because since we have different levels in separate tables the logic would require to use couple of LEFT JOINs and in SQL Server we can't use these in recursive CTE query. This error would pop up:

Outer join is not allowed in the recursive part of a recursive common table expression

So we're creating the roles hierarchy view to convert left joins logic into one table that we can INNER JOIN to:

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
);

Let's see what our view roles presents:

+------------+--------+----------+------------+
| 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 |
+------------+--------+----------+------------+

So far so good. Let's set two variables for search criteria and write our final query. I'm choosing unittype = 'Area' and unitid = 2. This will make the expected hierarchy go this way:

(Area: 2) -> (Department: 1) -> (Division: 1)

Which should bring in results users assigned responsibility for these unit types:

simpsonh <- from Area 2
doej     <- from Department 1
smhg     <- from Division 1
smithj   <- from Division 1

Now to the query. At first we're picking up our unit type and id of choice (through set variables) in the roles view so that we already have the first parent to start going through the hierarchy. We're also joining to userroles table to find any user that may be directly responsible for the unit we're investigating. We use LEFT JOIN because there may be nobody that has a direct responsibility at this level (in the example: Area). Then we're doing the recursive part by joining roles and userroles back to our result from previous query (cte) to look for users that are responsible for direct parent. In our case for Area this would be looking for Department and then for Division. At last we're discarding any rows where userid IS NULL because we don't want to show rows that appeared from first level if nobody had direct responsibility. In our case they do, but if you remove user assigned to (Area, 2) you'll see how it works:

SQL solution code

-- 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
Kamil Gosciminski
  • 16,547
  • 8
  • 49
  • 72
  • 1
    I am well aware of what CTEs are for. Please post your answer if you feel like manual typing of each level is a better and more flexible solution. IMHO recursive CTE may actually help in the future provided that the structure may grow or change and also has a potential benefit for other users stumbling upon this question. – Kamil Gosciminski Aug 17 '18 at 06:52
  • That is as thorough an answer as anyone could hope for - I appreciate the time it must have taken to compose and commit to words. You're right, the structure may grow and it may change, and this accommodates that beautifully. – Damien H Aug 17 '18 at 07:05
  • "You need a recursive CTE to query hierarchical data" *to abitrary depth*--like UserRoles if it can have arbitray unittype values--but otherwise not. PS @DamienH Read re [sql/database subtying/inheritance](https://stackoverflow.com/q/3579079/3404097) & [hierarchcal data/queries](https://stackoverflow.com/q/20215744/3404097).) – philipxy Aug 17 '18 at 12:39
  • @philipxy I've altered my answer so that instead of "need" it says "could use". – Kamil Gosciminski Aug 17 '18 at 12:47