Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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: оператори зміни даних

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.

Докладніше в документації: WITH: виявлення циклів