SQL 中的分层控制跨度报告,没有 Oracle CONNECT BY 语法?

Hierarchical Span of Control report in SQL, without Oracle CONNECT BY syntax?

总结

控制跨度是指向给定经理报告的员工人数。直接和间接报告计数应拆分为各自的总数。 还需要其他统计数据,包括组织中直接和间接报告的许多职位空缺。经理是有其他职位向其报告的任何职位。 展平 结构需要从顶部到树中任何位置的报告路径。

我发现这个问题经常出现在人力资源报告和数据仓库项目中。我只能在 Oracle 中解决它。 这份报告可以用与另一个数据库兼容的 (ANSI) SQL 编写,例如 SQL Server 或 PostgreSQL?

详情

组织层次结构的可视化表示:

Level 1                                1:3
                                        |
                        ----------------+-----------------------------------
                        |               |               |                  |
Level 2                2:1            13:             10:12               4:2
                        |                               |
               ---------+----------           ----------+----------
               |        |         |           |         |         |
Level 3      12:10     3:        3:         5:10-1    11:11      6:
               |                              |                   |
            ---+---               ------------+------------       |
            |     |               |     |     |     |     |       |
Level 4    7:4   7:9             8:5   8:7   8:6   8:    8:      9:8

树的每个节点或叶子由以下之一表示:

预期输出

POSITION_ID    POSITION_DESCR         REPORTSTO_POSITION_ID      EMPLOYEE_ID    MULTI_JOB_SEQUENCE      EMPLOYEE_NAME      TREE_LEVEL_NUM      IS_MANAGER     MAX_INCUMBENTS       FILLED_HEAD_COUNT      VACANT_HEAD_COUNT     FILLED_DIRECT_REPORTS     VACANT_DIRECT_REPORTS       FILLED_INDIRECT_REPORTS     VACANT_INDIRECT_REPORTS       EMPLOYEES_UNDER_POSITION        VACANCIES_UNDER_POSITION       REPORTING_PATH_POSITION_ID     REPORTING_PATH_POSITION_DESCR                       REPORTING_PATH_EMPLOYEE        REPORTING_PATH_EMPLOYEE_NAME
1              CEO                    NULL                       3              0                       Jill               1                   1              1                    1                      0                     3                         1                           9                           5                             12                              6                              1                              CEO                                                 3                              Jill
2              Senior Manager         1                          1              0                       Tom                2                   1              1                    1                      0                     1                         2                           2                           0                             3                               2                              1>2                            CEO>Senior Manager                                  3>1                            Jill>Tom
3              West Winger            2                          NULL           NULL                    NULL               3                   0              2                    0                      2                     0                         0                           0                           0                             0                               0                              1>2>3                          CEO>Senior Manager>West Winger                      3>1>(vacant)                   Jill>Tom>(vacant)
4              Executive Assistant    1                          2              0                       Doug               2                   0              1                    1                      0                     0                         0                           0                           0                             0                               0                              1>4                            CEO>Executive Assistant                             3>2                            Jill>Doug
5              Supervisor South       10                         10             1                       Frank              3                   1              1                    1                      0                     3                         2                           0                           0                             3                               2                              1>10>5                         CEO>Senior Manager>Supervisor South                 3>12>10-1                      Jill>Fred>Frank
6              Supervisor East        10                         NULL           NULL                    NULL               3                   1              1                    0                      1                     1                         0                           0                           0                             1                               0                              1>10>6                         CEO>Senior Manager>Supervisor East                  3>12>(vacant)                  Jill>Fred>(vacant)
7              Expert                 12                         4              0                       Olivia             4                   0              2                    2                      0                     0                         0                           0                           0                             0                               0                              1>2>12>7                       CEO>Senior Manager>Supervisor West>Expert           3>1>10>4                       Jill>Tom>Frank>Olivia
7              Expert                 12                         9              0                       David              4                   0              2                    2                      0                     0                         0                           0                           0                             0                               0                              1>2>12>7                       CEO>Senior Manager>Supervisor West>Expert           3>1>10>9                       Jill>Tom>Frank>David
8              Minion                 5                          5              0                       Carol              4                   0              5                    3                      2                     0                         0                           0                           0                             0                               0                              1>10>5>8                       CEO>Senior Manager>Supervisor South>Minion          3>12>10-1>5                    Jill>Fred>Frank>Carol
8              Minion                 5                          6              0                       Mary               4                   0              5                    3                      2                     0                         0                           0                           0                             0                               0                              1>10>5>8                       CEO>Senior Manager>Supervisor South>Minion          3>12>10-1>6                    Jill>Fred>Frank>Mary
8              Minion                 5                          7              0                       Michael            4                   0              5                    3                      2                     0                         0                           0                           0                             0                               0                              1>10>5>8                       CEO>Senior Manager>Supervisor South>Minion          3>12>10-1>7                    Jill>Fred>Frank>Michael
9              Administrator          6                          8              0                       Nigel              4                   0              1                    1                      0                     0                         0                           0                           0                             0                               0                              1>10>6>9                       CEO>Senior Manager>Supervisor East>Administrator    3>12>(vacant)>8                Jill>Fred>(vacant)>Nigel
10             Senior Manager         1                          12             0                       Fred               2                   1              1                    1                      0                     2                         1                           4                           2                             6                               3                              1>10                           CEO>Senior Manager                                  3>12                           Jill>Fred
11             Supervisor South       10                         11             0                       Wilson             3                   0              1                    1                      0                     0                         0                           0                           0                             0                               0                              1>10>11                        CEO>Senior Manager>Supervisor South                 3>12>11                        Jill>Fred>Wilson
12             Supervisor West        2                          10             0                       Frank              3                   1              1                    1                      0                     2                         0                           0                           0                             2                               0                              1>2>12                         CEO>Senior Manager>Supervisor West                  3>1>10                         Jill>Tom>Frank
13             Executive Mid-West     1                          NULL           NULL                    NULL               2                   0              1                    0                      1                     0                         0                           0                           0                             0                               0                              1>13                           CEO>Executive Mid-West                              3>(vacant)                     Jill>(vacant)

