Листовые строки CONNECT_BY_ISLEAF

Строку в рекурсивном запросе называют листом, если в рекурсивной части подзапроса для нее не найдено дочерних строк.

В Oracle в рекурсивных запросах через CONNECT BY есть элегантный способ определения листовых строк - псевдостолбец CONNECT_BY_ISLEAF. Он равен 1 для листовых строк, и 0 для родительских.

К сожалению, Postgres нам такой роскоши не предоставляет. Поэтому придется что-то придумывать самим.

Возьмем иерархию категорий товаров

WITH RECURSIVE lv_hierarchy AS (
 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        1 AS level,
        '/' || c.name AS path,
        array[row_number () over (order by c.name)] AS path_sort
   FROM category c
  WHERE c.parent_category_id IS NULL

  UNION ALL

 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        p.level + 1 AS level,
        p.path || '/' || c.name AS path,
        p.path_sort || row_number () over (partition by c.parent_category_id order by c.name) AS path_sort
   FROM lv_hierarchy p,
        category c
  WHERE c.parent_category_id = p.category_id
)
SELECT c.category_id,
       lpad('', (c.level - 1) * 4, '.') || array_to_string(c.path_sort, '.') || '. ' || c.name AS full_name,
       c.level
  FROM lv_hierarchy c
 ORDER BY path_sort
# category_id full_name level
1 1 1. Товары для дома 1
2 3 ....1.1. Бытовая техника 2
3 2 2. Цифровая техника 1
4 7 ....2.1. Аудиотехника 2
5 6 ....2.2. Игровые консоли 2
6 4 ....2.3. Ноутбуки и аксессуары 2
7 9 ........2.3.1. Ноутбуки 3
8 10 ........2.3.2. Рюкзаки 3
9 8 ....2.4. Сотовые телефоны 2
10 5 ....2.5. Фотоаппараты 2

Способ 1. Подзапрос not exists

Какие категории являются листьями? Те, у которых нет подкатегорий.

Определить категории, в которых нет подкатегорий, можно с помощью подзапроса not exists:

WITH RECURSIVE lv_hierarchy AS (
 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        1 AS level,
        '/' || c.name AS path,
        array[row_number () over (order by c.name)] AS path_sort
   FROM category c
  WHERE c.parent_category_id IS NULL

  UNION ALL

 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        p.level + 1 AS level,
        p.path || '/' || c.name AS path,
        p.path_sort || row_number () over (partition by c.parent_category_id order by c.name) AS path_sort
   FROM lv_hierarchy p,
        category c
  WHERE c.parent_category_id = p.category_id
)
SELECT c.category_id,
       lpad('', (c.level - 1) * 4, '.') || array_to_string(c.path_sort, '.') || '. ' || c.name AS full_name,
       not exists (
         SELECT 1
           FROM category ch
          WHERE ch.parent_category_id = c.category_id
       ) AS is_leaf
  FROM lv_hierarchy c
 ORDER BY path_sort
# category_id full_name is_leaf
1 1 1. Товары для дома FALSE
2 3 ....1.1. Бытовая техника TRUE
3 2 2. Цифровая техника FALSE
4 7 ....2.1. Аудиотехника TRUE
5 6 ....2.2. Игровые консоли TRUE
6 4 ....2.3. Ноутбуки и аксессуары FALSE
7 9 ........2.3.1. Ноутбуки TRUE
8 10 ........2.3.2. Рюкзаки TRUE
9 8 ....2.4. Сотовые телефоны TRUE
10 5 ....2.5. Фотоаппараты TRUE

Способ 2. Сравнить уровень вложенности текущей и следующей строки.

Посмотри внимательно на уровень вложенности в отсортированных строках иерархии.

# category_id full_name level
1 1 1. Товары для дома 1
2 3 ....1.1. Бытовая техника 2
3 2 2. Цифровая техника 1
4 7 ....2.1. Аудиотехника 2
5 6 ....2.2. Игровые консоли 2
6 4 ....2.3. Ноутбуки и аксессуары 2
7 9 ........2.3.1. Ноутбуки 3
8 10 ........2.3.2. Рюкзаки 3
9 8 ....2.4. Сотовые телефоны 2
10 5 ....2.5. Фотоаппараты 2

