БД:Теория:Глава 5: различия между версиями
Ivabus (обсуждение | вклад) Импорт Метки: очистка отменено |
Ivabus (обсуждение | вклад) Импорт Метка: ручная отмена |
||
Строка 1: | Строка 1: | ||
В этой главе мы углубимся в команды для изменения данных (DML), рассмотрим мощный инструмент SQL — представления (Views), научимся создавать собственные функции и процедуры с использованием процедурного расширения PL/pgSQL, разберемся с триггерами для реализации сложной логики и автоматизации, а также подробно рассмотрим механизм транзакций для обеспечения целостности данных. Эта глава основана на материалах Лекции 5: “PL/pgSQL” и частично затрагивает DML из Лекции 4/5. | |||
<span id="часть-1-манипулирование-данными-dml"></span> | |||
=== Часть 1: Манипулирование Данными (DML) === | |||
DML (Data Manipulation Language) — это часть SQL, отвечающая за добавление, изменение и удаление данных ''внутри'' таблиц. | |||
<ol style="list-style-type: decimal;"> | |||
<li><p>'''<code>INSERT</code>''': Добавляет одну или несколько новых строк в таблицу.</p> | |||
<ul> | |||
<li><p>'''Синтаксис 1 (Указание столбцов):'''</p> | |||
<syntaxhighlight lang="sql">INSERT INTO имя_таблицы (столбец1, столбец2, ...) | |||
VALUES (значение1, значение2, ...);</syntaxhighlight> | |||
<p>''Порядок значений должен соответствовать порядку указанных столбцов.''</p></li> | |||
<li><p>'''Синтаксис 2 (Без указания столбцов):'''</p> | |||
<syntaxhighlight lang="sql">INSERT INTO имя_таблицы VALUES (значение1, значение2, ...);</syntaxhighlight> | |||
<p>''Значения должны быть перечислены в том же порядке, в каком столбцы определены в таблице. Менее надежный способ, т.к. зависит от порядка столбцов.''</p></li> | |||
<li><p>'''Синтаксис 3 (Вставка из <code>SELECT</code>):'''</p> | |||
<syntaxhighlight lang="sql">INSERT INTO имя_таблицы (столбец1, столбец2, ...) | |||
SELECT выражение1, выражение2, ... | |||
FROM другая_таблица | |||
WHERE условие;</syntaxhighlight> | |||
<p>''Позволяет вставить в таблицу результат другого запроса. Количество и типы столбцов в <code>INSERT</code> и <code>SELECT</code> должны совпадать.''</p></li></ul> | |||
<p>'''Пример (из слайдов):'''</p> | |||
<syntaxhighlight lang="sql">-- Пример 1 и 2: Добавление студента | |||
INSERT INTO student VALUES (123, 'Vasya', 345); | |||
INSERT INTO student (StudID, GroupID, Name) VALUES (123, 345, 'Vasya'); | |||
-- Пример 3: Добавление студентов из временной таблицы по условию | |||
INSERT INTO student (StudID, Name, GroupID) -- Указываем порядок для INSERT | |||
SELECT ID, Name, 345 -- Указываем порядок в SELECT | |||
FROM tmp_person | |||
WHERE Exam > 70;</syntaxhighlight></li> | |||
<li><p>'''<code>UPDATE</code>''': Изменяет существующие строки в таблице.</p> | |||
<ul> | |||
<li><p>'''Синтаксис:'''</p> | |||
<syntaxhighlight lang="sql">UPDATE имя_таблицы | |||
SET столбец1 = новое_значение1, | |||
столбец2 = новое_значение2, | |||
... | |||
[ WHERE условие ]; -- Если WHERE отсутствует, обновятся ВСЕ строки!</syntaxhighlight> | |||
<p>''Условие <code>WHERE</code> определяет, какие строки нужно обновить.''</p></li></ul> | |||
<p>'''Пример (из слайдов):'''</p> | |||
<syntaxhighlight lang="sql">-- Установить всем студентам GroupID = 578 | |||
UPDATE student SET GroupID = 578; | |||
-- Установить GroupID = 34 для студента с именем 'Ivan' | |||
UPDATE student SET GroupID = 34 WHERE Name = 'Ivan';</syntaxhighlight></li> | |||
<li><p>'''<code>DELETE</code>''': Удаляет существующие строки из таблицы.</p> | |||
<ul> | |||
<li><p>'''Синтаксис:'''</p> | |||
<syntaxhighlight lang="sql">DELETE FROM имя_таблицы | |||
[ WHERE условие ]; -- Если WHERE отсутствует, удалятся ВСЕ строки!</syntaxhighlight> | |||
<p>''Условие <code>WHERE</code> определяет, какие строки нужно удалить.''</p></li></ul> | |||
<p>'''Пример (из слайдов):'''</p> | |||
<syntaxhighlight lang="sql">-- Удалить студентов с GroupID = 34 | |||
DELETE FROM student WHERE GroupID = 34; | |||
-- Удалить ВСЕХ студентов | |||
DELETE FROM student;</syntaxhighlight></li> | |||
<li><p>'''<code>TRUNCATE</code>''': Быстро удаляет '''все''' строки из таблицы (или нескольких таблиц).</p> | |||
<ul> | |||
<li><p>'''Синтаксис:'''</p> | |||
<syntaxhighlight lang="sql">TRUNCATE [TABLE] имя_таблицы1 [, имя_таблицы2 ...];</syntaxhighlight></li> | |||
<li><p>'''Отличия от <code>DELETE</code> без <code>WHERE</code>:'''</p> | |||
<ul> | |||
<li>Обычно '''намного быстрее''' для больших таблиц, так как не сканирует таблицу построчно и не записывает удаление каждой строки в лог транзакций так же подробно.</li> | |||
<li>Не запускает триггеры <code>DELETE</code> на уровне строк (<code>FOR EACH ROW</code>).</li> | |||
<li>Сбрасывает счетчики последовательностей (<code>SERIAL</code> и т.п.), связанных с таблицей (в PostgreSQL, если указано <code>RESTART IDENTITY</code>).</li> | |||
<li>Не возвращает количество удаленных строк.</li></ul> | |||
</li> | |||
<li><p>Используйте <code>TRUNCATE</code>, когда нужно быстро и полностью очистить таблицу.</p></li></ul> | |||
<p>'''Пример (из слайдов):'''</p> | |||
<syntaxhighlight lang="sql">-- Быстро очистить таблицу student | |||
TRUNCATE TABLE student; | |||
-- Быстро очистить несколько таблиц | |||
TRUNCATE TABLE student, groups, exams;</syntaxhighlight></li></ol> | |||
<span id="часть-2-выполнение-скриптов"></span> | |||
=== Часть 2: Выполнение Скриптов === | |||
Часто SQL-код (особенно DDL для создания схемы или DML для наполнения данными) хранится в файлах (<code>.sql</code>). Такие файлы называются '''скриптами'''. Выполнить скрипт в PostgreSQL можно несколькими способами: | |||
<ol style="list-style-type: decimal;"> | |||
<li><p>'''Через утилиту <code>psql</code> при запуске:'''</p> | |||
<syntaxhighlight lang="bash">psql -U имя_пользователя -d имя_базы -f путь/к/файлу/script.sql</syntaxhighlight> | |||
<ul> | |||
<li><code>-U</code>: Имя пользователя PostgreSQL.</li> | |||
<li><code>-d</code>: Имя базы данных, к которой подключаемся.</li> | |||
<li><code>-f</code>: Путь к файлу скрипта.</li></ul> | |||
</li> | |||
<li><p>'''Внутри интерактивной сессии <code>psql</code>:''' Используются мета-команды (начинаются с <code>\</code>):</p> | |||
<syntaxhighlight lang="sql">-- Подключаемся к базе | |||
psql -U имя_пользователя -d имя_базы | |||
-- Выполняем скрипт из файла | |||
\i путь/к/файлу/script.sql | |||
-- или эквивалентная команда | |||
\include путь/к/файлу/script.sql</syntaxhighlight> | |||
<p>''Это удобно для выполнения небольших скриптов или частей кода во время работы с БД.''</p></li></ol> | |||
<span id="часть-3-представления-views"></span> | |||
=== Часть 3: Представления (Views) === | |||
Мы уже кратко упоминали '''виртуальные таблицы'''. Основной их вид — это '''представления'''. | |||
* '''Представление (View)''' — это сохраненный в базе данных '''именованный SQL-запрос''' (<code>SELECT</code>). | |||
* С представлением можно работать почти так же, как с обычной таблицей (выполнять <code>SELECT</code>, иногда <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>, если представление обновляемое). | |||
* Представление '''не хранит данные''' само по себе (за исключением материализованных представлений). Каждый раз при обращении к представлению СУБД выполняет лежащий в его основе <code>SELECT</code>-запрос к базовым таблицам. | |||
'''Зачем нужны представления?''' | |||
# '''Упрощение сложных запросов:''' Длинный и сложный запрос можно “спрятать” за простым именем представления. | |||
# '''Сокрытие структуры данных:''' Можно предоставить пользователям доступ только к представлению, которое показывает лишь необходимые им столбцы и строки, скрывая сложность или конфиденциальность базовых таблиц. | |||
# '''Логическая независимость данных:''' Если структура базовых таблиц меняется, можно изменить только определение представления, а запросы пользователей, обращающиеся к представлению, останутся прежними. | |||
# '''Обеспечение обратной совместимости:''' При рефакторинге схемы можно создать представления, имитирующие старую структуру. | |||
'''Создание Представления (<code>CREATE VIEW</code>)''' | |||
<syntaxhighlight lang="sql">CREATE [ OR REPLACE ] VIEW имя_представления [ (имя_колонки1, имя_колонки2, ...) ] | |||
AS | |||
SELECT_запрос; -- Запрос, определяющий представление</syntaxhighlight> | |||
* <code>OR REPLACE</code>: Если представление с таким именем уже существует, оно будет заменено. | |||
* <code>[ (имя_колонки1, ...) ]</code>: Необязательный список имен для столбцов представления. Если опущен, используются имена столбцов из <code>SELECT_запрос</code>. | |||
'''Пример (из слайдов):''' Создать представление, показывающее студентов факультета ПИКТ (предполагаем, что это группы, начинающиеся с ‘P3’). | |||
<syntaxhighlight lang="sql">-- Создаем представление | |||
CREATE VIEW PICTStudents AS | |||
SELECT * -- Выбираем все колонки из student | |||
FROM student | |||
WHERE GroupID IN ( -- Выбираем студентов, чья группа в списке групп ПИКТ | |||
SELECT GroupID | |||
FROM groups | |||
WHERE GroupName LIKE 'P3%' -- Находим группы, чье имя начинается с 'P3' | |||
); | |||
-- Используем представление как обычную таблицу | |||
SELECT st_name, GroupID FROM PICTStudents WHERE st_id > 100;</syntaxhighlight> | |||
'''Пример (с переименованием колонок):''' | |||
<syntaxhighlight lang="sql">CREATE VIEW PICTStudents2 (PICTId, pSurname) AS -- Задаем свои имена колонок | |||
SELECT StudentID, Surname -- Выбираем нужные колонки из student | |||
FROM student | |||
WHERE GroupID IN ( | |||
SELECT GroupID FROM groups WHERE GroupName LIKE 'P3%' | |||
); | |||
-- Используем представление с новыми именами | |||
SELECT PICTId FROM PICTStudents2 WHERE pSurname LIKE 'Иван%';</syntaxhighlight> | |||
'''Материализованные Представления (<code>MATERIALIZED VIEW</code>)''' | |||
* В отличие от обычных представлений, материализованное представление '''хранит результат''' своего запроса физически в базе данных (как кэш). | |||
* Обращение к материализованному представлению происходит очень быстро, так как не требует выполнения основного запроса каждый раз. | |||
* '''Проблема:''' Данные в материализованном представлении могут '''устареть''', если данные в базовых таблицах изменились. | |||
* '''Обновление:''' Данные нужно обновлять '''вручную''' (или по расписанию) с помощью команды <code>REFRESH MATERIALIZED VIEW</code>. | |||
'''Создание и Обновление:''' | |||
<syntaxhighlight lang="sql">-- Создаем материализованное представление | |||
CREATE MATERIALIZED VIEW PICTStudents_MV (PICTId, pSurname) AS -- Добавляем MATERIALIZED | |||
SELECT StudentID, Surname | |||
FROM student | |||
WHERE GroupID IN ( | |||
SELECT GroupID FROM groups WHERE GroupName LIKE 'P3%' | |||
); | |||
-- Используем (быстро, читает сохраненные данные) | |||
SELECT * FROM PICTStudents_MV; | |||
-- Обновляем данные (выполняет основной SELECT и перезаписывает сохраненные данные) | |||
REFRESH MATERIALIZED VIEW PICTStudents_MV;</syntaxhighlight> | |||
''Материализованные представления полезны для сложных, ресурсоемких запросов, результаты которых нужны часто, а актуальность данных с точностью до секунды не критична (например, для отчетов).'' | |||
<span id="часть-4-введение-в-plpgsql"></span> | |||
=== Часть 4: Введение в PL/pgSQL === | |||
SQL — декларативный язык. Иногда нам нужно выполнить последовательность действий, использовать циклы, условия, переменные — то есть написать ''процедурный'' код. Для этого в PostgreSQL (и других СУБД) существуют процедурные расширения языка. | |||
* '''PL/pgSQL (Procedural Language / PostgreSQL SQL)''' — стандартный, блочно-структурированный процедурный язык для PostgreSQL. Его синтаксис во многом основан на языке Ada. | |||
'''Зачем нужен PL/pgSQL?''' | |||
* Создание '''пользовательских функций (UDF)''' и '''хранимых процедур'''. | |||
* Реализация сложной бизнес-логики непосредственно в базе данных. | |||
* Создание '''триггеров''' для автоматизации действий при изменении данных. | |||
* Повышение производительности за счет уменьшения обмена данными между приложением и СУБД (логика выполняется на сервере БД). | |||
'''Типы Пользовательских Функций:''' | |||
В PostgreSQL можно создавать функции на разных языках: | |||
# '''SQL-функции:''' Тело функции состоит из одного или нескольких SQL-запросов. Выполняются быстро, но возможности ограничены самим SQL. | |||
# '''PL/pgSQL-функции:''' Тело функции написано на языке PL/pgSQL, позволяет использовать переменные, циклы, условия и т.д. Самый распространенный вариант для сложной логики. | |||
# '''Функции на других языках (C, Python, Perl, Tcl и др.):''' Требуют установки соответствующих расширений (<code>CREATE EXTENSION plpython3u;</code>). Позволяют использовать возможности и библиотеки этих языков внутри БД. | |||
<span id="часть-5-пользовательские-функции-и-процедуры"></span> | |||
=== Часть 5: Пользовательские Функции и Процедуры === | |||
'''Создание Функции (<code>CREATE FUNCTION</code>)''' | |||
<syntaxhighlight lang="sql">CREATE [ OR REPLACE ] FUNCTION имя_функции ( [ [имя_арг1] тип_арг1, [имя_арг2] тип_арг2, ...] ) | |||
RETURNS тип_возвращаемого_значения -- Или RETURNS TABLE(...) для возврата таблицы, или VOID если ничего не возвращает | |||
AS $$ -- Или AS '...' - тело функции в $$ или одинарных кавычках | |||
-- Тело функции (SQL или PL/pgSQL код) | |||
$$ LANGUAGE язык; -- язык: sql, plpgsql, plpython3u и т.д.</syntaxhighlight> | |||
* <code>OR REPLACE</code>: Заменяет существующую функцию с тем же именем и типами аргументов. | |||
* '''Аргументы:''' | |||
** В старых версиях и в SQL-функциях часто используются позиционные параметры (<code>$1</code>, <code>$2</code>, …). | |||
** В PL/pgSQL и современных SQL-функциях можно (и рекомендуется) использовать именованные аргументы. | |||
* <code>RETURNS</code>: Указывает тип возвращаемого значения. <code>VOID</code> означает, что функция ничего не возвращает (похоже на процедуру, но это все еще функция). | |||
* <code>AS $$ ... $$</code>: Тело функции. Использование <code>$$</code> (долларовое квотирование) предпочтительнее одинарных кавычек (<code>'...'</code>), так как позволяет легко использовать одинарные кавычки внутри тела функции без экранирования (<code>''</code>). | |||
* <code>LANGUAGE</code>: Язык, на котором написано тело функции. | |||
'''Пример SQL-функции (позиционные параметры):''' | |||
<syntaxhighlight lang="sql">-- Функция для изменения группы студента | |||
CREATE FUNCTION updateStudentGroup_v1(int, int) -- Аргументы: stud_id, group_id | |||
RETURNS void -- Ничего не возвращает | |||
AS ' | |||
UPDATE student | |||
SET GroupID = $2 -- $2 - второй аргумент (group_id) | |||
WHERE StudID = $1; -- $1 - первый аргумент (stud_id) | |||
' LANGUAGE sql; | |||
-- Вызов функции | |||
SELECT updateStudentGroup_v1(101, 2); -- Обновить группу для студента 101 на группу 2</syntaxhighlight> | |||
'''Пример SQL-функции (именованные параметры, PostgreSQL 9.2+):''' | |||
<syntaxhighlight lang="sql">CREATE FUNCTION updateStudentGroup_v2( | |||
p_stud_id INT, -- Имя аргумента p_stud_id, тип INT | |||
p_group_id INT -- Имя аргумента p_group_id, тип INT | |||
) | |||
RETURNS void | |||
AS $$ | |||
UPDATE student | |||
SET GroupID = p_group_id -- Используем имя параметра | |||
WHERE StudID = p_stud_id; -- Используем имя параметра | |||
$$ LANGUAGE sql; | |||
-- Вызов функции (можно по именам или по позиции) | |||
SELECT updateStudentGroup_v2(p_stud_id := 102, p_group_id := 1); | |||
SELECT updateStudentGroup_v2(103, 1);</syntaxhighlight> | |||
'''Процедуры (<code>CREATE PROCEDURE</code>, PostgreSQL 11+)''' | |||
Процедуры похожи на функции, возвращающие <code>VOID</code>, но имеют ключевое отличие: внутри процедур '''можно управлять транзакциями''' (<code>COMMIT</code>, <code>ROLLBACK</code>), а внутри функций — нельзя. | |||
<syntaxhighlight lang="sql">CREATE [ OR REPLACE ] PROCEDURE имя_процедуры ( [ [имя_арг1] тип_арг1, ...] ) | |||
AS $$ | |||
-- Тело процедуры (обычно PL/pgSQL) | |||
$$ LANGUAGE язык; | |||
-- Вызов процедуры | |||
CALL имя_процедуры(значение1, ...);</syntaxhighlight> | |||
'''Пример:''' | |||
<syntaxhighlight lang="sql">CREATE PROCEDURE transfer_money(sender_acc INT, receiver_acc INT, amount NUMERIC) | |||
AS $$ | |||
BEGIN | |||
UPDATE accounts SET balance = balance - amount WHERE acc_id = sender_acc; | |||
UPDATE accounts SET balance = balance + amount WHERE acc_id = receiver_acc; | |||
-- Здесь можно было бы добавить COMMIT или ROLLBACK, если бы это не было частью бОльшей транзакции | |||
END; | |||
$$ LANGUAGE plpgsql; | |||
-- Вызов | |||
CALL transfer_money(1, 2, 100.00);</syntaxhighlight> | |||
<span id="часть-6-основы-plpgsql"></span> | |||
=== Часть 6: Основы PL/pgSQL === | |||
PL/pgSQL — '''блочно-структурированный''' язык. Основной элемент — блок кода. | |||
'''Структура блока:''' | |||
<syntaxhighlight lang="sql">[ <<метка_блока>> ] -- Необязательная метка | |||
[ DECLARE | |||
-- Объявление переменных | |||
имя_переменной тип_данных [ := начальное_значение ]; | |||
... | |||
] | |||
BEGIN | |||
-- Исполняемые операторы (SQL-запросы, присваивания, циклы, условия и т.д.) | |||
... | |||
[ RETURN значение; ] -- Только для функций, возвращающих значение | |||
END [ метка_блока ]; -- Метка конца блока (необязательно)</syntaxhighlight> | |||
* Секция <code>DECLARE</code> необязательна, если переменные не нужны. | |||
* <code>BEGIN</code>/<code>END</code> обязательны. | |||
* Блоки могут быть вложенными. | |||
* Метки используются для разрешения имен переменных во вложенных блоках (обращение к переменной внешнего блока: <code>метка_блока.имя_переменной</code>). | |||
* Присваивание значения: <code>переменная := выражение;</code>. | |||
* Выполнение SQL-запросов: Просто пишете SQL-запрос. Чтобы сохранить результат <code>SELECT</code> в переменную, используется <code>SELECT ... INTO переменная ...</code>. | |||
* Вывод отладочной информации: <code>RAISE NOTICE 'Сообщение: %', переменная;</code> (<code>%</code> - место для подстановки значения переменной). | |||
'''Пример блока (из слайда 33):''' | |||
<syntaxhighlight lang="sql">CREATE FUNCTION somefunc() RETURNS integer AS $$ -- SQL command | |||
<< outerblock >> -- Метка внешнего блока (pl/pgSQL) | |||
DECLARE | |||
quantity integer := 30; -- Переменная внешнего блока | |||
BEGIN | |||
-- Создаем вложенный блок | |||
DECLARE | |||
quantity integer := 80; -- Переменная внутреннего блока (то же имя!) | |||
BEGIN | |||
RAISE NOTICE 'Inner quantity = %', quantity; -- Выведет 80 (переменная внутреннего блока) | |||
RAISE NOTICE 'Outer quantity = %', outerblock.quantity; -- Выведет 30 (обращение к переменной внешнего блока по метке) | |||
END; -- Конец вложенного блока | |||
RAISE NOTICE 'Сейчас quantity = %', quantity; -- Выведет 30 (переменная внешнего блока, т.к. вышли из вложенного) | |||
RETURN quantity; -- Вернет 30 | |||
END; | |||
$$ LANGUAGE plpgsql; | |||
-- Вызов функции | |||
SELECT somefunc();</syntaxhighlight> | |||
'''Вывод в <code>NOTICE</code> при вызове:''' | |||
<pre>NOTICE: Inner quantity = 80 | |||
NOTICE: Outer quantity = 30 | |||
NOTICE: Сейчас quantity = 30</pre> | |||
'''Результат <code>SELECT</code>:''' | |||
<pre> somefunc | |||
---------- | |||
30 | |||
(1 row)</pre> | |||
'''Анонимные блоки (<code>DO</code>)''' | |||
Позволяют выполнить блок PL/pgSQL кода без создания функции или процедуры. Удобно для одноразовых задач или скриптов. | |||
<syntaxhighlight lang="sql">DO $$ | |||
[ DECLARE ...] | |||
BEGIN | |||
-- PL/pgSQL код | |||
END; | |||
$$ LANGUAGE plpgsql; -- Язык можно опустить, если используется plpgsql по умолчанию</syntaxhighlight> | |||
'''Пример (из слайда 34):''' Посчитать и вывести количество студентов. | |||
<syntaxhighlight lang="sql">DO $$ | |||
<<studentBlock>> | |||
DECLARE | |||
studCount integer := 0; | |||
BEGIN | |||
SELECT COUNT(*) | |||
INTO studCount -- Сохраняем результат COUNT(*) в переменную studCount | |||
FROM student; | |||
RAISE NOTICE 'Students: %', studCount; -- Выводим результат | |||
END studentBlock $$;</syntaxhighlight> | |||
'''Вывод в <code>NOTICE</code>:''' (Зависит от количества студентов в таблице) | |||
<pre>NOTICE: Students: 15 | |||
DO</pre> | |||
<span id="часть-7-триггеры"></span> | |||
=== Часть 7: Триггеры === | |||
Триггеры — это специальные процедуры, которые '''автоматически''' выполняются (срабатывают) в ответ на определенные события, происходящие с таблицей (обычно это операции DML: <code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>). | |||
'''Зачем нужны триггеры?''' | |||
# '''Реализация сложных ограничений целостности:''' Правила, которые сложно или невозможно выразить стандартными <code>CHECK</code>, <code>FOREIGN KEY</code> (например, проверка баланса перед списанием, сложные зависимости между таблицами). | |||
# '''Аудит изменений:''' Автоматическая запись информации о том, кто, когда и какие данные изменил, в отдельную таблицу логов. | |||
# '''Автоматическое обновление связанных данных:''' Например, пересчет итоговых сумм при изменении деталей заказа. | |||
# '''Репликация данных''' (в некоторых случаях). | |||
'''Как работает триггер?''' | |||
# '''Событие:''' Происходит операция DML (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>) или DDL (для событийных триггеров) с таблицей, для которой определен триггер. | |||
# '''Срабатывание:''' СУБД проверяет, есть ли триггеры, связанные с этим событием и таблицей. | |||
# '''Выполнение:''' Если триггер найден, выполняется связанная с ним '''триггерная функция'''. | |||
'''Создание триггера в PostgreSQL:''' | |||
Процесс состоит из двух шагов: | |||
<ol style="list-style-type: decimal;"> | |||
<li><p>'''Создание триггерной функции:''' Это обычная функция PL/pgSQL (или на другом языке), но со специфическими особенностями:</p> | |||
<ul> | |||
<li>Она '''не принимает аргументов'''.</li> | |||
<li>Она должна возвращать специальный тип '''<code>TRIGGER</code>''' (для DML триггеров) или <code>EVENT_TRIGGER</code> (для DDL триггеров).</li> | |||
<li>Внутри функции доступны специальные переменные (<code>NEW</code>, <code>OLD</code>, <code>TG_OP</code>, <code>TG_WHEN</code> и т.д.), содержащие информацию о событии и изменяемых данных.</li> | |||
<li>Возвращаемое значение функции имеет значение (особенно для <code>BEFORE ROW</code> триггеров): | |||
<ul> | |||
<li>Возврат <code>NEW</code> (или измененной строки <code>NEW</code>): Операция продолжается с этой (возможно измененной) строкой.</li> | |||
<li>Возврат <code>OLD</code> (для <code>UPDATE</code>/<code>DELETE</code>): Операция продолжается со старой строкой (редко используется).</li> | |||
<li>Возврат <code>NULL</code>: '''Операция для данной строки отменяется''', последующие триггеры для этой строки не срабатывают. Позволяет “запретить” изменение.</li> | |||
<li>Для <code>AFTER</code> триггеров возвращаемое значение игнорируется (операция уже произошла), но рекомендуется возвращать <code>NULL</code> или ту же запись (<code>NEW</code> или <code>OLD</code>).</li></ul> | |||
</li></ul> | |||
</li> | |||
<li><p>'''Создание самого триггера:''' Связывает триггерную функцию с конкретной таблицей и событием.</p> | |||
<p>'''Синтаксис <code>CREATE TRIGGER</code>:'''</p> | |||
<syntaxhighlight lang="sql">CREATE TRIGGER имя_триггера | |||
{ BEFORE | AFTER | INSTEAD OF } -- Когда срабатывать | |||
{ event [ OR ... ] } -- На какое событие(я) (INSERT, UPDATE, DELETE, TRUNCATE) | |||
ON имя_таблицы | |||
[ FOR [ EACH ] { ROW | STATEMENT } ] -- Уровень срабатывания | |||
[ WHEN ( условие ) ] -- Дополнительное условие срабатывания | |||
EXECUTE PROCEDURE имя_триггерной_функции(); -- Какую функцию вызвать</syntaxhighlight> | |||
<ul> | |||
<li>'''<code>BEFORE | AFTER | INSTEAD OF</code>''': | |||
<ul> | |||
<li><code>BEFORE</code>: Функция выполняется ''перед'' выполнением операции DML и перед проверкой ограничений. Позволяет изменить данные (<code>NEW</code>) или отменить операцию (вернув <code>NULL</code>).</li> | |||
<li><code>AFTER</code>: Функция выполняется ''после'' выполнения операции DML и проверки ограничений. Не может изменить данные (операция уже прошла) или отменить ее. Используется для аудита, обновления связанных данных.</li> | |||
<li><code>INSTEAD OF</code>: Специальный тип для '''представлений (Views)'''. Функция выполняется ''вместо'' операции DML над представлением, позволяя реализовать логику обновления базовых таблиц.</li></ul> | |||
</li> | |||
<li>'''<code>event</code>''': <code>INSERT</code>, <code>UPDATE [ OF column1, ... ]</code>, <code>DELETE</code>, <code>TRUNCATE</code>. Можно указать несколько через <code>OR</code>. <code>UPDATE OF</code> срабатывает только при изменении указанных колонок.</li> | |||
<li>'''<code>FOR EACH ROW | STATEMENT</code>''': | |||
<ul> | |||
<li><code>ROW</code>: Функция выполняется '''для каждой строки''', затронутой операцией DML. Внутри доступны переменные <code>NEW</code> (для <code>INSERT</code>/<code>UPDATE</code>) и <code>OLD</code> (для <code>UPDATE</code>/<code>DELETE</code>).</li> | |||
<li><code>STATEMENT</code> (По умолчанию): Функция выполняется '''один раз на всю операцию DML''', независимо от количества затронутых строк. Переменные <code>NEW</code> и <code>OLD</code> недоступны.</li></ul> | |||
</li> | |||
<li>'''<code>WHEN (условие)</code>''': Дополнительное условие (использующее значения <code>NEW</code>/<code>OLD</code>), которое проверяется перед вызовом функции (только для <code>ROW</code> триггеров). Если условие ложно, функция не вызывается.</li></ul> | |||
</li></ol> | |||
'''Пример: Аудит таблицы <code>EMPLOYEE</code> (из слайдов)''' | |||
'''Задача:''' При добавлении нового сотрудника в таблицу <code>EMPLOYEE</code> автоматически записывать ID сотрудника и время добавления в таблицу <code>AUDIT</code>. | |||
'''1. Таблицы:''' | |||
<syntaxhighlight lang="sql">CREATE TABLE employee ( | |||
id INT PRIMARY KEY, | |||
name TEXT NOT NULL, | |||
addr CHAR(50), | |||
salary REAL | |||
); | |||
CREATE TABLE audit ( | |||
emp_id INT NOT NULL, | |||
entry_date TEXT NOT NULL -- Лучше использовать TIMESTAMPTZ | |||
);</syntaxhighlight> | |||
'''2. Триггерная функция:''' | |||
<syntaxhighlight lang="sql">CREATE OR REPLACE FUNCTION auditfunc() | |||
RETURNS TRIGGER AS $$ | |||
BEGIN | |||
-- Вставляем запись в таблицу аудита | |||
-- NEW.id - это ID из строки, которая сейчас вставляется в employee | |||
-- current_timestamp - встроенная функция PostgreSQL, возвращает текущее время | |||
INSERT INTO audit(emp_id, entry_date) VALUES (NEW.id, current_timestamp); | |||
-- Возвращаем NEW, чтобы операция INSERT продолжилась успешно | |||
-- Для AFTER триггера возвращаемое значение игнорируется, но хорошей практикой | |||
-- является возврат соответствующей строки или NULL. | |||
RETURN NEW; | |||
END; | |||
$$ LANGUAGE plpgsql;</syntaxhighlight> | |||
'''3. Создание триггера:''' | |||
<syntaxhighlight lang="sql">CREATE TRIGGER employee_audit_insert | |||
AFTER INSERT ON employee -- Срабатывать ПОСЛЕ вставки | |||
FOR EACH ROW -- Для каждой вставляемой строки | |||
EXECUTE PROCEDURE auditfunc(); -- Вызвать нашу функцию</syntaxhighlight> | |||
'''Проверка:''' | |||
<syntaxhighlight lang="sql">INSERT INTO employee (id, name, salary) VALUES (1, 'Иван', 1000); | |||
-- После этой команды в таблице audit появится запись: (1, <текущее время>) | |||
INSERT INTO employee (id, name, salary) VALUES (2, 'Петр', 1500); | |||
-- После этой команды в таблице audit появится вторая запись: (2, <текущее время>)</syntaxhighlight> | |||
'''Событийные триггеры (Event Triggers):''' | |||
* Срабатывают на DDL-команды (<code>CREATE TABLE</code>, <code>ALTER TABLE</code>, <code>DROP TABLE</code> и т.д.). | |||
* Используются для аудита изменений схемы, запрета определенных DDL-операций и т.п. | |||
* Триггерная функция должна возвращать <code>EVENT_TRIGGER</code>. | |||
* Доступны специальные переменные (например, <code>tg_event</code>, <code>tg_tag</code>). | |||
'''Пример (из слайдов):''' Логгирование DDL команд. | |||
<syntaxhighlight lang="sql">-- Функция | |||
CREATE OR REPLACE FUNCTION eventtest() | |||
RETURNS event_trigger AS $$ | |||
BEGIN | |||
RAISE NOTICE 'DDL Event: %, Command Tag: %', tg_event, tg_tag; -- tg_tag содержит текст команды | |||
END; | |||
$$ LANGUAGE plpgsql; | |||
-- Триггер | |||
CREATE EVENT TRIGGER eventtest_trigger | |||
ON ddl_command_start -- Срабатывать перед началом любой DDL команды | |||
EXECUTE PROCEDURE eventtest(); | |||
-- Проверка | |||
CREATE TABLE some_table (id int);</syntaxhighlight> | |||
'''Вывод в <code>NOTICE</code>:''' | |||
<pre>NOTICE: DDL Event: ddl_command_start, Command Tag: CREATE TABLE | |||
CREATE TABLE</pre> | |||
'''Удаление триггера:''' | |||
<syntaxhighlight lang="sql">DROP TRIGGER имя_триггера ON имя_таблицы;</syntaxhighlight> | |||
<span id="часть-8-транзакции"></span> | |||
=== Часть 8: Транзакции === | |||
Транзакция — это '''логическая единица работы''', состоящая из одной или нескольких операций SQL, которая должна быть выполнена '''атомарно'''. | |||
'''Свойства ACID:''' | |||
Транзакции в реляционных СУБД обычно гарантируют свойства ACID: | |||
# '''Atomicity (Атомарность):''' Либо ''все'' операции внутри транзакции успешно выполняются и фиксируются, либо ''ни одна'' из них не оказывает влияния на базу данных (все изменения отменяются). “Все или ничего”. | |||
# '''Consistency (Согласованность):''' Транзакция переводит базу данных из одного ''согласованного'' (целостного) состояния в другое ''согласованное'' состояние. Во время выполнения транзакции целостность может временно нарушаться, но к моменту фиксации она должна быть восстановлена. | |||
# '''Isolation (Изолированность):''' Параллельно выполняющиеся транзакции не должны мешать друг другу. Каждая транзакция должна выполняться так, как будто других транзакций в системе нет. (На практике существуют разные ''уровни изоляции'', которые допускают те или иные аномалии для повышения производительности). | |||
# '''Durability (Долговечность/Устойчивость):''' Если транзакция успешно завершена (зафиксирована), ее результаты должны быть сохранены постоянно и не должны быть потеряны даже в случае сбоев системы (например, отключения питания). Обычно достигается за счет записи изменений в '''журналы транзакций (WAL - Write-Ahead Log)''' перед применением их к основным файлам данных. | |||
'''Управление транзакциями в SQL:''' | |||
* '''<code>BEGIN</code>''' или '''<code>START TRANSACTION</code>''': Начинает новую транзакцию. В PostgreSQL многие команды DDL (как <code>CREATE TABLE</code>) не могут выполняться внутри явного блока <code>BEGIN...COMMIT</code>, они сами по себе являются транзакциями. DML команды (<code>INSERT</code>, <code>UPDATE</code>, <code>DELETE</code>) могут быть сгруппированы. Если <code>BEGIN</code> не вызван явно, каждая отдельная DML команда часто выполняется в своей собственной неявной транзакции (режим autocommit, зависит от настроек клиента/СУБД). | |||
* '''<code>COMMIT</code>''': Успешно завершает текущую транзакцию, делая все ее изменения видимыми для других транзакций и постоянными. | |||
* '''<code>ROLLBACK</code>''': Отменяет все изменения, сделанные в текущей транзакции с момента ее начала (или с последней точки сохранения), и завершает транзакцию. | |||
'''Пример (Банковский перевод):''' | |||
<syntaxhighlight lang="sql">BEGIN; -- Начать транзакцию | |||
-- Снять деньги со счета Алекса | |||
UPDATE account SET balance = balance - 50.00 WHERE name = 'Alex'; | |||
-- Добавить деньги на счет Ивана | |||
UPDATE account SET balance = balance + 50.00 WHERE name = 'Ivan'; | |||
-- Если обе операции прошли успешно: | |||
COMMIT; -- Зафиксировать изменения | |||
-- Если на каком-то этапе произошла ошибка (например, недостаточно средств), | |||
-- нужно выполнить ROLLBACK вместо COMMIT: | |||
-- ROLLBACK; -- Отменить все изменения с момента BEGIN</syntaxhighlight> | |||
'''Точки сохранения (<code>SAVEPOINT</code>):''' | |||
Позволяют установить “закладку” внутри транзакции, к которой можно будет откатиться, не отменяя всю транзакцию. | |||
* '''<code>SAVEPOINT имя_точки;</code>''': Устанавливает точку сохранения. | |||
* '''<code>ROLLBACK TO SAVEPOINT имя_точки;</code>''': Отменяет все изменения, сделанные ''после'' указанной точки сохранения. Сама точка сохранения остается активной. | |||
* '''<code>RELEASE SAVEPOINT имя_точки;</code>''': Удаляет точку сохранения, но ''не'' отменяет изменения, сделанные после нее. | |||
'''Пример с <code>SAVEPOINT</code>:''' | |||
<syntaxhighlight lang="sql">BEGIN; | |||
UPDATE account SET balance = balance - 50.00 WHERE name = 'Alex'; | |||
SAVEPOINT savepoint1; -- Установить точку сохранения | |||
UPDATE account SET balance = balance + 50.00 WHERE name = 'Ivan'; | |||
-- Предположим, здесь возникла проблема или нужно отменить перевод Ивану | |||
ROLLBACK TO SAVEPOINT savepoint1; -- Откат к состоянию после списания у Алекса | |||
-- Теперь можно попробовать перевести деньги кому-то другому | |||
UPDATE account SET balance = balance + 50.00 WHERE name = 'Ivan2'; | |||
COMMIT; -- Фиксируем результат (списание у Алекса и зачисление Ivan2)</syntaxhighlight> | |||
''При откате к точке сохранения (<code>ROLLBACK TO</code>), все точки, созданные ''после* нее, автоматически удаляются.* | |||
[[Category:БД]] | |||
[[Category:БД:Теория]] |
Текущая версия от 10:40, 14 апреля 2025
В этой главе мы углубимся в команды для изменения данных (DML), рассмотрим мощный инструмент SQL — представления (Views), научимся создавать собственные функции и процедуры с использованием процедурного расширения PL/pgSQL, разберемся с триггерами для реализации сложной логики и автоматизации, а также подробно рассмотрим механизм транзакций для обеспечения целостности данных. Эта глава основана на материалах Лекции 5: “PL/pgSQL” и частично затрагивает DML из Лекции 4/5.
Часть 1: Манипулирование Данными (DML)
DML (Data Manipulation Language) — это часть SQL, отвечающая за добавление, изменение и удаление данных внутри таблиц.
INSERT
: Добавляет одну или несколько новых строк в таблицу.Синтаксис 1 (Указание столбцов):
INSERT INTO имя_таблицы (столбец1, столбец2, ...) VALUES (значение1, значение2, ...);
Порядок значений должен соответствовать порядку указанных столбцов.
Синтаксис 2 (Без указания столбцов):
INSERT INTO имя_таблицы VALUES (значение1, значение2, ...);
Значения должны быть перечислены в том же порядке, в каком столбцы определены в таблице. Менее надежный способ, т.к. зависит от порядка столбцов.
Синтаксис 3 (Вставка из
SELECT
):INSERT INTO имя_таблицы (столбец1, столбец2, ...) SELECT выражение1, выражение2, ... FROM другая_таблица WHERE условие;
Позволяет вставить в таблицу результат другого запроса. Количество и типы столбцов в
INSERT
иSELECT
должны совпадать.
Пример (из слайдов):
-- Пример 1 и 2: Добавление студента INSERT INTO student VALUES (123, 'Vasya', 345); INSERT INTO student (StudID, GroupID, Name) VALUES (123, 345, 'Vasya'); -- Пример 3: Добавление студентов из временной таблицы по условию INSERT INTO student (StudID, Name, GroupID) -- Указываем порядок для INSERT SELECT ID, Name, 345 -- Указываем порядок в SELECT FROM tmp_person WHERE Exam > 70;
UPDATE
: Изменяет существующие строки в таблице.Синтаксис:
UPDATE имя_таблицы SET столбец1 = новое_значение1, столбец2 = новое_значение2, ... [ WHERE условие ]; -- Если WHERE отсутствует, обновятся ВСЕ строки!
Условие
WHERE
определяет, какие строки нужно обновить.
Пример (из слайдов):
-- Установить всем студентам GroupID = 578 UPDATE student SET GroupID = 578; -- Установить GroupID = 34 для студента с именем 'Ivan' UPDATE student SET GroupID = 34 WHERE Name = 'Ivan';
DELETE
: Удаляет существующие строки из таблицы.Синтаксис:
DELETE FROM имя_таблицы [ WHERE условие ]; -- Если WHERE отсутствует, удалятся ВСЕ строки!
Условие
WHERE
определяет, какие строки нужно удалить.
Пример (из слайдов):
-- Удалить студентов с GroupID = 34 DELETE FROM student WHERE GroupID = 34; -- Удалить ВСЕХ студентов DELETE FROM student;
TRUNCATE
: Быстро удаляет все строки из таблицы (или нескольких таблиц).Синтаксис:
TRUNCATE [TABLE] имя_таблицы1 [, имя_таблицы2 ...];
Отличия от
DELETE
безWHERE
:- Обычно намного быстрее для больших таблиц, так как не сканирует таблицу построчно и не записывает удаление каждой строки в лог транзакций так же подробно.
- Не запускает триггеры
DELETE
на уровне строк (FOR EACH ROW
). - Сбрасывает счетчики последовательностей (
SERIAL
и т.п.), связанных с таблицей (в PostgreSQL, если указаноRESTART IDENTITY
). - Не возвращает количество удаленных строк.
Используйте
TRUNCATE
, когда нужно быстро и полностью очистить таблицу.
Пример (из слайдов):
-- Быстро очистить таблицу student TRUNCATE TABLE student; -- Быстро очистить несколько таблиц TRUNCATE TABLE student, groups, exams;
Часть 2: Выполнение Скриптов
Часто SQL-код (особенно DDL для создания схемы или DML для наполнения данными) хранится в файлах (.sql
). Такие файлы называются скриптами. Выполнить скрипт в PostgreSQL можно несколькими способами:
Через утилиту
psql
при запуске:psql -U имя_пользователя -d имя_базы -f путь/к/файлу/script.sql
-U
: Имя пользователя PostgreSQL.-d
: Имя базы данных, к которой подключаемся.-f
: Путь к файлу скрипта.
Внутри интерактивной сессии
psql
: Используются мета-команды (начинаются с\
):-- Подключаемся к базе psql -U имя_пользователя -d имя_базы -- Выполняем скрипт из файла \i путь/к/файлу/script.sql -- или эквивалентная команда \include путь/к/файлу/script.sql
Это удобно для выполнения небольших скриптов или частей кода во время работы с БД.
Часть 3: Представления (Views)
Мы уже кратко упоминали виртуальные таблицы. Основной их вид — это представления.
- Представление (View) — это сохраненный в базе данных именованный SQL-запрос (
SELECT
). - С представлением можно работать почти так же, как с обычной таблицей (выполнять
SELECT
, иногдаINSERT
,UPDATE
,DELETE
, если представление обновляемое). - Представление не хранит данные само по себе (за исключением материализованных представлений). Каждый раз при обращении к представлению СУБД выполняет лежащий в его основе
SELECT
-запрос к базовым таблицам.
Зачем нужны представления?
- Упрощение сложных запросов: Длинный и сложный запрос можно “спрятать” за простым именем представления.
- Сокрытие структуры данных: Можно предоставить пользователям доступ только к представлению, которое показывает лишь необходимые им столбцы и строки, скрывая сложность или конфиденциальность базовых таблиц.
- Логическая независимость данных: Если структура базовых таблиц меняется, можно изменить только определение представления, а запросы пользователей, обращающиеся к представлению, останутся прежними.
- Обеспечение обратной совместимости: При рефакторинге схемы можно создать представления, имитирующие старую структуру.
Создание Представления (CREATE VIEW
)
CREATE [ OR REPLACE ] VIEW имя_представления [ (имя_колонки1, имя_колонки2, ...) ]
AS
SELECT_запрос; -- Запрос, определяющий представление
OR REPLACE
: Если представление с таким именем уже существует, оно будет заменено.[ (имя_колонки1, ...) ]
: Необязательный список имен для столбцов представления. Если опущен, используются имена столбцов изSELECT_запрос
.
Пример (из слайдов): Создать представление, показывающее студентов факультета ПИКТ (предполагаем, что это группы, начинающиеся с ‘P3’).
-- Создаем представление
CREATE VIEW PICTStudents AS
SELECT * -- Выбираем все колонки из student
FROM student
WHERE GroupID IN ( -- Выбираем студентов, чья группа в списке групп ПИКТ
SELECT GroupID
FROM groups
WHERE GroupName LIKE 'P3%' -- Находим группы, чье имя начинается с 'P3'
);
-- Используем представление как обычную таблицу
SELECT st_name, GroupID FROM PICTStudents WHERE st_id > 100;
Пример (с переименованием колонок):
CREATE VIEW PICTStudents2 (PICTId, pSurname) AS -- Задаем свои имена колонок
SELECT StudentID, Surname -- Выбираем нужные колонки из student
FROM student
WHERE GroupID IN (
SELECT GroupID FROM groups WHERE GroupName LIKE 'P3%'
);
-- Используем представление с новыми именами
SELECT PICTId FROM PICTStudents2 WHERE pSurname LIKE 'Иван%';
Материализованные Представления (MATERIALIZED VIEW
)
- В отличие от обычных представлений, материализованное представление хранит результат своего запроса физически в базе данных (как кэш).
- Обращение к материализованному представлению происходит очень быстро, так как не требует выполнения основного запроса каждый раз.
- Проблема: Данные в материализованном представлении могут устареть, если данные в базовых таблицах изменились.
- Обновление: Данные нужно обновлять вручную (или по расписанию) с помощью команды
REFRESH MATERIALIZED VIEW
.
Создание и Обновление:
-- Создаем материализованное представление
CREATE MATERIALIZED VIEW PICTStudents_MV (PICTId, pSurname) AS -- Добавляем MATERIALIZED
SELECT StudentID, Surname
FROM student
WHERE GroupID IN (
SELECT GroupID FROM groups WHERE GroupName LIKE 'P3%'
);
-- Используем (быстро, читает сохраненные данные)
SELECT * FROM PICTStudents_MV;
-- Обновляем данные (выполняет основной SELECT и перезаписывает сохраненные данные)
REFRESH MATERIALIZED VIEW PICTStudents_MV;
Материализованные представления полезны для сложных, ресурсоемких запросов, результаты которых нужны часто, а актуальность данных с точностью до секунды не критична (например, для отчетов).
Часть 4: Введение в PL/pgSQL
SQL — декларативный язык. Иногда нам нужно выполнить последовательность действий, использовать циклы, условия, переменные — то есть написать процедурный код. Для этого в PostgreSQL (и других СУБД) существуют процедурные расширения языка.
- PL/pgSQL (Procedural Language / PostgreSQL SQL) — стандартный, блочно-структурированный процедурный язык для PostgreSQL. Его синтаксис во многом основан на языке Ada.
Зачем нужен PL/pgSQL?
- Создание пользовательских функций (UDF) и хранимых процедур.
- Реализация сложной бизнес-логики непосредственно в базе данных.
- Создание триггеров для автоматизации действий при изменении данных.
- Повышение производительности за счет уменьшения обмена данными между приложением и СУБД (логика выполняется на сервере БД).
Типы Пользовательских Функций:
В PostgreSQL можно создавать функции на разных языках:
- SQL-функции: Тело функции состоит из одного или нескольких SQL-запросов. Выполняются быстро, но возможности ограничены самим SQL.
- PL/pgSQL-функции: Тело функции написано на языке PL/pgSQL, позволяет использовать переменные, циклы, условия и т.д. Самый распространенный вариант для сложной логики.
- Функции на других языках (C, Python, Perl, Tcl и др.): Требуют установки соответствующих расширений (
CREATE EXTENSION plpython3u;
). Позволяют использовать возможности и библиотеки этих языков внутри БД.
Часть 5: Пользовательские Функции и Процедуры
Создание Функции (CREATE FUNCTION
)
CREATE [ OR REPLACE ] FUNCTION имя_функции ( [ [имя_арг1] тип_арг1, [имя_арг2] тип_арг2, ...] )
RETURNS тип_возвращаемого_значения -- Или RETURNS TABLE(...) для возврата таблицы, или VOID если ничего не возвращает
AS $$ -- Или AS '...' - тело функции в $$ или одинарных кавычках
-- Тело функции (SQL или PL/pgSQL код)
$$ LANGUAGE язык; -- язык: sql, plpgsql, plpython3u и т.д.
OR REPLACE
: Заменяет существующую функцию с тем же именем и типами аргументов.- Аргументы:
- В старых версиях и в SQL-функциях часто используются позиционные параметры (
$1
,$2
, …). - В PL/pgSQL и современных SQL-функциях можно (и рекомендуется) использовать именованные аргументы.
- В старых версиях и в SQL-функциях часто используются позиционные параметры (
RETURNS
: Указывает тип возвращаемого значения.VOID
означает, что функция ничего не возвращает (похоже на процедуру, но это все еще функция).AS $$ ... $$
: Тело функции. Использование$$
(долларовое квотирование) предпочтительнее одинарных кавычек ('...'
), так как позволяет легко использовать одинарные кавычки внутри тела функции без экранирования ().
LANGUAGE
: Язык, на котором написано тело функции.
Пример SQL-функции (позиционные параметры):
-- Функция для изменения группы студента
CREATE FUNCTION updateStudentGroup_v1(int, int) -- Аргументы: stud_id, group_id
RETURNS void -- Ничего не возвращает
AS '
UPDATE student
SET GroupID = $2 -- $2 - второй аргумент (group_id)
WHERE StudID = $1; -- $1 - первый аргумент (stud_id)
' LANGUAGE sql;
-- Вызов функции
SELECT updateStudentGroup_v1(101, 2); -- Обновить группу для студента 101 на группу 2
Пример SQL-функции (именованные параметры, PostgreSQL 9.2+):
CREATE FUNCTION updateStudentGroup_v2(
p_stud_id INT, -- Имя аргумента p_stud_id, тип INT
p_group_id INT -- Имя аргумента p_group_id, тип INT
)
RETURNS void
AS $$
UPDATE student
SET GroupID = p_group_id -- Используем имя параметра
WHERE StudID = p_stud_id; -- Используем имя параметра
$$ LANGUAGE sql;
-- Вызов функции (можно по именам или по позиции)
SELECT updateStudentGroup_v2(p_stud_id := 102, p_group_id := 1);
SELECT updateStudentGroup_v2(103, 1);
Процедуры (CREATE PROCEDURE
, PostgreSQL 11+)
Процедуры похожи на функции, возвращающие VOID
, но имеют ключевое отличие: внутри процедур можно управлять транзакциями (COMMIT
, ROLLBACK
), а внутри функций — нельзя.
CREATE [ OR REPLACE ] PROCEDURE имя_процедуры ( [ [имя_арг1] тип_арг1, ...] )
AS $$
-- Тело процедуры (обычно PL/pgSQL)
$$ LANGUAGE язык;
-- Вызов процедуры
CALL имя_процедуры(значение1, ...);
Пример:
CREATE PROCEDURE transfer_money(sender_acc INT, receiver_acc INT, amount NUMERIC)
AS $$
BEGIN
UPDATE accounts SET balance = balance - amount WHERE acc_id = sender_acc;
UPDATE accounts SET balance = balance + amount WHERE acc_id = receiver_acc;
-- Здесь можно было бы добавить COMMIT или ROLLBACK, если бы это не было частью бОльшей транзакции
END;
$$ LANGUAGE plpgsql;
-- Вызов
CALL transfer_money(1, 2, 100.00);
Часть 6: Основы PL/pgSQL
PL/pgSQL — блочно-структурированный язык. Основной элемент — блок кода.
Структура блока:
[ <<метка_блока>> ] -- Необязательная метка
[ DECLARE
-- Объявление переменных
имя_переменной тип_данных [ := начальное_значение ];
...
]
BEGIN
-- Исполняемые операторы (SQL-запросы, присваивания, циклы, условия и т.д.)
...
[ RETURN значение; ] -- Только для функций, возвращающих значение
END [ метка_блока ]; -- Метка конца блока (необязательно)
- Секция
DECLARE
необязательна, если переменные не нужны. BEGIN
/END
обязательны.- Блоки могут быть вложенными.
- Метки используются для разрешения имен переменных во вложенных блоках (обращение к переменной внешнего блока:
метка_блока.имя_переменной
). - Присваивание значения:
переменная := выражение;
. - Выполнение SQL-запросов: Просто пишете SQL-запрос. Чтобы сохранить результат
SELECT
в переменную, используетсяSELECT ... INTO переменная ...
. - Вывод отладочной информации:
RAISE NOTICE 'Сообщение: %', переменная;
(%
- место для подстановки значения переменной).
Пример блока (из слайда 33):
CREATE FUNCTION somefunc() RETURNS integer AS $$ -- SQL command
<< outerblock >> -- Метка внешнего блока (pl/pgSQL)
DECLARE
quantity integer := 30; -- Переменная внешнего блока
BEGIN
-- Создаем вложенный блок
DECLARE
quantity integer := 80; -- Переменная внутреннего блока (то же имя!)
BEGIN
RAISE NOTICE 'Inner quantity = %', quantity; -- Выведет 80 (переменная внутреннего блока)
RAISE NOTICE 'Outer quantity = %', outerblock.quantity; -- Выведет 30 (обращение к переменной внешнего блока по метке)
END; -- Конец вложенного блока
RAISE NOTICE 'Сейчас quantity = %', quantity; -- Выведет 30 (переменная внешнего блока, т.к. вышли из вложенного)
RETURN quantity; -- Вернет 30
END;
$$ LANGUAGE plpgsql;
-- Вызов функции
SELECT somefunc();
Вывод в NOTICE
при вызове:
NOTICE: Inner quantity = 80 NOTICE: Outer quantity = 30 NOTICE: Сейчас quantity = 30
Результат SELECT
:
somefunc ---------- 30 (1 row)
Анонимные блоки (DO
)
Позволяют выполнить блок PL/pgSQL кода без создания функции или процедуры. Удобно для одноразовых задач или скриптов.
DO $$
[ DECLARE ...]
BEGIN
-- PL/pgSQL код
END;
$$ LANGUAGE plpgsql; -- Язык можно опустить, если используется plpgsql по умолчанию
Пример (из слайда 34): Посчитать и вывести количество студентов.
DO $$
<<studentBlock>>
DECLARE
studCount integer := 0;
BEGIN
SELECT COUNT(*)
INTO studCount -- Сохраняем результат COUNT(*) в переменную studCount
FROM student;
RAISE NOTICE 'Students: %', studCount; -- Выводим результат
END studentBlock $$;
Вывод в NOTICE
: (Зависит от количества студентов в таблице)
NOTICE: Students: 15 DO
Часть 7: Триггеры
Триггеры — это специальные процедуры, которые автоматически выполняются (срабатывают) в ответ на определенные события, происходящие с таблицей (обычно это операции DML: INSERT
, UPDATE
, DELETE
).
Зачем нужны триггеры?
- Реализация сложных ограничений целостности: Правила, которые сложно или невозможно выразить стандартными
CHECK
,FOREIGN KEY
(например, проверка баланса перед списанием, сложные зависимости между таблицами). - Аудит изменений: Автоматическая запись информации о том, кто, когда и какие данные изменил, в отдельную таблицу логов.
- Автоматическое обновление связанных данных: Например, пересчет итоговых сумм при изменении деталей заказа.
- Репликация данных (в некоторых случаях).
Как работает триггер?
- Событие: Происходит операция DML (
INSERT
,UPDATE
,DELETE
) или DDL (для событийных триггеров) с таблицей, для которой определен триггер. - Срабатывание: СУБД проверяет, есть ли триггеры, связанные с этим событием и таблицей.
- Выполнение: Если триггер найден, выполняется связанная с ним триггерная функция.
Создание триггера в PostgreSQL:
Процесс состоит из двух шагов:
Создание триггерной функции: Это обычная функция PL/pgSQL (или на другом языке), но со специфическими особенностями:
- Она не принимает аргументов.
- Она должна возвращать специальный тип
TRIGGER
(для DML триггеров) илиEVENT_TRIGGER
(для DDL триггеров). - Внутри функции доступны специальные переменные (
NEW
,OLD
,TG_OP
,TG_WHEN
и т.д.), содержащие информацию о событии и изменяемых данных. - Возвращаемое значение функции имеет значение (особенно для
BEFORE ROW
триггеров):- Возврат
NEW
(или измененной строкиNEW
): Операция продолжается с этой (возможно измененной) строкой. - Возврат
OLD
(дляUPDATE
/DELETE
): Операция продолжается со старой строкой (редко используется). - Возврат
NULL
: Операция для данной строки отменяется, последующие триггеры для этой строки не срабатывают. Позволяет “запретить” изменение. - Для
AFTER
триггеров возвращаемое значение игнорируется (операция уже произошла), но рекомендуется возвращатьNULL
или ту же запись (NEW
илиOLD
).
- Возврат
Создание самого триггера: Связывает триггерную функцию с конкретной таблицей и событием.
Синтаксис
CREATE TRIGGER
:CREATE TRIGGER имя_триггера { BEFORE | AFTER | INSTEAD OF } -- Когда срабатывать { event [ OR ... ] } -- На какое событие(я) (INSERT, UPDATE, DELETE, TRUNCATE) ON имя_таблицы [ FOR [ EACH ] { ROW | STATEMENT } ] -- Уровень срабатывания [ WHEN ( условие ) ] -- Дополнительное условие срабатывания EXECUTE PROCEDURE имя_триггерной_функции(); -- Какую функцию вызвать
BEFORE | AFTER | INSTEAD OF
:BEFORE
: Функция выполняется перед выполнением операции DML и перед проверкой ограничений. Позволяет изменить данные (NEW
) или отменить операцию (вернувNULL
).AFTER
: Функция выполняется после выполнения операции DML и проверки ограничений. Не может изменить данные (операция уже прошла) или отменить ее. Используется для аудита, обновления связанных данных.INSTEAD OF
: Специальный тип для представлений (Views). Функция выполняется вместо операции DML над представлением, позволяя реализовать логику обновления базовых таблиц.
event
:INSERT
,UPDATE [ OF column1, ... ]
,DELETE
,TRUNCATE
. Можно указать несколько черезOR
.UPDATE OF
срабатывает только при изменении указанных колонок.FOR EACH ROW | STATEMENT
:ROW
: Функция выполняется для каждой строки, затронутой операцией DML. Внутри доступны переменныеNEW
(дляINSERT
/UPDATE
) иOLD
(дляUPDATE
/DELETE
).STATEMENT
(По умолчанию): Функция выполняется один раз на всю операцию DML, независимо от количества затронутых строк. ПеременныеNEW
иOLD
недоступны.
WHEN (условие)
: Дополнительное условие (использующее значенияNEW
/OLD
), которое проверяется перед вызовом функции (только дляROW
триггеров). Если условие ложно, функция не вызывается.
Пример: Аудит таблицы EMPLOYEE
(из слайдов)
Задача: При добавлении нового сотрудника в таблицу EMPLOYEE
автоматически записывать ID сотрудника и время добавления в таблицу AUDIT
.
1. Таблицы:
CREATE TABLE employee (
id INT PRIMARY KEY,
name TEXT NOT NULL,
addr CHAR(50),
salary REAL
);
CREATE TABLE audit (
emp_id INT NOT NULL,
entry_date TEXT NOT NULL -- Лучше использовать TIMESTAMPTZ
);
2. Триггерная функция:
CREATE OR REPLACE FUNCTION auditfunc()
RETURNS TRIGGER AS $$
BEGIN
-- Вставляем запись в таблицу аудита
-- NEW.id - это ID из строки, которая сейчас вставляется в employee
-- current_timestamp - встроенная функция PostgreSQL, возвращает текущее время
INSERT INTO audit(emp_id, entry_date) VALUES (NEW.id, current_timestamp);
-- Возвращаем NEW, чтобы операция INSERT продолжилась успешно
-- Для AFTER триггера возвращаемое значение игнорируется, но хорошей практикой
-- является возврат соответствующей строки или NULL.
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
3. Создание триггера:
CREATE TRIGGER employee_audit_insert
AFTER INSERT ON employee -- Срабатывать ПОСЛЕ вставки
FOR EACH ROW -- Для каждой вставляемой строки
EXECUTE PROCEDURE auditfunc(); -- Вызвать нашу функцию
Проверка:
INSERT INTO employee (id, name, salary) VALUES (1, 'Иван', 1000);
-- После этой команды в таблице audit появится запись: (1, <текущее время>)
INSERT INTO employee (id, name, salary) VALUES (2, 'Петр', 1500);
-- После этой команды в таблице audit появится вторая запись: (2, <текущее время>)
Событийные триггеры (Event Triggers):
- Срабатывают на DDL-команды (
CREATE TABLE
,ALTER TABLE
,DROP TABLE
и т.д.). - Используются для аудита изменений схемы, запрета определенных DDL-операций и т.п.
- Триггерная функция должна возвращать
EVENT_TRIGGER
. - Доступны специальные переменные (например,
tg_event
,tg_tag
).
Пример (из слайдов): Логгирование DDL команд.
-- Функция
CREATE OR REPLACE FUNCTION eventtest()
RETURNS event_trigger AS $$
BEGIN
RAISE NOTICE 'DDL Event: %, Command Tag: %', tg_event, tg_tag; -- tg_tag содержит текст команды
END;
$$ LANGUAGE plpgsql;
-- Триггер
CREATE EVENT TRIGGER eventtest_trigger
ON ddl_command_start -- Срабатывать перед началом любой DDL команды
EXECUTE PROCEDURE eventtest();
-- Проверка
CREATE TABLE some_table (id int);
Вывод в NOTICE
:
NOTICE: DDL Event: ddl_command_start, Command Tag: CREATE TABLE CREATE TABLE
Удаление триггера:
DROP TRIGGER имя_триггера ON имя_таблицы;
Часть 8: Транзакции
Транзакция — это логическая единица работы, состоящая из одной или нескольких операций SQL, которая должна быть выполнена атомарно.
Свойства ACID:
Транзакции в реляционных СУБД обычно гарантируют свойства ACID:
- Atomicity (Атомарность): Либо все операции внутри транзакции успешно выполняются и фиксируются, либо ни одна из них не оказывает влияния на базу данных (все изменения отменяются). “Все или ничего”.
- Consistency (Согласованность): Транзакция переводит базу данных из одного согласованного (целостного) состояния в другое согласованное состояние. Во время выполнения транзакции целостность может временно нарушаться, но к моменту фиксации она должна быть восстановлена.
- Isolation (Изолированность): Параллельно выполняющиеся транзакции не должны мешать друг другу. Каждая транзакция должна выполняться так, как будто других транзакций в системе нет. (На практике существуют разные уровни изоляции, которые допускают те или иные аномалии для повышения производительности).
- Durability (Долговечность/Устойчивость): Если транзакция успешно завершена (зафиксирована), ее результаты должны быть сохранены постоянно и не должны быть потеряны даже в случае сбоев системы (например, отключения питания). Обычно достигается за счет записи изменений в журналы транзакций (WAL - Write-Ahead Log) перед применением их к основным файлам данных.
Управление транзакциями в SQL:
BEGIN
илиSTART TRANSACTION
: Начинает новую транзакцию. В PostgreSQL многие команды DDL (какCREATE TABLE
) не могут выполняться внутри явного блокаBEGIN...COMMIT
, они сами по себе являются транзакциями. DML команды (INSERT
,UPDATE
,DELETE
) могут быть сгруппированы. ЕслиBEGIN
не вызван явно, каждая отдельная DML команда часто выполняется в своей собственной неявной транзакции (режим autocommit, зависит от настроек клиента/СУБД).COMMIT
: Успешно завершает текущую транзакцию, делая все ее изменения видимыми для других транзакций и постоянными.ROLLBACK
: Отменяет все изменения, сделанные в текущей транзакции с момента ее начала (или с последней точки сохранения), и завершает транзакцию.
Пример (Банковский перевод):
BEGIN; -- Начать транзакцию
-- Снять деньги со счета Алекса
UPDATE account SET balance = balance - 50.00 WHERE name = 'Alex';
-- Добавить деньги на счет Ивана
UPDATE account SET balance = balance + 50.00 WHERE name = 'Ivan';
-- Если обе операции прошли успешно:
COMMIT; -- Зафиксировать изменения
-- Если на каком-то этапе произошла ошибка (например, недостаточно средств),
-- нужно выполнить ROLLBACK вместо COMMIT:
-- ROLLBACK; -- Отменить все изменения с момента BEGIN
Точки сохранения (SAVEPOINT
):
Позволяют установить “закладку” внутри транзакции, к которой можно будет откатиться, не отменяя всю транзакцию.
SAVEPOINT имя_точки;
: Устанавливает точку сохранения.ROLLBACK TO SAVEPOINT имя_точки;
: Отменяет все изменения, сделанные после указанной точки сохранения. Сама точка сохранения остается активной.RELEASE SAVEPOINT имя_точки;
: Удаляет точку сохранения, но не отменяет изменения, сделанные после нее.
Пример с SAVEPOINT
:
BEGIN;
UPDATE account SET balance = balance - 50.00 WHERE name = 'Alex';
SAVEPOINT savepoint1; -- Установить точку сохранения
UPDATE account SET balance = balance + 50.00 WHERE name = 'Ivan';
-- Предположим, здесь возникла проблема или нужно отменить перевод Ивану
ROLLBACK TO SAVEPOINT savepoint1; -- Откат к состоянию после списания у Алекса
-- Теперь можно попробовать перевести деньги кому-то другому
UPDATE account SET balance = balance + 50.00 WHERE name = 'Ivan2';
COMMIT; -- Фиксируем результат (списание у Алекса и зачисление Ivan2)
При откате к точке сохранения (ROLLBACK TO
), все точки, созданные после* нее, автоматически удаляются.*