技术要求

  1. reportsto_position_id 包含经理的 position_id,最高职位为 NULL。
  2. position_id必须一直存在,但可以空着。
  3. 经理必须有唯一的 position_id(和 max_incumbents=1)树才能正常工作。
  4. 不同子树或不同级别的相似职位也必须有不同的position_id来维护报告结构。这是因为 reportsto_position_id 是为树中的每个节点定义的。
  5. 一个employee_id可以存在于多个节点上,说明该员工在组织中有多个职位。如果一名员工有 1 份工作,他们的 multi_job_sequence 将是 0。如果一名员工有多个工作,他们的 multi_job_sequence 会递增。
  6. 职位有一个 max_incumbents 来限制允许填补该职位的员工人数。职位空缺没有职位行数,但可以计算
  7. 经理职位可能空缺,即使员工仍向该职位汇报。
  8. 如果组织决定按 adding/deleting 级别或子树进行重组,则 SQL 代码不应更改。
  9. 这个例子过于简单化了。大型组织可以为职位和员工提供更多级别和选项(例如生效日期或状态)。为了降低复杂性,此示例中的所有员工和职位都处于活动状态。

Span of Control 报告业务要求

报告必须回答以下问题,这些问题在层级组织中很常见:

  1. 一位经理有多少直接下属(只比他们低一级的员工数)?
  2. 一名经理有多少间接下属(比他们低一级的员工数,一直到树的最低级别)?
  3. 这位经理有多少人"under their position"(即直接下属+间接下属)?
  4. 有多少经理有空缺职位需要填补他们的团队(直接下属空缺)?
  5. 有多少经理有经理向他们报告他们的团队中有职位空缺(职位空缺的间接报告)?
  6. 从顶部到树中每个位置的路径是什么,按名称还是按 ID:例如CEO>Senior Manager>Supervisor South>Minion1>2>5>8?
  7. 从顶部到树中每个员工的路径是什么,按名称或 ID(考虑可能有多个工作的员工):例如Jill>Tom>Frank>Olivia3>1>10-1>4?

