12 Занятие 12: PL/pgSQL / Выполнение запросов
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_12_plpgsql_queries.html Archive: DEV1-handouts-16.zip Страниц PDF: 24
Page 1: PL/pgSQL / Выполнение запросов
Авторские права
© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Использование команд SQL в коде PL/pgSQL
- Устранение неоднозначностей именования
- Проверка статуса команды
- Табличные функции
Page 3: Команды без результата
Команды SQL встраиваются в код PL/pgSQL
- как и в выражениях:
- запрос подготавливается,
- переменные PL/pgSQL подставляются как параметры
- SELECT → PERFORM
- удобно для вызова функций с побочными эффектами
- запросы, начинающиеся на WITH, надо «оборачивать» в SELECT
- INSERT, UPDATE, DELETE и другие команды SQL
- кроме служебных команд
- управление транзакциями — только в процедурах и анонимных блоках
Комментарии преподавателя
Как мы уже видели в предыдущей теме, PL/pgSQL очень тесно интегрирован с SQL. В частности, все выражения вычисляются с помощью подготовленных SQL-операторов. В выражениях можно использовать переменные PL/pgSQL и параметры подпрограмм — они автоматически подставляются в запрос в виде параметров.
Внутри кода на PL/pgSQL можно выполнять и отдельные SQL-запросы. Чтобы выполнить запрос, не возвращающий результат (INSERT, UPDATE, DELETE, CREATE, DROP и т. п.), достаточно просто написать команду SQL внутри кода на PL/pgSQL как отдельный оператор.
Как и выражения, команды подготавливаются, а переменные PL/pgSQL становятся параметрами. Это позволяет закешировать разобранный (или спланированный) запрос.
Таким же образом можно вызвать и обычный запрос SELECT, а если его результат не важен — тогда ключевое слово SELECT надо заменить на PERFORM. Это удобно для вызова функций с побочным эффектом. Если запрос начинается с WITH, его необходимо «обернуть» в SELECT (чтобы в конечном итоге запрос начинался на PERFORM).
Напомним также, что в подпрограммах нельзя использовать служебные команды, такие как VACUUM, REINDEX и т. п.
Команды COMMIT и ROLLBACK допускаются только в процедурах и в анонимных блоках кода (выполняемых SQL-командой DO).
https://postgrespro.ru/docs/postgresql/16/plpgsql-statements#PLPGSQL-ST ATEMENTS-GENERAL-SQL
Page 4: Команды, не возвращающие результат
Если результат запроса не нужен, заменяем SELECT на PERFORM:
Внутри PL/pgSQL можно использовать без изменений практически любые команды SQL, не возвращающие результат:
Управление транзакциями в процедурах
В процедурах (и в анонимных блоках кода) на PL/pgSQL можно использовать команды управления транзакциями:
Действуют определенные ограничения. Во-первых, процедура в таких случаях должна сама начинать новую транзакцию, а не выполняться в контексте уже начатой ранее.
Page 5: Page 5
Во-вторых, в стеке вызовов процедуры не должно быть ничего, кроме операторов CALL. Иными словами, если процедура вызывает процедуру, которая вызывает процедуру… которая выполняет команду управления транзакцией, то все работает:
Но стоит в этой цепочке появиться, например, вызову функции, то получается, что транзакция должна завершиться где-то «посередине» оператора, в контексте которого вызывается эта функция, например SELECT. Это недопустимо, получаем ошибку:
Page 6: Одна строка результата
SELECT … INTO
- получение первой по порядку строки результата
- одна переменная составного типа
- или подходящее количество скалярных переменных
- INSERT, UPDATE, DELETE RETURNING … INTO
- получение вставленной (измененной, удаленной) строки
- одна переменная составного типа
- или подходящее количество скалярных переменных
Комментарии преподавателя
Если результат запроса важен, его можно получить с помощью фразы INTO в одну переменную составного типа или в несколько скалярных переменных. Если запрос возвращает несколько строк, в переменную попадет только первая из них (порядок можно гарантировать фразой ORDER BY). Если запрос не возвращает ни одной строки, переменные (или атрибуты переменной составного типа) получат неопределенные значение.
Аналогично можно использовать команды INSERT, UPDATE, DELETE с фразой RETURNING. Отличие состоит в том, что эти команды не должны возвращать более одной строки — это приведет к ошибке, так как нет способа указать, какая из строк считается «первой».
https://postgrespro.ru/docs/postgresql/16/plpgsql-statements#PLPGSQL-ST ATEMENTS-SQL-ONEROW
Если запрос возвращает несколько строк, из которых используется только одна, это скорее всего говорит о том, что запрос написан неправильно. PL/pgSQL может сообщать о такой подозрительной ситуации (и о некоторых других).
https://postgrespro.ru/docs/postgresql/16/plpgsql-development-tips#PLPGS QL-EXTRA-CHECKS
Более широкими возможностями обладает стороннее расширение plpgsql_check (автор — Павел Стехуле).
https://github.com/okbob/plpgsql\_check
Page 7: Команды, возвращающие одну строку
Наверное, наиболее часто используется в PL/pgSQL команда SELECT, возвращающая одну строку. Пример выводит одну строку, хотя запрос внутри анонимного блока возвращает две:
Обратите внимание, что в приведенном коде PostgreSQL есть конструкция, очень похожая на SQL-команду SELECT INTO, в которой после INTO указывается имя новой таблицы, создающейся и наполняющейся результатами запроса. В PL/pgSQL для решения такой задачи нужно использовать эквивалентный синтаксис CREATE TABLE … AS SELECT. Команды INSERT, UPDATE, DELETE тоже могут возвращать результат с помощью фразы RETURNING. Их можно использовать в PL/pgSQL точно так же, как SELECT, добавив фразу INTO:
Проверки при создании и при выполнении подпрограмм
PL/pgSQL может выдавать предупреждения в некоторых подозрительных случаях. Для этого надо установить параметр (значение по умолчанию — none):
Это предупреждение о перекрывающих друг друга определениях переменных.
Page 8: Page 8
А здесь мы видим два предупреждения времени выполнения: запрос вернул более одной строки, а в предложении INTO указано неверное число параметров (PL/pgSQL присвоит второму неопределенное значение). Других проверок в настоящее время не предусмотрено, но они могут появиться в следующих версиях PostgreSQL. Значение параметра plpgsql.extra_warnings можно ограничить только определенными проверками. Аналогичный параметр plpgsql.extra_errors будет приводить не к предупреждениям, а к ошибкам.
Стороннее расширение plpgsql_check, написанное и развиваемое Павлом Стехуле, позволяет проверить код более детально. Расширение уже установлено в виртуальной машине курса.
Дополнительно обнаружены неиспользуемые переменные и тот факт, что выходному параметру не присваивается значение. Расширение имеет множество возможностей для обнаружения проблем в коде, в том числе и на этапе выполнения. Кроме того, расширение включает и профилировщик для целей оптимизации PL/pgSQL-кода.
Устранение неоднозначностей именования
Получится ли выполнить следующий код?
Page 9: Page 9
Не получится из-за неоднозначности в SELECT: id может означать и имя столбца, и имя переменной. Причем во фразе INTO неоднозначности нет — она относится только к PL/pgSQL. В сообщении, кстати, видно, как PL/pgSQL вырезает фразу INTO, прежде чем передать запрос в SQL. Есть несколько подходов к устранению неоднозначностей. Первый состоит в том, чтобы неоднозначностей не допускать. Для этого к переменным добавляют префикс, который обычно выбирается в зависимости от «класса» переменной, например: Для параметров p_ (parameter); Для обычных переменных l_ (local) или v_ (variable); Для констант c_ (constant); Это простой и действенный способ, если использовать его систематически и никогда не использовать префиксы в именах столбцов. К минусам можно отнести некоторую неряшливость и пестроту кода из-за лишних подчеркиваний. Вот как это может выглядеть в нашем случае:
Второй способ состоит в использовании квалифицированных имен — к имени объекта через точку дописывается уточняющий квалификатор: для столбца — имя или псевдоним таблицы; для переменной — метку блока; для параметра — имя функции. Такой способ более «честный», чем добавление префиксов, поскольку работает для любых названий столбцов. Вот как будет выглядеть наш пример с использованием квалификаторов:
Третий вариант — установить приоритет переменных над столбцами или наоборот, столбцов над переменными. За это отвечает конфигурационный параметр plpgsql.variable_conflict. Его возможные значения use_column, use_variable и error. В ряде случаев это упрощает разрешение конфликтов, но не устраняет их полностью. Кроме того, неявное правило (которое, к тому же, может внезапно поменяться) непременно приведет к тому, что какой-то код будет выполняться не так, как предполагал разработчик. Тем не менее приведем пример. Здесь устанавливается приоритет переменных, поэтому достаточно квалифицировать только столбцы таблицы:
Page 10: Page 10
Какой способ выбрать — дело опыта и вкуса. Мы рекомендуем остановиться либо на первом (префиксы), либо на втором (квалификаторы), и не смешивать их в одном проекте, поскольку систематичность крайне важна для облегчения понимания кода. В курсе мы будем использовать второй способ, но только в тех случаях, когда это действительно необходимо — чтобы не загромождать примеры. Однако в коде, предназначенном для промышленной эксплуатации, думать о неоднозначностях надо всегда: нет никакой гарантии, что завтра в таблице не появится новый столбец с именем, совпадающим с вашей переменной!
Page 11: Проверка результата
INTO STRICT
- гарантия получения ровно одной строки — ни больше, ни меньше
- Диагностика ROW_COUNT
- число строк, возвращенных (обработанных) последней командой SQL
- Переменная FOUND
- после команды SQL: истина, если команда вернула (обработала) строку
- после цикла: признак того, что выполнилась хотя бы одна итерация
Комментарии преподавателя
Добавив к предложению INTO ключевое слово STRICT, мы получим гарантию того, что команда вернула или обработала ровно одну строку — иначе будет зафиксирована ошибка.
Кроме того, можно узнать статус только что выполненной команды SQL (если она не завершилась ошибкой). Для этого есть два способа.
Во-первых, можно получить число строк, затронутых командой, с помощью конструкции GET DIAGNOSTICS.
Во-вторых, специальная логическая переменная FOUND показывает, были ли обработаны командой хотя бы какие-то данные.
Переменную FOUND можно использовать и как индикатор того, что тело цикла выполнилось минимум один раз.
https://postgrespro.ru/docs/postgresql/16/plpgsql-statements#PLPGSQL-ST ATEMENTS-DIAGNOSTICS
Page 12: Ровно одна строка
Что произойдет, если запрос вернет несколько строк?
В переменную будет записана только первая строка. Поскольку мы не указали ORDER BY, то порядок строк в общем случае непредсказуем:
Поскольку в командах INSERT, UPDATE, DELETE нет возможности указать порядок строк, то команда, затрагивающая несколько строк, приводит к ошибке:
А если запрос не вернет ни одной строки?
Переменные будут содержать неопределенные значения. То же относится и командам INSERT, UPDATE, DELETE. Например:
Иногда хочется быть уверенным, что в результате выборки получилась ровно одна строка: ни больше, ни меньше. В этом случае удобно воспользоваться фразой INTO STRICT:
Page 13: Page 13
Как мы видели, команды INSERT, UPDATE, DELETE, затрагивающие несколько строк, приводят к ошибке. Фраза STRICT позволяет гарантировать, что строка будет ровно одна (а не ноль):
Явная проверка состояния
Другая возможность — проверять состояние последней выполненной SQL-команды: Команда GET DIAGNOSTICS позволяет получить количество затронутых строк (ROW_COUNT); Предопределенная логическая переменная FOUND показывает, была ли затронута хотя бы одна строка.
Page 14: Page 14
Заметьте: диагностика не позволяет обнаружить, что запросу соответствует несколько строк. Элемент диагностики ROW_COUNT возвращает единицу, так как только одна строка из полученного набора была помещена в переменную r.
Page 15: Табличные функции
Строки запроса
- Одна строка
- RETURN NEXT выражение;
- если нет выходных параметров
- RETURN NEXT;
- если есть выходные параметры
- Особенности
- строки добавляются к результату,
- но выполнение функции не прекращается
- команды можно выполнять несколько раз
- результат не возвращается, пока функция не завершится
Комментарии преподавателя
Чтобы создать на PL/pgSQL табличную функцию, нужно объявить ее как RETURNS SETOF или RETURNS TABLE (точно так же, как и для SQL).
Отдельные элементы возвращаемого значения формируются в специальном буфере командами RETURN QUERY и(или) RETURN NEXT. Эти операторы не завершают выполнение, их вызовы можно смешивать и вызывать несколько раз в теле функции, при этом их результаты будут объединены. По окончании работы функции (вызов RETURN без параметров или достижение конца ее тела) будет возвращен результирующий набор строк из буфера.
Если после выполнения одиночной команды RETURN QUERY запрос выполнение будет завершено, то результат функции будет таким же, как в случае SQL-функции, содержащей этот запрос в качестве последнего. Однако запрос из SQL-функции имеет шансы быть подставленным в объемлющий запрос, а в случае PL/pgSQL это исключено.
Команда RETURN NEXT накапливает результат функции построчно, она похожа на обычный RETURN, но, как мы уже отмечали, не прекращает выполнение функции, а добавляет возвращаемое значение в качестве очередной строки будущего результата.
Таким образом, команды RETURN NEXT и RETURN QUERY не работают как yield в функциях-генераторах современных языков.
https://postgrespro.ru/docs/postgresql/16/plpgsql-control-structures#PLPGS QL-STATEMENTS-RETURNING
Page 16: Табличные функции
Пример табличной функции на PL/pgSQL:
Другой вариант — возвращать значения построчно.
Почему функция объявлена как STABLE, а не как IMMUTABLE? На первый взгляд кажется, что при повторных вызовах функция будет возвращать тот же результат, но на самом деле он неявно зависит от текущей локали:
Еще один пример иллюстрирует «смешанный» подход. Здесь мы создаем функцию, которая возвращает список подпрограмм из своей схемы. Элемент диагностики PG_ROUTINE_OID позволяет функции получить свой oid:
Page 17: Page 17
Пробуем вызов:
Page 18: Итоги
PL/pgSQL тесно интегрирован с SQL
- в процедурном коде можно выполнять запросы
- (оформленные как выражения или отдельные команды)
- в запросах можно использовать переменные
- можно получать результаты запросов и их статус
- Нужно следить за неоднозначностями разрешения имен
Page 19: Практика
- Напишите функцию add_author для добавления новых авторов. Функция должна принимать три параметра (фамилия, имя, отчество) и возвращать идентификатор нового автора.
- Проверьте, что приложение позволяет добавлять авторов. 2. Напишите функцию buy_book для покупки книги. Функция принимает идентификатор книги и уменьшает количество таких книг на складе на единицу. Возвращаемое значение отсутствует.
- Проверьте, что в «Магазине» появилась возможность
- покупки книг.
Комментарии преподавателя
Вы можете обратить внимание, что при покупке книг приложение позволяет «уйти в минус». Если бы количество книг хранилось в столбце, простым и хорошим решением было бы сделать ограничение CHECK. Но в нашем случае количество рассчитывается, и мы отложим написание проверки до темы «Триггеры».
Page 20: 1. Функция add_author
2. Функция buy\_book
Page 21: Практика+
Напишите игру, в которой сервер пытается угадать загаданное пользователем животное, задавая последовательные уточняющие вопросы, на которые можно отвечать «да» или «нет». Если сервер предложил неправильный вариант, он запрашивает у пользователя имя животного и отличающий вопрос. Эта новая информация запоминается и используется в следующих играх. 1. Создайте таблицу для представления информации. 2. Придумайте интерфейс и реализуйте необходимые функции. 3. Проверьте реализацию.
Комментарии преподавателя
Пример диалога (между людьми):
- Это млекопитающее?
- Да.
- Это слон?
- Нет.
- Сдаюсь. Кто это?
- Кит.
- Как отличить кита от слона?
- Он живет в воде.
Информацию удобно представить в виде двоичного дерева. Внутренние узлы хранят вопросы, листовые узлы — названия животных. Один из дочерних узлов соответствует ответу «да», другой — ответу «нет».
Между вызовами функций надо передавать информацию о том, на каком узле дерева мы остановились («контекст» диалога). Функции могут быть, например, такими.
● начать игру (нет входного контекста): FUNCTION start_game(OUT context integer, OUT question text)
● продолжение игры (получаем ответ, выдаем следующий вопрос): FUNCTION continue_game( INOUT context integer, IN answer boolean, OUT you_win boolean, OUT question text)
● завершение игры (внесение информации о новом животном): FUNCTION end_game( IN context integer, IN name text, IN question text) RETURNS void
Page 22: 1. Таблица
Первая строка считается корнем дерева.
2. Функции
Page 23: Page 23
3. Пример сеанса игры
Загадываем слово «кит».
Page 24: Page 24
Теперь в таблице:
Снова загадали «кит».