Senior: питання на співбесіді з теми «Можливості SQL у PostgreSQL»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
2 питання
У PostgreSQL у WITH можна використовувати не лише SELECT, а й INSERT, UPDATE, DELETE з RETURNING. Результат такого CTE доступний основному запиту.
Архівування: перенести старі замовлення в архів одним запитом:
WITH moved AS (
DELETE FROM orders
WHERE created_at < now() - interval '2 years'
RETURNING *
)
INSERT INTO orders_archive
SELECT * FROM moved;
Видалення й вставка - один оператор, одна атомарна операція: рядок не може зникнути з orders, не з'явившись в архіві.
Створити пов'язані записи з отриманим id:
WITH new_user AS (
INSERT INTO users (email) VALUES ('olena@example.com')
RETURNING id
)
INSERT INTO profiles (user_id, locale)
SELECT id, 'uk' FROM new_user;
Журнал змін разом з оновленням:
WITH changed AS (
UPDATE products SET price = price * 1.1
WHERE category_id = 3
RETURNING id, price
)
INSERT INTO price_history (product_id, price, changed_at)
SELECT id, price, now() FROM changed;
Важливі правила виконання:
- усі частини бачать один знімок даних - знімок на момент початку оператора. Основний запит не бачить змін, зроблених у CTE, через звичайне читання таблиці - лише через
RETURNING; - не можна змінювати той самий рядок двічі в одному операторі (в CTE й в основному запиті) - результат непередбачуваний;
- CTE зі зміною даних виконується завжди повністю, навіть якщо основний запит не читає всіх його рядків;
- порядок виконання частин не гарантований - не можна покладатися на те, що один CTE спрацює раніше за інший, якщо вони не пов'язані через
RETURNING.
Переваги перед кількома запитами в транзакції:
- один мережевий обмін замість кількох;
- немає проміжного стану в застосунку - не треба передавати тисячі id з бази в PHP і назад.
Обмеження й ризики:
- великі обсяги: перенесення мільйонів рядків одним оператором - довга транзакція, роздування таблиць і блокування. Для архівування великих таблиць - порціями (
... WHERE id IN (SELECT id ... LIMIT 10000)) у циклі; - читабельність: складний ланцюжок CTE важко рев'ювати - коментарі й тести на ці запити обов'язкові;
- переносимість: MySQL такого не підтримує.
У Laravel - сирий запит через DB::statement() чи DB::select() (якщо потрібен результат RETURNING).
WITH RECURSIVE складається з двох частин, об'єднаних UNION ALL:
- початкова - стартові рядки;
- рекурсивна - посилається на сам CTE і додає наступний рівень, доки не поверне порожній результат.
Усі підкатегорії категорії з глибиною:
WITH RECURSIVE tree AS (
SELECT id, parent_id, name, 1 AS depth
FROM categories
WHERE id = 10
UNION ALL
SELECT c.id, c.parent_id, c.name, t.depth + 1
FROM categories c
JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree;
Шлях від вузла до кореня (хлібні крихти) - те саме в зворотному напрямку: JOIN tree t ON c.id = t.parent_id.
Проблема циклів. У дереві циклів немає, але в графі (рекомендації, залежності, «хто кого запросив») чи в зіпсованих даних (категорія стала батьком свого предка) рекурсія ніколи не завершиться - запит працюватиме до вичерпання пам'яті чи тайм-ауту.
PostgreSQL 14+: CYCLE - вбудоване виявлення циклів:
WITH RECURSIVE graph AS (
SELECT id, parent_id FROM categories WHERE id = 10
UNION ALL
SELECT c.id, c.parent_id FROM categories c JOIN graph g ON c.parent_id = g.id
) CYCLE id SET is_cycle USING path
SELECT * FROM graph WHERE NOT is_cycle;
PostgreSQL сам відстежує шлях і зупиняє гілку, що повертається до вже відвіданого вузла.
SEARCH задає порядок обходу:
) SEARCH DEPTH FIRST BY id SET ordercol
SELECT * FROM tree ORDER BY ordercol;
DEPTH FIRST дає порядок, у якому зручно виводити дерево з відступами; BREADTH FIRST - рівень за рівнем.
До PG 14 - вручну: накопичувати масив відвіданих id і перевіряти NOT c.id = ANY(path).
Додаткові запобіжники:
- обмеження глибини (
WHERE t.depth < 20) - навіть зCYCLEзахищає від неочікувано глибоких структур; statement_timeoutдля таких запитів;UNIONзамістьUNION ALLприбирає дублікати рядків і теж обриває прості цикли, але дорожчий і не завжди достатній.
Продуктивність: індекс на parent_id обов'язковий - кожен рівень рекурсії шукає дітей за ним.
Альтернативи рекурсії для дерев, які часто читають і рідко змінюють: матеріалізований шлях (ltree), closure table, nested sets - вони дають піддерево одним простим запитом.
У Laravel рекурсивні CTE пишуть сирим SQL або через пакет staudenmeir/laravel-adjacency-list, що додає зв'язки descendants() і ancestors() на основі WITH RECURSIVE.