示例数据

位置table

position_id  descr                            reportsto_position_id  max_incumbents
1            CEO                              NULL                   1
2            Senior Manager                   1                      1
3            West Winger                      2                      2
4            Executive Assistant              1                      1
5            Supervisor South                 10                     1
6            Supervisor East                  10                     1
7            Expert                           12                     2
8            Minion                           5                      5
9            Administrator                    6                      1
10           Senior Manager                   1                      1
11           Supervisor South                 10                     1
12           Supervisor West                  2                      1
13           Executive Mid-West               1                      1

工作 table

employee_id  multi_job_sequence  employee_name  position_id
1            0                   Tom            2
2            0                   Doug           4
3            0                   Jill           1
4            0                   Olivia         7
5            0                   Carol          8
6            0                   Mary           8
7            0                   Michael        8
8            0                   Nigel          9
9            0                   David          7
10           0                   Frank          12
10           1                   Frank          5
11           0                   Wilson         11
12           0                   Fred           10

SQL

-- Position incumbents. One row for each position, employee_id, multi_job_sequence combination.
with cte_incumbents
as
(
    select
    cp.position_id,
    cp.reportsto_position_id,
    cp.max_incumbents,
    cj.employee_id,
    cj.multi_job_sequence
    from position cp
    left join job cj on cj.position_id = cp.position_id
),
-- Incumbents count (filled and vacant) per position
cte_incumbents_count
as
(
    select
    i.reportsto_position_id,
    i.position_id,
    count(to_char(i.employee_id) || '-' || to_char(i.multi_job_sequence)) as filled_count,
    (i.max_incumbents - count(to_char(i.employee_id) || '-' || to_char(i.multi_job_sequence))) as vacant_count,
    i.max_incumbents
    from cte_incumbents i
    where i.employee_id is not null
    group by i.reportsto_position_id,
             i.position_id,
             i.max_incumbents

    UNION ALL

    select
    i.reportsto_position_id,
    i.position_id,
    0 as filled_count,
    (count(*) * i.max_incumbents) as vacant_count,
    i.max_incumbents
    from cte_incumbents i
    where i.employee_id is null
    group by i.reportsto_position_id,
             i.position_id,
             i.max_incumbents
),
-- Count the filled and vacant reports_to positions
cte_reportsto_count
as
(
    select
    i.reportsto_position_id,
    sum(i.filled_count) as filled_count,
    sum(i.vacant_count) as vacant_count,
    sum(i.max_incumbents) as total_incumbents
    from cte_incumbents_count i
    group by i.reportsto_position_id
),
-- Create the organisation tree, based on the reportsto_position_id
cte_reportsto_tree
as
(
    select
    rtt.position_id,
    rtt.employee_id,
    rtt.multi_job_sequence,
    rtt.position_descr,
    rtt.reportsto_position_id,
    rtt.employee_name,
    level as tree_level_num,
    case when connect_by_isleaf = 0 then 1 else 0 end as is_manager,
    rtt.max_incumbents,
    nvl((
        select
        rtc.filled_count
        from cte_reportsto_count rtc
        where rtc.reportsto_position_id = rtt.position_id
    ),0) as filled_direct_reports,
    nvl((
        select
        rtc.vacant_count
        from cte_reportsto_count rtc
        where rtc.reportsto_position_id = rtt.position_id
    ),0) as vacant_direct_reports,
    substr(sys_connect_by_path(rtt.position_id,'>'),2,length(sys_connect_by_path(rtt.position_id,'>'))-1) as reporting_path_position_id,
    substr(sys_connect_by_path(rtt.position_descr,'>'),2,length(sys_connect_by_path(rtt.position_descr,'>'))-1) as reporting_path_position_descr,
    substr(sys_connect_by_path(nvl(case when rtt.employee_id is null then null else case when rtt.multi_job_sequence = 0 then to_char(rtt.employee_id) else rtt.employee_id || '-' || rtt.multi_job_sequence end end,'(vacant)'),'>'),2,length(sys_connect_by_path(nvl(case when rtt.employee_id is null then null else rtt.employee_id || '-' || rtt.multi_job_sequence end,'(vacant)'),'>'))-1) as reporting_path_employee,
    substr(sys_connect_by_path(nvl(rtt.employee_name,'(vacant)'),'>'),2,length(sys_connect_by_path(nvl(rtt.employee_name,'(vacant)'),'>'))-1) as reporting_path_name
    from
    (
        select
        cp.position_id,
        cp.descr as position_descr,
        cp.max_incumbents,
        cp.reportsto_position_id,
        cj.employee_id,
        cj.multi_job_sequence,
        cj.employee_name
        from position cp
        left join job cj on cj.position_id = cp.position_id -- Positions may not be filled
    ) rtt
    connect by prior rtt.position_id = rtt.reportsto_position_id
    start with rtt.reportsto_position_id is null -- Start at the top of the tree
),
-- Create the report detail, traversing the tree (creating subtrees to get the indirect values). This is the tough part!
cte_report_detail
as
(
    select
    soc.position_id,
    soc.position_descr,
    soc.reportsto_position_id,
    soc.employee_id,
    soc.multi_job_sequence,
    soc.employee_name,
    soc.tree_level_num,
    soc.is_manager,
    soc.max_incumbents,
    nvl(
        (
         select
         ic.filled_count
         from cte_incumbents_count ic
         where ic.position_id = soc.position_id
        ),0) as filled_head_count,
    nvl(
        (
         select
         ic.vacant_count
         from cte_incumbents_count ic
         where ic.position_id = soc.position_id
        ),0) as vacant_head_count,
    soc.filled_direct_reports as filled_direct_reports,
    soc.vacant_direct_reports as vacant_direct_reports,
    case when soc.is_manager = 1 then
    -- Get the filled count of all of the positions underneath and subtract the direct reports to arrive at the filled indirect reports count
    (
        select
        sum(
             (
                select
                rtc.filled_count
                from cte_reportsto_count rtc
                where rtc.reportsto_position_id = cp.position_id
             )
           )
        from position cp
        connect by prior cp.position_id = cp.reportsto_position_id
        start with cp.position_id = soc.position_id
    ) - soc.filled_direct_reports else 0 end as filled_indirect_reports,
    -- Get the vacant count of all of the positions underneath and subtract the direct reports to arrive at the vacant indirect reports count
    case when soc.is_manager = 1 then
    (
        select
        sum(
             (
                select
                rtc.vacant_count
                from cte_reportsto_count rtc
                where rtc.reportsto_position_id = cp.position_id
             )
           )
        from position cp
        connect by prior cp.position_id = cp.reportsto_position_id
        start with cp.position_id = soc.position_id
    ) - soc.vacant_direct_reports else 0 end as vacant_indirect_reports,
    to_clob(cast(soc.reporting_path_position_id as varchar2(4000))) as reporting_path_position_id,
    to_clob(cast(soc.reporting_path_position_descr as varchar2(4000))) as reporting_path_position_descr,
    to_clob(cast(soc.reporting_path_employee as varchar2(4000))) as reporting_path_employee,
    to_clob(cast(soc.reporting_path_name as varchar2(4000))) as reporting_path_employee_name
    from cte_reportsto_tree soc
)
-- Final calculations and sort
select
r.position_id,
r.position_descr,
r.reportsto_position_id,
r.employee_id,
r.multi_job_sequence,
r.employee_name,
r.tree_level_num,
r.is_manager,
r.max_incumbents,
r.filled_head_count,
r.vacant_head_count,
r.filled_direct_reports,
r.vacant_direct_reports,
r.filled_indirect_reports,
r.vacant_indirect_reports,
(r.filled_direct_reports + r.filled_indirect_reports) as employees_under_position,
(r.vacant_direct_reports + r.vacant_indirect_reports) as vacancies_under_position,
r.reporting_path_position_id,
r.reporting_path_position_descr,
r.reporting_path_employee,
r.reporting_path_employee_name
from cte_report_detail r
order by r.position_id,
         r.employee_id,
         r.multi_job_sequence;

