Зачем эта тема: учимся программировать на стороне БД — функции, процедуры, триггеры, рекурсивные запросы и window functions. Это то, что отделяет крепкого мидла от джуна и стабильно появляется на финале.
Производительность — данные не гоняются по сети туда-обратно.
Переиспользование — одна реализация для всех клиентов.
Безопасность — пользователю можно дать право только на процедуру, а не на таблицу.
stored procedure — именованный блок кода, хранящийся в БД, вызывается через CALL. Может изменять данные. dbterm
function — то же, но возвращает значение и используется внутри SELECT. dbterm
Procedure
Function
Вызов
CALL proc(...)
SELECT func(...)
Возвращает
необязательно (OUT-параметры)
обязательно
В SELECT
нельзя
можно
Транзакции
можно управлять (COMMIT внутри)
обычно нельзя
Изменяет данные
да
в большинстве СУБД с ограничениями
1.1 Function — пример (PostgreSQL)
CREATE OR REPLACE FUNCTION get_total_balance(cust_id INT)RETURNS DECIMAL(15,2) AS $$DECLARE total DECIMAL(15,2);BEGIN SELECT COALESCE(SUM(balance), 0) INTO total FROM accounts WHERE customer_id = cust_id; RETURN total;END;$$ LANGUAGE plpgsql;-- использованиеSELECT name, get_total_balance(customer_id) AS totalFROM customers;
CREATE FUNCTION name(params) RETURNS type AS $$ … $$ LANGUAGE plpgsql;sqlcommand
Создаёт функцию.
DECLARE var TYPE; — объявление локальной переменной.
SELECT … INTO var — записать результат в переменную.
RETURN val; — вернуть значение.
1.2 Procedure — пример
CREATE OR REPLACE PROCEDURE transfer( from_id INT, to_id INT, amount DECIMAL) AS $$BEGIN IF amount <= 0 THEN RAISE EXCEPTION 'Amount must be positive'; END IF; UPDATE accounts SET balance = balance - amount WHERE account_id = from_id; UPDATE accounts SET balance = balance + amount WHERE account_id = to_id;END;$$ LANGUAGE plpgsql;CALL transfer(1, 2, 1000);
CREATE PROCEDURE name(params) AS $$ … $$ LANGUAGE plpgsql;sqlcommand
Создаёт процедуру.
В Postgres триггер = функция + trigger declaration.
-- Функция-триггерCREATE OR REPLACE FUNCTION log_balance_change()RETURNS TRIGGER AS $$BEGIN IF NEW.balance <> OLD.balance THEN INSERT INTO audit_log(account_id, old_bal, new_bal, changed_at) VALUES (OLD.account_id, OLD.balance, NEW.balance, NOW()); END IF; RETURN NEW;END;$$ LANGUAGE plpgsql;-- Сам триггерCREATE TRIGGER trg_balance_auditAFTER UPDATE ON accountsFOR EACH ROWEXECUTE FUNCTION log_balance_change();
CREATE TRIGGER name BEFORE/AFTER event ON table FOR EACH ROW EXECUTE FUNCTION fn();sqlcommand
Создаёт триггер.
Опция
Варианты
Время
BEFORE, AFTER, INSTEAD OF (для views)
Событие
INSERT, UPDATE, DELETE, UPDATE OF col
Уровень
FOR EACH ROW, FOR EACH STATEMENT
2.3 Псевдо-таблицы NEW и OLD
OLD
NEW
INSERT
—
вставляемая строка
UPDATE
старая
новая
DELETE
удаляемая
—
-- BEFORE INSERT — установить created_atBEGIN NEW.created_at := NOW(); RETURN NEW;END;-- AFTER DELETE — записать в архивBEGIN INSERT INTO archived VALUES (OLD.*); RETURN OLD;END;
2.4 BEFORE vs AFTER
BEFORE — можно изменить NEW, можно отменить операцию (вернув NULL).
AFTER — данные уже записаны, обычно для аудита/каскадов.
Триггеры — мина замедленного действия
Триггер невидим для разработчика, читающего INSERT. Пишет лог, обновляет счётчик, дёргает другую таблицу — а ты не знаешь почему. Документируй и не злоупотребляй.
2.5 Включение / выключение
ALTER TABLE accounts DISABLE TRIGGER trg_balance_audit;ALTER TABLE accounts ENABLE TRIGGER trg_balance_audit;DROP TRIGGER trg_balance_audit ON accounts;
3. Рекурсивные запросы — WITH RECURSIVE
Используются для иерархий (сотрудник→менеджер, категория→подкатегория) и графов.
WITH RECURSIVE org_chart AS ( -- Anchor: корни иерархии SELECT emp_id, name, manager_id, 1 AS level FROM employees WHERE manager_id IS NULL UNION ALL -- Recursive: добавить подчинённых на следующий уровень SELECT e.emp_id, e.name, e.manager_id, oc.level + 1 FROM employees e JOIN org_chart oc ON e.manager_id = oc.emp_id)SELECT * FROM org_chart ORDER BY level, name;
WITH RECURSIVE name AS (anchor UNION ALL recursive_part) …sqlcommand
Рекурсивная CTE: anchor + повторяющаяся часть, которая ссылается на саму CTE.
Структура recursive CTE
Anchor — стартовый запрос (без рекурсии).
UNION ALL.
Recursive part — запрос, ссылающийся на саму CTE.
Условие выхода — когда JOIN перестаёт находить новые строки.
Бесконечная рекурсия
Если в графе есть циклы — recursive CTE зациклится. Решение: добавить флаг “уже посетили” (массив пути).
4. Window functions
window function — агрегат, вычисляемый на “окне” вокруг строки, БЕЗ свёртки строк в одну. dbterm
GROUP BY свернёт 100 строк в 1. Window function вернёт 100 строк с агрегатом для каждой.
4.1 Синтаксис
function() OVER ( PARTITION BY col -- разбить на группы (опционально) ORDER BY col -- порядок внутри группы (для running/ranking) ROWS/RANGE … -- границы окна (опционально))
OVER (PARTITION BY … ORDER BY …)sqlcommand — задаёт окно.
4.2 Ranking functions
Функция
Что делает
ROW_NUMBER()
уникальный номер строки в окне
RANK()
ранг с пропусками при равенстве (1, 2, 2, 4)
DENSE_RANK()
ранг без пропусков (1, 2, 2, 3)
NTILE(n)
разбить на n равных групп
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rank, DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rnFROM employees;
4.3 Aggregates как window
-- Текущая сумма по дням (running total)SELECT day, amount, SUM(amount) OVER (ORDER BY day ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_totalFROM sales;-- Среднее по последним 7 днямSELECT day, amount, AVG(amount) OVER (ORDER BY day ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS avg_7dFROM sales;
4.4 LAG / LEAD
-- Сравнить с предыдущим днёмSELECT day, amount, LAG(amount, 1) OVER (ORDER BY day) AS prev_day, amount - LAG(amount, 1) OVER (ORDER BY day) AS diffFROM sales;
LAG(col, n)sqlcommand — значение на n строк назад.
LEAD(col, n)sqlcommand — значение на n строк вперёд.
4.5 PARTITION BY
-- Топ-3 зарплат В КАЖДОМ отделеSELECT * FROM ( SELECT name, dept_id, salary, DENSE_RANK() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rk FROM employees) subWHERE rk <= 3;
PARTITION BY = "GROUP BY без свёртки"
PARTITION BY dept_id = вычислять окно отдельно для каждого отдела, но строки сохранить.
Window vs GROUP BY
Window
GROUP BY
Сохраняет все строки
Сворачивает в одну на группу
Применяется ПОСЛЕ WHERE/GROUP BY
Применяется к строкам после WHERE
Можно несколько разных окон
Одна группировка на запрос
5. OLAP-расширения GROUP BY
Для аналитики поверх обычного GROUP BY.
5.1 ROLLUP — иерархические итоги
SELECT year, quarter, SUM(sales)FROM salesGROUP BY ROLLUP(year, quarter);
Даст: (year, quarter), (year), () — итоги по году + grand total.
5.2 CUBE — все комбинации
SELECT year, region, SUM(sales)FROM salesGROUP BY CUBE(year, region);
ROLLUP(a, b) = (a, b), (a), ().
CUBE(a, b) = (a, b), (a), (b), ().
GROUPING SETS = что хочешь, то и пишешь.
6. PIVOT — превратить строки в столбцы
В чистом SQL делается через CASE + SUM:
SELECT year, SUM(CASE WHEN region = 'KZ' THEN sales ELSE 0 END) AS kz, SUM(CASE WHEN region = 'KG' THEN sales ELSE 0 END) AS kg, SUM(CASE WHEN region = 'UZ' THEN sales ELSE 0 END) AS uzFROM salesGROUP BY year;
В Oracle/SQL Server есть отдельный PIVOT оператор. В Postgres — через crosstab extension.
7. Cursor — построчная обработка
cursor — указатель на строку результата запроса, позволяет обрабатывать строки по одной. dbterm
DO $$DECLARE cur CURSOR FOR SELECT account_id, balance FROM accounts; rec RECORD;BEGIN OPEN cur; LOOP FETCH cur INTO rec; EXIT WHEN NOT FOUND; IF rec.balance < 0 THEN UPDATE accounts SET frozen = TRUE WHERE account_id = rec.account_id; END IF; END LOOP; CLOSE cur;END $$;
Cursor = медленно
Построчная обработка отказывается от set-based мощи SQL. Используй только когда нельзя выразить через UPDATE/JOIN/window function. На больших данных cursor в 100 раз медленнее set-операции.
8. Транзакции в advanced коде
CREATE PROCEDURE transfer(...) AS $$BEGIN BEGIN UPDATE accounts SET balance = balance - amt WHERE id = src; UPDATE accounts SET balance = balance + amt WHERE id = dst; EXCEPTION WHEN OTHERS THEN RAISE NOTICE 'Failed: %', SQLERRM; ROLLBACK; END;END;$$ LANGUAGE plpgsql;
SAVEPOINT
Можно откатить часть транзакции до отметки:
BEGIN;UPDATE …;SAVEPOINT s1;UPDATE …; -- что-то пошло не такROLLBACK TO s1; -- откатили только этоCOMMIT; -- остальное зафиксируем
9. Часто на экзамене
Exam traps
Procedure vs Function — где можно вызвать в SELECT, где нет.
NEW vs OLD в триггерах — зависит от события.
BEFORE vs AFTER триггер — BEFORE может изменить NEW.
Recursive CTE — нужны anchor + UNION ALL + recursive part.
RANK vs DENSE_RANK vs ROW_NUMBER — отличие при равных значениях.
PARTITION BY ≠ GROUP BY — window не сворачивает строки.
Cursor — антипаттерн на больших данных.
10. Mini-quiz
Можно ли вызвать procedure внутри SELECT?
Что вернёт NEW.balance в triggere BEFORE INSERT? А в AFTER DELETE?
RANK() для зарплат 100, 100, 90 — какие значения вернёт?
Что произойдёт в recursive CTE без условия выхода?
В чём разница между ROLLUP(a, b) и CUBE(a, b)?
Если понял эту главу
Ты можешь писать функции, процедуры, триггеры, рекурсивные запросы по иерархиям, window functions для аналитики, OLAP-агрегаты. Это сильно отличает от тех, кто умеет только SELECT.