Описанный в предыдущем задании способ сортировки не является надежным. Сортировка сбивается, когда на одном уровне есть два родителя, название одного из которых включает название другого. В случае ФИО это могут быть полные тезки.
Добавим к нашему запросу получения категорий товаров еще одну категорию "Товары для дома и бани" и посмотрим, как отработает сортировка.
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
, только оперирует строками одного родителя.