SQL Fiddle example

简而言之,答案是肯定的。

标准 SQL:1999 定义了 "Recursive CTEs"(递归通用 Table 表达式)来完成 CONNECT BY 的工作等等。它们旨在遍历任何类型的图形——层次结构是它们可以处理的子集。

您的查询非常广泛,所以我没有时间检查它并用标准重写它 SQL。

你问哪个数据库可以做。好吧,它们目前由以下人员实施:

  • 甲骨文。
  • DB2。不在 Linux/Unix/Windows 中实现循环检测。它在 z/OS.
  • PostgreSQL.
  • SQL 服务器(自 2012 年以来?)。不执行循环检测。
  • MariaDB,从 10.2 开始。不执行循环检测。
  • MySQL 从 8.0 开始。不执行循环检测。
  • H2(从 1.4 开始?)。不执行循环检测。
  • 超级SQL.
  • 其他数据库...

如果您提供一个更小的示例,我会对使用递归 CTE 改写它非常感兴趣。

例如,以下递归 CTE(在 Oracle 中)将查找(直接和间接)向职位 = 2 报告的员工的所有子树:

with
x (position_id, descr, reportsto_position_id, max_incumbents, cur_level) as (
  select
    position_id, descr, reportsto_position_id, max_incumbents,
    1
    from position
    where position_id = 2 -- start at position = 2
  union all
  select
    p.position_id, p.descr, p.reportsto_position_id, p.max_incumbents,
    x.cur_level + 1
    from position p
    join x on p.reportsto_position_id = x.position_id
)
select * from x;

