Описанный в предыдущем задании способ сортировки не является надежным. Сортировка сбивается, когда на одном уровне есть два родителя, название одного из которых включает название другого. В случае ФИО это могут быть полные тезки.
Добавим к нашему запросу получения категорий товаров еще одну категорию "Товары для дома и бани" и посмотрим, как отработает сортировка.
WITH RECURSIVE lv_category AS (
SELECT c.category_id,
c.parent_category_id,
c.name
FROM category c
UNION ALL
SELECT 11 AS category_id,
NULL AS parent_category_id,
'Товары для дома и бани' AS name
),
lv_hierarchy AS (
SELECT c.category_id,
c.parent_category_id,
c.name,
1 AS level,
'/' || c.name AS path
FROM lv_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
FROM lv_hierarchy p,
lv_category c
WHERE c.parent_category_id = p.category_id
)
SELECT *
FROM lv_hierarchy
ORDER BY path
| # | category_id | parent_category_id | name | level | path |
|---|---|---|---|---|---|
| 1 | 1 | NULL | Товары для дома | 1 | /Товары для дома |
| 2 | 11 | NULL | Товары для дома и бани | 1 | /Товары для дома и бани |
| 3 | 3 | 1 | Бытовая техника | 2 | /Товары для дома/Бытовая техника |
| 4 | 2 | NULL | Цифровая техника | 1 | /Цифровая техника |
| 5 | 7 | 2 | Аудиотехника | 2 | /Цифровая техника/Аудиотехника |
| 6 | 6 | 2 | Игровые консоли | 2 | /Цифровая техника/Игровые консоли |
| 7 | 4 | 2 | Ноутбуки и аксессуары | 2 | /Цифровая техника/Ноутбуки и аксессуары |
| 8 | 9 | 4 | Ноутбуки | 3 | /Цифровая техника/Ноутбуки и аксессуары/Ноутбуки |
| 9 | 10 | 4 | Рюкзаки | 3 | /Цифровая техника/Ноутбуки и аксессуары/Рюкзаки |
| 10 | 8 | 2 | Сотовые телефоны | 2 | /Цифровая техника/Сотовые телефоны |
| 11 | 5 | 2 | Фотоаппараты | 2 | /Цифровая техника/Фотоаппараты |
Обрати внимание на первые три строки результата
| # | category_id | parent_category_id | name | level | path |
|---|---|---|---|---|---|
| 1 | 1 | NULL | Товары для дома | 1 | /Товары для дома |
| 2 | 11 | NULL | Товары для дома и бани | 1 | /Товары для дома и бани |
| 3 | 3 | 1 | Бытовая техника | 2 | /Товары для дома/Бытовая техника |
Строки 2 и 3 поменялись местами.
Решение 1 (костыльное)
Чтобы избежать включения названия одной категории в другую, можно при формировании пути для сортировки в конец названия для каждой категории добавить какую-нибудь уникальную подстроку. Например, идентификатор записи.
Добавим в наше решение еще один путь path_sort с добавлением идентификатора через пробел и отсортируем результат по нему:
WITH RECURSIVE lv_category AS (
SELECT c.category_id,
c.parent_category_id,
c.name
FROM category c
UNION ALL
SELECT 11 AS category_id,
NULL AS parent_category_id,
'Товары для дома и бани' AS name
),
lv_hierarchy AS (
SELECT c.category_id,
c.parent_category_id,
c.name,
1 AS level,
'/' || c.name AS path,
'/' || c.name || ' ' || c.category_id AS path_sort
FROM lv_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 || '/' || c.name || ' ' || c.category_id AS path_sort
FROM lv_hierarchy p,
lv_category c
WHERE c.parent_category_id = p.category_id
)
SELECT *
FROM lv_hierarchy
ORDER BY path_sort
| # | category_id | parent_category_id | name | level | path | path_sort |
|---|---|---|---|---|---|---|
| 1 | 1 | NULL | Товары для дома | 1 | /Товары для дома | /Товары для дома 1 |
| 2 | 3 | 1 | Бытовая техника | 2 | /Товары для дома/Бытовая техника | /Товары для дома 1/Бытовая техника 3 |
| 3 | 11 | NULL | Товары для дома и бани | 1 | /Товары для дома и бани | /Товары для дома и бани 11 |
| 4 | 2 | NULL | Цифровая техника | 1 | /Цифровая техника | /Цифровая техника 2 |
| 5 | 7 | 2 | Аудиотехника | 2 | /Цифровая техника/Аудиотехника | /Цифровая техника 2/Аудиотехника 7 |
| 6 | 6 | 2 | Игровые консоли | 2 | /Цифровая техника/Игровые консоли | /Цифровая техника 2/Игровые консоли 6 |
| 7 | 4 | 2 | Ноутбуки и аксессуары | 2 | /Цифровая техника/Ноутбуки и аксессуары | /Цифровая техника 2/Ноутбуки и аксессуары 4 |
| 8 | 9 | 4 | Ноутбуки | 3 | /Цифровая техника/Ноутбуки и аксессуары/Ноутбуки | /Цифровая техника 2/Ноутбуки и аксессуары 4/Ноутбуки 9 |
| 9 | 10 | 4 | Рюкзаки | 3 | /Цифровая техника/Ноутбуки и аксессуары/Рюкзаки | /Цифровая техника 2/Ноутбуки и аксессуары 4/Рюкзаки 10 |
| 10 | 8 | 2 | Сотовые телефоны | 2 | /Цифровая техника/Сотовые телефоны | /Цифровая техника 2/Сотовые телефоны 8 |
| 11 | 5 | 2 | Фотоаппараты | 2 | /Цифровая техника/Фотоаппараты | /Цифровая техника 2/Фотоаппараты 5 |
Строки встали на свои места, но по-прежнему можно подобрать данные, на которых оно сломается. Мы хотим иметь решение, которое будет работать в 100% случаев.
Надежное решение
Чтобы сделать надежное решение, нужно отказаться от сортировки по одному текстовому полю.
PostgreSQL позволяет в ORDER BY использовать массивы.
Например, если отсортировать массивы
- [2, 1, 2]
- [2, 1, 1]
- [2, 1]
- [2]
- [1]
- [1, 1]
- [1, 2]
по возрастанию, то строки встанут в следующем порядке
- [1]
- [1, 1]
- [1, 2]
- [2]
- [2, 1]
- [2, 1, 1]
- [2, 1, 2]
Воспользуемся этим для сортировки нашей иерархии каталогов товаров.
Осталось научиться 3 вещам:
- Определять номер строки для дочерних строк одного родителя на очередной итерации.
- Создавать массив из одного элемента.
- Добавлять элемент в массив на каждой итерации.
Определение номера строки для дочерних строк одного родителя
Для определение номера строки воспользуемся аналитической функцией row_number(). Аналитические функции работают как агрегатные функции, только не изменяют итоговое количество строк, а вычисляют результат для каждой строки.
WITH RECURSIVE lv_category AS (
SELECT c.category_id,
c.parent_category_id,
c.name
FROM category c
UNION ALL
SELECT 11 AS category_id,
NULL AS parent_category_id,
'Товары для дома и бани' AS name
)
SELECT c.parent_category_id,
c.name,
row_number () OVER (PARTITION BY c.parent_category_id ORDER BY c.name) AS sorting
FROM lv_category c
ORDER BY c.parent_category_id NULLS FIRST,
c.name
| # | parent_category_id | name | sorting |
|---|---|---|---|
| 1 | NULL | Товары для дома | 1 |
| 2 | NULL | Товары для дома и бани | 2 |
| 3 | NULL | Цифровая техника | 3 |
| 4 | 1 | Бытовая техника | 1 |
| 5 | 2 | Аудиотехника | 1 |
| 6 | 2 | Игровые консоли | 2 |
| 7 | 2 | Ноутбуки и аксессуары | 3 |
| 8 | 2 | Сотовые телефоны | 4 |
| 9 | 2 | Фотоаппараты | 5 |
| 10 | 4 | Ноутбуки | 1 |
| 11 | 4 | Рюкзаки | 2 |
Подробнее аналитические функции разберем в отдельной теме. Сейчас же кратко посмотрим на получение столбца sorting:
row_number () OVER (PARTITION BY c.parent_category_id ORDER BY c.name) AS sorting
Общий синтаксис вызова аналитических функций:
функция(...) OVER (условия для группировки строк и сортировки)
У нас:
row_number() - возвращает номер строки в группе, задаваемой PARTITION BY, с учетом сортировки из ORDER BY
PARTITION BY - по каким столбцам формировать группы строк. Работает так же, как и GROUP BY.
ORDER BY - по каким столбцам сортировать результат в рамках группы. Работает так же, как и ORDER BY в основном запросе.
Результат аналитической функции вычисляется для каждой строки.
Создание массива чисел
Для создание массива воспользуемся функцией array
SELECT array[1] AS one,
array[1, 2] AS two,
array[1, 2, 3] AS three
| # | one | two | three |
|---|---|---|---|
| 1 | [1] | [1, 2] | [1, 2, 3] |
Добавление элемента в массив
Для добавления элемента в конец массива достаточно новый элемент написать через ||
SELECT array[1] || 2 AS two,
array[1, 2] || 3 AS three
| # | two | three |
|---|---|---|
| 1 | [1, 2] | [1, 2, 3] |
Итоговое решение
Воспользуемся новыми знаниями для сортировки строк в иерархии каталогов товаров:
WITH RECURSIVE lv_category AS (
SELECT c.category_id,
c.parent_category_id,
c.name
FROM category c
UNION ALL
SELECT 11 AS category_id,
NULL AS parent_category_id,
'Товары для дома и бани' AS name
),
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 lv_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,
lv_category c
WHERE c.parent_category_id = p.category_id
)
SELECT *
FROM lv_hierarchy
ORDER BY path_sort
| # | category_id | parent_category_id | name | level | path | path_sort |
|---|---|---|---|---|---|---|
| 1 | 1 | NULL | Товары для дома | 1 | /Товары для дома | [1] |
| 2 | 3 | 1 | Бытовая техника | 2 | /Товары для дома/Бытовая техника | [1, 1] |
| 3 | 11 | NULL | Товары для дома и бани | 1 | /Товары для дома и бани | [2] |
| 4 | 2 | NULL | Цифровая техника | 1 | /Цифровая техника | [3] |
| 5 | 7 | 2 | Аудиотехника | 2 | /Цифровая техника/Аудиотехника | [3, 1] |
| 6 | 6 | 2 | Игровые консоли | 2 | /Цифровая техника/Игровые консоли | [3, 2] |
| 7 | 4 | 2 | Ноутбуки и аксессуары | 2 | /Цифровая техника/Ноутбуки и аксессуары | [3, 3] |
| 8 | 9 | 4 | Ноутбуки | 3 | /Цифровая техника/Ноутбуки и аксессуары/Ноутбуки | [3, 3, 1] |
| 9 | 10 | 4 | Рюкзаки | 3 | /Цифровая техника/Ноутбуки и аксессуары/Рюкзаки | [3, 3, 2] |
| 10 | 8 | 2 | Сотовые телефоны | 2 | /Цифровая техника/Сотовые телефоны | [3, 4] |
| 11 | 5 | 2 | Фотоаппараты | 2 | /Цифровая техника/Фотоаппараты | [3, 5] |
P.S. Если вы используется СУБД Oracle, то проблема с сортировкой в иерархических запросах решается очень легко. В
конструкции CONNECT BY отсортировать дочерние строки одного родителя можно с помощью ORDER SIBLINGS BY. ORDER SIBLINGS BY работает точно так же, как и ORDER BY, только оперирует строками одного родителя.