Если у текущего каталога есть вложенные подкаталоги, то на следующей строке будет располагаться один из них. Уровень вложенности подкаталога будет больше уровня вложенности текущего каталога на 1.

Получить значение поля в следущей строке можно с помощью аналитической функции lead.

lead(value, shift, default_value) over (order by sorting)

где:

  • value - какое значение взять, ссылается на следующую строку.
  • shift - через сколько строк смотреть.
  • default_value - какое значение использовать, если следующей строки нет. Ссылается на текущую строку.
  • sorting - в каком порядке расположить строки для вычисления значения.
WITH RECURSIVE lv_hierarchy AS (
 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        1 AS level,
        '/' || c.name AS path,
        array[row_number () over (order by c.name)] AS path_sort
   FROM category c
  WHERE c.parent_category_id IS NULL

  UNION ALL

 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        p.level + 1 AS level,
        p.path || '/' || c.name AS path,
        p.path_sort || row_number () over (partition by c.parent_category_id order by c.name) AS path_sort
   FROM lv_hierarchy p,
        category c
  WHERE c.parent_category_id = p.category_id
)
SELECT c.category_id,
       lpad('', (c.level - 1) * 4, '.') || array_to_string(c.path_sort, '.') || '. ' || c.name AS full_name,
       c.level,
       lead(c.level, 1, c.level) over (order by c.path_sort) AS level_next
  FROM lv_hierarchy c
 ORDER BY path_sort
# category_id full_name level level_next
1 1 1. Товары для дома 1 2
2 3 ....1.1. Бытовая техника 2 1
3 2 2. Цифровая техника 1 2
4 7 ....2.1. Аудиотехника 2 2
5 6 ....2.2. Игровые консоли 2 2
6 4 ....2.3. Ноутбуки и аксессуары 2 3
7 9 ........2.3.1. Ноутбуки 3 3
8 10 ........2.3.2. Рюкзаки 3 2
9 8 ....2.4. Сотовые телефоны 2 2
10 5 ....2.5. Фотоаппараты 2 2

Осталось сравнить level и level_next:

WITH RECURSIVE lv_hierarchy AS (
 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        1 AS level,
        '/' || c.name AS path,
        array[row_number () over (order by c.name)] AS path_sort
   FROM category c
  WHERE c.parent_category_id IS NULL

  UNION ALL

 SELECT c.category_id,
        c.parent_category_id,
        c.name,
        p.level + 1 AS level,
        p.path || '/' || c.name AS path,
        p.path_sort || row_number () over (partition by c.parent_category_id order by c.name) AS path_sort
   FROM lv_hierarchy p,
        category c
  WHERE c.parent_category_id = p.category_id
)
SELECT c.category_id,
       lpad('', (c.level - 1) * 4, '.') || array_to_string(c.path_sort, '.') || '. ' || c.name AS full_name,
       c.level >= lead(c.level, 1, c.level) over (order by c.path_sort) AS is_leaf
  FROM lv_hierarchy c
 ORDER BY path_sort
# category_id full_name is_leaf
1 1 1. Товары для дома FALSE
2 3 ....1.1. Бытовая техника TRUE
3 2 2. Цифровая техника FALSE
4 7 ....2.1. Аудиотехника TRUE
5 6 ....2.2. Игровые консоли TRUE
6 4 ....2.3. Ноутбуки и аксессуары FALSE
7 9 ........2.3.1. Ноутбуки TRUE
8 10 ........2.3.2. Рюкзаки TRUE
9 8 ....2.4. Сотовые телефоны TRUE
10 5 ....2.5. Фотоаппараты TRUE

P.S. Зачем вообще решать одну задачу разными способами? Когда в таблицах очень много строк, тогда запросы могут выполняться медленно. В зависимости от плана выполнения запроса время выполнения может различаться во много раз. Чтобы запрос выполнялся по разному, нужно уметь получать один и тот же результат разными способами.

10.11 Нумерация вложенных списков

11.1 Получение номера строки