经过一番努力,我设法回答了我自己的问题,使用 CTE 重现了完全相同的结果。

在这种情况下,递归 CTE 功能在 Oracle 中有效,但有一些限制。另一个数据库需要支持 DEPTH FIRST 搜索的递归,以及分析功能。 理论上,可以通过对语法进行微小更改来移植此代码。

关键points/lessons学习到:

  1. 递归 CTE 的结果不能在另一个 CTE 或子查询中使用。如果要在子查询中使用递归 CTE 的结果,则必须先在 table 中具体化。
  2. 您不能将视图用于递归 CTE,否则您将收到错误 ORA-01702: a view is not appropriate here
  3. LEAD() 分析函数的帮助下,SEARCH DEPTH FIRST BY reportsto_position_id SET seq 子句对于设置 is_manager 很重要。
  4. 将树展平到 reporting_path 字段中对于正确遍历它很有用。我使用 INSTR() 函数来确保给定路径中存在一个位置。
  5. SQL Fiddle 有 8000 个字符的限制,这迫使我在 运行 报告之前具体化其他 CTE。这在普通的 Oracle 数据库上没有必要,因为查询大小没有限制。

样本数据table和基本计数

-- Create a table for each current position.
create table position
(
    position_id           NUMBER(11) NOT NULL,
    descr                 VARCHAR2(50) NOT NULL,
    reportsto_position_id NUMBER(11),
    max_incumbents        NUMBER(4) NOT NULL
);

create unique index position_idx1 on position (position_id);

