После небольшой работы мне удалось ответить на мой собственный вопрос, чтобы воспроизвести точно такой же результат с использованием CTE.
Функция рекурсивного CTE работает в Oracle в этой ситуации с некоторыми ограничениями.Другая база данных должна поддерживать рекурсию с поиском DEPTH FIRST
вместе с аналитическими функциями.Теоретически этот код может быть перенесен с незначительными изменениями в синтаксис.
Ключевые моменты / извлеченные уроки:
- Результаты рекурсивных CTE нельзя использовать в другом CTE или подзапросе,Если вы хотите использовать результаты рекурсивного CTE в подзапросе, вы должны сначала материализовать в таблицу.
- Нельзя использовать представление для рекурсивных CTE, иначе вы получите ошибку
ORA-01702: a view is not appropriate here
. - Предложение
SEARCH DEPTH FIRST BY reportsto_position_id SET seq
было важно для установки is_manager
с помощью аналитической функции LEAD()
. - Уплощение дерева в поля
reporting_path
было полезно для его правильного обхода.,Я использовал функцию INSTR()
, чтобы гарантировать наличие позиции в заданном пути. - У скрипта SQL ограничение в 8000 символов, что вынудило меня материализовать другие CTE, прежде чем я смог запустить отчет.В обычной базе данных Oracle в этом нет необходимости, поскольку размер запроса не ограничен.
Образцы таблиц данных и базовые значения
-- 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