В стиле SQL - 6 (иерархии)
Dec. 8th, 2014 01:47 am![[personal profile]](https://www.dreamwidth.org/img/silk/identity/user.png)
Иерархии традиционно вызывают много вопросов. Обычно довольно простых, например, связанных с порядком выполнения частей запроса, особенно когда иерархия строится над несколькими таблицами. Но встречаются задачи и поинтереснее.
Предметная область состоит из сотрудников, каждый из которых работает в некотором отделе. Отделы образуют оргструктуру, то есть иерархию. Некоторым (но не всем) отделам приписан адрес.
1 <-- адрес A / \ 2 3 <-- адрес B / \ 4 5 <-- адрес C
Чтобы найти расположение отдела, у которого нет своего адреса, надо пройтись вверх по иерархии до тех пор, пока не будет обнаружен вышестоящий отдел с адресом.
Например, для картинки выше, адреса отделов должны быть такими:
DEP_ID ADDRESS ------ ------- 1 A 2 A 3 B 4 B 5 C
Задача состоит в написании запроса, который выведет всех сотрудников с указанием их отделов и адресов.
Вот тестовые данные.
create table dep( id number primary key, address varchar2(1000) ); create table dep_hier( parent_id references dep(id), child_id references dep(id) ); create table emp( id number primary key, dep_id number references dep(id) ); insert into dep values(1, 'A'); insert into dep values(2, ''); insert into dep values(3, 'B'); insert into dep values(4, ''); insert into dep values(5, 'C'); insert into dep_hier values(1, 2); insert into dep_hier values(1, 3); insert into dep_hier values(3, 4); insert into dep_hier values(3, 5); insert into emp values(101, 1); insert into emp values(102, 2); insert into emp values(103, 3); insert into emp values(104, 4); insert into emp values(105, 5); commit;
Возможное решение состоит в написании функции, возвращающей адрес указанного отдела (по сути, простой иерархический запрос, поднимающийся вверх по иерархии):
create or replace package hier as function get_address( p_dep_id number ) return varchar2; end; / create or replace package body hier as function get_address( p_dep_id number ) return varchar2 is l_address varchar2(1000); begin select min(dep.address) keep (dense_rank first order by level) into l_address from dep left join dep_hier on dep.id = dep_hier.child_id where dep.address is not null start with dep.id = p_dep_id connect by dep.id = prior dep_hier.parent_id ; return l_address; end; end; /
С такой функцией запрос выглядит тривиально:
select emp.id, emp.dep_id, hier.get_address(emp.dep_id) address from emp order by emp.id;
Есть ли минусы у такого решения? Разумеется, и один из них — производительность.
Дело в том, что функция вызывается каждый раз для каждого сотрудника; следовательно, чем больше сотрудников, тем дольше выполняется запрос. Посмотрим на график:
Красная линия — это то, как наш запрос ведет себя при увеличении числа сотрудников. А зеленая — как мог бы вести, если бы был написан в стиле SQL. Не обманитесь экспоненциальной шкалой: в реальном масштабе разглядеть зеленый график просто не получится.
Разумеется, процедурное решение можно ускорить, добавив волшебное слово result_cache, чтобы функция вызывалась не для каждого сотрудника, а для каждого уникального отдела. Но хорошего результата достичь все равно не получится, потому что неэффективность вылезет при росте иерархии:
Тут уместна аналогия с доступом к таблице по индексу. Для нескольких значений это эффективно, но для всех — катастрофа.
Так каково же «зеленое» решение? Оно состоит в том, чтобы заранее вычислить адреса всех отделов, обойдя дерево иерархии сверху вниз ровно один раз. Удобнее всего использовать для этого ANSI-синтаксис.
with adr(dep_id, address) as ( -- сверху... select dep.id, dep.address from dep where not exists ( -- опредеяем вершину как узел, не имеющий предка select null from dep_hier where dep_hier.child_id = dep.id ) union all -- ...вниз select dep.id, nvl(dep.address, adr.address) -- спускаем адрес сверху, если не указан from adr, dep, dep_hier where dep_hier.parent_id = adr.dep_id and dep.id = dep_hier.child_id ) select emp.id, emp.dep_id, adr.address from emp, adr where adr.dep_id = emp.dep_id order by emp.id;
Есть ли еще минусы у процедурного решения? Да, и это масштабируемость.
Представим себе, что помимо адреса для некоторых отделов аналогичным образом указана и другая информация, например, куратор.
Как изменится процедурное решение для вывода и адреса, и куратора? Очевидно, придется написать еще одну функцию, get_curator, во всем аналогичную get_address, кроме названия поля. Дублируем код, умножаем время на два и получаем минус в карму.
Как изменится решение в стиле SQL? Добавятся ровно три строчки, и это никак не скажется на производительности.
Вопрос со звездочкой состоит в том, что в ряде случаев надо получить ровно одно значение адреса, например, чтобы вывести его на форму. Очевидно, что один раз подняться по иерархии проще и быстрее, чем обойти все дерево. Но ведь поддерживать два способа сделать одно и то же — нехорошо?