-- Create a table to store the current job data.
create table job
(
    employee_id        NUMBER(11) NOT NULL,
    multi_job_sequence NUMBER(1) NOT NULL,
    employee_name      VARCHAR2(50) NOT NULL,
    position_id        NUMBER(11) NOT NULL
);

create unique index job_idx1 on job (employee_id, multi_job_sequence);
create index job_idx2 on job (position_id, employee_id, multi_job_sequence);

-- Insert data into position table
insert into position values (1, 'CEO', NULL, 1);
insert into position values (2, 'Senior Manager', 1, 1);
insert into position values (3, 'West Winger', 2, 2);
insert into position values (4, 'Executive Assistant', 1, 1);
insert into position values (5, 'Supervisor South', 10, 1);
insert into position values (6, 'Supervisor East', 10, 1);
insert into position values (7, 'Expert', 12, 2);
insert into position values (8, 'Minion', 5, 5);
insert into position values (9, 'Administrator', 6, 1);
insert into position values (10, 'Senior Manager', 1, 1);
insert into position values (11, 'Supervisor South', 10, 1);
insert into position values (12, 'Supervisor West', 2, 1);
insert into position values (13, 'Executive Mid-West', 1, 1);

commit;

-- Insert data into job table
insert into job values (1, 0, 'Tom', 2);
insert into job values (2, 0, 'Doug', 4);
insert into job values (3, 0, 'Jill', 1);
insert into job values (4, 0, 'Olivia', 7);
insert into job values (5, 0, 'Carol', 8);
insert into job values (6, 0, 'Mary', 8);
insert into job values (7, 0, 'Michael', 8);
insert into job values (8, 0, 'Nigel', 9);
insert into job values (9, 0, 'David', 7);
insert into job values (10, 0, 'Frank', 12);
insert into job values (10, 1, 'Frank', 5);
insert into job values (11, 0, 'Wilson', 11);
insert into job values (12, 0, 'Fred', 10);

commit;

-- Build up the tables

-- Position incumbents. One row for each position, employee_id, multi_job_sequence combination.
create table cte_incumbents
as
(
    select
    cp.position_id,
    cp.reportsto_position_id,
    cp.max_incumbents,
    cj.employee_id,
    cj.multi_job_sequence
    from position cp
    left join job cj on cj.position_id = cp.position_id
);

create unique index cte_incumbents_idx1 on cte_incumbents (position_id, employee_id, multi_job_sequence);
create index cte_incumbents_idx2 on cte_incumbents (position_id, reportsto_position_id);

-- Incumbents count (filled and vacant) per position
create table cte_incumbents_count
as
(
    select
    i.reportsto_position_id,
    i.position_id,
    count(to_char(i.employee_id) || '-' || to_char(i.multi_job_sequence)) as filled_count,
    (i.max_incumbents - count(to_char(i.employee_id) || '-' || to_char(i.multi_job_sequence))) as vacant_count,
    i.max_incumbents
    from cte_incumbents i
    where i.employee_id is not null
    group by i.reportsto_position_id,
             i.position_id,
             i.max_incumbents

    UNION ALL

    select
    i.reportsto_position_id,
    i.position_id,
    0 as filled_count,
    (count(*) * i.max_incumbents) as vacant_count,
    i.max_incumbents
    from cte_incumbents i
    where i.employee_id is null
    group by i.reportsto_position_id,
             i.position_id,
             i.max_incumbents
);

create unique index cte_incumbents_count_idx on cte_incumbents_count (reportsto_position_id, position_id);


-- Count the filled and vacant reports_to positions
create table cte_reportsto_count
as
(
    select
    i.reportsto_position_id,
    sum(i.filled_count) as filled_count,
    sum(i.vacant_count) as vacant_count,
    sum(i.max_incumbents) as total_incumbents
    from cte_incumbents_count i
    group by i.reportsto_position_id
);

create unique index cte_reportsto_count_idx on cte_reportsto_count (reportsto_position_id);

使用 CTE 报告

