Дървета и редици: WITH RECURSIVE
Накратко: WITH RECURSIVE е заявка със стъпка, която чете собствения си резултат, и така обхожда дървета като категориите или прави редица от дати.
Проблем
Пътят до категорията
Сайтът показва над всеки продукт пътя до него: „Храни > Млечни“. Категориите са дърво: всяка има parent_id, който сочи категорията над нея. С self join от модула за свързване става така:
Това работи, защото дървото е на две нива. Ако утре под „Млечни“ се появи „Сирена“, трябва още едно свързване, а при четири нива още едно. Колко нива ще има, не знаеш предварително.
Идеята
Стъпка, която чете себе си
WITH RECURSIVE позволява на една стъпка да чете собствените си редове. Ето най-малкия пример, числата от 1 до 5:
Стъпката има две части, свързани с UNION ALL:
- Началото
SELECT 1дава първия ред. - Рекурсивната част
SELECT x + 1 FROM n WHERE x < 5взима редовете от предишния кръг и прави нови: от 1 прави 2, от 2 прави 3.
Това се повтаря, докато рекурсивната част не върне нищо: при x = 5 условието вече не е вярно. Такава стъпка се казва рекурсивна (recursive CTE).
Без WHERE x < 5 заявката никога няма да спре. Всяка рекурсивна част трябва да има условие, което рано или късно спира.
Идеята
Дървото на категориите
Сега пътят. Началото са главните категории (без родител). Рекурсивната част намира децата на всичко, намерено досега, и добавя името им към пътя:
В първия кръг са „Храни“ и „Дом“ (ниво 1). Във втория са шестте подкатегории (ниво 2), всяка с пътя на родителя, „ > “ и своето име. В третия кръг няма деца и всичко спира. Ако дървото стане по-дълбоко, същата заявка ще стигне и до новите нива.
Пробвай
Всичко в „Храни“
Покажи името и цената на всички продукти в „Храни“, заедно с подкатегориите ѝ. Номерата на категориите вземи с рекурсивна стъпка, а не на ръка. Полето започва от началото на стъпката.
Решение
WITH RECURSIVE sub(id) AS (
SELECT id FROM categories WHERE name = 'Храни'
UNION ALL
SELECT c.id FROM categories c JOIN sub s ON c.parent_id = s.id
)
SELECT name, price FROM products
WHERE category_id IN (SELECT id FROM sub);Началото е „Храни“, рекурсивната част добавя децата ѝ, а продуктите се търсят с category_id IN (SELECT id FROM sub). 18 продукта. Самата „Храни“ няма продукти, но е в стъпката и не пречи.
Виж го
Дните без продажби
Управителят иска графика на продажбите по дни за октомври 2026 г. Първият опит:
Пет реда. Октомври има 31 дни, а графиката ще свърже 3 октомври направо с 5-и и ще скрие, че от 7 до 17 октомври не е продадено нищо. Ден без поръчки го няма в orders, затова GROUP BY няма от какво да направи ред.
Идеята
Редица от дати
Рекурсивна стъпка може да направи всички дни сама. Началото е 1 октомври, а всеки кръг добавя един ден с date(day, '+1 day'):
31 реда. Сега продажбите се закачат към дните с LEFT JOIN, а за дните без поръчки COALESCE слага 0:
Всички 31 дни са тук, и тези с нула. Редът на LEFT JOIN е важен: отляво са дните, които искаш всичките, отдясно са поръчките, които може да липсват.
По-точно, за любопитните
PostgreSQL има готова функция за редица: generate_series(DATE '2026-10-01', DATE '2026-10-31', INTERVAL '1 day'). В SQLite и в MySQL (от версия 8) такава редица се прави с WITH RECURSIVE, както тук.
Упражнение
Две задачи
1. Колко дни през октомври 2026 г. магазинът не е имал нито една поръчка?
Преброй дните от октомври 2026 г. без нито една поръчка, до края на месеца, включително дните след последната поръчка (колона quiet_days). Полето вече има редицата от дни.
Решение
WITH RECURSIVE days(day) AS (
SELECT '2026-10-01'
UNION ALL
SELECT date(day, '+1 day') FROM days WHERE day < '2026-10-31'
)
SELECT COUNT(*) AS quiet_days FROM days d
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.ordered_at = d.day);WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.ordered_at = d.day) и COUNT(*): 26 дни. Поръчки има само в 5 дни.
2. Сайтът иска пълния път за всеки продукт: „Храни > Млечни > Сирене“.
Покажи името на всеки продукт, който има категория, и пълния му път (колона breadcrumb), от главната категория до името на продукта. Полето вече има дървото.
Решение
WITH RECURSIVE tree(id, name, path, level) AS (
SELECT id, name, name, 1
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, t.path || ' > ' || c.name, t.level + 1
FROM categories c
JOIN tree t ON c.parent_id = t.id
)
SELECT p.name, t.path || ' > ' || p.name AS breadcrumb
FROM products p
JOIN tree t ON t.id = p.category_id;JOIN tree t ON t.id = p.category_id и t.path || ' > ' || p.name. 24 реда: подаръчната карта няма категория и JOIN я изпуска. С LEFT JOIN ще е вътре, но с NULL за пътя.
Тест
Провери се
Свързани уроци
Раздел „Свързани уроци“- Нужно отпреди: LEFT JOIN: и редовете без двойка · Много таблици и таблица със себе си · Подзаявки · Отчет на стъпки: WITH
- Продължава в: Графови бази
- Задачи по темата: Подзаявки и прозорци