create table cte_reportsto_tree as
-- Create the organisation tree, based on the reportsto_position_id
with cte_reportsto_tree_base (
                                 position_id,
                                 position_descr,
                                 reportsto_position_id,
                                 employee_id,
                                 multi_job_sequence,
                                 employee_name,
                                 tree_level_num,
                                 max_incumbents,
                                 filled_direct_reports,
                                 vacant_direct_reports,
                                 reporting_path_position_id,
                                 reporting_path_position_descr,
                                 reporting_path_employee,
                                 reporting_path_employee_name
                             )
as
(
    -- Anchor member
    select
    cp1.position_id,
    cp1.descr as position_descr,
    cp1.reportsto_position_id,
    cj1.employee_id,
    cj1.multi_job_sequence,
    cj1.employee_name,
    1 as tree_level_num,
    cp1.max_incumbents,
    nvl((
        select
        rtc.filled_count
        from cte_reportsto_count rtc
        where rtc.reportsto_position_id = cp1.position_id
    ),0) as filled_direct_reports,
    nvl((
        select
        rtc.vacant_count
        from cte_reportsto_count rtc
        where rtc.reportsto_position_id = cp1.position_id
    ),0) as vacant_direct_reports,
    to_char(cp1.position_id) as reporting_path_position_id,
    cp1.descr as reporting_path_position_descr,
    to_char(cj1.employee_id) as reporting_path_employee,
    cj1.employee_name as reporting_path_employee_name
    from position cp1
    left join job cj1 on cj1.position_id = cp1.position_id -- Positions may not be filled
    where cp1.position_id = 1 -- start at position = 1

    UNION ALL

    -- Recursive member
    select
    cp2.position_id,
    cp2.descr as position_descr,
    cp2.reportsto_position_id,
    cj2.employee_id,
    cj2.multi_job_sequence,
    cj2.employee_name,
    rtt.tree_level_num + 1 as tree_level_num,
    cp2.max_incumbents,
    nvl((
        select
        rtc.filled_count
        from cte_reportsto_count rtc
        where rtc.reportsto_position_id = cp2.position_id
    ),0) as filled_direct_reports,
    nvl((
        select
        rtc.vacant_count
        from cte_reportsto_count rtc
        where rtc.reportsto_position_id = cp2.position_id
    ),0) as vacant_direct_reports,
    rtt.reporting_path_position_id || '>' || to_char(cp2.position_id) as reporting_path_position_id,
    rtt.reporting_path_position_descr || '>' || cp2.descr as reporting_path_position_descr,
    rtt.reporting_path_employee || '>' || nvl(case when cj2.employee_id is null then null else case when cj2.multi_job_sequence = 0 then to_char(cj2.employee_id) else to_char(cj2.employee_id) || '-' || to_char(cj2.multi_job_sequence) end end,'(vacant)') as reporting_path_employee,
    rtt.reporting_path_employee_name || '>' || nvl(cj2.employee_name,'(vacant)') as reporting_path_employee_name
    from position cp2
    inner join cte_reportsto_tree_base rtt on rtt.position_id = cp2.reportsto_position_id
    left join job cj2 on cj2.position_id = cp2.position_id -- Positions may not be filled
)
SEARCH DEPTH FIRST BY reportsto_position_id SET seq
select
rtt.position_id,
rtt.position_descr,
rtt.reportsto_position_id,
rtt.employee_id,
rtt.multi_job_sequence,
rtt.employee_name,
rtt.tree_level_num,
rtt.max_incumbents,
rtt.filled_direct_reports,
rtt.vacant_direct_reports,
rtt.reporting_path_position_id,
rtt.reporting_path_position_descr,
rtt.reporting_path_employee,
rtt.reporting_path_employee_name,
case when (rtt.tree_level_num - lead(rtt.tree_level_num) over (order by seq)) < 0 then 1 else 0 end is_manager -- Is a manager if there is a difference between levels on the tree.
from cte_reportsto_tree_base rtt;

create index cte_reportsto_tree_idx on cte_reportsto_tree (position_id, reportsto_position_id, employee_id, multi_job_sequence);

create table cte_fir as
(
    select
    soc.position_id,
    soc.is_manager,
    soc.tree_level_num,
    soc.filled_direct_reports,
    soc.vacant_direct_reports,
    case when soc.is_manager = 1 then
        nvl((
            select
            sum(ind.filled_direct_reports)
            from cte_reportsto_tree ind
            where ind.tree_level_num > soc.tree_level_num -- Must be at a lower level
        ),0)
    else 0 end as filled_indirect_reports,
    case when soc.is_manager = 1 then
        nvl((
            select
            sum(ind.vacant_direct_reports)
            from cte_reportsto_tree ind
            where ind.tree_level_num > soc.tree_level_num -- Must be at a lower level
        ),0)
    else 0 end as vacant_indirect_reports
    from cte_reportsto_tree soc
    where soc.tree_level_num = 1

    UNION ALL

    select
    soc.position_id,
    soc.is_manager,
    soc.tree_level_num,
    soc.filled_direct_reports,
    soc.vacant_direct_reports,
    case when soc.is_manager = 1 then
        nvl((
            select
            sum(ind.filled_direct_reports)
            from cte_reportsto_tree ind
            where ind.tree_level_num > soc.tree_level_num -- Must be at a lower level
            and instr(ind.reporting_path_position_id, '>'|| soc.position_id || '>') > 0 -- The position must be in the flattened tree path (quick way of traversing the tree)
        ),0)
    else 0 end as filled_indirect_reports,
    case when soc.is_manager = 1 then
        nvl((
            select
            sum(ind.vacant_direct_reports)
            from cte_reportsto_tree ind
            where ind.tree_level_num > soc.tree_level_num -- Must be at a lower level
            and instr(ind.reporting_path_position_id, '>'|| soc.position_id ||'>') > 0 -- The position must be in the flattened tree path (quick way of traversing the tree)
        ),0)
    else 0 end as vacant_indirect_reports
    from cte_reportsto_tree soc
    where soc.tree_level_num > 1
);

create index cte_fir_idx on cte_fir (position_id);

select
soc.position_id,
soc.position_descr,
soc.reportsto_position_id,
soc.employee_id,
soc.multi_job_sequence,
soc.employee_name,
soc.tree_level_num,
soc.is_manager,
soc.max_incumbents,
nvl(
    (
        select
        ic.filled_count
        from cte_incumbents_count ic
        where ic.position_id = soc.position_id
    ),0) as filled_head_count,
nvl(
    (
        select
        ic.vacant_count
        from cte_incumbents_count ic
        where ic.position_id = soc.position_id
    ),0) as vacant_head_count,
soc.filled_direct_reports as filled_direct_reports,
soc.vacant_direct_reports as vacant_direct_reports,
(
    select
    sum(fir.filled_indirect_reports)
    from cte_fir fir
    where fir.position_id = soc.position_id
) as filled_indirect_reports,
(
    select
    sum(fir.vacant_indirect_reports)
    from cte_fir fir
    where fir.position_id = soc.position_id
) as vacant_indirect_reports,
(
    select
    sum(fir.filled_indirect_reports)
    from cte_fir fir
    where fir.position_id = soc.position_id
) + soc.filled_direct_reports as employees_under_position,
(
    select
    sum(fir.vacant_indirect_reports)
    from cte_fir fir
    where fir.position_id = soc.position_id
) + soc.vacant_direct_reports as vacancies_under_position,
soc.reporting_path_position_id,
soc.reporting_path_position_descr,
soc.reporting_path_employee,
soc.reporting_path_employee_name
from cte_reportsto_tree soc
order by soc.position_id,
         soc.employee_id,
         soc.multi_job_sequence;

SQL Fiddle example