Перейти к содержимому

Разработка серверной части приложений PostgreSQL. Базовый курс

Функции, процедуры, триггеры, динамические команды и серверная логика на SQL и PL/pgSQL.

Курс сохраняет официальный порядок тем. Русские материалы извлечены из PDF в читаемый нативный Markdown для дальнейшей редакторской работы.

Состояние материалов: 官方 PDF 已提取为原生 Markdown

Базовый язык: Базовый язык: English. Текущий английский архив используется как базовый; русская версия сохранена для сверки глав.

О курсе

Функции, процедуры, триггеры, динамические команды и серверная логика на SQL и PL/pgSQL.

Состояние материалов

ЯзыкТекущий архивВерсияТемыСтраница курса
EnglishDEV1-handouts-16-en.zip1621HTTP 200
РусскийDEV1-handouts-16.zip1621HTTP 200

Содержание

Дополнительные материалы

  • catalogs.pdf
  • datatypes.pdf
  • dev1_new_features.pdf
  • dev1_student_guide.pdf
  • unix_commands.pdf

Редакторский статус

Русский комплект доступен в виде нативного Markdown. Если английский комплект не найден, английский маршрут оставлен для будущего источника.

00 Занятие 00: Разработка серверной части приложений / Базовый курс

Исходная страница в нативном Markdown: Разработка серверной части приложений / Базовый курс
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_00_introduction.html Archive: DEV1-handouts-16.zip Страниц PDF: 10

Page 1: Разработка серверной части приложений / Базовый курс

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Разработчикам

Source slide page 2: Разработчикам{#source-figure-002 caption=“Source slide page 2: Разработчикам”}

  • DEV2, 4 дня

Комментарии преподавателя

Для разработчиков серверной части приложений мы предлагаем специальную линейку курсов.

Для прохождения этих курсов необходимы предварительные знания основ языка SQL. Специального курса по языку SQL в нашей линейке нет, но существует множество книг и других образовательных ресурсов, с помощью которых можно освоить SQL. Мы рекомендуем книгу Евгения Моргунова «PostgreSQL. Основы языка SQL»:

https://postgrespro.ru/education/books/sqlprimer

Базовым курсом для разработчиков является данный курс DEV1.

В курсе DEV2 рассматриваются особенности внутреннего устройства сервера, влияющие на разработку прикладного кода, и всесторонне изучается расширяемость: возможность дополнить серверные механизмы собственным кодом, что позволяет использовать PostgreSQL для решения самых разнообразных задач.

В курсе QPT детально разбираются механизмы планирования и выполнения запросов, рассматривается настройка параметров экземпляра, связанных с производительностью, и изучаются возможности для поиска проблемных запросов и их оптимизации.

Курс DBS рассказывает про настройку подключений клиентов к СУБД и про управление доступом внутри самой СУБД.

В курсе PGPRO рассматриваются дополнительные возможности, которые предоставляет СУБД Postgres Pro Enterprise.

https://postgrespro.ru/education/courses

Page 3: Администраторам

Source slide page 3: Администраторам{#source-figure-003 caption=“Source slide page 3: Администраторам”}

  • DBA2, 4 дня

Комментарии преподавателя

Для администраторов мы предлагаем следующие курсы.

В базовом курсе DBA1 даются общие сведения об архитектуре PostgreSQL, процессе установки, базовой настройке и управлении сервером. Рассматриваются основные задачи администрирования, вопросы управления доступом. Приводится обзор резервного копирования и репликации.

В курсе DBA2 обсуждается настройка различных конфигурационных параметров исходя из понимания внутренней организации сервера; говорится о мониторинге сервера с использованием обратной связи для итеративной настройки параметров. Также рассматриваются настройки, связанные с локализацией, управление расширениями и знакомство с процедурой обновления сервера.

Курс DBA3 посвящен рассмотрению резервного копирования, а также настройкам физической и логической репликации и сценариям ее использования. Также дается общее представление о способах и сложностях построения высокодоступных, масштабируемых кластеров.

Курсы DBS, QPT и PGPRO являются общими для разработчиков и администраторов.

Курсы по администрированию могут быть интересны и разработчикам, желающим детальнее изучить внутреннее устройство PostgreSQL, а также в случае, когда на проекте нет выделенной роли администратора.

Page 4: О курсе DEV1

Продолжительность: 4 дня

  • Предварительные знания
    • основы SQL
    • опыт работы с любым процедурным языком программирования
    • минимальные сведения о работе в Unix
  • Какие навыки будут получены
    • общие сведения об архитектуре PostgreSQL
    • использование основных объектов БД: таблиц, индексов, представлений
    • программирование на стороне сервера на языках SQL и PL/pgSQL
    • использование основных типов данных, включая записи и массивы
    • организация взаимодействия с клиентской частью приложения

Комментарии преподавателя

Базовый курс знакомит разработчиков приложений, работающих над серверной частью, с основами PostgreSQL и написанием хранимых процедур и функций на языках SQL и PL/pgSQL.

Page 5: Используемые материалы

Подготовленная виртуальная машина

  • ОС Xubuntu 24.04
  • PostgreSQL 16 с документацией на русском языке
  • учебное веб-приложение «Книжный магазин»
  • DBeaver
  • Учебные материалы
    • руководство слушателя
    • презентации, демонстрации, практические задания и их решение
    • (в форматах html и pdf)
    • справочные материалы — функции и типы данных PostgreSQL,
    • схема основных таблиц системного каталога с командами psql,
    • некоторые команды Unix

Комментарии преподавателя

Если вы проходите курс самостоятельно, обязательно начните со знакомства с Руководством слушателя. В числе прочего в нем написано, где скачать и как использовать виртуальную машину курса и прочие материалы. Все материалы курса доступны по адресу:

https://postgrespro.ru/education/courses/DEV1

Выполнение практических заданий очень важно для получения навыков работы с PostgreSQL. Обязательно старайтесь сначала самостоятельно выполнить задания, а затем просмотрите предлагаемые нами решения, даже если задание не вызвало вопросов. В решениях могут содержаться дополнительные сведения, которые не упоминаются в презентациях и демонстрациях.

Учебные материалы (презентации, демонстрации, практические задания и их решения) доступны в двух форматах. Формат html удобен для онлайн-работы, он позволяет копировать фрагменты текста и кода. Документ в формате pdf разбит на страницы и удобен для печати.

Дополнительные справочные материалы помогут быстро найти нужную информацию.

В виртуальной машине установлен DBeaver. В курсе мы используем только psql, но при желании можно воспользоваться и графической средой.

Page 6: Организационные вопросы

День: ~8 академических часов + обед (1час)

  • Каждая тема, как правило, состоит из
    • презентации и демонстраций: ~25–60 мин
    • практических заданий: ~20–30 мин, включая перерыв

Page 7: День 1

Базовый инструментарий

    1. Установка и управление, psql
  • Архитектура
      1. Общее устройство PostgreSQL
      1. Изоляция и многоверсионность
      1. Буферный кеш и журнал
  • Организация данных
      1. Логическая структура
      1. Физическая структура

Комментарии преподавателя

Первый день занятий посвящен в основном теоретической подготовке. Здесь рассматриваются основы архитектуры PostgreSQL, без знания которых невозможно полноценное использование СУБД. Полученные знания будут применяться на практике в последующих темах курса.

Page 8: День 2

Source slide page 8: День 2{#source-figure-008 caption=“Source slide page 8: День 2”}

Учебное приложение

    1. Книжный магазин
  • SQL
      1. Функции
      1. Процедуры
      1. Составные типы
  • PL/pgSQL
      1. Обзор и конструкции языка

Комментарии преподавателя

Начиная с модуля «Учебное приложение» второго дня занятий каждая тема содержит два набора практических заданий: основных, связанных с приложением «Книжный магазин» (они отмечены значком книги) и дополнительных. За время, которое отводится на задания (около 30 минут), выполнить всю практику невозможно. Используйте дополнительные задания для самостоятельной работы.

Page 9: День 3

PL/pgSQL (продолжение)

    1. Выполнение запросов
    1. Курсоры
    1. Динамические команды
    1. Массивы
    1. Обработка ошибок

Page 10: День 4

PL/pgSQL (продолжение)

    1. Триггеры
    1. Отладка
  • Управление доступом
      1. Обзор
  • Резервное копирование
      1. Логическое резервирование

01 Занятие 01: Обзор базового инструментария / Установка и управление, psql

Исходная страница в нативном Markdown: Обзор базового инструментария / Установка и управление, psql
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_01_tools_overview.html Archive: DEV1-handouts-16.zip Страниц PDF: 20

Page 1: Обзор базового инструментария / Установка и управление, psql

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Варианты установки PostgreSQL
  • Управление сервером
  • Журнал сообщений сервера
  • Настройка параметров конфигурации
  • Использование psql

Page 3: Кластер БД, сервер, клиент

Source slide page 3: Кластер БД, сервер, клиент{#source-figure-003 caption=“Source slide page 3: Кластер БД, сервер, клиент”}

  • клиент

Комментарии преподавателя

Начнем с основных понятий.

PostgreSQL — программа, которая относится к классу систем управления базами данных.

Когда эта программа выполняется, мы называем ее сервером PostgreSQL или экземпляром сервера. Пока сервер представляется для нас «черным ящиком», но постепенно мы познакомимся с тем, как он устроен.

Данные, которыми управляет PostgreSQL, хранятся в базах данных. Один экземпляр PostgreSQL одновременно работает с несколькими базами данных. Этот набор баз данных называется кластером баз данных. Подробнее мы будем говорить о базах данных в теме «Организация данных. Логическая структура».

С сервером взаимодействуют клиенты — внешние приложения, которые могут подключаться к одной из баз сервера и посылать запросы для выполнения.

Итак: кластер баз данных — это данные в файлах; сервер или экземпляр сервера — программа, управляющая кластером баз данных, а клиент — программа, позволяющая «общаться» с сервером.

Page 4: Установка

Варианты

  • готовые пакеты (предпочтительный способ)
  • установка из исходных кодов
  • без установки — облачные сервисы
  • Расширения
    • дополнительный функционал
    • устанавливаются отдельно
    • в поставке с сервером — модули и программы (~50 штук)

Комментарии преподавателя

Предпочтительный вариант установки PostgreSQL — использование пакетных менеджеров (таких, как apt или rpm) и готовых пакетов. В этом случае получается понятная, поддерживаемая и легко обновляемая установка. Пакеты существуют для большинства операционных систем.

Другой вариант — самостоятельная сборка PostgreSQL из исходных кодов. Это может понадобиться для установки нестандартных значений параметров или при использовании непопулярной платформы.

Готовые пакеты и исходные коды: http://www.postgresql.org/download/

Кроме того, можно использовать готовые облачные решения, что позволяет обойтись вообще без установки. Такую возможность дают многие ведущие зарубежные (Amazon RDS, Google Cloud SQL, Microsoft Azure) и отечественные (Yandex Cloud, Облако Mail.ru) платформы.

В курсе мы будем использовать виртуальную машину с ОС Xubuntu и сервер PostgreSQL 16, установленный из пакета для этой ОС. В этом случае сразу настраивается автоматический запуск и останов сервера PostgreSQL при запуске и останове операционной системы.

Для PostgreSQL существует большое количество расширений, которые подключают новый функционал к СУБД «на лету», без изменения ядра системы. В состав дистрибутива входит 50 расширений.

https://postgrespro.ru/docs/postgresql/16/contrib

https://postgrespro.ru/docs/postgresql/16/contrib-prog

Список доступных расширений и статус их установки можно посмотреть в представлении pg_available_extensions.

Page 5: Управление сервером

Утилита для управления

  • pg_ctlcluster
  • pg_ctl
  • Основные задачи
    • запуск сервера
    • останов сервера
    • обновление параметров конфигурации

Комментарии преподавателя

К основным операциям управления сервером относятся начальная инициализация кластера баз данных, запуск и останов сервера, обновление конфигурации и некоторые другие. Для выполнения этих действий предназначена утилита pg_ctl, входящая в состав PostgreSQL.

В пакетном дистрибутиве для Ubuntu доступ к утилите pg_ctl осуществляется не напрямую, а через специальную обертку pg_ctlcluster. Справку по использованию pg_ctlcluster можно получить командой:

$ man pg_ctlcluster

Также можно получить информацию об установленных кластерах и их текущем состоянии при помощи команд:

$ pg_lsclusters

$ pg_ctlcluster status

Более подробная информация об управлении сервером для администраторов баз данных:

https://postgrespro.ru/docs/postgresql/16/app-pg-ctl

https://postgrespro.ru/docs/postgresql/16/runtime

Page 6: Установка и управление

В виртуальной машине курса установка выполнена из пакета. Каталог установки PostgreSQL:

student$ ls -l /usr/lib/postgresql/16
total 8
drwxr-xr-x 2 root root 4096 ноя 25 17:25 bin
drwxr-xr-x 4 root root 4096 ноя 25 18:28 lib

Владелец ПО сервера — пользователь root. Кластер баз данных автоматически инициализируется при установке из пакета и находится в каталоге /var/lib/postgresql/16/main. В последующих темах мы будем ссылаться на этот каталог как PGDATA, по имени переменной ОС, которую можно установить для использования в некоторых утилитах сервера. Владельцем каталога является пользователь postgres. Вот содержимое каталога:

student$ sudo ls -l /var/lib/postgresql/16/main
total 96
drwx------ 1 postgres postgres 4096 апр 10 15:33 base
drwx------ 1 postgres postgres 4096 апр 10 15:33 global
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_commit_ts
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_dynshmem
drwx------ 4 postgres postgres 4096 ноя 25 18:29 pg_logical
drwx------ 4 postgres postgres 4096 ноя 25 18:28 pg_multixact
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_notify
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_replslot
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_serial
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_snapshots
drwx------ 1 postgres postgres 4096 апр 10 15:33 pg_stat
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_stat_tmp
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_subtrans
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_tblspc
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_twophase
-rw------- 1 postgres postgres    3 ноя 25 18:28 PG_VERSION
drwx------ 1 postgres postgres 4096 ноя 25 18:28 pg_wal
drwx------ 2 postgres postgres 4096 ноя 25 18:28 pg_xact
-rw------- 1 postgres postgres   88 ноя 25 18:28 postgresql.auto.conf
-rw------- 1 postgres postgres  130 апр 10 15:33 postmaster.opts
-rw------- 1 postgres postgres  109 апр 10 15:33 postmaster.pid

При установке из пакета в настройки запуска ОС добавляется автоматический запуск PostgreSQL. Поэтому после загрузки операционной системы отдельно стартовать PostgreSQL не нужно. Можно явным образом управлять сервером с помощью следующих команд, которые выдаются от имени привилегированного пользователя ОС через sudo: Остановить сервер:

student$ sudo pg_ctlcluster 16 main stop

Запустить сервер:

student$ sudo pg_ctlcluster 16 main start

Перезапустить:

student$ sudo pg_ctlcluster 16 main restart

Обновить конфигурацию:

student$ sudo pg_ctlcluster 16 main reload

Получить информацию о сервере:

student$ sudo pg_ctlcluster 16 main status
pg_ctl: server is running (PID: 88989)
/usr/lib/postgresql/16/bin/postgres "-D" "/var/lib/postgresql/16/main" "-c"
"config_file=/etc/postgresql/16/main/postgresql.conf"

Список установленных экземпляров (можно без sudo):

student$ pg_lsclusters

Page 7: Page 7

Ver Cluster Port Status Owner    Data directory              Log file
16  main    5432 online postgres /var/lib/postgresql/16/main
/var/log/postgresql/postgresql-16-main.log

Page 8: Журнал сообщений

В журнал записываются

  • служебные сообщения сервера
  • сообщения пользовательских сеансов
  • сообщения приложений
  • Настройка журнала
    • расположение
    • формат записей
    • какие события регистрировать

Комментарии преподавателя

Информация о ходе работы СУБД записывается в журнал сообщений сервера. Сюда попадают сведения о запуске и останове сервера, различная служебная информация, в том числе сообщения о возникающих проблемах.

Также сюда могут выводиться сообщения о выполняющихся командах и времени их работы, о возникающих блокировках и тому подобное. Это позволяет выполнять трассировку пользовательских сеансов.

Разработчики приложений могут формировать и записывать в журнал сервера свои собственные сообщения.

Настройки PostgreSQL позволяют гибко определять, какие именно сообщения и в каком формате должны попадать в журнал сервера.

Например, вывод в форматах csv и json удобен для автоматизации анализа журнала.

https://postgrespro.ru/docs/postgresql/16/runtime-config-logging

Page 9: Журнал сообщений сервера

Журнал сообщений сервера находится здесь:

student$ ls -l /var/log/postgresql/postgresql-16-main.log
-rw-r----- 1 postgres adm 51181 апр 10 15:33 /var/log/postgresql/postgresql-16-main.log

Заглянем в конец журнала:

student$ tail -n 10 /var/log/postgresql/postgresql-16-main.log
2026-04-10 15:33:20.897 MSK [82414] LOG:  background worker "logical replication
launcher" (PID 82420) exited with exit code 1
2026-04-10 15:33:20.898 MSK [82415] LOG:  shutting down
2026-04-10 15:33:20.912 MSK [82415] LOG:  checkpoint starting: shutdown immediate
2026-04-10 15:33:20.926 MSK [82415] LOG:  checkpoint complete: wrote 0 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.001 s, sync=0.001 s, total=0.029 s;
sync files=0, longest=0.000 s, average=0.000 s; distance=0 kB, estimate=3504 kB;
lsn=0/1D95E50, redo lsn=0/1D95E50
2026-04-10 15:33:20.937 MSK [82414] LOG:  database system is shut down
2026-04-10 15:33:21.808 MSK [88989] LOG:  starting PostgreSQL 16.11 (Ubuntu
16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu
13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
2026-04-10 15:33:21.808 MSK [88989] LOG:  listening on IPv4 address "127.0.0.1", port 5432
2026-04-10 15:33:21.824 MSK [88989] LOG:  listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2026-04-10 15:33:21.843 MSK [88992] LOG:  database system was shut down at 2025-11-25
18:29:40 MSK
2026-04-10 15:33:21.872 MSK [88989] LOG:  database system is ready to accept connections

Page 10: Параметры конфигурации

Для всего экземпляра

  • основной файл параметров — postgresql.conf
ALTER SYSTEM — postgresql.auto.conf
  • Для текущего сеанса
SET/RESET
set_config()
  • Просмотр текущего значения
SHOW
current_setting()
pg_settings
\dconfig

Комментарии преподавателя

Сервер PostgreSQL настраивается с помощью разнообразных параметров конфигурации, которые позволяют управлять ресурсами, настраивать служебные процессы и пользовательские сеансы, управлять журналом сервера и решать многие другие задачи. Поэтому нужно знать, как проверить текущие значения параметров и установить новые.

Настройки всего сервера обычно задаются в конфигурационных файлах. Основной конфигурационный файл — postgresql.conf, он редактируется вручную. Второй конфигурационный файл — postgresql.auto.conf — предназначен для изменения специальной командой ALTER SYSTEM. Параметры, установленные через ALTER SYSTEM, имеют приоритет над параметрами в postgresql.conf.

Директивы включения файлов и каталогов include и include_dir позволяют разделять сложные файлы postgresql.conf на части. Это может быть удобно, например, при управлении несколькими серверами с похожими конфигурациями.

Большинство параметров конфигурации допускает изменение значений в пользовательских сеансах прямо во время выполнения. Помимо системных, можно определять и пользовательские параметры и работать с ними с помощью этих же команд и функций.

Варианты установки и управления параметрами: https://postgrespro.ru/docs/postgresql/16/config-setting Текущие значения параметров доступны в представлении pg_settings: https://postgrespro.ru/docs/postgresql/16/view-pg-settings

Page 11: Параметры конфигурации

Основной файл конфигурации postgresql.conf расположен в этом каталоге:

student$ ls -l /etc/postgresql/16/main
total 60
drwxr-xr-x 2 postgres postgres  4096 ноя 25 18:28 conf.d
-rw-r--r-- 1 postgres postgres   315 ноя 25 18:28 environment
-rw-r--r-- 1 postgres postgres   143 ноя 25 18:28 pg_ctl.conf
-rw-r----- 1 postgres postgres  5743 ноя 25 18:28 pg_hba.conf
-rw-r----- 1 postgres postgres  2640 ноя 25 18:28 pg_ident.conf
-rw-r--r-- 1 postgres postgres 29960 ноя 25 18:28 postgresql.conf
-rw-r--r-- 1 postgres postgres   317 ноя 25 18:28 start.conf

Здесь же находятся и другие конфигурационные файлы. Проверим значение параметра work_mem:

=> SHOW work_mem;
 work_mem
----------
 4MB
(1 row)

Параметр work_mem задает объем памяти, который будет использоваться для внутренних операций сортировки и размещения хеш-таблиц, прежде чем будут задействованы временные файлы на диске. 4MB — это значение по умолчанию и оно слишком мало. Допустим, мы хотим увеличить его до 16MB для всего экземпляра. Для этого есть различные пути. Во-первых, можно внести изменение в postgresql.conf, раскомментировав и изменив строку, где определяется параметр:

student$ grep '#work_mem' /etc/postgresql/16/main/postgresql.conf
#work_mem = 4MB
# min 64kB

Во-вторых, можно поместить определение параметра в файл с суффиксом .conf в каталоге /etc/postgresql/16/main/conf.d или в пользовательский файл конфигурации, местоположение которого следует задать в директиве include основного конфигурационного файла postgresql.conf. В-третьих, можно изменить значение параметра с помощью команды SQL — что мы и сделаем:

=> ALTER SYSTEM SET work_mem TO '16MB';
ALTER SYSTEM

Такое изменение попадает не в postgresql.conf, а в файл postgresql.auto.conf, который находится в каталоге PGDATA:

student$ sudo cat /var/lib/postgresql/16/main/postgresql.auto.conf
# Do not edit this file manually!
# It will be overwritten by the ALTER SYSTEM command.
work_mem = '16MB'

Чтобы изменение вступило в силу, нужно перечитать конфигурационные файлы. Для этого можно воспользоваться pg_ctlcluster, либо использовать функцию SQL:

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Убедимся, что новое значение параметра применилось. Кроме команды SHOW, можно сделать это таким образом:

=> SELECT current_setting('work_mem');
 current_setting
-----------------
 16MB
(1 row)

Чтобы восстановить значение параметра по умолчанию, достаточно вместо SET использовать команду RESET (и, конечно, перечитать конфигурационные файлы):

Page 12: Page 12

=> ALTER SYSTEM RESET work_mem;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Большинству параметров можно установить новое значение для текущего сеанса прямо во время его выполнения. Например, если мы собираемся выполнить запрос, сортирующий большой объем данных, то для сеанса можно увеличить значение work_mem:

=> SET work_mem = '64MB';
SET

Новое значение действует только в текущем сеансе или даже в текущей транзакции (при указании SET LOCAL). Еще один способ проверить текущее значение — выполнить запрос к представлению:

=> SELECT name, setting, unit FROM pg_settings WHERE name = 'work_mem';
   name   | setting | unit
----------+---------+------
 work_mem | 65536   | kB
(1 row)

Можно увидеть значение параметра и с помощью команды \dconfig:

=> \dconfig work_mem
List of configuration parameters
 Parameter | Value
-----------+-------
 work_mem  | 64MB
(1 row)

Page 13: Клиент psql

Терминальный клиент для работы с PostgreSQL

  • Поставляется вместе с СУБД
  • Используется администраторами и разработчиками для интерактивной работы и выполнения скриптов

Комментарии преподавателя

Для работы с СУБД PostgreSQL существуют различные сторонние инструменты, рассмотрение которых не входит в рамки курса.

В курсе мы будем использовать терминальный клиент psql:

  1. psql — это единственный клиент, поставляемый вместе с СУБД.

  2. Навыки работы c psql пригодятся разработчикам и администраторам вне зависимости от того, с каким инструментом они будут работать.

Для интерактивной работы в psql встроена поддержка readline, программ постраничного просмотра результатов запросов (таких, как less и pspg), а также подключения внешних редакторов. Возможности psql позволяют взаимодействовать с ОС, просматривать содержимое системного каталога, создавать скрипты для автоматизации повторяющихся задач.

https://postgrespro.ru/docs/postgresql/16/app-psql

Page 14: Подключение

При запуске psql нужно указать параметры подключения. К обязательным параметрам относятся: имя базы данных, по умолчанию совпадает с именем пользователя; имя пользователя (роль), по умолчанию совпадает с именем пользователя ОС; узел (host), по умолчанию — локальное соединение; порт, по умолчанию — обычно 5432. Параметры указываются так:

student$ psql -d база -U роль -h узел -p порт

Настройки, сделанные в виртуальной машине курса, позволяют подключаться к PostgreSQL без указания параметров:

student$ psql

Проверим текущее подключение:

=> \conninfo
You are connected to database "student" as user "student" via socket in
"/var/run/postgresql" at port "5432".

Команда \connect выполняет новое подключение, не покидая psql. Ее можно сократить до \c. Мы будем указывать необязательную часть имени команды в квадратных скобках: \c[onnect].

Справочная информация

Справку по psql можно получить не только в документации, но и прямо в системе. Команда

student$ psql --help

выдает справку по запуску. А если PostgreSQL устанавливался с документацией, то справочное руководство можно получить командой

student$ man psql

Утилита psql умеет выполнять команды SQL и свои собственные команды, которые начинаются с обратной косой черты, как \conninfo. Команды psql всегда однострочные — в отличие от команд SQL. Внутри psql есть возможность получить список и краткое описание его собственных команд: \? выдает список команд psql, \h[elp] выдает список команд SQL, которые поддерживает сервер, а также синтаксис конкретной команды SQL.

Форматирование вывода

Клиент psql умеет выводить результаты запросов в разных форматах: формат с выравниванием значений; формат без выравнивания; расширенный формат. Формат с выравниванием используется по умолчанию:

=> SELECT name, setting, unit FROM pg_settings LIMIT 7;
            name            |  setting   | unit
----------------------------+------------+------
 allow_in_place_tablespaces | off        |
 allow_system_table_mods    | off        |
 application_name           | psql       |
 archive_cleanup_command    |            |
 archive_command            | (disabled) |
 archive_library            |            |
 archive_mode               | off        |
(7 rows)

Ширина столбцов выровнена по значениям. Также выводится строка заголовков и итоговая строка. Команды psql для переключения режима выравнивания: \a — переключатель режима: с выравниванием/без выравнивания. \t — переключатель отображения строки заголовка и итоговой строки. Отключим выравнивание, заголовок и итоговую строку:

=> \a \t
Output format is unaligned.
Tuples only is on.
=> SELECT name, setting, unit FROM pg_settings LIMIT 7;
allow_in_place_tablespaces|off|
allow_system_table_mods|off|
application_name|psql|
archive_cleanup_command||
archive_command|(disabled)|
archive_library||
archive_mode|off|

Page 15: Page 15

=> \a \t
Output format is aligned.
Tuples only is off.

Такой формат неудобен для просмотра, но может оказаться полезным для автоматической обработки результатов. Расширенный формат удобен, когда нужно вывести много столбцов для одной или нескольких записей. Для этого вместо точки с запятой указываем в конце команды \gx:

=> SELECT name, setting, unit, category, context, vartype,
    min_val, max_val, boot_val, reset_val
    FROM pg_settings
    WHERE name = 'work_mem' \gx
-[ RECORD 1 ]----------------------
name      | work_mem
setting   | 4096
unit      | kB
category  | Resource Usage / Memory
context   | user
vartype   | integer
min_val   | 64
max_val   | 2147483647
boot_val  | 4096
reset_val | 4096

Если расширенный формат нужен не для одной команды, а постоянно, можно включить его переключателем \x. Все возможности форматирования результатов запросов доступны через команду \pset.

Взаимодействие с ОС и выполнение скриптов

Из psql можно выполнять команды shell:

=> \! pwd
/home/student

С помощью запроса SQL можно сформировать несколько других запросов SQL и записать их в файл, используя команду \o[ut]:

=> \a \t \pset fieldsep ''
Output format is unaligned.
Tuples only is on.
Field separator is "".
=> \o dev1_tools.sql
=> SELECT format('SELECT %L AS tbl, count(*) FROM %I;', tablename, tablename)
FROM pg_tables LIMIT 3;

На экран (в стандартный вывод) ничего не попало. Посмотрим в файле:

=> \! cat dev1_tools.sql
SELECT 'pg_statistic' AS tbl, count(*) FROM pg_statistic;
SELECT 'pg_type' AS tbl, count(*) FROM pg_type;
SELECT 'pg_foreign_table' AS tbl, count(*) FROM pg_foreign_table;

Вернем вывод на экран и восстановим форматирование по умолчанию.

=> \o \t \a
Tuples only is off.
Output format is aligned.

Выполним теперь эти команды из файла с помощью \i[nclude]:

=> \i dev1_tools.sql
     tbl      | count
--------------+-------
 pg_statistic |   409
(1 row)
   tbl   | count
---------+-------
 pg_type |   613
(1 row)
       tbl        | count
------------------+-------
 pg_foreign_table |     0
(1 row)

Есть и другие способы выполнить команды, в том числе из файлов. После выполнения команд сеанс psql будет завершен: psql < имя_файла psql -f имя_файла psql -c ‘команда’ (работает только для одной команды)

Переменные psql

По аналогии с shell, psql имеет собственные переменные. Установим переменную:

Page 16: Page 16

=> \set TEST Hi!

Чтобы подставить значение переменной, надо предварить ее имя двоеточием:

=> \echo :TEST
Hi!

Значение переменной можно сбросить:

=> \unset TEST
=> \echo :TEST
:TEST

Переменные можно использовать, например, для хранения текста часто используемых запросов. Вот запрос на получение списка пяти самых больших по размеру таблиц:

=> \set top5 'SELECT tablename, pg_total_relation_size(schemaname||''.''||tablename) AS bytes FROM pg_tables ORDER BY bytes DESC LIMIT 5;'

Для выполнения запроса достаточно набрать:

=> :top5
   tablename    |  bytes
----------------+---------
 pg_proc        | 1245184
 pg_rewrite     |  745472
 pg_attribute   |  720896
 pg_description |  630784
 pg_statistic   |  294912
(5 rows)

Присвоение значения переменной top5 удобно записать в стартовый файл .psqlrc в домашнем каталоге пользователя. Команды из .psqlrc будут автоматически выполняться каждый раз при старте psql. Без параметров \set выдает значения всех переменных, включая встроенные. Справку по встроенным переменным можно получить так:

\? variables

Выйти из psql можно с помощью команд \q[uit], quit, exit или нажав Ctrl+D:

=> \q

Page 17: Итоги

Установка PostgreSQL из готовых пакетов — предпочтительный способ установки

  • Пакетные дистрибутивы учитывают особенности ОС, которые нужно знать
    • как запускать и останавливать сервер
    • расположение файлов конфигурации
    • расположение журнала сервера
  • psql — клиент для работы с PostgreSQL

Page 18: Практика

  1. Установите для всех сеансов значение параметра work_mem равным 8 Мбайт. Обновите конфигурацию и проверьте, что изменения вступили в силу. Восстановите значение по умолчанию. 2. Запишите в файл ddl.sql команду CREATE TABLE на создание любой таблицы. Запишите в файл populate.sql команды на вставку строк в эту таблицу. Выполните оба скрипта и проверьте, что таблица создалась и в ней появились записи. 3. Найдите в журнале сервера строки за сегодняшний день.

Комментарии преподавателя

Для запуска psql в окне терминала наберите psql без параметров:

student:~$ psql

Для выполнения заданий каждой темы удобно создавать отдельную базу данных:

student/student=# CREATE DATABASE tools_overview;
CREATE DATABASE
student/student=# \c tools_overview
You are now connected to database "tools_overview" as user
"student".
student/tools_overview=#
  1. Воспользуйтесь командой ALTER SYSTEM.

  2. Если результат запроса не помещается в окно терминала, psql передает его утилите less для постраничной выдачи. Чтобы вернуться в режим ввода команд, нажмите клавишу q.

Page 19: 1. Параметры конфигурации

Подключаемся к базе данных:

student$ psql

Задаем значение параметра:

=> ALTER SYSTEM SET work_mem = '8MB';
ALTER SYSTEM

Обновляем конфигурацию:

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Проверяем значение параметра:

=> \dconfig work_mem
List of configuration parameters
 Parameter | Value
-----------+-------
 work_mem  | 8MB
(1 row)

Вернем значение по умолчанию:

=> ALTER SYSTEM RESET work_mem;
ALTER SYSTEM

Применяем изменения:

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

2. Выполнение скриптов в psql

Запишем в файл ddl.sql команду на создание таблицы с ключевыми словами PostgreSQL (для этого можно использовать и любой текстовый редактор):

student$ cat > ~/ddl.sql <<EOF
CREATE TABLE keywords (
    word text,
    category text,
    description text
);
EOF

Запишем команды для заполнения таблицы keywords в файл populate.sql:

student$ cat > ~/populate.sql <<EOF
INSERT INTO keywords
    SELECT word, catcode, catdesc FROM pg_get_keywords();
EOF

Создаем базу данных и подключаемся к ней:

=> CREATE DATABASE tools_overview;
CREATE DATABASE
=> \c tools_overview
You are now connected to database "tools_overview" as user "student".

Выполняем скрипты и проверяем записи в таблице:

Page 20: Page 20

=> \i ddl.sql
CREATE TABLE
=> \i populate.sql
INSERT 0 471
=> SELECT * FROM keywords LIMIT 10;
   word    | category | description
-----------+----------+-------------
 abort     | U        | unreserved
 absent    | U        | unreserved
 absolute  | U        | unreserved
 access    | U        | unreserved
 action    | U        | unreserved
 add       | U        | unreserved
 admin     | U        | unreserved
 after     | U        | unreserved
 aggregate | U        | unreserved
 all       | R        | reserved
(10 rows)

3. Просмотр журнала

Журнал можно открыть любым текстовым редактором. Каждая запись в журнале начинается с даты, содержит номер серверного процесса (таковы настройки журнала после установки из пакета) и может состоять из нескольких строк. Последние записи будут в конце файла.

student$ tail /var/log/postgresql/postgresql-16-main.log
2026-04-10 15:42:47.674 MSK [131116] LOG:  database system is shut down
2026-04-10 15:42:48.608 MSK [133485] LOG:  starting PostgreSQL 16.11 (Ubuntu
16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu
13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
2026-04-10 15:42:48.608 MSK [133485] LOG:  listening on IPv4 address "127.0.0.1", port
5432
2026-04-10 15:42:48.632 MSK [133485] LOG:  listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2026-04-10 15:42:48.649 MSK [133488] LOG:  database system was shut down at 2025-11-25
18:29:40 MSK
2026-04-10 15:42:48.675 MSK [133485] LOG:  database system is ready to accept connections
2026-04-10 15:42:51.592 MSK [133485] LOG:  received SIGHUP, reloading configuration files
2026-04-10 15:42:51.594 MSK [133485] LOG:  parameter "work_mem" changed to "8MB"
2026-04-10 15:42:52.216 MSK [133485] LOG:  received SIGHUP, reloading configuration files
2026-04-10 15:42:52.217 MSK [133485] LOG:  parameter "work_mem" removed from
configuration file, reset to default

Созданную нами базу можно удалить, предварительно отключившись от нее (подробности будут в модуле «Организация данных»)

=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE tools_overview;
DROP DATABASE

02 Занятие 02: Архитектура / Общее устройство PostgreSQL

Исходная страница в нативном Markdown: Архитектура / Общее устройство PostgreSQL
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_02_arch_general.html Archive: DEV1-handouts-16.zip Страниц PDF: 20

Page 1: Архитектура / Общее устройство PostgreSQL

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Клиент-серверный протокол
  • Транзакционность и механизмы ее реализации
  • Схема обработки и способы выполнения запросов
  • Процессы и структуры памяти
  • Хранение данных на диске и работа с ними
  • Расширяемость системы

Page 3: Клиент и сервер

Source slide page 3: Клиент и сервер{#source-figure-003 caption=“Source slide page 3: Клиент и сервер”}

подключение аутентификация формирование запросов выполнение запросов управление транзакциями поддержка транзакционности

Комментарии преподавателя

Клиентское приложение — например, psql или любая другая программа, написанная на любом языке программирования, — подключается к серверу и «общается» с ним. Чтобы клиент и сервер понимали друг друга, они должны использовать один и тот же протокол взаимодействия. Обычно клиент использует драйвер, реализующий протокол и предоставляющий набор функций для использования в программе. Внутри драйвер может пользоваться стандартной реализацией протокола (библиотекой libpq), либо реализовывать этот протокол самостоятельно.

Не так важно, на каком языке написан клиент — за разным синтаксисом будут стоять возможности, определенные протоколом. Мы будем использовать для примеров язык SQL с помощью клиента psql. В реальной жизни клиентскую часть на SQL пишут редко, но нам это удобно для учебных целей. Мы рассчитываем, что сопоставить команды SQL с аналогичными возможностями какого-либо другого языка программирования не составит для вас большого труда.

Протокол позволяет клиенту подключиться к одной из баз данных кластера. При этом сервер выполняет аутентификацию — решает, можно ли разрешить подключение, например, запросив пароль.

Далее клиент посылает серверу запросы на языке SQL, а сервер выполняет их и возвращает результат. Наличие мощного и удобного языка запросов — одна из особенностей реляционных СУБД.

Другая особенность — поддержка согласованной работы транзакций.

https://postgrespro.ru/docs/postgresql/16/protocol

Page 4: Транзакции

Source slide page 4: Транзакции{#source-figure-004 caption=“Source slide page 4: Транзакции”}

  • атомарность — все или ничего согласованность — ограничения целостности и пользовательские ограничения изоляция — влияние параллельных процессов
  • долговечность — сохранность данных даже после сбоя

Комментарии преподавателя

Под транзакцией понимается последовательность операций, которая сохраняет согласованность данных при условии, что операции выполнены полностью и без помех со стороны других транзакций.

От транзакций ожидают выполнения четырех свойств (ACID):

● Атомарность: транзакция либо выполняется полностью, либо не выполняется вовсе. Для этого начало транзакции отмечается командой BEGIN, а конец — либо COMMIT (фиксация изменений), либо ROLLBACK (отмена изменений).

● Согласованность: транзакция переводит базу данных из одного согласованного состояния в другое согласованное состояние (согласованность - соблюдение установленных ограничений).

● Изоляция: другие транзакции, выполняющиеся одновременно с данной, не должны оказывать на нее влияния.

● Долговечность: после того, как данные зафиксированы, они не должны потеряться даже в случае сбоя.

За управление транзакциями (то есть за определение команд, составляющих транзакцию, и за фиксацию или отмену транзакции) в PostgreSQL, как правило, отвечает клиентское приложение. На стороне сервера управлять транзакциями могут хранимые процедуры.

https://postgrespro.ru/docs/postgresql/16/sql-begin

https://postgrespro.ru/docs/postgresql/16/sql-savepoint

https://postgrespro.ru/docs/postgresql/16/transactions

Page 5: Управление транзакциями

По умолчанию psql работает в режиме автофиксации:

=> \echo :AUTOCOMMIT
on

Это приводит к тому, что любая одиночная команда, выданная без явного указания начала транзакции, сразу же фиксируется. Проверьте, включен ли аналогичный режим в драйвере PostgreSQL вашего любимого языка программирования? Создадим таблицу с одной строкой:

=> CREATE TABLE t(
  id integer,
  s text
);
CREATE TABLE
=> INSERT INTO t(id, s) VALUES (1, 'foo');
INSERT 0 1

Увидит ли таблицу и строку другая транзакция?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
(1 row)

Да. Сравните:

=> BEGIN; -- явно начинаем транзакцию
BEGIN
=> INSERT INTO t(id, s) VALUES (2, 'bar');
INSERT 0 1

Что увидит другая транзакция на этот раз?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
(1 row)

Изменения еще не зафиксированы, поэтому не видны другой транзакции.

=> COMMIT;
COMMIT

А теперь?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
(2 rows)

Режим без автофиксации неявно начинает транзакцию при первой выданной команде; изменения надо фиксировать самостоятельно.

=> \set AUTOCOMMIT off
=> INSERT INTO t(id, s) VALUES (3, 'baz');
INSERT 0 1

Page 6: Page 6

Что на этот раз?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
(2 rows)

Изменения не видны; транзакция была начата неявно.

=> COMMIT;
COMMIT

Ну и наконец:

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
(3 rows)

Восстановим режим, в котором psql работает по умолчанию.

=> \set AUTOCOMMIT on

Отдельные изменения можно откатывать, не прерывая транзакцию целиком (хотя необходимость в этом возникает нечасто).

=> BEGIN;
BEGIN
=> SAVEPOINT sp; -- точка сохранения
SAVEPOINT
=> INSERT INTO t(id, s) VALUES (4, 'qux');
INSERT 0 1
=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
  4 | qux
(4 rows)

Обратите внимание: свои собственные изменения транзакция видит, даже если они не зафиксированы. Теперь откатим все до точки сохранения. Откат к точке сохранения не подразумевает передачу управления (то есть не работает как GOTO); отменяются только те изменения состояния БД, которые были выполнены от момента установки точки до текущего момента.

=> ROLLBACK TO sp;
ROLLBACK

Что увидим?

=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
(3 rows)

Сейчас изменения отменены, но транзакция продолжается:

=> INSERT INTO t(id, s) VALUES (4, 'xyz');

Page 7: Page 7

INSERT 0 1
=> COMMIT;
COMMIT
=> SELECT * FROM t;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
  4 | xyz
(4 rows)

Page 8: Выполнение запроса

Source slide page 8: Выполнение запроса{#source-figure-008 caption=“Source slide page 8: Выполнение запроса”}

разбор ← системный каталог переписывание ← правила планирование ← статистика выполнение ← данные

Комментарии преподавателя

Выполнение запроса — довольно сложная задача. Запрос передается от клиента серверу в виде текста. Текст надо разобрать — выполнить синтаксический разбор (складываются ли буквы в слова, а слова — в команды) и семантический разбор (есть ли в базе данных таблицы и другие объекты, на которые запрос ссылается по имени). Для этого требуется информация о том, что вообще содержится в базе данных. Такая мета-информация называется системным каталогом, она хранится в самой базе данных в специальных таблицах.

Запрос может переписываться (трансформироваться) — например, вместо имени представления подставляется текст запроса. Можно придумать и свои трансформации, для чего есть механизм правил.

SQL — декларативный язык: запрос, составленный на нем, говорит о том, какие данные надо получить, но не говорит, как это сделать. Поэтому запрос (уже разобранный и представленный в виде дерева), передается планировщику, который разрабатывает план выполнения. Например, планировщик решает, надо ли использовать индексы. Чтобы качественно спланировать работу, планировщику нужна информация о размере таблиц и о распределении данных в них — статистика.

Далее запрос выполняется в соответствии с планом и результат возвращается клиенту — целиком и полностью:

https://postgrespro.ru/docs/postgresql/16/query-path

Это удобный и простой способ для небольших выборок, он обеспечи-вается простым режимом протокола.

Page 9: Подготовка операторов

Source slide page 9: Подготовка операторов{#source-figure-009 caption=“Source slide page 9: Подготовка операторов”}

разбор переписывание

  • привязка ← значения параметров планирование выполнение

Комментарии преподавателя

Итак, каждый запрос проходит перечисленные ранее шаги: разбор, переписывание, планирование и выполнение. Но если один и тот же запрос (с точностью до параметров) выполняется много раз, нет смысла каждый раз разбирать его заново.

Поэтому кроме обычного выполнения запросов протокол PostgreSQL предусматривает расширенный режим, который позволяет более детально управлять выполнением операторов.

В качестве одной из возможностей расширенный режим позволяет подготовить запрос — заранее выполнить разбор и переписывание и запомнить дерево разбора.

При выполнении запроса выполняется привязка конкретных значений параметров. Если необходимо, здесь выполняется планирование (в некоторых случаях PostgreSQL запоминает план запроса и не выполняет повторно этот шаг). Затем запрос выполняется.

Еще одно преимущество использования подготовленных операторов — невозможность внедрения SQL-кода.

https://postgrespro.ru/docs/postgresql/16/sql-prepare

https://postgrespro.ru/docs/postgresql/16/sql-execute

Page 10: Подготовленные операторы

В SQL оператор подготавливается командой PREPARE (эта команда является расширением PostgreSQL, она отсутствует в стандарте):

=> PREPARE q(integer) AS
  SELECT * FROM t WHERE id = $1;
PREPARE

При этом выполняются разбор и переписывание, и полученное дерево разбора запоминается. После подготовки оператор можно вызывать по имени, передавая фактические параметры:

=> EXECUTE q(1);
 id |  s
----+-----
  1 | foo
(1 row)

Если у запроса нет параметров, при подготовке запоминается и построенный план выполнения. Если же параметры есть, как в этом примере, то их фактические значения принимаются во внимание при планировании. Планировщик может счесть, что план, построенный без учета параметров, окажется не хуже, и тогда перестанет выполнять планирование повторно. А как подготовить и выполнить оператор в вашем любимом языке? Есть ли возможность выполнить оператор, НЕ подготавливая его? Все подготовленные операторы текущего сеанса можно увидеть в представлении:

=> SELECT * FROM pg_prepared_statements \gx
-[ RECORD 1 ]---+---------------------------------
name            | q
statement       | PREPARE q(integer) AS           +
                |   SELECT * FROM t WHERE id = $1;
prepare_time    | 2026-04-10 15:33:49.332002+03
parameter_types | {integer}
result_types    | {integer,text}
from_sql        | t
generic_plans   | 0
custom_plans    | 1

Page 11: Курсоры

Source slide page 11: Курсоры{#source-figure-011 caption=“Source slide page 11: Курсоры”}

разбор переписывание

  • привязка ← значения параметров планирование выполнение
  • получение результата

Комментарии преподавателя

Не всегда клиенту бывает удобно получить все результаты сразу. Данных может оказаться много, но не все они могут быть нужны.

Для этого расширенный режим протокола предусматривает курсоры. Можно открыть курсор для какого-либо оператора, а затем получать результирующие данные построчно по мере необходимости.

Курсор можно рассматривать как окно, в котором видна только часть из множества результатов. При получении строки данных окно сдвигается. Иными словами, курсоры позволяют работать с реляционными данными (множествами) итеративно, строка за строкой.

Открытый курсор представлен на сервере так называемым порталом. Это слово встречается в документации; в первом приближении можно считать «курсор» и «портал» синонимами.

Запрос, используемый в курсоре, неявно подготавливается (то есть сохраняется его дерево разбора и, возможно, план выполнения).

https://postgrespro.ru/docs/postgresql/16/sql-declare

https://postgrespro.ru/docs/postgresql/16/sql-fetch

Page 12: Курсоры

При выполнении команды SELECT сервер передает, а клиент получает сразу все строки:

=> SELECT * FROM t ORDER BY id;
 id |  s
----+-----
  1 | foo
  2 | bar
  3 | baz
  4 | xyz
(4 rows)

Курсор позволяет получать данные построчно.

=> BEGIN;
BEGIN
=> DECLARE c CURSOR FOR
  SELECT * FROM t ORDER BY id;
DECLARE CURSOR
=> FETCH c;
 id |  s
----+-----
  1 | foo
(1 row)

Размер выборки можно указывать:

=> FETCH 2 c;
 id |  s
----+-----
  2 | bar
  3 | baz
(2 rows)

Этот размер играет важную роль, когда строк очень много: обрабатывать большой объем данных построчно крайне неэффективно. Что, если в процессе чтения мы дойдем до конца таблицы?

=> FETCH 2 c;
 id |  s
----+-----
  4 | xyz
(1 row)
=> FETCH 2 c;
 id | s
----+---
(0 rows)

FETCH просто перестанет возвращать строки. В обычных языках программирования всегда есть возможность проверить это условие. Как в вашем языке программирования получать данные построчно с помощью курсора? Есть ли возможность НЕ пользоваться курсором и получить все строки сразу? Как настраивается размер выборки для курсора? По окончании работы открытый курсор закрывают, освобождая ресурсы:

=> CLOSE c;
CLOSE CURSOR

Однако курсоры автоматически закрываются по завершению транзакции, так что можно не закрывать их явно. (Исключение составляют курсоры, открытые с указанием WITH HOLD.)

Page 13: Page 13

=> COMMIT;
COMMIT

Page 14: Процессы и память

Source slide page 14: Процессы и память{#source-figure-014 caption=“Source slide page 14: Процессы и память”}

Комментарии преподавателя

Между обращениями клиента сервер должен хранить вспомогательную информацию: разобранные запросы и их планы, состояние открытых курсоров (порталы). Где и как он это делает?

Сервер PostgreSQL состоит из нескольких взаимодействующих процессов. В первую очередь при старте сервера запускается процесс, традиционно называемый postmaster. Он запускает все остальные процессы (с помощью системного вызова fork в Unix) и «присматривает» за ними — если какой-нибудь процесс завершится аварийно, postmaster перезапустит его (или перезапустит весь сервер, если сочтет, что процесс мог повредить общие данные).

Работу сервера обеспечивает ряд фоновых служебных процессов. В следующих темах этого модуля мы поговорим об основных из них.

Чтобы процессы могли обмениваться информацией, postmaster выделяет общую память, доступ к которой могут получить все процессы. Кроме общей памяти, каждый процесс имеет и свою локальную память, доступную только ему самому.

Postmaster слушает входящие соединения. Для каждого вновь подключающегося клиента postmaster порождает обслуживающий процесс (backend) и дальше клиент общается уже с этим процессом. Обслуживающий процесс, в том числе, производит аутентификацию.

Место, необходимое для выполнения запроса (разобранные запросы и их планы, состояние курсоров, кеш системного каталога, место для сортировки данных и т. п.), выделяется в локальной памяти обслуживающего процесса.

Page 15: Много клиентов

Source slide page 15: Много клиентов{#source-figure-015 caption=“Source slide page 15: Много клиентов”}

Комментарии преподавателя

Когда к серверу подключается много клиентов, для каждого из них порождается собственный обслуживающий процесс. Это не проблема, пока клиентов не очень много: на всех хватает оперативной памяти, а соединения не происходят слишком часто.

Тем не менее при одновременной работе с какими-либо объектами приходится принимать меры, чтобы один процесс не поменял какие-то данные в то время, пока с ними работает другой процесс.

Для объектов в общей памяти используются короткоживущие блокировки. PostgreSQL делает это достаточно аккуратно для того, чтобы система хорошо масштабировалась при увеличении числа процессоров (ядер).

С таблицами сложнее, поскольку блокировки нужно удерживать до конца транзакций (то есть потенциально в течение длительного времени), из-за чего масштабируемость может пострадать. Поэтому PostgreSQL использует механизм многоверсионности (MVCC, multiversion concurrency control) и изоляцию на основе снимков данных: одни и те же данные могут одновременно существовать в разных версиях, а каждый процесс видит собственную (но всегда согласованную) картину данных. Это позволяет блокировать только те процессы, которые пытаются изменить данные, уже измененные, но еще не зафиксированные другими процессами.

Многоверсионность — основной механизм, который обеспечивает первые три свойства транзакций (атомарность, согласованность, изоляция). Про него мы будем говорить отдельно в теме «Изоляция и многоверсионность».

Page 16: Пул соединений

Source slide page 16: Пул соединений{#source-figure-016 caption=“Source slide page 16: Пул соединений”}

Комментарии преподавателя

Если клиентов слишком много, или соединения устанавливаются и разрываются слишком часто, стоит подумать о применении пула соединений. Такую функцию обычно предоставляет сервер приложений или можно воспользоваться сторонними менеджерами пула (наиболее известен pgBouncer).

Клиенты подключаются не к серверу PostgreSQL, а к менеджеру пула. Менеджер удерживает открытыми несколько соединений с сервером баз данных и использует одно из свободных для того, чтобы выполнять запросы клиента. Таким образом, с точки зрения сервера число клиентов остается постоянным вне зависимости от того, сколько клиентов обращаются к менеджеру пула.

Но при таком режиме работы несколько клиентов разделяют один и тот же обслуживающий процесс, который — как мы говорили — в своей локальной памяти хранит определенное состояние (в частности, разобранные запросы для подготовленных операторов). Как правило, это необходимо учитывать при разработке приложений.

Одна из возможностей pgBouncer — временная приостановка обслуживания клиентов без разрыва соединения. Такая приостановка может использоваться для обновления программного обеспечения сервера или других операций, требующих рестарта сервера.

Подробнее вопросы применения пула соединений рассматриваются в курсе DEV2.

Page 17: Хранение данных

Source slide page 17: Хранение данных{#source-figure-017 caption=“Source slide page 17: Хранение данных”}

Комментарии преподавателя

Данные хранятся на дисках в обычных файлах операционной системы. Как именно данные распределены по файлам, рассматривается в одной из следующих тем.

Логически файлы разделены на страницы (иногда используется термин блок). Страница обычно имеет размер 8 Кбайт. Его в некоторых пределах можно поменять (16 или 32 Кбайта), но только при сборке сервера. Таким образом, собранный и запущенный кластер может работать со страницами только одного размера.

Каждая страница имеет внутреннюю разметку. Страница содержит заголовок и полезные данные; между ними может находиться свободное пространство, если страница занята не полностью.

Из-за того, что диски работают значительно медленнее, чем оперативная память (особенно HDD, но и SSD тоже), применяется кеширование: в оперативной памяти отводится место под недавно прочитанные с диска страницы — буферный кеш. Расчет делается на то, что одни и те же страницы понадобятся несколько раз и можно будет сэкономить на повторном обращении к диску. Измененные данные также записываются на диск не сразу, а через некоторое время.

Page 18: Хранение данных

Source slide page 18: Хранение данных{#source-figure-018 caption=“Source slide page 18: Хранение данных”}

Комментарии преподавателя

Буферный кеш PostgreSQL располагается в общей памяти, чтобы все процессы имели к нему доступ.

PostgreSQL работает с дисками, на которых находятся данные, не напрямую, а через операционную систему. У операционной системы тоже имеется собственный кеш данных. Поэтому, если страница не будет найдена в буферном кеше, остается шанс, что она есть в кеше ОС и обращения к диску удастся избежать.

При сбое (например, питания) содержимое оперативной памяти пропадает, при этом измененные, но еще не записанные на диск данные теряются. Это недопустимо и противоречит свойству долговечности транзакций. Поэтому в процессе работы PostgreSQL постоянно записывает специальный журнал, позволяющий повторно выполнить потерянные операции и восстановить данные в согласованном состоянии. Про буферный кеш и журнал мы будем говорить отдельно в одноименной теме.

Page 19: Расширяемость

Source slide page 19: Расширяемость{#source-figure-019 caption=“Source slide page 19: Расширяемость”}

Комментарии преподавателя

PostgreSQL спроектирован с расчетом на расширяемость. Он предоставляет возможность создавать новые типы данных на основе уже имеющихся, писать хранимые подпрограммы для обработки данных, а также обеспечивает удобный инструментарий для администрирования, мониторинга и настройки производительности.

При необходимости можно написать расширение, которое добавляет недостающий функционал. Большинство расширений можно устанавливать «на лету», без рестарта сервера. Благодаря такой архитектуре, существует большое количество расширений, которые:

● добавляют поддержку языков программирования (помимо стандартных SQL, PL/pgSQL, PL/Perl, PL/Python и PL/Tcl);

● вводят новые типы данных и операторы для работы с ними;

● создают новые типы индексов для эффективной работы с разнообразными типами данных (помимо стандартных B-деревьев, хеш-индексов, GiST, SP-GiST, GIN, BRIN);

● запускают служебные фоновые процессы для выполнения дополнительных задач;

● позволяют подключаться к внешним источникам данных;

● собирают информацию о нагрузке на систему, выполняют мониторинг и строят отчеты;

● позволяют исследовать системные структуры данных.

Подробнее расширения рассматриваются в курсах DBA2 и DEV2.

Page 20: Итоги

Сервер управляет кластером баз данных

  • Протокол позволяет клиентам подключаться к серверу, выполнять запросы и управлять транзакциями
  • Каждый клиент обслуживается своим процессом
  • Данные хранятся в файлах, обращение происходит через операционную систему
  • Кеширование как в локальной памяти (каталог, разобранные запросы), так и в общей (буферный кеш)

03 Занятие 03: Архитектура / Изоляция и многоверсионность

Исходная страница в нативном Markdown: Архитектура / Изоляция и многоверсионность
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_03_arch_mvcc_overview.html Archive: DEV1-handouts-16.zip Страниц PDF: 18

Page 1: Архитектура / Изоляция и многоверсионность

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Многоверсионность
  • Снимок данных
  • Уровни изоляции
  • Очистка и ее горизонт
  • Блокировки
  • Статус транзакций

Page 3: Многоверсионность

Source slide page 3: Многоверсионность{#source-figure-003 caption=“Source slide page 3: Многоверсионность”}

Наличие нескольких версий одной и той же строки

  • версии различаются временем действия
  • время = номер транзакции (номера выдаются по возрастанию)

Комментарии преподавателя

При одновременной работе нескольких сеансов возникает вопрос: что делать, если две транзакции одновременно обращаются к одной и той же строке? Если обе транзакции читающие, сложностей нет. Если обе пишущие — тоже (в этом случае они выстраиваются в очередь и выполняют изменения друг за другом). Самый интересный вариант — как взаимодействуют пишущая и читающая транзакции.

Простых пути два. Такие транзакции могут блокировать друг друга — но тогда страдает производительность. Либо читающая транзакция сразу может видеть изменения, сделанные пишущей транзакцией, даже если они не зафиксированы (это называется «грязным чтением») — но это очень плохо, ведь изменения могут быть отменены.

PostgreSQL идет сложным путем и использует многоверсионность — хранит несколько версий одной и той же строки. При этом пишущая транзакция работает со своей версией, а читающая видит свою.

Версии надо как-то отличать друг от друга. Для этого каждая из них хранит две отметки, определяющие «время действия» данной версии.

В качестве времени здесь используются всегда возрастающие номера транзакций (в действительности все немного сложнее, но это детали). Когда строка создается, она помечается номером транзакции, выполнившей команду INSERT. Когда удаляется — версия помечается номером транзакции, выполнившей DELETE (но физически не удаляется). UPDATE состоит из двух операций DELETE и INSERT.

https://postgrespro.ru/docs/postgresql/16/mvcc-intro

Page 4: Снимок данных

Source slide page 4: Снимок данных{#source-figure-004 caption=“Source slide page 4: Снимок данных”}

Согласованный срез на определенный момент времени

  • номер транзакции — определяет момент времени
  • список активных транзакций — чтобы не смотреть
  • на еще не зафиксированные изменения

Комментарии преподавателя

В PostgreSQL применяется изоляция на основе снимков данных.

Транзакция, обращаясь к таблице, должна видеть только одну из имеющихся версий каждой строки (или не видеть ни одной). Для этого транзакция работает со снимком данных, созданным в определенный момент времени. В снимке видны самые последние версии уже зафиксированных данных, а еще не зафиксированные данные не видны. Иными словами, от каждой строки в снимок попадает версия, соответствующая моменту создания снимка.

Снимок — это не физическая копия данных, а всего несколько чисел:

● номер последней зафиксированной транзакции на момент создания снимка;

● список активных транзакций на этот момент.

Список нужен для того, чтобы исключить из снимка изменения тех транзакций, которые начались до создания снимка, но еще не были зафиксированы.

Зная эти числа, мы всегда можем сказать, какая из версий строки будет видна в снимке. Иногда это будет актуальная (самая последняя зафиксированная) версия, как для строки 1 на иллюстрации. Иногда не самая последняя: строка 2 удалена (и изменение уже зафиксировано), но транзакция еще продолжает видеть эту строку, пока работает со своим снимком. Это правильное поведение — оно дает согласованную картину данных на выбранный момент времени.

Какие-то строки вовсе не попадут в снимок: строка 3 удалена до того, как был построен снимок, поэтому в снимке ее нет.

Page 5: Уровни изоляции

Read Uncommitted

  • не поддерживается PostgreSQL: работает как Read Committed
  • Read Committed — используется по умолчанию
    • снимок строится на момент начала оператора
    • одинаковые запросы могут каждый раз получать разные данные
  • Repeatable Read
    • снимок строится на момент начала первого оператора транзакции
    • транзакция может завершиться ошибкой сериализации
  • Serializable
    • полная изоляция, но дополнительные накладные расходы
    • транзакция может завершиться ошибкой сериализации

Комментарии преподавателя

Стандарт SQL определяет четыре уровня изоляции: чем строже уровень, тем меньше влияния оказывают параллельно работающие транзакции друг на друга. Во времена, когда стандарт принимался, считалось, что чем строже уровень, тем сложнее его реализовать и тем сильнее его влияние на производительность (с тех пор эти представления несколько изменились).

Самый нестрогий уровень Read Uncommitted допускает грязные чтения. Он не поддерживается PostgreSQL, поскольку не представляет практической ценности и не дает выигрыша в производительности.

Уровень Read Committed является уровнем изоляции по умолчанию в PostgreSQL. На этом уровне снимки данных строятся в начале выполнения каждого оператора SQL. Таким образом, оператор работает с неизменной и согласованной картиной данных, но два одинаковых запроса, следующих один за другим, могут показать разные данные.

На уровне Repeatable Read снимок строится в начале транзакции (при выполнении первого оператора) — поэтому все запросы в одной транзакции видят одни и те же данные. Этот уровень удобен, например, для отчетов, состоящих из нескольких запросов.

Уровень Serializable гарантирует полную изоляцию: можно писать операторы так, как будто транзакция работает одна. Но при этом некоторая доля транзакций будет прерываться; приложение должно уметь повторять такие транзакции.

https://postgrespro.ru/docs/postgresql/16/transaction-iso

Page 6: Видимость версий строк

Как убедиться в том, что одна и та же строка может существовать в нескольких версиях? Создадим таблицу:

=> CREATE TABLE t(s text);
CREATE TABLE

И вставим одну строку. Напомним, что если не начать транзакцию явно командой BEGIN, psql выполняет команду и немедленно фиксирует результат:

=> INSERT INTO t VALUES ('Первая версия');
INSERT 0 1

Начнем транзакцию и выведем ее номер:

=> BEGIN;
BEGIN
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                736
(1 row)

Транзакция видит первую (и пока единственную) версию строки:

=> SELECT *, xmin, xmax FROM t;
       s       | xmin | xmax
---------------+------+------
 Первая версия |  735 |    0
(1 row)

Здесь скрытые столбцы показывают номера транзакций, ограничивающих видимость версии строки: xmin — номер предыдущей транзакции, которая создала версию, а xmax=0 означает, что эта версия актуальна. Теперь начнем другую транзакцию в другом сеансе:

=> BEGIN;
BEGIN
=> SELECT pg_current_xact_id();
 pg_current_xact_id
--------------------
                737
(1 row)

Транзакция видит ту же единственную версию:

=> SELECT *, xmin, xmax FROM t;
       s       | xmin | xmax
---------------+------+------
 Первая версия |  735 |    0
(1 row)

Теперь изменим строку во второй транзакции.

=> UPDATE t SET s = 'Вторая версия';
UPDATE 1

Вот что получилось:

=> SELECT *, xmin, xmax FROM t;

Page 7: Page 7

       s       | xmin | xmax
---------------+------+------
 Вторая версия |  737 |    0
(1 row)

А что увидит первая транзакция?

=> SELECT *, xmin, xmax FROM t;
       s       | xmin | xmax
---------------+------+------
 Первая версия |  735 |  737
(1 row)

Поскольку изменение не зафиксировано, первая транзакция продолжает видеть первую версию строки. Обратите внимание на xmax — значение показывает, что в настоящий момент другая транзакция меняет строку. Вообще говоря, такое «подглядывание» нарушает изоляцию, поэтому поля xmin и xmax скрыты и в реальной работе их использовать не стоит. Теперь зафиксируем изменения.

=> COMMIT;
COMMIT

Что теперь увидит первая транзакция?

=> SELECT *, xmin, xmax FROM t;
       s       | xmin | xmax
---------------+------+------
 Вторая версия |  737 |    0
(1 row)

Теперь и первая транзакция видит вторую версию строки. После фиксации первая версия строки больше не будет видна ни в одной транзакции.

=> COMMIT;
COMMIT

Page 8: Очистка и ее горизонт

Source slide page 8: Очистка и ее горизонт{#source-figure-008 caption=“Source slide page 8: Очистка и ее горизонт”}

Горизонт один на каждую базу данных

  • Долгие транзакции могут удерживать горизонт
    • мешая тем самым вычищать неактуальные версии строк

Комментарии преподавателя

Механизм многоверсионности позволяет эффективно реализовать изоляцию на основе снимков, но в результате в табличных страницах накапливаются старые («мертвые») версии строк.

Какое-то время исторические версии нужны транзакциям, чтобы они могли работать со своим снимком данных, а в случае отката — вернуться к старым значениям. При этом для каждой базы данных существует такой номер xid, что все исторические версии, удаленные транзакциями с меньшими номерами, уже не видны ни в одном снимке. Этот номер называется горизонтом очистки.

Очистка производится группой специальных фоновых процессов; можно также выполнить ее вручную командой VACUUM.

Долгие транзакции и долго выполняющиеся запросы могут удерживать горизонт очистки, что не дает удалять накопившиеся версии строк. Если своевременно не вычищать исторические данные, таблицы и индексы будут разрастаться, занимая лишнее место на диске, а поиск в них актуальных версий строк будет замедляться.

https://postgrespro.ru/docs/postgresql/16/routine-vacuuming

Page 9: Блокировки

Блокировки строк

  • чтение никогда не блокирует строки
  • изменение строк блокирует их для изменений, но не для чтений
  • Блокировки таблиц
    • запрещают изменение или удаление таблицы, пока с ней идет работа
    • запрещают чтение таблицы при перестроении или перемещении
    • и т. п.
  • Время жизни блокировок
    • устанавливаются по мере необходимости или вручную
    • снимаются автоматически при завершении транзакции или при откате
    • к точке сохранения

Комментарии преподавателя

Что же дает многоверсионность? Она позволяет обойтись необходимым минимумом блокировок, тем самым увеличивая производительность системы.

Основные блокировки устанавливаются на уровне строк. При этом чтение никогда не блокирует ни читающие, ни пишущие транзакции. Изменение строки не блокирует ее чтение. Единственный случай, когда транзакция будет ждать освобождения блокировки — если она пытается менять строку, которая уже изменена другой, еще не зафиксированной транзакцией.

Блокировки также устанавливаются на более высоком уровне, в частности, на таблицах. Они нужны для того, чтобы никто не смог удалить таблицу, пока другие транзакции читают из нее данные, или чтобы запретить доступ к перестраиваемой таблице. На практике удаление и перестроение таблиц выполняются редко, поэтому обычно не вызывают проблем. Однако нужно учитывать, что перестроение таблицы происходит неявно при некоторых изменениях ее структуры и полностью блокирует доступ к таблице и ее индексам. Подробно блокировки рассматриваются в модуле «Блокировки» курса DBA2.

Все необходимые блокировки устанавливаются автоматически и автоматически же снимаются при завершении транзакции. Но если блокировка была получена после установки точки сохранения, она будет снята немедленно в случае отката к этой точке.

Можно также установить и дополнительные пользовательские блокировки; необходимость в этом возникает не часто.

https://postgrespro.ru/docs/postgresql/16/explicit-locking

Page 10: Блокировки

Повторим наш опыт, но теперь пусть обе транзакции попытаются изменить одну и ту же строку.

=> BEGIN;
BEGIN
=> UPDATE t SET s = 'Третья версия' RETURNING *;
       s
---------------
 Третья версия
(1 row)
UPDATE 1

И во второй транзакции:

=> BEGIN;
BEGIN
=> UPDATE t SET s = 'Четвертая версия' RETURNING *;

Вторая транзакция «повисла»: она не может изменить строку, пока первая транзакция не снимет блокировку.

=> COMMIT;
COMMIT

Теперь вторая транзакция может продолжить выполнение:

        s
------------------
 Четвертая версия
(1 row)
UPDATE 1
=> COMMIT;
COMMIT

Обе транзакции зафиксировали свои изменения. Первый сеанс снова читает таблицу и видит актуальную строку — это результат, зафиксированный второй транзакцией:

=> SELECT * FROM t;
        s
------------------
 Четвертая версия
(1 row)

Page 11: Статус транзакций

Статус транзакций (clog)

  • служебная информация; два бита на транзакцию
  • специальные файлы на диске
  • буферы в общей памяти
  • Фиксация
    • устанавливается бит «транзакция зафиксирована»
  • Обрыв
    • устанавливается бит «транзакция прервана»
    • выполняется так же быстро, как и фиксация (не нужен откат данных)

Комментарии преподавателя

Для работы многоверсионности серверу надо понимать, в каком статусе находятся транзакции. Транзакция может быть активна или завершена. Завершиться транзакция может либо фиксацией, либо обрывом. Таким образом, для представления состояния каждой транзакции требуются два бита.

Статусы транзакций (commit log, clog) хранятся в специальных служебных файлах в каталоге PGDATA/pg_xact, а работа с ними происходит в общей памяти сервера, чтобы не приходилось постоянно обращаться к диску.

При любом завершении транзакции (как успешном, так и неуспешном) необходимо всего лишь установить соответствующие биты статуса. И фиксация, и обрыв транзакций происходят одинаково быстро.

Если прерванная транзакция успела создать новые версии строк, эти версии не уничтожаются (не происходит «физического» отката данных). Благодаря информации о статусах другие транзакции увидят, что транзакция, создавшая или удалившая версии строк, была прервана и не станут принимать во внимание сделанные ей изменения.

Page 12: Итоги

В файлах данных могут храниться несколько версий каждой строки

  • Транзакции работают со снимками данных — согласованными срезами на определенный момент времени
  • Уровни изоляции отличаются временем создания снимка
  • Мертвые версии строк за горизонтом очистки должны периодически вычищаться
  • Писатели не блокируют читателей, читатели не блокируют никого

Page 13: Практика

  1. Создайте таблицу с одной строкой. Начните первую транзакцию на уровне изоляции Read Committed и выполните запрос к таблице. Во втором сеансе удалите строку и зафиксируйте изменения. Сколько строк увидит первая транзакция, выполнив тот же запрос повторно? Проверьте. Завершите первую транзакцию. 2. Повторите то же самое, но пусть теперь транзакция работает на уровне изоляции Repeatable Read: BEGIN ISOLATION LEVEL REPEATABLE READ;
  • Объясните отличия.

Page 14: 1. Уровень изоляции Read Committed

Создаем таблицу:

=> CREATE TABLE t(n integer);
CREATE TABLE
=> INSERT INTO t VALUES (42);
INSERT 0 1

Запрос из первой транзакции (по умолчанию используется уровень изоляции Read Committed):

=> BEGIN;
BEGIN
=> SELECT * FROM t;
 n
----
 42
(1 row)

Удаляем строку во второй транзакции и фиксируем изменения:

=> DELETE FROM t;
DELETE 1

Повторим запрос:

=> SELECT * FROM t;
 n
---
(0 rows)

Первая транзакция видит произошедшие изменения: строка удалена.

=> COMMIT;
COMMIT

2. Уровень изоляции Repeatable Read

Вернем строку:

=> INSERT INTO t VALUES (42);
INSERT 0 1

Запрос из первой транзакции:

=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT * FROM t;
 n
----
 42
(1 row)

Удаляем строку во второй транзакции и фиксируем изменения:

=> DELETE FROM t;
DELETE 1

Повторим запрос:

=> SELECT * FROM t;

Page 15: Page 15

 n
----
 42
(1 row)

На этом уровне изоляции первая транзакция не видит изменений: для нее строка по-прежнему существует.

=> COMMIT;
COMMIT

Page 16: Практика+

  1. Начните транзакцию и создайте новую таблицу с одной строкой. Не завершая транзакцию, откройте второй сеанс и выполните в нем запрос к таблице. Проверьте, что увидит транзакция во втором сеансе.
  • Зафиксируйте транзакцию в первом сеансе и повторите
  • запрос к таблице во втором сеансе.
    1. Повторите задание 1, но откатите, а не зафиксируйте транзакцию в первом сеансе. Что изменилось?
    1. В первом сеансе начните транзакцию и выполните запрос к созданной ранее таблице. Получится ли удалить эту таблицу во втором сеансе, пока первая транзакция не завершена? Проверьте.

Page 17: 1. Транзакции и команды DDL — фиксация

Начнем транзакцию и создадим новую таблицу:

=> BEGIN;
BEGIN
=> CREATE TABLE t1(n integer);
CREATE TABLE
=> INSERT INTO t1 VALUES (42);
INSERT 0 1

Во втором сеансе сделаем запрос к таблице:

=> SELECT * FROM t1;
ERROR:  relation "t1" does not exist
LINE 1: SELECT * FROM t1;
                      ^

Пока создавшая таблицу транзакция не завершена, все остальные транзакции таблицу не видят. Таблица будет видна только после завершения создавшей ее транзакции:

=> COMMIT;
COMMIT
=> SELECT * FROM t1;
 n
----
 42
(1 row)

2. Транзакции и команды DDL — откат

Снова начнем транзакцию и создадим новую таблицу:

=> BEGIN;
BEGIN
=> CREATE TABLE t2(n integer);
CREATE TABLE
=> INSERT INTO t2 VALUES (42);
INSERT 0 1

Запрос во втором сеансе ожидаемо не видит изменений:

=> SELECT * FROM t2;
ERROR:  relation "t2" does not exist
LINE 1: SELECT * FROM t2;
                      ^

При откате первой транзакции команда создания таблицы тоже откатывается:

=> ROLLBACK;
ROLLBACK
=> SELECT * FROM t2;
ERROR:  relation "t2" does not exist
LINE 1: SELECT * FROM t2;
                      ^

Команды DDL в PostgreSQL являются транзакционными.

3. Блокировки таблиц

Page 18: Page 18

Начав транзакцию, обратимся к таблице:

=> BEGIN;
BEGIN
=> SELECT * FROM t1;
 n
----
 42
(1 row)

При попытке удалить таблицу вторая транзакция будет заблокирована — нельзя удалить таблицу, которая используется.

=> DROP TABLE t1;

Таблица будет удалена только после завершения первой транзакции:

=> COMMIT;
COMMIT
DROP TABLE

04 Занятие 04: Архитектура / Буферный кеш и журнал

Исходная страница в нативном Markdown: Архитектура / Буферный кеш и журнал
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_04_arch_wal_overview.html Archive: DEV1-handouts-16.zip Страниц PDF: 17

Page 1: Архитектура / Буферный кеш и журнал

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Устройство буферного кеша
  • Алгоритм вытеснения
  • Журнал предзаписи
  • Контрольная точка
  • Процессы, связанные с буферным кешем и журналом

Page 3: Буферный кеш

Source slide page 3: Буферный кеш{#source-figure-003 caption=“Source slide page 3: Буферный кеш”}

Массив буферов

  • страница данных (8 Кбайт)
  • доп. информация
  • «Грязные» буферы
    • асинхронная запись
  • Блокировки в памяти
    • для совместного доступа

Комментарии преподавателя

Буферный кеш используется для сглаживания скорости работы оперативной памяти и дисков. Он состоит из массива буферов, которые содержат страницы данных и дополнительную информацию (например, имя файла и положение страницы внутри этого файла).

Размер страницы обычно составляет 8 Кбайт; размер можно изменить только при сборке PostgreSQL.

Любая работа со страницами данных проходит через буферный кеш. Если какой-либо процесс собирается работать со страницей, он в первую очередь пытается найти ее в кеше. Если страницы нет, он обращается к операционной системе с просьбой прочитать эту страницу и помещает ее в буферный кеш. (Обратите внимание, что ОС может прочитать страницу с диска, а может обнаружить ее в собственном кеше.)

После того, как страница записана в буферный кеш, к ней можно обращаться многократно без накладных расходов на вызовы ОС.

Если процесс изменил данные в странице, соответствующий буфер становится «грязным». Измененная страница подлежит записи на диск, но по соображениям производительности запись происходит асинхронно и может откладываться.

Буферный кеш, как и другие структуры общей памяти, защищен блокировками для управления одновременным доступом. Хотя блокировки и реализованы эффективно, доступ к буферному кешу далеко не так быстр, как простое обращение к оперативной памяти. Поэтому в общем случае чем меньше данных читает и изменяет запрос, тем быстрее он будет работать.

Page 4: Вытеснение

Source slide page 4: Вытеснение{#source-figure-004 caption=“Source slide page 4: Вытеснение”}

Вытеснение редко используемых страниц

  • грязный буфер
  • записывается на диск
  • на освободившееся место
  • читается другая страница

Комментарии преподавателя

Размер буферного кеша обычно не так велик, чтобы база данных помещалась в него целиком. Его ограничивают и доступная оперативная память, и возрастающие при его увеличении накладные расходы. Поэтому при чтении очередной страницы рано или поздно окажется, что место в буферном кеше закончилось. В этом случае применяется вытеснение страниц.

Алгоритм вытеснения выбирает в кеше страницу, которая в последнее время использовалась реже других. Если выбранный буфер оказался грязным, страница записывается на диск, чтобы не потерять сделанные в ней изменения. Затем в освободившийся буфер читается новая страница.

Такой алгоритм вытеснения называется LRU — Least Recently Used. Он сохраняет в кеше данные, с которыми происходит активная работа. Таких «горячих» данных обычно не слишком много, и при достаточном объеме буферного кеша получается существенно сократить количество обращений к ОС (и дисковых операций).

Page 5: Влияние буферного кеша на выполнение запросов

Создаем новую БД в кластере и подключаемся к ней (подробнее про базы данных — в модуле «Организация данных»):

=> CREATE DATABASE arch_wal_overview;
CREATE DATABASE
=> \c arch_wal_overview
You are now connected to database "arch_wal_overview" as user "student".

Создадим таблицу:

=> CREATE TABLE t(n integer);
CREATE TABLE

Заполним таблицу некоторым количеством строк:

=> INSERT INTO t SELECT id FROM generate_series(1,100_000) AS id;
INSERT 0 100000

Размер буферного кеша показывает параметр shared_buffers:

=> SHOW shared_buffers;
 shared_buffers
----------------
 128MB
(1 row)

Значение по умолчанию слишком мало; в любой реальной системе его требуется увеличить сразу после установки сервера (изменение требует перезапуска). Теперь перезапустим сервер, чтобы содержимое буферного кеша сбросилось.

=> \q
student$ sudo pg_ctlcluster 16 main restart
student$ psql arch_wal_overview

Сравним, что происходит при первом и при втором выполнении одного и того же запроса. В этом курсе мы не рассматриваем подробно планы запросов, но иногда будем в них заглядывать. Сейчас мы воспользуемся командой EXPLAIN ANALYZE, которая выполняет запрос и выводит не только план выполнения, но и дополнительную информацию:

=> EXPLAIN (analyze, buffers, costs off, timing off)
SELECT * FROM t;
                 QUERY PLAN
--------------------------------------------
 Seq Scan on t (actual rows=100000 loops=1)
   Buffers: shared read=443
 Planning:
   Buffers: shared hit=12 read=8 dirtied=1
 Planning Time: 0.266 ms
 Execution Time: 20.084 ms
(6 rows)

Строка «Buffers: shared» показывает использование буферного кеша. read — количество буферов, в которые пришлось прочитать страницы с диска.

=> EXPLAIN (analyze, buffers, costs off, timing off)
SELECT * FROM t;
                 QUERY PLAN
--------------------------------------------
 Seq Scan on t (actual rows=100000 loops=1)
   Buffers: shared hit=443
 Planning Time: 0.034 ms
 Execution Time: 7.612 ms
(4 rows)

Page 6: Page 6

hit — количество буферов, в которых нашлись нужные для запроса страницы. Обратите внимание, что во второй раз уменьшилось и время выполнения запроса, и время его планирования (потому что таблицы системного каталога тоже кешируются).

Page 7: Журнал предзаписи (WAL)

Проблема: при сбое теряются данные из оперативной памяти, не записанные на диск

  • Журнал
    • поток информации о выполняемых действиях,
    • позволяющий повторно выполнить потерянные при сбое операции
    • запись попадает на диск раньше, чем измененные данные
  • Журнал защищает
    • страницы таблиц, индексов и других объектов
    • статус транзакций (clog)
  • Журнал не защищает
    • временные и нежурналируемые таблицы

Комментарии преподавателя

Наличие буферного кеша (и других буферов в оперативной памяти) увеличивает производительность, но уменьшает надежность. В случае сбоя в СУБД содержимое буферного кеша потеряется. Если сбой произойдет в операционной системе или на аппаратном уровне, то пропадет содержимое и буферов ОС (но с этим справляется сама операционная система).

Для обеспечения надежности PostgreSQL использует журналирование. При выполнении любой операции формируется запись, содержащая минимально необходимую информацию для того, чтобы операцию можно было выполнить повторно. Такая запись должна попасть на диск (или другой энергонезависимый накопитель) раньше, чем будут записаны изменяемые операцией данные (поэтому журнал и называется журналом предзаписи, write-ahead log).

Файлы журнала располагаются в каталоге PGDATA/pg_wal.

Журнал защищает все объекты, работа с которыми ведется в оперативной памяти: таблицы, индексы и другие объекты, статус транзакций.

В журнал не попадают данные о временных таблицах (такие таблицы доступны только создавшему их пользователю и только на время сеанса или транзакции) и о нежурналируемых таблицах (такие таблицы ничем не отличаются от обычных, кроме того, что не защищены журналом). В случае сбоя нежурналируемые таблицы просто очищаются, зато работа с ними происходит быстрее.

https://postgrespro.ru/docs/postgresql/16/wal-intro

Page 8: Журнал предзаписи

Логически журнал можно представить в виде непрерывного потока записей. Каждая запись имеет номер, называемый LSN (Log Sequence Number). Это 64-разрядное число — смещение записи в байтах относительно начала журнала. Текущую позицию показывает функция pg_current_wal_lsn:

=> SELECT pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 0/2378B68
(1 row)

Позиция записывается как два 32-разрядных числа через косую черту. Запомним это значение. Выполним теперь какие-нибудь операции и посмотрим, как изменилась позиция.

=> UPDATE t SET n = 100_001 WHERE n = 1;
UPDATE 1
=> SELECT pg_current_wal_lsn();
 pg_current_wal_lsn
--------------------
 0/237BB80
(1 row)

Интересны не абсолютные числа, а их разница, которая показывает размер сгенерированных журнальных записей в байтах:

=> SELECT '0/237BB80'::pg_lsn - '0/2378B68'::pg_lsn AS bytes;
 bytes
-------
 12312
(1 row)

Физически журнал хранится в файлах в отдельном каталоге (PGDATA/pg_wal). По умолчанию файлы имеют размер 16 Мбайт, задать другой размер можно при инициализации кластера баз данных. На файлы можно взглянуть не только средствами файловой системы, но и с помощью функции:

=> SELECT * FROM pg_ls_waldir() ORDER BY name LIMIT 10;
           name           |   size   |      modification
--------------------------+----------+------------------------
 000000010000000000000002 | 16777216 | 2026-04-10 15:34:23+03
 000000010000000000000003 | 16777216 | 2026-04-10 15:34:17+03
(2 rows)

Page 9: Контрольная точка

Source slide page 9: Контрольная точка{#source-figure-009 caption=“Source slide page 9: Контрольная точка”}

Периодический сброс всех грязных буферов на диск

  • гарантирует попадание на диск всех изменений до контрольной точки
  • ограничивает размер журнала, необходимого для восстановления
  • Восстановление при сбое
    • начинается с последней контрольной точки
    • последовательно проигрываются записи, если изменений нет на диске

Комментарии преподавателя

Когда сервер PostgreSQL запускается после сбоя, он входит в режим восстановления. Информация на диске в это время несогласованна: изменения «горячих» страниц пропали, поскольку эти страницы все еще находились в кеше, а более поздние изменения уже были сброшены на диск.

Чтобы восстановить согласованность, PostgreSQL читает WAL и после-довательно проигрывает каждую его запись, если соответствующее изменение не попало на диск. Таким образом восстанавливается работа всех транзакций. Транзакции, запись о фиксации которых не успела попасть в журнал, считаются оборванными.

Однако объем журнала за время работы сервера мог бы достигнуть гигантских размеров. Хранить его целиком и просматривать при сбое совершенно не реально. Поэтому СУБД периодически выполняет контрольную точку (КТ): принудительно сбрасывает на диск все грязные буферы (включая буферы clog, хранящие состояние транзакций).

Собственно «точка» — это момент начала записи буферов, которые были грязными на этот момент. Но точка считается выполненной только после того, как все такие буферы записаны. Это гарантирует, что все изменения, сделанные до момента КТ, находятся на диске.

В реальных системах с большим буферным кешем КТ может сбрасывать много грязных буферов, поэтому сервер распределяет процесс записи по времени, чтобы сгладить нагрузку на ввод-вывод.

Восстановление после сбоя начинается с момента последней завершенной КТ, что позволяет хранить только файлы журнала, записанные после нее.

Page 10: Восстановление при помощи журнала

Измененные табличные страницы находятся в буферном кеше, но еще не записаны на диск. При обычной остановке сервер выполняет контрольную точку, чтобы записать все грязные страницы на диск, но мы сымитируем сбой системы, послав сигнал процессу postmaster.

student$ sudo kill -QUIT $(sudo head -n 1 /var/lib/postgresql/16/main/postmaster.pid)

При старте произойдет восстановление согласованности данных с помощью журнала. Проверим:

student$ sudo pg_ctlcluster 16 main start
student$ psql arch_wal_overview
=> SELECT min(n), max(n) FROM t;
 min |  max
-----+--------
   2 | 100001
(1 row)

Все изменения были восстановлены. PostgreSQL автоматически удаляет журнальные файлы, не требующиеся для восстановления, после выполнения контрольной точки.

Page 11: Производительность

Source slide page 11: Производительность{#source-figure-011 caption=“Source slide page 11: Производительность”}

Синхронный режим

  • запись при фиксации
  • обслуживающий процесс
  • Асинхронный режим
    • фоновая запись
    • walwriter

Комментарии преподавателя

Механизм журналирования более эффективен, чем работа напрямую с диском без буферного кеша. Во-первых, размер журнальных записей меньше, чем размер целой страницы данных; во-вторых, журнал записывается строго последовательно (и обычно не читается, пока не случится сбой), с чем вполне справляются простые HDD-диски.

На эффективность можно также влиять настройкой. Если запись происходит сразу (синхронно), то гарантируется, что зафиксированная транзакция не пропадет. Но запись — довольно дорогая операция, в течение которой обслуживающий процесс, выполняющий фиксацию, вынужден ждать. Чтобы журнальная запись не «застряла» в кеше операционной системы, выполняется системный вызов fsync: PostgreSQL полагается на то, что это гарантирует попадание данных на энергонезависимый носитель.

Поэтому есть и режим отложенной (асинхронной) записи. В этом случае записи пишутся фоновым процессом walwriter постепенно, с небольшой задержкой. Надежность уменьшается, зато производительность увеличивается. Но и в этом случае после сбоя гарантируется восстановление согласованности.

На самом деле оба режима работают совместно. Журнальные записи долгой транзакции будут записываться асинхронно (чтобы освободить буферы WAL). А если при сбросе страницы данных окажется, что соответствующая журнальная запись еще не на диске, она тут же будет записана в синхронном режиме.

Page 12: Основные процессы

Source slide page 12: Основные процессы{#source-figure-012 caption=“Source slide page 12: Основные процессы”}

Запись журнала

  • Контрольная точка
    • сброс всех
    • грязных буферов
  • Фоновая запись
    • сброс части
    • грязных буферов
  • Обслуживающие процессы
    • сброс вытесняемого
    • грязного буфера

Комментарии преподавателя

Вернемся еще раз к процессам, связанным с обслуживанием буферного кеша и журнала.

Во-первых, это процесс walwriter, занимающийся асинхронной записью журнала на диск. При синхронном режиме записью журнала занимается тот процесс, который выполняет фиксацию транзакции.

Во-вторых, процесс контрольной точки checkpointer, периодически сбрасывающий все грязные буферы на диск.

В-третьих, процесс фоновой записи background writer (или bgwriter). Этот процесс похож на процесс контрольной точки, но записывает только часть грязных буферов, причем те, которые с большой вероятностью будут вытеснены в ближайшее время. Таким образом, когда обслуживающий процесс выберет буфер, чтобы прочитать новую страницу, старая страница скорее всего уже не будет грязной и не надо будет тратить время, чтобы сбросить ее на диск.

И в-четвертых, обслуживающие процессы, читающие данные в буферный кеш. Если, несмотря на работу процессов контрольной точки и фоновой записи, вытесняемый буфер окажется грязным, обслуживающий процесс самостоятельно запишет его на диск.

Page 13: Уровни журнала

Minimal

  • гарантия восстановления после сбоя
  • Replica (по умолчанию)
    • резервное копирование
    • репликация: передача и проигрывание журнала на другом сервере
  • Logical
    • логическая репликация: информация о добавлении, изменении
    • и удалении табличных строк

Комментарии преподавателя

Как уже говорилось, причиной появления журнала является необходимость защищать информацию от сбоев из-за потери содержимого оперативной памяти.

Однако журнал — механизм, который оказалось удобно применять и для других целей, если добавить в него дополнительную информацию.

Объем данных, который попадает в журнал, регулируется параметром wal_level.

● На уровне minimal журнал обеспечивает только восстановление после сбоя.

● На уровне replica в журнал добавляется информация, позволяющая использовать его для создания резервных копий и репликации. При репликации журнальные записи передаются на другой сервер и применяются там; таким образом создается и поддерживается точная копия (реплика) основного сервера.

● На уровне logical в журнал добавляется информация, позволяющая декодировать «физические» журнальные записи и сформировать из них «логические» записи о добавлении, изменении и удалении табличных строк. Это позволяет организовать логическую репликацию (рассматривается в соответствующих темах курсов DEV2 и DBA3).

Page 14: Итоги

Буферный кеш существенно ускоряет работу, уменьшая число дисковых операций

  • Надежность обеспечивается журналированием
  • Размер журнала ограничен благодаря контрольным точкам
  • Журнал удобен и используется во многих случаях
    • для восстановления после сбоя
    • при резервном копировании
    • для репликации между серверами

Page 15: Практика

  1. Средствами операционной системы найдите процессы, отвечающие за работу буферного кеша и журнала WAL. 2. Остановите PostgreSQL в режиме fast; снова запустите его. Просмотрите журнал сообщений сервера. 3. Теперь остановите в режиме immediate и снова запустите. Просмотрите журнал сообщений сервера и сравните с предыдущим разом.

Комментарии преподавателя

  1. Для останова в режиме fast используйте команду
pg_ctlcluster 16 main stop

При этом сервер обрывает все открытые соединения и перед выключением выполняет контрольную точку, чтобы на диск записались согласованные данные. Таким образом, выключение может выполняться относительно долго, но при запуске сервер сразу же будет готов к работе.

  1. Для останова в режиме immediate используйте команду
pg_ctlcluster 16 main stop -m immediate --skip-systemctl-redirect

При этом сервер также обрывает открытые соединения, но не выполняет контрольную точку. На диске остаются несогласованные данные, как после сбоя. Таким образом, выключение происходит быстро, но при запуске сервер должен будет восстановить согласованность данных с помощью журнала.

Для PostgreSQL, собранного из исходных кодов, останов в режиме fast выполняется командой

pg_ctl stop

а останов в режиме immediate — командой

pg_ctl stop -m immediate

Page 16: 1. Процессы операционной системы

Сначала получим идентификатор процесса postmaster. Он записан в первой строке файла postmaster.pid. Этот файл расположен в каталоге с данными и создается каждый раз при старте сервера.

student$ sudo cat /var/lib/postgresql/16/main/postmaster.pid
135904
/var/lib/postgresql/16/main
1775825008
5432
/var/run/postgresql
localhost
  1343416     32816
ready

Теперь найдем все процессы, порожденные процессом postmaster:

student$ sudo ps -o pid,command --ppid 135904
    PID COMMAND
 135905 postgres: 16/main: checkpointer
 135906 postgres: 16/main: background writer
 135908 postgres: 16/main: walwriter
 135909 postgres: 16/main: autovacuum launcher
 135910 postgres: 16/main: logical replication launcher
 135953 postgres: 16/main: student student [local] idle

К процессам, обслуживающим буферный кеш и журнал, можно отнести: checkpointer; background writer; walwriter.

2. Остановка в режиме fast

Чтобы легко отделить старые сообщения от новых, очистим журнал сообщений перед перезапуском сервера. Конечно, в реальной работе так поступать не следует.

student$ sudo -u postgres truncate -cs0 /var/log/postgresql/postgresql-16-main.log
student$ sudo pg_ctlcluster 16 main stop
student$ sudo pg_ctlcluster 16 main start

Журнал сообщений сервера:

student$ cat /var/log/postgresql/postgresql-16-main.log
2026-04-10 15:43:32.541 MSK [135904] LOG:  received fast shutdown request
2026-04-10 15:43:32.562 MSK [135904] LOG:  aborting any active transactions
2026-04-10 15:43:32.562 MSK [135953] student@student FATAL:  terminating connection due
to administrator command
2026-04-10 15:43:32.582 MSK [135904] LOG:  background worker "logical replication
launcher" (PID 135910) exited with exit code 1
2026-04-10 15:43:32.582 MSK [135905] LOG:  shutting down
2026-04-10 15:43:32.595 MSK [135905] LOG:  checkpoint starting: shutdown immediate
2026-04-10 15:43:32.745 MSK [135905] LOG:  checkpoint complete: wrote 5 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.034 s, sync=0.012 s, total=0.163 s;
sync files=4, longest=0.004 s, average=0.003 s; distance=0 kB, estimate=0 kB;
lsn=0/1921F18, redo lsn=0/1921F18
2026-04-10 15:43:32.754 MSK [135904] LOG:  database system is shut down
2026-04-10 15:43:33.236 MSK [136206] LOG:  starting PostgreSQL 16.11 (Ubuntu
16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu
13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
2026-04-10 15:43:33.237 MSK [136206] LOG:  listening on IPv4 address "127.0.0.1", port
5432
2026-04-10 15:43:33.254 MSK [136206] LOG:  listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2026-04-10 15:43:33.268 MSK [136209] LOG:  database system was shut down at 2026-04-10
15:43:32 MSK
2026-04-10 15:43:33.281 MSK [136206] LOG:  database system is ready to accept connections

3. Остановка в режиме immediate

student$ sudo -u postgres truncate -cs0 /var/log/postgresql/postgresql-16-main.log

Page 17: Page 17

student$ sudo pg_ctlcluster 16 main stop -m immediate --skip-systemctl-redirect
student$ sudo pg_ctlcluster 16 main start

Журнал сообщений сервера:

student$ cat /var/log/postgresql/postgresql-16-main.log
2026-04-10 15:43:35.919 MSK [136206] LOG:  received immediate shutdown request
2026-04-10 15:43:35.937 MSK [136206] LOG:  database system is shut down
2026-04-10 15:43:36.410 MSK [136394] LOG:  starting PostgreSQL 16.11 (Ubuntu
16.11-1.pgdg24.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu
13.3.0-6ubuntu2~24.04) 13.3.0, 64-bit
2026-04-10 15:43:36.410 MSK [136394] LOG:  listening on IPv4 address "127.0.0.1", port
5432
2026-04-10 15:43:36.432 MSK [136394] LOG:  listening on Unix socket
"/var/run/postgresql/.s.PGSQL.5432"
2026-04-10 15:43:36.447 MSK [136397] LOG:  database system was interrupted; last known up
at 2026-04-10 15:43:33 MSK
2026-04-10 15:43:46.246 MSK [136397] LOG:  database system was not properly shut down;
automatic recovery in progress
2026-04-10 15:43:46.255 MSK [136397] LOG:  invalid record length at 0/1921F90: expected
at least 24, got 0
2026-04-10 15:43:46.255 MSK [136397] LOG:  redo is not required
2026-04-10 15:43:46.273 MSK [136395] LOG:  checkpoint starting: end-of-recovery immediate
wait
2026-04-10 15:43:46.304 MSK [136395] LOG:  checkpoint complete: wrote 3 buffers (0.0%); 0
WAL file(s) added, 0 removed, 0 recycled; write=0.013 s, sync=0.005 s, total=0.035 s;
sync files=2, longest=0.003 s, average=0.003 s; distance=0 kB, estimate=0 kB;
lsn=0/1921F90, redo lsn=0/1921F90
2026-04-10 15:43:46.316 MSK [136394] LOG:  database system is ready to accept connections

Перед тем, как начать принимать соединения, СУБД выполнила восстановление (automatic recovery in progress).

05 Занятие 05: Организация данных / Логическая структура

Исходная страница в нативном Markdown: Организация данных / Логическая структура
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_05_data_logical.html Archive: DEV1-handouts-16.zip Страниц PDF: 19

Page 1: Организация данных / Логическая структура

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Базы данных и шаблоны
  • Схемы и путь поиска
  • Специальные схемы
  • Системный каталог

Page 3: Кластер баз данных

Source slide page 3: Кластер баз данных{#source-figure-003 caption=“Source slide page 3: Кластер баз данных”}

Инициализация кластера создает три базы данных

  • Новая база всегда клонируется из существующей
CREATE DATABASE

Комментарии преподавателя

Экземпляр PostgreSQL управляет несколькими базами данных — кластером. При инициализации кластера (автоматически при установке PostgreSQL либо вручную командой initdb) создаются три одинаковые базы данных. Все остальные БД, создаваемые пользователем, клонируются из какой-либо существующей.

Шаблонная БД template1 используется по умолчанию для создания новых баз данных. В нее можно добавить объекты и расширения, которые будут копироваться в каждую новую базу данных.

Шаблон template0 не должен изменяться. Он нужен как минимум в двух ситуациях. Во-первых, для восстановления БД из резервной копии, выполненной утилитой pg_dump (это рассматривается в теме «Резервное копирование. Логическое резервирование»). Во-вторых, при создании новой БД с кодировкой, отличной от указанной при инициализации кластера (подробнее обсуждается в курсе DBA2).

База данных postgres используется при подключении по умолчанию пользователем postgres. Она не является обязательной, но некоторые утилиты предполагают ее наличие, поэтому ее не рекомендуется удалять, даже если она не нужна.

https://postgrespro.ru/docs/postgresql/16/manage-ag-templatedbs

Page 4: Базы данных

Список баз данных можно получить в psql такой командой:

=> \l
                                                       List of databases
   Name    |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | ICU
Locale | ICU Rules |   Access privileges
-----------+----------+----------+-----------------+-------------+-------------+----------
--+-----------+-----------------------
 postgres  | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           |
 student   | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           |
 template0 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           | =c/postgres          +
           |          |          |                 |             |             |
  |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
  |           | =c/postgres          +
           |          |          |                 |             |             |
  |           | postgres=CTc/postgres
(4 rows)

База данных student была создана для удобства подключения одноименного пользователя. В выводе команды присутствует ряд столбцов, которые нас сейчас не интересуют. Когда мы создаем новую базу данных, она (по умолчанию) копируется из шаблона template1.

=> CREATE DATABASE data_logical;
CREATE DATABASE
=> \c data_logical
You are now connected to database "data_logical" as user "student".
=> \l
                                                         List of databases
     Name     |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | ICU
Locale | ICU Rules |   Access privileges
--------------+----------+----------+-----------------+-------------+-------------+-------
-----+-----------+-----------------------
 data_logical | student  | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           |
 postgres     | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           |
 student      | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           |
 template0    | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           | =c/postgres          +
              |          |          |                 |             |             |
     |           | postgres=CTc/postgres
 template1    | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           | =c/postgres          +
              |          |          |                 |             |             |
     |           | postgres=CTc/postgres
(5 rows)

Page 5: Схемы

Пространство имен для объектов

  • разделение объектов на логические группы
  • предотвращение конфликта имен между приложениями
  • Схема и пользователь — разные сущности
  • Специальные схемы
    • public — по умолчанию в ней создаются все объекты
    • pg_catalog — системный каталог
    • information_schema — вариант системного каталога
    • pg_temp — для временных таблиц
    • …

Комментарии преподавателя

Схемы представляют собой пространства имен для объектов БД. Они позволяют разделить объекты на логические группы для управления ими, предотвратить конфликты имен при работе нескольких пользователей или при установке приложений и расширений.

В PostgreSQL схема и пользователь — разные сущности (хотя настройки по умолчанию позволяют пользователям удобно работать с одноименными схемами).

Существует несколько специальных схем, обычно присутствующих в каждой базе данных.

Схема public используется по умолчанию для хранения объектов, если не выполнены иные настройки.

Схема pg_catalog хранит объекты системного каталога. Системный каталог — это метаинформация об объектах, принадлежащих кластеру, которая хранится в самом кластере в виде таблиц. Альтернативное представление системного каталога (определенное в стандарте SQL) дает схема information_schema.

Схема pg_temp служит для хранения временных таблиц. (На самом деле таблицы создаются в схемах pg_temp_1, pg_temp_2 и т. п. — у каждого пользователя своя схема. Но обращаются все пользователи к ней как к pg_temp.)

Есть и другие схемы, но они носят технический характер.

https://postgrespro.ru/docs/postgresql/16/ddl-schemas

Page 6: Базы и схемы кластера

Source slide page 6: Базы и схемы кластера{#source-figure-006 caption=“Source slide page 6: Базы и схемы кластера”}

Комментарии преподавателя

Схемы принадлежат базам данных, все объекты БД распределены по каким-либо схемам.

Однако несколько таблиц системного каталога хранят информацию, общую для всего кластера. Это список баз данных, список пользователей и некоторые другие сведения. Эти таблицы хранятся вне какой-либо конкретной базы данных, но при этом одинаково видны из каждой БД.

Таким образом, клиент, подключенный к какой-либо базе данных, видит в системном каталоге описание объектов не только данной базы, но и общих объектов кластера. Описание объектов других баз данных можно получить, только подключившись к ним.

Page 7: Схемы

Для вывода списка схем в psql есть специальная команда (\dn = describe namespace):

=> \dn
      List of schemas
  Name  |       Owner
--------+-------------------
 public | pg_database_owner
(1 row)

Эта команда не показывает служебные схемы. Чтобы увидеть их, нужно добавить модификатор S (он работает аналогичным образом и для многих других команд):

=> \dnS
            List of schemas
        Name        |       Owner
--------------------+-------------------
 information_schema | postgres
 pg_catalog         | postgres
 pg_toast           | postgres
 public             | pg_database_owner
(4 rows)

Про некоторые из этих схем (public, pg_catalog, information_schema) мы уже говорили; про остальные поговорим позже в других темах. Еще один полезный модификатор — знак «плюс», который выводит дополнительную информацию:

=> \dn+
                                       List of schemas
  Name  |       Owner       |           Access privileges            |      Description
--------+-------------------+----------------------------------------+--------------------
----
 public | pg_database_owner | pg_database_owner=UC/pg_database_owner+| standard public
schema
        |                   | =U/pg_database_owner                   |
(1 row)

Создадим новую схему:

=> CREATE SCHEMA special;
CREATE SCHEMA
=> \dn
       List of schemas
  Name   |       Owner
---------+-------------------
 public  | pg_database_owner
 special | student
(2 rows)

Создадим таблицу:

=> CREATE TABLE t(n integer);
CREATE TABLE

По умолчанию таблица будет создана в схеме public. Список таблиц в этой схеме можно получить командой \dt с указанием шаблона для имен схем и таблиц:

=> \dt public.*

Page 8: Page 8

        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+---------
 public | t    | table | student
(1 row)

Таблицу (как и другие объекты) можно перемещать между схемами. Поскольку речь идет о логической организации, перемещение происходит только в системном каталоге; сами данные физически остаются на месте.

=> ALTER TABLE t SET SCHEMA special;
ALTER TABLE

Что останется в схеме public?

=> \dt public.*
Did not find any relation named "public.*".

Ничего. А в special?

=> \dt special.*
        List of relations
 Schema  | Name | Type  |  Owner
---------+------+-------+---------
 special | t    | table | student
(1 row)

Таблица переместилась. Теперь к ней можно обращаться с явным указанием схемы:

=> SELECT * FROM special.t;
 n
---
(0 rows)

Но если опустить имя схемы, таблица не будет найдена:

=> SELECT * FROM t;
ERROR:  relation "t" does not exist
LINE 1: SELECT * FROM t;
                      ^

Page 9: Путь поиска

Определение схемы объекта

  • квалифицированное имя (схема.имя) явно определяет схему
  • имя без квалификатора проверяется в схемах, указанных в пути поиска
  • Путь поиска
    • определяется параметром search_path,
    • реальное значение — функция current_schemas
    • исключаются несуществующие схемы и схемы, к которым нет доступа
    • первая явно указанная в пути схема используется для создания объектов
    • схемы pg_temp и pg_catalog неявно включаются первыми,
    • если не указаны в search_path

Комментарии преподавателя

При указании объекта надо определить, о какой схеме идет речь, ведь в разных схемах могут храниться объекты с одинаковыми именами.

Если имя объекта квалифицировано именем схемы, то используется явно указанная схема. Если схема не указана явным образом, то она определяется с помощью конфигурационного параметра search_path. Этот параметр содержит путь поиска — список схем, который просматривается последовательно слева направо, при этом из него исключаются несуществующие схемы и те, к которым у пользователя нет доступа.

При создании нового объекта с именем без квалификатора для выбора целевой схемы берется первая из оставшихся в списке, а при поиске объекта в начало пути неявно добавляются:

● схема pg_catalog, чтобы всегда иметь доступ к системному каталогу

● схема pg_temp, если пользователь создавал временные объекты

Реальный путь поиска, включающий неявные схемы, возвращает вызов функции: current_schemas(true).

Можно провести аналогию между путем поиска search_path и путем PATH в операционных системах.

https://postgrespro.ru/docs/postgresql/16/runtime-config-client#GUC-SEAR CH-PATH

Page 10: Путь поиска

Путь поиска по умолчанию имеет такое значение:

=> SHOW search_path;
   search_path
-----------------
 "$user", public
(1 row)

Конструкция «$user» обозначает схему с тем же именем, что и имя текущего пользователя (в нашем случае — student). Поскольку такой схемы нет, она игнорируется. Чтобы не думать над тем, какие схемы есть, каких нет, а какие недоступны, можно воспользоваться функцией:

=> SELECT current_schemas(false);
 current_schemas
-----------------
 {public}
(1 row)

Передаваемый в функцию логический параметр управляет отображением системных схем, неявно включаемых при поиске. Мы можем увидеть, что кроме исключения несуществующей схемы PostgreSQL неявно включил в начало списка схему системного каталога:

=> SELECT current_schemas(true);
   current_schemas
---------------------
 {pg_catalog,public}
(1 row)

Установим путь поиска, например, так:

=> SET search_path = public, special;
SET

Теперь таблица будет найдена.

=> SELECT * FROM t;
 n
---
(0 rows)

Здесь мы установили конфигурационный параметр на уровне сеанса и при переподключении его значение пропадет. Устанавливать такое значение на уровне всего кластера тоже неправильно — возможно, этот путь нужен не всегда и не всем, к тому же в разных БД может быть разный набор схем. Но параметр можно установить и на уровне отдельной базы данных:

=> ALTER DATABASE data_logical SET search_path = public, special;
ALTER DATABASE

Теперь он будет устанавливаться для всех новых подключений к БД data_logical. Проверим:

=> \c data_logical
You are now connected to database "data_logical" as user "student".
=> SHOW search_path;
   search_path
-----------------
 public, special
(1 row)

Page 11: Системный каталог

Описание всех объектов кластера

  • набор таблиц в каждой базе данных (схема pg_catalog)
  • и несколько глобальных объектов кластера
  • набор представлений для удобства
  • Доступ
    • запросы SQL, специальные команды psql
  • Правила организации
    • названия таблиц начинаются с pg_
    • имена столбцов содержат трехбуквенный префикс
    • в качестве ключа используется столбец oid типа oid
    • названия объектов хранятся в нижнем регистре

Комментарии преподавателя

Системный каталог хранит метаинформацию об объектах кластера. В каждой базе данных доступен собственный набор таблиц, описывающих объекты этой конкретной БД, и нескольких таблиц, общих для всего кластера. Для удобства над таблицами также определены несколько представлений.

https://postgrespro.ru/docs/postgresql/16/catalogs

К системному каталогу можно обращаться с помощью обычных запросов SQL, а выполнение команд DDL приводит к изменению данных в системном каталоге. Кроме того, psql имеет целый ряд команд, позволяющих удобно просматривать системный каталог.

https://postgrespro.ru/docs/postgresql/16/app-psql

Все имена таблиц системного каталога начинаются с pg_, например, pg_database. Столбцы таблиц начинаются с префикса, обычно соответствующего имени таблицы, например, datname. Имена объектов хранятся в нижнем регистре, например, ‘postgres’.

Таблицы системного каталога имеют первичные ключи — как правило, это столбцы с именем oid и типом oid (object identifier, целое 32-битное число). Эти идентификаторы встречаются и в других столбцах в виде отдельных значений или массивов, обеспечивая логические связи между таблицами. Внешние ключи в системном каталоге явно не определены.

https://postgrespro.ru/docs/postgresql/16/datatype-oid

Page 12: Системный каталог

Для того, чтобы вывести информацию о любых объектах, psql (как и другие интерактивные пользовательские средства) обращается к таблицам системного каталога. Например, команда \l для получения списка баз данных кластера, обращается к таблице:

=> SELECT datname FROM pg_database;
   datname
--------------
 postgres
 student
 template1
 template0
 data_logical
(5 rows)

Мы всегда можем посмотреть, какие запросы выполняет команда:

=> \set ECHO_HIDDEN on
=> \l
********* QUERY **********
SELECT
  d.datname as "Name",
  pg_catalog.pg_get_userbyid(d.datdba) as "Owner",
  pg_catalog.pg_encoding_to_char(d.encoding) as "Encoding",
  CASE d.datlocprovider WHEN 'c' THEN 'libc' WHEN 'i' THEN 'icu' END AS "Locale Provider",
  d.datcollate as "Collate",
  d.datctype as "Ctype",
  d.daticulocale as "ICU Locale",
  d.daticurules as "ICU Rules",
  pg_catalog.array_to_string(d.datacl, E'\n') AS "Access privileges"
FROM pg_catalog.pg_database d
ORDER BY 1;
**************************
                                                         List of databases
     Name     |  Owner   | Encoding | Locale Provider |   Collate   |    Ctype    | ICU
Locale | ICU Rules |   Access privileges
--------------+----------+----------+-----------------+-------------+-------------+-------
-----+-----------+-----------------------
 data_logical | student  | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           |
 postgres     | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           |
 student      | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           |
 template0    | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           | =c/postgres          +
              |          |          |                 |             |             |
     |           | postgres=CTc/postgres
 template1    | postgres | UTF8     | libc            | en_US.UTF-8 | en_US.UTF-8 |
     |           | =c/postgres          +
              |          |          |                 |             |             |
     |           | postgres=CTc/postgres
(5 rows)

Таким образом можно исследовать системный каталог. Отключим вывод команд.

=> \set ECHO_HIDDEN off

Список схем находится в таблице:

=> SELECT nspname FROM pg_namespace;

Page 13: Page 13

      nspname
--------------------
 pg_toast
 pg_catalog
 public
 information_schema
 special
(5 rows)

А на такие объекты, как таблицы и индексы, можно посмотреть так:

=> SELECT relname, relkind, relnamespace FROM pg_class WHERE relname = 't';
 relname | relkind | relnamespace
---------+---------+--------------
 t       | r       |        16387
(1 row)

Все столбцы здесь начинаются на rel (relation, отношение). relkind — тип объекта (r — таблица, i — индекс и т. п.); relnamespace — схема. Поле relnamespace имеет тип oid; вот соответствующая строка таблицы pg_namespace:

=> SELECT oid, nspname FROM pg_namespace WHERE oid = 16387;
  oid  | nspname
-------+---------
 16387 | special
(1 row)

Для удобства преобразования между текстовым представлением и oid можно воспользоваться приведением к специальному типу-псевдониму regnamespace:

=> SELECT relname, relkind, relnamespace::regnamespace::text
FROM pg_class WHERE relname = 't';
 relname | relkind | relnamespace
---------+---------+--------------
 t       | r       | special
(1 row)

А вот как можно получить список объектов в схеме, например, pg_catalog:

=> SELECT relname, relkind FROM pg_class
WHERE relnamespace = 'pg_catalog'::regnamespace LIMIT 5;
            relname             | relkind
--------------------------------+---------
 pg_statistic                   | r
 pg_type                        | r
 pg_foreign_table               | r
 pg_proc_oid_index              | i
 pg_proc_proname_args_nsp_index | i
(5 rows)

Аналогичные reg-типы определены и для некоторых других таблиц системного каталога. Они позволяют упростить запросы и обойтись без явного соединения таблиц.

Удаление объектов

Можно ли удалить схему special?

=> DROP SCHEMA special;
ERROR:  cannot drop schema special because other objects depend on it
DETAIL:  table t depends on schema special
HINT:  Use DROP ... CASCADE to drop the dependent objects too.

Схему нельзя удалить, если в ней находятся какие-либо объекты. Сначала надо удалить или перенести их. Но можно удалить схему сразу вместе со всеми ее объектами:

=> DROP SCHEMA special CASCADE;

Page 14: Page 14

NOTICE:  drop cascades to table t
DROP SCHEMA

А что с удалением базы данных целиком? Во-первых, нельзя удалить базу, к которой вы подключены в данный момент, поэтому отключимся от нее.

=> \conninfo
You are connected to database "data_logical" as user "student" via socket in
"/var/run/postgresql" at port "5432".
=> \c postgres
You are now connected to database "postgres" as user "student".

Во-вторых, базу данных также нельзя удалить, если к ней есть активные подключения. Создадим такое подключение в отдельном сеансе и попробуем удалить ее:

=> \c data_logical
You are now connected to database "data_logical" as user "student".
=> DROP DATABASE data_logical;
ERROR:  database "data_logical" is being accessed by other users
DETAIL:  There is 1 other session using the database.

Получили ошибку. Однако можно вызвать команду удаления с параметром FORCE, тогда она будет пытаться принудительно завершить все подключения к БД, а затем удалит ее:

=> DROP DATABASE data_logical WITH (FORCE);
DROP DATABASE

Page 15: Итоги

Логически

  • кластер содержит базы данных,
  • базы данных — схемы,
  • схемы — конкретные объекты (таблицы, индексы и т. п.)
  • Базы данных создаются клонированием существующих
  • Схема объекта определяется по пути поиска
  • Полное описание содержимого кластера баз данных хранится в системном каталоге

Page 16: Практика

  1. В новой базе данных создайте схему, названную так же, как и пользователь. Создайте схему app. Создайте несколько таблиц в обеих схемах. 2. Получите в psql описание созданных схем и список всех таблиц в них. 3. Установите путь поиска так, чтобы при подключении к базе данных таблицы из обеих схем были доступны по неквалифицированному имени; приоритет должна иметь «пользовательская» схема. Проверьте правильность настройки.

Page 17: 1. База данных, схемы, таблицы

Создаем базу данных:

=> CREATE DATABASE data_logical;
CREATE DATABASE
=> \c data_logical
You are now connected to database "data_logical" as user "student".

Схемы:

=> CREATE SCHEMA student;
CREATE SCHEMA
=> CREATE SCHEMA app;
CREATE SCHEMA

Таблицы для схемы student:

=> CREATE TABLE a(s text);
CREATE TABLE
=> INSERT INTO a VALUES ('student');
INSERT 0 1
=> CREATE TABLE b(s text);
CREATE TABLE
=> INSERT INTO b VALUES ('student');
INSERT 0 1

Таблицы для схемы app:

=> CREATE TABLE app.a(s text);
CREATE TABLE
=> INSERT INTO app.a VALUES ('app');
INSERT 0 1
=> CREATE TABLE app.c(s text);
CREATE TABLE
=> INSERT INTO app.c VALUES ('app');
INSERT 0 1

2. Описание схем и таблиц

Описание схем:

=> \dn
       List of schemas
  Name   |       Owner
---------+-------------------
 app     | student
 public  | pg_database_owner
 student | student
(3 rows)

Описание таблиц:

=> \dt student.*

Page 18: Page 18

        List of relations
 Schema  | Name | Type  |  Owner
---------+------+-------+---------
 student | a    | table | student
 student | b    | table | student
(2 rows)
=> \dt app.*
        List of relations
 Schema | Name | Type  |  Owner
--------+------+-------+---------
 app    | a    | table | student
 app    | c    | table | student
(2 rows)

3. Путь поиска

С текущими настройками пути поиска видны только таблицы, находящиеся в схеме student:

=> SELECT * FROM a;
    s
---------
 student
(1 row)
=> SELECT * FROM b;
    s
---------
 student
(1 row)
=> SELECT * FROM c;
ERROR:  relation "c" does not exist
LINE 1: SELECT * FROM c;
                      ^

Изменим путь поиска на уровне базы.

=> ALTER DATABASE data_logical SET search_path = "$user",app,public;
ALTER DATABASE
=> \c
You are now connected to database "data_logical" as user "student".
=> SHOW search_path;
     search_path
----------------------
 "$user", app, public
(1 row)

Теперь видны таблицы из обеих схем, но приоритет остается за student:

=> SELECT * FROM a;
    s
---------
 student
(1 row)
=> SELECT * FROM b;
    s
---------
 student
(1 row)
=> SELECT * FROM c;

Page 19: Page 19

  s
-----
 app
(1 row)
=> select usename, application_name from pg_stat_activity where datname = 'data_logical';
 usename | application_name
---------+------------------
 student | psql
(1 row)
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE data_logical;
DROP DATABASE

06 Занятие 06: Организация данных / Физическая структура

Исходная страница в нативном Markdown: Организация данных / Физическая структура
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_06_data_physical.html Archive: DEV1-handouts-16.zip Страниц PDF: 15

Page 1: Организация данных / Физическая структура

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Табличные пространства и каталоги
  • Файлы и страницы данных
  • Слои: данные, карты видимости и свободного пространства
  • Технология TOAST

Page 3: Табличные пространства

Source slide page 3: Табличные пространства{#source-figure-003 caption=“Source slide page 3: Табличные пространства”}

Комментарии преподавателя

Табличные пространства (ТП) служат для организации физического хранения данных и определяют расположение данных в файловой системе.

Например, можно создать одно ТП на медленных дисках для архивных данных, а другое – на быстрых дисках для данных, с которыми идет активная работа.

При инициализации кластера создаются два ТП: pg_default и pg_global.

Одно и то же ТП может использоваться разными базами данных, а одна база данных может хранить данные в нескольких ТП.

При этом у каждой БД есть так называемое «ТП по умолчанию», в котором создаются все объекты базы, если явно не указать иное. В этом же ТП хранятся и объекты системного каталога. Изначально в качестве «ТП по умолчанию» используется ТП pg_default, но можно установить и другое.

ТП pg_global особенное: в нем хранятся те объекты системного каталога, которые являются общими для кластера.

Page 4: Каталоги

Source slide page 4: Каталоги{#source-figure-004 caption=“Source slide page 4: Каталоги”}

PGDATA/global/
   ...
   ...
   ...
PGDATA/base/dboid/
   ...
   NNN
   NNN.1
   NNN.2
   ...
PGDATA/pg_tblspc/tsoid
/путь-к-каталогу/ver/dboid/
   ...
   ...
   ...

Комментарии преподавателя

По сути, табличное пространство — это указание на каталог, в котором располагаются данные. Стандартные ТП pg_global и pg_default всегда находятся в PGDATA/global/ и PGDATA/base/ соответственно. При создании пользовательского ТП указывается произвольный каталог; для собственного удобства PostgreSQL создает на него символьную ссылку в каталоге PGDATA/pg_tblspc/.

Внутри каталога PGDATA/base/ данные дополнительно разложены по подкаталогам баз данных (для PGDATA/global/ это не требуется, так как данные в нем относятся к кластеру в целом).

Внутри каталога пользовательского ТП появляется еще один уровень вложенности: версия сервера PostgreSQL. Это сделано для удобства обновления сервера на другую версию.

Собственно объекты хранятся в файлах внутри этих каталогов — каждый объект размещается в одном или нескольких файлах.

Каждый такой файл, называемый сегментом, занимает по умолчанию не более 1 Гбайт (этот размер можно изменить при сборке сервера). В том числе поэтому каждому объекту может соответствовать несколько файлов. Необходимо учитывать влияние потенциально большого количества файлов на используемую файловую систему.

https://postgrespro.ru/docs/postgresql/16/storage-file-layout

Page 5: Использование табличных пространств

Изначально в кластере присутствуют два табличных пространства. Информация о них содержится в системном каталоге:

=> SELECT spcname FROM pg_tablespace;
  spcname
------------
 pg_default
 pg_global
(2 rows)

Конечно, это одна из глобальных для всего кластера таблиц. Аналогичная команда psql:

=> \db
       List of tablespaces
    Name    |  Owner   | Location
------------+----------+----------
 pg_default | postgres |
 pg_global  | postgres |
(2 rows)

Для нового табличного пространства нужен пустой каталог, владельцем которого является пользователь ОС, запускающий сервер СУБД:

=> \! sudo mkdir /var/lib/postgresql/ts_dir

Сменим владельца каталога:

=> \! sudo chown postgres /var/lib/postgresql/ts_dir

Теперь можем выполнить команду создания табличного пространства:

=> CREATE TABLESPACE ts LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE
=> \db
                List of tablespaces
    Name    |  Owner   |          Location
------------+----------+----------------------------
 pg_default | postgres |
 pg_global  | postgres |
 ts         | student  | /var/lib/postgresql/ts_dir
(3 rows)

При создании базы данных можно указать табличное пространство по умолчанию:

=> CREATE DATABASE data_physical TABLESPACE ts;
CREATE DATABASE
=> \c data_physical
You are now connected to database "data_physical" as user "student".

Это означает, что все объекты базы по умолчанию будут создаваться в этом табличном пространстве.

=> CREATE TABLE t(id integer PRIMARY KEY, s text);
CREATE TABLE
=> INSERT INTO t(id, s)
    SELECT id, id::text FROM generate_series(1,100_000) id;
INSERT 0 100000

Page 6: Слои и файлы

Source slide page 6: Слои и файлы{#source-figure-006 caption=“Source slide page 6: Слои и файлы”}

Основной

  • собственно данные
  • Карта свободного пространства
    • в страницах после
    • очистки
  • Карта видимости
    • отмечает страницы,
    • на которых все
    • версии строк
    • видны во всех снимках

Комментарии преподавателя

Обычно каждому объекту соответствует несколько слоев (forks). Каждый слой — это набор сегментов (то есть один или несколько файлов). Файлы-сегменты, из которых состоят слои, логически разбиты на страницы, обычно по 8 Кбайт (этот размер можно установить для всего кластера только при сборке сервера). Страницы файлов всех слоев объектов считываются с диска совершенно однотипно через общий механизм буферного кеша. Сами слои содержат разную информацию и имеют разное внутреннее устройство страниц.

Основной слой — это собственно данные: версии строк таблиц или строки индексов.

Слой vm (visibility map) — битовая карта видимости. В ней отмечены страницы, которые содержат только актуальные версии строк, причем эти версии видны во всех снимках данных. Такие страницы давно не изменялись и успели полностью очиститься от неактуальных версий.

Карта видимости применяется для оптимизации очистки (отмеченные страницы не нуждаются в очистке) и для ускорения индексного доступа. Информация о версионности строк хранится только для таблиц, но не для индексов. Поэтому, получив из индекса ссылку на версию строки, нужно проверить ее видимость, обратившись к табличной странице. Но если в самом индексе уже есть все нужные столбцы, а страница отмечена в карте видимости, то к таблице можно не обращаться.

Слой fsm (free space map) — карта свободного пространства. В ней отмечено доступное место внутри страниц, образующееся, например, при работе очистки. Эта карта используется при вставке новых версий строк для того, чтобы быстро найти подходящую страницу.

Page 7: Слои и файлы

Очистка обеспечит нам создание всех слоев таблицы:

=> VACUUM t;
VACUUM

Узнать расположение файлов, из которых состоит объект, можно так:

=> SELECT pg_relation_filepath('t');
            pg_relation_filepath
---------------------------------------------
 pg_tblspc/16386/PG_16_202307071/16387/16388
(1 row)

Посмотрим на сами файлы (имя и размер в байтах):

student$ sudo bash -c 'cd /var/lib/postgresql/16/main/pg_tblspc/16386/PG_16_202307071/16387; ls -l
16388*'
-rw------- 1 postgres postgres 4423680 апр 10 15:35 16388
-rw------- 1 postgres postgres   24576 апр 10 15:35 16388_fsm
-rw------- 1 postgres postgres    8192 апр 10 15:35 16388_vm

Видно, что они относятся к трем слоям: основному, fsm и vm. Объекты можно перемещать между табличными пространствами, но (в отличие от схем) это приводит к физическому перемещению данных:

=> ALTER TABLE t SET TABLESPACE pg_default;
ALTER TABLE
=> SELECT pg_relation_filepath('t');
 pg_relation_filepath
----------------------
 base/16387/16395
(1 row)

Размер объектов

Узнать размер, занимаемый базой данных и объектами в ней, можно с помощью ряда функций.

=> SELECT pg_database_size('data_physical');
 pg_database_size
------------------
         14422499
(1 row)

Для упрощения восприятия можно вывести число в отформатированном виде:

=> SELECT pg_size_pretty(pg_database_size('data_physical'));
 pg_size_pretty
----------------
 14 MB
(1 row)

Полный размер таблицы (вместе со всеми индексами):

=> SELECT pg_size_pretty(pg_total_relation_size('t'));
 pg_size_pretty
----------------
 6568 kB
(1 row)

А также отдельно размер таблицы…

=> SELECT pg_size_pretty(pg_table_size('t'));

Page 8: Page 8

 pg_size_pretty
----------------
 4360 kB
(1 row)

…и индексов:

=> SELECT pg_size_pretty(pg_indexes_size('t'));
 pg_size_pretty
----------------
 2208 kB
(1 row)

При желании можно узнать и размер отдельных слоев таблицы, например:

=> SELECT pg_size_pretty(pg_relation_size('t','main'));
 pg_size_pretty
----------------
 4320 kB
(1 row)

Объем, который занимает на диске табличное пространство, показывает другая функция:

=> SELECT pg_size_pretty(pg_tablespace_size('ts'));
 pg_size_pretty
----------------
 9740 kB
(1 row)

Page 9: TOAST

Версия строки должна помещаться на одну страницу

  • можно сжать часть атрибутов,
  • или вынести в отдельную TOAST-таблицу,
  • или сжать и вынести одновременно
  • TOAST-таблица
    • схема pg_toast
    • поддержана собственным индексом
    • «длинные» атрибуты разделены на части размером меньше страницы
    • читается только при обращении к «длинному» атрибуту
    • собственная версионность
    • работает прозрачно для приложения

Комментарии преподавателя

Любая версия строки в PostgreSQL должна целиком помещаться на одну страницу. Для «длинных» версий строк применяется технология TOAST — The Oversized Attributes Storage Technique. Она подразумевает несколько стратегий. Подходящий «длинный» атрибут может быть сжат так, чтобы версия строки поместилась на страницу. Если это не получается, атрибут может быть отправлен в отдельную служебную таблицу. Могут применяться и оба подхода.

Для каждой основной таблицы при необходимости создается отдельная toast-таблица (и к ней специальный индекс). Такие таблицы и индексы располагаются в отдельной схеме pg_toast и поэтому обычно не видны.

Версии строк в toast-таблице тоже должны помещаться на одну страницу, поэтому «длинные» значения хранятся порезанными на части. Из этих частей PostgreSQL прозрачно для приложения «склеивает» необходимое значение.

Toast-таблица используется только при обращении к «длинному» значению. Кроме того, для toast-таблицы поддерживается своя версионность: если обновление данных не затрагивает «длинное» значение, новая версия строки будет ссылаться на то же самое значение в toast-таблице — это экономит место.

https://postgrespro.ru/docs/postgresql/16/storage-toast

Page 10: TOAST

Добавим в таблицу очень длинную строку:

=> INSERT INTO t(id, s)
SELECT 0, string_agg(id::text,'.') FROM generate_series(1,5000) AS id;
INSERT 0 1

Изменится ли размер таблицы?

=> SELECT pg_size_pretty(pg_table_size('t'));
 pg_size_pretty
----------------
 4416 kB
(1 row)

Да. А размер основного слоя, в котором хранятся данные?

=> SELECT pg_size_pretty(pg_relation_size('t','main'));
 pg_size_pretty
----------------
 4320 kB
(1 row)

Нет. Поскольку версия строки не помещается в одну страницу, значение атрибута s будет разрезано на части и помещено в отдельную toast-таблицу. Ее можно отыскать в системном каталоге (мы используем тип regclass, чтобы преобразовать oid в имя отношения):

=> SELECT oid, reltoastrelid::regclass::text FROM pg_class WHERE relname='t';
  oid  |      reltoastrelid
-------+-------------------------
 16388 | pg_toast.pg_toast_16388
(1 row)

Символьная строка хранится по частям, из которых PostgreSQL при необходимости склеивает полное значение:

=> SELECT chunk_id, chunk_seq, left(chunk_data::text,45) AS chuck_data
FROM pg_toast.pg_toast_16388;
 chunk_id | chunk_seq |                  chuck_data
----------+-----------+-----------------------------------------------
    16398 |         0 | \x545d000000312e322e332e342e00352e362e372e382
    16398 |         1 | \x392e353161ff31002e3531322e353133002e3531342
    16398 |         2 | \xe215e216e217e218abe219e11a30e11b30e11c30e11
    16398 |         3 | \x11f4aa3611f43611f43611f43611f4aa3611f43611f
    16398 |         4 | \xf43211f4325511f43211f43211f43211f4325511f43
    16398 |         5 | \xf43811f43811f43811f4385511f43811f43811f4381
    16398 |         6 | \x11f4aa3411f43411f43411f43411f4aa3411f43411f
    16398 |         7 | \x0132370132aa370132370132370132370132aa38013
    16398 |         8 | \xf43611f43611f43611f4365511f43611f43611f4361
(9 rows)

В заключение удалим базу данных.

=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE data_physical;
DROP DATABASE

После того, как в табличном пространстве не осталось объектов, можно удалить и его:

=> DROP TABLESPACE ts;
DROP TABLESPACE
=> \! sudo rmdir /var/lib/postgresql/ts_dir

Page 11: Page 11

Page 12: Итоги

Физически

  • данные распределены по табличным пространствам (каталогам)
  • объект представлен несколькими слоями
  • каждый слой состоит из одного или нескольких файлов-сегментов
  • Табличными пространствами управляет администратор
  • Слои, файлы, TOAST — внутренняя кухня PostgreSQL

Page 13: Практика

  1. Создайте новую базу данных и подключитесь к ней. Создайте табличное пространство ts. Создайте таблицу t в табличном пространстве ts и добавьте в нее несколько строк. 2. Вычислите объем, занимаемый базой данных, таблицей и табличными пространствами ts и pg_default. 3. Перенесите таблицу в табличное пространство pg_default. Как изменился объем табличных пространств? 4. Удалите табличное пространство ts.

Page 14: 1. Табличные пространства и таблица

Создаем базу данных:

=> CREATE DATABASE data_physical;
CREATE DATABASE
=> \c data_physical
You are now connected to database "data_physical" as user "student".

Табличное пространство:

student$ sudo mkdir /var/lib/postgresql/ts_dir
student$ sudo chown postgres /var/lib/postgresql/ts_dir
=> CREATE TABLESPACE ts LOCATION '/var/lib/postgresql/ts_dir';
CREATE TABLESPACE

Создаем таблицу:

=> CREATE TABLE t(n integer) TABLESPACE ts;
CREATE TABLE
=> INSERT INTO t SELECT 1 FROM generate_series(1,1000);
INSERT 0 1000

2. Размер данных

Объем базы данных:

=> SELECT pg_size_pretty(pg_database_size('data_physical')) AS db_size;
 db_size
---------
 7580 kB
(1 row)

Размер таблицы:

=> SELECT pg_size_pretty(pg_total_relation_size('t')) AS t_size;
 t_size
--------
 64 kB
(1 row)

Объем табличных пространств:

=> SELECT
    pg_size_pretty(pg_tablespace_size('pg_default')) AS pg_default_size,
    pg_size_pretty(pg_tablespace_size('ts')) AS ts_size;
 pg_default_size | ts_size
-----------------+---------
 36 MB           | 68 kB
(1 row)

Размер табличного пространства больше размера таблицы на 4kB из-за особенностей вычисления размера содержимого каталога в Linux.

3. Перенос таблицы

Перенесем таблицу:

=> ALTER TABLE t SET TABLESPACE pg_default;
ALTER TABLE

Новый объем табличных пространств:

Page 15: 4. Удаление табличного пространства

=> SELECT
    pg_size_pretty(pg_tablespace_size('pg_default')) AS pg_default_size,
    pg_size_pretty(pg_tablespace_size('ts')) AS ts_size;
 pg_default_size |  ts_size
-----------------+------------
 36 MB           | 4096 bytes
(1 row)

Удаляем табличное пространство…

=> DROP TABLESPACE ts;
DROP TABLESPACE

…и каталог, где были размещены его данные:

student$ sudo rm -rf /var/lib/postgresql/ts_dir
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE data_physical;
DROP DATABASE

07 Занятие 07: Учебное приложение / Книжный магазин

Исходная страница в нативном Markdown: Учебное приложение / Книжный магазин
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_07_bstore_schema.html Archive: DEV1-handouts-16.zip Страниц PDF: 28

Page 1: Учебное приложение / Книжный магазин

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Обзор приложения «Книжный магазин»
  • Проектирование схемы данных, нормализация
  • Итоговая схема данных приложения
  • Организация интерфейса между клиентом и сервером

Page 3: Приложение

Source slide page 3: Приложение{#source-figure-003 caption=“Source slide page 3: Приложение”}

Комментарии преподавателя

Приложение состоит из нескольких частей, представленных вкладками.

«Магазин» — это интерфейс веб-пользователя, в котором он может покупать книги.

Остальные вкладки соответствуют внутреннему интерфейсу, доступному только сотрудникам («админка» сайта).

«Каталог» — интерфейс кладовщика, в котором он может заказывать закупку книг на склад магазина и просматривать операции поступлений и покупок.

«Книги» и «Авторы» — интерфейс библиотекаря, в котором он может регистрировать авторов и их книги.

В учебных целях вся функциональность представлена на одной общей веб-странице. Если какая-то часть функциональности недоступна из-за того, что на сервере нет подходящего объекта (таблицы, функции и т. п.), приложение сообщит об этом. Также приложение выводит текст запросов, которые оно посылает на сервер.

В ходе курса мы начнем с пустой базы данных и постепенно реализуем в ней все необходимые компоненты.

P. S. Исходный код приложения не является темой курса, но может быть получен в репозитории https://pubgit.postgrespro.ru/pub/dev1app.git

Page 4: Демонстрация приложения

В этой демонстрации мы показываем приложение «Книжный магазин» в том виде, в котором оно будет после завершения всех практических заданий. Приложение откроется в отдельной вкладке браузера виртуальной машины курса: Открываем http://localhost…

student$ xdg-open http://localhost

Page 5: Книги

ER-модель для высокоуровневого проектирования

  • сущности — понятия предметной области
  • связи — отношения между сущностями
  • атрибуты — свойства сущностей и связей
  • сущность

Книга

  • атрибуты
  • название
  • авторы
  • количество
  • операции

Комментарии преподавателя

После того, как мы посмотрели на внешний вид приложения и поняли его функциональность, нам нужно разобраться со схемой данных. Мы не будем хоть сколько-нибудь глубоко вникать в вопросы проектирования баз данных — это отдельная дисциплина, не входящая в данный курс. Но совсем обойти вниманием эту тему тоже нельзя.

Часто для высокоуровневого проектирования баз данных используется модель «сущность — связи», или ER-модель (Entity — Relationship). Она оперирует сущностями (понятиями предметной области), связями между ними и атрибутами (свойствами сущностей и связей). Модель позволяет рассуждать на логическом уровне, не опускаясь до деталей представления данных на физическом (в виде таблиц).

Первым подходом к проектированию может служить диаграмма, представленная на слайде: можно представить одну большую сущность «Книга», а все остальное сделать ее атрибутами.

Page 6: Схема данных

Source slide page 6: Схема данных{#source-figure-006 caption=“Source slide page 6: Схема данных”}

 id |       title       |            author            | qty | operation
−−−−+−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−+−−−−−+−−−−−−−−−−−
  1 | Муму              | Тургенев Иван Сергеевич      | 10  |       +11
  1 | МУМУ              | Тургенев Иван Сергеевич      | 10  |        -1
  2 | Отцы и дети       | Тургенев Иван Сергеевич      |  4  |        +4
  3 | Трудно быть богом | Стругацкий Аркадий Натанович |  7  |        +7
  3 | Трудно быть богом | Стругацкий Борис Натанович   |  7  |         0
  • Данные дублируются
    • сложно поддерживать согласованность
    • сложно обновлять
    • сложно писать запросы

Комментарии преподавателя

Очевидно, что это неправильный подход. На диаграмме это может быть не так заметно, но давайте попробуем отобразить диаграмму на таблицы БД. Это можно сделать разными способами, например так, как показано на слайде: сущность становится таблицей, ее атрибуты — столбцами этой таблицы.

Здесь хорошо видно, что часть данных дублируется (эти фрагменты выделены на рисунке). Дублирование приводит к сложностям с обеспечением согласованности — а это едва ли не главная задача СУБД.

Например, каждая из двух строк, относящихся к книге 3, должна содержать общее количество (7 штук). Что нужно сделать, чтобы отразить покупку книги? С одной стороны, надо добавить строки для отражения операции покупки (а сколько строк добавлять, еще две?). С другой, во всех строках надо уменьшить количество с 7 до 6. А что делать, если в результате какой-нибудь ошибки значение количества в одной из этих строк будет отличаться от значения в другой строке? Как сформулировать ограничение целостности, которое запрещает такую ситуацию?

Также усложняются и многие запросы. Как найти общее число книг? Как найти всех уникальных авторов?

Итак, такая схема плохо работает для реляционных баз данных.

Page 7: Схема данных (вариант)

 entity | attribute  | value
−−−−−−−−+−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−
      1 | title      | Муму
      1 | author     | Тургенев Иван Сергеевич
      1 | qty        | 10
      1 | operation  | +11
      1 | operation  | -1
      2 | title      | Отцы и дети
      2 | author     | Тургенев Иван Сергеевич
      2 | qty        | 4
      2 | operation  | +4
    ... | ...        | ...
  • Данные без схемы
    • поддержка согласованности на стороне приложения
    • сложно писать запросы
    • низкая производительность (множественные соединения)

Комментарии преподавателя

Другой вариант отображения сущности в таблицу — так называемая схема EAV: сущность — атрибут — значение. Она позволяет уложить вообще все, что угодно, в одну-единственную таблицу. Формально мы получаем реляционную базу данных, а фактически здесь отсутствует схема и СУБД не может гарантировать согласованность данных. Поддержка согласованности данных целиком ложится на приложение, что, безусловно, рано или поздно приведет к ее нарушению.

В такой схеме сложно писать запросы (хотя и довольно просто их генерировать), и в результате работа со сколько-нибудь большим объемом данных становится проблемой из-за постоянных многократных соединений таблицы самой с собой.

Однако следует отметить, что такой подход имеет и плюсы. Например, очевидно, что такую «схему» данных легко наращивать: при добавлении новой сущности или атрибута мы просто добавляем в нашу таблицу новые строки.

И все-таки так делать не стоит.

Page 8: Схема данных (вариант)

 book_id |                       description
−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
       1 | {"title": "Муму",
         |  "authors": [ "Тургенев Иван Сергеевич" ],
         |  "qty": 10,
         |  "operations": [ +11, -1 ]}
       3 | {"title": "Трудно быть богом",
         |  "authors": [ "Стругацкий Аркадий Натанович",
         |               "Стругацкий Борис Натанович" ],
         |  "qty": 7,
         |  "operations": [ +7 ]}
     ... | ...
  • Данные без схемы
    • поддержка согласованности на стороне приложения
    • сложно писать запросы (специальный язык)
    • индексная поддержка есть

Комментарии преподавателя

Еще одна вариация на ту же тему — представление данных в виде JSON, в духе NoSQL. По сути, тут применимы все замечания, сделанные ранее.

Кроме того, запросы к такой структуре придется писать не на SQL, а используя какой-то специализированный язык (раньше скорее всего выбор пал бы на jsQuery, а в PostgreSQL 16 удобно пользоваться возможностями SQL/JSON Path Language, определенными в стандарте SQL:2016).

https://github.com/postgrespro/jsquery

https://postgrespro.ru/docs/postgresql/16/functions-json#FUNCTIONS-SQLJ SON-PATH

Хотя у PostgreSQL есть индексная поддержка JSON, производительность все равно под вопросом.

Такую схему удобно применять, когда от базы данных требуется только получение JSON по идентификатору, но не требуется серьезная работа с данными внутри JSON. Это не наш случай.

(Разумеется, это не категоричное утверждение. См. последнее задание в практике.)

Page 9: Книги и операции

Source slide page 9: Книги и операции{#source-figure-009 caption=“Source slide page 9: Книги и операции”}

Нормализация — уменьшение избыточности данных

  • разбиение крупных сущностей на более мелкие

Книга

Операция

Комментарии преподавателя

Итак, нам требуется устранить избыточность данных, чтобы привести их в вид, удобный для обработки в реляционной СУБД. Этот процесс называется нормализацией.

Возможно, вы знакомы с понятиями нормальных форм (первая, вторая, третья, Бойса-Кодда и т. д.). Мы не будем говорить о них; на неформальном уровне достаточно понимать, что весь этот математический аппарат преследует одну-единственную цель: устранение избыточности.

Средство для уменьшения избыточности — разбиение большой сущности на несколько меньших. Как именно это сделать, подскажет здравый смысл (который все равно нельзя заменить одним только знанием нормальных форм).

В нашем случае все достаточно просто. Давайте начнем с отделения книг от операций. Эти две сущности связаны отношением «один ко многим»: каждая книга может иметь несколько операций, но каждая операция относится только к одной книге.

Page 10: Схема данных

books
 book_id |       title       |            author
−−−−−−−−−+−−−−−−−−−−−−−−−−−−−+−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−
       1 | Муму              | Тургенев Иван Сергеевич
       2 | Отцы и дети       | Тургенев Иван Сергеевич
       3 | Трудно быть богом | Стругацкий Аркадий Натанович
       3 | Трудно быть богом | Стругацкий Борис Натанович
operations
 operation_id | book_id | qty_change | date_created
−−−−−−−−−−−−−−+−−−−−−−−−+−−−−−−−−−−−−+−−−−−−−−−−−−−−
            1 |       1 |        +10 | 2020-07-13
            2 |       1 |         -1 | 2020-08-25
            3 |       3 |         +7 | 2020-07-13
            4 |       2 |         +4 | 2020-07-13

Комментарии преподавателя

На уровне структуры базы данных это может быть представлено двумя таблицами: книги (books) и операции (operations).

Операция состоит в изменении количества книг (положительном при заказе; отрицательном при покупке). Заметьте, что теперь у книги нет атрибута «количество». Вместо этого достаточно просуммировать изменения количества по всем операциям, относящимся к данной книге. Дополнительный атрибут «количество» у книги снова привел бы к избыточности данных.

Не исключено, что такое решение вас насторожило. Удобно ли вместо простого обращения к полю подсчитывать сумму? Но мы можем создать представление, которое для каждой книги будет показывать количество. Это не приведет к появлению избыточности, поскольку представление — это всего лишь запрос.

Второй момент — производительность. Если подсчет суммы приведет к ухудшению производительности, мы можем выполнить обратный процесс — денормализацию: добавить в таблицу книг поле «количество» и обеспечить его согласованность с таблицей операций. Надо ли этим заниматься — вопрос, ответ на который выходит за рамки этого курса (он рассматривается в курсе QPT «Оптимизация запросов»). Но из общих соображений понятно, что для нашего «игрушечного» магазина это не требуется. Однако мы еще вернемся к вопросу денормализации в теме «Триггеры».

Итак, как видно на слайде, выделение операций в отдельную сущность решило часть проблем дублирования, но не все.

Page 11: Книги, авторы и операции

Source slide page 11: Книги, авторы и операции{#source-figure-011 caption=“Source slide page 11: Книги, авторы и операции”}

Книга

Операция

Комментарии преподавателя

Поэтому надо сделать еще один шаг: отделить авторов от книг и связать их соотношением «многие ко многим»: и каждая книга может быть написана несколькими авторами, и каждый автор может написать несколько книг. На уровне таблиц такая связь реализуется с помощью дополнительной промежуточной таблицы.

Атрибутами автора можно сделать фамилию, имя и отчество. Это имеет смысл, поскольку нам может потребоваться работать отдельно с каждым из этих атрибутов, например, выводить фамилию и инициалы.

Page 12: Схема данных приложения

student$ psql bookstore

Итак, схема данных приложения состоит из четырех таблиц:

=> \dt
            List of relations
  Schema   |    Name    | Type  |  Owner
-----------+------------+-------+---------
 bookstore | authors    | table | student
 bookstore | authorship | table | student
 bookstore | books      | table | student
 bookstore | operations | table | student
(4 rows)

Книги

=> \d books
                         Table "bookstore.books"
 Column  |  Type   | Collation | Nullable |           Default
---------+---------+-----------+----------+------------------------------
 book_id | integer |           | not null | generated always as identity
 title   | text    |           | not null |
Indexes:
    "books_pkey" PRIMARY KEY, btree (book_id)
Referenced by:
    TABLE "authorship" CONSTRAINT "authorship_book_id_fkey" FOREIGN KEY (book_id)
REFERENCES books(book_id)
    TABLE "operations" CONSTRAINT "operations_book_id_fkey" FOREIGN KEY (book_id)
REFERENCES books(book_id)

Здесь мы используем типы данных: integer — целое число; text — символьный, текстовая строка произвольной длины. А также используем ограничение целостности: PRIMARY KEY — первичный ключ. Конструкция GENERATED AS IDENTITY служит для автоматической генерации уникальных значений (в версиях до 10 для этого использовался псевдотип serial).

  • Строки столбцов, объявленных как GENERATED AS IDENTITY, получают значения из специальных объектов базы данных — последовательностей. Имя использованной последовательности можно узнать так:
=> SELECT pg_get_serial_sequence('books','book_id');
   pg_get_serial_sequence
-----------------------------
 bookstore.books_book_id_seq
(1 row)

Последовательности можно при необходимости создавать вручную, а также обращаться к ним непосредственно:

=> SELECT nextval('books_book_id_seq');
 nextval
---------
       7
(1 row)

Последовательность — самый эффективный способ генерации уникальных номеров. Но следует иметь в виду, что последовательность не гарантирует: отсутствия пропусков в нумерации (так как изменение происходит не транзакционно); монотонного возрастания номеров (если используется кеширование значений в сеансах).

Page 13: Page 13

Вот какие данные находятся в таблице книг:

=> SELECT * FROM books \gx
-[ RECORD 1
]-----------------------------------------------------------------------------------------
------------------------------------------------------
book_id | 1
title   | Сказка о царе Салтане
-[ RECORD 2
]-----------------------------------------------------------------------------------------
------------------------------------------------------
book_id | 2
title   | Муму
-[ RECORD 3
]-----------------------------------------------------------------------------------------
------------------------------------------------------
book_id | 3
title   | Трудно быть богом
-[ RECORD 4
]-----------------------------------------------------------------------------------------
------------------------------------------------------
book_id | 4
title   | Война и мир
-[ RECORD 5
]-----------------------------------------------------------------------------------------
------------------------------------------------------
book_id | 5
title   | Путешествия в некоторые удаленные страны
мира в четырех частях: сочинение Лемюэля
Гулливера, сначала хирурга, а затем капитана
нескольких кораблей
-[ RECORD 6
]-----------------------------------------------------------------------------------------
------------------------------------------------------
book_id | 6
title   | Хрестоматия

Обратите внимание, что названия могут быть длинными. Мы используем расширенный формат вывода psql, а появившийся в PostgreSQL 16 параметр xheader_width поможет обрезать строку заголовка “широкого” столбца:

=> \pset xheader_width 40
Expanded header width is 40.

Сравните вывод:

=> SELECT * FROM books \gx
-[ RECORD 1 ]---------------------------
book_id | 1
title   | Сказка о царе Салтане
-[ RECORD 2 ]---------------------------
book_id | 2
title   | Муму
-[ RECORD 3 ]---------------------------
book_id | 3
title   | Трудно быть богом
-[ RECORD 4 ]---------------------------
book_id | 4
title   | Война и мир
-[ RECORD 5 ]---------------------------
book_id | 5
title   | Путешествия в некоторые удаленные страны
мира в четырех частях: сочинение Лемюэля
Гулливера, сначала хирурга, а затем капитана
нескольких кораблей
-[ RECORD 6 ]---------------------------
book_id | 6
title   | Хрестоматия

Авторы

=> \d authors

Page 14: Page 14

                          Table "bookstore.authors"
   Column    |  Type   | Collation | Nullable |           Default
-------------+---------+-----------+----------+------------------------------
 author_id   | integer |           | not null | generated always as identity
 last_name   | text    |           | not null |
 first_name  | text    |           | not null |
 middle_name | text    |           |          |
Indexes:
    "authors_pkey" PRIMARY KEY, btree (author_id)
Referenced by:
    TABLE "authorship" CONSTRAINT "authorship_author_id_fkey" FOREIGN KEY (author_id)
REFERENCES authors(author_id)

Здесь дополнительно используется ограничение целостности: NOT NULL — обязательность, то есть недопустимость неопределенных значений.

=> SELECT * FROM authors;
 author_id | last_name  | first_name | middle_name
-----------+------------+------------+-------------
         1 | Пушкин     | Александр  | Сергеевич
         2 | Тургенев   | Иван       | Сергеевич
         3 | Стругацкий | Борис      | Натанович
         4 | Стругацкий | Аркадий    | Натанович
         5 | Толстой    | Лев        | Николаевич
         6 | Свифт      | Джонатан   |
(6 rows)

Обратите внимание, что отчество может отсутствовать (или быть заданным пустой строкой). Ограничение PRIMARY KEY в выводе команды \d упоминалось вместе со словами «индекс» и «btree». Btree (B-дерево) — основной тип индекса, используемый в базах данных для ускорения поиска и для поддержки ограничений целостности (первичного ключа и уникальности). Представим себе, что в магазине продаются книги миллиона авторов-однофамильцев:

=> BEGIN; -- начнем транзакцию, чтобы откатить потом изменения
BEGIN
=> INSERT INTO authors(first_name, last_name)
    SELECT 'Графоман', 'Графоманов' FROM generate_series(1, 1_000_000);
INSERT 0 1000000

Сколько времени занимает поиск одного автора в такой таблице?

=> \timing on
Timing is on.
=> SELECT * FROM authors WHERE last_name = 'Пушкин';
 author_id | last_name | first_name | middle_name
-----------+-----------+------------+-------------
         1 | Пушкин    | Александр  | Сергеевич
(1 row)
Time: 157,298 ms
=> \timing off
Timing is off.

Если попросить оптимизатор показать план запроса, мы увидим в нем Seq Scan — последовательное сканирование всей таблицы в поисках нужного значения (Filter):

=> EXPLAIN (costs off)
SELECT * FROM authors WHERE last_name = 'Пушкин';
               QUERY PLAN
----------------------------------------
 Seq Scan on authors
   Filter: (last_name = 'Пушкин'::text)
(2 rows)

А если искать по полю, которое проиндексировано?

=> \timing on

Page 15: Page 15

Timing is on.
=> SELECT * FROM authors WHERE author_id = 1;
 author_id | last_name | first_name | middle_name
-----------+-----------+------------+-------------
         1 | Пушкин    | Александр  | Сергеевич
(1 row)
Time: 0,579 ms
=> \timing off
Timing is off.

Время уменьшилось на несколько порядков. А в плане запроса появился индекс:

=> EXPLAIN (costs off)
SELECT * FROM authors WHERE author_id = 1;
                QUERY PLAN
------------------------------------------
 Index Scan using authors_pkey on authors
   Index Cond: (author_id = 1)
(2 rows)

Можно создать индекс и по фамилии (и проанализировать таблицу, чтобы собрать актуальную статистику):

=> ANALYZE authors;
ANALYZE
=> CREATE INDEX ON authors(last_name);
CREATE INDEX
=> EXPLAIN (costs off)
SELECT * FROM authors WHERE last_name = 'Пушкин';
                    QUERY PLAN
---------------------------------------------------
 Index Scan using authors_last_name_idx on authors
   Index Cond: (last_name = 'Пушкин'::text)
(2 rows)

Однако индекс не является универсальным средством увеличения производительности. Обычно индекс очень полезен, если из таблицы требуется выбрать небольшую долю всех имеющихся строк. Если нужно прочитать много данных, индекс будет только мешать, и оптимизатор это понимает:

=> EXPLAIN (costs off)
SELECT * FROM authors WHERE last_name = 'Графоманов';
                 QUERY PLAN
--------------------------------------------
 Seq Scan on authors
   Filter: (last_name = 'Графоманов'::text)
(2 rows)

Кроме того, надо учитывать накладные расходы на обновление индексов при изменении таблицы и занимаемое ими место на диске. Отменим все наши изменения (включая создание индекса):

=> ROLLBACK;
ROLLBACK
=> ANALYZE authors;
ANALYZE

Авторства

С помощью этой таблицы реализуется связь «многие ко многим».

=> \d authorship

Page 16: Page 16

             Table "bookstore.authorship"
  Column   |  Type   | Collation | Nullable | Default
-----------+---------+-----------+----------+---------
 book_id   | integer |           | not null |
 author_id | integer |           | not null |
 seq_num   | integer |           | not null |
Indexes:
    "authorship_pkey" PRIMARY KEY, btree (book_id, author_id)
Foreign-key constraints:
    "authorship_author_id_fkey" FOREIGN KEY (author_id) REFERENCES authors(author_id)
    "authorship_book_id_fkey" FOREIGN KEY (book_id) REFERENCES books(book_id)

Здесь к уже использованным ограничениям добавляется ограничение ссылочной целостности: FOREIGN KEY — внешний ключ. Фактически таблица содержит два внешних ключа: один ссылается на таблицу книг, другой — на таблицу авторов. Столбец seq_num определяет последовательность, в которой должны перечисляться авторы одной книги, если их несколько. Также обратите внимание на то, что первичный ключ — составной.

=> SELECT * FROM authorship;
 book_id | author_id | seq_num
---------+-----------+---------
       1 |         1 |       1
       2 |         2 |       1
       3 |         3 |       2
       3 |         4 |       1
       4 |         5 |       1
       5 |         6 |       1
       6 |         1 |       1
       6 |         5 |       2
       6 |         2 |       3
(9 rows)

Операции

=> \d operations
                         Table "bookstore.operations"
    Column    |  Type   | Collation | Nullable |           Default
--------------+---------+-----------+----------+------------------------------
 operation_id | integer |           | not null | generated always as identity
 book_id      | integer |           | not null |
 qty_change   | integer |           | not null |
 date_created | date    |           | not null | CURRENT_DATE
Indexes:
    "operations_pkey" PRIMARY KEY, btree (operation_id)
Foreign-key constraints:
    "operations_book_id_fkey" FOREIGN KEY (book_id) REFERENCES books(book_id)

В этой таблице используется еще один тип данных: date — дата (без указания времени). Для столбца date_created указано значение по умолчанию (DEFAULT) — текущая дата.

=> SELECT * FROM operations;
 operation_id | book_id | qty_change | date_created
--------------+---------+------------+--------------
            1 |       1 |         10 | 2026-04-10
            2 |       1 |         10 | 2026-04-10
            3 |       1 |         -1 | 2026-04-10
(3 rows)

Кроме тех типов данных, которые используются в таблицах приложения, мы постоянно будем встречаться с логическим типом (boolean). Например, такой тип имеют логические выражения в условии WHERE.

Примечание

Note Важно помнить, что, в отличие от традиционных языков программирования, SQL использует трехзначную логику: кроме значений true и false, логическое значение может принимать неопределенное значение NULL (которое можно понимать как «значение неизвестно»).

Page 17: Page 17

В примерах встречаются и некоторые другие типы данных, информацию о работе с ними можно найти в материалах курса («Основные типы данных и функции», datatypes.pdf). Кроме того, в курсе мы подробно рассмотрим более сложные типы: составной — записи, аналогичные строкам таблиц (в теме «SQL. Составные типы»); массивы (в теме «PL/pgSQL. Массивы»).

Page 18: Организация интерфейса

Таблицы и триггеры

  • чтение данных напрямую из таблицы (представления);
  • запись данных напрямую в таблицу (представление),
  • плюс триггеры для изменения связанных таблиц
  • приложение должно быть в курсе модели данных,
  • максимальная гибкость
  • сложно поддерживать согласованность
  • Функции
    • чтение данных из табличных функций;
    • запись данных через вызов функций
    • приложение отделено от модели данных и ограничено API
    • большой объем работы по изготовлению функций-оберток,
    • потенциальные проблемы с производительностью

Комментарии преподавателя

Есть несколько способов организации интерфейса между клиентской и серверной частями приложения.

Один вариант — разрешить приложению напрямую обращаться к таблицам в базе данных и изменять их. При этом от приложения требуется детальное «знание» модели данных. Отчасти это требование можно ослабить за счет использования представлений (view).

Кроме того, от приложения требуется и определенная дисциплина — иначе очень сложно поддержать согласованность данных, защищаясь на уровне БД от любых возможных некорректных действий приложения. Но в этом случае достигается максимальная гибкость.

Другой вариант — запретить приложению доступ к таблицам и разрешить только вызовы функций. Чтение данных можно организовать с помощью табличных функций (которые возвращают набор строк). Изменение данных можно выполнять, вызывая другие функции и передавая им необходимые данные. В этом случае внутри функций можно реализовать все необходимые проверки согласованности — база данных будет защищена, но приложение сможет пользоваться только предоставленным и ограниченным набором возможностей. Такой вариант требует написания большого количества функций-оберток и может привести к потере производительности.

Вполне возможны и промежуточные варианты. Например, разрешить чтение данных непосредственно из таблиц, а изменение выполнять только через вызов функций.

Page 19: Интерфейс покупателя

Source slide page 19: Интерфейс покупателя{#source-figure-019 caption=“Source slide page 19: Интерфейс покупателя”}

Комментарии преподавателя

В нашем приложении мы попробуем разные варианты организации интерфейса (хотя в реальной жизни обычно лучше систематически придерживаться какого-то одного подхода).

Магазин будет использовать интерфейсные функции:

● для поиска книг — get_catalog (тема «SQL. Составные типы»);

● для покупки книг — buy_book («PL/pgSQL. Выполнение запросов»).

Page 20: Интерфейс сотрудника

Source slide page 20: Интерфейс сотрудника{#source-figure-020 caption=“Source slide page 20: Интерфейс сотрудника”}

Комментарии преподавателя

«Админка» для получения данных будет использовать представления (которые мы создадим в практике к этой теме):

● список книг — catalog_v;

● список авторов — authors_v;

● список операций — operations_v.

Добавление автора будет выполнять функция add_author (создадим ее в теме «PL/pgSQL. Выполнение запросов»), добавление книги — функция add_book («PL/pgSQL. Массивы»).

Для заказа книг сделаем представление catalog_v обновляемым («PL/pgSQL. Триггеры»).

Page 21: Представления

Представление — запрос, у которого есть имя. Например, можно создать представление, которое показывает только авторов без отчества:

=> CREATE VIEW authors_no_middle_name AS
    SELECT author_id, first_name, last_name
    FROM authors
    WHERE nullif(middle_name,'') IS NULL;
CREATE VIEW

Теперь имя представления можно использовать в запросах практически так же, как и таблицу:

=> SELECT * FROM authors_no_middle_name;
 author_id | first_name | last_name
-----------+------------+-----------
         6 | Джонатан   | Свифт
(1 row)

В простом случае с представлением будут работать и другие операции, например:

=> UPDATE authors_no_middle_name SET last_name = initcap(last_name);
UPDATE 1

С помощью триггеров можно сделать так, чтобы и в сложных случаях для представлений работали вставка, обновление и удаление строк. Мы рассмотрим это в теме «PL/pgSQL. Триггеры». При планировании запроса представление «разворачивается» до базовых таблиц:

=> EXPLAIN (costs off)
SELECT * FROM authors_no_middle_name;
                    QUERY PLAN
---------------------------------------------------
 Seq Scan on authors
   Filter: (NULLIF(middle_name, ''::text) IS NULL)
(2 rows)

Приложение использует три представления. Сначала они будут очень простыми, но в следующих темах мы перенесем в них часть логики приложения. Представление для авторов — конкатенация фамилии, имени и отчества (если оно есть):

=> SELECT * FROM authors_v;
 author_id |         display_name
-----------+------------------------------
         1 | Пушкин Александр Сергеевич
         2 | Тургенев Иван Сергеевич
         3 | Стругацкий Борис Натанович
         4 | Стругацкий Аркадий Натанович
         5 | Толстой Лев Николаевич
         6 | Свифт Джонатан
(6 rows)

Представление для каталога книг — пока просто название книги:

=> SELECT * FROM catalog_v \gx

Page 22: Page 22

-[ RECORD 1 ]+--------------------------------------------------------
book_id      | 1
display_name | Сказка о царе Салтане
-[ RECORD 2 ]+--------------------------------------------------------
book_id      | 2
display_name | Муму
-[ RECORD 3 ]+--------------------------------------------------------
book_id      | 3
display_name | Трудно быть богом
-[ RECORD 4 ]+--------------------------------------------------------
book_id      | 4
display_name | Война и мир
-[ RECORD 5 ]+--------------------------------------------------------
book_id      | 5
display_name | Путешествия в некоторые удаленные
страны мира в четырех частях: сочинение Лемюэля
Гулливера, сначала хирурга, а затем капитана
нескольких кораблей
-[ RECORD 6 ]+--------------------------------------------------------
book_id      | 6
display_name | Хрестоматия

Представление для операций — дополнительно определяет тип операции (поступление или покупка):

=> SELECT * FROM operations_v;
 book_id |   op_type   | qty_change | date_created
---------+-------------+------------+--------------
       1 | Поступление |         10 | 10.04.2026
       1 | Поступление |         10 | 10.04.2026
       1 | Покупка     |          1 | 10.04.2026
(3 rows)

Page 23: Итоги

Проектирование баз данных — отдельная тема

  • теория важна, но не заменяет здравый смысл
  • Отсутствие избыточности в данных делает работу удобнее и упрощает поддержку согласованности
  • Для клиент-серверного интерфейса можно использовать таблицы, представления, функции, триггеры

Page 24: Практика

  1. В базе данных bookstore создайте схему bookstore. Настройте путь поиска к этой схеме на уровне подключения к БД. 2. В схеме bookstore создайте таблицы books, authors, authorship и operations с необходимыми ограничениями целостности так, чтобы они соответствовали показанным в демонстрации. 3. Вставьте в таблицы данные о нескольких книгах. Проверьте себя с помощью запросов. 4. В схеме bookstore создайте представления authors_v, catalog_v и operations_v так, чтобы они соответствовали показанным в демонстрации. Проверьте, что приложение стало показывать данные на вкладках «Книги», «Авторы» и «Каталог».

Комментарии преподавателя

  1. Вспомните материал темы «Организация данных. Логическая структура».

  2. Ориентируйтесь на показанный в демонстрации вывод команд \d утилиты psql.

  3. Вы можете использовать те же данные, что были показаны в демонстрации, или придумать свои собственные.

  4. Попробуйте написать запросы к базовым таблицам, возвращающие тот же результат, что и показанные в демонстрации запросы к представлениям. Затем оформите запросы в виде представлений.

После выполнения практики обязательно сверьте свои запросы с решением, приведенным к этой теме. При необходимости внесите коррективы.

Page 25: 1. Схема и путь поиска

=> CREATE DATABASE bookstore;
CREATE DATABASE
=> \c bookstore
You are now connected to database "bookstore" as user "student".
=> CREATE SCHEMA bookstore;
CREATE SCHEMA
=> ALTER DATABASE bookstore SET search_path = bookstore, public;
ALTER DATABASE
=> \c
You are now connected to database "bookstore" as user "student".
=> SHOW search_path;
    search_path
-------------------
 bookstore, public
(1 row)

2. Таблицы

Авторы:

=> CREATE TABLE authors(
    author_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    last_name text NOT NULL,
    first_name text NOT NULL,
    middle_name text
);
CREATE TABLE

Книги:

=> CREATE TABLE books(
    book_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    title text NOT NULL
);
CREATE TABLE

Авторство:

=> CREATE TABLE authorship(
    book_id integer REFERENCES books,
    author_id integer REFERENCES authors,
    seq_num integer NOT NULL,
    PRIMARY KEY (book_id,author_id)
);
CREATE TABLE

Операции:

=> CREATE TABLE operations(
    operation_id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    book_id integer NOT NULL REFERENCES books,
    qty_change integer NOT NULL,
    date_created date NOT NULL DEFAULT current_date
);
CREATE TABLE

3. Данные

Авторы:

=> INSERT INTO authors(last_name, first_name, middle_name)
VALUES
    ('Пушкин', 'Александр', 'Сергеевич'),
    ('Тургенев', 'Иван', 'Сергеевич'),
    ('Стругацкий', 'Борис', 'Натанович'),
    ('Стругацкий', 'Аркадий', 'Натанович'),
    ('Толстой', 'Лев', 'Николаевич'),
    ('Свифт', 'Джонатан', NULL);
INSERT 0 6

Книги:

=> INSERT INTO books(title)
VALUES
    ('Сказка о царе Салтане'),
    ('Муму'),
    ('Трудно быть богом'),
    ('Война и мир'),
    ('Путешествия в некоторые удаленные страны мира в четырех частях: сочинение Лемюэля Гулливера, сначала хирурга, а затем капитана нескольких кораблей'),
    ('Хрестоматия');
INSERT 0 6

Авторство:

Page 26: Page 26

=> INSERT INTO authorship(book_id, author_id, seq_num)
VALUES
    (1, 1, 1),
    (2, 2, 1),
    (3, 3, 2),
    (3, 4, 1),
    (4, 5, 1),
    (5, 6, 1),
    (6, 1, 1),
    (6, 5, 2),
    (6, 2, 3);
INSERT 0 9

Операции:

=> INSERT INTO operations(book_id, qty_change)
VALUES
    (1, 10),
    (1, 10),
    (1, -1);
INSERT 0 3

4. Представления

Представление для авторов:

=> CREATE VIEW authors_v AS
SELECT a.author_id,
       a.last_name || ' ' ||
       a.first_name ||
       coalesce(' ' || nullif(a.middle_name, ''), '') AS display_name
FROM   authors a;
CREATE VIEW

Представление для каталога:

=> CREATE VIEW catalog_v AS
SELECT b.book_id,
       b.title AS display_name
FROM   books b;
CREATE VIEW

Представление для операций:

=> CREATE VIEW operations_v AS
SELECT book_id,
       CASE
           WHEN qty_change > 0 THEN 'Поступление'
           ELSE 'Покупка'
       END op_type,
       abs(qty_change) qty_change,
       to_char(date_created, 'DD.MM.YYYY') date_created
FROM   operations
ORDER BY operation_id;
CREATE VIEW

Page 27: Практика+

  1. Какие дополнительные атрибуты могут появиться у выделенных сущностей при развитии приложения? 2. Допустим, требуется хранить информацию об издательстве. Дополните ER-диаграмму и отобразите ее в таблицы. 3. Некоторые книги могут входить в серии (например, «Библиотека приключений»). Как изменится схема данных? 4. Пусть наш магазин стал торговать компьютерными комплектующими (материнскими платами, процессорами, памятью, жесткими дисками, мониторами и т. п.). Какие сущности и какие атрибуты вы бы выделили? Учтите, что на рынке постоянно появляются новые типы оборудования со своими характеристиками.

Комментарии преподавателя

  1. Разные издательства вполне могут иметь серии, названные одинаково.

Page 28: 1. Дополнительные атрибуты

Несколько примеров: Авторы: роль (автор, редактор, переводчик и т. п.); Книги: аннотация; Операции: текущий статус (оплачено, передано в службу доставки и т. п.).

2. Издательства

Надо добавить сущность «Издательство» с атрибутом «Название» (как минимум). Книги связаны с издательствами отношением «многие ко многим»: книга может публиковаться в разных издательствах. Поэтому на физическом уровне потребуется промежуточная таблица «Публикации» с атрибутом «Год издания». (Разумеется, это упрощенная модель; при желании ее можно уточнять еще очень долго.)

3. Серии

Добавим сущность «Серия». К серии относится не сама книга, а ее конкретная публикация, так что имеет смысл вывести «Публикацию» на уровень ER-модели и связать ее с серий отношением «один ко многим» (каждая публикация принадлежит к одной серии, каждая серия может включать несколько публикаций). Также серия связана отношением «один ко многим» с издательством (у издательства может быть несколько серий, а каждая серия принадлежит конкретному издательству). Остается вопрос о внесерийных изданиях. Его можно решить либо введением фиктивной серии «Без серии», либо возможностью не указывать для публикации внешний ключ серии.

4. Компьютерные комплектующие

Рассматривая каждый конкретный тип комплектующих, можно без труда выделить необходимые атрибуты. Какие-то атрибуты будут общими (скажем, фирма-производитель и название модели), а какие-то будут иметь смысл только для данного конкретного типа. Например: Процессор: частота; Монитор: диагональ, разрешение; Жесткий диск: типоразмер, емкость. Проблема в том, что рынок комплектующих очень динамичен. Некоторое время назад жесткие диски определялись частотой вращения и емкостью, а сейчас важен тип (твердотельный, вращающийся, гибридный). Для мониторов во времена ЭЛТ была важна частота обновления, а сейчас важен тип матрицы. Дисководы уже никому не нужны, зато появились флеш-накопители. И так далее. Таким образом, либо придется постоянно изменять схему данных (а, значит, и постоянно изменять приложение, которое работает с этими данными!), либо искать более универсальную модель за счета отказа от жесткой структуры и контроля согласованности. Некоторые универсальные модели (например, хранение части данных в JSON) мы затрагивали в презентации.

08 Занятие 08: SQL / Функции

Исходная страница в нативном Markdown: SQL / Функции
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_08_sql_func.html Archive: DEV1-handouts-16.zip Страниц PDF: 24

Page 1: SQL / Функции

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Функции и их особенности в базах данных
  • Параметры и возвращаемое значение
  • Способы передачи параметров при вызове
  • Категории изменчивости и оптимизация

Page 3: Функции в базе данных

Основной мотив: упрощение задачи

  • интерфейс (параметры) и реализация (тело функции)
  • о функции можно думать вне контекста всей задачи
  • Традиционные языки
  • PostgreSQL
  • побочные глобальные переменные вся база данных эффекты (категории изменчивости)
  • модули со своим интерфейсом пространства имен, и реализацией клиент и сервер
  • сложности накладные расходы скрытие запроса на вызов от планировщика (подстановка) (подстановка, подзапросы, представления)

Комментарии преподавателя

Основная цель появления функций в программировании вообще — упростить решаемую задачу за счет ее декомпозиции на более мелкие подзадачи. Упрощение достигается за счет того, что о функции можно думать, абстрагировавшись от «большой» задачи. Для этого функция определяет четкий интерфейс с внешним миром (параметры и возвращаемое значение). Ее реализация (тело функции) может меняться; вызывающая сторона «не видит» этих изменений и не зависит от них. Этой идеальной ситуации может мешать глобальное состояние (глобальные переменные), и надо учитывать, что в случае БД таким состоянием является вся база данных.

В традиционных языках функции часто объединяются в модули (пакеты, классы для ООП и т. п.), имеющие собственный интерфейс и реализацию. Границы модулей могут проводиться более или менее произвольно. Для PostgreSQL есть жесткая граница между клиентской частью и серверной: серверный код работает с базой, клиентский — управляет транзакциями. Модули (пакеты) отсутствуют, есть только пространства имен.

Для традиционных языков единственный минус широкого использования функций состоит в накладных расходах на их вызов. Иногда его преодолевают с помощью подстановки (inlining) кода функции в вызывающую программу. Для БД последствия могут быть более серьезные: если в функцию выносится часть запроса, планировщик перестает видеть «общую картину» и не может построить хороший план. В некоторых случаях PostgreSQL умеет выполнять подстановку; альтернативные варианты — использование подзапросов или представлений.

Page 4: Общие сведения

Объект базы данных

  • определение хранится в системном каталоге
  • Основные составные части определения
    • имя
    • параметры
    • тип возвращаемого значения
    • тело
  • Доступны несколько языков, в том числе SQL
    • код в виде строковой константы
    • обычно интерпретируется при вызове
  • Вызывается в контексте выражения

Комментарии преподавателя

Функции являются такими же объектами базы данных, как, например, таблицы и индексы. Определение функции сохраняется в системном каталоге; поэтому функции в базе данных называют хранимыми.

В PostgreSQL доступно большое количество стандартных функций, с некоторыми из них можно познакомиться в справочном материале «Основные типы данных и функции».

И, конечно, можно писать собственные функции на разных языках программирования. Материал этой темы относится к функциям на любом языке, но примеры будут использовать язык SQL.

Определение функции состоит из имени, необязательных параметров, типа возвращаемого значения и тела. Тело записывается в виде строковой константы, которая содержит код на выбранном языке. За счет этого определение функции выглядит одинаково независимо от выбранного языка. Тело-строка сохраняется в системном каталоге и интерпретируется каждый раз, когда функция вызывается. Начиная с версии PostgreSQL 14 для кода на SQL появилась возможность производить разбор заранее, в системном каталоге при этом сохраняется не исходный текст, а результат разбора. Еще один способ избежать интерпретации времени выполнения — написать функцию на языке Си, но в данном курсе эта тема не рассматривается.

Функция всегда вызывается в контексте какого-либо выражения. Например, в списке выражений команды SELECT, в условии WHERE, в ограничении целостности CHECK и т. п.

https://postgrespro.ru/docs/postgresql/16/sql-createfunction

https://postgrespro.ru/docs/postgresql/16/sql-syntax-calling-funcs

Page 5: Функции без параметров

Вот простой пример функции без параметров, содержащей один оператор:

=> CREATE FUNCTION hello_world() -- имя и пустой список параметров
RETURNS text                     -- тип возвращаемого значения
AS $$ SELECT 'Hello, world!'; $$ -- тело
LANGUAGE sql;                    -- указание языка
CREATE FUNCTION

Тело удобно записывать в строке, заключенной в кавычки-доллары, как в приведенном примере. Иначе придется заботиться об экранировании кавычек, которые наверняка встретятся в теле функции. Сравните:

=> SELECT ' SELECT ''Hello, world!''; ';
         ?column?
---------------------------
  SELECT 'Hello, world!';
(1 row)
=> SELECT $$ SELECT 'Hello, world!'; $$;
         ?column?
---------------------------
  SELECT 'Hello, world!';
(1 row)

При необходимости кавычки-доллары могут быть вложенными. Для этого в каждой паре кавычек надо использовать разный текст между долларами:

=> SELECT $func$ SELECT $$Hello, world!$$; $func$;
          ?column?
-----------------------------
  SELECT $$Hello, world!$$;
(1 row)

Функция вызывается в контексте выражения, например:

=> SELECT hello_world(); -- пустые скобки обязательны
  hello_world
---------------
 Hello, world!
(1 row)

Давайте взглянем на то, как тело функции хранится в системном каталоге.

=> \pset xheader_width 60
Expanded header width is 60.
=> SELECT proname, prosrc, prosqlbody FROM pg_proc
WHERE proname = 'hello_world' \gx
-[ RECORD 1 ]-------------------------
proname    | hello_world
prosrc     |  SELECT 'Hello, world!';
prosqlbody |

Мы видим сохраненную в исходном виде тело-строку. А теперь реализуем современную возможность пересоздать нашу функцию в другом виде — в стиле стандарта SQL. В нашем случае телом функции может быть один оператор вида RETURN <выражение>:

=> CREATE OR REPLACE FUNCTION hello_world() RETURNS text
LANGUAGE sql
RETURN 'Hello, world!';
CREATE FUNCTION

Снова заглянем в системный каталог — тело функции сохранено по-другому:

=> SELECT proname, prosrc, left(prosqlbody, 100) AS body FROM pg_proc WHERE proname = 'hello_world' \gx

Page 6: Page 6

-[ RECORD 1 ]-----------------------------------------------
proname | hello_world
prosrc  |
body    | {QUERY :commandType 1 :querySource 0 :canSetTag true :utilityStmt <>
:resultRelation 0 :hasAggs fals

Исходный код в этом случае не хранится, получить его можно командой \sf:

=> \sf hello_world
CREATE OR REPLACE FUNCTION public.hello_world()
 RETURNS text
 LANGUAGE sql
RETURN 'Hello, world!'::text

В случае, если тело функции состоит из нескольких операторов SQL, в качестве результата возвращается значение из первой строки, которую вернул последний оператор. Если код такой функции написан в стиле стандарта SQL, потребуется использовать конструкцию BEGIN ATOMIC … END, которая охватывает выполняемый блок операторов:

=> CREATE OR REPLACE FUNCTION hello_world() RETURNS text
LANGUAGE sql
BEGIN ATOMIC
  SELECT 'First Line';
  SELECT 'Second Line';
END;
CREATE FUNCTION

Пробуем вызов:

=> SELECT hello_world();
 hello_world
-------------
 Second Line
(1 row)

Обратите внимание на особенности синтаксиса стиля стандарта SQL — в отличие от традиционного «строчного»: нет конструкции AS, содержащей код функции в виде строки; может использоваться новое ключевое слово RETURN для возврата значения; указание LANGUAGE sql не является обязательным; при создании функции ее код разбирается, а результат разбора сохраняется в pg_proc.prosqlbody (в традиционной нотации текст функции сохраняется в pg_proc.prosrc). Это лучше соответствует стандарту и в большей мере совместимо с другими реализациями SQL. Теперь при вызове функции ее команды заново не интерпретируются, а используется заранее разобранный вариант. Не все операторы SQL можно использовать в функции. Запрещены: команды управления транзакциями (BEGIN, COMMIT, ROLLBACK и т. п.); служебные команды (такие, как VACUUM или CREATE INDEX). Вот пример неправильной функции. Здесь мы использовали псевдотип void, который говорит о том, что функция не возвращает ничего.

=> CREATE FUNCTION do_commit() RETURNS void
LANGUAGE sql
BEGIN ATOMIC COMMIT; END;
ERROR:  COMMIT is not yet supported in unquoted SQL function body

Управлять транзакциями можно в процедурах, о чем мы будет говорить в следующей теме.

Функции с входными параметрами

Пример функции с одним параметром:

=> CREATE FUNCTION hello(name text) -- формальный параметр
RETURNS text
LANGUAGE sql
RETURN 'Hello, ' || name || '!';
CREATE FUNCTION

При вызове функции мы указываем фактический параметр, соответствующий формальному:

=> SELECT hello('Alice');

Page 7: Page 7

     hello
---------------
 Hello, Alice!
(1 row)

При указании типа параметра можно указать и модификатор (например, varchar(10)), но он игнорируется. Можно определить параметр функции без имени; тогда внутри тела функции на параметры придется ссылаться по номеру. Удалим функцию и создадим новую:

=> DROP FUNCTION hello(text); -- достаточно указать тип параметра
DROP FUNCTION
=> CREATE FUNCTION hello(text)
RETURNS text
LANGUAGE sql
RETURN 'Hello, ' || $1 || '!'; -- номер вместо имени
CREATE FUNCTION
=> SELECT hello('Alice');
     hello
---------------
 Hello, Alice!
(1 row)

Но так лучше не делать, это неудобно. Удалим функцию и создадим заново, добавив еще два параметра — приветствие и обращение.

=> DROP FUNCTION hello(text);
DROP FUNCTION

Здесь мы используем необязательное ключевое слово IN, обозначающее входной параметр. Предложение DEFAULT позволяет определить значение по умолчанию для параметра:

=> CREATE FUNCTION hello(IN name text, IN greet text DEFAULT 'Dear', IN title text DEFAULT 'Mr')
RETURNS text
LANGUAGE sql
RETURN 'Hello, ' || greet || ' ' || title || ' ' || name || '!';
CREATE FUNCTION
=> SELECT hello('Alice', 'Charming', 'Mrs'); -- указаны второй и третий параметры
           hello
----------------------------
 Hello, Charming Mrs Alice!
(1 row)

Обратите внимание, что параметры со значениями по умолчанию должны идти в конце всего списка. При вызове функции значения фактических параметров, определенных как default, можно опускать, тогда остальные default-параметры, идущие в списке после, также получат значения по умолчанию

=> SELECT hello('Bob', 'Excellent'); -- указан только первый default-параметр
          hello
--------------------------
 Hello, Excellent Mr Bob!
(1 row)
=> SELECT hello('Bob'); -- опущены оба параметра, имеющие значение по умолчанию
        hello
---------------------
 Hello, Dear Mr Bob!
(1 row)

До сих пор мы вызывали функцию, указывая фактические параметры позиционным способом — в том порядке, в котором они определены при создании функции. Во многих стандартных функциях имена параметров не заданы, так что этот способ оказывается единственным. Но если формальным параметрам даны имена, можно использовать их при указании фактических параметров. В этом случае параметры могут указываться в произвольном порядке:

=> SELECT hello(title => 'Mrs', name => 'Alice');

Page 8: Page 8

         hello
------------------------
 Hello, Dear Mrs Alice!
(1 row)

Такой способ удобен, когда порядок параметров неочевиден, особенно если их много; также можно явно указать значение одного из параметров по умолчанию. Можно совмещать оба способа: часть параметров (начиная с первого) указать позиционно, а оставшиеся — по имени:

=> SELECT hello('Alice', title => 'Mrs');
         hello
------------------------
 Hello, Dear Mrs Alice!
(1 row)

Если функция должна возвращать неопределенное значение, когда хотя бы один из входных параметров не определен, ее можно объявить как строгую (STRICT). Тело функции при этом вообще не будет выполняться.

=> DROP FUNCTION hello(text, text, text);
DROP FUNCTION
=> CREATE FUNCTION hello(IN name text, IN title text DEFAULT 'Mr')
RETURNS text
LANGUAGE sql STRICT
RETURN 'Hello, ' || title || ' ' || name || '!';
CREATE FUNCTION
=> SELECT hello('Alice', NULL);
 hello
-------
(1 row)

Page 9: Вход и выход

Входные значения

  • определяются параметрами с режимом IN и INOUT
  • Выходное значение
    • определяется либо предложением RETURNS,
    • либо параметрами с режимом INOUT и OUT
    • если одновременно указаны обе формы, они должны быть согласованы

Комментарии преподавателя

Формальные параметры с режимом IN и INOUT считаются входными. Значения соответствующих фактических параметров должны быть указаны при вызове функции (либо должны быть определены значения по умолчанию).

Возвращаемое значение можно определить двумя способами:

  • использовать предложение RETURNS для указания типа;

  • определить выходные параметры с режимом INOUT или OUT.

Две эти формы записи эквивалентны. Например, функция с указанием RETURNS integer и функция с параметром OUT integer возвращают целое число.

Можно использовать и оба способа одновременно. В этом случае функция также будет возвращать одно целое число. Но при этом типы RETURNS и выходных параметров должны быть согласованы друг с другом.

Таким образом, нельзя написать функцию, которая будет возвращать одно значение, и при этом передавать другое значение в OUT-параметре — что позволяет большинство традиционных языков программирования.

Page 10: Функции с выходными параметрами

Альтернативный способ вернуть значение — использовать выходной параметр.

=> DROP FUNCTION hello(text, text);
DROP FUNCTION
=> CREATE FUNCTION hello(
    IN name text,
    OUT text -- имя можно не указывать, если оно не нужно
)
LANGUAGE sql
RETURN 'Hello, ' || name || '!';
CREATE FUNCTION
=> SELECT hello('Alice');
     hello
---------------
 Hello, Alice!
(1 row)

Результат тот же самый. Можно использовать и RETURNS, и OUT-параметр вместе — результат снова будет тем же:

=> DROP FUNCTION hello(text); -- OUT-параметры не указываем
DROP FUNCTION
=> CREATE FUNCTION hello(IN name text, OUT text)
RETURNS text
LANGUAGE sql
RETURN 'Hello, ' || name || '!';
CREATE FUNCTION
=> SELECT hello('Alice');
     hello
---------------
 Hello, Alice!
(1 row)

Или даже так, использовав INOUT-параметр:

=> DROP FUNCTION hello(text);
DROP FUNCTION
=> CREATE FUNCTION hello(INOUT name text)
LANGUAGE sql
RETURN 'Hello, ' || name || '!';
CREATE FUNCTION
=> SELECT hello('Alice');
     hello
---------------
 Hello, Alice!
(1 row)

Обратите внимание, что, в отличие от многих языков программирования, фактическое значение, переданное SQL-функции в INOUT-параметре, никак не изменяется: мы передаем входное значение, а выходное возвращается функцией в качестве результата. Поэтому мы можем указать константу, хотя другие языки требовали бы переменную. В то время как в RETURNS можно указать только одно значение, выходных параметров может быть несколько. Например:

=> DROP FUNCTION hello(text);
DROP FUNCTION

Page 11: Page 11

=> CREATE FUNCTION hello(
    IN name text,
    OUT greeting text,
    OUT clock timetz)
LANGUAGE sql
RETURN ('Hello, ' || name || '!', current_time);
CREATE FUNCTION

Здесь возвращаемое RETURN выражение пришлось взять в скобки.

=> SELECT hello('Alice');
                hello
--------------------------------------
 ("Hello, Alice!",15:36:23.105545+03)
(1 row)

Действительно, наша функция вернула не одно значение, а сразу несколько. Подробнее о такой возможности и составных типах мы будем говорить в теме «SQL. Составные типы».

Page 12: Категории изменчивости

Volatile

  • возвращаемое значение может произвольно меняться
  • при одинаковых значениях входных параметров
  • используется по умолчанию
  • Stable
    • значение не меняется в пределах одного оператора SQL
    • функция не может менять состояние базы данных
  • Immutable
    • значение не меняется, функция детерминирована
    • функция не может менять состояние базы данных

Комментарии преподавателя

Каждой функции сопоставлена категория изменчивости, которая определяет свойства возвращаемого значения при одинаковых значениях входных параметров.

Категория volatile говорит о том, что возвращаемое значение может произвольно меняться. Такие функции будут выполняться каждый раз при каждом вызове. Если при создании функции категория не указана, назначается именно эта категория.

Категория stable используется для функций, возвращаемое значение которых не меняется в пределах одного SQL-оператора. В частности, такие функции не могут менять состояние БД. Такая функция может быть выполнена один раз во время выполнения запроса, а затем будет использоваться вычисленное значение.

Категория immutable еще более строгая: возвращаемое значение не меняется никогда. Такую функцию можно выполнить на этапе планирования запроса, а не во время выполнения.

Можно — не означает, что всегда происходит именно так, но планировщик вправе выполнить такие оптимизации. В некоторых (простых) случаях планировщик делает собственные выводы об изменчивости функции, невзирая на указанную явно категорию.

https://postgrespro.ru/docs/postgresql/16/xfunc-volatility

Page 13: Категории изменчивости и изоляция

В целом использование функций внутри запросов не нарушает установленный уровень изоляции транзакции, но есть два момента, о которых полезно знать. Во-первых, функции с изменчивостью volatile на уровне изоляции Read Committed приводят к рассогласованию данных внутри одного запроса. Сделаем функцию, возвращающую число строк в таблице:

=> CREATE TABLE t(n integer);
CREATE TABLE
=> CREATE FUNCTION cnt() RETURNS bigint
LANGUAGE sql VOLATILE
RETURN (SELECT count(*) FROM t);
CREATE FUNCTION

Теперь вызовем ее несколько раз с задержкой, а в параллельном сеансе вставим в таблицу строку.

=> BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN
=> SELECT (SELECT count(*) FROM t), cnt(), pg_sleep(1)
FROM generate_series(1,4);
=> INSERT INTO t VALUES (1);
INSERT 0 1
 count | cnt | pg_sleep
-------+-----+----------
     0 |   0 |
     0 |   0 |
     0 |   1 |
     0 |   1 |
(4 rows)
=> END;
COMMIT

При изменчивости stable или immutable, либо при использовании более строгих уровней изоляции такого не происходит потому, что в этих случаях всегда используется снимок основного запроса. В предыдущем же примере (volatile, Read Committed) основной запрос и запросы в функции используют разные снимки данных.

=> ALTER FUNCTION cnt() STABLE;
ALTER FUNCTION
=> TRUNCATE t;
TRUNCATE TABLE
=> BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN
=> SELECT (SELECT count(*) FROM t), cnt(), pg_sleep(1)
FROM generate_series(1,4);
=> INSERT INTO t VALUES (1);
INSERT 0 1
 count | cnt | pg_sleep
-------+-----+----------
     0 |   0 |
     0 |   0 |
     0 |   0 |
     0 |   0 |
(4 rows)
=> END;

Page 14: Page 14

COMMIT

Второй момент связан с видимостью изменений, сделанных собственной транзакцией. Функции с изменчивостью volatile видят все изменения, в том числе сделанные текущим, еще не завершенным оператором SQL.

=> ALTER FUNCTION cnt() VOLATILE;
ALTER FUNCTION
=> TRUNCATE t;
TRUNCATE TABLE
=> INSERT INTO t SELECT cnt() FROM generate_series(1,5);
INSERT 0 5
=> SELECT * FROM t;
 n
---
 0
 1
 2
 3
 4
(5 rows)

Это верно для любых уровней изоляции. Функции с изменчивостью stable или immutable видят изменения только уже завершенных операторов.

=> ALTER FUNCTION cnt() STABLE;
ALTER FUNCTION
=> TRUNCATE t;
TRUNCATE TABLE
=> INSERT INTO t SELECT cnt() FROM generate_series(1,5);
INSERT 0 5
=> SELECT * FROM t;
 n
---
 0
 0
 0
 0
 0
(5 rows)

Категории изменчивости и оптимизация

Благодаря дополнительной информации о поведении функции, которую дает указание категории изменчивости, оптимизатор может сэкономить на вызовах функции. Для экспериментов создадим функцию, возвращающую случайное число:

=> CREATE FUNCTION rnd() RETURNS float
LANGUAGE sql VOLATILE
RETURN random();
CREATE FUNCTION

Проверим план выполнения следующего запроса:

=> EXPLAIN (costs off)
SELECT * FROM generate_series(1,10) WHERE rnd() > 0.5;
                   QUERY PLAN
------------------------------------------------
 Function Scan on generate_series
   Filter: (random() > '0.5'::double precision)
(2 rows)

Page 15: Page 15

В плане мы видим «честное» обращение к функции generate_series; каждая строка результата сравнивается со случайным числом и при необходимости отбрасывается фильтром. В этом можно убедиться и воочию:

=> SELECT * FROM generate_series(1,10) WHERE rnd() > 0.5;
 generate_series
-----------------
               1
               2
               3
               4
               7
               8
               9
              10
(8 rows)
=> \g
 generate_series
-----------------
               1
               2
               3
               4
(4 rows)
=> \g
 generate_series
-----------------
               1
               3
               4
               7
               8
(5 rows)
=> \g
 generate_series
-----------------
               1
               3
               4
               5
               6
               7
               8
               9
(8 rows)
=> \g
 generate_series
-----------------
               2
               9
              10
(3 rows)

Здесь с разной вероятностью получаем от 0 до 10 строк. Функция с изменчивостью stable будет вызвана всего один раз — поскольку мы фактически указали, что ее значение не может измениться в пределах оператора:

=> ALTER FUNCTION rnd() STABLE;
ALTER FUNCTION
=> EXPLAIN (costs off)
SELECT * FROM generate_series(1,10) WHERE rnd() > 0.5;

Page 16: Page 16

                      QUERY PLAN
------------------------------------------------------
 Result
   One-Time Filter: (rnd() > '0.5'::double precision)
   ->  Function Scan on generate_series
(3 rows)

Результатом запроса будет либо 0, либо 10 строк.

=> SELECT * FROM generate_series(1,10) WHERE rnd() > 0.5;
 generate_series
-----------------
(0 rows)
=> \g
 generate_series
-----------------
               1
               2
               3
               4
               5
               6
               7
               8
               9
              10
(10 rows)
=> \g
 generate_series
-----------------
               1
               2
               3
               4
               5
               6
               7
               8
               9
              10
(10 rows)

Наконец, изменчивость immutable позволяет вычислить значение функции еще на этапе планирования, поэтому во время выполнения вычисление фильтра уже не требуется:

=> ALTER FUNCTION rnd() IMMUTABLE;
ALTER FUNCTION
=> EXPLAIN (costs off)
SELECT * FROM generate_series(1,10) WHERE rnd() > 0.5;
        QUERY PLAN
--------------------------
 Result
   One-Time Filter: false
(2 rows)
=> \g
            QUERY PLAN
----------------------------------
 Function Scan on generate_series
(1 row)
=> \g

Page 17: Page 17

        QUERY PLAN
--------------------------
 Result
   One-Time Filter: false
(2 rows)

Для immutable получаем случайный план! Ответственность «за дачу заведомо ложных показаний» лежит на разработчике.

Подстановка тела функции в SQL-запрос

В некоторых (очень простых) случаях тело функции на языке SQL может быть подставлено прямо в основной SQL-оператор на этапе разбора запроса. В этом случае время на вызов функции не тратится. Упрощенно — требуется выполнение следующих условий: Тело функции состоит из одного оператора SELECT; Нет обращений к таблицам, отсутствуют подзапросы, группировки и т. п.; Возвращаемое значение должно быть одно; Вызываемые функции не должны противоречить указанной категории изменчивости. Пример мы уже видели: наша функция rnd(), объявленная volatile. Посмотрим еще раз.

=> ALTER FUNCTION rnd() VOLATILE;
ALTER FUNCTION
=> EXPLAIN (costs off)
SELECT * FROM generate_series(1,10) WHERE rnd() > 0.5;
                   QUERY PLAN
------------------------------------------------
 Function Scan on generate_series
   Filter: (random() > '0.5'::double precision)
(2 rows)

В фильтре упоминается функция random(), но не rnd(). Она будет вызываться напрямую, минуя «обертку» в виде функции rnd().

Page 18: Итоги

Можно создавать собственные функции и использовать их так же, как и встроенные

  • Функции можно писать на разных языках, в том числе SQL
  • Изменчивость влияет на возможности оптимизации
  • Иногда функция на SQL может быть подставлена в запрос

Page 19: Практика

  1. Создайте функцию author_name для формирования имени автора. Функция принимает три параметра (фамилия, имя, отчество) и возвращает строку с фамилией и инициалами.
  • Используйте эту функцию в представлении authors_v. 2. Создайте функцию book_name для формирования названия книги. Функция принимает два параметра (идентификатор книги и заголовок) и возвращает строку, составленную из заголовка и списка авторов в порядке seq_num. Имя каждого автора формируется функцией author_name.
  • Используйте эту функцию в представлении catalog_v. Проверьте изменения в приложении.

Комментарии преподавателя

Напомним, что необходимые функции можно посмотреть в раздаточном материале «Основные типы данных и функции».

1. FUNCTION author_name(
    last_name text, first_name text, middle_name text
)
RETURNS text
Например: author_name('Толстой','Лев','Николаевич') →
→ 'Толстой Л. Н.'
3. FUNCTION book_name(book_id integer, title text)
RETURNS text
Например: book_name(3,'Трудно быть богом') →
→ 'Трудно быть богом. Стругацкий А. Н., Стругацкий Б. Н.'

Все инструменты позволяют «непосредственно» редактировать хранимые функции. Например, в psql есть команда \ef, открывающая текст функции в редакторе и сохраняющая изменения в базу.

Такой возможностью лучше не пользоваться (или как минимум не злоупотреблять). В нормально построенном процессе разработки весь код должен находиться в файлах под версионным контролем. При необходимости изменить функцию файл редактируется и выполняется (с помощью psql или средствами IDE). Если же менять определение функций сразу в БД, изменения легко потерять. (Вообще же вопрос организации процесса разработки намного сложнее и в курсе мы его не затрагиваем.)

Page 20: 1. Функция author_name

=> CREATE FUNCTION author_name(
    last_name text,
    first_name text,
    middle_name text
) RETURNS text
LANGUAGE sql IMMUTABLE
RETURN last_name || ' ' ||
       left(first_name, 1) || '.' ||
       CASE WHEN middle_name != '' -- подразумевает NOT NULL
           THEN ' ' || left(middle_name, 1) || '.'
           ELSE ''
       END;
CREATE FUNCTION

Категория изменчивости — immutable. Функция всегда возвращает одинаковое значение при одних и тех же входных параметрах.

=> CREATE OR REPLACE VIEW authors_v AS
SELECT a.author_id,
       author_name(a.last_name, a.first_name, a.middle_name) AS display_name
FROM   authors a
ORDER BY display_name;
CREATE VIEW

2. Функция book\_name

=> CREATE FUNCTION book_name(book_id integer, title text)
RETURNS text
LANGUAGE sql STABLE
RETURN (
SELECT title || '. ' ||
       string_agg(
           author_name(a.last_name, a.first_name, a.middle_name), ', '
           ORDER BY ash.seq_num
       )
FROM   authors a
       JOIN authorship ash ON a.author_id = ash.author_id
WHERE  ash.book_id = book_name.book_id
);
CREATE FUNCTION

Категория изменчивости — stable. Функция возвращает одинаковое значение при одних и тех же входных параметрах, но только в рамках одного SQL-запроса.

=> CREATE OR REPLACE VIEW catalog_v AS
SELECT b.book_id,
       book_name(b.book_id, b.title) AS display_name
FROM   books b
ORDER BY display_name;
CREATE VIEW

Page 21: Практика+

  1. Напишите функцию, выдающую случайное время, равномерно распределенное в указанном отрезке. Начало отрезка задается временной отметкой (timestamptz), конец — либо временной отметкой, либо интервалом (interval). 2. В таблице хранятся номера автомобилей, введенные кое-как: встречаются как латинские, так и русские буквы в любом регистре; между буквами и цифрами могут быть пробелы. Считая, что формат номера «буква три-цифры две-буквы», напишите функцию, выдающую число уникальных номеров. Например, «К 123 ХМ» и «k123xm» считаются равными. 3. Напишите функцию, находящую корни квадратного уравнения.

Комментарии преподавателя

Во всех заданиях обратите особое внимание на категорию изменчивости функций.

  1. Сначала напишите функцию «нормализации» номера, то есть приводящую номер к какому-нибудь стандартному виду. Например, без пробелов и только заглавными латинскими буквами.

В номерах используются только 12 русских букв, имеющих латинские аналоги аналогичного начертания, а именно: АВЕКМНОРСТУХ.

  1. Для уравнения вида y = ax2 + bx + c вычисляется дискриминант D = b2 – 4ac:
  • при D > 0 два корня x1,2 = (–b ± √D) / 2a;

  • при D = 0 один корень x = –b / 2a (в качестве x2 можно вернуть null);

  • при D < 0 корней нет (оба корня null).

Page 22: 1. Случайная временнáя отметка

=> CREATE DATABASE sql_func;
CREATE DATABASE
=> \c sql_func
You are now connected to database "sql_func" as user "student".

Функция с двумя временными отметками:

=> CREATE FUNCTION rnd_timestamp(t_start timestamptz, t_end timestamptz)
RETURNS timestamptz
LANGUAGE sql VOLATILE
RETURN t_start + (t_end - t_start) * random();
CREATE FUNCTION

Категория изменчивости — volatile. Используется функция random, поэтому функция будет возвращать разные значения при одних и тех же входных параметрах.

=> SELECT current_timestamp,
    rnd_timestamp(
        current_timestamp,
        current_timestamp + interval '1 hour'
    )
FROM generate_series(1,10);
      current_timestamp       |         rnd_timestamp
------------------------------+-------------------------------
 2026-04-10 15:44:34.21506+03 | 2026-04-10 15:53:13.234155+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 15:48:29.981534+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 16:32:55.195133+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 15:54:45.292437+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 16:35:16.553482+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 16:11:44.099131+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 15:58:13.044624+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 15:56:15.470016+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 16:35:39.157347+03
 2026-04-10 15:44:34.21506+03 | 2026-04-10 16:14:34.447788+03
(10 rows)

Вторую функцию (с параметром-интервалом) можно определить через первую:

=> CREATE FUNCTION rnd_timestamp(t_start timestamptz, t_delta interval)
RETURNS timestamptz
LANGUAGE sql VOLATILE
RETURN rnd_timestamp(t_start, t_start + t_delta);
CREATE FUNCTION
=> SELECT rnd_timestamp(current_timestamp, interval '1 hour');
         rnd_timestamp
-------------------------------
 2026-04-10 16:16:50.908848+03
(1 row)

2. Автомобильные номера

Создадим таблицу с номерами.

=> CREATE TABLE cars(
    id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    regnum text
);
CREATE TABLE
=> INSERT INTO cars(regnum) VALUES
    ('К 123 ХМ'), ('k123xm'), ('A 098BC');
INSERT 0 3

Функция нормализации:

Page 23: Page 23

=> CREATE FUNCTION to_normal(regnum text) RETURNS text
LANGUAGE sql IMMUTABLE
RETURN upper(translate(regnum, 'АВЕКМНОРСТУХ ', 'ABEKMHOPCTYX'));
CREATE FUNCTION

Категория изменчивости — immutable. Функция всегда возвращает одинаковое значение при одних и тех же входных параметрах.

=> SELECT to_normal(regnum) FROM cars;
 to_normal
-----------
 K123XM
 K123XM
 A098BC
(3 rows)

Теперь легко исключить дубликаты:

=> CREATE FUNCTION num_unique() RETURNS bigint
LANGUAGE sql STABLE
RETURN (
SELECT count(DISTINCT to_normal(regnum))
FROM cars
);
CREATE FUNCTION
=> SELECT num_unique();
 num_unique
------------
          2
(1 row)

3. Корни квадратного уравнения

=> CREATE FUNCTION square_roots(
    a float,
    b float,
    c float,
    x1 OUT float,
    x2 OUT float
)
LANGUAGE sql IMMUTABLE
RETURN (
WITH discriminant(d) AS (
    SELECT b*b - 4*a*c
)
SELECT (CASE WHEN d >= 0.0 THEN (-b + sqrt(d))/2/a END,
        CASE WHEN d >  0.0 THEN (-b - sqrt(d))/2/a END)
FROM discriminant
);
CREATE FUNCTION

Категория изменчивости — immutable. Функция всегда возвращает одинаковое значение при одних и тех же входных параметрах.

=> SELECT square_roots(1,  0, -4);
 square_roots
--------------
 (2,-2)
(1 row)
=> SELECT square_roots(1, -4,  4);
 square_roots
--------------
 (2,)
(1 row)
=> SELECT square_roots(1,  1,  1);

Page 24: Page 24

 square_roots
--------------
 (,)
(1 row)
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE sql_func;
DROP DATABASE

09 Занятие 09: SQL / Процедуры

Исходная страница в нативном Markdown: SQL / Процедуры
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_09_sql_proc.html Archive: DEV1-handouts-16.zip Страниц PDF: 19

Page 1: SQL / Процедуры

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Процедуры и их отличие от функций
  • Входные и выходные параметры
  • Перегрузка и полиморфизм

Page 3: Подпрограммы

Функции

  • вызываются в контексте выражения
  • не могут управлять транзакциями
  • возвращают результат
  • Процедуры
    • вызываются оператором CALL
    • могут управлять транзакциями
    • могут возвращать результат

Комментарии преподавателя

Процедуры были введены в PostgreSQL 11. Основная причина их появления состоит в том, что функции не могут управлять транзакциями. Функции вызываются в контексте какого-либо выражения, которое вычисляется как часть уже начатого оператора (например, SELECT) в уже начатой транзакции. Нельзя завершить транзакцию и начать новую «посередине» выполнения оператора.

Процедуры всегда вызываются специальным оператором CALL. Если этот оператор сам начинает новую транзакцию (а не вызывается из уже начатой), то в процедуре можно использовать команды управления транзакциями.

К сожалению, процедуры, написанные на языке SQL, лишены сейчас возможности использовать команды COMMIT и ROLLBACK, хотя те из них, что оформлены в новом стиле стандарта SQL, вероятно смогут это делать в будущем. Поэтому пример процедуры, управляющей транзакциями, придется отложить до темы «PL/pgSQL. Выполнение запросов».

Иногда можно услышать, что процедура отличается от функции тем, что не возвращает результат. Но это не так — процедуры тоже могут возвращать результат, если необходимо.

Функции и процедуры имеют общее пространство имен и вместе называются подпрограммами (routine).

https://postgrespro.ru/docs/postgresql/16/sql-createprocedure

https://postgrespro.ru/docs/postgresql/16/sql-call

Page 4: Процедуры без параметров

Начнем с примера простой процедуры без параметров.

=> CREATE TABLE t(a float);
CREATE TABLE
=> CREATE PROCEDURE fill()
AS $$
    TRUNCATE t;
    INSERT INTO t SELECT random() FROM generate_series(1,3);
$$ LANGUAGE sql;
CREATE PROCEDURE

Чтобы вызвать процедуру, необходимо использовать специальный оператор:

=> CALL fill();
CALL

Результат работы виден в таблице:

=> SELECT * FROM t;
          a
---------------------
  0.6555830623802215
 0.08906944464482747
  0.8696380776099704
(3 rows)

А теперь переопределим нашу процедуру в стиле стандарта SQL:

=> CREATE OR REPLACE PROCEDURE fill()
LANGUAGE sql
BEGIN ATOMIC
    DELETE FROM t;  -- команда TRUNCATE пока что не поддерживается в таких подпрограммах
    INSERT INTO t SELECT random() FROM generate_series(1,3);
END;
CREATE PROCEDURE

Убедимся в ее работоспособности:

=> CALL fill();
CALL
=> SELECT * FROM t;
         a
--------------------
 0.8232510652064371
 0.7274001897871394
 0.8499914745554373
(3 rows)

И попробуем в процедуре выполнить фиксацию транзакции:

=> CREATE OR REPLACE PROCEDURE fill()
LANGUAGE sql
BEGIN ATOMIC
    DELETE FROM t;
    INSERT INTO t SELECT random() FROM generate_series(1,3);
    COMMIT;
END;
ERROR:  COMMIT is not yet supported in unquoted SQL function body

Обратите внимание, что мы получили ошибку о недопустимой команде еще на этапе определения подпрограммы. Переименуем таблицу, с которой работает наша процедура:

=> ALTER TABLE t RENAME TO ta;
ALTER TABLE

Page 5: Page 5

Вызов ниже не приведет к ошибке — таблица в определении процедуры в системном каталоге теперь представлена не по имени, а по идентификатору, который был получен еще на этапе создания подпрограммы.

=> CALL fill();
CALL

Ту же самую задачу, что выполняла процедура, можно решить и с помощью функции, возвращаемое значение которой определяется последним оператором. Можно объявить тип результата void, если фактически функция ничего не возвращает, или вернуть что-то осмысленное. Возвратим таблице прежнее имя и определим функцию:

=> ALTER TABLE ta RENAME TO t;
ALTER TABLE
=> CREATE FUNCTION fill_avg() RETURNS float
LANGUAGE sql
BEGIN ATOMIC
    DELETE FROM t;
    INSERT INTO t SELECT random() FROM generate_series(1, 3);
    SELECT avg(a) FROM t;
END;
CREATE FUNCTION

В любом случае функция вызывается в контексте какого-либо выражения:

=> SELECT fill_avg();
      fill_avg
--------------------
 0.7256557081193605
(1 row)
=> SELECT * FROM t;
         a
--------------------
 0.7228244239143955
 0.9129684235340032
 0.5411742769096826
(3 rows)

Чего нельзя достичь с помощью функции — это управления транзакциями. Но и в процедурах на языке SQL, как мы видели, это пока не поддерживается (зато поддерживается при использовании других языков).

Процедуры с параметрами

Добавим в процедуру входной параметр — число строк:

=> DROP PROCEDURE fill();
DROP PROCEDURE
=> CREATE PROCEDURE fill(nrows integer)
LANGUAGE sql
BEGIN ATOMIC
    DELETE FROM t;
    INSERT INTO t SELECT random() FROM generate_series(1, nrows);
END;
CREATE PROCEDURE

Точно так же, как и в случае функций, при вызове процедур фактические параметры можно передавать позиционным способом или по имени:

=> CALL fill(nrows => 5);
CALL
=> SELECT * FROM t;

Page 6: Page 6

          a
---------------------
  0.6157560832219215
  0.8004849398627427
 0.37948868752520437
  0.6064821790562178
 0.23810577112893738
(5 rows)

Процедуры могут также иметь OUT-и INOUT-параметры, с помощью которых можно возвращать значения:

=> DROP PROCEDURE fill(integer);
DROP PROCEDURE
=> CREATE PROCEDURE fill(IN nrows integer, OUT average float)
LANGUAGE sql
BEGIN ATOMIC
    DELETE FROM t;
    INSERT INTO t SELECT random() FROM generate_series(1, nrows);
    SELECT avg(a) FROM t; -- как в функции
END;
CREATE PROCEDURE

Попробуем:

=> CALL fill(5, NULL /* значение не используется, но его необходимо указать*/);
      average
--------------------
 0.5911483639915046
(1 row)

Page 7: Перегрузка

Несколько подпрограмм с одним и тем же именем

  • подпрограмма однозначно определяется сигнатурой —
  • именем и входными параметрами
  • тип возвращаемого значения и выходные параметры игнорируются
  • подходящая подпрограмма выбирается во время выполнения
  • в зависимости от фактических параметров
  • Команда CREATE OR REPLACE
    • при несовпадении типов входных параметров создаст новую
    • перегруженную подпрограмму
    • при совпадении — изменит существующую подпрограмму,
    • но нельзя поменять тип подпрограммы, тип возвращаемого значения,
    • типы OUT-параметров

Комментарии преподавателя

Перегрузка — это возможность использования одного и того же имени для нескольких подпрограмм, отличающихся типами параметров IN и INOUT.

Сигнатура подпрограммы — ее имя и типы входных параметров. При вызове PostgreSQL находит подпрограмму, соответствующую сигнатуре. Возможны ситуации, когда подходящую подпрограмму невозможно определить однозначно; в таком случае во время выполнения возникнет ошибка.

Однако в сигнатуру, в частности, не входят:

  • тип подпрограммы (процедура или функция);

  • типы параметров OUT;

  • тип возвращаемого значения.

Перегрузку надо учитывать, выполняя команду CREATE OR REPLACE (FUNCTION или PROCEDURE). Если сигнатура не совпадает ни с одной из существующих, будет создана новая перегруженная подпрограмма, а при совпадении — изменена существующая. В последнем случае не разрешается изменять тип подпрограммы, тип OUT-параметров или тип возвращаемого значения, однако можно изменить язык и другие свойства. Поэтому иногда приходится удалять подпрограмму и создавать ее заново, в этом случае потребуется также удалить зависящие от нее объекты: другие подпрограммы, представления, триггеры и т. п. (DROP ROUTINE … CASCADE).

https://postgrespro.ru/docs/postgresql/16/xfunc-overload

Page 8: Полиморфизм

Подпрограмма, принимающая параметры разных типов

  • формальные параметры используют полиморфные псевдотипы
  • (например, anyelement или anycompatible)
  • конкретный тип данных выбирается во время выполнения
  • по типу фактических параметров

Комментарии преподавателя

В некоторых случаях удобно не создавать несколько перегруженных подпрограмм для разных типов, а написать одну, принимающую параметры любого (или почти любого) типа.

Для этого в качестве типа формального параметра указывается специальный полиморфный псевдотип. Пока мы рассмотрим два из них — anyelement и anycompatible — но позже встретимся и с другими.

Полиморфные псевдотипы в определении позволяют использовать в качестве параметров любые типы, при этом конкретный тип, с которым будет работать подпрограмма, выбирается во время выполнения по типу фактического параметра.

Если в определении указано несколько полиморфных параметров типа anyelement, то все фактические параметры неявно приводятся к типу первого из них. Если же указано несколько параметров типа anycompatible, то типы фактических параметров приводятся к некоторому общему типу.

Если подпрограмма объявлена с возвращаемым значением полиморфного типа, то она должна иметь по крайней мере один входной полиморфный параметр. Конкретный тип возвращаемого значения также определяется исходя из типа фактического входного параметра. Для подпрограмм в стиле стандарта SQL возможности использовать полиморфные типы данных для аргументов нет.

https://postgrespro.ru/docs/postgresql/16/extend-type-system#EXTEND-TY PES-POLYMORPHIC

https://postgrespro.ru/docs/postgresql/16/xfunc-sql#XFUNC-SQL-POLYMO RPHIC-FUNCTIONS

Page 9: Перегруженные подпрограммы

Перегрузка работает одинаково и для функций, и для процедур. Они имеют общее пространство имен. В качестве примера напишем функцию, возвращающую большее из двух целых чисел. (Похожее выражение есть в SQL и называется greatest, но мы напишем собственную функцию.)

=> CREATE FUNCTION maximum(a integer, b integer) RETURNS integer
LANGUAGE sql
RETURN CASE WHEN a > b THEN a ELSE b END;
CREATE FUNCTION

Проверим:

=> SELECT maximum(10, 20);
 maximum
---------
      20
(1 row)

Допустим, мы решили сделать аналогичную функцию для трех чисел. Благодаря перегрузке, не надо придумывать для нее какое-то новое название:

=> CREATE FUNCTION maximum(a integer, b integer, c integer)
RETURNS integer
LANGUAGE sql
RETURN CASE
         WHEN a > b THEN maximum(a, c)
         ELSE maximum(b, c)
       END;
CREATE FUNCTION

Теперь у нас две функции с одним именем, но разным числом параметров:

=> \df maximum
                              List of functions
 Schema |  Name   | Result data type |       Argument data types       | Type
--------+---------+------------------+---------------------------------+------
 public | maximum | integer          | a integer, b integer            | func
 public | maximum | integer          | a integer, b integer, c integer | func
(2 rows)

И обе работают:

=> SELECT maximum(10, 20), maximum(10, 20, 30);
 maximum | maximum
---------+---------
      20 |      30
(1 row)

Команда CREATE OR REPLACE позволяет создать подпрограмму или заменить существующую, не удаляя ее. Поскольку в данном случае функция с такой сигнатурой уже существует, она будет заменена:

=> CREATE OR REPLACE FUNCTION maximum(a integer, b integer, c integer)
RETURNS integer
LANGUAGE sql
RETURN CASE
         WHEN a > b THEN
           CASE WHEN a > c THEN a ELSE c END
         ELSE
           CASE WHEN b > c THEN b ELSE c END
         END;
CREATE FUNCTION

Пусть наша функция работает не только для целых чисел, но и для вещественных. Как этого добиться? Можно определить еще такую функцию:

=> CREATE FUNCTION maximum(a real, b real) RETURNS real
LANGUAGE sql
RETURN CASE WHEN a > b THEN a ELSE b END;

Page 10: Page 10

CREATE FUNCTION

Теперь у нас три функции с одинаковым именем:

=> \df maximum
                              List of functions
 Schema |  Name   | Result data type |       Argument data types       | Type
--------+---------+------------------+---------------------------------+------
 public | maximum | integer          | a integer, b integer            | func
 public | maximum | integer          | a integer, b integer, c integer | func
 public | maximum | real             | a real, b real                  | func
(3 rows)

Две из них имеют одинаковое количество параметров, но отличаются их типами:

=> SELECT maximum(10, 20), maximum(1.1, 2.2);
 maximum | maximum
---------+---------
      20 |     2.2
(1 row)

Если подпрограмма перегружена несколько раз, то для получения информации только о некоторых из них можно указать в команде \df типы интересующих параметров:

=> \df maximum real
                        List of functions
 Schema |  Name   | Result data type | Argument data types | Type
--------+---------+------------------+---------------------+------
 public | maximum | real             | a real, b real      | func
(1 row)

Дальше нам придется определить функции для всех остальных типов данных и повторить все то же самое для трех параметров. Притом что операторы в теле этих функций будут одни и те же.

Полиморфные функции

Здесь нам помогут полиморфные типы anyelement и anycompatible. Это псевдотипы, взамен которых при вызове и интерпретации функции будет подставлен тип фактического параметра. Разумеется, в случае определения подпрограммы в стиле стандарта SQL ее код будет разобран еще на этапе создания, и воспользоваться полиморфными псевдотипами не удастся. Удалим все три наши функции…

=> DROP FUNCTION maximum(integer, integer);
DROP FUNCTION
=> DROP FUNCTION maximum(integer, integer, integer);
DROP FUNCTION
=> DROP FUNCTION maximum(real, real);
DROP FUNCTION

…и затем создадим новую:

=> CREATE FUNCTION maximum(a anyelement, b anyelement)
RETURNS anyelement
AS $$
    SELECT CASE WHEN a > b THEN a ELSE b END;
$$ LANGUAGE sql;
CREATE FUNCTION

Такая функция должна принимать любой тип данных (а работать будет с любым типом, для которого определен оператор «больше»). Получится?

=> SELECT maximum('A', 'B');
ERROR:  could not determine polymorphic type because input has type unknown

Увы, нет. В данном случае строковые литералы могут быть типа char, varchar, text — конкретный тип нам неизвестен. Но можно применить явное приведение типов:

Page 11: Page 11

=> SELECT maximum('A'::text, 'B'::text);
 maximum
---------
 B
(1 row)

Еще пример с другим типом:

=> SELECT maximum(now(), now() + interval '1 day');
            maximum
-------------------------------
 2026-04-11 15:36:52.135967+03
(1 row)

Тип результата функции всегда будет тот же, что и тип параметров. Но можно продвинуться еще дальше — сделать так, чтобы можно было использовать в полиморфных подпрограммах не абсолютно одинаковые типы, а совместимые, то есть те, что могут быть приведены друг к другу неявно. Для этого нужно использовать полиморфный псевдотип anycompatible. Удалим нашу функцию и взамен нее создадим другую:

=> DROP FUNCTION maximum;
DROP FUNCTION
=> CREATE FUNCTION maximum(a anycompatible, b anycompatible)
RETURNS anycompatible
AS $$
    SELECT CASE WHEN a > b THEN a ELSE b END;
$$ LANGUAGE sql;
CREATE FUNCTION

Повторим наш опыт с параметрами-литералами:

=> SELECT maximum('A', 'B');
 maximum
---------
 B
(1 row)

Получилось! Но если типы параметров не совпадают и не могут быть неявно приведены к некоторому общему типу, то будет ошибка:

=> SELECT maximum(1, 'A');
ERROR:  invalid input syntax for type integer: "A"
LINE 1: SELECT maximum(1, 'A');
                          ^

В этом примере такое ограничение выглядит естественно, хотя в некоторых случаях оно может оказаться и неудобным. Определим теперь функцию с тремя параметрами, но так, чтобы третий можно было не указывать:

=> CREATE FUNCTION maximum(
    a anycompatible,
    b anycompatible,
    c anycompatible DEFAULT NULL
) RETURNS anycompatible
AS $$
SELECT CASE
         WHEN c IS NULL THEN
             x
         ELSE
             CASE WHEN x > c THEN x ELSE c END
       END
FROM (
    SELECT CASE WHEN a > b THEN a ELSE b END
) max2(x);
$$ LANGUAGE sql;
CREATE FUNCTION
=> SELECT maximum(10, 11.21, 3e3);

Page 12: Page 12

 maximum
---------
    3000
(1 row)

Так работает. А так?

=> SELECT maximum(10, 11.21);
ERROR:  function maximum(integer, numeric) is not unique
LINE 1: SELECT maximum(10, 11.21);
               ^
HINT:  Could not choose a best candidate function. You might need to add explicit type
casts.

А так произошел конфликт перегруженных функций:

=> \df maximum
                                                  List of functions
 Schema |  Name   | Result data type |                           Argument data types
                     | Type
--------+---------+------------------+----------------------------------------------------
---------------------+------
 public | maximum | anycompatible    | a anycompatible, b anycompatible
                     | func
 public | maximum | anycompatible    | a anycompatible, b anycompatible, c anycompatible
DEFAULT NULL::unknown | func
(2 rows)

Невозможно понять, имеем ли мы в виду функцию с двумя параметрами, или с тремя (но просто не указали последний). Мы решим этот конфликт просто — удалим первую функцию за ненадобностью.

=> DROP FUNCTION maximum(anycompatible, anycompatible);
DROP FUNCTION
=> SELECT maximum(10, 11.21), maximum(10, 11.21, 3e3);
 maximum | maximum
---------+---------
   11.21 |    3000
(1 row)

Теперь все работает. А в теме «PL/pgSQL. Массивы» мы узнаем, как определять подпрограммы с произвольным числом параметров.

Page 13: Итоги

Можно создавать и использовать собственные процедуры

  • В отличие от функций, процедуры вызываются оператором CALL и могут управлять транзакциями
  • Для процедур и функций поддерживаются перегрузка и полиморфизм

Page 14: Практика

  1. В таблице authors имена, фамилии и отчества авторов по смыслу должны быть уникальны, но это условие никак не проверяется. Напишите процедуру, удаляющую возможные дубликаты авторов. 2. Чтобы необходимость в подобной процедуре не возникала, создайте ограничение целостности, которое не позволит появляться дубликатам в будущем.

Комментарии преподавателя

  1. В приложении возможность добавлять авторов появится в теме «PL/pgSQL. Выполнение запросов». А пока для проверки можно добавить дубликаты в таблицу вручную.

Page 15: 1. Устранение дубликатов

В целях проверки добавим второго Пушкина:

=> INSERT INTO authors(last_name, first_name, middle_name)
    VALUES ('Пушкин', 'Александр', 'Сергеевич');
INSERT 0 1
=> SELECT last_name, first_name, middle_name, count(*)
FROM authors
GROUP BY last_name, first_name, middle_name;
 last_name  | first_name | middle_name | count
------------+------------+-------------+-------
 Свифт      | Джонатан   |             |     1
 Стругацкий | Борис      | Натанович   |     1
 Пушкин     | Александр  | Сергеевич   |     2
 Стругацкий | Аркадий    | Натанович   |     1
 Толстой    | Лев        | Николаевич  |     1
 Тургенев   | Иван       | Сергеевич   |     1
(6 rows)

Задачу устранения дубликатов можно решить разными способами. Например, так:

=> CREATE PROCEDURE authors_dedup()
LANGUAGE sql
BEGIN ATOMIC
DELETE FROM authors
WHERE author_id IN (
    SELECT author_id
    FROM (
        SELECT author_id,
               row_number() OVER (
                   PARTITION BY first_name, last_name, middle_name
                   ORDER BY author_id
               ) AS rn
        FROM authors
    ) t
    WHERE t.rn > 1
);
END;
CREATE PROCEDURE
=> CALL authors_dedup();
CALL
=> SELECT last_name, first_name, middle_name, count(*)
FROM authors
GROUP BY last_name, first_name, middle_name;
 last_name  | first_name | middle_name | count
------------+------------+-------------+-------
 Свифт      | Джонатан   |             |     1
 Стругацкий | Борис      | Натанович   |     1
 Пушкин     | Александр  | Сергеевич   |     1
 Стругацкий | Аркадий    | Натанович   |     1
 Толстой    | Лев        | Николаевич  |     1
 Тургенев   | Иван       | Сергеевич   |     1
(6 rows)

2. Ограничение целостности

Создать подходящее ограничение целостности мешает тот факт, что отчество может быть неопределенным (NULL). Неопределенные значения считаются различными, поэтому ограничение

UNIQUE(first_name, last_name, middle_name)

не помешает добавить второго Джонатана Свифта без отчества. Задачу можно решить, создав уникальный индекс:

Page 16: Page 16

=> CREATE UNIQUE INDEX authors_full_name_idx ON authors(
    last_name, first_name, coalesce(middle_name,'')
);
CREATE INDEX

Проверим:

=> INSERT INTO authors(last_name, first_name)
    VALUES ('Свифт', 'Джонатан');
ERROR:  duplicate key value violates unique constraint "authors_full_name_idx"
DETAIL:  Key (last_name, first_name, COALESCE(middle_name, ''::text))=(Свифт,
Джонатан, ) already exists.
=> INSERT INTO authors(last_name, first_name, middle_name)
    VALUES ('Пушкин', 'Александр', 'Сергеевич');
ERROR:  duplicate key value violates unique constraint "authors_full_name_idx"
DETAIL:  Key (last_name, first_name, COALESCE(middle_name, ''::text))=(Пушкин,
Александр, Сергеевич) already exists.

Page 17: Практика+

  1. Получится ли создать в одной и той же схеме и имеющие одно и то же имя: а) процедуру с одним входным параметром, б) функцию с одним входным параметром того же типа, возвращающую некоторое значение? в) процедуру с одним входным и одним выходным параметром? Проверьте. 2. В таблице хранятся вещественные числа (например, результаты каких-либо измерений). Напишите процедуру нормализации данных, которая умножает все числа на определенный коэффициент так, чтобы все значения попали в интервал от −1 до 1. Процедура должна возвращать выбранный коэффициент.

Комментарии преподавателя

  1. В качестве коэффициента возьмите максимальное абсолютное значение из таблицы.

Page 18: 1. Перегрузка процедур и функций

=> CREATE DATABASE sql_proc;
CREATE DATABASE
=> \c sql_proc
You are now connected to database "sql_proc" as user "student".

Не получится, так как в сигнатуру подпрограммы входит только имя и тип входных параметров (возвращаемое значение игнорируется), и при этом процедуры и функции имеют общее пространство имен.

=> CREATE PROCEDURE test(IN x integer)
LANGUAGE sql
RETURN 1;
CREATE PROCEDURE
=> CREATE FUNCTION test(IN x integer) RETURNS integer
LANGUAGE sql
RETURN 1;
ERROR:  function "test" already exists with same argument types

В некоторых сообщениях, как и в этом, вместо слова «процедура» используется «функция», поскольку во многом они устроены одинаково.

=> CREATE OR REPLACE PROCEDURE test(IN x integer, OUT y integer)
LANGUAGE sql
RETURN x;
ERROR:  cannot change whether a procedure has output parameters
HINT:  Use DROP PROCEDURE test(integer) first.

Такую процедуру тоже создать нельзя, так как уже имеется процедура с такой же сигнатурой, а изменять выходные параметры (и факт их наличия) для имеющейся подпрограммы также запрещено. Нам предлагают удалить подпрограмму, чтобы затем создать ее заново.

2. Нормализация данных

Таблица с тестовыми данными:

=> CREATE TABLE samples(a float);
CREATE TABLE
=> INSERT INTO samples(a)
    SELECT (0.5 - random())*100 FROM generate_series(1,10);
INSERT 0 10

Процедуру можно написать, используя один SQL-оператор:

=> CREATE PROCEDURE normalize_samples(INOUT coeff float)
LANGUAGE sql
BEGIN ATOMIC
   WITH c(coeff) AS (
       SELECT 1/max(abs(a))
       FROM samples
   ),
   upd AS (
       UPDATE samples
       SET a = a * c.coeff
       FROM c
   )
   SELECT coeff FROM c;
END;
CREATE PROCEDURE
=> CALL normalize_samples(NULL);
        coeff
---------------------
 0.02017240688722836
(1 row)

Page 19: Page 19

=> SELECT * FROM samples;
          a
----------------------
 -0.25509621714380126
                   -1
   0.7473568093452344
  -0.8855333339633462
   0.1247964388514916
 -0.06446930490906046
  0.42323723595948254
   0.5662755975385148
   0.2672450033337049
  -0.5659556144691733
(10 rows)
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE sql_proc;
DROP DATABASE

10 Занятие 10: SQL / Составные типы

Исходная страница в нативном Markdown: SQL / Составные типы
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_10_sql_row.html Archive: DEV1-handouts-16.zip Страниц PDF: 24

Page 1: SQL / Составные типы

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Составные типы и работа с ними
  • Параметры функций составных типов
  • Функции, возвращающие одну строку
  • Функции, возвращающие множество строк

Page 3: Составные типы

Составной тип

  • набор именованных атрибутов (полей)
  • то же, что табличная строка, но без ограничений целостности
  • Создание
    • явное объявление нового типа
    • неявно при создании таблицы
    • неопределенный составной тип record
  • Использование
    • атрибуты как скалярные значения
    • операции со значениями составного типа: сравнение, проверка
    • на NULL, использование с подзапросами

Комментарии преподавателя

Составной тип — это набор атрибутов, каждый из которых имеет свое имя и свой тип. Составной тип можно рассматривать как табличную строку. Часто он называется «записью» (а в Си-подобных языках такой тип называется «структурой»).

https://postgrespro.ru/docs/postgresql/16/rowtypes

Составной тип — объект базы данных, его объявление регистрирует новый тип в системном каталоге, после чего он становится полноценным типом SQL. При создании таблицы автоматически создается и одноименный составной тип, представляющий строку этой таблицы. Важное отличие состоит в том, что в составном типе нет ограничений целостности.

https://postgrespro.ru/docs/postgresql/16/sql-createtype

Атрибуты составного типа могут использоваться как отдельные значения (хотя атрибут, в свою очередь, тоже может иметь составной тип).

Составной тип можно использовать как любой другой тип SQL, например, создавать столбцы таблиц этого типа и т. п. Значения составного типа можно сравнивать между собой, проверять на неопределенность (NULL), использовать с подзапросами в таких конструкциях, как IN, ANY/SOME, ALL.

https://postgrespro.ru/docs/postgresql/16/functions-comparisons

https://postgrespro.ru/docs/postgresql/16/functions-subquery

Page 4: Явное объявление составного типа

Первый способ ввести составной тип — явным образом объявить его:

=> CREATE TYPE currency AS (
    amount numeric,
    code   text
);
CREATE TYPE
=> \dT
       List of data types
 Schema |   Name   | Description
--------+----------+-------------
 public | currency |
(1 row)

Такой тип можно использовать точно так же, как любой другой тип SQL. Например, мы можем создать таблицу со столбцами такого типа:

=> CREATE TABLE transactions(
    account_id   integer,
    debit        currency,
    credit       currency,
    date_entered date DEFAULT current_date
);
CREATE TABLE

Нужно ли так делать — вопрос сложный, универсальных рецептов тут не существует. В каких-то случаях это может быть полезно; в каких-то удобнее действовать в реляционных рамках: выделить сущность, представляемую типом, в отдельную таблицу и ссылаться на нее. Это позволить избежать избыточности данных (нормализация) и упростить индексирование (в случае составного типа скорее всего потребуется индекс по выражению). В целом PostgreSQL обладает достаточно большим количеством встроенных типов данных, так что, вероятно, необходимость в создании собственного типа будет возникать не часто, если не сказать редко.

Конструирование значений составных типов

Значения составного типа можно формировать в виде строки, внутри которой в скобках перечислены значения. Обратите внимание, что строковые значения заключается в двойные кавычки:

=> INSERT INTO transactions VALUES (1, NULL, '(100.00,"RUB")');
INSERT 0 1

Другой способ — конструктор ROW:

=> INSERT INTO transactions VALUES (2, ROW(80.00,'RUB'), NULL);
INSERT 0 1

Если составной тип содержит более одного поля, то слово ROW можно опустить:

=> INSERT INTO transactions VALUES (3, (20.00,'RUB'), NULL);
INSERT 0 1
=> SELECT * FROM transactions;
 account_id |    debit    |    credit    | date_entered
------------+-------------+--------------+--------------
          1 |             | (100.00,RUB) | 2026-04-10
          2 | (80.00,RUB) |              | 2026-04-10
          3 | (20.00,RUB) |              | 2026-04-10
(3 rows)

Атрибуты составного типа как отдельные значения

Обращение к отдельному атрибуту составного типа — по сути то же, что и обращению к столбцу таблицы, ведь строка таблицы — это и есть составной тип:

Page 5: Page 5

=> SELECT t.account_id FROM transactions t;
 account_id
------------
          1
          2
          3
(3 rows)

Как правило, требуется брать составное значение в скобки, например, чтобы отличать атрибут записи от столбца таблицы:

=> SELECT (t.debit).amount, (t.credit).amount FROM transactions t;
 amount | amount
--------+--------
        | 100.00
  80.00 |
  20.00 |
(3 rows)

Или в случае, когда используется выражение:

=> SELECT ((10.00,'RUB')::currency).amount;
 amount
--------
  10.00
(1 row)

Составное значение не обязательно связано с каким-то конкретным типом, оно может быть неопределенной записью псевдотипа record:

=> SELECT (10.00,'RUB')::record;
     row
-------------
 (10.00,RUB)
(1 row)

Но получится ли обратиться к атрибуту такой записи?

=> SELECT ((10.00,'RUB')::record).amount;
ERROR:  could not identify column "amount" in record data type
LINE 1: SELECT ((10.00,'RUB')::record).amount;
                ^

Нет, поскольку атрибуты такого типа безымянные.

Неявный составной тип для таблиц

Более частое на практике применение составных типов — упрощение работы функций с таблицами. При создании таблицы неявно создается и одноименный составной тип. Например, места в кинотеатре:

=> CREATE TABLE seats(
    line text,
    number integer
);
CREATE TABLE
=> INSERT INTO seats VALUES
    ('A', 42), ('B',  1), ('C', 27);
INSERT 0 3

Команда \dT «прячет» такие неявные типы, но при желании их можно увидеть непосредственно в таблице pg_type:

=> SELECT typtype FROM pg_type WHERE typname = 'seats';
 typtype
---------
 c
(1 row)

c — composite, то есть составной тип.

Page 6: Операции над значениями составных типов

Значения составных типов можно сравнивать между собой. Это происходит поэлементно (примерно так же, как строки сравниваются посимвольно):

=> SELECT * FROM seats s WHERE s < ('B',52)::seats;
 line | number
------+--------
 A    |     42
 B    |      1
(2 rows)

Осторожно: существует много тонкостей, связанных с неопределенными значениями внутри записей. Также работает проверка на неопределенность IS [NOT] NULL и сравнение IS [NOT] DISTINCT FROM. Составные типы можно использовать с подзапросами, что бывает очень удобно. Добавим таблицу с билетами:

=> CREATE TABLE tickets(
    line text,
    number integer,
    movie_start date
);
CREATE TABLE
=> INSERT INTO tickets VALUES
    ('A', 42, current_date),
    ('B',  1, current_date+1);
INSERT 0 2

Теперь, например, можно написать такой запрос для поиска мест в билетах на сегодняшний сеанс:

=> SELECT * FROM seats WHERE (line, number) IN (
    SELECT line, number FROM tickets WHERE movie_start = current_date
);
 line | number
------+--------
 A    |     42
(1 row)

Без возможности использовать подзапрос пришлось бы явно соединять таблицы.

Page 7: Параметры подпрограмм

Подпрограмма может принимать параметры составного типа

  • Способ реализации вычисляемых полей
    • взаимозаменяемость table.column и column(table)
  • Другие способы
    • представления
    • столбцы GENERATED ALWAYS

Комментарии преподавателя

Разумеется, подпрограммы могут принимать параметры составных типов.

Интересно, что для доступа к столбцу таблицы можно использовать не только привычную форму «таблица.столбец», но и функциональную: «столбец(таблица)». Это позволяет создавать вычисляемые поля, определяя функцию, принимающую на вход составной тип.

https://postgrespro.ru/docs/postgresql/16/xfunc-sql

Это несколько курьезный способ, поскольку того же результата можно добиться более явно с помощью представления. Стандарт SQL также предусматривает генерируемые (GENERATED ALWAYS) столбцы, хотя в PostgreSQL эта возможность пока реализована не в соответствии со стандартом — столбцы не вычисляются на лету, а сохраняются в таблице.

https://postgrespro.ru/docs/postgresql/16/ddl-generated-columns

Page 8: Параметры составного типа

Определим функцию, принимающую значение составного типа и возвращающую текстовый номер места.

=> CREATE FUNCTION seat_no(seat seats) RETURNS text
IMMUTABLE LANGUAGE sql
RETURN seat.line || seat.number;
CREATE FUNCTION

Обратите внимание, что в общем случае конкатенация имеет категорию изменчивости stable, а не immutable, поскольку для некоторых типов данных приведение к строке может давать разные результаты в зависимости от настроек.

=> SELECT seat_no(ROW('A',42));
 seat_no
---------
 A42
(1 row)

Что удобно, такой функции можно передавать непосредственно строку таблицы:

=> SELECT s.line, s.number, seat_no(s.*) FROM seats s;
 line | number | seat_no
------+--------+---------
 A    |     42 | A42
 B    |      1 | B1
 C    |     27 | C27
(3 rows)

Можно обойтись и без «звездочки»:

=> SELECT s.line, s.number, seat_no(s) FROM seats s;
 line | number | seat_no
------+--------+---------
 A    |     42 | A42
 B    |      1 | B1
 C    |     27 | C27
(3 rows)

Синтаксисом допускается обращение к функции как к столбцу таблицы (и наоборот, к столбцу как к функции):

=> SELECT s.line, number(s), s.seat_no FROM seats s;
 line | number | seat_no
------+--------+---------
 A    |     42 | A42
 B    |      1 | B1
 C    |     27 | C27
(3 rows)

Таким образом можно использовать функции как вычисляемые «на лету» столбцы таблиц. Что, если и в таблице окажется столбец с тем же именем? Раньше в любом случае предпочтение отдавалось столбцу, а начиная с версии 11 выбор зависит от синтаксической формы. Разумеется, такого же эффекта можно добиться, определив представление.

=> CREATE VIEW seats_v AS
    SELECT s.line, s.number, seat_no(s) FROM seats s;
CREATE VIEW
=> SELECT line, number, seat_no FROM seats_v;
 line | number | seat_no
------+--------+---------
 A    |     42 | A42
 B    |      1 | B1
 C    |     27 | C27
(3 rows)

Page 9: Page 9

А начиная с версии 12, PostgreSQL позволяет при создании таблиц объявить «настоящие» вычисляемые столбцы. Правда, в отличие от стандарта SQL, такие столбцы не вычисляются на лету, а сохраняются в таблице:

=> CREATE TABLE seats2(
    line text,
    number integer,
    seat_no text GENERATED ALWAYS AS (seat_no(ROW(line,number))) STORED
);
CREATE TABLE
=> \d seats2
                                       Table "public.seats2"
 Column  |  Type   | Collation | Nullable |                         Default
---------+---------+-----------+----------+-----------------------------------------------
----------
 line    | text    |           |          |
 number  | integer |           |          |
 seat_no | text    |           |          | generated always as (seat_no(ROW(line,
number))) stored
=> INSERT INTO seats2 (line, number)
    SELECT line, number FROM seats;
INSERT 0 3
=> SELECT * FROM seats2;
 line | number | seat_no
------+--------+---------
 A    |     42 | A42
 B    |      1 | B1
 C    |     27 | C27
(3 rows)

Если мы впоследствии решили, что следует явно задавать значение поля, то нам достаточно удалить выражение:

=> ALTER TABLE seats2 ALTER COLUMN seat_no DROP EXPRESSION;
ALTER TABLE

Существующие данные в столбце остались, но столбец теперь не вычисляемый, а «обычный»:

=> SELECT * FROM seats2;
 line | number | seat_no
------+--------+---------
 A    |     42 | A42
 B    |      1 | B1
 C    |     27 | C27
(3 rows)
=> \d seats2
               Table "public.seats2"
 Column  |  Type   | Collation | Nullable | Default
---------+---------+-----------+----------+---------
 line    | text    |           |          |
 number  | integer |           |          |
 seat_no | text    |           |          |

Page 10: «Однострочные» функции

Возвращают значение составного типа

  • Обычно вызываются в списке выборки запроса
  • При вызове в предложении FROM возвращают однострочную таблицу

Комментарии преподавателя

Функции могут не только принимать параметры составного типа, но и возвращать значение составного типа.

Обычно функции вызываются в списке выборки запроса (предложение SELECT).

Но функцию можно вызвать и в предложении FROM, как будто таблицу из одной строки.

Page 11: Функции, возвращающие одно значение

Напишем функцию, конструирующую и возвращающую табличную строку по отдельным компонентам. Такую функцию можно объявить как RETURNS seats:

=> CREATE FUNCTION seat(line text, number integer) RETURNS seats
IMMUTABLE LANGUAGE sql
RETURN ROW(line, number)::seats;
CREATE FUNCTION
=> SELECT seat('A', 42);
  seat
--------
 (A,42)
(1 row)

Мы получаем результат составного типа. Его можно «развернуть» в однострочную таблицу:

=> SELECT (seat('A', 42)).*;
 line | number
------+--------
 A    |     42
(1 row)

Имена столбцов и их типы получены здесь из описания составного типа seats. Но функцию можно вызывать не только в списке выборки запроса или в условиях, как часть выражения. К функции можно обратиться и в предложении FROM, как к таблице:

=> SELECT * FROM seat('A', 42);
 line | number
------+--------
 A    |     42
(1 row)

При этом мы тоже получаем однострочную таблицу. Кстати, можно ли подобным образом вызвать функцию, возвращающую отдельное (скалярное) значение?

=> SELECT * FROM abs(-1.5);
 abs
-----
 1.5
(1 row)

Да, так тоже можно: мы получили одну строку, состоящую из одного столбца. Другой вариант, который мы уже видели в теме «SQL. Функции» — объявить выходные параметры. Заодно отметим, что в запросе не обязательно собирать составной тип из отдельных полей — это будет проделано автоматически:

=> DROP FUNCTION seat(text, integer);
DROP FUNCTION
=> CREATE FUNCTION seat(line INOUT text, number INOUT integer)
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC SELECT line, number; END;
CREATE FUNCTION
=> SELECT seat('A', 42);
  seat
--------
 (A,42)
(1 row)

Page 12: Page 12

=> SELECT * FROM seat('A', 42);
 line | number
------+--------
 A    |     42
(1 row)

Получаем тот же результат — но имена и типы полей в данном случае получены из выходных параметров функции, а сам составной тип остается анонимным. И еще один вариант — объявить функцию как возвращающую псевдотип record, который обозначает составной тип «вообще», без уточнения его структуры.

=> DROP FUNCTION seat(text, integer);
DROP FUNCTION
=> CREATE FUNCTION seat(line text, number integer) RETURNS record
IMMUTABLE LANGUAGE sql
RETURN (line, number);
CREATE FUNCTION
=> SELECT seat('A',42);
  seat
--------
 (A,42)
(1 row)

Но вызвать такую функцию в предложении FROM уже не получится, поскольку возвращаемый составной тип не просто анонимный, но и количество и типы его полей заранее (на этапе разбора запроса) неизвестны:

=> SELECT * FROM seat('A',42);
ERROR:  a column definition list is required for functions returning "record"
LINE 1: SELECT * FROM seat('A',42);
                      ^

В этом случае при вызове функции структуру составного типа придется уточнить — мы явно указываем названия и типы столбцов:

=> SELECT * FROM seat('A',42) AS (line text, number integer);
 line | number
------+--------
 A    |     42
(1 row)

При написании функций допустим любой из этих трех вариантов, но лучше сразу подумать об использовании: будет ли удобен анонимный тип и уточнение структуры типа при вызове.

Page 13: Табличные функции

Объявляются как RETURNS SETOF или RETURNS TABLE

  • Могут возвращать несколько строк
  • Обычно вызываются в предложении FROM
  • Можно использовать как представление с параметрами
    • особенно удобно в сочетании с подстановкой тела функции в запрос

Комментарии преподавателя

Мы знаем, что к функциям можно обращаться в предложении FROM, однако до сих пор результатом была одна строка. Конечно, интереснее было бы иметь функции, возвращающие множество строк (табличные функции) — и их действительно можно определять.

Табличные функции естественно вызывать в предложении FROM и можно рассматривать их как своеобразное представление. (Формально PostgreSQL позволяет вызвать такую функцию и в списке выборки, но так лучше не делать.)

Как и с обычными функциями, в ряде случаев планировщик может подставить тело функции в основной запрос. Это позволяет создавать «представления с параметрами» без накладных расходов.

https://wiki.postgresql.org/wiki/Inlining\_of\_SQL\_functions

Page 14: Функции, возвращающие множество строк (табличные функции)

Напишем функцию, которая вернет все места в прямоугольном зале заданного размера.

=> CREATE FUNCTION rect_hall(max_line integer, max_number integer)
RETURNS SETOF seats
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT chr(line+64), number
    FROM generate_series(1,max_line) AS line,
         generate_series(1,max_number) AS number;
END;
CREATE FUNCTION

Ключевое отличие — слово SETOF. В таком случае функция возвращает не первую строку последнего запроса, как обычно, а все строки последнего запроса.

=> SELECT * FROM rect_hall(max_line => 2, max_number => 3);
 line | number
------+--------
 A    |      1
 A    |      2
 A    |      3
 B    |      1
 B    |      2
 B    |      3
(6 rows)

Вместо SETOF seats можно использовать и SETOF record:

=> DROP FUNCTION rect_hall(integer, integer);
DROP FUNCTION
=> CREATE FUNCTION rect_hall(max_line integer, max_number integer)
RETURNS SETOF record
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT chr(line+64), number
    FROM generate_series(1,max_line) AS line,
         generate_series(1,max_number) AS number;
END;
CREATE FUNCTION

Но в этом случае, как мы видели, при вызове функции придется уточнять структуру составного типа:

=> SELECT * FROM rect_hall(max_line => 2, max_number => 3)
    AS (a_line text, a_number integer);
 a_line | a_number
--------+----------
 A      |        1
 A      |        2
 A      |        3
 B      |        1
 B      |        2
 B      |        3
(6 rows)

А можно объявить функцию с выходными параметрами. Но SETOF record все равно придется написать, чтобы показать, что функция возвращает не одну строку, а множество:

=> DROP FUNCTION rect_hall(integer, integer);
DROP FUNCTION

Page 15: Page 15

=> CREATE FUNCTION rect_hall(
    max_line integer, max_number integer,
    OUT p_line text, OUT p_number integer
)
RETURNS SETOF record
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT chr(line+64), number
    FROM generate_series(1,max_line) AS line,
         generate_series(1,max_number) AS number;
END;
CREATE FUNCTION
=> SELECT * FROM rect_hall(max_line => 2, max_number => 3);
 p_line | p_number
--------+----------
 A      |        1
 A      |        2
 A      |        3
 B      |        1
 B      |        2
 B      |        3
(6 rows)

Еще один равнозначный (и к тому же описанный в стандарте SQL) способ объявить табличную функцию — указать слово TABLE:

=> DROP FUNCTION rect_hall(integer, integer);
DROP FUNCTION
=> CREATE FUNCTION rect_hall(max_line integer, max_number integer)
RETURNS TABLE(t_line text, t_number integer)
LANGUAGE sql
BEGIN ATOMIC
    SELECT chr(line+64), number
    FROM generate_series(1,max_line) AS line,
         generate_series(1,max_number) AS number;
END;
CREATE FUNCTION
=> SELECT * FROM rect_hall(max_line => 2, max_number => 3);
 t_line | t_number
--------+----------
 A      |        1
 A      |        2
 A      |        3
 B      |        1
 B      |        2
 B      |        3
(6 rows)

Иногда в запросах бывает полезно пронумеровать строки в том порядке, в котором они получены от функции. Для этого есть специальная конструкция:

=> SELECT *
FROM rect_hall(max_line => 2, max_number => 3) WITH ORDINALITY;
 t_line | t_number | ordinality
--------+----------+------------
 A      |        1 |          1
 A      |        2 |          2
 A      |        3 |          3
 B      |        1 |          4
 B      |        2 |          5
 B      |        3 |          6
(6 rows)

При использовании функции в предложении FROM, перед ней неявно подразумевается ключевое слово LATERAL, что позволяет функции обращаться к столбцам таблиц, стоящих в запросе слева от нее. Иногда это позволяет упростить формулировку запросов. Например, напишем функцию, конструирующую зал наподобие амфитеатра, в котором дальние ряды имеют больше мест, чем ближние:

Page 16: Page 16

=> CREATE FUNCTION amphitheatre(max_line integer)
RETURNS TABLE(t_line text, t_number integer)
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT chr(line + 64), number
    FROM generate_series(1,max_line) AS line, -- <--+
         generate_series(1, --                      |
                         line ----------------------+
                        ) AS number;
END;
CREATE FUNCTION
=> SELECT * FROM amphitheatre(3);
 t_line | t_number
--------+----------
 A      |        1
 B      |        1
 B      |        2
 C      |        1
 C      |        2
 C      |        3
(6 rows)

Интересно, что к функции, возвращающей множество строк, можно обратиться и в списке выборки запроса:

=> SELECT rect_hall(3,4);
 rect_hall
-----------
 (A,1)
 (A,2)
 (A,3)
 (A,4)
 (B,1)
 (B,2)
 (B,3)
 (B,4)
 (C,1)
 (C,2)
 (C,3)
 (C,4)
(12 rows)

В некоторых случаях это смотрится логично, но иногда результат может удивить. Например, сколько строк вернет такой запрос?

=> SELECT rect_hall(2,3), rect_hall(2,2);
 rect_hall | rect_hall
-----------+-----------
 (A,1)     | (A,1)
 (A,2)     | (A,2)
 (A,3)     | (B,1)
 (B,1)     | (B,2)
 (B,2)     |
 (B,3)     |
(6 rows)

Получается 6 строк, а в версиях до 10 получалось наименьшее общее кратное числа строк, возвращенных каждой функцией (12 в данном случае). Еще хуже, что запрос может вернуть меньше строк, чем ожидалось, если функция не вернет ни одной строки при каком-то значении параметров. Поэтому такой способ вызова не стоит использовать.

Функции как представления с параметрами

Как мы видели, функцию можно использовать во фразе FROM как таблицу или представление. Но при этом мы дополнительно получаем возможность использовать параметры, что в ряде случаев бывает удобно. Единственная сложность с таким подходом состоит в том, что при обращении к функции (Function Scan) запросы из нее сначала выполняются полностью, и только затем к результату применяются дополнительные условия из запроса.

Page 17: Page 17

=> EXPLAIN (costs off)
SELECT * FROM rect_hall(3,4) WHERE t_line = 'A';
           QUERY PLAN
--------------------------------
 Function Scan on rect_hall
   Filter: (t_line = 'A'::text)
(2 rows)

Если бы функция содержала сложный, долгий запрос, это могло бы стать проблемой. В некоторых случаях тело функции может подставляться в вызывающий запрос. Для табличных функций ограничения более мягкие: функция написана на языке SQL; функция сама не должна быть изменчивой (VOLATILE) и не должна содержать вызовов таких функций; функция не должна быть строгой (STRICT); тело должно содержать единственный оператор SELECT (но он может быть сложным); и ряд других ограничений. В нашем случае дело в том, что последний раз мы объявили функцию как изменчивую, не указав категорию изменчивости явно.

=> ALTER FUNCTION rect_hall(integer, integer) IMMUTABLE;
ALTER FUNCTION
=> EXPLAIN (costs off)
SELECT * FROM rect_hall(3,4) WHERE t_line = 'A';
                   QUERY PLAN
------------------------------------------------
 Nested Loop
   ->  Function Scan on generate_series line
         Filter: (chr((line + 64)) = 'A'::text)
   ->  Function Scan on generate_series number
(4 rows)

Теперь нет вызова функции как такового, а условие подставлено внутрь запроса, что более эффективно.

Page 18: Итоги

Составной тип объединяет значения других типов

  • Упрощает и обогащает работу функций с таблицами
  • Позволяет создавать вычислимые поля и представления с параметрами
  • Функции могут возвращать множество строк

Page 19: Практика

  1. Создайте функцию onhand_qty для подсчета имеющихся в наличии книг. Функция принимает параметр составного типа books и возвращает целое число.
  • Используйте эту функцию в представлении catalog_v
  • в качестве «вычисляемого поля».
  • Проверьте, что приложение отображает количество книг. 2. Создайте табличную функцию get_catalog для поиска книг. Функция принимает значения полей формы поиска («имя автора», «название книги», «есть на складе») и возвращает подходящие книги в формате catalog_v.
    • Проверьте, что в «Магазине» начал работать поиск и
    • просмотр.

Комментарии преподавателя

FUNCTION onhand_qty(book books) RETURNS integer
FUNCTION get_catalog(
    author_name text, book_title text, in_stock boolean
)
RETURNS TABLE(
    book_id integer, display_name text, onhand_qty integer
)

При решении хотелось бы воспользоваться уже готовым представлением catalog_v, просто наложив ограничения на строки. Но в этом представлении и название книги, и авторы находятся в одном поле, к тому же в сокращенном виде. Очевидно, что поиск автора «Лев» по полю «Л .Н. Толстой» не даст результата.

Можно было бы повторить в функции get_catalog запрос из catalog_v, но это дублирование кода, что плохо. Поэтому расширьте представление catalog_v, добавив в него дополнительные поля: заголовок книги и полный список авторов.

Проверьте, что корректно обрабатываются пустые поля на форме. Когда клиент вызывает функцию get_catalog, передает ли он в этом случае пустые строки или неопределенные значения?

Page 20: 1. Функция onhand_qty

=> CREATE FUNCTION onhand_qty(book books) RETURNS integer
STABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT coalesce(sum(o.qty_change),0)::integer
    FROM operations o
    WHERE o.book_id = book.book_id;
END;
CREATE FUNCTION
=> CREATE OR REPLACE VIEW catalog_v AS
SELECT b.book_id,
       book_name(b.book_id, b.title) AS display_name,
       b.onhand_qty
FROM   books b
ORDER BY display_name;
CREATE VIEW

2. Функция get\_catalog

Расширяем catalog_v заголовком книги и полным списком авторов (приложение игнорирует неизвестные ему поля). Функция, возвращающая полный список авторов:

=> CREATE FUNCTION authors(book books) RETURNS text
STABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT string_agg(
               a.last_name ||
               ' ' ||
               a.first_name ||
               coalesce(' ' || nullif(a.middle_name,''), ''),
               ', '
               ORDER BY ash.seq_num
           )
    FROM   authors a
           JOIN authorship ash ON a.author_id = ash.author_id
    WHERE  ash.book_id = book.book_id;
END;
CREATE FUNCTION

Используем эту функцию в представлении catalog_v. Такое представление уже существует; мы пересоздадим его — изменим порядок столбцов и запрос:

=> DROP VIEW catalog_v;
DROP VIEW
=> CREATE VIEW catalog_v AS
SELECT b.book_id,
       b.title,
       b.onhand_qty,
       book_name(b.book_id, b.title) AS display_name,
       b.authors
FROM   books b
ORDER BY display_name;
CREATE VIEW

Функция get_catalog теперь использует расширенное представление:

Page 21: Page 21

=> CREATE FUNCTION get_catalog(
    author_name text,
    book_title text,
    in_stock boolean
)
RETURNS TABLE(book_id integer, display_name text, onhand_qty integer)
STABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT cv.book_id,
           cv.display_name,
           cv.onhand_qty
    FROM   catalog_v cv
    WHERE  cv.title   ILIKE '%'||coalesce(book_title,'')||'%'
    AND    cv.authors ILIKE '%'||coalesce(author_name,'')||'%'
    AND    (in_stock AND cv.onhand_qty > 0 OR in_stock IS NOT TRUE)
    ORDER BY display_name;
END;
CREATE FUNCTION

Page 22: Практика+

  1. Напишите функцию, переводящую строку, содержащую число в шестнадцатеричной системе, в обычное целое число. Например: convert(‘FF’) → 255 2. Добавьте в функцию второй необязательный параметр — основание системы счисления (по умолчанию — 16). Например: convert(‘0110’,2) → 6 3. Табличная функция generate_series не работает со строковыми типами. Предложите свою функцию для генерации последовательностей строк из заглавных английских букв.

Комментарии преподавателя

  1. Например:
convert('FF') → 255

Для решения пригодятся: табличная функция regexp_split_to_table, функции upper и reverse, конструкция WITH ORDINALITY.

Другое решение возможно с помощью рекурсивного запроса.

Проверить реализацию можно, используя шестнадцатеричные константы: SELECT X’FF’::integer;

  1. Например:
convert('0110',2) → 6
  1. Считайте, что на вход подаются строки равной длины. Например:
generate_series('AA','ZZ') →
 → 'AA'
   'AB'
   'AC'
   ...
   'ZY'
   'ZZ'

Page 23: 1. Функция для шестнадцатеричной системы

=> CREATE DATABASE sql_row;
CREATE DATABASE
=> \c sql_row
You are now connected to database "sql_row" as user "student".

Сначала для удобства определим функцию для одной цифры:

=> CREATE FUNCTION digit(d text) RETURNS integer
IMMUTABLE LANGUAGE sql
RETURN ascii(d) - CASE
         WHEN d BETWEEN '0' AND '9' THEN ascii('0')
         ELSE ascii('A') - 10
       END;
CREATE FUNCTION

Теперь основная функция:

=> CREATE FUNCTION convert(hex text) RETURNS integer
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
  WITH s(d,ord) AS (
    SELECT *
    FROM regexp_split_to_table(reverse(upper(hex)),'') WITH ORDINALITY
  )
  SELECT sum(digit(d) * 16^(ord-1))::integer FROM s;
END;
CREATE FUNCTION
=> SELECT convert('0FE'), convert('0FF'), convert('100');
 convert | convert | convert
---------+---------+---------
     254 |     255 |     256
(1 row)

2. Функция для любой системы счисления

Предполагаем, что основание системы счисления от 2 до 36, то есть число записывается цифрами от 0 до 9, либо буквами от A до Z. В этом случае изменения минимальные.

=> DROP FUNCTION convert(text);
DROP FUNCTION
=> CREATE FUNCTION convert(num text, radix integer DEFAULT 16) RETURNS integer
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
  WITH s(d,ord) AS (
    SELECT *
    FROM regexp_split_to_table(reverse(upper(num)),'') WITH ORDINALITY
  )
  SELECT sum(digit(d) * radix^(ord-1))::integer FROM s;
END;
CREATE FUNCTION
=> SELECT convert('101100', 2), convert('2C'), convert('54', 8);
 convert | convert | convert
---------+---------+---------
      44 |      44 |      44
(1 row)

Заметим, что в PostgreSQL начиная с версии 16 есть возможность записи целочисленных констант не только в десятичном, но и в двоичном, шестнадцатеричном и восьмеричном виде. Такая возможность закреплена в современном стандарте SQL:

=> SELECT 0b101100 AS bin, 0x2C AS hex, 0o54 AS oct;

Page 24: 3. Функция generate_series для строк

 bin | hex | oct
-----+-----+-----
  44 |  44 |  44
(1 row)

Сначала напишем вспомогательные функции, переводящие строку в числовое представление и обратно. Первая очень похожа на функцию из предыдущего задания:

=> CREATE FUNCTION text2num(s text) RETURNS integer
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
  WITH s(d,ord) AS (
    SELECT *
    FROM regexp_split_to_table(reverse(s),'') WITH ORDINALITY
  )
  SELECT sum( (ascii(d)-ascii('A')) * 26^(ord-1))::integer FROM s;
END;
CREATE FUNCTION

Обратную функцию напишем с помощью рекурсивного запроса:

=> CREATE FUNCTION num2text(n integer, digits integer) RETURNS text
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
  WITH RECURSIVE r(num,txt, level) AS (
    SELECT n/26, chr( n%26 + ascii('A') )::text, 1
    UNION ALL
    SELECT r.num/26, chr( r.num%26 + ascii('A') ) || r.txt, r.level+1
    FROM r
    WHERE r.level < digits
  )
  SELECT r.txt FROM r WHERE r.level = digits;
END;
CREATE FUNCTION
=> SELECT num2text( text2num('ABC'), length('ABC') );
 num2text
----------
 ABC
(1 row)

Теперь функцию generate_series для строк можно переписать, используя generate_series для целых чисел.

=> CREATE FUNCTION generate_series(start text, stop text) RETURNS SETOF text
IMMUTABLE LANGUAGE sql
BEGIN ATOMIC
  SELECT num2text( g.n, length(start)) FROM generate_series(text2num(start), text2num(stop)) g(n);
END;
CREATE FUNCTION
=> SELECT generate_series('AZ','BC');
 generate_series
-----------------
 AZ
 BA
 BB
 BC
(4 rows)
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE sql_row;
DROP DATABASE

11 Занятие 11: PL/pgSQL / Обзор и конструкции языка

Исходная страница в нативном Markdown: PL/pgSQL / Обзор и конструкции языка
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_11_plpgsql_introduction.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: Темы

  • История PL/pgSQL
  • Структура блока, объявление переменных
  • Анонимные блоки
  • Подпрограммы на языке PL/pgSQL
  • Условные операторы и циклы
  • Вычисление выражений

Page 3: История PL/pgSQL

Появился в версии 6.4 в 1998 году

  • устанавливается по умолчанию с версии 9.0
  • Цели создания
    • простой язык для написания пользовательских функций и триггеров
    • добавить управляющие структуры к языку SQL
    • сохранить возможность использования любых пользовательских типов,
    • функций и операторов
  • Родословная: Oracle PL/SQL, Ада

Комментарии преподавателя

PL/pgSQL — один из первых процедурных языков в PostgreSQL. Он впервые появился в 1998 году в версии 6.4, а с версии 9.0 стал устанавливаться по умолчанию при создании базы данных.

PL/pgSQL дополняет SQL, предоставляя такие возможности процедурных языков, как использование переменных и курсоров, условные операторы, циклы, обработка ошибок и т. д.

PL/pgSQL проектировался на основе языка Oracle PL/SQL, а тот, в свою очередь, был создан на основе подмножества языка Ада. Эта ветвь происходит от таких языков, как Алгол и Паскаль. Большинство современных языков программирования принадлежат другой ветви — ветви Си-подобных языков, поэтому PL/pgSQL может показаться непривычным, избыточно многословным (характерное отличие — использование ключевых слов BEGIN и END вместо фигурных скобок). Впрочем, этот синтаксис отлично сочетается с синтаксисом SQL.

https://postgrespro.ru/docs/postgresql/16/plpgsql-overview

Page 4: Структура блока

Метка блока

  • Объявления переменных
    • область действия — блок
    • область видимости может перекрываться вложенными блоками,
    • но можно использовать метку блока
    • любые типы SQL, ссылки на типы объектов (%TYPE)
  • Операторы
    • управляющие конструкции
    • операторы SQL, кроме служебных
    • вложенные блоки
  • Обработка исключительных ситуаций

Комментарии преподавателя

Операторы языка PL/pgSQL объединяются в блоки. В структуре блока можно выделить:

● Необязательную метку, которую можно использовать для устранения неоднозначности в именовании.

● Необязательную секцию, предназначенную для объявления локальных переменных и курсоров. В качестве типа можно указать любой тип, определенный в SQL. Можно также сослаться на тип столбца таблицы с помощью конструкции %TYPE.

● Основную секцию исполнения, в которой располагаются операторы.

● Необязательную секцию обработки исключительных ситуаций.

В качестве операторов можно использовать как команды PL/pgSQL, так и большинство команд SQL, то есть этих два языка интегрированы практически бесшовно. Нельзя использовать служебные команды SQL, такие, как VACUUM. А команды управления транзакциями (например, COMMIT, ROLLBACK) допускаются только в процедурах.

В качестве оператора может выступать и другой (вложенный) PL/pgSQL-блок.

https://postgrespro.ru/docs/postgresql/16/plpgsql-structure

https://postgrespro.ru/docs/postgresql/16/plpgsql-declarations#PLPGSQL-D ECLARATION-TYPE

Page 5: Анонимные блоки

Разовое выполнение процедурного кода

  • без создания хранимой подпрограммы
  • без возможности передать параметры
  • без возможности вернуть значение
  • Оператор DO языка SQL

Комментарии преподавателя

Для использования PL/pgSQL не обязательно создавать подпрограммы. Код на PL/pgSQL можно оформить и выполнить как анонимный блок, при помощи SQL-команды DO.

Эту команду можно использовать с разными серверными языками, но если язык не указать явно, то будет использоваться PL/pgSQL.

Код анонимного блока не сохраняется на сервере. Также нет возможности передать в анонимный блок параметры или вернуть из него значение. Хотя косвенно это можно сделать, например, через таблицы.

https://postgrespro.ru/docs/postgresql/16/sql-do

Page 6: Анонимные блоки

Общая структура блока PL/pgSQL:

<<метка>>
DECLARE
    -- объявления переменных
BEGIN
    -- операторы
EXCEPTION
    -- обработка ошибок
END метка;

Все секции, кроме секции операторов, являются необязательными. Минимальный блок PL/pgSQL-кода:

=> DO $$
BEGIN
    -- сами операторы могут и отсутствовать
END
$$;
DO

Вариант программы Hello, World!

=> DO $$
DECLARE
    -- Это однострочный комментарий.
    /* А это — многострочный.
       После каждого объявления ставится знак ';'.
       Этот же знак ставится после каждого оператора.
    */
    foo text;
    bar text := 'World'; -- также допускается = или DEFAULT
BEGIN
    foo := 'Hello'; -- это присваивание
    RAISE NOTICE '%, %!', foo, bar; -- вывод сообщения
END
$$;
NOTICE:  Hello, World!
DO

После BEGIN точка с запятой не ставится! Переменные могут иметь модификаторы: CONSTANT — значение переменной не должно изменяться после инициализации; NOT NULL — не допускается неопределенное значение.

=> DO $$
DECLARE
    foo integer NOT NULL := 0;
    bar CONSTANT text := 42;
BEGIN
    bar := bar + 1; -- ошибка
END
$$;
ERROR:  variable "bar" is declared CONSTANT
LINE 6:     bar := bar + 1; -- ошибка
            ^

Пример вложенных блоков. Переменная во внутреннем блоке перекрывает переменную из внешнего блока, но с помощью меток можно обратиться к любой из них:

Page 7: Page 7

=> DO $$
<<outer_block>>
DECLARE
    foo text := 'Hello';
BEGIN
    <<inner_block>>
    DECLARE
        foo text := 'World';
    BEGIN
        RAISE NOTICE '%, %!', outer_block.foo, inner_block.foo;
        RAISE NOTICE 'Без метки — внутренняя переменная: %', foo;
    END inner_block;
END outer_block
$$;
NOTICE:  Hello, World!
NOTICE:  Без метки — внутренняя переменная: World
DO

Page 8: Подпрограммы PL/pgSQL

Заголовок подпрограммы не зависит от языка

  • имя, входные и выходные параметры
  • для функций: возвращаемое значение, категория изменчивости
  • Указание LANGUAGE plpgsql
  • Возврат значений
    • оператор RETURN
    • присвоение значений выходным (INOUT, OUT) параметрам

Комментарии преподавателя

Мы уже познакомились с хранимыми функциями и процедурами на примере языка SQL. Большинство рассмотренных вопросов, связанных с созданием и управлением подпрограммами, относится и к подпрограммам на PL/pgSQL:

● создание, изменение, удаление подпрограмм;

● расположение в системном каталоге (pg_proc);

● параметры;

● возвращаемое значение и категории изменчивости (для функций);

● перегрузка и полиморфизм;

● и т. д.

Если в SQL подпрограмма возвращала значение, выданное последним SQL-оператором, то в подпрограммах на PL/pgSQL требуется либо присваивать возвращаемые значения формальным INOUT-и OUT-параметрам, либо (для функций) использовать специальный оператор RETURN.

Page 9: Подпрограммы PL/pgSQL

Пример функции, возвращающей значение с помощью оператора RETURN:

=> CREATE FUNCTION sqr_in(IN a numeric) RETURNS numeric
AS $$
BEGIN
    RETURN a * a;
END
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE FUNCTION

Та же функция, но с OUT-параметром. Возвращаемое значение присваивается параметру:

=> CREATE FUNCTION sqr_out(IN a numeric, OUT retval numeric)
AS $$
BEGIN
    retval := a * a;
END
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE FUNCTION

Та же функция, но с INOUT-параметром. Такой параметр используется и для принятия входного значения, и для возврата значения функции:

=> CREATE FUNCTION sqr_inout(INOUT a numeric)
AS $$
BEGIN
    a := a * a;
END
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE FUNCTION
=> SELECT sqr_in(3), sqr_out(3), sqr_inout(3);
 sqr_in | sqr_out | sqr_inout
--------+---------+-----------
      9 |       9 |         9
(1 row)

Page 10: Условные операторы

IF

  • стандартный условный оператор
  • CASE
    • похож на CASE языка SQL, но не возвращает значение
Примечание

Внимание: трехзначная логика

  • условие должно быть истинно; false и NULL не подходят

Комментарии преподавателя

PL/pgSQL предлагает два условных оператора: IF и CASE.

Первый — совершенно стандартный оператор, имеющийся во всех языках.

Второй похож на конструкцию CASE в языке SQL, но это именно оператор, он не возвращает значение. Это некий аналог оператора switch в C или Java.

Важно всегда помнить о том, что логические выражения в SQL (и, следовательно, в PL/pgSQL) могут принимать три значения: true, false и NULL. Условие срабатывает, только когда оно истинно, и не срабатывает, когда ложно или не определено. Это одинаково верно и для условий WHERE в SQL, и для условных операторов PL/pgSQL.

https://postgrespro.ru/docs/postgresql/16/plpgsql-control-structures#PLPGS QL-CONDITIONALS

Page 11: Условные операторы

Общий вид оператора IF:

IF условие THEN
    -- операторы
ELSIF условие THEN
    -- операторы
ELSE
    -- операторы
END IF;

Секция ELSIF может повторяться несколько раз, а может отсутствовать. Секция ELSE может отсутствовать. Выполняются операторы, соответствующие первому истинному условию. Если ни одно из условий не истинно, выполняются операторы ELSE (если есть). Пример функции, использующей условный оператор для форматирования номера телефона. Функция возвращает значение составного типа:

=> CREATE FUNCTION fmt (IN phone text, OUT code text, OUT num text)
AS $$
BEGIN
    IF phone ~ '^[0-9]*$' AND length(phone) = 10 THEN
        code := substr(phone,1,3);
        num  := substr(phone,4);
    ELSE
        code := NULL;
        num  := NULL;
    END IF;
END
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE FUNCTION
=> SELECT fmt('8122128506');
      fmt
---------------
 (812,2128506)
(1 row)

Общий вид оператора CASE (первый вариант — по условию):

CASE
    WHEN условие THEN
        -- операторы
    ELSE
        -- операторы
END CASE;

Секция WHEN может повторяться несколько раз. Секция ELSE может отсутствовать. Выполняются операторы, соответствующие первому истинному условию. Если ни одно из условий не истинно, выполняются операторы ELSE (отсутствие ELSE в таком случае — ошибка). Пример использования:

Page 12: Page 12

=> DO $$
DECLARE
    code text := (fmt('8122128506')).code;
BEGIN
    CASE
        WHEN code IN ('495','499') THEN
            RAISE NOTICE '% — Москва', code;
        WHEN code = '812' THEN
            RAISE NOTICE '% — Санкт-Петербург', code;
        WHEN code = '384' THEN
            RAISE NOTICE '% — Кемеровская область', code;
        ELSE
            RAISE NOTICE '% — Прочие', code;
    END CASE;
END
$$;
NOTICE:  812 — Санкт-Петербург
DO

Общий вид оператора CASE (второй вариант — по выражению):

CASE выражение
    WHEN значение, ... THEN
        -- операторы
    ELSE
        -- операторы
END CASE;

Секция WHEN может повторяться несколько раз. Секция ELSE может отсутствовать. Выполняются операторы, соответствующие первому истинному условию «выражение = значение». Если ни одно из условий не истинно, выполняются операторы ELSE (отсутствие ELSE в таком случае — ошибка). При однотипных условиях эта форма CASE может оказаться компактней:

=> DO $$
DECLARE
    code text := (fmt('8122128506')).code;
BEGIN
    CASE code
        WHEN '495', '499' THEN
            RAISE NOTICE '% — Москва', code;
        WHEN '812' THEN
            RAISE NOTICE '% — Санкт-Петербург', code;
        WHEN '384' THEN
            RAISE NOTICE '% — Кемеровская область', code;
        ELSE
            RAISE NOTICE '% — Прочие', code;
    END CASE;
END
$$;
NOTICE:  812 — Санкт-Петербург
DO

Page 13: Циклы

Цикл FOR по диапазону чисел

  • Цикл WHILE с предусловием
  • Бесконечный цикл
  • Цикл может иметь метку, как блок
  • Управление
    • выход из цикла (EXIT)
    • переход на новую итерацию (CONTINUE)

Комментарии преподавателя

Для повторного выполнения набора операторов PL/pgSQL предлагает несколько видов циклов:

● цикл FOR по диапазону целых чисел;

● цикл WHILE с предусловием;

● бесконечный цикл.

Цикл представляет собой разновидность блока и может иметь собственную метку.

Выполнением цикла можно дополнительно управлять, инициируя переход на новую итерацию или выход из цикла.

https://postgrespro.ru/docs/postgresql/16/plpgsql-control-structures#PLPGS QL-CONTROL-STRUCTURES-LOOPS

Цикл FOR может работать не только по диапазону чисел, но и по результатам выполнения запроса и по массивам. Эти варианты цикла FOR будут рассмотрены в следующих темах.

Page 14: Циклы

В PL/pgSQL все циклы используют общую конструкцию:

LOOP
    -- операторы
END LOOP;

К ней может добавляться заголовок, определяющий условие выхода из цикла. Цикл по диапазону FOR повторяется, пока счетчик цикла пробегает значения от нижней границы до верхней. С каждой итерацией счетчик увеличивается на 1 (но инкремент можно изменить в необязательной фразе BY).

FOR имя IN низ .. верх BY инкремент
LOOP
    -- операторы
END LOOP;

Переменная, выступающая счетчиком цикла, объявляется неявно и существует только внутри блока LOOP — END LOOP. При указании REVERSE значение счетчика на каждой итерации уменьшается, а нижнюю и верхнюю границы цикла нужно поменять местами:

FOR имя IN REVERSE верх .. низ BY инкремент
LOOP
    -- операторы
END LOOP;

Пример использования цикла FOR — функция, “переворачивающая” строку:

=> CREATE FUNCTION reverse_for (line text) RETURNS text
AS $$
DECLARE
    line_length CONSTANT int := length(line);
    retval text := '';
BEGIN
    FOR i IN 1 .. line_length
    LOOP
        retval := substr(line, i, 1) || retval;
    END LOOP;
    RETURN retval;
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT;
CREATE FUNCTION

Напомним, что строгая (STRICT) функция немедленно возвращает NULL, если хотя бы один из входных параметров не определен. Тело функции при этом не выполняется. Цикл WHILE выполняется до тех пор, пока истинно условие:

WHILE условие
LOOP
    -- операторы
END LOOP;

Та же функция, обращающая строку, с использованием цикла WHILE:

=> CREATE FUNCTION reverse_while (line text) RETURNS text
AS $$
DECLARE
    line_length CONSTANT int := length(line);
    i int := 1;
    retval text := '';
BEGIN
    WHILE i <= line_length
    LOOP
        retval := substr(line, i, 1) || retval;
        i := i + 1;
    END LOOP;
    RETURN retval;
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

Page 15: Page 15

CREATE FUNCTION

Цикл LOOP без заголовка выполняется бесконечно. Для выхода используется оператор EXIT:

EXIT метка WHEN условие;

Метка необязательна; если не указана, будет прерван самый вложенный цикл. Фраза WHEN также необязательна; при отсутствии цикл прерывается безусловно. Пример использования цикла LOOP:

=> CREATE FUNCTION reverse_loop (line text) RETURNS text
AS $$
DECLARE
    line_length CONSTANT int := length(reverse_loop.line);
    i int := 1;
    retval text := '';
BEGIN
    <<main_loop>>
    LOOP
        EXIT main_loop WHEN i > line_length;
        retval := substr(reverse_loop.line, i,1) || retval;
        i := i + 1;
    END LOOP;
    RETURN retval;
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT;
CREATE FUNCTION

Тело функции помещается в неявный блок, метка которого совпадает с именем функции. Поэтому к параметрам можно обращаться как «имя_функции.параметр». Убедимся, что все функции работают правильно:

=> SELECT reverse_for('главрыба') as "for",
          reverse_while('главрыба') as "while",
          reverse_loop('главрыба') as "loop";
   for    |  while   |   loop
----------+----------+----------
 абырвалг | абырвалг | абырвалг
(1 row)

Замечание. В PostgreSQL есть встроенная функция reverse. Иногда бывает полезен оператор CONTINUE, начинающий новую итерацию цикла:

=> DO $$
DECLARE
    s integer := 0;
BEGIN
    FOR i IN 1 .. 100
    LOOP
        s := s + i;
        CONTINUE WHEN mod(i, 10) != 0;
        RAISE NOTICE 'i = %, s = %', i, s;
    END LOOP;
END
$$;
NOTICE:  i = 10, s = 55
NOTICE:  i = 20, s = 210
NOTICE:  i = 30, s = 465
NOTICE:  i = 40, s = 820
NOTICE:  i = 50, s = 1275
NOTICE:  i = 60, s = 1830
NOTICE:  i = 70, s = 2485
NOTICE:  i = 80, s = 3240
NOTICE:  i = 90, s = 4095
NOTICE:  i = 100, s = 5050
DO

Page 16: Вычисление выражений

Любое выражение вычисляется в контексте SQL

  • выражение автоматически преобразуется в запрос
  • запрос подготавливается
  • переменные PL/pgSQL подставляются как параметры
  • Особенности
    • можно использовать все возможности SQL, включая подзапросы
    • невысокая скорость выполнения,
    • хотя разобранный запрос (и, возможно, план запроса) кешируются
    • неоднозначности при разрешении имен требуют внимания

Комментарии преподавателя

Выражения, встречающиеся в PL/pgSQL-коде, вычисляются с помощью SQL-запросов к базе данных. Интерпретатор сам строит нужный запрос, формируя оператор вида SELECT и заменяя переменные PL/pgSQL на параметры, затем подготавливает его (при этом разобранный запрос кешируется, как это обычно и происходит с подготовленными операторами), после чего планирует и выполняет.

С одной стороны, такой подход не способствует скорости работы PL/pgSQL, зато обеспечивает теснейшую интеграцию с SQL. Фактически, в выражениях можно использовать любые возможности SQL без ограничений, включая вызов встроенных и пользовательских функций, выполнение подзапросов и т. п.

Начиная с версии PostgreSQL 14 оптимизировано выполнение простых выражений (как минимум не содержащих обращений к таблицам) — такие выражения обрабатываются синтаксическим анализатором сервера напрямую, без обращений к планировщику.

https://postgrespro.ru/docs/postgresql/16/plpgsql-expressions

Page 17: Вычисление выражений

Выражения в PL/pgSQL выполняются SQL-движком. Таким образом, в PL/pgSQL доступно ровно то, что доступно в SQL. Например, если в SQL можно использовать конструкцию CASE, то точно такая же конструкция будет работать и в коде на PL/pgSQL (в качестве выражения; не путайте с оператором CASE … END CASE, который есть только в PL/pgSQL):

=> DO $$
BEGIN
    RAISE NOTICE '%', CASE 2+2 WHEN 4 THEN 'Все в порядке' END;
END
$$;
NOTICE:  Все в порядке
DO

В выражениях можно использовать и подзапросы:

=> DO $$
BEGIN
    RAISE NOTICE '%', (
        SELECT code
        FROM (VALUES (1, 'Раз'), (2, 'Два')) t(id, code)
        WHERE id = 1
    );
END
$$;
NOTICE:  Раз
DO

Еще пример с вычислением выражения в PL/pgSQL — сколько функций «обращения» строк у нас в результате получилось?

=> DO $$
DECLARE
  s integer;
BEGIN
  s := count(*) FROM pg_proc WHERE proname LIKE 'reverse%';
  RAISE NOTICE 'Всего функций "reverse" : %', s;
END
$$;
NOTICE:  Всего функций "reverse" : 4
DO

Page 18: Итоги

PL/pgSQL — доступный по умолчанию, интегрированный с SQL, удобный и простой в использовании язык

  • Управление подпрограммами на PL/pgSQL не отличается от работы с подпрограммами на других языках
  • DO — команда SQL для выполнения анонимного блока
  • Переменные PL/pgSQL могут использовать любые типы SQL
  • Язык поддерживает обычные управляющие конструкции, такие как условные операторы и циклы

Page 19: Практика

  1. Измените функцию book_name так, чтобы длина возвращаемого значения не превышала 45 символов. Если название книги при этом обрезается, оно должно завершаться на троеточие.
  • Проверьте реализацию в SQL и в приложении; при необходимости добавьте книг с длинными названиями. 2. Снова измените функцию book_name так, чтобы избыточно длинное название обрезалось по границе целого слова (укорачивание по словам).
    • Проверьте реализацию.

Комментарии преподавателя

  1. Например:
Путешествия в некоторые удалённые страны мира в четырёх частях:
сочинение Лемюэля Гулливера, сначала хирурга, а затем капитана
нескольких кораблей →
→ Путешествия в некоторые удалённые страны м...

Вот некоторые случаи, которые имеет смысл проверить:

● длина названия меньше 45 символов (не должно измениться);

● длина названия ровно 45 символов (не должно измениться);

● длина названия 46 символов (от названия должны быть отрезаны 4 символа, т. к. добавятся еще три точки).

Лучше всего написать и отладить отдельную функцию укорачивания, которую затем использовать в book_name. Это полезно и по другим соображениям:

● такая функция может пригодиться где-то еще;

● каждая функция будет выполнять ровно одну задачу.

  1. Например:
Путешествия в некоторые удалённые страны мира в четырёх частях:
сочинение Лемюэля Гулливера, сначала хирурга, а затем капитана
нескольких кораблей →
→ Путешествия в некоторые удалённые страны...

Как поведет себя ваша реализация, если название состоит из одного длинного слова без пробелов?

Page 20: 1. Укорачивание названия книги

Напишем более универсальную функцию, принимающую строку, максимальную длину и суффикс, добавляемый при укорачивании. Это не потребует усложнения кода, и позволит обойтись без «магических констант».

=> CREATE FUNCTION shorten(
    s text,
    max_len integer DEFAULT 45,
    suffix text DEFAULT '...'
)
RETURNS text AS $$
DECLARE
    suffix_len integer := length(suffix);
BEGIN
    RETURN CASE WHEN length(s) > max_len
        THEN left(s, max_len - suffix_len) || suffix
        ELSE s
    END;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим:

=> SELECT shorten(
    'Путешествия в некоторые удаленные страны мира в четырех частях: сочинение Лемюэля Гулливера, сначала хирурга, а затем капитана нескольких кораблей'
);
                    shorten
-----------------------------------------------
 Путешествия в некоторые удаленные страны м...
(1 row)
=> SELECT shorten(
    'Путешествия в некоторые удаленные страны мира в четырех частях: сочинение Лемюэля Гулливера, сначала хирурга, а затем капитана нескольких кораблей',
    30
);
            shorten
--------------------------------
 Путешествия в некоторые уда...
(1 row)

Используем написанную функцию:

=> CREATE OR REPLACE FUNCTION book_name(book_id integer, title text)
RETURNS text
STABLE LANGUAGE sql
BEGIN ATOMIC
SELECT shorten(book_name.title) ||
       CASE WHEN (right(shorten(book_name.title), 3) != '...') THEN '. '::text ELSE ' ' END ||
       string_agg(
           author_name(a.last_name, a.first_name, a.middle_name), ', '
           ORDER BY ash.seq_num
       )
FROM   authors a
       JOIN authorship ash ON a.author_id = ash.author_id
WHERE  ash.book_id = book_name.book_id;
END;
CREATE FUNCTION

2. Укорачивание названия книги с переносом по словам

=> CREATE OR REPLACE FUNCTION shorten(
    s text,
    max_len integer DEFAULT 45,
    suffix text DEFAULT '...'
)
RETURNS text
AS $$
DECLARE
    suffix_len integer := length(suffix);
    short text := suffix;
BEGIN
    IF length(s) < max_len THEN
        RETURN s;
    END IF;
    FOR pos in 1 .. least(max_len-suffix_len+1, length(s))
    LOOP
        IF substr(s,pos-1,1) != ' ' AND substr(s,pos,1) = ' ' THEN
            short := left(s, pos-1) || suffix;
        END IF;
    END LOOP;
    RETURN short;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим:

=> SELECT shorten(
    'Путешествия в некоторые удаленные страны мира в четырех частях: сочинение Лемюэля Гулливера, сначала хирурга, а затем капитана нескольких кораблей'
);
                   shorten
---------------------------------------------
 Путешествия в некоторые удаленные страны...
(1 row)

Page 21: Page 21

=> SELECT shorten(
    'Путешествия в некоторые удаленные страны мира в четырех частях: сочинение Лемюэля Гулливера, сначала хирурга, а затем капитана нескольких кораблей',
    30
);
          shorten
----------------------------
 Путешествия в некоторые...
(1 row)

Page 22: Практика+

  1. Напишите PL/pgSQL-функцию, которая возвращает строку заданной длины из случайных символов. 2. Задача про игру в «наперстки». В одном из трех наперстков спрятан выигрыш. Игрок выбирает один из этих трех. Ведущий убирает один из двух оставшихся наперстков (обязательно пустой) и дает игроку возможность поменять решение, то есть выбрать второй из двух оставшихся. Есть ли смысл игроку менять выбор или нет смысла менять первоначальный вариант? Задание: используя PL/pgSQL, посчитайте вероятность выигрыша и для начального выбора, и для измененного.

Комментарии преподавателя

Предварительно можно создать функцию rnd_integer, которая возвращает случайное целое число в заданном диапазоне. Функция будет полезна для решения обоих заданий.

Например: rnd_integer(30, 1000) → 616
  1. Помимо длины строки на вход функции можно подавать список допустимых символов. По умолчанию, это могут быть все символы алфавита, числа и некоторые знаки. Для определения случайных символов из списка можно использовать функцию rnd_integer. Объявление функции может быть таким:
CREATE FUNCTION rnd_text(
   len int,
   list_of_chars text DEFAULT
'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэю
яABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789'
) RETURNS text AS ...
Пример вызова: rnd_text(10) → 'ЛжdфbЁ_OОJ'
  1. Для решения можно использовать анонимный блок.

Сначала нужно реализовать одну игру и посмотреть, какой вариант выиграл: начальный или измененный. Для загадывания и угадывания одного из трех наперстков можно использовать rnd_integer(1,3).

Затем игру поместить в цикл и «сыграть», например, 1000 раз, подсчитывая, какой вариант сколько раз победил. В конце через RAISE NOTICE вывести значения счетчиков и выявить победивший вариант (или отсутствие такового).

Page 23: 1. Случайная строка заданного размера

=> CREATE DATABASE plpgsql_introduction;
CREATE DATABASE
=> \c plpgsql_introduction
You are now connected to database "plpgsql_introduction" as user "student".

Вначале определим вспомогательную функцию для получения случайного целого числа в заданном диапазоне. Такую функцию легко написать на чистом SQL, но здесь представлен вариант на PL/pgSQL:

=> CREATE FUNCTION rnd_integer(min_value integer, max_value integer)
RETURNS integer
AS $$
DECLARE
    retval integer;
BEGIN
    IF max_value <= min_value THEN
       RETURN NULL;
    END IF;
    retval := floor(
            (max_value+1 - min_value)*random()
)::integer + min_value;
    RETURN retval;
END
$$ STRICT LANGUAGE plpgsql;
CREATE FUNCTION

Проверяем работу:

=> SELECT rnd_integer(0,1) as "0 - 1",
          rnd_integer(1,365) as "1 - 365",
          rnd_integer(-30,30) as "-30 - +30"
   FROM generate_series(1,10);
 0 - 1 | 1 - 365 | -30 - +30
-------+---------+-----------
     1 |     319 |       -30
     0 |     344 |        21
     1 |      31 |       -22
     1 |      86 |       -14
     0 |     120 |        26
     0 |      91 |       -25
     1 |       5 |       -27
     1 |     156 |        11
     1 |     197 |        13
     1 |     148 |       -14
(10 rows)

Функция гарантирует равномерное распределение случайных значений по всему диапазону, включая граничные значения:

=> SELECT rnd_value, count(*)
FROM (
    SELECT rnd_integer(1,5) AS rnd_value
    FROM generate_series(1,100_000)
)
GROUP BY rnd_value ORDER BY rnd_value;
 rnd_value | count
-----------+-------
         1 | 19934
         2 | 19871
         3 | 20015
         4 | 20162
         5 | 20018
(5 rows)

Теперь можно приступить к написанию функции для получения случайной строки заданного размера. Будем использовать функцию rnd_integer для получения случайного символа из списка.

=> CREATE FUNCTION rnd_text(
   len int,
   list_of_chars text DEFAULT 'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдеёжзийклмнопрстуфхцчшщъыьэюяABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz_0123456789'
) RETURNS text
AS $$
DECLARE
    len_of_list CONSTANT integer := length(list_of_chars);
    i integer;
    retval text := '';
BEGIN
    FOR i IN 1 .. len
    LOOP
        -- добавляем к строке случайный символ
        retval := retval ||
                  substr(list_of_chars, rnd_integer(1,len_of_list),1);
    END LOOP;
    RETURN retval;
END
$$ STRICT LANGUAGE plpgsql;
CREATE FUNCTION

Проверяем:

=> SELECT rnd_text(rnd_integer(1,30)) FROM generate_series(1,10);

Page 24: 2. Игра в наперстки

      rnd_text
--------------------
 щc_кАТпQг
 SьPSчПЙ6БЛDJMЕярЕу
 ж2бiVвS1
 ЫОХf
 ДтЁIй
 ЪjSzRzРу
 Oe
 гuvчgvнSЫJюuЗХцУW
 KрЙf
 сЗ7ЬёsЬмёрuФEЛМuч
(10 rows)

Для загадывания и угадывания наперстка используем rnd_integer(1,3).

=> DO $$
DECLARE
    x integer;
    choice integer;
    new_choice integer;
    remove integer;
    total_games integer := 1000;
    old_choice_win_counter integer := 0;
    new_choice_win_counter integer := 0;
BEGIN
    FOR i IN 1 .. total_games
    LOOP
        -- Загадываем выигрышный наперсток
        x := rnd_integer(1,3);
        -- Игрок делает выбор
        choice := rnd_integer(1,3);
        -- Убираем один неверный ответ, кроме выбора игрока
        FOR i IN 1 .. 3
        LOOP
            IF i NOT IN (x, choice) THEN
                remove := i;
                EXIT;
            END IF;
        END LOOP;
        -- Нужно ли игроку менять свой выбор?
        -- Что лучше: оставить choice или заменить его на оставшийся?
        -- Измененный выбор
        FOR i IN 1 .. 3
        LOOP
            IF i NOT IN (remove, choice) THEN
                new_choice := i;
                EXIT;
            END IF;
        END LOOP;
        -- Или начальный, или новый выбор обязательно выиграют
        IF choice = x THEN
            old_choice_win_counter := old_choice_win_counter + 1;
        ELSIF new_choice = x THEN
            new_choice_win_counter := new_choice_win_counter + 1;
        END IF;
    END LOOP;
    RAISE NOTICE 'Выиграл начальный выбор:  % из %',
        old_choice_win_counter, total_games;
    RAISE NOTICE 'Выиграл измененный выбор: % из %',
        new_choice_win_counter, total_games;
END
$$;
NOTICE:  Выиграл начальный выбор:  346 из 1000
NOTICE:  Выиграл измененный выбор: 654 из 1000
DO

Вначале мы выбираем 1 из 3, поэтому вероятность начального выбора 1/3. Если же выбор изменить, то изменится и вероятность на противоположные 2/3. Таким образом, вероятность выиграть при смене выбора выше. Поэтому есть смысл выбор поменять.

=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_introduction;
DROP DATABASE

12 Занятие 12: PL/pgSQL / Выполнение запросов

Исходная страница в нативном Markdown: 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:

=> CREATE FUNCTION do_something() RETURNS void
AS $$
BEGIN
    RAISE NOTICE 'Что-то сделалось.';
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> DO $$
BEGIN
    PERFORM do_something();
END
$$;
NOTICE:  Что-то сделалось.
DO

Внутри PL/pgSQL можно использовать без изменений практически любые команды SQL, не возвращающие результат:

=> DO $$
BEGIN
    CREATE TABLE test(n integer);
    INSERT INTO test VALUES (1),(2),(3);
    UPDATE test SET n = n + 1 WHERE n > 1;
    DELETE FROM test WHERE n = 1;
    DROP TABLE test;
END
$$;
DO

Управление транзакциями в процедурах

В процедурах (и в анонимных блоках кода) на PL/pgSQL можно использовать команды управления транзакциями:

=> CREATE TABLE test(n integer);
CREATE TABLE
=> CREATE PROCEDURE foo()
AS $$
BEGIN
    INSERT INTO test VALUES (1);
    COMMIT;
    INSERT INTO test VALUES (2);
    ROLLBACK;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CALL foo();
CALL
=> SELECT * FROM test;
 n
---
 1
(1 row)

Действуют определенные ограничения. Во-первых, процедура в таких случаях должна сама начинать новую транзакцию, а не выполняться в контексте уже начатой ранее.

=> BEGIN;
BEGIN
=> CALL foo(); -- ошибка

Page 5: Page 5

ERROR:  invalid transaction termination
CONTEXT:  PL/pgSQL function foo() line 4 at COMMIT
=> ROLLBACK;
ROLLBACK

Во-вторых, в стеке вызовов процедуры не должно быть ничего, кроме операторов CALL. Иными словами, если процедура вызывает процедуру, которая вызывает процедуру… которая выполняет команду управления транзакцией, то все работает:

=> CREATE OR REPLACE PROCEDURE foo()
AS $$
BEGIN
    CALL bar();
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CREATE PROCEDURE bar()
AS $$
BEGIN
    CALL baz();
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CREATE PROCEDURE baz()
AS $$
BEGIN
    COMMIT;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CALL foo(); -- работает
CALL

Но стоит в этой цепочке появиться, например, вызову функции, то получается, что транзакция должна завершиться где-то «посередине» оператора, в контексте которого вызывается эта функция, например SELECT. Это недопустимо, получаем ошибку:

=> CREATE FUNCTION qux() RETURNS void
AS $$
BEGIN
    CALL bar();
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT qux(); -- ошибка
ERROR:  invalid transaction termination
CONTEXT:  PL/pgSQL function baz() line 3 at COMMIT
SQL statement "CALL baz()"
PL/pgSQL function bar() line 3 at CALL
SQL statement "CALL bar()"
PL/pgSQL function qux() line 3 at CALL

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, возвращающая одну строку. Пример выводит одну строку, хотя запрос внутри анонимного блока возвращает две:

=> CREATE TABLE t(id integer, code text);
CREATE TABLE
=> INSERT INTO t VALUES (1, 'Раз'), (2, 'Два');
INSERT 0 2
=> DO $$
DECLARE
    r record;
BEGIN
    SELECT id, code INTO r FROM t;
    RAISE NOTICE '%', r;
END
$$;
NOTICE:  (1,Раз)
DO

Обратите внимание, что в приведенном коде PostgreSQL есть конструкция, очень похожая на SQL-команду SELECT INTO, в которой после INTO указывается имя новой таблицы, создающейся и наполняющейся результатами запроса. В PL/pgSQL для решения такой задачи нужно использовать эквивалентный синтаксис CREATE TABLE … AS SELECT. Команды INSERT, UPDATE, DELETE тоже могут возвращать результат с помощью фразы RETURNING. Их можно использовать в PL/pgSQL точно так же, как SELECT, добавив фразу INTO:

=> DO $$
DECLARE
    r record;
BEGIN
    UPDATE t SET code = code || '!' WHERE id = 1 RETURNING * INTO r;
    RAISE NOTICE 'Изменили: %', r;
END
$$;
NOTICE:  Изменили: (1,Раз!)
DO

Проверки при создании и при выполнении подпрограмм

PL/pgSQL может выдавать предупреждения в некоторых подозрительных случаях. Для этого надо установить параметр (значение по умолчанию — none):

=> SET plpgsql.extra_warnings = 'all';
SET
=> CREATE PROCEDURE bugs(INOUT a integer)
AS $$
DECLARE
    a integer;
    b integer;
BEGIN
    SELECT id INTO a, b FROM t;
END
$$ LANGUAGE plpgsql;
WARNING:  variable "a" shadows a previously defined variable
LINE 4:     a integer;
            ^
CREATE PROCEDURE

Это предупреждение о перекрывающих друг друга определениях переменных.

=> CALL bugs(42);

Page 8: Page 8

WARNING:  query returned more than one row
HINT:  Make sure the query returns a single row, or use LIMIT 1.
WARNING:  number of source and target fields in assignment does not match
DETAIL:  strict_multi_assignment check of extra_warnings is active.
HINT:  Make sure the query returns the exact list of columns.
 a
----
 42
(1 row)

А здесь мы видим два предупреждения времени выполнения: запрос вернул более одной строки, а в предложении INTO указано неверное число параметров (PL/pgSQL присвоит второму неопределенное значение). Других проверок в настоящее время не предусмотрено, но они могут появиться в следующих версиях PostgreSQL. Значение параметра plpgsql.extra_warnings можно ограничить только определенными проверками. Аналогичный параметр plpgsql.extra_errors будет приводить не к предупреждениям, а к ошибкам.

=> RESET plpgsql.extra_warnings;
RESET

Стороннее расширение plpgsql_check, написанное и развиваемое Павлом Стехуле, позволяет проверить код более детально. Расширение уже установлено в виртуальной машине курса.

=> CREATE SCHEMA plpgsql_check;
CREATE SCHEMA
=> CREATE EXTENSION plpgsql_check SCHEMA plpgsql_check;
CREATE EXTENSION
=> SELECT * FROM plpgsql_check.plpgsql_check_function('bugs(integer)');
                        plpgsql_check_function
-----------------------------------------------------------------------
 warning:00000:5:statement block:parameter "a" is shadowed
 Detail: Local variable shadows function parameter.
 warning:00000:6:SQL statement:too few attributes for target variables
 Query: SELECT id           FROM t
 --     ^
 Detail: There are more target variables than output columns in query.
 Hint: Check target variables in SELECT INTO statement.
 Context: at SQL statement to a, b variables
 warning extra:00000:3:DECLARE:never read variable "a"
 warning extra:00000:4:DECLARE:never read variable "b"
 warning extra:00000:unused parameter "a"
 warning extra:00000:unmodified OUT variable "a"
(12 rows)

Дополнительно обнаружены неиспользуемые переменные и тот факт, что выходному параметру не присваивается значение. Расширение имеет множество возможностей для обнаружения проблем в коде, в том числе и на этапе выполнения. Кроме того, расширение включает и профилировщик для целей оптимизации PL/pgSQL-кода.

Устранение неоднозначностей именования

Получится ли выполнить следующий код?

=> DO $$
DECLARE
    id   integer := 1;
    code text;
BEGIN
    SELECT id, code INTO id, code
    FROM t WHERE id = id;
    RAISE NOTICE '%, %', id, code;
END
$$;
ERROR:  column reference "id" is ambiguous
LINE 1: SELECT id, code                   FROM t WHERE id = id
               ^
DETAIL:  It could refer to either a PL/pgSQL variable or a table column.
QUERY:  SELECT id, code                   FROM t WHERE id = id
CONTEXT:  PL/pgSQL function inline_code_block line 6 at SQL statement

Page 9: Page 9

Не получится из-за неоднозначности в SELECT: id может означать и имя столбца, и имя переменной. Причем во фразе INTO неоднозначности нет — она относится только к PL/pgSQL. В сообщении, кстати, видно, как PL/pgSQL вырезает фразу INTO, прежде чем передать запрос в SQL. Есть несколько подходов к устранению неоднозначностей. Первый состоит в том, чтобы неоднозначностей не допускать. Для этого к переменным добавляют префикс, который обычно выбирается в зависимости от «класса» переменной, например: Для параметров p_ (parameter); Для обычных переменных l_ (local) или v_ (variable); Для констант c_ (constant); Это простой и действенный способ, если использовать его систематически и никогда не использовать префиксы в именах столбцов. К минусам можно отнести некоторую неряшливость и пестроту кода из-за лишних подчеркиваний. Вот как это может выглядеть в нашем случае:

=> DO $$
DECLARE
    l_id   integer := 1;
    l_code text;
BEGIN
    SELECT id, code INTO l_id, l_code
    FROM t WHERE id = l_id;
    RAISE NOTICE '%, %', l_id, l_code;
END
$$;
NOTICE:  1, Раз!
DO

Второй способ состоит в использовании квалифицированных имен — к имени объекта через точку дописывается уточняющий квалификатор: для столбца — имя или псевдоним таблицы; для переменной — метку блока; для параметра — имя функции. Такой способ более «честный», чем добавление префиксов, поскольку работает для любых названий столбцов. Вот как будет выглядеть наш пример с использованием квалификаторов:

=> DO $$
<<local>>
DECLARE
    id   integer := 1;
    code text;
BEGIN
    SELECT t.id, t.code INTO local.id, local.code
    FROM t WHERE t.id = local.id;
    RAISE NOTICE '%, %', id, code;
END
$$;
NOTICE:  1, Раз!
DO

Третий вариант — установить приоритет переменных над столбцами или наоборот, столбцов над переменными. За это отвечает конфигурационный параметр plpgsql.variable_conflict. Его возможные значения use_column, use_variable и error. В ряде случаев это упрощает разрешение конфликтов, но не устраняет их полностью. Кроме того, неявное правило (которое, к тому же, может внезапно поменяться) непременно приведет к тому, что какой-то код будет выполняться не так, как предполагал разработчик. Тем не менее приведем пример. Здесь устанавливается приоритет переменных, поэтому достаточно квалифицировать только столбцы таблицы:

=> SET plpgsql.variable_conflict = use_variable;
SET

Page 10: Page 10

=> DO $$
DECLARE
    id   integer := 1;
    code text;
BEGIN
    SELECT t.id, t.code INTO id, code
    FROM t WHERE t.id = id;
    RAISE NOTICE '%, %', id, code;
END
$$;
NOTICE:  1, Раз!
DO
=> RESET plpgsql.variable_conflict;  -- сбросим значение к умолчательному
RESET
=> SHOW plpgsql.variable_conflict;
 plpgsql.variable_conflict
---------------------------
 error
(1 row)

Какой способ выбрать — дело опыта и вкуса. Мы рекомендуем остановиться либо на первом (префиксы), либо на втором (квалификаторы), и не смешивать их в одном проекте, поскольку систематичность крайне важна для облегчения понимания кода. В курсе мы будем использовать второй способ, но только в тех случаях, когда это действительно необходимо — чтобы не загромождать примеры. Однако в коде, предназначенном для промышленной эксплуатации, думать о неоднозначностях надо всегда: нет никакой гарантии, что завтра в таблице не появится новый столбец с именем, совпадающим с вашей переменной!

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: Ровно одна строка

Что произойдет, если запрос вернет несколько строк?

=> DO $$
DECLARE
    r record;
BEGIN
    SELECT id, code INTO r FROM t;
    RAISE NOTICE '%', r;
END
$$;
NOTICE:  (2,Два)
DO

В переменную будет записана только первая строка. Поскольку мы не указали ORDER BY, то порядок строк в общем случае непредсказуем:

=> SELECT * FROM t;
 id | code
----+------
  2 | Два
  1 | Раз!
(2 rows)

Поскольку в командах INSERT, UPDATE, DELETE нет возможности указать порядок строк, то команда, затрагивающая несколько строк, приводит к ошибке:

=> DO $$
DECLARE
    r record;
BEGIN
    UPDATE t SET code = code || '!' RETURNING * INTO r;
    RAISE NOTICE 'Изменили: %', r;
END
$$;
ERROR:  query returned more than one row
HINT:  Make sure the query returns a single row, or use LIMIT 1.
CONTEXT:  PL/pgSQL function inline_code_block line 5 at SQL statement

А если запрос не вернет ни одной строки?

=> DO $$
DECLARE
    r record;
BEGIN
    r := (-1,'!!!');
    SELECT id, code INTO r FROM t WHERE false;
    RAISE NOTICE '%', r;
END
$$;
NOTICE:  (,)
DO

Переменные будут содержать неопределенные значения. То же относится и командам INSERT, UPDATE, DELETE. Например:

=> DO $$
DECLARE
    r record;
BEGIN
    UPDATE t SET code = code || '!' WHERE id = -1
        RETURNING * INTO r;
    RAISE NOTICE 'Изменили: %', r;
END
$$;
NOTICE:  Изменили: (,)
DO

Иногда хочется быть уверенным, что в результате выборки получилась ровно одна строка: ни больше, ни меньше. В этом случае удобно воспользоваться фразой INTO STRICT:

Page 13: Page 13

=> DO $$
DECLARE
    r record;
BEGIN
    SELECT id, code INTO STRICT r FROM t;
    RAISE NOTICE '%', r;
END
$$;
ERROR:  query returned more than one row
HINT:  Make sure the query returns a single row, or use LIMIT 1.
CONTEXT:  PL/pgSQL function inline_code_block line 5 at SQL statement
=> DO $$
DECLARE
    r record;
BEGIN
    SELECT id, code INTO STRICT r FROM t WHERE false;
    RAISE NOTICE '%', r;
END
$$;
ERROR:  query returned no rows
CONTEXT:  PL/pgSQL function inline_code_block line 5 at SQL statement

Как мы видели, команды INSERT, UPDATE, DELETE, затрагивающие несколько строк, приводят к ошибке. Фраза STRICT позволяет гарантировать, что строка будет ровно одна (а не ноль):

=> DO $$
DECLARE
    r record;
BEGIN
    UPDATE t SET code = code || '!' WHERE id = -1 RETURNING * INTO STRICT r;
    RAISE NOTICE 'Изменили: %', r;
END
$$;
ERROR:  query returned no rows
CONTEXT:  PL/pgSQL function inline_code_block line 5 at SQL statement

Явная проверка состояния

Другая возможность — проверять состояние последней выполненной SQL-команды: Команда GET DIAGNOSTICS позволяет получить количество затронутых строк (ROW_COUNT); Предопределенная логическая переменная FOUND показывает, была ли затронута хотя бы одна строка.

=> DO $$
DECLARE
    r record;
    rowcount integer;
BEGIN
    SELECT id, code INTO r FROM t WHERE false;
    GET DIAGNOSTICS rowcount := ROW_COUNT;
    RAISE NOTICE 'rowcount = %', rowcount;
    RAISE NOTICE 'found = %', FOUND;
END
$$;
NOTICE:  rowcount = 0
NOTICE:  found = f
DO
=> DO $$
DECLARE
    r record;
    rowcount integer;
BEGIN
    SELECT id, code INTO r FROM t;
    GET DIAGNOSTICS rowcount := ROW_COUNT;
    RAISE NOTICE 'rowcount = %', rowcount;
    RAISE NOTICE 'found = %', FOUND;
END
$$;
NOTICE:  rowcount = 1
NOTICE:  found = t
DO

Page 14: Page 14

Заметьте: диагностика не позволяет обнаружить, что запросу соответствует несколько строк. Элемент диагностики ROW_COUNT возвращает единицу, так как только одна строка из полученного набора была помещена в переменную r.

Page 15: Табличные функции

Строки запроса

RETURN QUERY запрос;
  • Одна строка
    • 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:

=> CREATE FUNCTION t() RETURNS TABLE(LIKE t)
AS $$
BEGIN
    RETURN QUERY SELECT id, code FROM t ORDER BY id;
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT * FROM t();
 id | code
----+------
  1 | Раз!
  2 | Два
(2 rows)

Другой вариант — возвращать значения построчно.

=> CREATE FUNCTION days_of_week() RETURNS SETOF text
AS $$
BEGIN
    FOR i IN 7 .. 13 LOOP
        RETURN NEXT to_char(to_date(i::text,'J'),'TMDy');
    END LOOP;
END;
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT * FROM days_of_week() WITH ORDINALITY;
 days_of_week | ordinality
--------------+------------
 Пн           |          1
 Вт           |          2
 Ср           |          3
 Чт           |          4
 Пт           |          5
 Сб           |          6
 Вс           |          7
(7 rows)

Почему функция объявлена как STABLE, а не как IMMUTABLE? На первый взгляд кажется, что при повторных вызовах функция будет возвращать тот же результат, но на самом деле он неявно зависит от текущей локали:

=> SET lc_time = 'en_US.UTF8';
SET
=> SELECT * FROM days_of_week() WITH ORDINALITY;
 days_of_week | ordinality
--------------+------------
 Mon          |          1
 Tue          |          2
 Wed          |          3
 Thu          |          4
 Fri          |          5
 Sat          |          6
 Sun          |          7
(7 rows)

Еще один пример иллюстрирует «смешанный» подход. Здесь мы создаем функцию, которая возвращает список подпрограмм из своей схемы. Элемент диагностики PG_ROUTINE_OID позволяет функции получить свой oid:

Page 17: Page 17

=> CREATE FUNCTION where_am_i() RETURNS TABLE(name text, isitme text)
AS $$
DECLARE
    my_oid oid;
    schema_oid oid;
BEGIN
    GET DIAGNOSTICS my_oid := PG_ROUTINE_OID;
    -- oid схемы
    schema_oid := pronamespace FROM pg_proc WHERE oid = my_oid;
    -- Заголовок
    name := '=== Схема: ' || schema_oid::regnamespace;
    RETURN NEXT;  -- заголовок с названием схемы
    -- Список подпрограмм
    RETURN QUERY
SELECT proname::text, CASE WHEN oid = my_oid THEN 'It''s me!' END
FROM pg_proc
WHERE pronamespace = schema_oid
ORDER BY 1;
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

Пробуем вызов:

=> SELECT * FROM where_am_i();
       name        |  isitme
-------------------+----------
 === Схема: public |
 bar               |
 baz               |
 bugs              |
 days_of_week      |
 do_something      |
 foo               |
 qux               |
 t                 |
 where_am_i        | It's me!
(10 rows)

Page 18: Итоги

PL/pgSQL тесно интегрирован с SQL

  • в процедурном коде можно выполнять запросы
  • (оформленные как выражения или отдельные команды)
  • в запросах можно использовать переменные
  • можно получать результаты запросов и их статус
  • Нужно следить за неоднозначностями разрешения имен

Page 19: Практика

  1. Напишите функцию add_author для добавления новых авторов. Функция должна принимать три параметра (фамилия, имя, отчество) и возвращать идентификатор нового автора.
  • Проверьте, что приложение позволяет добавлять авторов. 2. Напишите функцию buy_book для покупки книги. Функция принимает идентификатор книги и уменьшает количество таких книг на складе на единицу. Возвращаемое значение отсутствует.
    • Проверьте, что в «Магазине» появилась возможность
    • покупки книг.

Комментарии преподавателя

FUNCTION add_author(last_name text, first_name text, middle_name
text)
RETURNS integer
FUNCTION buy_book(book_id integer)
RETURNS void

Вы можете обратить внимание, что при покупке книг приложение позволяет «уйти в минус». Если бы количество книг хранилось в столбце, простым и хорошим решением было бы сделать ограничение CHECK. Но в нашем случае количество рассчитывается, и мы отложим написание проверки до темы «Триггеры».

Page 20: 1. Функция add_author

=> CREATE FUNCTION add_author(
    last_name text,
    first_name text,
    middle_name text
) RETURNS integer
AS $$
DECLARE
    author_id integer;
BEGIN
    INSERT INTO authors(last_name, first_name, middle_name)
        VALUES (last_name, first_name, middle_name)
        RETURNING authors.author_id INTO author_id;
    RETURN author_id;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

2. Функция buy\_book

=> CREATE FUNCTION buy_book(book_id integer)
RETURNS void
AS $$
BEGIN
    INSERT INTO operations(book_id, qty_change)
        VALUES (book_id, -1);
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Page 21: Практика+

Напишите игру, в которой сервер пытается угадать загаданное пользователем животное, задавая последовательные уточняющие вопросы, на которые можно отвечать «да» или «нет». Если сервер предложил неправильный вариант, он запрашивает у пользователя имя животного и отличающий вопрос. Эта новая информация запоминается и используется в следующих играх. 1. Создайте таблицу для представления информации. 2. Придумайте интерфейс и реализуйте необходимые функции. 3. Проверьте реализацию.

Комментарии преподавателя

Пример диалога (между людьми):

  • Это млекопитающее?
  • Да.
  • Это слон?
  • Нет.
  • Сдаюсь. Кто это?
  • Кит.
  • Как отличить кита от слона?
  • Он живет в воде.
  1. Информацию удобно представить в виде двоичного дерева. Внутренние узлы хранят вопросы, листовые узлы — названия животных. Один из дочерних узлов соответствует ответу «да», другой — ответу «нет».

  2. Между вызовами функций надо передавать информацию о том, на каком узле дерева мы остановились («контекст» диалога). Функции могут быть, например, такими.

● начать игру (нет входного контекста): 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. Таблица

=> CREATE DATABASE plpgsql_queries;
CREATE DATABASE
=> \c plpgsql_queries
You are now connected to database "plpgsql_queries" as user "student".
=> CREATE TABLE animals(
    id     integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    yes_id integer REFERENCES animals(id),
    no_id  integer REFERENCES animals(id),
    name   text
);
CREATE TABLE
=> INSERT INTO animals(name) VALUES
    ('млекопитающее'), ('слон'), ('черепаха');
INSERT 0 3
=> UPDATE animals SET yes_id = 2, no_id = 3 WHERE id = 1;
UPDATE 1
=> SELECT * FROM animals ORDER BY id;
 id | yes_id | no_id |     name
----+--------+-------+---------------
  1 |      2 |     3 | млекопитающее
  2 |        |       | слон
  3 |        |       | черепаха
(3 rows)

Первая строка считается корнем дерева.

2. Функции

=> CREATE FUNCTION start_game(
    OUT context integer,
    OUT question text
)
AS $$
DECLARE
    root_id CONSTANT integer := 1;
BEGIN
    SELECT id, name||'?'
    INTO context, question
    FROM animals
    WHERE id = root_id;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Page 23: Page 23

=> CREATE FUNCTION continue_game(
    INOUT context integer,
    IN answer boolean,
    OUT you_win boolean,
    OUT question text
)
AS $$
DECLARE
    new_context integer;
BEGIN
    SELECT CASE WHEN answer THEN yes_id ELSE no_id END
    INTO new_context
    FROM animals
    WHERE id = context;
    IF new_context IS NULL THEN
        you_win := NOT answer;
        question := CASE
            WHEN you_win THEN 'Сдаюсь'
            ELSE 'Вы проиграли'
        END;
    ELSE
        SELECT id, null, name||'?'
        INTO context, you_win, question
        FROM animals
        WHERE id = new_context;
    END IF;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE FUNCTION end_game(
    IN context integer,
    IN name text,
    IN question text
) RETURNS void
AS $$
DECLARE
    new_animal_id integer;
    new_question_id integer;
BEGIN
    INSERT INTO animals(name) VALUES (name)
        RETURNING id INTO new_animal_id;
    INSERT INTO animals(name) VALUES (question)
        RETURNING id INTO new_question_id;
    UPDATE animals SET yes_id = new_question_id
    WHERE yes_id = context;
    UPDATE animals SET  no_id = new_question_id
    WHERE  no_id = context;
    UPDATE animals SET yes_id = new_animal_id, no_id = context
    WHERE id = new_question_id;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

3. Пример сеанса игры

Загадываем слово «кит».

=> SELECT * FROM start_game();
 context |    question
---------+----------------
       1 | млекопитающее?
(1 row)
=> SELECT * FROM continue_game(1,true);
 context | you_win | question
---------+---------+----------
       2 |         | слон?
(1 row)
=> SELECT * FROM continue_game(2,false);

Page 24: Page 24

 context | you_win | question
---------+---------+----------
       2 | t       | Сдаюсь
(1 row)
=> SELECT * FROM end_game(2,'кит','живет в воде');
 end_game
----------
(1 row)

Теперь в таблице:

=> SELECT * FROM animals ORDER BY id;
 id | yes_id | no_id |     name
----+--------+-------+---------------
  1 |      5 |     3 | млекопитающее
  2 |        |       | слон
  3 |        |       | черепаха
  4 |        |       | кит
  5 |      4 |     2 | живет в воде
(5 rows)

Снова загадали «кит».

=> SELECT * FROM start_game();
 context |    question
---------+----------------
       1 | млекопитающее?
(1 row)
=> SELECT * FROM continue_game(1,true);
 context | you_win |   question
---------+---------+---------------
       5 |         | живет в воде?
(1 row)
=> SELECT * FROM continue_game(5,true);
 context | you_win | question
---------+---------+----------
       4 |         | кит?
(1 row)
=> SELECT * FROM continue_game(4,true);
 context | you_win |   question
---------+---------+--------------
       4 | f       | Вы проиграли
(1 row)
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_queries;
DROP DATABASE

13 Занятие 13: PL/pgSQL / Курсоры

Исходная страница в нативном Markdown: PL/pgSQL / Курсоры
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_13_plpgsql_cursors.html Archive: DEV1-handouts-16.zip Страниц PDF: 19

Page 1: PL/pgSQL / Курсоры

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Причины использования
  • Объявление и открытие курсора
  • Операции с курсором
  • Циклы по курсору и по результатам запроса
  • Передача курсора клиенту

Page 3: Причины использования

Курсор подразумевает итеративную обработку

  • полная выборка занимает слишком много памяти
  • нужна не вся выборка, но размер заранее неизвестен
  • способ отдать управление выборкой клиенту
  • действительно требуется построчная обработка (обычно нет)

Комментарии преподавателя

С концепцией курсоров мы уже знакомы по теме «Архитектура. Общее устройство PostgreSQL». Там речь шла о курсорах как о возможности, предоставляемой сервером, и мы смотрели, как к этой возможности обращаться средствами SQL. Теперь поговорим о том, как использовать те же самые курсоры в языке PL/pgSQL.

Зачем вообще может возникнуть необходимость в курсорах? Декларативный SQL в первую очередь предназначен для работы с множествами строк — в этом его сила и преимущество. PL/pgSQL, как процедурный язык, должен работать со строками по одной за раз, используя явные циклы. Такой подход как раз реализуется при помощи курсоров.

Например, полная выборка может занимать слишком много места, так что приходится обрабатывать результаты по частям. Или мы получаем набор данных неизвестного заранее размера — и в процессе выборки нужно вовремя остановиться. Или есть необходимость предоставить управление выборкой клиенту.

(Однако еще раз отметим, что хотя необходимость такой построчной обработки может возникать, в большом количестве случаев ее можно заменить чистым SQL; код в итоге окажется проще и будет быстрее работать.)

Page 4: Объявление и открытие

Не связанные с запросом курсорные переменные

  • объявляется переменная типа refcursor
  • конкретный запрос указывается при открытии
  • нет в SQL
  • Связанные с запросом курсорные переменные
    • при объявлении указывается запрос (возможно, с параметрами)
    • при открытии указываются фактические значения параметров
  • Особенности
    • значение курсорной переменной — имя курсора
    • (можно задать явно или сгенерируется автоматически)
    • переменные PL/pgSQL в запросе становятся неявными параметрами
    • (значения подставляются при открытии курсора)
    • запрос предварительно подготавливается

Комментарии преподавателя

Как мы видели, в SQL курсор объявлялся и открывался одновременно командой DECLARE. В PL/pgSQL это два отдельных шага. Кроме того, для доступа к курсорам используются так называемые курсорные переменные, имеющие тип refcursor и, фактически, содержащие имя курсора (причем если не указывать это имя явно, PL/pgSQL сам позаботится о его уникальности).

Курсорную переменную можно объявить, не связывая ее с конкретным запросом. Тогда при открытии курсора нужно будет указать запрос.

Другой вариант — уже при объявлении переменной указать запрос, возможно, с параметрами. Тогда при открытии курсора указываются только фактические параметры.

Оба способа равноценны; какой использовать — дело вкуса. И связанные, и несвязанные курсорные переменные инициализируются только при открытии курсора; в обоих случаях соответствующий запрос может иметь неявные параметры — переменные PL/pgSQL.

Напомним, что запрос, открытый с помощью курсора, автоматически подготавливается.

https://postgrespro.ru/docs/postgresql/16/plpgsql-cursors#PLPGSQL-CURS OR-DECLARATIONS

https://postgrespro.ru/docs/postgresql/16/plpgsql-cursors#PLPGSQL-CURS OR-OPENING

Page 5: Объявление и открытие

Создадим таблицу:

=> CREATE TABLE t(id integer, s text);
CREATE TABLE
=> INSERT INTO t VALUES (1, 'Раз'), (2, 'Два'), (3, 'Три');
INSERT 0 3

Несвязанная переменная:

=> DO $$
DECLARE
    -- объявление переменной
    cur refcursor;
BEGIN
    -- связывание с запросом и открытие курсора
    OPEN cur FOR SELECT * FROM t;
END
$$;
DO

Связанная переменная: запрос указывается уже при объявлении. При этом переменная cur имеет тот же тип refcursor.

=> DO $$
DECLARE
    -- объявление и связывание переменной
    cur CURSOR FOR SELECT * FROM t;
BEGIN
    -- открытие курсора
    OPEN cur;
END
$$;
DO

В случае связанной переменной запрос может иметь параметры. Обратите внимание на устранение неоднозначности имен в этом и следующем примерах.

=> DO $$
DECLARE
    -- объявление и связывание переменной
    cur CURSOR(id integer) FOR SELECT * FROM t WHERE t.id = cur.id;
BEGIN
    -- открытие курсора с указанием фактических параметров
    OPEN cur(1);
END
$$;
DO

Переменные PL/pgSQL также являются (неявными) параметрами курсора.

=> DO $$
<<local>>
DECLARE
    id integer := 3;
    -- объявление и связывание переменной
    cur CURSOR FOR SELECT * FROM t WHERE t.id = local.id;
BEGIN
    id := 1;
    -- открытие курсора (значение id берется на этот момент)
    OPEN cur;
END
$$;
DO

В качестве запроса можно использовать не только команду SELECT, но и любую другую, возвращающую результат (например, INSERT, UPDATE, DELETE с фразой RETURNING).

Page 6: Операции с курсором

  • в SQL
  • размер выборки
  • Выборка
    • определяется
    • только по одной строке
  • Обращение к текущей строке курсора
    • только для простых запросов
    • (одна таблица, без группировок и сортировок)
  • Обычно обработка выполняется в цикле
    • цикл FOR по курсору
    • цикл FOR по запросу без явного определения курсора
    • в SQL
    • DECLARE
    • WITH HOLD
  • Закрытие
    • явно или автоматически при завершении транзакции

Комментарии преподавателя

Выборка строк из курсора возможна в PL/pgSQL только по одной. Для этого служит команда FETCH INTO.

Если запрос достаточно простой (одна таблица, без группировок и сортировок), то в процессе работы с курсором можно обращаться к текущей строке, например, в командах UPDATE или DELETE.

Процедурная обработка данных подразумевает работу с циклами. Можно организовать перебор и обработку всех строк, возвращаемых курсором, с помощью тех управляющих команд, что мы уже знаем. Но, поскольку часто нужен именно такой цикл, в PL/pgSQL есть специальная команда FOR. Целочисленный вариант FOR мы уже видели в теме «Обзор и конструкции языка», а этот вариант работает с курсором. Более того, есть еще один вариант цикла FOR, в котором не требуется даже объявления курсора — в команде указывается сам запрос.

Курсор можно закрыть явно командой CLOSE, но он в любом случае будет закрыт по окончании транзакции (в SQL курсор может оставаться открытым и после завершения транзакции, если указать фразу WITH HOLD).

https://postgrespro.ru/docs/postgresql/16/plpgsql-cursors#PLPGSQL-CURS OR-USING

https://postgrespro.ru/docs/postgresql/16/plpgsql-cursors#PLPGSQL-CURS OR-FOR-LOOP

Page 7: Операции с курсором

Команда FETCH читает следующую строку из курсора. Если нужно только переместиться на следующую строку, можно воспользоваться другой командой — MOVE. Что будет выведено на экран?

=> DO $$
DECLARE
    cur refcursor;
    rec record; -- можно использовать и несколько скалярных переменных
BEGIN
    OPEN cur FOR SELECT * FROM t ORDER BY id;
    MOVE cur;
    FETCH cur INTO rec;
    RAISE NOTICE '%', rec;
    CLOSE cur;
END
$$;
NOTICE:  (2,Два)
DO

Обычно выборка происходит в цикле, который можно организовать так:

=> DO $$
DECLARE
    cur refcursor;
    rec record;
BEGIN
    OPEN cur FOR SELECT * FROM t;
    LOOP
        FETCH cur INTO rec;
        EXIT WHEN NOT FOUND; -- FOUND: выбрана ли очередная строка?
        RAISE NOTICE '%', rec;
    END LOOP;
    CLOSE cur;
END
$$;
NOTICE:  (1,Раз)
NOTICE:  (2,Два)
NOTICE:  (3,Три)
DO

Но чтобы не писать много команд, в PL/pgSQL имеется цикл FOR по курсору, который делает ровно то же самое:

=> DO $$
DECLARE
    cur CURSOR FOR SELECT * FROM t;
    -- переменная цикла не объявляется
BEGIN
    FOR rec IN cur LOOP -- cur должна быть связана с запросом
        RAISE NOTICE '%', rec;
    END LOOP;
END
$$;
NOTICE:  (1,Раз)
NOTICE:  (2,Два)
NOTICE:  (3,Три)
DO

Более того, можно вообще обойтись без явной работы с курсором, если цикл — это все, что требуется. Скобки вокруг запроса не обязательны, но удобны.

=> DO $$
DECLARE
    rec record; -- надо объявить явно
BEGIN
    FOR rec IN (SELECT * FROM t) LOOP
        RAISE NOTICE '%', rec;
    END LOOP;
END
$$;

Page 8: Page 8

NOTICE:  (1,Раз)
NOTICE:  (2,Два)
NOTICE:  (3,Три)
DO

Как и для любого цикла, здесь можно указать метку, что может оказаться полезным во вложенных циклах: Что будет выведено?

=> DO $$
DECLARE
    rec_outer record;
    rec_inner record;
BEGIN
    <<outer>>
    FOR rec_outer IN (SELECT * FROM t ORDER BY id) LOOP
        <<inner>>
        FOR rec_inner IN (SELECT * FROM t ORDER BY id) LOOP
            EXIT outer WHEN rec_inner.id = 3;
            RAISE NOTICE '%, %', rec_outer, rec_inner;
        END LOOP inner;
    END LOOP outer;
END
$$;
NOTICE:  (1,Раз), (1,Раз)
NOTICE:  (1,Раз), (2,Два)
DO

После выполнения цикла переменная FOUND позволяет узнать, была ли обработана хотя бы одна строка:

=> DO $$
DECLARE
    rec record;
BEGIN
    FOR rec IN (SELECT * FROM t WHERE false) LOOP
        RAISE NOTICE '%', rec;
    END LOOP;
    RAISE NOTICE 'Была ли как минимум одна итерация? %', FOUND;
END
$$;
NOTICE:  Была ли как минимум одна итерация? f
DO

На текущую строку курсора, связанного с простым запросом (по одной таблице, без группировок и сортировок) можно сослаться с помощью предложения CURRENT OF. Типичный случай применения — обработка пакета заданий с изменением статуса каждого из них.

=> DO $$
DECLARE
    cur refcursor;
    rec record;
BEGIN
    OPEN cur FOR SELECT * FROM t
        FOR UPDATE; -- строки блокируются по мере обработки
    LOOP
        FETCH cur INTO rec;
        EXIT WHEN NOT FOUND;
        UPDATE t SET s = s || ' (обработано)' WHERE CURRENT OF cur;
    END LOOP;
    CLOSE cur;
END
$$;
DO
=> SELECT * FROM t;
 id |        s
----+------------------
  1 | Раз (обработано)
  2 | Два (обработано)
  3 | Три (обработано)
(3 rows)

Заметим, что CURRENT OF не работает с циклом FOR по запросу, поскольку этот цикл не использует курсор явным образом. Конечно, аналогичный результат можно получить, явно указав в команде UPDATE или DELETE уникальный ключ таблицы (WHERE id = rec.id). Но CURRENT OF работает быстрее и не требует наличия индекса. Следует заметить, что в большом числе случаев вместо использования циклов можно выполнить задачу одним

Page 9: Page 9

оператором SQL — и это будет проще и еще быстрее. Часто циклы используют просто потому, что это более привычный, «процедурный» стиль программирования. Но для баз данных этот стиль не подходит. Например:

=> BEGIN;
DO $$
DECLARE
    rec record;
BEGIN
    FOR rec IN (SELECT * FROM t) LOOP
        RAISE NOTICE '%', rec;
        DELETE FROM t WHERE id = rec.id;
    END LOOP;
END
$$;
ROLLBACK;
BEGIN
NOTICE:  (1,"Раз (обработано)")
NOTICE:  (2,"Два (обработано)")
NOTICE:  (3,"Три (обработано)")
DO
ROLLBACK

Такой цикл заменяется одной простой SQL-командой:

=> BEGIN;
DELETE FROM t RETURNING *;
ROLLBACK;
BEGIN
 id |        s
----+------------------
  1 | Раз (обработано)
  2 | Два (обработано)
  3 | Три (обработано)
(3 rows)
DELETE 3
ROLLBACK

Page 10: Передача курсора клиенту

Source slide page 10: Передача курсора клиенту{#source-figure-010 caption=“Source slide page 10: Передача курсора клиенту”}

Комментарии преподавателя

Как уже говорилось, курсорная переменная PL/pgSQL (типа refcursor) содержит имя открытого SQL-курсора. Когда говорят о курсоре как о памяти, отведенной в обслуживающем процессе для хранения его состояния, в документации используется термин портал.

Таким образом, функция на PL/pgSQL может открыть курсор и вернуть его имя клиенту. Дальше клиент сможет работать с курсором так, как будто он сам его и открыл, но доступ будет иметь только к тем данным, которые предоставлены при открытии. Это дает еще одну возможность для организации взаимодействия приложения и базы данных.

Page 11: Передача курсора клиенту

Откроем курсор и выведем значение курсорной переменной:

=> DO $$
DECLARE
    cur refcursor;
BEGIN
    OPEN cur FOR SELECT * FROM t;
    RAISE NOTICE '%', cur;
END
$$;
NOTICE:  <unnamed portal 14>
DO

Это имя курсора (портала), который был открыт на сервере. Оно было сгенерировано автоматически. При желании имя можно задать явно (но оно должно быть уникальным в сеансе):

=> DO $$
DECLARE
    cur refcursor := 'cursor12345';
BEGIN
    OPEN cur FOR SELECT * FROM t;
    RAISE NOTICE '%', cur;
END
$$;
NOTICE:  cursor12345
DO

Пользуясь этим, можно написать функцию, которая откроет курсор и вернет его имя:

=> CREATE FUNCTION t_cur() RETURNS refcursor
AS $$
DECLARE
    cur refcursor;
BEGIN
    OPEN cur FOR SELECT * FROM t;
    RETURN cur;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Клиент начинает транзакцию…

=> BEGIN;
BEGIN

…вызывает функцию, узнает имя курсора…

=> SELECT t_cur();
        t_cur
---------------------
 <unnamed portal 15>
(1 row)

…и получает возможность читать из него данные (кавычки нужны из-за спецсимволов в имени):

=> FETCH "<unnamed portal 15>";
 id |        s
----+------------------
  1 | Раз (обработано)
(1 row)
=> COMMIT;
COMMIT

Для удобства можно позволить клиенту самому устанавливать имя курсора:

=> DROP FUNCTION t_cur();

Page 12: Page 12

DROP FUNCTION
=> CREATE FUNCTION t_cur(cur refcursor) RETURNS void
AS $$
BEGIN
    OPEN cur FOR SELECT * FROM t;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Клиентский код упрощается:

=> BEGIN;
BEGIN
=> SELECT t_cur('cursor12345');
 t_cur
-------
(1 row)
=> FETCH cursor12345;
 id |        s
----+------------------
  1 | Раз (обработано)
(1 row)
=> COMMIT;
COMMIT

Функция может вернуть и несколько открытых курсоров, используя OUT-параметры. Таким образом можно за один вызов функции обеспечить клиента информацией из разных таблиц, если это необходимо. Альтернативный подход — сразу выбрать все необходимые данные на стороне сервера и сформировать из них документ JSON или XML. Работа с этими форматами рассматривается в курсе DEV2.

Page 13: Итоги

Курсор позволяет получать и обрабатывать данные построчно

  • Цикл FOR упрощает работу с курсорами
  • Обработка в цикле естественна для процедурных языков, но этим не стоит злоупотреблять

Page 14: Практика

  1. Измените функцию book_name: если у книги больше двух авторов, то в названии должны указываться первые два с добавлением «и др.» в конце.
  • Проверьте работу функции в SQL и в приложении. 2. Попробуйте написать функцию book_name на SQL. Какой вариант нравится больше — PL/pgSQL или SQL?
  1. Например:
Хрестоматия. Пушкин А. С., Толстой Л. Н., Тургенев И. С. →
→ Хрестоматия. Пушкин А. С., Толстой Л. Н. и др.

Page 15: 1. Функция book_name (сокращение авторов)

Напишем более универсальную функцию с дополнительным параметром — максимальное число авторов в названии. Поскольку функция меняет сигнатуру (число и/или типы входных параметров), ее необходимо сначала удалить, а потом создать заново. В данном случае у функции есть зависимый объект — представление catalog_v, в котором она используется. Представление тоже придется пересоздать (в реальной работе все эти действия надо выполнять в одной транзакции, чтобы изменения вступили в силу атомарно).

=> DROP FUNCTION book_name(integer,text) CASCADE;
NOTICE:  drop cascades to 2 other objects
DETAIL:  drop cascades to view catalog_v
drop cascades to function get_catalog(text,text,boolean)
DROP FUNCTION
=> CREATE FUNCTION book_name(
    book_id integer,
    title text,
    maxauthors integer DEFAULT 2
)
RETURNS text
AS $$
DECLARE
    r record;
    res text := shorten(title);
BEGIN
    IF (right(res, 3) != '...') THEN res := res || '.'; END IF;
    res := res || ' ';
    FOR r IN (
        SELECT a.last_name, a.first_name, a.middle_name, ash.seq_num
        FROM   authors a
               JOIN authorship ash ON a.author_id = ash.author_id
        WHERE  ash.book_id = book_name.book_id
        ORDER BY ash.seq_num
    )
    LOOP
        EXIT WHEN r.seq_num > maxauthors;
        res := res || author_name(r.last_name, r.first_name, r.middle_name) || ', ';
    END LOOP;
    res := rtrim(res, ', ');
    IF r.seq_num > maxauthors THEN
        res := res || ' и др.';
    END IF;
    RETURN res;
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE OR REPLACE VIEW catalog_v AS
SELECT b.book_id,
       b.title,
       b.onhand_qty,
       book_name(b.book_id, b.title) AS display_name,
       b.authors
FROM   books b
ORDER BY display_name;
CREATE VIEW
=> SELECT book_id, display_name FROM catalog_v;
 book_id |                     display_name
---------+-------------------------------------------------------
       4 | Война и мир. Толстой Л. Н.
       2 | Муму. Тургенев И. С.
       5 | Путешествия в некоторые удаленные
страны... Свифт Д.
       1 | Сказка о царе Салтане. Пушкин А. С.
       3 | Трудно быть богом. Стругацкий А. Н.,
Стругацкий Б. Н.
       6 | Хрестоматия. Пушкин А. С., Толстой Л. Н. и др.
(6 rows)

Не забудем также создать заново удаленную выше функцию get_catalog:

Page 16: Page 16

=> CREATE FUNCTION get_catalog(
    author_name text,
    book_title text,
    in_stock boolean
)
RETURNS TABLE(book_id integer, display_name text, onhand_qty integer)
STABLE LANGUAGE sql
BEGIN ATOMIC
    SELECT cv.book_id,
           cv.display_name,
           cv.onhand_qty
    FROM   catalog_v cv
    WHERE  cv.title   ILIKE '%'||coalesce(book_title,'')||'%'
    AND    cv.authors ILIKE '%'||coalesce(author_name,'')||'%'
    AND    (in_stock AND cv.onhand_qty > 0 OR in_stock IS NOT TRUE)
    ORDER BY display_name;
END;
CREATE FUNCTION

2. Вариант на чистом SQL

=> CREATE OR REPLACE FUNCTION book_name(
    book_id integer,
    title text,
    maxauthors integer DEFAULT 2
)
RETURNS text
STABLE LANGUAGE sql
BEGIN ATOMIC
SELECT shorten(book_name.title) ||
       CASE WHEN (right(shorten(book_name.title), 3) != '...') THEN '. '::text ELSE ' ' END ||
       string_agg(
           author_name(a.last_name, a.first_name, a.middle_name), ', '
           ORDER BY ash.seq_num
       ) FILTER (WHERE ash.seq_num <= maxauthors) ||
       CASE
           WHEN max(ash.seq_num) > maxauthors THEN ' и др.'
           ELSE ''
       END
FROM   authors a
       JOIN authorship ash ON a.author_id = ash.author_id
WHERE  ash.book_id = book_name.book_id;
END;
CREATE FUNCTION
=> SELECT book_id, display_name FROM catalog_v;
 book_id |                     display_name
---------+-------------------------------------------------------
       4 | Война и мир. Толстой Л. Н.
       2 | Муму. Тургенев И. С.
       5 | Путешествия в некоторые удаленные
страны... Свифт Д.
       1 | Сказка о царе Салтане. Пушкин А. С.
       3 | Трудно быть богом. Стругацкий А. Н.,
Стругацкий Б. Н.
       6 | Хрестоматия. Пушкин А. С., Толстой Л. Н. и др.
(6 rows)

Page 17: Практика+

  1. Требуется распределить расходы на электроэнергию по отделам компании пропорционально количеству сотрудников (перечень отделов находится в таблице). Напишите функцию, которая примет общую сумму расходов и запишет распределенные расходы в строки таблицы. Числа округляются до копеек; сумма расходов всех отделов должна в точности совпадать с общей суммой. 2. Напишите табличную функцию, имитирующую сортировку слиянием. Функция принимает две курсорные переменные; оба курсора уже открыты и возвращают упорядоченные по неубыванию целые числа. Требуется выдать общую упорядоченную последовательность чисел из обоих источников.

Комментарии преподавателя

  1. В качестве таблицы можно взять:
CREATE TABLE depts(
    id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    employees integer,
    expenses numeric(10,2)
);
INSERT INTO depts(employees) VALUES (10),(10),(10);

Функция:

FUNCTION distribute_expenses(amount numeric) RETURNS void;

Ожидаемый результат после вызова функции с параметром 100.0:

 expenses
----------
 33.33
 33.34
 33.33
  1. Функция:
FUNCTION merge(c1 refcursor, c2 refcursor) RETURNS SETOF integer;

Например, если первый курсор возвращает последовательность 1, 3, 5, а второй — 2, 3, 4, то ожидается результат:

 merge
-------
 1
 2
 3
 3
 4
 5

Page 18: 1. Распределение расходов

=> CREATE DATABASE plpgsql_cursors;
CREATE DATABASE
=> \c plpgsql_cursors
You are now connected to database "plpgsql_cursors" as user "student".

Таблица:

=> CREATE TABLE depts(
    id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    employees integer,
    expenses numeric(10,2)
);
CREATE TABLE
=> INSERT INTO depts(employees) VALUES (20),(10),(30);
INSERT 0 3

Функция:

=> CREATE FUNCTION distribute_expenses(amount numeric) RETURNS void
AS $$
DECLARE
    depts_cur CURSOR FOR
        SELECT employees FROM depts FOR UPDATE;
    total_employees numeric;
    expense numeric;
    rounding_err numeric := 0.0;
    cent numeric;
BEGIN
    SELECT sum(employees) FROM depts INTO total_employees;
    FOR dept IN depts_cur LOOP
        expense := amount * (dept.employees / total_employees);
        rounding_err := rounding_err + (expense - round(expense,2));
        cent := round(rounding_err,2);
        expense := expense + cent;
        rounding_err := rounding_err - cent;
        UPDATE depts SET expenses = round(expense,2)
        WHERE CURRENT OF depts_cur;
    END LOOP;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Проверка:

=> SELECT distribute_expenses(100.0);
 distribute_expenses
---------------------
(1 row)
=> SELECT * FROM depts;
 id | employees | expenses
----+-----------+----------
  1 |        20 |    33.33
  2 |        10 |    16.67
  3 |        30 |    50.00
(3 rows)

Разумеется, возможны и другие алгоритмы, например, перенос всех ошибок округления на одну строку и т. п. В курсе DEV2 рассматривается другое решение этой задачи с помощью пользовательских агрегатных функций.

2. Слияние отсортированных наборов

Page 19: Page 19

Эта реализация предполагает, что числа не могут иметь неопределенные значения NULL.

=> CREATE FUNCTION merge(c1 refcursor, c2 refcursor)
RETURNS SETOF integer
AS $$
DECLARE
    a integer;
    b integer;
BEGIN
    FETCH c1 INTO a;
    FETCH c2 INTO b;
    LOOP
        EXIT WHEN a IS NULL AND b IS NULL;
        IF a < b OR b IS NULL THEN
            RETURN NEXT a;
            FETCH c1 INTO a;
        ELSE
            RETURN NEXT b;
            FETCH c2 INTO b;
        END IF;
    END LOOP;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Проверяем.

=> BEGIN;
BEGIN
=> DECLARE c1 CURSOR FOR
    SELECT * FROM (VALUES (1),(3),(5));
DECLARE CURSOR
=> DECLARE c2 CURSOR FOR
    SELECT * FROM (VALUES (2),(3),(4));
DECLARE CURSOR
=> SELECT * FROM merge('c1','c2');
 merge
-------
     1
     2
     3
     3
     4
     5
(6 rows)
=> COMMIT;
COMMIT
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_cursors;
DROP DATABASE

14 Занятие 14: PL/pgSQL / Динамические команды

Исходная страница в нативном Markdown: PL/pgSQL / Динамические команды
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_14_plpgsql_dynamic.html Archive: DEV1-handouts-16.zip Страниц PDF: 16

Page 1: PL/pgSQL / Динамические команды

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Причины использования
  • Выполнение динамического запроса
  • Способы формирования динамического запроса

Page 3: Динамический SQL

Текст SQL-команды формируется в момент выполнения

  • Причины использования
    • дополнительная гибкость в приложении
    • формирование нескольких конкретных запросов
    • вместо одного универсального для оптимизации
  • Цена
    • операторы не подготавливаются
    • возрастает риск внедрения SQL-кода
    • возрастает сложность сопровождения

Комментарии преподавателя

Под динамическими командами понимаются команды SQL, текст которых формируется и затем выполняется внутри PL/pgSQL-блока в подпрограммах или в анонимных блоках.

В большинстве случаев можно обойтись без динамических команд, но иногда они могут предоставить дополнительную гибкость. Например, можно встроить в определенные места приложения возможность выполнять команды, считанные из настроек системы. Управлять такими настройками могут специалисты поддержки во время эксплуатации приложения, а не программисты в момент разработки.

Иногда, при формировании отчета с большим количеством необязательных параметров, бывает проще формировать текст запроса прямо во время выполнения только для указанных параметров, чем заранее, при разработке, писать сложный запрос, учитывающий все возможные комбинации параметров.

Платой за использование динамических команд будет отказ от подготовленных операторов, которые по умолчанию используются в PL/pgSQL. Также нужно следить за безопасностью динамических команд с точки зрения возможности внедрения SQL-кода.

Следует отметить и существенное возрастание сложности сопровождения. В частности, в исходном коде приложения невозможно будет текстовым поиском обнаружить все выполняемые команды.

Page 4: Выполнение запроса

Конструкция EXECUTE

  • выполняет строковое представление SQL-запроса
  • позволяет использовать параметры
  • переменные PL/pgSQL не становятся неявными параметрами
  • Может использоваться вместо SQL-запроса
    • сама по себе
    • при открытии курсора
    • в цикле по запросу
    • в предложении RETURN QUERY

Комментарии преподавателя

Для выполнения динамических команд в PL/pgSQL используется конструкция EXECUTE, которая выполняет SQL-оператор, заданный в виде текстовой строки.

Динамический запрос может содержать явно заданные параметры. В тексте команды параметры обозначаются как $1, $2 и т. д., а значения параметров указываются в предложении USING. Параметры работают точно так же, как в подготовленных операторах (что рассматривалось в теме «Архитектура. Общее устройство PostgreSQL»). Однако переменные PL/pgSQL не становятся неявными параметрами, как это происходит при обычном, не динамическом, использовании SQL в PL/pgSQL.

Конструкция EXECUTE может использоваться как самостоятельный оператор (в этом случае просто выполняется динамическая команда). Она также может использоваться в циклах по запросу, при открытии курсора, в операторе RETURN QUERY — во всех этих случаях EXECUTE заменяет собой команду SQL.

https://postgrespro.ru/docs/postgresql/16/plpgsql-statements#PLPGSQL-ST ATEMENTS-EXECUTING-DYN

Тонкий момент: процедура не может управлять транзакциями, если она вызывается оператором EXECUTE.

Page 5: Выполнение динамического запроса

Оператор EXECUTE позволяет выполнить SQL-команду, заданную в виде строки.

=> DO $$
DECLARE
    cmd CONSTANT text := 'CREATE TABLE city_msk(
        name text, architect text, founded integer
    )';
BEGIN
    EXECUTE cmd; -- таблица для исторических зданий Москвы
END
$$;
DO

Предложение INTO оператора EXECUTE позволяет вернуть одну (первую) строку результата в переменную составного типа или несколько скалярных переменных. Для проверки результата выполнения динамической команды можно использовать команду GET DIAGNOSTICS, как и в случае статических команд (но не переменную FOUND).

=> DO $$
DECLARE
    rec record;
    cnt bigint;
BEGIN
    EXECUTE 'INSERT INTO city_msk (name, architect, founded) VALUES
                 (''Пашков дом'', ''Василий Баженов'', 1784),
                 (''Ансамбль Царицыно'', ''Василий Баженов'', 1776),
                 (''Усадьба Тутолмина'', ''Василий Баженов'', 1788),
                 (''Музей Пушкина'', ''Роман Клейн'', 1898),
                 (''ЦУМ'', ''Роман Клейн'', 1908)
             RETURNING name, architect, founded'
    INTO rec;
    RAISE NOTICE '%', rec;
    GET DIAGNOSTICS cnt := ROW_COUNT;
    RAISE NOTICE 'Добавлено строк: %', cnt;
END
$$;
NOTICE:  ("Пашков дом","Василий Баженов",1784)
NOTICE:  Добавлено строк: 5
DO

При необходимости с помощью STRICT можно гарантировать, что команда обработает ровно одну строку. Результат динамического запроса можно обработать в цикле FOR.

=> DO $$
DECLARE
    rec record;
BEGIN
    FOR rec IN EXECUTE 'SELECT * FROM city_msk WHERE architect = ''Роман Клейн'' ORDER BY founded'
    LOOP
        RAISE NOTICE '%', rec;
    END LOOP;
END
$$;
NOTICE:  ("Музей Пушкина","Роман Клейн",1898)
NOTICE:  (ЦУМ,"Роман Клейн",1908)
DO

Этот же пример с использованием курсора.

Page 6: Page 6

=> DO $$
DECLARE
    cur refcursor;
    rec record;
BEGIN
    OPEN cur FOR EXECUTE 'SELECT * FROM city_msk WHERE architect = ''Роман Клейн'' ORDER BY founded';
    LOOP
        FETCH cur INTO rec;
        EXIT WHEN NOT FOUND;
        RAISE NOTICE '%', rec;
    END LOOP;
END
$$;
NOTICE:  ("Музей Пушкина","Роман Клейн",1898)
NOTICE:  (ЦУМ,"Роман Клейн",1908)
DO

Оператор RETURN QUERY для возврата строк из функции также может использовать динамические запросы. Напишем функцию, возвращающую все здания, возведенные архитектором до определенной даты. Для этого нам понадобятся параметры:

=> CREATE FUNCTION sel_msk(architect text, founded integer DEFAULT NULL)
RETURNS SETOF text
AS $$
DECLARE
    -- параметры пронумерованы: $1, $2...
    cmd text := '
        SELECT name FROM city_msk
        WHERE architect = $1 AND ($2 IS NULL OR founded < $2)';
BEGIN
    RETURN QUERY
        EXECUTE cmd
        USING architect, founded; -- указываем значения по порядку
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT * FROM sel_msk('Роман Клейн');
    sel_msk
---------------
 Музей Пушкина
 ЦУМ
(2 rows)
=> SELECT * FROM sel_msk('Роман Клейн', 1905 );  -- до событий на Красной Пресне
    sel_msk
---------------
 Музей Пушкина
(1 row)

Page 7: Формирование команды

Подстановка значений параметров

  • предложение USING
  • гарантируется невозможность внедрения SQL-кода
  • Экранирование значений
    • идентификаторы: format(’%I’), quote_ident
    • литералы: format(’%L’), quote_literal, quote_nullable
    • внедрение SQL-кода невозможно при правильном использовании
  • Обычные строковые функции
    • конкатенация и др.
    • возможно внедрение SQL-кода!

Комментарии преподавателя

Использование оператора EXECUTE имеет смысл, если команда формируется динамически. Приведенные ранее примеры можно было бы записать и без оператора EXECUTE.

Поскольку команда представляется текстовой строкой, ее можно сформировать обычными строковыми функциями, такими как конкатенация и т. п. В этом случае надо проявлять крайнюю осторожность, поскольку возможно внедрение SQL-кода.

Внедрение SQL-кода невозможно в принципе, если значения передаются как параметры с помощью предложения USING.

Однако параметры применимы не всегда: может понадобиться конкатенировать отдельные части запроса или подставлять в запрос имя таблицы. В этом случае для защиты от внедрений следует экранировать значения, полученные из ненадежного источника.

Для идентификаторов используется функция format со спецификатором %I или функция quote_ident. Эти функции формируют правильные имена идентификаторов, при необходимости заключая их в двойные кавычки и экранируя специальные символы.

Для подстановки литералов внутрь текста команды можно использовать функции quote_literal, quote_nullable или функцию format со спецификатором %L.

https://postgrespro.ru/docs/postgresql/16/functions-string

Page 8: Возможность внедрения SQL-кода

Перепишем функцию, возвращающую здания, добавив параметр — код города. По задумке такая функция должна позволять обращаться только к таблицам, начинающимся на city_.

=> CREATE FUNCTION sel_city(
    city_code text,
    architect text,
    founded integer DEFAULT NULL
)
RETURNS SETOF text AS $$
DECLARE
    cmd text := '
        SELECT name FROM city_' || city_code || '
        WHERE architect = $1 AND ($2 IS NULL OR founded < $2)';
BEGIN
    RAISE NOTICE '%', cmd;
    RETURN QUERY
        EXECUTE cmd
        USING architect, founded;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Функция правильно работает при «нормальных» значениях параметров:

=> SELECT * FROM sel_city('msk', 'Василий Баженов');
NOTICE:
        SELECT name FROM city_msk
        WHERE architect = $1 AND ($2 IS NULL OR founded < $2)
     sel_city
-------------------
 Пашков дом
 Ансамбль Царицыно
 Усадьба Тутолмина
(3 rows)

Однако злоумышленник может подобрать такое значение, которое изменит синтаксическую конструкцию запроса и позволит ему получить несанкционированный доступ к данным:

=> SELECT * FROM sel_city('msk WHERE false
        UNION ALL
        SELECT usename FROM pg_user
        UNION ALL
        SELECT name FROM city_msk', '');
NOTICE:
        SELECT name FROM city_msk WHERE false
        UNION ALL
        SELECT usename FROM pg_user
        UNION ALL
        SELECT name FROM city_msk
        WHERE architect = $1 AND ($2 IS NULL OR founded < $2)
 sel_city
----------
 student
 postgres
(2 rows)

При использовании подготовленных операторов или динамических команд с параметрами это невозможно в принципе, так как структура SQL-запроса фиксируется при синтаксическом разборе. Выражение всегда останется выражением и не сможет превратиться, например, в имя таблицы.

Формирование динамической команды

Параметры в предложении USING нельзя использовать для имен объектов (названия таблиц, столбцов и пр.) в динамической команде. Такие идентификаторы необходимо экранировать, чтобы структура запроса не могла измениться:

Page 9: Page 9

=> SELECT format('%I', 'foo'),
          format('%I', 'foo bar'),
          format('%I', 'foo"bar');
 format |  format   |   format
--------+-----------+------------
 foo    | "foo bar" | "foo""bar"
(1 row)

То же самое выполняет и другая функция:

=> SELECT quote_ident('foo'),
          quote_ident('foo bar'),
          quote_ident('foo"bar');
 quote_ident | quote_ident | quote_ident
-------------+-------------+-------------
 foo         | "foo bar"   | "foo""bar"
(1 row)

Вот как может выглядеть пример с созданием таблицы:

=> DO $$
DECLARE
    cmd CONSTANT text := 'CREATE TABLE %I(
        name text, architect text, founded integer
    )';
BEGIN
    EXECUTE format(cmd, 'city_spb'); -- таблица для Санкт-Петербурга
    EXECUTE format(cmd, 'city_nov'); -- таблица для Новгорода
END
$$;
DO

Вместо использования параметров, можно вставлять в строку литералы. В этом случае также требуется экранирование, но другое:

=> SELECT format('%L', 'foo bar'),
          format('%L', 'foo''bar'),
          format('%L', NULL);
  format   |   format   | format
-----------+------------+--------
 'foo bar' | 'foo''bar' | NULL
(1 row)

Это же выполняет и функция quote_nullable:

=> SELECT quote_nullable('foo bar'),
          quote_nullable('foo''bar'),
          quote_nullable(NULL);
 quote_nullable | quote_nullable | quote_nullable
----------------+----------------+----------------
 'foo bar'      | 'foo''bar'     | NULL
(1 row)

Похожая функция quote_literal отличается тем, что не превращает неопределенное значение в литерал:

=> SELECT quote_literal(NULL);
 quote_literal
---------------
(1 row)

В качестве примера перепишем функцию, возвращающую здания в определенном городе, без использования параметров, но так, чтобы она осталась безопасной.

Page 10: Page 10

=> CREATE OR REPLACE FUNCTION sel_city(
    city_code text,
    architect text,
    founded integer DEFAULT NULL
)
RETURNS SETOF text
AS $$
DECLARE
    cmd text := '
        SELECT name FROM %I
        WHERE architect = %L AND (%L IS NULL OR founded < %L::integer)';
BEGIN
    RETURN QUERY EXECUTE format(
        cmd, 'city_'||city_code, architect, founded, founded
    );
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Обратите внимание, что в этом случае получается два лишних приведения типов: сначала параметр типа integer приводится к строке, а затем, на этапе выполнения, строка обратно приводится к integer (в случае использования параметров USING такого не происходит):

=> SELECT * FROM sel_city('msk', 'Василий Баженов', 1785);  -- до приезда императрицы в Царицыно
     sel_city
-------------------
 Пашков дом
 Ансамбль Царицыно
(2 rows)

Попытка передачи некорректного значения не приведет к успеху:

=> SELECT * FROM sel_city('msk WHERE false
        UNION ALL
        SELECT usename FROM pg_user
        UNION ALL
        SELECT name FROM city_msk', '');
NOTICE:  identifier "city_msk WHERE false
        UNION ALL
        SELECT usename FROM pg_user
        UNION ALL
        SELECT name FROM city_msk" will be truncated to "city_msk WHERE false
        UNION ALL
        SELECT usename F"
ERROR:  relation "city_msk WHERE false
        UNION ALL
        SELECT usename F" does not exist
LINE 2:         SELECT name FROM "city_msk WHERE false
                                 ^
QUERY:
        SELECT name FROM "city_msk WHERE false
        UNION ALL
        SELECT usename FROM pg_user
        UNION ALL
        SELECT name FROM city_msk"
        WHERE architect = '' AND (NULL IS NULL OR founded < NULL::integer)
CONTEXT:  PL/pgSQL function sel_city(text,text,integer) line 7 at RETURN QUERY

Page 11: Итоги

Динамические команды дают дополнительную гибкость

  • Позволяют формировать отдельные запросы для разных значений параметров с целью оптимизации
  • Не подходят для коротких, частых запросов
  • Увеличивается сложность поддержки

Page 12: Практика

  1. Измените функцию get_catalog так, чтобы запрос к представлению catalog_v формировался динамически и содержал условия только на те поля, которые заполнены на форме поиска в «Магазине».
  • Убедитесь, что реализация не допускает возможности
  • внедрения SQL-кода.
  • Проверьте работу функции в приложении.

Комментарии преподавателя

  1. Скажем, если на форме поиска не заполнены поля «Название книги» и «Имя автора», но установлен флажок «Есть на складе», должен быть сформирован такой, например, запрос:
SELECT ... FROM catalog_v WHERE onhand_qty > 0;

Учтите, что поиск при такой реализации вовсе не обязательно будет работать эффективнее, но поддерживать его совершенно точно будет сложнее. Поэтому в реальной работе не стоит прибегать к такому приему, если для того нет веских оснований. Тема оптимизации запросов рассматривается в курсе QPT.

Page 13: 1. Функция get_catalog

=> CREATE OR REPLACE FUNCTION get_catalog(
    author_name text,
    book_title text,
    in_stock boolean
)
RETURNS TABLE(book_id integer, display_name text, onhand_qty integer)
AS $$
DECLARE
    title_cond text := '';
    author_cond text := '';
    qty_cond text := '';
    cmd text;
BEGIN
    IF book_title != '' THEN
        title_cond := format(
            ' AND cv.title ILIKE %L', '%'||book_title||'%'
        );
    END IF;
    IF author_name != '' THEN
        author_cond := format(
            ' AND cv.authors ILIKE %L', '%'||author_name||'%'
        );
    END IF;
    IF in_stock THEN
        qty_cond := ' AND cv.onhand_qty > 0';
    END IF;
    cmd := 'SELECT cv.book_id,
               cv.display_name,
               cv.onhand_qty
        FROM   catalog_v cv
        WHERE  true'
        || title_cond || author_cond || qty_cond || '
        ORDER BY display_name';
    RAISE NOTICE '%', cmd;
    RETURN QUERY EXECUTE cmd;
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

Page 14: Практика+

  1. Создайте функцию, которая возвращает строки матричного отчета по функциям в базе данных.
  • Столбцы должны содержать имена владельцев функций,
  • строки — названия схем, а ячейки — количество функций
  • данного владельца в данной схеме.
  • Как можно вызвать такую функцию?

Комментарии преподавателя

  1. Примерный вид результата:
       schema        total  postgres  student ...
 information_schema     12        12        0
 pg_catalog           2811      2811        0
 public                  3         0        3
 ...

Количество столбцов в запросе заранее не известно. Поэтому необходимо сконструировать запрос и затем динамически его выполнить. Текст запроса может быть таким:

SELECT pronamespace::regnamespace::text AS schema,
       COUNT(*) AS total
      ,SUM(CASE WHEN proowner = 10 THEN 1 ELSE 0 END) postgres
      ,SUM(CASE WHEN proowner = 16384 THEN 1 ELSE 0 END) student
FROM pg_proc
GROUP BY pronamespace::regnamespace
ORDER BY schema

Выделенные строки — динамическая часть — необходимо сформировать дополнительным запросом. Начало и конец запроса — статические.

Столбец proowner имеет тип oid, для получения имени владельца можно воспользоваться конструкцией proowner::regrole::text.

Page 15: Получение матричного отчета

=> CREATE DATABASE plpgsql_dynamic;
CREATE DATABASE
=> \c plpgsql_dynamic
You are now connected to database "plpgsql_dynamic" as user "student".

Вспомогательная функция для формирования текста динамического запроса:

=> CREATE FUNCTION form_query() RETURNS text
AS $$
DECLARE
    query_text text;
    columns text := '';
    r record;
BEGIN
    -- Статическая часть запроса.
    -- Первые два столбца: имя схемы и общее количество функций в ней
    query_text :=
$query$
SELECT pronamespace::regnamespace::text AS schema
     , count(*) AS total{{columns}}
FROM pg_proc
GROUP BY pronamespace::regnamespace
ORDER BY schema
$query$;
    -- Динамическая часть запроса.
    -- Получаем список владельцев функций, для каждого — отдельный столбец
    FOR r IN SELECT DISTINCT proowner AS owner FROM pg_proc ORDER BY 1
    LOOP
        columns := columns || format(
            E'\n     , sum(CASE WHEN proowner = %s THEN 1 ELSE 0 END) AS %I',
            r.owner,
            r.owner::regrole
        );
    END LOOP;
    RETURN replace(query_text, '{{columns}}', columns);
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

Итоговый текст запроса:

=> SELECT form_query();
                             form_query
---------------------------------------------------------------------
                                                                    +
 SELECT pronamespace::regnamespace::text AS schema                  +
      , count(*) AS total                                           +
      , sum(CASE WHEN proowner = 10 THEN 1 ELSE 0 END) AS postgres  +
      , sum(CASE WHEN proowner = 16384 THEN 1 ELSE 0 END) AS student+
 FROM pg_proc                                                       +
 GROUP BY pronamespace::regnamespace                                +
 ORDER BY schema                                                    +
(1 row)

Теперь создаем функцию для матричного отчета:

=> CREATE FUNCTION matrix() RETURNS SETOF record
AS $$
BEGIN
    RETURN QUERY EXECUTE form_query();
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

Простое выполнение запроса приведет к ошибке, так как не указана структура возвращаемых записей:

Page 16: Page 16

=> SELECT * FROM matrix();
ERROR:  a column definition list is required for functions returning "record"
LINE 1: SELECT * FROM matrix();
                      ^

В этом состоит важное ограничение на использование функций, возвращающих произвольную выборку. В момент вызова необходимо знать и указать структуру возвращаемой записи. В общем случае структура возвращаемой записи может быть неизвестна, но, применительно к нашему матричному отчету, можно выполнить еще один запрос, который покажет, как правильно вызвать функцию matrix. Подготовим текст запроса:

=> CREATE FUNCTION matrix_call() RETURNS text
AS $$
DECLARE
    cmd text;
    r record;
BEGIN
    cmd := 'SELECT * FROM matrix() AS (
    schema text, total bigint';
    FOR r IN SELECT DISTINCT proowner AS owner FROM pg_proc ORDER BY 1
    LOOP
        cmd := cmd || format(', %I bigint', r.owner::regrole::text);
    END LOOP;
    cmd := cmd || E'\n)';
    RAISE NOTICE '%', cmd;
    RETURN cmd;
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

Теперь мы можем вызовом martix_call получить запрос, отражающий структуру матричного отчета, а затем выполнить этот запрос и получить отчет (psql позволяет все это сделать одной командой \gexec):

=> BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN
=> SELECT matrix_call() \gexec
NOTICE:  SELECT * FROM matrix() AS (
    schema text, total bigint, postgres bigint, student bigint
)
       schema       | total | postgres | student
--------------------+-------+----------+---------
 information_schema |    11 |       11 |       0
 pg_catalog         |  3286 |     3286 |       0
 public             |     3 |        0 |       3
(3 rows)
=> COMMIT;
COMMIT

Матричный отчет корректно формируется. Уровень изоляции Repeatable Read гарантирует, что отчет сформируется, даже если между двумя запросами появится функция у нового владельца. Можно было бы и напрямую выполнить запрос, возвращаемый функцией form_query. Но задача получить в клиентском приложении список возвращаемых столбцов все равно останется. Функция matrix_call показывает, как ее можно решить дополнительным запросом. Еще один вариант решения заключается в том, чтобы вместо набора записей произвольной структуры возвращать набор строк слабоструктурированного типа (такого, как JSON или XML). Эти типы рассматриваются в курсе DEV2.

=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_dynamic;
DROP DATABASE

15 Занятие 15: PL/pgSQL / Массивы

Исходная страница в нативном Markdown: PL/pgSQL / Массивы
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_15_plpgsql_arrays.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: Темы

  • Массивы и работа с ними в PL/pgSQL
  • Циклы по элементам массивов
  • Функции с переменным числом параметров и полиморфные функции
  • Использование массивов в столбцах таблиц

Page 3: Тип массива

Массив

  • набор пронумерованных элементов одного и того же типа
  • одномерные, многомерные
  • Создание
использование без явного определения (имя-типа[])
неявно при создании типа или таблицы (_имя-типа)
  • Использование
    • элементы как скалярные значения
    • срезы массива
    • операции с массивами: сравнение, вхождение, пересечение,
    • конкатенация, использование с ANY и ALL вместо подзапроса, …

Комментарии преподавателя

Массив, как и составной тип (запись), не является скалярным и состоит из нескольких элементов другого типа. Но, в отличие от записей, а) все эти элементы имеют одинаковый тип и б) обращение к ним происходит не по имени, а по целочисленному индексу (здесь индекс понимается в математическом смысле, а не как индекс БД).

Тип массива не надо специально объявлять, поскольку при создании любого типа (в том числе таблицы) создается также тип массива элементов этого типа с именем _тип. При объявлении переменной-массива достаточно добавить квадратные скобки к имени типа элементов.

Массив является полноценным типом SQL, его можно использовать как любой другой тип: создавать столбцы таблиц этого типа, использовать его для параметров функций и т. п. Элементы массива могут использоваться как обычные скалярные значения. Можно использовать и срезы (slice) массивов.

Массивы можно сравнивать, проверять на неопределенность, искать вхождение элемента и находить пересечение с другими массивами, конкатенировать и пр. Также массивы можно использовать по аналогии с подзапросами в конструкциях ANY/SOME и ALL.

https://postgrespro.ru/docs/postgresql/16/arrays

Различные функции для работы с массивами можно найти в справочном материале, прилагаемом к курсу.

Page 4: Инициализация и обращение к элементам

Объявление переменной и инициализация массива целиком:

=> DO $$
DECLARE
    a integer[2]; -- размер игнорируется
BEGIN
    a := ARRAY[10,20,30];
    RAISE NOTICE '%', a;
    -- по умолчанию элементы нумеруются с единицы
    RAISE NOTICE 'a[1] = %, a[2] = %, a[3] = %', a[1], a[2], a[3];
    -- срез массива
    RAISE NOTICE 'Срез [2:3] = %', a[2:3];
    -- присваиваем значения срезу массива
    a[2:3] := ARRAY[222,333];
    -- выводим весь массив
    RAISE NOTICE '%', a;
END;
$$ LANGUAGE plpgsql;
NOTICE:  {10,20,30}
NOTICE:  a[1] = 10, a[2] = 20, a[3] = 30
NOTICE:  Срез [2:3] = {20,30}
NOTICE:  {10,222,333}
DO

Одномерный массив можно заполнять и поэлементно — при необходимости он автоматически расширяется. Если пропустить какие-то элементы, они получают неопределенные значения. Что будет выведено?

=> DO $$
DECLARE
    a integer[];
BEGIN
    a[2] := 10;
    a[3] := 20;
    a[6] := 30;
    RAISE NOTICE '%', a;
END;
$$ LANGUAGE plpgsql;
NOTICE:  [2:6]={10,20,NULL,NULL,30}
DO

Поскольку нумерация началась не с единицы, перед самим массивом дополнительно выводится диапазон номеров элементов. Мы можем определить составной тип и создать массив из элементов этого типа:

=> CREATE TYPE currency AS (amount numeric, code text);
CREATE TYPE
=> DO $$
DECLARE
    c currency[];  -- массив из элементов составного типа
BEGIN
  -- присваиваем значения отдельным элементам
    c[1].amount := 10;  c[1].code := 'RUB';
    c[2].amount := 50;  c[2].code := 'KZT';
    RAISE NOTICE '%', c;
END
$$ LANGUAGE plpgsql;
NOTICE:  {"(10,RUB)","(50,KZT)"}
DO

Еще один способ получить массив — создать его из подзапроса:

Page 5: Page 5

=> DO $$
DECLARE
    a integer[];
BEGIN
    a := ARRAY( SELECT n FROM generate_series(1,3) n );
    RAISE NOTICE '%', a;
END
$$ LANGUAGE plpgsql;
NOTICE:  {1,2,3}
DO

Можно и наоборот, массив преобразовать в таблицу:

=> SELECT unnest( ARRAY[1,2,3] );
 unnest
--------
      1
      2
      3
(3 rows)

Интересно, что выражение IN со списком значений преобразуется в поиск по массиву:

=> EXPLAIN (costs off)
SELECT * FROM generate_series(1,10) g(id) WHERE id IN (1,2,3);
                 QUERY PLAN
---------------------------------------------
 Function Scan on generate_series g
   Filter: (id = ANY ('{1,2,3}'::integer[]))
(2 rows)

Двумерный массив — прямоугольная матрица, память под которую выделяется при инициализации. Литерал выглядит как массив массивов, имеющих одинаковое число элементов. Здесь мы использовали другой способ инициализации — с помощью символьной строки. После инициализации многомерный массив уже нельзя расширить.

=> DO $$
DECLARE
    a integer[][] := '{
        { 10, 20, 30},
        {100,200,300}
    }';
BEGIN
    RAISE NOTICE 'Двумерный массив : %', a;
    RAISE NOTICE 'Неограниченный срез массива [2:] = %', a[2:];
    -- присваиваем значения этому срезу
    a[2:] := ARRAY[ARRAY[111, 222, 333]];
    -- снова выводим весь массив
    RAISE NOTICE '%', a;
    -- расширять нельзя ни по какому измерению
    a[4][4] := 1;
END
$$ LANGUAGE plpgsql;
NOTICE:  Двумерный массив : {{10,20,30},{100,200,300}}
NOTICE:  Неограниченный срез массива [2:] = {{100,200,300}}
NOTICE:  {{10,20,30},{111,222,333}}
ERROR:  array subscript out of range
CONTEXT:  PL/pgSQL function inline_code_block line 15 at assignment

Page 6: Массивы и циклы

Обычный цикл по индексам элементов

  • array_lower
  • array_upper
  • Цикл FOREACH по элементам массива
    • проще, но индексы элементов недоступны

Комментарии преподавателя

Для итерации по элементам массива вполне можно использовать обычный целочисленный цикл FOR, используя функции, возвращающие минимальный и максимальный индексы массива.

Однако есть и специализированный вариант цикла: FOREACH. В таком варианте переменная цикла пробегает не индексы элементов, а сами элементы. Поэтому переменная должна иметь тот же тип, что и элементы массива (как обычно, если элементами являются записи, то одну переменную составного типа можно заменить несколькими скалярными переменными).

Тот же цикл с фразой SLICE позволяет итерировать срезы массива. Например, для двумерного массива одномерными срезами будут его строки.

https://postgrespro.ru/docs/postgresql/16/plpgsql-control-structures#PLPGS QL-FOREACH-ARRAY

Page 7: Массивы и циклы

Цикл можно организовать, итерируя индексы элементов массива. Второй параметр функций array_lower и array_upper — номер размерности (единица для одномерных массивов).

=> DO $$
DECLARE
    a integer[] := ARRAY[10,20,30];
BEGIN
    FOR i IN array_lower(a,1)..array_upper(a,1) LOOP
        RAISE NOTICE 'a[%] = %', i, a[i];
    END LOOP;
END
$$ LANGUAGE plpgsql;
NOTICE:  a[1] = 10
NOTICE:  a[2] = 20
NOTICE:  a[3] = 30
DO

Если индексы не нужны, то проще итерировать сами элементы:

=> DO $$
DECLARE
    a integer[] := ARRAY[10,20,30];
    x integer;
BEGIN
    FOREACH x IN ARRAY a LOOP
        RAISE NOTICE '%', x;
    END LOOP;
END
$$ LANGUAGE plpgsql;
NOTICE:  10
NOTICE:  20
NOTICE:  30
DO

Итерация индексов в двумерном массиве:

=> DO $$
DECLARE
    -- можно и без двойных квадратных скобок
    a integer[] := ARRAY[
        ARRAY[ 10, 20, 30],
        ARRAY[100,200,300]
    ];
BEGIN
    FOR i IN array_lower(a,1)..array_upper(a,1) LOOP -- по строкам
        FOR j IN array_lower(a,2)..array_upper(a,2) LOOP -- по столбцам
            RAISE NOTICE 'a[%][%] = %', i, j, a[i][j];
        END LOOP;
    END LOOP;
END
$$ LANGUAGE plpgsql;
NOTICE:  a[1][1] = 10
NOTICE:  a[1][2] = 20
NOTICE:  a[1][3] = 30
NOTICE:  a[2][1] = 100
NOTICE:  a[2][2] = 200
NOTICE:  a[2][3] = 300
DO

Итерация элементов двумерного массива не требует вложенного цикла:

Page 8: Page 8

=> DO $$
DECLARE
    a integer[] := ARRAY[
        ARRAY[ 10, 20, 30],
        ARRAY[100,200,300]
    ];
    x integer;
BEGIN
    FOREACH x IN ARRAY a LOOP
        RAISE NOTICE '%', x;
    END LOOP;
END
$$ LANGUAGE plpgsql;
NOTICE:  10
NOTICE:  20
NOTICE:  30
NOTICE:  100
NOTICE:  200
NOTICE:  300
DO

Существует также возможность выполнять в подобном цикле итерацию по срезам, а не по отдельным элементам. Значение SLICE в такой конструкции должно быть целым числом, не превышающим размерность массива, а переменная, куда читаются срезы, сама должна быть массивом. В примере — цикл по одномерным срезам:

=> DO $$
DECLARE
    a integer[] := ARRAY[
        ARRAY[ 10, 20, 30],
        ARRAY[100,200,300]
    ];
    x integer[];
BEGIN
    FOREACH x SLICE 1 IN ARRAY a LOOP
        RAISE NOTICE '%', x;
    END LOOP;
END
$$ LANGUAGE plpgsql;
NOTICE:  {10,20,30}
NOTICE:  {100,200,300}
DO

Page 9: Массивы и подпрограммы

Подпрограммы с переменным числом параметров

  • все необязательные параметры должны иметь одинаковый тип
  • передаются в подпрограмму в виде массива
  • последний параметр-массив определяется с режимом VARIADIC
  • Полиморфные подпрограммы
    • работают со значениями разных типов;
    • тип конкретизируется во время выполнения
    • дополнительные полиморфные псевдотипы anyarray, anynonarray,
    • anycompatiblearray и anycompatiblenonarrray
    • могут иметь переменное число параметров

Комментарии преподавателя

Массивы позволяют создавать подпрограммы (функции или процедуры) с переменным числом параметров.

В отличие от параметров со значениями по умолчанию, которые при объявлении подпрограммы надо явно перечислить, необязательных параметров может быть сколько угодно и все они передаются подпрограмме в виде массива. Но, как следствие, все они должны иметь один и тот же (или совместимый, в случае использования anycompatible/anycompatiblearray) тип.

При объявлении подпрограммы последним указывается один параметр с режимом VARIADIC, имеющий тип массива.

https://postgrespro.ru/docs/postgresql/16/xfunc-sql#XFUNC-SQL-VARIADIC -FUNCTIONS

Мы уже говорили про полиморфные подпрограммы, которые могут работать с параметрами разных типов. При объявлении подпрограммы указывается специальный полиморфный псевдотип, а конкретный тип уточняется во время выполнения по фактическому типу переданных параметров.

Для массивов есть отдельные полиморфные типы anyarray, anycompatiblearray (и anynonarray, anycompatiblenonarrray для не-массивов).

Эти типы можно использовать совместно с передачей переменного числа аргументов при объявлении VARIADIC-параметра.

https://postgrespro.ru/docs/postgresql/16/xfunc-sql#XFUNC-SQL-POLYMO RPHIC-FUNCTIONS

Page 10: Массивы и подпрограммы

В теме «SQL. Процедуры» мы рассматривали перегрузку и полиморфизм и создали функцию maximum, которая находила максимальное из трех чисел. Обобщим ее на произвольное число аргументов. Для этого объявим один VARIADIC-параметр:

=> CREATE FUNCTION maximum(VARIADIC a integer[]) RETURNS integer
AS $$
DECLARE
    x integer;
    maxsofar integer;
BEGIN
    FOREACH x IN ARRAY a LOOP
        IF x IS NOT NULL AND (maxsofar IS NULL OR x > maxsofar) THEN
            maxsofar := x;
        END IF;
    END LOOP;
    RETURN maxsofar;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Пробуем:

=> SELECT maximum(12, 65, 47);
 maximum
---------
      65
(1 row)
=> SELECT maximum(12, 65, 47, null, 87, 24);
 maximum
---------
      87
(1 row)
=> SELECT maximum(null, null);
 maximum
---------
(1 row)

Для полноты картины и эта функция может быть сделана полиморфной, чтобы принимать любой тип данных (для которого, конечно, должны быть определены операции сравнения).

=> DROP FUNCTION maximum(integer[]);
DROP FUNCTION

Полиморфные типы anycompatiblearray и anycompatible (а также anyarray и anyelement) всегда согласованы между собой: anycompatiblearray = anycompatible[], anyarray = anyelement[]; Нам нужна переменная, имеющая тип элемента массива. Но объявить ее как anycompatible нельзя — она должна иметь реальный тип. Здесь помогает конструкция %TYPE.

=> CREATE FUNCTION maximum(VARIADIC a anycompatiblearray, maxsofar OUT anycompatible)
AS $$
DECLARE
    x maxsofar%TYPE;
BEGIN
    FOREACH x IN ARRAY a LOOP
        IF x IS NOT NULL AND (maxsofar IS NULL OR x > maxsofar) THEN
            maxsofar := x;
        END IF;
    END LOOP;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим:

Page 11: Page 11

=> SELECT maximum(12, 65, 47);
 maximum
---------
      65
(1 row)
=> SELECT maximum(12.1, 65.3, 47.6);
 maximum
---------
    65.3
(1 row)
=> SELECT maximum(12, 65.3, 15e2, 3.14);
 maximum
---------
    1500
(1 row)

Вот теперь у нас получился практически полный аналог выражения greatest!

Page 12: Массив или таблица?

Source slide page 12: Массив или таблица?{#source-figure-012 caption=“Source slide page 12: Массив или таблица?”}

1
...
{A}
2
...
{B,C,D}
3
...
{A,C}
1
1
1
A
...
2
2
1
...
2
B
...
2
3
2
...
2
4
3
C
...
3
...
3
1
4
D
...
3
3

Комментарии преподавателя

Классический реляционный подход предполагает, что в таблице хранятся атомарные значения (первая нормальная форма). Язык SQL не имеет средств для «заглядывания внутрь» сложносоставных значений.

Поэтому обычный подход состоит в создании отдельной таблицы, связанной с основной отношением «многие ко многим».

Тем не менее, мы можем создать таблицу со столбцом типа массива. PostgreSQL имеет богатый набор функций для работы с массивами, а поиск элемента в массиве может быть ускорен специальными индексами (такие индексы рассматриваются в курсе DEV2).

Такой подход бывает удобен: получается компактное представление, не требующее соединений. В частности, массивы активно используются в системном каталоге PostgreSQL.

Какое решение выбрать? Зависит от того, какие ставятся задачи, какие требуются операции. Рассмотрим пример.

Page 13: Массив или таблица?

Представим себе, что мы проектируем базу данных для ведения блога. В блоге есть сообщения, и нам хотелось бы сопоставлять им теги. Традиционный подход состоит в том, что для тегов надо создать отдельную таблицу, например, так:

=> CREATE TABLE posts(
    post_id integer PRIMARY KEY,
    message text
);
CREATE TABLE
=> CREATE TABLE tags(
    tag_id integer PRIMARY KEY,
    name text
);
CREATE TABLE

Связываем сообщения и теги отношением многие ко многим через еще одну таблицу:

=> CREATE TABLE posts_tags(
    post_id integer REFERENCES posts(post_id),
    tag_id integer REFERENCES tags(tag_id)
);
CREATE TABLE

Наполним таблицы тестовыми данными:

=> INSERT INTO posts(post_id,message) VALUES
    (1, 'Перечитывал пейджер, много думал.'),
    (2, 'Это было уже весной и я отнес елку обратно.');
INSERT 0 2
=> INSERT INTO tags(tag_id,name) VALUES
    (1, 'былое и думы'), (2, 'технологии'), (3, 'семья');
INSERT 0 3
=> INSERT INTO posts_tags(post_id,tag_id) VALUES
    (1,1), (1,2), (2,1), (2,3);
INSERT 0 4

Теперь мы можем вывести сообщения и теги:

=> SELECT p.message, t.name
FROM posts p
     JOIN posts_tags pt ON pt.post_id = p.post_id
     JOIN tags t ON t.tag_id = pt.tag_id
ORDER BY p.post_id, t.name;
                   message                   |     name
---------------------------------------------+--------------
 Перечитывал пейджер, много думал.           | былое и
думы
 Перечитывал пейджер, много думал.           |
технологии
 Это было уже весной и я отнес елку обратно. |
былое и думы
 Это было уже весной и я отнес елку обратно. |
семья
(4 rows)

Или чуть иначе — возможно удобнее получить массив тегов. Для этого используем агрегирующую функцию:

=> SELECT p.message, array_agg(t.name ORDER BY t.name) tags
FROM posts p
     JOIN posts_tags pt ON pt.post_id = p.post_id
     JOIN tags t ON t.tag_id = pt.tag_id
GROUP BY p.post_id
ORDER BY p.post_id;

Page 14: Page 14

                   message                   |            tags
---------------------------------------------+-----------------------------
 Перечитывал пейджер, много думал.           | {"былое
и думы",технологии}
 Это было уже весной и я отнес елку обратно. |
{"былое и думы",семья}
(2 rows)

Можем найти все сообщения с определенным тегом:

=> SELECT p.message
FROM posts p
     JOIN posts_tags pt ON pt.post_id = p.post_id
     JOIN tags t ON t.tag_id = pt.tag_id
WHERE t.name = 'былое и думы'
ORDER BY p.post_id;
                   message
---------------------------------------------
 Перечитывал пейджер, много думал.
 Это было уже весной и я отнес елку обратно.
(2 rows)

Может потребоваться найти все уникальные теги — это совсем просто:

=> SELECT t.name
FROM tags t
ORDER BY t.name;
     name
--------------
 былое и думы
 семья
 технологии
(3 rows)

Теперь попробуем подойти к задаче по-другому. Пусть теги будут представлены текстовым массивом прямо внутри таблицы сообщений.

=> DROP TABLE posts_tags;
DROP TABLE
=> DROP TABLE tags;
DROP TABLE
=> ALTER TABLE posts ADD COLUMN tags text[];
ALTER TABLE

Теперь у нас нет идентификаторов тегов, но они нам не очень и нужны.

=> UPDATE posts SET tags = '{"былое и думы","технологии"}'
WHERE post_id = 1;
UPDATE 1
=> UPDATE posts SET tags = '{"былое и думы","семья"}'
WHERE post_id = 2;
UPDATE 1

Вывод всех сообщений упростился:

=> SELECT p.message, p.tags
FROM posts p
ORDER BY p.post_id;
                   message                   |            tags
---------------------------------------------+-----------------------------
 Перечитывал пейджер, много думал.           | {"былое
и думы",технологии}
 Это было уже весной и я отнес елку обратно. |
{"былое и думы",семья}
(2 rows)

Сообщения с определенным тегом тоже легко найти (используем оператор пересечения &&). Эта операция может быть ускорена с помощью индекса GIN, и для такого запроса не придется перебирать всю таблицу

Page 15: Page 15

сообщений.

=> SELECT p.message
FROM posts p
WHERE p.tags && '{"былое и думы"}'
ORDER BY p.post_id;
                   message
---------------------------------------------
 Перечитывал пейджер, много думал.
 Это было уже весной и я отнес елку обратно.
(2 rows)

А вот получить список тегов довольно сложно. Это требует разворачивания всех массивов тегов в большую таблицу и поиск уникальных значений — тяжелая операция.

=> SELECT DISTINCT unnest(p.tags) AS name
FROM posts p;
     name
--------------
 технологии
 былое и думы
 семья
(3 rows)

Тут хорошо видно, что имеет место дублирование данных. Итак, оба подхода вполне могут применяться. В простых случаях массивы выглядят проще и работают хорошо. В более сложных сценариях (представьте, что вместе с именем тега мы хотим хранить дату его создания; или требуется проверка ограничений целостности) классический вариант становится более привлекательным.

Page 16: Итоги

Массив состоит из пронумерованных элементов одного и того же типа данных

  • Столбец с массивом как альтернатива отдельной таблице: удобные операции и индексная поддержка
  • Позволяет создавать функции с переменным числом параметров

Page 17: Практика

  1. Создайте функцию add_book для добавления новой книги.
  • Функция должна принимать два параметра —
  • название книги и массив идентификаторов авторов —
  • и возвращать идентификатор новой книги.
  • Проверьте, что в приложении появилась возможность
  • добавлять книги.
FUNCTION add_book(title text, authors integer[])
RETURNS integer

Page 18: 1. Функция add_book

=> CREATE FUNCTION add_book(title text, authors integer[])
RETURNS integer
AS $$
DECLARE
    book_id integer;
    id integer;
    seq_num integer := 1;
BEGIN
    INSERT INTO books(title)
        VALUES(title)
        RETURNING books.book_id INTO book_id;
    FOREACH id IN ARRAY authors LOOP
        INSERT INTO authorship(book_id, author_id, seq_num)
            VALUES (book_id, id, seq_num);
        seq_num := seq_num + 1;
    END LOOP;
    RETURN book_id;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Page 19: Практика+

  1. Реализуйте функцию map, принимающую два параметра: массив вещественных чисел и название вспомогательной функции, принимающей один параметр вещественного типа.
  • Функция должна возвращать исходный массив, в котором
  • к каждому элементу применена вспомогательная функция.
    1. Реализуйте функцию reduce, принимающую два параметра: массив вещественных чисел и название вспомогательной функции, принимающей два параметра вещественного типа.
    • Функция должна возвращать вещественное число,
    • полученное последовательной сверткой массива слева
    • направо.
    1. Сделайте функции map и reduce полиморфными.
  1. Например:
map(ARRAY[4.0,9.0],'sqrt') → ARRAY[2.0,3.0]
  1. Например:
reduce(ARRAY[1.0,3.0,2.0,0.5],'greatest') → 3.0

В этом случае значение вычисляется как

greatest( greatest( greatest(1.0,3.0), 2.0 ), 0.5 )

Page 20: 1. Функция map

=> CREATE DATABASE plpgsql_arrays;
CREATE DATABASE
=> \c plpgsql_arrays
You are now connected to database "plpgsql_arrays" as user "student".
=> CREATE FUNCTION map(a INOUT float[], func text)
AS $$
DECLARE
    i integer;
    x float;
BEGIN
    IF cardinality(a) > 0 THEN
        FOR i IN array_lower(a,1)..array_upper(a,1) LOOP
            EXECUTE format('SELECT %I($1)',func) USING a[i] INTO x;
            a[i] := x;
        END LOOP;
    END IF;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

INTO a[i] не работает, поэтому нужна отдельная переменная.

=> SELECT map(ARRAY[4.0,9.0,16.0],'sqrt');
   map
---------
 {2,3,4}
(1 row)
=> SELECT map(ARRAY[]::float[],'sqrt');
 map
-----
 {}
(1 row)

Другой вариант реализации с циклом FOREACH:

=> CREATE OR REPLACE FUNCTION map(a float[], func text) RETURNS float[]
AS $$
DECLARE
    x float;
    b float[]; -- пустой массив
BEGIN
    FOREACH x IN ARRAY a LOOP
        EXECUTE format('SELECT %I($1)',func) USING x INTO x;
        b := b || x;
    END LOOP;
    RETURN b;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT map(ARRAY[4.0,9.0,16.0],'sqrt');
   map
---------
 {2,3,4}
(1 row)
=> SELECT map(ARRAY[]::float[],'sqrt');
 map
-----
(1 row)

Page 21: 2. Функция reduce

=> CREATE FUNCTION reduce(a float[], func text) RETURNS float
AS $$
DECLARE
    i integer;
    r float := NULL;
BEGIN
    IF cardinality(a) > 0 THEN
        r := a[array_lower(a,1)];
        FOR i IN array_lower(a,1)+1 .. array_upper(a,1) LOOP
            EXECUTE format('SELECT %I($1,$2)',func) USING r, a[i]
                INTO r;
        END LOOP;
    END IF;
    RETURN r;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Greatest (как и least) — не функция, а встроенное условное выражение, поэтому из-за экранирования не получится использовать ее напрямую:

=> SELECT reduce( ARRAY[1.0,3.0,2.0], 'greatest');
ERROR:  function greatest(double precision, double precision) does not exist
LINE 1: SELECT "greatest"($1,$2)
               ^
HINT:  No function matches the given name and argument types. You might need to add
explicit type casts.
QUERY:  SELECT "greatest"($1,$2)
CONTEXT:  PL/pgSQL function reduce(double precision[],text) line 9 at EXECUTE

Вместо нее используем реализованную в демонстрации функцию maximum.

=> CREATE FUNCTION maximum(VARIADIC a anycompatiblearray, maxsofar OUT anycompatible)
AS $$
DECLARE
    x maxsofar%TYPE;
BEGIN
    FOREACH x IN ARRAY a LOOP
        IF x IS NOT NULL AND (maxsofar IS NULL OR x > maxsofar) THEN
            maxsofar := x;
        END IF;
    END LOOP;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT reduce(ARRAY[1.0,3.0,2.0], 'maximum');
 reduce
--------
      3
(1 row)
=> SELECT reduce(ARRAY[1.0], 'maximum');
 reduce
--------
      1
(1 row)
=> SELECT reduce(ARRAY[]::float[], 'maximum');
 reduce
--------
(1 row)

Вариант с циклом FOREACH:

Page 22: Page 22

=> CREATE OR REPLACE FUNCTION reduce(a float[], func text) RETURNS float
AS $$
DECLARE
    x float;
    r float;
    first boolean := true;
BEGIN
    FOREACH x IN ARRAY a LOOP
        IF first THEN
            r := x;
            first := false;
        ELSE
            EXECUTE format('SELECT %I($1,$2)',func) USING r, x INTO r;
        END IF;
    END LOOP;
    RETURN r;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT reduce(ARRAY[1.0,3.0,2.0], 'maximum');
 reduce
--------
      3
(1 row)
=> SELECT reduce(ARRAY[1.0], 'maximum');
 reduce
--------
      1
(1 row)
=> SELECT reduce(ARRAY[]::float[], 'maximum');
 reduce
--------
(1 row)

3. Полиморфные варианты функций

Функция map.

=> DROP FUNCTION map(float[],text);
DROP FUNCTION
=> CREATE FUNCTION map(
    a anyarray,
    func text,
    elem anyelement DEFAULT NULL
)
RETURNS anyarray
AS $$
DECLARE
    x elem%TYPE;
    b a%TYPE;
BEGIN
    FOREACH x IN ARRAY a LOOP
        EXECUTE format('SELECT %I($1)',func) USING x INTO x;
        b := b || x;
    END LOOP;
    RETURN b;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Требуется фиктивный параметр типа anyelement, чтобы внутри функции объявить переменную такого же типа.

=> SELECT map(ARRAY[4.0,9.0,16.0],'sqrt');

Page 23: Page 23

                           map
---------------------------------------------------------
 {2.000000000000000,3.000000000000000,4.000000000000000}
(1 row)
=> SELECT map(ARRAY[]::float[],'sqrt');
 map
-----
(1 row)

Пример вызова с другим типом данных:

=> SELECT map(ARRAY[' a ','  b','c  '],'btrim');
   map
---------
 {a,b,c}
(1 row)

Функция reduce.

=> DROP FUNCTION reduce(float[],text);
DROP FUNCTION
=> CREATE FUNCTION reduce(
    a anyarray,
    func text,
    elem anyelement DEFAULT NULL
)
RETURNS anyelement
AS $$
DECLARE
    x elem%TYPE;
    r elem%TYPE;
    first boolean := true;
BEGIN
    FOREACH x IN ARRAY a LOOP
        IF first THEN
            r := x;
            first := false;
        ELSE
            EXECUTE format('SELECT %I($1,$2)',func) USING r, x INTO r;
        END IF;
    END LOOP;
    RETURN r;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE FUNCTION add(x anyelement, y anyelement) RETURNS anyelement
AS $$
BEGIN
    RETURN x + y;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT reduce(ARRAY[1,-2,4], 'add');
 reduce
--------
      3
(1 row)
=> SELECT reduce(ARRAY['a','b','c'], 'concat');
 reduce
--------
 abc
(1 row)
=> \c postgres

Page 24: Page 24

You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_arrays;
DROP DATABASE

16 Занятие 16: PL/pgSQL / Обработка ошибок

Исходная страница в нативном Markdown: PL/pgSQL / Обработка ошибок
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_16_plpgsql_exceptions.html Archive: DEV1-handouts-16.zip Страниц PDF: 27

Page 1: PL/pgSQL / Обработка ошибок

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Обработка ошибок в блоке PL/pgSQL
  • Имена и коды ошибок
  • Как происходит поиск обработчика
  • Накладные расходы на обработку ошибок

Page 3: Обработка ошибок в блоке

Обработка выполняется, если есть секция EXCEPTION

  • Выполняется откат к точке сохранения в начале блока
    • точка сохранения устанавливается неявно, если в блоке есть
    • секция EXCEPTION
  • Если имеется обработчик, соответствующий ошибке
    • выполняются команды обработчика
    • блок завершается успешно
  • Если нет подходящего обработчика
    • блок завершается ошибкой

Комментарии преподавателя

Если внутри блока кода происходит ошибка времени выполнения, то обычно программа (блок, функция или процедура) аварийно прерывается, а текущая транзакция переходит в состояние сбоя: ее нельзя зафиксировать и можно только откатить.

Но ошибку можно перехватить и обработать. Для этого в блоке можно указать дополнительную секцию EXCEPTION, внутри которой перечислить условия, соответствующие ошибке, и операторы для обработки каждой такой ситуации.

EXCEPTION в целом соответствует конструкции try-catch в некоторых языках программирования (за исключением особенностей, связанных с обработкой транзакций, конечно).

В начале каждого блока с секцией EXCEPTION неявно устанавливается точка сохранения. Перед обработкой ошибки происходит откат к этой точке, в результате отменяются сделанные в блоке изменения, а также снимаются установленные в блоке блокировки.

Из-за наличия точки сохранения в блоках процедур с EXCEPTION запрещено использовать команды COMMIT и ROLLBACK. Зато, хотя команды SAVEPOINT и ROLLBACK TO SAVEPOINT не работают в PL/pgSQL, точку сохранения и откат к ней все-таки можно использовать и в процедурах, и в функциях — неявно.

https://postgrespro.ru/docs/postgresql/16/plpgsql-control-structures#PLPGS QL-ERROR-TRAPPING

Page 4: Обработка ошибок в блоке

Рассмотрим простой пример.

=> CREATE TABLE t(id integer);
CREATE TABLE
=> INSERT INTO t(id) VALUES (1);
INSERT 0 1

Когда нет ошибок, все операторы блока выполняются обычным образом:

=> DO $$
DECLARE
    n integer;
BEGIN
    SELECT id INTO STRICT n FROM t;
    RAISE NOTICE 'Оператор SELECT INTO выполнился, n = %', n;
END
$$;
NOTICE:  Оператор SELECT INTO выполнился, n = 1
DO

Теперь добавим в таблицу «лишнюю» строку, чтобы спровоцировать ошибку.

=> INSERT INTO t(id) VALUES (2);
INSERT 0 1

Если в блоке нет секции EXCEPTION, выполнение операторов в блоке прерывается и весь блок считается завершившимся с ошибкой:

=> DO $$
DECLARE
    n integer;
BEGIN
    SELECT id INTO STRICT n FROM t;
    RAISE NOTICE 'Оператор SELECT INTO выполнился, n = %', n;
END
$$;
ERROR:  query returned more than one row
HINT:  Make sure the query returns a single row, or use LIMIT 1.
CONTEXT:  PL/pgSQL function inline_code_block line 5 at SQL statement

Чтобы перехватить ошибку, в блоке нужна секция EXCEPTION, определяющая обработчик или несколько обработчиков. Эта конструкция работает аналогично CASE: условия просматриваются сверху вниз, выбирается первая подходящая ветвь и выполняются ее операторы. Что будет выведено?

=> DO $$
DECLARE
    n integer;
BEGIN
    n := 3;
    INSERT INTO t(id) VALUES (n);
    SELECT id INTO STRICT n FROM t;
    RAISE NOTICE 'Оператор SELECT INTO выполнился, n = %', n;
EXCEPTION
    WHEN no_data_found THEN
        RAISE NOTICE 'Нет данных';
    WHEN too_many_rows THEN
        RAISE NOTICE 'Слишком много данных';
        RAISE NOTICE 'Строк в таблице: %, n = %', (SELECT count(*) FROM t), n;
END
$$;
NOTICE:  Слишком много данных
NOTICE:  Строк в таблице: 2, n = 3
DO

Выполняется обработчик, соответствующий ошибке too_many_rows. Обратите внимание: после обработки в таблице остается 2 строки, так как перед выполнением обработчика произошел откат к неявной точке сохранения, которая устанавливается в начале блока.

Page 5: Page 5

Также заметьте, что локальная переменная функции сохранила то значение, которое было на момент возникновения ошибки. Тонкий момент: если ошибка произойдет в секции DECLARE или в самом обработчике внутри EXCEPTION, то в этом блоке ее перехватить не получится.

=> DO $$
DECLARE
    n integer := 1 / 0; -- ошибка в этом месте не перехватывается
BEGIN
    RAISE NOTICE 'Все успешно';
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'Деление на ноль';
END
$$;
ERROR:  division by zero
CONTEXT:  SQL expression "1 / 0"
PL/pgSQL function inline_code_block line 3 during statement block local variable
initialization

Page 6: Имена и коды ошибок

Source slide page 6: Имена и коды ошибок{#source-figure-006 caption=“Source slide page 6: Имена и коды ошибок”}

Информация об ошибке

  • имя
  • пятисимвольный код
  • дополнительные сведения: сообщение, детальное сообщение, подсказка,
  • имена объектов, связанных с ошибкой
  • Двухуровневая иерархия

Комментарии преподавателя

Каждой возможной ошибке соответствует имя и код (строка из пяти символов). В условии WHEN можно указывать как имя, так и код.

Ошибки организованы в своеобразную двухуровневую иерархию. Категория ошибки имеет код, заканчивающийся на три нуля, и соответствует любой ошибке с теми же первыми двумя символами.

Например, код 23000 соответствует категории, которая включает все ошибки, связанные с нарушением ограничений целостности (такие, как 23502 — нарушение ограничения NOT NULL или 23505 — нарушение уникальности).

Таким образом, кроме обычных ошибок можно использовать имя или код категории. Можно также использовать специальное имя OTHERS для того, чтобы перехватить любую ошибку (кроме самых фатальных).

Кроме имени и кода любая ошибка может иметь дополнительные сведения, облегчающие ее диагностику: сообщение, детальное сообщение, подсказку.

Все ошибки описаны в приложении A документации:

https://postgrespro.ru/docs/postgresql/16/errcodes-appendix

Ошибки можно не только перехватывать, но и программно вызывать.

https://postgrespro.ru/docs/postgresql/16/plpgsql-errors-and-messages

Page 7: Имена и коды ошибок

Имена ошибок мы уже видели, а для указания кода служит конструкция SQLSTATE. В обработчике можно получить код ошибки и сообщение с помощью предопределенных переменных SQLSTATE и SQLERRM (вне блока EXCEPTION эти переменные не определены).

=> DO $$
DECLARE
    n integer;
BEGIN
    SELECT id INTO STRICT n FROM t;
EXCEPTION
    WHEN SQLSTATE 'P0003' OR no_data_found THEN -- можно несколько
        RAISE NOTICE '%: %', SQLSTATE, SQLERRM;
END
$$;
NOTICE:  P0003: query returned more than one row
DO

Какой обработчик будет использован?

=> DO $$
DECLARE
    n integer;
BEGIN
    SELECT id INTO STRICT n FROM t;
EXCEPTION
    WHEN no_data_found THEN
        RAISE NOTICE 'Нет данных. %: %', SQLSTATE, SQLERRM;
    WHEN plpgsql_error THEN
        RAISE NOTICE 'Другая ошибка. %: %', SQLSTATE, SQLERRM;
    WHEN too_many_rows THEN
        RAISE NOTICE 'Слишком много данных. %: %', SQLSTATE, SQLERRM;
END
$$;
NOTICE:  Другая ошибка. P0003: query returned more than one row
DO

Выбирается первый подходящий обработчик, в данном случае — plpgsql_error (напомним, что это не отдельная ошибка, а категория). До последнего обработчика дело никогда не дойдет. Ошибку можно принудительно вызвать по ее коду или имени. Здесь мы используем специальное имя others, соответствующее любой ошибке, которую можно перехватить (за исключением прерванного клиентом выполнения и нарушения отладочной проверки assert — их можно перехватить отдельно, но обычно это не имеет смысла).

=> DO $$
BEGIN
    RAISE no_data_found;
EXCEPTION
    WHEN others THEN
        RAISE NOTICE '%: %', SQLSTATE, SQLERRM;
END
$$;
NOTICE:  P0002: no_data_found
DO

При необходимости можно задействовать и пользовательские коды ошибок, отсутствующие в справочнике, а также указать некоторую дополнительную информацию (в примере — только часть из возможного):

=> DO $$
BEGIN
    RAISE SQLSTATE 'ERR01' USING
        message := 'Сбой матрицы',
        detail  := 'При выполнении произошел непоправимый сбой матрицы',
        hint := 'Обратитесь к системному администратору';
END
$$;

Page 8: Page 8

ERROR:  Сбой матрицы
DETAIL:  При выполнении произошел непоправимый сбой
матрицы
HINT:  Обратитесь к системному администратору
CONTEXT:  PL/pgSQL function inline_code_block line 3 at RAISE

В обработчике эту информацию нельзя получить из переменных; если нужно проанализировать такие данные в коде, есть специальная конструкция:

=> DO $$
DECLARE
    message text;
    detail text;
    hint text;
BEGIN
    RAISE SQLSTATE 'ERR01' USING
        message := 'Сбой матрицы',
        detail  := 'При выполнении произошел непоправимый сбой матрицы',
        hint := 'Обратитесь к системному администратору';
EXCEPTION
    WHEN others THEN
        GET STACKED DIAGNOSTICS
            message := MESSAGE_TEXT,
            detail := PG_EXCEPTION_DETAIL,
            hint := PG_EXCEPTION_HINT;
        RAISE NOTICE E'\nmessage = %\ndetail = %\nhint = %',
            message, detail, hint;
END
$$;
NOTICE:
message = Сбой матрицы
detail = При выполнении произошел непоправимый сбой
матрицы
hint = Обратитесь к системному администратору
DO

Page 9: Поиск обработчика

Необработанная ошибка «поднимается» на уровень выше

  • в объемлющий блок PL/pgSQL, если он есть
  • в вызывающую подпрограмму, если она есть
  • Поиск обработчика определяется стеком вызовов
    • то есть не определен статически, а зависит от выполнения программы
  • Никем не обработанная ошибка отправляется клиенту
    • транзакция переходит в состояние сбоя, клиент должен ее откатить
    • информация об ошибке записывается в журнал сообщений сервера

Комментарии преподавателя

Если ни одно из условий, перечисленных в секции EXCEPTION, не сработает, то ошибка «поднимается» на уровень выше.

Если ошибка возникла в блоке, вложенном в другой блок, то сервер будет пытаться найти обработчик в объемлющем блоке. Если и там не найдется подходящего обработчика, весь объемлющий блок будет считаться ошибочным и ошибка поднимется еще выше, и так далее.

Если мы перебрали все объемлющие блоки и не обнаружили подходящего обработчика возникшей ошибки, ошибка «поднимается» на уровень той подпрограммы, которая вызвала наш блок. То есть, чтобы разобраться, в каком порядке будут просматриваться обработчики ошибки, надо проанализировать стек вызовов.

Если ни один из возможных обработчиков ошибки не сработает:

● сообщение об ошибке обычно попадает в журнал сообщений сервера (это зависит от настроек сервера, см. тему «PL/pgSQL. Отладка»);

● об ошибке сообщается клиенту, который инициировал вызов кода в базе данных. Клиент ставится перед фактом: транзакция переходит в состояние сбоя, и ее можно только откатить.

Как именно клиент будет обрабатывать ошибку, зависит от него самого. Например, psql выведет сообщение об ошибке и всю доступную диагностическую информацию. Клиент, ориентированный на конечного пользователя, может вывести знаменитое «обратитесь к системному администратору» и т. д.

Page 10: Поиск обработчика

Рассмотрим несколько примеров поиска обработчика в случае вложенных блоков. Что будет выведено?

=> DO $$
BEGIN
    BEGIN
        SELECT 1/0;
        RAISE NOTICE 'Вложенный блок выполнен';
    EXCEPTION
        WHEN division_by_zero THEN
            RAISE NOTICE 'Ошибка во вложенном блоке';
    END;
    RAISE NOTICE 'Внешний блок выполнен';
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'Ошибка во внешнем блоке';
END
$$;
NOTICE:  Ошибка во вложенном блоке
NOTICE:  Внешний блок выполнен
DO

Ошибка обрабатывается в том блоке, в котором она возникла. Внешний блок выполняется так, как будто никакой ошибки не было. А так?

=> DO $$
BEGIN
    BEGIN
        SELECT 1/0;
        RAISE NOTICE 'Вложенный блок выполнен';
    EXCEPTION
        WHEN no_data_found THEN
            RAISE NOTICE 'Ошибка во вложенном блоке';
    END;
    RAISE NOTICE 'Внешний блок выполнен';
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'Ошибка во внешнем блоке';
END
$$;
NOTICE:  Ошибка во внешнем блоке
DO

Обработчик во внутреннем блоке не подходит; блок завершается с ошибкой, которая обрабатывается уже во внешнем блоке. Не забывайте, что в блоке с секцией EXCEPTION происходит откат к точке сохранения, неявно установленной в начале блока. В данном случае будут отменены все изменения, сделанные в обоих блоках. А так?

=> DO $$
BEGIN
    BEGIN
        SELECT 1/0;
        RAISE NOTICE 'Вложенный блок выполнен';
    EXCEPTION
        WHEN no_data_found THEN
            RAISE NOTICE 'Ошибка во вложенном блоке';
    END;
    RAISE NOTICE 'Внешний блок выполнен';
EXCEPTION
    WHEN no_data_found THEN
        RAISE NOTICE 'Ошибка во внешнем блоке';
END
$$;
ERROR:  division by zero
CONTEXT:  SQL statement "SELECT 1/0"
PL/pgSQL function inline_code_block line 4 at SQL statement

Page 11: Page 11

Так не срабатывает ни один обработчик, и вся транзакция обрывается. Обычно не нужно стремиться обработать все возможные ошибки в серверном коде. Нет ничего плохого в том, чтобы передать возникшую ошибку клиенту. В целом, обрабатывать ошибку имеет смысл на том уровне, на котором можно сделать что-то осмысленное в возникшей ситуации. Поэтому обрабатывать ошибку внутри базы данных стоит, когда можно что-то сделать именно на серверной стороне (например, повторить операцию при ошибке сериализации). Про журналирование сообщений об ошибках мы еще будем говорить в теме «PL/pgSQL. Отладка». Теперь рассмотрим пример с процедурами.

=> CREATE PROCEDURE foo()
AS $$
BEGIN
    CALL bar();
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CREATE PROCEDURE bar()
AS $$
BEGIN
    CALL baz();
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CREATE PROCEDURE baz()
AS $$
BEGIN
    PERFORM 1 / 0;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Что произойдет при вызове?

=> CALL foo();
ERROR:  division by zero
CONTEXT:  SQL statement "SELECT 1 / 0"
PL/pgSQL function baz() line 3 at PERFORM
SQL statement "CALL baz()"
PL/pgSQL function bar() line 3 at CALL
SQL statement "CALL bar()"
PL/pgSQL function foo() line 3 at CALL

То, что мы видим в сообщении об ошибке — это стек вызовов: сверху вниз = изнутри наружу. Заметьте, что в этом сообщении, как и во многих других, вместо слова procedure используется function. В обработчике ошибки тоже можно получить доступ к стеку, правда, в виде одной строки:

=> CREATE OR REPLACE PROCEDURE bar()
AS $$
DECLARE
    msg text;
    ctx text;
BEGIN
    CALL baz();
EXCEPTION
    WHEN others THEN
        GET STACKED DIAGNOSTICS
             msg := MESSAGE_TEXT,
             ctx := PG_EXCEPTION_CONTEXT;
        RAISE NOTICE E'\nОшибка: %\nСтек ошибки:\n%\n', msg, ctx;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Проверим:

=> CALL foo();

Page 12: Page 12

NOTICE:
Ошибка: division by zero
Стек ошибки:
SQL statement "SELECT 1 / 0"
PL/pgSQL function baz() line 3 at PERFORM
SQL statement "CALL baz()"
PL/pgSQL function bar() line 6 at CALL
SQL statement "CALL bar()"
PL/pgSQL function foo() line 3 at CALL
CALL

Поскольку блок с секцией EXCEPTION устанавливает неявную точку сохранения, то и в этом блоке, и во всех блоках выше по стеку вызовов, процедуры лишаются возможности использовать команды COMMIT и ROLLBACK.

=> CREATE OR REPLACE PROCEDURE baz()
AS $$
BEGIN
    COMMIT;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CALL foo();
NOTICE:
Ошибка: invalid transaction termination
Стек ошибки:
PL/pgSQL function baz() line 3 at COMMIT
SQL statement "CALL baz()"
PL/pgSQL function bar() line 6 at CALL
SQL statement "CALL bar()"
PL/pgSQL function foo() line 3 at CALL
CALL

Page 13: Накладные расходы

Любой блок с секцией EXCEPTION выполняется медленнее

  • из-за установки неявной точки сохранения
  • Дополнительные расходы при возникновении ошибки
    • из-за отката к точке сохранения
  • Обработку ошибок можно и нужно использовать, но не стоит употреблять без необходимости
    • PL/pgSQL и так интерпретируется и использует SQL для вычисления
    • выражений
    • для многих задач эта скорость более чем удовлетворительна
    • проблемы с производительностью обычно связаны с запросами,
    • а не с кодом PL/pgSQL

Комментарии преподавателя

Одно только указание секции EXCEPTION уже влечет за собой накладные расходы из-за того, что в начале блока будет неявно устанавливаться точка сохранения. А если ошибка действительно возникает, то накладные расходы еще более увеличиваются из-за того, что выполняется откат транзакции к точке сохранения.

Поэтому если есть простой способ обойтись без обработки исключений, лучше от нее отказаться. Не стоит строить логику работы программы на «жонглировании» исключениями.

Однако если обработка ошибок действительно нужна, не стоит сомневаться — обрабатывать ошибки можно и нужно несмотря на накладные расходы.

Во-первых, язык PL/pgSQL довольно медленный сам по себе из-за интерпретации и обращений к SQL для вычисления выражений.

Во-вторых, очень часто эта скорость вполне удовлетворительна. Да, можно и быстрее, если написать на C, но зачем?

И в-третьих, основные проблемы с производительностью, как правило, связаны со скоростью выполнения запросов из-за неправильно построенных планов, а вовсе не со скоростью работы процедурного кода (см. курс QPT «Оптимизация запросов»).

Так что если есть альтернативное решение, которое и проще, и быстрее — конечно лучше воспользоваться им.

Page 14: Накладные расходы

Чтобы оценить накладные расходы, рассмотрим простой пример. Пусть имеется таблица с текстовым полем, в которое пользователи заносят произвольные данные (обычно это признак неудачного дизайна, но иногда приходится). Нам нужно выделить числа в отдельный столбец числового типа.

=> CREATE TABLE data(comment text, n integer);
CREATE TABLE
=> INSERT INTO data(comment)
SELECT CASE
        WHEN random() < 0.01 THEN 'не число' --  1%
        ELSE (random()*1000)::integer::text  -- 99%
    END
FROM generate_series(1,1_000_000);
INSERT 0 1000000

Решим задачу с помощью обработки ошибок, возникающих при преобразовании текстовых данных к числу:

=> CREATE FUNCTION safe_to_integer_ex(s text) RETURNS integer
AS $$
BEGIN
    RETURN s::integer;
EXCEPTION
    WHEN invalid_text_representation THEN
        RETURN NULL;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим:

=> \timing on
Timing is on.
=> UPDATE data SET n = safe_to_integer_ex(comment);
UPDATE 1000000
Time: 12090,298 ms (00:12,090)
=> \timing off
Timing is off.
=> SELECT count(*) FROM data WHERE n IS NOT NULL;
 count
--------
 989998
(1 row)

В другом варианте функции вместо обработки ошибки будем проверять формат с помощью регулярного выражения (слегка упрощенного):

=> CREATE FUNCTION safe_to_integer_re(s text) RETURNS integer
AS $$
BEGIN
    RETURN CASE
        WHEN s ~ '^\d+$' THEN s::integer
        ELSE NULL
    END;
END
$$ IMMUTABLE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим этот вариант:

=> \timing on
Timing is on.
=> UPDATE data SET n = safe_to_integer_re(comment);

Page 15: Page 15

UPDATE 1000000
Time: 9247,980 ms (00:09,248)
=> \timing off
Timing is off.
=> SELECT count(*) FROM data WHERE n IS NOT NULL;
 count
--------
 989998
(1 row)

Получается заметно быстрее. В этом примере исключение срабатывало всего в 1% случаев. Чем чаще — тем больше будет дополнительных накладных расходов на откат к точке сохранения.

=> UPDATE data SET comment = 'не число'; -- 100%
UPDATE 1000000
=> \timing on
Timing is on.
=> UPDATE data SET n = safe_to_integer_ex(comment);
UPDATE 1000000
Time: 20894,758 ms (00:20,895)
=> \timing off
Timing is off.

Встречаются (и довольно часто) случаи, когда можно обойтись без обработки ошибок, если выбрать другие подходящие средства. Задача: вернуть строку из справочника или NULL, если строки нет.

=> CREATE TABLE categories(code text UNIQUE, description text);
CREATE TABLE
=> INSERT INTO categories VALUES ('books','Книги'), ('discs','Диски');
INSERT 0 2

Функция с обработкой ошибки:

=> CREATE FUNCTION get_cat_desc(code text) RETURNS text
AS $$
DECLARE
    desc text;
BEGIN
    SELECT c.description INTO STRICT desc
    FROM categories c
    WHERE c.code = get_cat_desc.code;
    RETURN desc;
EXCEPTION
    WHEN no_data_found THEN
        RETURN NULL;
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим, что функция работает правильно:

=> SELECT get_cat_desc('books');
 get_cat_desc
--------------
 Книги
(1 row)
=> SELECT get_cat_desc('movies');
 get_cat_desc
--------------
(1 row)

Page 16: Page 16

Можно ли проще? Да, надо просто убрать STRICT или использовать подзапрос:

=> CREATE OR REPLACE FUNCTION get_cat_desc(code text) RETURNS text
AS $$
BEGIN
    RETURN (SELECT c.description
            FROM categories c
            WHERE c.code = get_cat_desc.code);
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

В таком варианте хорошо видно, что PL/pgSQL тут вообще не нужен — достаточно SQL. Проверим:

=> SELECT get_cat_desc('books');
 get_cat_desc
--------------
 Книги
(1 row)
=> SELECT get_cat_desc('movies');
 get_cat_desc
--------------
(1 row)

Задача: обновить строку таблицы с определенным идентификатором, а если такой строки нет — вставить ее. Первый подход. Что здесь плохо?

=> CREATE OR REPLACE FUNCTION change(code text, description text)
RETURNS void
AS $$
DECLARE
    cnt integer;
BEGIN
    SELECT count(*) INTO cnt
    FROM categories c WHERE c.code = change.code;
    IF cnt = 0 THEN
        INSERT INTO categories VALUES (code, description);
    ELSE
        UPDATE categories c
        SET description = change.description
        WHERE c.code = change.code;
    END IF;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Плохо практически все, начиная с того, что такая функция будет работать некорректно на уровне изоляции Read Committed при наличии нескольких параллельно выполняющихся сеансов. Причина в том, что после выполнения SELECT и перед следующей операцией данные в базе могут измениться. Это легко продемонстрировать, если добавить задержку между командами. Для разнообразия возьмем немного другой (но тоже неправильный) вариант:

=> CREATE OR REPLACE FUNCTION change(code text, description text)
RETURNS void
AS $$
BEGIN
    UPDATE categories c
    SET description = change.description
    WHERE c.code = change.code;
    IF NOT FOUND THEN
        PERFORM pg_sleep(1); -- тут может произойти все, что угодно
        INSERT INTO categories VALUES (code, description);
    END IF;
END
$$ VOLATILE LANGUAGE plpgsql;

Page 17: Page 17

CREATE FUNCTION

Теперь выполним функцию в двух сеансах почти одновременно:

=> SELECT change('games', 'Игры');
=> SELECT change('games', 'Игры');
ERROR:  duplicate key value violates unique constraint "categories_code_key"
DETAIL:  Key (code)=(games) already exists.
CONTEXT:  SQL statement "INSERT INTO categories VALUES (code, description)"
PL/pgSQL function change(text,text) line 9 at SQL statement
 change
--------
(1 row)

Правильное решение можно построить с помощью обработки ошибки:

=> CREATE OR REPLACE FUNCTION change(code text, description text)
RETURNS void
AS $$
BEGIN
    LOOP
        UPDATE categories c
        SET description = change.description
        WHERE c.code = change.code;
        EXIT WHEN FOUND;
        PERFORM pg_sleep(1); -- для демонстрации
        BEGIN
            INSERT INTO categories VALUES (code, description);
            EXIT;
        EXCEPTION
            WHEN unique_violation THEN NULL;
        END;
    END LOOP;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим.

=> SELECT change('vynil', 'Грампластинки');
=> SELECT change('vynil', 'Грампластинки');
 change
--------
(1 row)
 change
--------
(1 row)

Да, теперь все правильно. Но можно решить задачу проще с помощью варианта команды INSERT, который пробует выполнить вставку, а при возникновении конфликта — обновление. И снова достаточно простого SQL.

=> CREATE OR REPLACE FUNCTION change(code text, description text)
RETURNS void
VOLATILE LANGUAGE sql
BEGIN ATOMIC
    INSERT INTO categories VALUES (code, description)
    ON CONFLICT(code)
        DO UPDATE SET description = change.description;
END;
CREATE FUNCTION

Page 18: Page 18

Задача: гарантировать, что данные обрабатываются одновременно только одним процессом (на уровне изоляции Read Committed). Используя ту же таблицу, представим, что периодически категория требует специальной однопоточной обработки. Можно написать функцию следующим образом:

=> CREATE OR REPLACE FUNCTION process_cat(code text) RETURNS text
AS $$
BEGIN
    PERFORM c.code FROM categories c WHERE c.code = process_cat.code
        FOR UPDATE NOWAIT;  -- пробуем блокировать строку без ожидания
    PERFORM pg_sleep(1); -- собственно обработка
    RETURN 'Категория обработана';
EXCEPTION
    WHEN lock_not_available THEN
        RETURN 'Другой процесс уже обрабатывает эту категорию';
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим, что все правильно:

=> SELECT process_cat('books');
=> SELECT process_cat('books');
                  process_cat
-----------------------------------------------
 Другой процесс уже обрабатывает эту категорию
(1 row)
     process_cat
----------------------
 Категория обработана
(1 row)

Но и эту задачу можно решить без обработки ошибок, используя рекомендательные блокировки:

=> CREATE OR REPLACE FUNCTION process_cat(code text) RETURNS text
AS $$
BEGIN
    IF pg_try_advisory_xact_lock(hashtext(code)) THEN
        PERFORM pg_sleep(1); -- собственно обработка
        RETURN 'Категория обработана';
    ELSE
        RETURN 'Другой процесс уже обрабатывает эту категорию';
    END IF;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Проверим:

=> SELECT process_cat('books');
=> SELECT process_cat('books');
                  process_cat
-----------------------------------------------
 Другой процесс уже обрабатывает эту категорию
(1 row)
     process_cat
----------------------
 Категория обработана
(1 row)

Приведем и пример, когда без обработки ошибок не обойтись. Задача: организовать обработку пакета документов; ошибка при обработке одного документа не должна приводить к общему сбою.

=> CREATE TYPE doc_status AS ENUM -- тип перечисления
    ('READY', 'ERROR', 'PROCESSED');

Page 19: Page 19

CREATE TYPE
=> CREATE TABLE documents(
    id integer,
    version integer,
    status doc_status,
    message text
);
CREATE TABLE
=> INSERT INTO documents(id, version, status)
    SELECT id, 1, 'READY' FROM generate_series(1,100) id;
INSERT 0 100

Процедура, обрабатывающая один документ, иногда приводит к ошибке:

=> CREATE PROCEDURE process_one_doc(id integer)
AS $$
BEGIN
    UPDATE documents d
    SET version = version + 1
    WHERE d.id = process_one_doc.id;
    -- обработка может длиться долго
    IF random() < 0.05 THEN
        RAISE EXCEPTION 'Случилось страшное';
    END IF;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Теперь напишем процедуру, обрабатывающую все документы. Она вызывает в цикле обработку одного документа и при необходимости обрабатывает ошибку. Обратите внимание, что фиксация транзакции выполняется вне блока с секцией EXCEPTION.

=> CREATE PROCEDURE process_docs()
AS $$
DECLARE
    doc record;
BEGIN
    FOR doc IN (SELECT id FROM documents WHERE status = 'READY')
    LOOP
        BEGIN
            CALL process_one_doc(doc.id);
            UPDATE documents d
            SET status = 'PROCESSED'
            WHERE d.id = doc.id;
        EXCEPTION
            WHEN others THEN
                UPDATE documents d
                SET status = 'ERROR', message = sqlerrm
                WHERE d.id = doc.id;
        END;
        COMMIT; -- каждый документ в своей транзакции
    END LOOP;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Такую же обработку можно организовать и при помощи функции, но тогда все документы будут обрабатываться в одной общей транзакции, что может приводить к проблемам, если обработка выполняется долго. Этот вопрос детально изучается в курсе DEV2. Проверим результат:

=> CALL process_docs();
CALL
=> SELECT d.status, d.version, count(*)::integer
FROM documents d
GROUP BY d.status, d.version;

Page 20: Page 20

  status   | version | count
-----------+---------+-------
 ERROR     |       1 |     7
 PROCESSED |       2 |    93
(2 rows)

Как видим, часть документов не обработалась, но это не помешало обработке остальных. Информация об ошибках удобно сохраняется в самой таблице:

=> SELECT * FROM documents d WHERE d.status = 'ERROR';
 id | version | status |      message
----+---------+--------+--------------------
  2 |       1 | ERROR  | Случилось страшное
  6 |       1 | ERROR  | Случилось страшное
  9 |       1 | ERROR  | Случилось страшное
 38 |       1 | ERROR  | Случилось страшное
 14 |       1 | ERROR  | Случилось страшное
 94 |       1 | ERROR  | Случилось страшное
 57 |       1 | ERROR  | Случилось страшное
(7 rows)

И еще раз обратим внимание, что при возникновении ошибки происходит откат к точке сохранения в начале блока: благодаря этому версии документов в статусе ERROR остались равными 1.

Page 21: Итоги

Поиск обработчика ошибки происходит «изнутри наружу» в порядке вложенности блоков и вызова подпрограмм

  • В начале блока с EXCEPTION устанавливается неявная точка сохранения, при ошибке происходит откат к этой точке
  • Неперехваченная ошибка приводит к обрыву транзакции, сообщения отправляются клиенту и в журнал сервера
  • Обработка ошибок связана с накладными расходами

Page 22: Практика

  1. Если при добавлении новой книги указать одного и того же автора несколько раз, произойдет ошибка. Измените функцию add_book: перехватите ошибку нарушения уникальности и вместо нее вызовите ошибку с понятным текстовым сообщением.
  • Проверьте изменения в приложении.

Комментарии преподавателя

  1. Чтобы определить название ошибки, которую необходимо перехватить, перехватите все ошибки (WHEN OTHERS) и выведите необходимую информацию (вызвав новую ошибку с соответствующим текстом).

После этого не забудьте заменить WHEN OTHERS на конкретную ошибку — пусть остальные типы ошибок обрабатываются на более высоком уровне, раз в этом месте кода нет возможности сделать что-то конструктивное.

(В реальной жизни не стоило бы обрабатывать и нарушение уникальности — лучше было бы изменить приложение так, чтобы оно не позволяло указывать двух одинаковых авторов.)

Page 23: 1. Обработка повторяющихся авторов при добавлении книги

=> CREATE OR REPLACE FUNCTION add_book(title text, authors integer[])
RETURNS integer
AS $$
DECLARE
    book_id integer;
    id integer;
    seq_num integer := 1;
BEGIN
    INSERT INTO books(title)
        VALUES(title)
        RETURNING books.book_id INTO book_id;
    FOREACH id IN ARRAY authors LOOP
        INSERT INTO authorship(book_id, author_id, seq_num)
            VALUES (book_id, id, seq_num);
        seq_num := seq_num + 1;
    END LOOP;
    RETURN book_id;
EXCEPTION
    WHEN unique_violation THEN
        RAISE EXCEPTION 'Один и тот же автор не может быть указан дважды';
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Page 24: Практика+

  1. Ряд языков имеет конструкцию try … catch … finally …, в которой try соответствует BEGIN, catch — EXCEPTION, а операторы из блока finally срабатывают всегда, независимо от того, возникло ли исключение и было ли оно обработано блоком catch. Предложите способ добиться подобного эффекта в PL/pgSQL. 2. Сравните стеки вызовов, получаемые конструкциями GET STACKED DIAGNOSTICS с элементом pg_exception_context и GET [CURRENT] DIAGNOSTICS с элементом pg_context. 3. Напишите функцию getstack, возвращающую текущий стек вызовов в виде массива строк. Сама функция getstack не должна фигурировать в стеке.

Комментарии преподавателя

  1. Самый простой способ — просто повторить операторы finally в нескольких местах. Однако попробуйте построить такую конструкцию, чтобы эти операторы можно было написать ровно один раз.

  2. Для начала обратитесь к документации:

https://postgrespro.ru/docs/postgresql/16/plpgsql-statements#PLPGSQL-ST ATEMENTS-DIAGNOSTICS

https://postgrespro.ru/docs/postgresql/16/plpgsql-control-structures#PLPGS QL-EXCEPTION-DIAGNOSTICS

Page 25: 1. Try-catch-finally

=> CREATE DATABASE plpgsql_exceptions;
CREATE DATABASE
=> \c plpgsql_exceptions
You are now connected to database "plpgsql_exceptions" as user "student".

Сложность состоит в том, что операторы finally должны выполняться всегда, даже в случае возникновения ошибки в операторах catch (блок EXCEPTION). Решение может использовать два вложенных блока и фиктивное исключение, которое вызывается при нормальном завершении внутреннего блока. Это дает возможность поместить операторы finally в одно место — обработчик ошибок внешнего блока.

=> DO $$
BEGIN
    BEGIN
        RAISE NOTICE 'Операторы try';
        --
        RAISE NOTICE '...нет исключения';
    EXCEPTION
        WHEN no_data_found THEN
            RAISE NOTICE 'Операторы catch';
    END;
    RAISE SQLSTATE 'ALLOK';
EXCEPTION
    WHEN others THEN
        RAISE NOTICE 'Операторы finally';
        IF SQLSTATE != 'ALLOK' THEN
            RAISE;
        END IF;
END
$$;
NOTICE:  Операторы try
NOTICE:  ...нет исключения
NOTICE:  Операторы finally
DO
=> DO $$
BEGIN
    BEGIN
        RAISE NOTICE 'Операторы try';
        --
        RAISE NOTICE '...исключение, которое обрабатывается';
        RAISE no_data_found;
    EXCEPTION
        WHEN no_data_found THEN
            RAISE NOTICE 'Операторы catch';
    END;
    RAISE SQLSTATE 'ALLOK';
EXCEPTION
    WHEN others THEN
        RAISE NOTICE 'Операторы finally';
        IF SQLSTATE != 'ALLOK' THEN
            RAISE;
        END IF;
END
$$;
NOTICE:  Операторы try
NOTICE:  ...исключение, которое обрабатывается
NOTICE:  Операторы catch
NOTICE:  Операторы finally
DO

Page 26: Page 26

=> DO $$
BEGIN
    BEGIN
        RAISE NOTICE 'Операторы try';
        --
        RAISE NOTICE '...исключение, которое не обрабатывается';
        RAISE division_by_zero;
    EXCEPTION
        WHEN no_data_found THEN
            RAISE NOTICE 'Операторы catch';
    END;
    RAISE SQLSTATE 'ALLOK';
EXCEPTION
    WHEN others THEN
        RAISE NOTICE 'Операторы finally';
        IF SQLSTATE != 'ALLOK' THEN
            RAISE;
        END IF;
END
$$;
NOTICE:  Операторы try
NOTICE:  ...исключение, которое не обрабатывается
NOTICE:  Операторы finally
ERROR:  division_by_zero
CONTEXT:  PL/pgSQL function inline_code_block line 7 at RAISE

Но в предложенном решении всегда происходит откат всех изменений, выполненных в блоке, поэтому оно не годится для команд, изменяющих состояние базы данных. Также не стоит забывать о накладных расходах на обработку исключений: это задание — не более, чем просто упражнение.

2. GET DIAGNOSTICS

=> DO $$
DECLARE
    ctx text;
BEGIN
    RAISE division_by_zero;                       -- line 5
EXCEPTION
    WHEN others THEN
        GET STACKED DIAGNOSTICS ctx := PG_EXCEPTION_CONTEXT;
        RAISE NOTICE E'stacked =\n%', ctx;
        GET CURRENT DIAGNOSTICS ctx := PG_CONTEXT; -- line 10
        RAISE NOTICE E'current =\n%', ctx;
END
$$;
NOTICE:  stacked =
PL/pgSQL function inline_code_block line 5 at RAISE
NOTICE:  current =
PL/pgSQL function inline_code_block line 10 at GET DIAGNOSTICS
DO

GET STACKED DIAGNOSTICS дает стек вызовов, приведший к ошибке. GET [CURRENT] DIAGNOSTICS дает текущий стек вызовов.

3. Стек вызовов как массив

Собственно функция:

=> CREATE FUNCTION getstack() RETURNS text[]
AS $$
DECLARE
    ctx text;
BEGIN
    GET DIAGNOSTICS ctx := PG_CONTEXT;
    RETURN (regexp_split_to_array(ctx, E'\n'))[2:];
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Чтобы проверить ее работу, создадим несколько функций, которые вызывают друг друга:

Page 27: Page 27

=> CREATE FUNCTION foo() RETURNS integer
AS $$
BEGIN
    RETURN bar();
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE FUNCTION bar() RETURNS integer
AS $$
BEGIN
    RETURN baz();
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE FUNCTION baz() RETURNS integer
AS $$
BEGIN
    RAISE NOTICE '%', getstack();
    RETURN 0;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
=> SELECT foo();
NOTICE:  {"PL/pgSQL function baz() line 3 at RAISE","PL/pgSQL function bar() line 3 at
RETURN","PL/pgSQL function foo() line 3 at RETURN"}
 foo
-----
   0
(1 row)
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_exceptions;
DROP DATABASE

17 Занятие 17: PL/pgSQL / Триггеры

Исходная страница в нативном Markdown: PL/pgSQL / Триггеры
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_17_plpgsql_triggers.html Archive: DEV1-handouts-16.zip Страниц PDF: 32

Page 1: PL/pgSQL / Триггеры

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Триггеры и триггерные функции
  • В какой момент срабатывают триггеры
  • Контекст выполнения триггерной функции
  • Возвращаемое значение
  • Для чего можно применять триггеры и для чего не нужно
  • Событийные триггеры

Page 3: Триггеры и функции

Триггер

  • объект базы данных — список обрабатываемых событий
  • при возникновении события вызывается триггерная функция
  • и ей передается контекст
  • Триггерная функция
    • объект базы данных — код обработки события
    • выполняется в той же транзакции, что и основная операция
    • соглашение: функция не принимает параметры,
    • возвращает значение псевдотипа trigger (фактически record)
    • может использоваться в нескольких триггерах

Комментарии преподавателя

Механизм триггеров позволяет выполнять определенные действия «в ответ» на определенные события. Триггер состоит из двух частей: собственно триггера (который определяет события) и триггерной функции (которая определяет действия). И триггер, и функция являются самостоятельными объектами БД.

Когда возникает событие, на которое «подписан» триггер, вызывается триггерная функция. Ей передается контекст вызова, чтобы можно было определить, какой именно триггер и в каких условиях вызвал функцию.

Триггерная функция — это обычная функция, которая написана с учетом некоторых соглашений:

● она пишется на любом языке, кроме чистого SQL;

● она не имеет параметров;

● она возвращает значение типа trigger (на самом деле это псевдотип, по факту возвращается запись, соответствующая строке таблицы).

Триггерная функция выполняется в той же транзакции, что и основная операция. Таким образом, если триггерная функция завершится с ошибкой, вся транзакция будет прервана.

https://postgrespro.ru/docs/postgresql/16/trigger-definition

Page 4: События

INSERT, UPDATE, DELETE

  • таблицы
  • before/after statement
  • before/after row
  • представления
  • before/after statement
  • instead of
  • row
  • TRUNCATE
    • таблицы
    • before/after statement
  • Условие WHEN
    • устанавливает дополнительный фильтр

Комментарии преподавателя

Триггеры могут срабатывать на вставку (INSERT), обновление (UPDATE) или удаление (DELETE) строк в таблице или представлении, а также на опустошение (TRUNCATE) таблиц.

Триггер может срабатывать до выполнения действия (BEFORE), после него (AFTER), или вместо него (INSTEAD OF).

Триггер может срабатывать один раз для всей операции (FOR EACH STATEMENT), или каждый раз для каждой затронутой строки (FOR EACH ROW).

Не для любой комбинации этих условий можно создать триггер. Например, instead-of-триггеры можно определить только для представлений на уровне строк, а truncate-триггер можно определить только для таблиц и только на уровне оператора. Допустимые варианты перечислены на слайде.

Кроме того, можно сузить область действия триггера, указав дополнительное условие WHEN: если условие не выполняется — триггер не срабатывает.

https://postgrespro.ru/docs/postgresql/16/sql-createtrigger

Page 5: Before statement

Срабатывает

  • BEFORE STATEMENT
  • до операции
  • выполнение операции
  • Возвращаемое значение
    • игнорируется
  • Контекст
    • TG-переменные

Комментарии преподавателя

Рассмотрим подробнее разные типы триггеров.

Триггер BEFORE STATEMENT срабатывает один раз для операции независимо от того, сколько строк будет затронуто (возможно, что и ни одной). Это происходит до того, как операция начала выполняться.

Возвращаемое значение триггерной функции игнорируется. Если в триггере возникает ошибка, операция отменяется.

Поскольку триггерная функция не имеет параметров, контекст вызова в PL/pgSQL передается ей с помощью предопределенных TG-переменных, таких, как:

● TG_WHEN = «BEFORE»,

● TG_LEVEL = «STATEMENT»,

● TG_OP = «INSERT»/«UPDATE»/«DELETE»/«TRUNCATE»

и др. Триггерной функции можно также передать пользовательский контекст (аналог отсутствующих параметров) через переменную TG_ARGV, хотя зачастую лучше вместо одной «обобщенной» функции создать несколько «частных».

https://postgrespro.ru/docs/postgresql/16/plpgsql-trigger

Page 6: Before row

Срабатывает

  • BEFORE STATEMENT
  • перед действием со строкой
  • в процессе выполнения операции
  • выполнение операции
  • Возвращаемое значение
    • BEFORE ROW
    • строка (возможно, измененная)
    • null отменяет действие
  • Контекст
    • OLD
    • update, delete
    • NEW
    • insert, update
    • TG-переменные

Комментарии преподавателя

Триггеры BEFORE ROW срабатывают каждый раз перед тем, как операция затронет строку. Это происходит непосредственно в процессе выполнения операции.

В качестве контекста триггерная функция получает переменные:

● OLD — старая строка (не определено для операции вставки),

● NEW — измененная строка (не определено для удаления),

● TG_WHEN = «BEFORE»,

● TG_LEVEL = «ROW»,

● TG_OP = «INSERT»/«UPDATE»/«DELETE»

и др.

Возврат неопределенного значения NULL воспринимается как отмена действия над данной строкой. Сама операция продолжит выполнение, но текущая строка не будет обработана и другие триггеры для этой строки не сработают.

Чтобы не вмешиваться в работу операции, триггер должен вернуть строку в том виде, в котором ее собирается изменить операция: NEW для вставки и обновления, любое значение (но не NULL) для удаления (обычно используют OLD).

Но триггерная функция может и изменить значение NEW, чтобы повлиять на результат операции — часто именно для этого такой триггер и создают.

Page 7: Instead of row

Срабатывает

  • BEFORE STATEMENT
  • вместо действия со строкой
  • для представлений
  • выполнение операции
  • Возвращаемое значение
    • BEFORE ROW
    • INSTEAD OF ROW
    • строка (возможно, измененная) —
    • будет видна в RETURNING
    • null отменяет действие
  • Контекст
    • OLD
    • update, delete
    • NEW
    • insert, update
    • TG-переменные

Комментарии преподавателя

Триггеры INSTEAD OF очень похожи на триггеры BEFORE, но определяются только для представлений и срабатывают не до, а вместо операции.

В задачу таких триггеров обычно входит выполнение необходимых операций над базовыми таблицами представления. Также триггер может вернуть измененное значение NEW — именно оно будет видно при выполнении операции с указанием фразы RETURNING.

Page 8: After row

Срабатывает

  • BEFORE STATEMENT
  • после выполнения операции
  • очередь из прошедших условие WHEN
  • выполнение операции
  • Возвращаемое значение
    • BEFORE ROW
    • INSTEAD OF ROW
    • игнорируется
  • Контекст
    • OLD, OLD TABLE
    • update, delete
    • NEW, NEW TABLE
    • insert, update
    • TG-переменные
    • AFTER ROW

Комментарии преподавателя

Триггеры AFTER ROW, как и BEFORE ROW, срабатывают для каждой затрагиваемой строки, но не сразу после действия над строкой, а после того, как выполнена вся операция - чтобы при обращении из этих триггеров к изменяющейся таблице результат не зависел от порядка обработки строк. Для этого события сначала помещаются в очередь и обрабатываются после окончания операции. Чем меньше событий попадет в очередь, тем меньше будет накладных расходов — поэтому именно в этом случае очень полезно использовать условие WHEN, чтобы отсечь заведомо ненужные строки.

Возвращаемое значение триггеров AFTER ROW игнорируется (поскольку операция уже выполнена).

Контекст триггерной функции составляют:

  • OLD — старая строка (не определено для операции вставки),

  • NEW — новое значение строки (не определено для удаления).

Кроме этих переменных, начиная с версии 10 триггерная функция может получить доступ к специальным переходным таблицам (transition tables). Таблица, указанная при создании триггера как OLD TABLE, содержит старые значения строк, обработанных триггером, а таблица NEW TABLE — новые значения тех же строк.

Доступны и обычные TG-переменные, включая:

● TG_WHEN = «AFTER»,

● TG_LEVEL = «ROW»,

● TG_OP = «INSERT»/«UPDATE»/«DELETE».

Page 9: After statement

Срабатывает

  • BEFORE STATEMENT
  • после операции
  • (даже если не затронута ни одна строка)
  • выполнение операции
  • Возвращаемое значение
    • BEFORE ROW
    • INSTEAD OF ROW
    • игнорируется
  • Контекст
    • OLD TABLE
    • update, delete
    • NEW TABLE
    • insert, update
    • TG-переменные
    • AFTER ROW
    • AFTER STATEMENT

Комментарии преподавателя

Триггер AFTER STATEMENT срабатывает один раз после окончания операции и после всех триггеров AFTER ROW (независимо от того, сколько строк было затронуто).

Возвращаемое значение триггерной функции игнорируется.

Контекст вызова передается с помощью переходных таблиц. Обращаясь к ним, триггерная функция может проанализировать все затронутые строки. Обычно переходные таблицы используются именно с триггерами AFTER STATEMENT, а не AFTER ROW.

Также определены обычные TG-переменные:

● TG_WHEN = «AFTER»,

● TG_LEVEL = «STATEMENT»,

● TG_OP = «INSERT»/«UPDATE»/«DELETE»/«TRUNCATE»

и др.

Page 10: Порядок вызова триггеров

Создадим «универсальную» триггерную функцию, которая описывает контекст, в котором она вызвана. Контекст передается в различных TG-переменных. Затем создадим триггеры на различные события, и будем смотреть, какие триггеры и в каком порядке вызываются при выполнении операций.

=> CREATE OR REPLACE FUNCTION describe() RETURNS trigger
AS $$
DECLARE
    rec record;
    str text := '';
BEGIN
    IF TG_LEVEL = 'ROW' THEN
        CASE TG_OP
            WHEN 'DELETE' THEN rec := OLD; str := OLD::text;
            WHEN 'UPDATE' THEN rec := NEW; str := OLD || ' -> ' || NEW;
            WHEN 'INSERT' THEN rec := NEW; str := NEW::text;
        END CASE;
    END IF;
    RAISE NOTICE '% % % %: %',
        TG_TABLE_NAME, TG_WHEN, TG_OP, TG_LEVEL, str;
    RETURN rec;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Таблица:

=> CREATE TABLE t(
    id integer PRIMARY KEY,
    s text
);
CREATE TABLE

Триггеры на уровне оператора.

=> CREATE TRIGGER t_before_stmt
BEFORE INSERT OR UPDATE OR DELETE -- события
ON t                              -- таблица
FOR EACH STATEMENT                -- уровень
EXECUTE FUNCTION describe();      -- триггерная функция
CREATE TRIGGER
=> CREATE TRIGGER t_after_stmt
AFTER INSERT OR UPDATE OR DELETE ON t
FOR EACH STATEMENT EXECUTE FUNCTION describe();
CREATE TRIGGER

И на уровне строк:

=> CREATE TRIGGER t_before_row
BEFORE INSERT OR UPDATE OR DELETE ON t
FOR EACH ROW EXECUTE FUNCTION describe();
CREATE TRIGGER
=> CREATE TRIGGER t_after_row
AFTER INSERT OR UPDATE OR DELETE ON t
FOR EACH ROW EXECUTE FUNCTION describe();
CREATE TRIGGER

Пробуем вставку:

=> INSERT INTO t VALUES (1,'aaa'), (2, 'bbb');
NOTICE:  t BEFORE INSERT STATEMENT:
NOTICE:  t BEFORE INSERT ROW: (1,aaa)
NOTICE:  t BEFORE INSERT ROW: (2,bbb)
NOTICE:  t AFTER INSERT ROW: (1,aaa)
NOTICE:  t AFTER INSERT ROW: (2,bbb)
NOTICE:  t AFTER INSERT STATEMENT:
INSERT 0 2

Page 11: Page 11

Обновление:

=> UPDATE t SET s = 'ccc' WHERE id = 1;
NOTICE:  t BEFORE UPDATE STATEMENT:
NOTICE:  t BEFORE UPDATE ROW: (1,aaa) -> (1,ccc)
NOTICE:  t AFTER UPDATE ROW: (1,aaa) -> (1,ccc)
NOTICE:  t AFTER UPDATE STATEMENT:
UPDATE 1

Триггеры на уровне оператора сработают, даже если команда не обработала ни одной строки:

=> UPDATE t SET s = 'ddd' WHERE id = 0;
NOTICE:  t BEFORE UPDATE STATEMENT:
NOTICE:  t AFTER UPDATE STATEMENT:
UPDATE 0

Тонкий момент: оператор INSERT с предложением ON CONFLICT приводит к тому, что срабатывают BEFORE-триггеры и на вставку, и на обновление:

=> INSERT INTO t VALUES (1,'ddd'), (3,'eee')
ON CONFLICT(id) DO UPDATE SET s = EXCLUDED.s;
NOTICE:  t BEFORE INSERT STATEMENT:
NOTICE:  t BEFORE UPDATE STATEMENT:
NOTICE:  t BEFORE INSERT ROW: (1,ddd)
NOTICE:  t BEFORE UPDATE ROW: (1,ccc) -> (1,ddd)
NOTICE:  t BEFORE INSERT ROW: (3,eee)
NOTICE:  t AFTER UPDATE ROW: (1,ccc) -> (1,ddd)
NOTICE:  t AFTER INSERT ROW: (3,eee)
NOTICE:  t AFTER UPDATE STATEMENT:
NOTICE:  t AFTER INSERT STATEMENT:
INSERT 0 2

И, наконец, удаление:

=> DELETE FROM t WHERE id = 2;
NOTICE:  t BEFORE DELETE STATEMENT:
NOTICE:  t BEFORE DELETE ROW: (2,bbb)
NOTICE:  t AFTER DELETE ROW: (2,bbb)
NOTICE:  t AFTER DELETE STATEMENT:
DELETE 1

Для появившегося в PostgreSQL 15 оператора MERGE специального триггера нет, работают триггеры на обновление, удаление и вставку:

=> MERGE INTO t
USING (VALUES (1, 'fff'), (3, 'ggg'), (4, 'hhh')) AS vals(id, s)
ON t.id = vals.id
WHEN MATCHED AND t.id = 1 THEN
  UPDATE SET s = vals.s
WHEN MATCHED THEN
  DELETE
WHEN NOT MATCHED THEN
  INSERT (id, s)
  VALUES (vals.id, vals.s);
NOTICE:  t BEFORE INSERT STATEMENT:
NOTICE:  t BEFORE UPDATE STATEMENT:
NOTICE:  t BEFORE DELETE STATEMENT:
NOTICE:  t BEFORE UPDATE ROW: (1,ddd) -> (1,fff)
NOTICE:  t BEFORE DELETE ROW: (3,eee)
NOTICE:  t BEFORE INSERT ROW: (4,hhh)
NOTICE:  t AFTER UPDATE ROW: (1,ddd) -> (1,fff)
NOTICE:  t AFTER DELETE ROW: (3,eee)
NOTICE:  t AFTER INSERT ROW: (4,hhh)
NOTICE:  t AFTER DELETE STATEMENT:
NOTICE:  t AFTER UPDATE STATEMENT:
NOTICE:  t AFTER INSERT STATEMENT:
MERGE 3

Переходные таблицы

Напишем триггерную функцию, показывающую содержимое переходных таблиц. Здесь мы используем имена old_table и new_table, которые будут объявлены при создании триггера.

Page 12: Page 12

Переходные таблицы «выглядят» настоящими, но не присутствуют в системном каталоге и располагаются в оперативной памяти (хотя и могут сбрасываться на диск при большом объеме).

=> CREATE OR REPLACE FUNCTION transition() RETURNS trigger
AS $$
DECLARE
    rec record;
BEGIN
    IF TG_OP = 'DELETE' OR TG_OP = 'UPDATE' THEN
        RAISE NOTICE 'Старое состояние:';
        FOR rec IN SELECT * FROM old_table LOOP
            RAISE NOTICE '%', rec;
        END LOOP;
    END IF;
    IF TG_OP = 'UPDATE' OR TG_OP = 'INSERT' THEN
        RAISE NOTICE 'Новое состояние:';
        FOR rec IN SELECT * FROM new_table LOOP
            RAISE NOTICE '%', rec;
        END LOOP;
    END IF;
    RETURN NULL;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Создадим новую таблицу:

=> CREATE TABLE trans(
    id integer PRIMARY KEY,
    n integer
);
CREATE TABLE
=> INSERT INTO trans VALUES (1,10), (2,20), (3,30);
INSERT 0 3

Чтобы при выполнении операции создавались переходные таблицы, необходимо указывать их имена при создании триггера:

=> CREATE TRIGGER t_after_upd_trans
AFTER UPDATE ON trans -- только одно событие на один триггер
REFERENCING
    OLD TABLE AS old_table
    NEW TABLE AS new_table -- можно и одну, не обязательно обе
FOR EACH STATEMENT
EXECUTE FUNCTION transition();
CREATE TRIGGER

Проверим:

=> UPDATE trans SET n = n + 1 WHERE n <= 20;
NOTICE:  Старое состояние:
NOTICE:  (1,10)
NOTICE:  (2,20)
NOTICE:  Новое состояние:
NOTICE:  (1,11)
NOTICE:  (2,21)
UPDATE 2

Переходные таблицы содержат только те строки, которые были затронуты операцией. Для операций вставки и удаления переходные таблицы работают точно так же, но доступна будет только одна из таблиц: NEW TABLE или OLD TABLE соответственно. Поскольку триггеры AFTER ROW срабатывают после выполнения всей операции, переходные таблицы можно использовать и в них. Но обычно это не имеет смысла.

Page 13: Возможное использование

  • проверка применимости операции
  • BEFORE STATEMENT
  • выполнение операции
    • проверка корректности;
    • модификация строки
  • BEFORE ROW
    • изменение базовых таблиц представления
  • INSTEAD OF ROW
    • проверка согласованности,
    • в том числе на уровне таблицы;
    • «аудит» операций;
    • каскадное изменение таблиц
    • (денормализация, асинхронная
    • обработка…)
  • AFTER ROW
  • AFTER STATEMENT

Комментарии преподавателя

Каково практическое применение триггеров?

BEFORE-триггеры можно использовать для проверки корректности операции и при необходимости вызывать ошибку.

Триггеры BEFORE ROW можно применять для модификации строки (например, заполнить пустое поле нужным значением). Это бывает удобно, чтобы не повторять логику заполнения «технических» полей в каждой операции, а также позволяет вмешаться в работу приложения, код которого недоступен для изменения.

Триггеры INSTEAD OF ROW применяют для того, чтобы отобразить операции над представлением в операции над базовыми таблицами.

Триггеры AFTER ROW и AFTER STATEMENT полезны в случаях, когда нужно знать точное состояние после операции (BEFORE-триггеры могут влиять на результат, так что на этом этапе ясности еще нет):

● для проверки согласованности операции;

● для «аудита», то есть записи изменений в отдельное хранилище;

● для каскадного изменения других таблиц (например, обновлять денормализованные данные при изменении базовых таблиц, или записывать изменения в очередь для последующей обработки вне данной транзакции).

Если операции затрагивают сразу много строк, то триггер AFTER STATEMENT с переходными таблицами может оказаться более эффективным решением, чем триггер AFTER ROW, поскольку позволяет обрабатывать все изменения пакетно, а не построчно.

Page 14: Сложности

Код вызывается неявно

  • сложно отследить логику выполнения
  • Правила видимости изменчивой триггерной функции
    • виден результат триггеров BEFORE ROW или INSTEAD OF ROW
  • Порядок вызова триггеров для одного события
    • триггеры отрабатывают в алфавитном порядке
  • Не предотвращается зацикливание
    • триггер может вызвать срабатывание других триггеров
  • Можно нарушить ограничения целостности
    • например, исключив из обработки строки, которые должны удалиться

Комментарии преподавателя

Однако не стоит злоупотреблять триггерами. Триггеры срабатывают неявно, что сильно затрудняет понимание логики работы и крайне усложняет поддержку приложения. Попытки реализовать сложную логику на триггерах обычно заканчиваются плачевно.

В некоторых случаях вместо триггеров можно использовать вычисляемые поля (GENERATED ALWAYS AS … STORED). Такое решение — если оно подходит — будет заведомо проще и прозрачнее.

Есть ряд тонкостей, связанных с триггерами, которые мы сознательно не рассматриваем подробно:

● правила видимости изменчивых (volatile) функций в триггерах BEFORE ROW и INSTEAD OF ROW (не стоит обращаться к таблице, полагаясь на порядок, в котором сработают триггеры);

● порядок вызова нескольких триггеров, обрабатывающих одно и то же событие (не стоит усугублять и без того неявное срабатывание триггеров завязкой на последовательность обработки);

● возможность зацикливания в случае каскадного срабатывания других триггеров, которые, в свою очередь, могут приводить к новым срабатываниям данного триггера;

● возможность нарушить ограничения целостности (например, при исключении из обработки строки, которая удаляется условием ON DELETE CASCADE, может быть нарушена ссылочная целостность).

Если вы столкнулись с тем, что эти тонкости важны для вашего приложения — серьезно задумайтесь.

Page 15: Примеры использования триггеров

Пример 1: сохранение истории изменения строк. Пусть есть таблица, содержащая актуальные данные. Задача состоит в том, чтобы в отдельной таблице сохранять всю историю изменения строк основной таблицы. Поддержку исторической таблицы можно было бы возложить на приложение, но тогда велика вероятность, что в каких-то случаях из-за ошибок история не будет сохраняться. Поэтому решим задачу с помощью триггера. Основная таблица:

=> CREATE TABLE coins(
    face_value numeric PRIMARY KEY,
    name text
);
CREATE TABLE

Историческая таблица должна называться так же, как основная, но заканчиваться на «_history». Сначала создаем клон основной таблицы…

=> CREATE TABLE coins_history(LIKE coins);
CREATE TABLE

…и затем добавляем столбцы «действительно с» и «действительно по»:

=> ALTER TABLE coins_history
    ADD start_date timestamp,
    ADD end_date timestamp;
ALTER TABLE

Одна триггерная функция будет вставлять новую историческую строку с открытым интервалом действия:

=> CREATE OR REPLACE FUNCTION history_insert() RETURNS trigger
AS $$
BEGIN
    EXECUTE format(
        'INSERT INTO %I SELECT ($1).*, current_timestamp, NULL',
        TG_TABLE_NAME||'_history'
    ) USING NEW;
    RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Другая функция будет закрывать интервал действия исторической строки:

=> CREATE OR REPLACE FUNCTION history_delete() RETURNS trigger
AS $$
BEGIN
    EXECUTE format(
        'UPDATE %I SET end_date = current_timestamp WHERE face_value = $1 AND end_date IS NULL',
        TG_TABLE_NAME||'_history'
    ) USING OLD.face_value;
    RETURN OLD;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Теперь создадим триггеры. Важные моменты: Обновление трактуется как удаление и последующая вставка; здесь важен порядок, в котором сработают триггеры (по алфавиту). Current_timestamp возвращает время начала транзакции, поэтому при обновлении start_date одной строки будет равен end_date другой. Использование AFTER-триггеров позволяет избежать проблем с INSERT … ON CONFLICT и потенциальными конфликтами с другими триггерами, которые могут существовать на основной таблице.

=> CREATE TRIGGER coins_history_insert
AFTER INSERT OR UPDATE ON coins
FOR EACH ROW EXECUTE FUNCTION history_insert();

Page 16: Page 16

CREATE TRIGGER
=> CREATE TRIGGER coins_history_delete
AFTER UPDATE OR DELETE ON coins
FOR EACH ROW EXECUTE FUNCTION history_delete();
CREATE TRIGGER

Проверим работу триггеров.

=> INSERT INTO coins VALUES (0.25, 'Полушка'), (3, 'Алтын');
INSERT 0 2
=> UPDATE coins SET name = '3 копейки' WHERE face_value = 3;
UPDATE 1
=> INSERT INTO coins VALUES (5, '5 копеек');
INSERT 0 1
=> DELETE FROM coins WHERE face_value = 0.25;
DELETE 1
=> SELECT * FROM coins;
 face_value |   name
------------+-----------
          3 | 3 копейки
          5 | 5 копеек
(2 rows)

В исторической таблице хранится вся история изменений:

=> SELECT * FROM coins_history ORDER BY face_value, start_date;
 face_value |   name    |         start_date         |          end_date
------------+-----------+----------------------------+----------------------------
       0.25 | Полушка   | 2026-04-10 15:40:23.565304 | 2026-04-10 15:40:28.922066
          3 | Алтын     | 2026-04-10 15:40:23.565304 | 2026-04-10 15:40:25.800641
          3 | 3 копейки | 2026-04-10 15:40:25.800641 |
          5 | 5 копеек  | 2026-04-10 15:40:26.861254 |
(4 rows)

И теперь по ней можно восстановить состояние на любой момент времени (это немного напоминает работу механизма MVCC). Например, на самое начало:

=> \set d '2026-04-10 15:40:23.755414+03'
=> SELECT face_value, name
FROM coins_history
WHERE start_date <= :'d' AND (end_date IS NULL OR :'d' < end_date)
ORDER BY face_value;
 face_value |  name
------------+---------
       0.25 | Полушка
          3 | Алтын
(2 rows)

Примеры использования триггеров

Пример 2: обновляемое представление. Пусть имеются две таблицы: аэропорты и рейсы:

=> CREATE TABLE airports(
    code char(3) PRIMARY KEY,
    name text NOT NULL
);
CREATE TABLE
=> INSERT INTO airports VALUES
    ('SVO', 'Москва. Шереметьево'),
    ('LED', 'Санкт-Петербург. Пулково'),
    ('TOF', 'Томск. Богашево');
INSERT 0 3

Page 17: Page 17

=> CREATE TABLE flights(
    id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    airport_from char(3) NOT NULL REFERENCES airports(code),
    airport_to   char(3) NOT NULL REFERENCES airports(code),
    UNIQUE (airport_from, airport_to)
);
CREATE TABLE
=> INSERT INTO flights(airport_from, airport_to) VALUES
    ('SVO','LED');
INSERT 0 1

Для удобства можно определить представление:

=> CREATE VIEW flights_v AS
SELECT id,
       (SELECT name
        FROM airports
        WHERE code = airport_from) airport_from,
       (SELECT name
        FROM airports
        WHERE code = airport_to) airport_to
FROM flights;
CREATE VIEW
=> SELECT * FROM flights_v;
 id |    airport_from     |        airport_to
----+---------------------+--------------------------
  1 | Москва. Шереметьево | Санкт-Петербург. Пулково
(1 row)

Но такое представление не допускает изменений. Например, не получится изменить пункт назначения таким образом:

=> UPDATE flights_v
SET airport_to = 'Томск. Богашево'
WHERE id = 1;
ERROR:  cannot update column "airport_to" of view "flights_v"
DETAIL:  View columns that are not columns of their base relation are not updatable.

Однако мы можем определить триггер. Триггерная функция может выглядеть, например, так (для краткости обрабатываем только аэропорт назначения, но не составит труда добавить и аэропорт вылета):

=> CREATE OR REPLACE FUNCTION flights_v_update() RETURNS trigger
AS $$
DECLARE
    code_to char(3);
BEGIN
    BEGIN
        SELECT code INTO STRICT code_to
        FROM airports
        WHERE name = NEW.airport_to;
    EXCEPTION
        WHEN no_data_found THEN
            RAISE EXCEPTION 'Аэропорт "%" отсутствует', NEW.airport_to;
    END;
    UPDATE flights
    SET airport_to = code_to
    WHERE id = OLD.id; -- изменение id игнорируем
    RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

И сам триггер:

=> CREATE TRIGGER flights_v_upd_trigger
INSTEAD OF UPDATE ON flights_v
FOR EACH ROW EXECUTE FUNCTION flights_v_update();
CREATE TRIGGER

Проверим:

=> UPDATE flights_v
SET airport_to = 'Томск. Богашево'
WHERE id = 1;

Page 18: Page 18

UPDATE 1
=> SELECT * FROM flights_v;
 id |    airport_from     |   airport_to
----+---------------------+-----------------
  1 | Москва. Шереметьево | Томск. Богашево
(1 row)

Попытка изменить аэропорт на отсутствующий в таблице:

=> UPDATE flights_v
SET airport_to = 'Южно-Сахалинск. Хомутово'
WHERE id = 1;
ERROR:  Аэропорт "Южно-Сахалинск. Хомутово"
отсутствует
CONTEXT:  PL/pgSQL function flights_v_update() line 11 at RAISE

Page 19: Событийные триггеры

Событийный триггер

  • похож на обычный «табличный» триггер, но другой объект
  • Триггерная функция
    • соглашение: функция не принимает параметры,
    • возвращает значение псевдотипа event_trigger
    • для получения контекста служат специальные функции
  • События
    • DDL_COMMAND_START
    • перед выполнением команды
    • DDL_COMMAND_END
    • после выполнения команды
    • TABLE_REWRITE
    • перед перезаписью таблицы
    • SQL_DROP
    • после удаления объектов

Комментарии преподавателя

Событийные триггеры — по сути те же триггеры, но срабатывают они не на DML-, а на DDL-операции (CREATE, ALTER, DROP, COMMENT, GRANT, REVOKE) и не привязаны к определенному объекту базы данных.

Такие триггеры являются не инструментом разработки приложений, а скорее служат для решения задач администрирования. Поэтому здесь мы упоминаем их только для полноты картины и рассмотрим только простые примеры.

https://postgrespro.ru/docs/postgresql/16/event-trigger-definition

https://postgrespro.ru/docs/postgresql/16/event-trigger-matrix

https://postgrespro.ru/docs/postgresql/16/sql-createeventtrigger

https://postgrespro.ru/docs/postgresql/16/functions-event-triggers

Page 20: Триггеры событий

Пример триггера для события ddl_command_end — завершение DDL-операции. Создадим функцию, которая описывает контекст вызова:

=> CREATE OR REPLACE FUNCTION describe_ddl() RETURNS event_trigger
AS $$
DECLARE
    r record;
BEGIN
    -- Для события ddl_command_end контекст вызова в специальной функции
    FOR r IN SELECT * FROM pg_event_trigger_ddl_commands()
    LOOP
        RAISE NOTICE '%. тип: %, OID: %, имя: % ',
            r.command_tag, r.object_type, r.objid, r.object_identity;
    END LOOP;
    -- Функции триггера событий не нужно возвращать значение
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Сам триггер:

=> CREATE EVENT TRIGGER after_ddl
ON ddl_command_end EXECUTE FUNCTION describe_ddl();
CREATE EVENT TRIGGER

Создаем новую таблицу:

=> CREATE TABLE t1(id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY);
NOTICE:  CREATE SEQUENCE. тип: sequence, OID: 16455, имя: public.t1_id_seq
NOTICE:  CREATE TABLE. тип: table, OID: 16456, имя: public.t1
NOTICE:  CREATE INDEX. тип: index, OID: 16459, имя: public.t1_pkey
NOTICE:  ALTER SEQUENCE. тип: sequence, OID: 16455, имя: public.t1_id_seq
CREATE TABLE

Создание таблицы может привести к выполнению нескольких команд DDL, поэтому функция pg_event_trigger_ddl_commands возвращает множество строк. Триггер на событие table_rewrite срабатывает до перезаписи таблицы командами ALTER TABLE и ALTER TYPE. Функция ниже выведет информацию о перезаписываемой таблице и код, описывающий причину перезаписи:

=> CREATE FUNCTION describe_rewrite() RETURNS event_trigger
AS $$
BEGIN
    -- Для события table_rewrite специальные функции возвращают данные:
    RAISE NOTICE 'Будет перезаписана таблица %, код %',
                pg_event_trigger_table_rewrite_oid()::regclass,  -- перезаписываемая таблица
                pg_event_trigger_table_rewrite_reason();  -- код причины перезаписи
END
$$ LANGUAGE plpgsql;
NOTICE:  CREATE FUNCTION. тип: function, OID: 16461, имя: public.describe_rewrite()
CREATE FUNCTION

Теперь создаем триггер:

=> CREATE EVENT TRIGGER before_rewrite
ON table_rewrite EXECUTE FUNCTION describe_rewrite();
CREATE EVENT TRIGGER

Изменим таблицу — назначим другой тип столбцу id:

=> ALTER TABLE t1 ALTER COLUMN id type bigint;
NOTICE:  Будет перезаписана таблица t1, код 4
NOTICE:  ALTER SEQUENCE. тип: sequence, OID: 16455, имя: public.t1_id_seq
NOTICE:  ALTER TABLE. тип: table, OID: 16456, имя: public.t1
ALTER TABLE

Хотя перезапись таблицы может быть вызвана и другими командами, в частности CLUSTER и VACUUM FULL, событие table_rewrite для них не вызывается.

Page 21: Page 21

Page 22: Итоги

Триггер — способ отреагировать на возникновение события

  • С помощью триггера можно отменить операцию, изменить ее результат или выполнить дополнительные действия
  • Триггер выполняется как часть транзакции; ошибка в триггере приводит к откату транзакции
  • Использование триггеров AFTER ROW и переходных таблиц удорожает обработку Все хорошо в меру: сложную логику трудно отлаживать из-за неявного выполнения триггеров

Page 23: Практика

  1. Создайте триггер, обрабатывающий обновление поля onhand_qty представления catalog_v.
  • Проверьте, что в «Каталоге» появилась возможность заказывать книги. 2. Обеспечьте выполнение требования согласованности: количество книг на складе не может быть отрицательным (нельзя купить книгу, которой нет в наличии).
    • Внимательно проверьте правильность реализации, учитывая,
    • что с приложением могут одновременно работать несколько
    • пользователей.

Комментарии преподавателя

  1. Может показаться, что достаточно создать AFTER-триггер на таблице operations, подсчитывающий сумму qty_change. Однако на уровне изоляции Read Committed, с которым работает приложение «Книжный магазин», нам придется блокировать таблицу operations в эксклюзивном режиме — иначе возможны сценарии, при которых такая проверка не сработает.

Лучше поступить следующим образом: добавить в таблицу books поле onhand_qty и создать триггер, изменяющий это поле при изменении таблицы operations (то есть, фактически, выполнить денормализацию данных). На поле onhand_qty теперь можно наложить ограничение CHECK, реализующее требование согласованности. А функция onhand_qty(), которую мы создавали ранее, больше не нужна.

Особое внимание надо уделить начальной установке значения, учитывая, что одновременно с выполнением наших операций в системе могут работают пользователи.

Page 24: 1. Триггер для обновления каталога

=> CREATE FUNCTION update_catalog() RETURNS trigger
AS $$
BEGIN
    INSERT INTO operations(book_id, qty_change) VALUES
        (OLD.book_id, NEW.onhand_qty - coalesce(OLD.onhand_qty,0));
    RETURN NEW;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE TRIGGER update_catalog_trigger
INSTEAD OF UPDATE ON catalog_v
FOR EACH ROW
EXECUTE FUNCTION update_catalog();
CREATE TRIGGER

2. Проверка количества книг

Добавляем к таблице книг поле наличного количества. (До версии 11 важно было учитывать, что указание предложения DEFAULT вызывало перезапись всех строк таблицы, удерживая блокировку.)

=> ALTER TABLE books ADD COLUMN onhand_qty integer;
ALTER TABLE

Триггерная функция для AFTER-триггера на вставку для обновления количества (предполагаем, что поле onhand_qty не может быть пустым):

=> CREATE FUNCTION update_onhand_qty() RETURNS trigger
AS $$
BEGIN
    UPDATE books
    SET onhand_qty = onhand_qty + NEW.qty_change
    WHERE book_id = NEW.book_id;
    RETURN NULL;
END
$$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION

Дальше все происходит внутри транзакции.

=> BEGIN;
BEGIN

Блокируем операции на время транзакции:

=> LOCK TABLE operations;
LOCK TABLE

Начальное заполнение:

=> UPDATE books b
SET onhand_qty = (
    SELECT coalesce(sum(qty_change),0)
    FROM operations o
    WHERE o.book_id = b.book_id
);
UPDATE 6

Теперь, когда поле заполнено, задаем ограничения:

=> ALTER TABLE books ALTER COLUMN onhand_qty SET DEFAULT 0;
ALTER TABLE
=> ALTER TABLE books ALTER COLUMN onhand_qty SET NOT NULL;
ALTER TABLE
=> ALTER TABLE books ADD CHECK(onhand_qty >= 0);

Page 25: Page 25

ALTER TABLE

Создаем триггер:

=> CREATE TRIGGER update_onhand_qty_trigger
AFTER INSERT ON operations
FOR EACH ROW
EXECUTE FUNCTION update_onhand_qty();
CREATE TRIGGER

Готово.

=> COMMIT;
COMMIT

Теперь books.onhand_qty обновляется, но представление catalog_v по-прежнему вызывает функцию для подсчета количества. Хоть в исходном запросе обращение к функции синтаксически не отличается от обращения к полю, запрос был запомнен в другом виде:

=> \d+ catalog_v
                            View "bookstore.catalog_v"
    Column    |  Type   | Collation | Nullable | Default | Storage  | Description
--------------+---------+-----------+----------+---------+----------+-------------
 book_id      | integer |           |          |         | plain    |
 title        | text    |           |          |         | extended |
 onhand_qty   | integer |           |          |         | plain    |
 display_name | text    |           |          |         | extended |
 authors      | text    |           |          |         | extended |
View definition:
 SELECT book_id,
    title,
    onhand_qty(b.*) AS onhand_qty,
    book_name(book_id, title) AS display_name,
    authors(b.*) AS authors
   FROM books b
  ORDER BY (book_name(book_id, title));
Triggers:
    update_catalog_trigger INSTEAD OF UPDATE ON catalog_v FOR EACH ROW EXECUTE FUNCTION
update_catalog()

Поэтому пересоздадим представление:

=> CREATE OR REPLACE VIEW catalog_v AS
SELECT b.book_id,
       b.title,
       b.onhand_qty,
       book_name(b.book_id, b.title) AS display_name,
       b.authors
FROM   books b
ORDER BY display_name;
CREATE VIEW

Теперь функцию можно удалить.

=> DROP FUNCTION onhand_qty(books);
DROP FUNCTION

Небольшая проверка:

=> SELECT * FROM catalog_v WHERE book_id = 1 \gx
-[ RECORD 1 ]+------------------------------------
book_id      | 1
title        | Сказка о царе Салтане
onhand_qty   | 19
display_name | Сказка о царе Салтане. Пушкин А. С.
authors      | Пушкин Александр Сергеевич
=> INSERT INTO operations(book_id, qty_change) VALUES (1,+10);
INSERT 0 1
=> SELECT * FROM catalog_v WHERE book_id = 1 \gx

Page 26: Page 26

-[ RECORD 1 ]+------------------------------------
book_id      | 1
title        | Сказка о царе Салтане
onhand_qty   | 29
display_name | Сказка о царе Салтане. Пушкин А. С.
authors      | Пушкин Александр Сергеевич

Некорректные операции обрываются:

=> INSERT INTO operations(book_id, qty_change) VALUES (1,-100);
ERROR:  new row for relation "books" violates check constraint "books_onhand_qty_check"
DETAIL:  Failing row contains (1, Сказка о царе Салтане, -71).
CONTEXT:  SQL statement "UPDATE books
    SET onhand_qty = onhand_qty + NEW.qty_change
    WHERE book_id = NEW.book_id"
PL/pgSQL function update_onhand_qty() line 3 at SQL statement

Page 27: Практика+

  1. Напишите триггер, увеличивающий счетчик (поле version) на единицу при каждом изменении строки. При вставке новой строки счетчик должен устанавливаться в единицу. Проверьте правильность работы. 2. Даны таблицы заказов (orders) и строк заказов (lines). Требуется выполнить денормализацию: автоматически обновлять сумму заказа в таблице orders при изменении строк в заказе.
  • Создайте необходимые триггеры с использованием
  • переходных таблиц для минимизации операций обновления.

Комментарии преподавателя

  1. Для создания таблиц используйте команды:
CREATE TABLE orders (
    id int PRIMARY KEY,
    total_amount numeric(20,2) NOT NULL DEFAULT 0
);
CREATE TABLE lines (
   id int PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
   order_id int NOT NULL REFERENCES orders(id),
   amount numeric(20,2) NOT NULL
);

Столбец orders.total_amount должен автоматически вычисляться как сумма значений столбца lines.amount всех строк, относящихся к соответствующему заказу.

Page 28: 1. Счетчик номера версии

=> CREATE DATABASE plpgsql_triggers;
CREATE DATABASE
=> \c plpgsql_triggers
You are now connected to database "plpgsql_triggers" as user "student".

Таблица:

=> CREATE TABLE t(
    id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    s text,
    version integer
);
CREATE TABLE

Триггерная функция:

=> CREATE FUNCTION inc_version() RETURNS trigger
AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        NEW.version := 1;
    ELSE
        NEW.version := OLD.version + 1;
    END IF;
    RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Триггер:

=> CREATE TRIGGER t_inc_version
BEFORE INSERT OR UPDATE ON t
FOR EACH ROW EXECUTE FUNCTION inc_version();
CREATE TRIGGER

Проверяем:

=> INSERT INTO t(s) VALUES ('Раз');
INSERT 0 1
=> SELECT * FROM t;
 id |  s  | version
----+-----+---------
  1 | Раз |       1
(1 row)

Явное указание version игнорируется:

=> INSERT INTO t(s,version) VALUES ('Два',42);
INSERT 0 1
=> SELECT * FROM t;
 id |  s  | version
----+-----+---------
  1 | Раз |       1
  2 | Два |       1
(2 rows)

Изменение:

=> UPDATE t SET s = lower(s) WHERE id = 1;
UPDATE 1
=> SELECT * FROM t;

Page 29: Page 29

 id |  s  | version
----+-----+---------
  2 | Два |       1
  1 | раз |       2
(2 rows)

Явное указание также игнорируется:

=> UPDATE t SET s = lower(s), version = 42 WHERE id = 2;
UPDATE 1
=> SELECT * FROM t;
 id |  s  | version
----+-----+---------
  1 | раз |       2
  2 | два |       2
(2 rows)

2. Автоматическое вычисление общей суммы заказов

Создаем таблицы упрощенной структуры, достаточной для демонстрации:

=> CREATE TABLE orders (
    id integer PRIMARY KEY,
    total_amount numeric(20,2) NOT NULL DEFAULT 0
);
CREATE TABLE
=> CREATE TABLE lines (
   id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
   order_id integer NOT NULL REFERENCES orders(id),
   amount numeric(20,2) NOT NULL
);
CREATE TABLE

Создаем триггерную функцию и триггер для обработки вставки:

=> CREATE FUNCTION total_amount_ins() RETURNS trigger
AS $$
BEGIN
    WITH l(order_id, total_amount) AS (
        SELECT order_id, sum(amount)
        FROM new_table
        GROUP BY order_id
    )
    UPDATE orders o
    SET total_amount = o.total_amount + l.total_amount
    FROM l
    WHERE o.id = l.order_id;
    RETURN NULL;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Предложение FROM в команде UPDATE позволяет соединить orders с подзапросом по переходной таблице и использовать столбцы подзапроса для вычисления значения.

=> CREATE TRIGGER lines_total_amount_ins
AFTER INSERT ON lines
REFERENCING
    NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE FUNCTION total_amount_ins();
CREATE TRIGGER

Функция и триггер для обработки обновления:

Page 30: Page 30

=> CREATE FUNCTION total_amount_upd() RETURNS trigger
AS $$
BEGIN
    WITH l_tmp(order_id, amount) AS (
        SELECT order_id, amount FROM new_table
        UNION ALL
        SELECT order_id, -amount FROM old_table
    ), l(order_id, total_amount) AS (
        SELECT order_id, sum(amount)
        FROM l_tmp
        GROUP BY order_id
        HAVING sum(amount) <> 0
    )
    UPDATE orders o
    SET total_amount = o.total_amount + l.total_amount
    FROM l
    WHERE o.id = l.order_id;
    RETURN NULL;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Условие HAVING позволяет пропускать изменения, не влияющие на общую сумму заказа.

=> CREATE TRIGGER lines_total_amount_upd
AFTER UPDATE ON lines
REFERENCING
    OLD TABLE AS old_table
    NEW TABLE AS new_table
FOR EACH STATEMENT
EXECUTE FUNCTION total_amount_upd();
CREATE TRIGGER

Функция и триггер для обработки удаления:

=> CREATE FUNCTION total_amount_del() RETURNS trigger
AS $$
BEGIN
    WITH l(order_id, total_amount) AS (
        SELECT order_id, -sum(amount)
        FROM old_table
        GROUP BY order_id
    )
    UPDATE orders o
    SET total_amount = o.total_amount + l.total_amount
    FROM l
    WHERE o.id = l.order_id;
    RETURN NULL;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE TRIGGER lines_total_amount_del
AFTER DELETE ON lines
REFERENCING
    OLD TABLE AS old_table
FOR EACH STATEMENT
EXECUTE FUNCTION total_amount_del();
CREATE TRIGGER

Остался неохваченным оператор TRUNCATE. Однако триггер для этого оператора не может использовать переходные таблицы. Но мы знаем, что после выполнения TRUNCATE в lines не останется строк, значит можно обнулить суммы всех заказов.

=> CREATE FUNCTION total_amount_truncate() RETURNS trigger
AS $$
BEGIN
    UPDATE orders SET total_amount = 0;
    RETURN NULL;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE TRIGGER lines_total_amount_truncate
AFTER TRUNCATE ON lines
FOR EACH STATEMENT
EXECUTE FUNCTION total_amount_truncate();

Page 31: Page 31

CREATE TRIGGER

Дополнительно нужно запретить изменять значение total_amount вручную, но это задача решается не триггерами. Проверяем работу. Добавили два новых заказа без строк:

=> INSERT INTO orders VALUES (1), (2);
INSERT 0 2
=> SELECT * FROM orders ORDER BY id;
 id | total_amount
----+--------------
  1 |         0.00
  2 |         0.00
(2 rows)

Добавили строки в заказы:

=> INSERT INTO lines (order_id, amount) VALUES
    (1,100), (1,100), (2,500), (2,500);
INSERT 0 4
=> SELECT * FROM lines;
 id | order_id | amount
----+----------+--------
  1 |        1 | 100.00
  2 |        1 | 100.00
  3 |        2 | 500.00
  4 |        2 | 500.00
(4 rows)
=> SELECT * FROM orders ORDER BY id;
 id | total_amount
----+--------------
  1 |       200.00
  2 |      1000.00
(2 rows)

Удвоили суммы всех строк всех заказов:

=> UPDATE lines SET amount = amount * 2;
UPDATE 4
=> SELECT * FROM orders ORDER BY id;
 id | total_amount
----+--------------
  1 |       400.00
  2 |      2000.00
(2 rows)

Удалим одну строку первого заказа:

=> DELETE FROM lines WHERE id = 1;
DELETE 1
=> SELECT * FROM orders ORDER BY id;
 id | total_amount
----+--------------
  1 |       200.00
  2 |      2000.00
(2 rows)

Опустошим таблицу строк:

=> TRUNCATE lines;
TRUNCATE TABLE
=> SELECT * FROM orders ORDER BY id;

Page 32: Page 32

 id | total_amount
----+--------------
  1 |         0.00
  2 |         0.00
(2 rows)
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_triggers;
DROP DATABASE

18 Занятие 18: PL/pgSQL / Отладка

Исходная страница в нативном Markdown: PL/pgSQL / Отладка
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_18_plpgsql_debug.html Archive: DEV1-handouts-16.zip Страниц PDF: 28

Page 1: PL/pgSQL / Отладка

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Проверки корректности
  • Отладчик PL/pgSQL
  • Служебные сообщения и различные способы их реализации
  • Трассировка сеансов

Page 3: Проверки корректности

Проверки времени компиляции и времени выполнения

  • plpgsql.extra_warnings
  • plpgsql.extra_errors
  • дополнительные проверки в расширении plpgsql_check
  • Проверки в коде
    • команда ASSERT
  • Тестирование

Комментарии преподавателя

Отладка предполагает выполнение программы и изучение возникающих проблем, обычно с помощью специализированного отладчика или с помощью вывода отладочных сообщений.

Но можно заранее исключить определенные классы ошибок, включив проверку исходного кода во время компиляции и выполнения. Это управляется параметрами plpgsql.extra_warnings и plpgsql.extra_errors, как рассматривалось в теме «PL/pgSQL. Выполнение запросов».

https://postgrespro.ru/docs/postgresql/16/plpgsql-development-tips#PLPGS QL-EXTRA-CHECKS

Там же рассматривалось применение расширения plpgsql_check, которое обеспечивает более широкий спектр проверок.

Еще одна возможность обезопасить свой код — добавить в него проверки условий, которые всегда должны выполняться (т. н. «sanity check»). Удобный способ для этого — оператор ASSERT в PL/pgSQL.

https://postgrespro.ru/docs/postgresql/16/plpgsql-errors-and-messages#PL PGSQL-STATEMENTS-ASSERT

Нельзя не сказать и о важности тестирования кода. Тестирование не только позволяет изначально убедиться, что код работает так, как задумано, но и облегчает его дальнейшую модификацию — дает уверенность в том, что изменения не поломали существующий функционал. Мы не будем затрагивать эту тему; отметим только, что тестирование кода, работающего с базой данных, может оказаться непростым делом из-за необходимости подготовки тестовых примеров.

Page 4: Проверки корректности

=> CREATE DATABASE plpgsql_debug;
CREATE DATABASE
=> \c plpgsql_debug
You are now connected to database "plpgsql_debug" as user "student".

Команда ASSERT позволяет указать условия, нарушения которых являются непредвиденной ошибкой. Можно провести определенную аналогию между такими условиями и ограничениями целостности в базе данных. Пример: функция, возвращающее номер подъезда по номеру квартиры:

=> CREATE FUNCTION entrance(
    floors integer,
    flats_per_floor integer,
    flat_no integer
)
RETURNS integer
AS $$
BEGIN
    RETURN floor((flat_no - 1)::real / (floors * flats_per_floor)) + 1;
END
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE FUNCTION

Убедиться в правильности работы функции можно при помощи тестирования, проверив результат на некоторых «интересных» значениях:

=> SELECT entrance(9, 4, 1), entrance(9, 4, 36), entrance(9, 4, 37);
 entrance | entrance | entrance
----------+----------+----------
        1 |        1 |        2
(1 row)

Но при некорректных входных значениях функция будет выдавать бессмысленный результат, который, например, может быть передан дальше в другие подпрограммы, которые из-за этого тоже могут повести себя некорректно. Тестирование только кода данной функции здесь никак не поможет.

=> SELECT entrance(9, 4, 0);
 entrance
----------
        0
(1 row)

Можно обезопасить себя, добавив проверку:

=> CREATE OR REPLACE FUNCTION entrance(
    floors integer,
    flats_per_floor integer,
    flat_no integer
)
RETURNS integer
AS $$
BEGIN
    ASSERT floors > 0 AND flats_per_floor > 0 AND flat_no > 0,
        'Некорректные входные параметры';
    RETURN floor((flat_no - 1)::real / (floors * flats_per_floor)) + 1;
END
$$ LANGUAGE plpgsql IMMUTABLE;
CREATE FUNCTION
=> SELECT entrance(9, 4, 0);
ERROR:  Некорректные входные параметры
CONTEXT:  PL/pgSQL function entrance(integer,integer,integer) line 3 at ASSERT

Теперь некорректный вызов сразу же приведет к ошибке.

Page 5: PL/pgSQL Debugger

Состав

  • расширение pldbgapi
  • встроенная поддержка в некоторых графических средах
  • Возможности
    • установка точек прерывания
    • пошаговое выполнение
    • проверка и установка значений переменных
    • не требуется изменение кода
    • отладка работающих приложений

Комментарии преподавателя

PL/pgSQL Debugger — это отладчик для PL/pgSQL. Представляет собой расширение pldbgapi, которое официально поддерживается разработчиками PostgreSQL.

Расширение pldbgapi — это набор интерфейсных функций для сервера PostgreSQL, которые позволяют устанавливать точки прерывания, пошагово выполнять код программ, проверять и устанавливать значения переменных.

Исходный код отлаживаемых программ изменять не требуется, что дает возможность выполнять отладку работающих приложений. То есть процесс отладки не обязательно запускать отдельно, можно подключиться к уже работающему сеансу и начать его отладку.

Непосредственно пользоваться этими функциями неудобно, они в первую очередь предназначены для вызова из кода графических сред разработки. Некоторые из таких сред (включая DBeaver) имеют встроенный интерфейс для отладки. Но чтобы им воспользоваться, сначала все равно придется установить расширение pldbgapi в соответствующую базу данных.

Исходный код отладчика доступен по ссылке:

https://github.com/EnterpriseDB/pldebugger

Page 6: Отладка с PL/pgSQL Debugger

Выполним отладку одной из функций нашего приложения. Пакет поддержки отладчика postgresql-16-pldebugger уже установлен в виртуальной машине курса. Теперь нам нужно обеспечить загрузку разделяемой библиотеки, для этого установим параметр и перезапустим сервер:

=> ALTER SYSTEM SET shared_preload_libraries = 'plugin_debugger';
ALTER SYSTEM
student$ sudo pg_ctlcluster 16 main restart

Установим расширение отладчика в базу данных bookstore:

student$ psql bookstore
=> CREATE EXTENSION pldbgapi SCHEMA public;
CREATE EXTENSION

Отладку проведем с помощью графической утилиты DBeaver.

student$ /opt/dbeaver/dbeaver 2>/dev/null

Page 7: Служебные сообщения

Не только отладка кода

  • мониторинг долго выполняющихся процессов
  • ведение журнала приложения
  • Подходы к реализации
    • вывод на консоль или в журнал сервера
    • запись в таблицу или в файл
    • передача информации другим процессам

Комментарии преподавателя

Второй способ отладки предполагает добавление в важные места кода служебных сообщений, выводящих информацию о текущем контексте. Анализируя сообщения, можно понять, что именно пошло не так.

Помимо собственно отладки, служебные сообщения могут выполнять и другие функции. Сообщения помогут определить, на каком этапе выполнения находится долго выполняющийся процесс. По аналогии с журналом сообщений СУБД приложение может записывать в свой собственный журнал важную информацию. Например, данные о запуске отчетов: название отчета, пользователь, когда и с какими параметрами запускался и т. д. Такая информация может сильно облегчить работу специалистов поддержки.

Можно выделить несколько подходов к реализации служебных сообщений в PL/pgSQL. Помимо уже знакомой команды RAISE, позволяющей выводить сообщения на консоль (а также в журнал сообщений сервера), можно отправлять сообщения другому процессу, записывать сообщения в таблицу или в файл.

При выборе того или иного подхода нужно обращать внимание на многие нюансы. Являются ли сообщения транзакционными (отправляются не дожидаясь окончания транзакции или только при ее завершении)? Можно ли одновременно использовать в нескольких сеансах? Как организовать доступ к журналу сообщений и как очищать его от старых записей? Какое влияние оказывают журналирование на производительность? Требуется ли модификация исходного кода?

Page 8: Команда RAISE

Source slide page 8: Команда RAISE{#source-figure-008 caption=“Source slide page 8: Команда RAISE”}

DEBUG LOG NOTICE WARNING ERROR FATAL PANIC
 DEBUG
 LOG
 NOTICE
RAISE
 INFO
 WARNING
DEBUG INFO NOTICE WARNING ERROR LOG FATAL PANIC

Комментарии преподавателя

Мы уже встречались с командой RAISE. Она служит и для вызова исключительных ситуаций, что подробно рассматривалось в теме «PL/pgSQL. Обработка ошибок», и для отправки сообщений. Причем сообщения можно не только отправлять клиенту, но и записывать в журнал сервера.

В простом случае для отладки нужно добавить вызовы RAISE NOTICE в код функции, запустить функцию на выполнение (например в сеансе psql) и проанализировать получаемые по ходу выполнения сообщения. Сообщения RAISE нетранзакционные: они отправляются асинхронно и не зависят от статуса завершения транзакции.

Для управления отправкой сообщений используются уровень сообщения (DEBUG, LOG, NOTICE, INFO, WARNING) и параметры сервера. От значений параметров зависит, будет ли сообщение отправлено клиенту (client_min_messages) и/или записано в журнал сервера (log_min_messages). Сообщение будет отправлено, если уровень команды RAISE равен значению соответствующего параметра или больше его (находится правее на рисунке).

Значения параметров по умолчанию настроены так, что сообщения с уровнем NOTICE отправляются только клиенту, с уровнем LOG — только в журнал, а с уровнем WARNING — и клиенту, и в журнал.

Сообщения с уровнем INFO всегда отправляются клиенту, их нельзя перехватить параметром client_min_messages.

https://postgrespro.ru/docs/postgresql/16/plpgsql-errors-and-messages

Page 9: Команда RAISE

Создадим функцию для подсчета количества строк в таблице, имя которой передается во входном параметре.

student$ psql plpgsql_debug
=> CREATE FUNCTION get_count(tabname text) RETURNS bigint
AS $$
DECLARE
    cmd text;
    retval bigint;
BEGIN
    cmd := 'SELECT COUNT(*) FROM ' || quote_ident(tabname);
    RAISE NOTICE 'cmd: %', cmd;
    EXECUTE cmd INTO retval;
    RETURN retval;
END
$$ LANGUAGE plpgsql STABLE;
CREATE FUNCTION

Для динамического выполнения текст команды лучше предварительно записывать в переменную. В случае ошибки можно проанализировать значение этой переменной.

=> SELECT get_count('pg_class');
NOTICE:  cmd: SELECT COUNT(*) FROM pg_class
 get_count
-----------
       413
(1 row)

Строка, начинающаяся с «NOTICE» — наша отладочная информация. RAISE можно использовать для отслеживания хода выполнения долгого запроса. Предположим, что внутри кода, решающего некую задачу, четко выделяются три этапа выполнения. И по ходу работы подпрограммы мы хотим понимать, на каком из них находимся. Для этого на каждом этапе будем вызывать такую процедуру:

=> CREATE PROCEDURE debug_message(msg text)
AS $$
BEGIN
RAISE NOTICE '%', msg;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Основная процедура по завершении очередного этапа работы вызывает процедуру debug_message:

=> CREATE PROCEDURE long_running()
AS $$
BEGIN
    CALL debug_message('long_running. Stage 1/3...');
    PERFORM pg_sleep(2);
    CALL debug_message('long_running. Stage 2/3...');
    PERFORM pg_sleep(3);
    CALL debug_message('long_running. Stage 3/3...');
    PERFORM pg_sleep(1);
    CALL debug_message('long_running. Done');
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Команда RAISE выдает сообщения сразу, а не по окончании работы подпрограммы:

=> CALL long_running();
NOTICE:  long_running. Stage 1/3...
NOTICE:  long_running. Stage 2/3...
NOTICE:  long_running. Stage 3/3...
NOTICE:  long_running. Done
CALL

Page 10: Page 10

Такой подход удобен, когда можно вызвать функцию в отдельном сеансе. Если же функция вызывается из приложения, то удобнее писать и затем смотреть в журнал сервера. Перепишем процедуру debug_message для выдачи сообщения с уровнем, установленным в пользовательском параметре app.raise_level:

=> CREATE OR REPLACE PROCEDURE debug_message(msg text)
AS $$
BEGIN
    CASE current_setting('app.raise_level', true)
        WHEN 'NOTICE'  THEN RAISE NOTICE  '%, %, %', user, clock_timestamp(), msg;
        WHEN 'DEBUG'   THEN RAISE DEBUG   '%, %, %', user, clock_timestamp(), msg;
        WHEN 'LOG'     THEN RAISE LOG     '%, %, %', user, clock_timestamp(), msg;
        WHEN 'INFO'    THEN RAISE INFO    '%, %, %', user, clock_timestamp(), msg;
        WHEN 'WARNING' THEN RAISE WARNING '%, %, %', user, clock_timestamp(), msg;
        ELSE NULL; -- все прочие значения отключают вывод сообщений
    END CASE;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Для целей примера установим параметр на уровне сеанса:

=> SET app.raise_level TO 'NONE';
SET

Теперь в «обычной» жизни (app.raise_level = NONE) отладочные сообщения не будут выдаваться:

=> CALL long_running();
CALL

Запуская функцию в отдельном сеансе, мы можем получить отладочные сообщения, выставив app.raise_level в NOTICE:

=> SET app.raise_level TO 'NOTICE';
SET
=> CALL long_running();
NOTICE:  student, 2026-04-10 15:41:09.430016+03, long_running. Stage 1/3...
NOTICE:  student, 2026-04-10 15:41:11.431707+03, long_running. Stage 2/3...
NOTICE:  student, 2026-04-10 15:41:14.432782+03, long_running. Stage 3/3...
NOTICE:  student, 2026-04-10 15:41:15.433706+03, long_running. Done
CALL

Если же мы хотим включить отладку в приложении с записью в журнал сервера, то переключаем app.raise_level в LOG:

=> SET app.raise_level TO 'LOG';
SET
=> CALL long_running();
CALL

Смотрим в журнал сервера:

student$ tail -n 30 /var/log/postgresql/postgresql-16-main.log | grep 'long_running\.'
2026-04-10 15:41:15.563 MSK [127012] student@plpgsql_debug LOG:  student, 2026-04-10
15:41:15.563761+03, long_running. Stage 1/3...
SQL statement "CALL debug_message('long_running. Stage 1/3...')"
2026-04-10 15:41:17.565 MSK [127012] student@plpgsql_debug LOG:  student, 2026-04-10
15:41:17.565786+03, long_running. Stage 2/3...
SQL statement "CALL debug_message('long_running. Stage 2/3...')"
2026-04-10 15:41:20.568 MSK [127012] student@plpgsql_debug LOG:  student, 2026-04-10
15:41:20.56873+03, long_running. Stage 3/3...
SQL statement "CALL debug_message('long_running. Stage 3/3...')"
2026-04-10 15:41:21.569 MSK [127012] student@plpgsql_debug LOG:  student, 2026-04-10
15:41:21.569739+03, long_running. Done
SQL statement "CALL debug_message('long_running. Done')"

Управляя параметрами app.raise_level, log_min_messages и client_min_messages, можно добиться различного поведения при выводе отладочных сообщений.

Примечание

Note Важно, что для этого не нужно менять код приложения.

Page 11: Процесс→процесс (IPC)

NOTIFY → LISTEN

  • команды SQL
  • транзакционное выполнение, неудобно для отладки
  • Статус сеанса
    • параметр application_name
    • виден в представлении pg_stat_activity и выводе команды ps
    • можно использовать в журнальных сообщениях

Комментарии преподавателя

Серверные процессы в PostgreSQL могут обмениваться информацией между собой.

Среди встроенных решений можно отметить следующие.

● Использование команд NOTIFY для отправки сообщений в одном процессе и LISTEN для получения в другом. Но эти команды являются транзакционными, поэтому они неудобны для отладки: 1. Сообщения отправляются только при фиксации транзакции, а не сразу после выполнения NOTIFY. Поэтому невозможно следить за ходом процесса. 2. Если транзакция завершится неуспешно, то сообщения вообще не будут отправлены.

● Использование параметра application_name. Сеанс с долго выполняющимся процессом может периодически записывать статус выполнения в application_name. В другом сеансе администратор может опрашивать представление pg_stat_activity, содержащее подробную информацию о всех выполняющихся сеансах. Также значение application_name обычно видно в выводе команды ps. Значение application_name также можно записывать в журнал сервера (настраивая параметр log_line_prefix). Это облегчит поиск нужных строк в журнале.

https://postgrespro.ru/docs/postgresql/16/runtime-config-logging#RUNTIME-CONFIG-LOGGING-WHAT

Page 12: Статус сеанса

Посмотрим, как использовать для отладки параметр application_name. Первый сеанс меняет значение этого параметра, второй — периодически опрашивает представление pg_stat_activity.

=> \c plpgsql_debug
You are now connected to database "plpgsql_debug" as user "student".

Новый вариант процедуры:

=> CREATE OR REPLACE PROCEDURE debug_message(msg text)
AS $$
BEGIN
PERFORM set_config('application_name', format('%s', msg), /* is_local */ true);
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Запускаем в первом сеансе:

=> CALL long_running();

Во втором с паузой в две секунды обновляем строку из pg_stat_activity:

=> SELECT pid, usename, application_name
FROM pg_stat_activity
WHERE datname = 'plpgsql_debug' AND pid <> pg_backend_pid();
  pid   | usename |      application_name
--------+---------+----------------------------
 127012 | student | long_running. Stage 1/3...
(1 row)
=> \g
  pid   | usename |      application_name
--------+---------+----------------------------
 127012 | student | long_running. Stage 2/3...
(1 row)
=> \g
  pid   | usename |      application_name
--------+---------+----------------------------
 127012 | student | long_running. Stage 3/3...
(1 row)
=> \g
  pid   | usename | application_name
--------+---------+------------------
 127012 | student | psql
(1 row)
CALL

Page 13: Процесс→таблица

Расширение dblink

  • входит в состав сервера
  • накладные расходы на создание соединения
  • Автономные транзакции
    • Postgres Pro Enterprise

Комментарии преподавателя

Еще один способ сохранения отладочных сообщений — запись в таблицу базы данных.

К плюсам данного подхода относится то, что параллельная работа и доступ журналу обеспечиваются средствами самой СУБД.

Однако нужно позаботиться о том, чтобы вставка в таблицу была нетранзакционной. Для этого можно использовать расширение dblink, идущее в составе сервера PostgreSQL. Это расширение позволяет открыть новое соединение к той же самой базе данных, поэтому вставка в таблицу выполняется в другой — независимой — транзакции.

К минусам данного подхода относится то, что открытие нового соединения требует дополнительных ресурсов сервера.

Подробнее работа с dblink рассматривается в курсе DEV2.

https://postgrespro.ru/docs/postgresql/16/dblink

В Postgres Pro Enterprise доступен механизм автономных транзакций, не имеющий таких существенных накладных расходов, которые характерны для dblink (см. курс PGPRO «Возможности Postgres Pro Enterprise»).

Page 14: Запись в таблицу: расширение dblink

Установим расширение:

=> CREATE EXTENSION dblink;
CREATE EXTENSION

Создаем таблицу для записи сообщений. В таблице полезно сохранять информацию о пользователе и времени вставки. Столбец id нужен для гарантированной сортировки результата в порядке добавления строк.

=> CREATE TABLE log (
    id       integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    username text,
    ts       timestamptz,
    message  text
);
CREATE TABLE

Теперь перепишем процедуру так, чтобы она добавляла записи в таблицу log. Процедура открывает новый сеанс, выполняет вставку в отдельной транзакции и закрывает сеанс.

=> CREATE OR REPLACE PROCEDURE debug_message(msg text)
AS $$
DECLARE
    cmd text;
BEGIN
    cmd := format(
        'INSERT INTO log (username, ts, message)
         VALUES (%L, %L::timestamptz, %L)',
        user, clock_timestamp()::text, debug_message.msg
    );
    PERFORM dblink('dbname=' || current_database(), cmd);
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Для проверки запустим процедуру long_running в отдельной транзакции, которую в конце откатим.

=> BEGIN;
BEGIN
=> CALL long_running();
CALL
=> ROLLBACK;
ROLLBACK

Убедимся, что в таблице сохранились все вызовы debug_message. По значениям ts можно проверить, сколько времени прошло между вызовами.

=> SELECT username, to_char(ts, 'HH24:MI:SS') as ts, message
FROM log
ORDER BY id;
 username |    ts    |          message
----------+----------+----------------------------
 student  | 15:41:30 | long_running. Stage 1/3...
 student  | 15:41:32 | long_running. Stage 2/3...
 student  | 15:41:35 | long_running. Stage 3/3...
 student  | 15:41:36 | long_running. Done
(4 rows)

Page 15: Процесс→файл

Расширение adminpack

  • входит в состав сервера
  • в том числе позволяет записывать текстовые файлы
  • Недоверенные языки
    • например, PL/Perl

Комментарии преподавателя

Записывать сообщения можно и в файл операционной системы.

Для этого, например, можно использовать расширение adminpack, позволяющее записывать данные в любой файл, доступ к которому есть у пользователя ОС postgres.

Другой вариант — написать функцию на недоверенном языке (таком, как PL/Perl — plperlu), которая будет выполнять ту же задачу. Различные языки серверного программирования рассматриваются в курсе DEV2.

https://postgrespro.ru/docs/postgresql/16/adminpack

Page 16: Запись в файл: pg_file_write

Установим расширение:

=> CREATE EXTENSION adminpack;
CREATE EXTENSION

В очередной раз перепишем процедуру debug_message, теперь она будет записывать отладочную информацию в файл. Пользователь postgres, запустивший экземпляр СУБД, должен иметь возможность записи в этот файл, поэтому расположим его в домашнем каталоге этого пользователя.

=> CREATE OR REPLACE PROCEDURE debug_message(msg text)
AS $$
DECLARE
    filename CONSTANT text := '/var/lib/postgresql/log.txt';
    message text;
BEGIN
    message := format(E'%s, %s, %s\n',
        session_user, clock_timestamp()::text, debug_message.msg
    );
    PERFORM pg_file_write(filename, message, /* append */ true);
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Функция записывает отдельной строкой в файл журнала сообщение, переданное параметром, вместе с информацией о том, кто и когда записал строку. Для проверки запустим long_running в отдельной транзакции, которую в конце откатим.

=> BEGIN;
BEGIN
=> CALL long_running();
CALL
=> ROLLBACK;
ROLLBACK

Проверим, что записи в журнале появились. Чтобы пользователь student мог получить доступ к этому файлу, нужно использовать команду sudo:

student$ sudo cat /var/lib/postgresql/log.txt
student, 2026-04-10 15:41:36.699417+03, long_running. Stage 1/3...
student, 2026-04-10 15:41:38.70172+03, long_running. Stage 2/3...
student, 2026-04-10 15:41:41.704727+03, long_running. Stage 3/3...
student, 2026-04-10 15:41:42.705762+03, long_running. Done

Page 17: Трассировка SQL

Стандартная трассировка в журнал сообщений

  • накладные расходы на запись в журнал
  • большой размер файла журнала
  • требуются инструменты для анализа
  • нужен доступ к журналу (безопасность)
  • Настройки
    • долго выполняющиеся команды
    • log_min_duration_statement
    • какие команды записывать
    • log_statement
    • контекст сообщения
    • log_line_prefix
    • …

Комментарии преподавателя

В некоторых случаях может оказаться полезной трассировка всего происходящего в коде. Штатными средствами можно получить в журнале сообщений сервера SQL-запросы сеансов. Надо учитывать:

● Высоконагруженное приложение может выполнять огромное число запросов. Их запись в файл журнала может оказывать влияние на производительность подсистемы ввода-вывода.

● В большинстве случаев требуются специальные инструменты для анализа такого объема данных. Стандарт де-факто — pgBadger. https://github.com/darold/pgbadger

● К файлу журнала на сервере может не быть доступа у разработчи-ков приложений. К тому же в журнале производственной системы могут оказаться команды, содержащие конфиденциальную информацию.

Для настройки трассировки имеется ряд параметров, основные из которых приведены на слайде. Полный список:

https://postgrespro.ru/docs/postgresql/16/runtime-config-logging

Необязательно устанавливать конфигурационные параметры для всего кластера. Их действие можно ограничить отдельными сеансами при помощи команд SET, ALTER DATABASE, ALTER ROLE (как рассказы-валось в темах «Обзор базового инструментария. Установка и управление, psql» и «Организация данных. Логическая структура»).

Page 18: Трассировка SQL-команд

Расширение auto\_explain

  • запись в журнал планов выполнения запросов
  • трассировка вложенных запросов
  • Настройки
    • планы долгих команд
    • auto_explain.log_min_duration
    • вложенные запросы
    • auto_explain.log_nested_statements
    • …

Комментарии преподавателя

При включении трассировки SQL-команды попадают в журнал в том виде, в каком они отправлены на сервер. Если была вызвана подпрограмма PL/pgSQL, то в журнал будет записан только вызов этой подпрограммы (например, оператор SELECT или CALL), но не будет тех команд, которые выполняются внутри подпрограммы.

Чтобы увидеть в журнале запросы не только верхнего уровня, но и вложенные, потребуется штатное расширение auto_explain.

Как следует из названия расширения, его основная задача — записывать в журнал не только текст команды, но и план ее выполнения. Это тоже может оказаться полезно, хотя относится не к трассировке как таковой, а к оптимизации запросов (которая рассматривается в курсе QPT).

https://postgrespro.ru/docs/postgresql/16/auto-explain

Page 19: Трассировка PL/pgSQL

Расширение plpgsql\_check

  • накладные расходы на запись сообщений
  • большой объем выдачи
  • Основные настройки
    • включение трассировки
    • plpgsql_check.enable_tracer
    • plpgsql_check.tracer
    • уровень сообщений
    • plpgsql_check.tracer_errlevel

Комментарии преподавателя

Чтобы разобраться по журналу, какой процедурный код выполнялся, придется сопоставить SQL-запросы с подпрограммами на PL/pgSQL, а это может оказаться непросто. Стандартной возможности для трассировки PL/pgSQL-кода не предусмотрено, но расширение Павла Стехуле plpgsql_check (которое упоминалось в теме «PL/pgSQL. Выполнение запросов») позволяет ее выполнить.

Подобная трассировка вызывает большие накладные расходы и должна использоваться только для отладки, но не в промышленной эксплуатации.

https://github.com/okbob/plpgsql\_check

Page 20: Трассировка сеансов

Простой пример включения трассировки — установка параметра log_statement в значение all (записывать все команды, включая DDL, модификацию данных и запросы).

=> SET log_statement = 'all';
SET

Выполним какой-нибудь запрос:

=> SELECT get_count('pg_views');
NOTICE:  cmd: SELECT COUNT(*) FROM pg_views
 get_count
-----------
       141
(1 row)

И выключим трассировку:

=> RESET log_statement;
RESET

Информация о выполненных командах окажется в журнале сервера:

student$ tail -n 2 /var/log/postgresql/postgresql-16-main.log
2026-04-10 15:41:43.089 MSK [127012] student@plpgsql_debug LOG:  statement: SELECT
get_count('pg_views');
2026-04-10 15:41:43.229 MSK [127012] student@plpgsql_debug LOG:  statement: RESET
log_statement;

Однако в журнал попадает только команда верхнего уровня, но не запрос внутри функции get_count. Воспользуемся расширением auto_explain. Это расширение не нужно устанавливать в базу данных, но требуется загрузить в память. Загрузку можно настроить для всего экземпляра с помощью параметра shared_preload_libraries, либо проделать однократно для текущего процесса:

=> LOAD 'auto_explain';
LOAD

Установим трассировку всех команд независимо от длительности выполнения:

=> SET auto_explain.log_min_duration = 0;
SET

Включим трассировку вложенных запросов:

=> SET auto_explain.log_nested_statements = on;
SET

Сообщения выводятся с помощью того же механизма, что использует команда RAISE. По умолчанию используется уровень LOG, что обычно соответствует выводу в журнал. Изменив параметр, можно получать трассировку непосредственно в консоли:

=> SET auto_explain.log_level = 'NOTICE';
SET

Повторим запрос:

=> SELECT get_count('pg_views');

Page 21: Page 21

NOTICE:  cmd: SELECT COUNT(*) FROM pg_views
NOTICE:  duration: 0.098 ms  plan:
Query Text: SELECT COUNT(*) FROM pg_views
Aggregate  (cost=19.52..19.53 rows=1 width=8)
  ->  Seq Scan on pg_class c  (cost=0.00..19.16 rows=141 width=0)
        Filter: (relkind = 'v'::"char")
NOTICE:  duration: 0.497 ms  plan:
Query Text: SELECT get_count('pg_views');
Result  (cost=0.00..0.26 rows=1 width=8)
 get_count
-----------
       141
(1 row)

Мы видим не только вызов функции, но и вложенный запрос вместе с планами выполнения.

Page 22: Итоги

PL/pgSQL Debugger — API отладчика, используется в графических средах разработки

  • Служебные сообщения — вывод на консоль, запись в журнал сообщений сервера, в таблицу или в файл, передача другим процессам
  • Возможность трассировки сеансов

Page 23: Практика

  1. Измените функцию get_catalog так, чтобы динамически формируемый текст запроса записывался в журнал сообщений сервера.
  • В приложении выполните несколько раз поиск, заполняя разные поля, и убедитесь, что команды SQL формируются правильно. 2. Включите трассировку команд SQL на уровне сервера.
    • Поработайте в приложении и проверьте, какие команды
    • попадают в журнал сообщений.
    • Выключите трассировку.

Комментарии преподавателя

  1. Для включения трассировки установите значение параметра log_min_duration_statement в 0 и перечитайте конфигурацию. В журнал будут записываться все команды и время их выполнения.

Проще всего это сделать командой ALTER SYSTEM SET. Другие способы рассматривались в теме «Обзор базового инструментария. Установка и управление, psql». Не забудьте перечитать измененный конфигурационный файл.

После просмотра журнала следует вернуть значение параметра log_min_duration_statement в значение по умолчанию (−1), чтобы отключить трассировку. Удобный способ — команда ALTER SYSTEM RESET.

Page 24: 1. Функция get_catalog

Текст динамического запроса формируем в отдельной переменной, которую перед выполнением запишем в журнал сервера. Для более полной информации включим в сообщение значения переданных в функцию параметров. Отладочные строки в журнале можно найти по подстроке «DEBUG get_catalog». После отладки команду RAISE LOG можно удалить или закомментировать.

=> CREATE OR REPLACE FUNCTION get_catalog(
    author_name text,
    book_title text,
    in_stock boolean
)
RETURNS TABLE(book_id integer, display_name text, onhand_qty integer)
AS $$
DECLARE
    title_cond text := '';
    author_cond text := '';
    qty_cond text := '';
    cmd text := '';
BEGIN
    IF book_title != '' THEN
        title_cond := format(
            ' AND cv.title ILIKE %L', '%'||book_title||'%'
        );
    END IF;
    IF author_name != '' THEN
        author_cond := format(
            ' AND cv.authors ILIKE %L', '%'||author_name||'%'
        );
    END IF;
    IF in_stock THEN
        qty_cond := ' AND cv.onhand_qty > 0';
    END IF;
    cmd := '
        SELECT cv.book_id,
               cv.display_name,
               cv.onhand_qty
        FROM   catalog_v cv
        WHERE  true'
        || title_cond || author_cond || qty_cond || '
        ORDER BY display_name';
    RAISE LOG 'DEBUG get_catalog (%, %, %): %',
        author_name, book_title, in_stock, cmd;
    RETURN QUERY EXECUTE cmd;
END
$$ STABLE LANGUAGE plpgsql;
CREATE FUNCTION

2. Включение и выключение трассировки SQL-запросов

Чтобы включить трассировку всех запросов на уровне сервера, можно выполнить:

=> ALTER SYSTEM SET log_min_duration_statement = 0;
ALTER SYSTEM
=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Чтобы выключить:

=> ALTER SYSTEM RESET log_min_duration_statement;
ALTER SYSTEM
=> SELECT pg_reload_conf();

Page 25: Page 25

 pg_reload_conf
----------------
 t
(1 row)

Последние две команды попали в журнал сообщений:

student$ tail -n 6 /var/log/postgresql/postgresql-16-main.log
2026-04-10 15:47:11.833 MSK [158335] LOG:  received SIGHUP, reloading configuration files
2026-04-10 15:47:11.836 MSK [158335] LOG:  parameter "log_min_duration_statement" changed
to "0"
2026-04-10 15:47:11.987 MSK [161369] student@bookstore LOG:  duration: 13.663 ms
statement: ALTER SYSTEM RESET log_min_duration_statement;
2026-04-10 15:47:12.044 MSK [161369] student@bookstore LOG:  duration: 0.169 ms
statement: SELECT pg_reload_conf();
2026-04-10 15:47:12.044 MSK [158335] LOG:  received SIGHUP, reloading configuration files
2026-04-10 15:47:12.045 MSK [158335] LOG:  parameter "log_min_duration_statement" removed
from configuration file, reset to default

Page 26: Практика+

1. Включите трассировку PL/pgSQL-кода средствами расширения plpgsql\_check и проверьте ее работу на примере нескольких подпрограмм, вызывающих одна другую. 2. При выводе отладочных сообщений из PL/pgSQL-кода важно понимать, к какой подпрограмме они относятся. В демонстрации имя функции выводилось вручную. Реализуйте функционал, автоматически добавляющий к тексту сообщений имя текущей функции или процедуры.

Комментарии преподавателя

  1. Для включения трассировки загрузите расширение plpgsql_check в память сеанса командой LOAD, затем установите в сеансе оба параметра plpgsql_check.enable_tracer и plpgsql_check.tracer в значение «on».

  2. Имя подпрограммы можно получить, разобрав стек вызовов. Воспользуйтесь подходом из практического задания 3 к теме «Обработка ошибок».

Page 27: 1. Трассировка с помощью plpgsql_check

=> CREATE DATABASE plpgsql_debug;
CREATE DATABASE
=> \c plpgsql_debug
You are now connected to database "plpgsql_debug" as user "student".

Загрузим расширение (в данном случае устанавливать его в базу данных командой CREATE EXTENSION не нужно):

=> LOAD 'plpgsql_check';
LOAD

Включим трассировку:

=> SET plpgsql_check.enable_tracer = on;
SET
=> SET plpgsql_check.tracer = on;
SET

Несколько функций, вызывающих друг друга:

=> CREATE FUNCTION foo(n integer) RETURNS integer
AS $$
BEGIN
    RETURN bar(n-1);
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE FUNCTION bar(n integer) RETURNS integer
AS $$
BEGIN
    RETURN baz(n-1);
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE FUNCTION baz(n integer) RETURNS integer
AS $$
BEGIN
    RETURN n;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION

Пример работы трассировки:

=> SELECT foo(3);
NOTICE:  #0   ->> start of function foo(integer) (oid=16387, tnl=1)
NOTICE:  #0       "n" => '3'
NOTICE:  #1     ->> start of function bar(integer) (oid=16388, tnl=1)
NOTICE:  #1         context: PL/pgSQL function foo(integer) line 3 at RETURN
NOTICE:  #1         "n" => '2'
NOTICE:  #2       ->> start of function baz(integer) (oid=16389, tnl=1)
NOTICE:  #2           context: PL/pgSQL function bar(integer) line 3 at RETURN
NOTICE:  #2           "n" => '1'
NOTICE:  #2       <<- end of function baz (elapsed time=0.025 ms)
NOTICE:  #1     <<- end of function bar (elapsed time=0.156 ms)
NOTICE:  #0   <<- end of function foo (elapsed time=1.280 ms)
 foo
-----
   1
(1 row)

Выводятся не только события начала и окончания работы функций, но и значения параметров, а также затраченное на выполнение время (в расширении есть и возможность профилирования, которую мы не рассматриваем). Выключим трассировку:

Page 28: 2. Имя функции в отладочных сообщениях

=> SET plpgsql_check.tracer = off;
SET

Напишем процедуру, которая выводит верхушку стека вызовов (за исключением самой процедуры трассировки). Сообщение выводится с отступом, который соответствует глубине стека.

=> CREATE PROCEDURE raise_msg(msg text)
AS $$
DECLARE
    ctx text;
    stack text[];
BEGIN
    GET DIAGNOSTICS ctx := PG_CONTEXT;
    stack := regexp_split_to_array(ctx, E'\n');
    RAISE NOTICE '%: %',
        repeat('. ', array_length(stack,1)-2) || stack[3], msg;
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE

Пример работы трассировки:

=> CREATE TABLE t(n integer);
CREATE TABLE
=> CREATE FUNCTION on_insert() RETURNS trigger
AS $$
BEGIN
    CALL raise_msg('NEW = '||NEW::text);
    RETURN NEW;
END
$$ LANGUAGE plpgsql;
CREATE FUNCTION
=> CREATE TRIGGER t_before_row
BEFORE INSERT ON t
FOR EACH ROW
EXECUTE FUNCTION on_insert();
CREATE TRIGGER
=> CREATE PROCEDURE insert_into_t()
AS $$
BEGIN
    CALL raise_msg('start');
    INSERT INTO t SELECT id FROM generate_series(1,3) id;
    CALL raise_msg('end');
END
$$ LANGUAGE plpgsql;
CREATE PROCEDURE
=> CALL insert_into_t();
NOTICE:  . PL/pgSQL function insert_into_t() line 3 at CALL: start
NOTICE:  . . . PL/pgSQL function on_insert() line 3 at CALL: NEW = (1)
NOTICE:  . . . PL/pgSQL function on_insert() line 3 at CALL: NEW = (2)
NOTICE:  . . . PL/pgSQL function on_insert() line 3 at CALL: NEW = (3)
NOTICE:  . PL/pgSQL function insert_into_t() line 5 at CALL: end
CALL
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE plpgsql_debug;
DROP DATABASE

19 Занятие 19: Управление доступом / Обзор

Исходная страница в нативном Markdown: Управление доступом / Обзор
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_19_access_overview.html Archive: DEV1-handouts-16.zip Страниц PDF: 30

Page 1: Управление доступом / Обзор

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Роли и атрибуты
  • Подключение к серверу
  • Парольная аутентификация
  • Привилегии и управление ими
  • Категории ролей
  • Групповые и предопределенные роли
  • Привилегии по умолчанию
  • Привилегии и подпрограммы

Page 3: Роли и атрибуты

Роль может быть пользователем СУБД

  • не связана с пользователем ОС
  • Роли можно включать друг в друга
    • удобно при настройке доступа
  • Свойства роли определяются атрибутами
    • LOGIN
    • возможность подключения
    • SUPERUSER
    • суперпользователь
    • CREATEDB
    • возможность создавать базы данных
    • CREATEROLE
    • возможность создавать роли
    • и другие

Комментарии преподавателя

Роли в PostgreSQL используют для двух целей. Во-первых, роль может выступать как пользователь, подключающийся к СУБД. Во-вторых, роли можно включать друг в друга — это удобно при настройке доступа.

Формально роли никак не связаны с пользователями операционной системы, хотя многие программы это предполагают, выбирая значения по умолчанию. Например, psql, запущенный от имени пользователя ОС student, автоматически использует одноименную роль student (если в ключах утилиты не указана какая-либо другая роль).

При создании кластера определяется одна начальная роль, имеющая суперпользовательский доступ (обычно она называется postgres). В дальнейшем роли можно создавать, изменять и удалять.

https://postgrespro.ru/docs/postgresql/16/database-roles

Роль обладает некоторыми атрибутами, определяющими ее общие особенности и права (не связанные с определенными объектами).

Обычно атрибуты имеют два варианта, например, CREATEDB (дает право на создание БД) и NOCREATEDB (не дает такого права).

Если у роли есть атрибут LOGIN, эта роль — пользователь. Если NOLOGIN — роль не является пользовательской и не сможет подключиться к серверу; такие роли обычно используют для включения других ролей.

В таблице перечислены лишь некоторые из атрибутов.

https://postgrespro.ru/docs/postgresql/16/role-attributes

https://postgrespro.ru/docs/postgresql/16/sql-createrole

Page 4: Роли и атрибуты

Создадим роль для пользователя Алисы. В команде указаны два атрибута. В этой теме нам важно, от имени какой роли выполняются команды, поэтому имя текущей роли вынесено в приглашение.

student=# CREATE ROLE alice LOGIN PASSWORD 'alice';
CREATE ROLE

Список ролей можно узнать командой:

student=# \du
                             List of roles
 Role name |                         Attributes
-----------+------------------------------------------------------------
 alice     |
 postgres  | Superuser, Create role, Create DB, Replication, Bypass RLS
 student   | Superuser

Обратите внимание, что роль student является суперпользователем. Поэтому до сих пор мы не задумывались о разграничении доступа. Создадим и базу данных:

student=# CREATE DATABASE access_overview;
CREATE DATABASE
student=# \c access_overview
You are now connected to database "access_overview" as user "student".

Page 5: Подключение к серверу

1. Строки pg\_hba.conf просматриваются сверху вниз

    1. Выбирается первая запись, соответствующая параметрам подключения (тип, база, пользователь и адрес)
# TYPE        DATABASE     USER             ADDRESS           METHOD
local         all          postgres                           peer
local         all          all                                peer
host          all          all              127.0.0.1/32      scram-sha-256
host          all          all              ::1/128           scram-sha-256
  • local — сокет all — любая роль host — TCP/IP имя роли
    • listen_addresses
    • all — любая БД
    • all — любой IP
    • имя БД
    • IP/маска
    • доменное имя

Комментарии преподавателя

Для каждого нового клиента сервер определяет, надо ли разрешить подключение к базе данных. Настройки подключения определяются в конфигурационном файле pg_hba.conf («host-based authentication»). Как и с основным конфигурационным файлом postgresql.conf, изменения вступают в силу только после перечитывания файла сервером (SQL-функцией pg_reload_conf() или командой reload утилиты управления).

При появлении нового клиента сервер просматривает конфигурацион-ный файл сверху вниз в поисках строки, подходящей к запрашиваемо-му клиентом подключению. Соответствие определяется по четырем полям: типу подключения, имени БД, имени пользователя и IP-адресу.

Ниже перечислены только самые основные возможности.

Подключение — local (unix-сокеты) или host (подключение по протоколу TCP/IP).

База данных – ключевое слово all (соответствует любой БД) или имя конкретной базы данных.

Пользователь — all или имя конкретной роли.

Адрес — all, диапазон IP-адресов или доменное имя. Не указывается для типа local. По умолчанию PostgreSQL слушает входящие соединения только с localhost; обычно параметр listen_addresses ставят в значение «*» (слушать все интерфейсы) и дальше регулируют доступ средствами pg_hba.conf.

https://postgrespro.ru/docs/postgresql/16/client-authentication

Page 6: Подключение к серверу

3. Выполняется аутентификация указанным методом

    1. Удачно — доступ разрешается, иначе — запрещается (если не подошла ни одна запись — доступ запрещается)
# TYPE        DATABASE     USER             ADDRESS           METHOD
local         all          postgres                           peer
local         all          all                                peer
host          all          all              127.0.0.1/32      scram-sha-256
host          all          all              ::1/128           scram-sha-256
  • trust — разрешить
  • reject — отказать
  • scram-sha-256 и md5 — запросить пароль
  • peer — спросить ОС

Комментарии преподавателя

Когда в файле найдена подходящая строка, выполняется аутентификация указанным в этой строке методом, а также проверяется наличие атрибута LOGIN и привилегии CONNECT. Если результат проверки успешен, то подключение разрешается, иначе — запрещается (другие строки файла при этом уже не рассматриваются).

Если ни одна из строк не подошла, то доступ также запрещается.

Таким образом, записи в файле должны идти сверху вниз от частных правил к общим.

Существует множество методов аутентификации:

https://postgrespro.ru/docs/postgresql/16/auth-methods

Ниже перечислены только самые основные.

Метод trust безусловно разрешает подключение. Если вопросы безопасности не важны, можно указать «все all» и метод trust — тогда будут разрешены все подключения.

Метод reject, наоборот, безусловно запрещает подключение.

Метод scram-sha-256 запрашивает у пользователя пароль и проверяет его соответствие хешу, который хранится в системном каталоге кластера. Парольный метод md5 также используется, но объявлен устаревшим.

Метод peer запрашивает имя пользователя у операционной системы и разрешает подключение, если имя пользователя ОС и пользователя БД совпадают (можно установить и другие соответствия имен).

Page 7: Парольная аутентификация

На сервере

  • пароль устанавливается при создании роли или позже
  • пользователю без пароля будет отказано в доступе
  • пароль хранится в системном каталоге pg_authid
  • Ввод пароля на клиенте
    • вручную
    • из переменной окружения PGPASSWORD
    • из файла ~/.pgpass (строки в формате узел:порт:база:роль:пароль)

Комментарии преподавателя

Если используется аутентификация по паролю, для роли обязательно должен быть установлен пароль, иначе в доступе будет отказано.

Хеш-код пароля хранится в системном каталоге в таблице pg_authid.

Пользователь может вводить пароль вручную, а может автоматизировать его ввод. Для этого есть две возможности.

Во-первых, можно задать пароль в переменной окружения PGPASSWORD на клиенте. Однако это неудобно, если приходится подключаться к нескольким базам, а также не рекомендуется из соображений безопасности.

Во-вторых, можно задать пароли в файле ~/.pgpass на клиенте. К файлу должен иметь доступ только его владелец (установлены права доступа 600), иначе PostgreSQL проигнорирует его.

Page 8: Подключение

Чтобы роль смогла подключиться к базе данных, она должна иметь не только атрибут LOGIN, но и разрешение в файле pg_hba.conf. Располагается он обычно рядом с основным конфигурационным файлом:

student=# SHOW hba_file;
              hba_file
-------------------------------------
 /etc/postgresql/16/main/pg_hba.conf
(1 row)

А прочитать его содержимое можно прямо из SQL:

student=# SELECT type, database, user_name, address, auth_method
FROM pg_hba_file_rules();
 type  |   database    | user_name |  address  |  auth_method
-------+---------------+-----------+-----------+---------------
 local | {all}         | {all}     |           | trust
 host  | {all}         | {all}     | 127.0.0.1 | scram-sha-256
 host  | {all}         | {all}     | ::1       | scram-sha-256
 local | {replication} | {all}     |           | trust
 host  | {replication} | {all}     | 127.0.0.1 | scram-sha-256
 host  | {replication} | {all}     | ::1       | scram-sha-256
(6 rows)

(В зависимости от сборки сервера содержимое файла может отличаться.) Мы будем использовать подключение к localhost по TCP/IP (host). Такому подключению соответствует вторая строка выборки. Она предполагает аутентификацию по паролю. Роль alice была создана с паролем, но вообще его можно изменить в любой момент:

student=# ALTER ROLE alice PASSWORD 'alicepass';
ALTER ROLE

Попробуем установить соединение, указав в строке подключения всю необходимую информацию:

student$ psql 'host=localhost user=alice dbname=access_overview password=alicepass'
alice=> \conninfo
You are connected to database "access_overview" as user "alice" on host "localhost"
(address "127.0.0.1") at port "5432".
SSL connection (protocol: TLSv1.3, cipher: TLS_AES_256_GCM_SHA384, compression: off)

Получилось!

Page 9: Привилегии

Привилегии определяют права доступа ролей к объектам

  • Таблицы и представления
    • SELECT
    • чтение данных
    • INSERT
    • вставка строк
    • можно на уровне столбцов
    • UPDATE
    • изменение строк
    • REFERENCES
    • внешний ключ (для таблиц)
    • DELETE
    • удаление строк
    • TRUNCATE
    • опустошение (для таблиц)
    • TRIGGER
    • создание триггеров

Комментарии преподавателя

Привилегии устанавливают связь между субъектами (ролями) и объектами кластера. Они определяют действия, доступные для ролей в отношении этих объектов.

Список возможных привилегий отличается для объектов различных типов. Привилегии для основных типов объектов приведены на этом и следующем слайдах.

Больше всего привилегий определено для таблиц и представлений. Часть из них можно задать не только для всего отношения, но и для отдельных столбцов.

https://postgrespro.ru/docs/postgresql/16/ddl-priv

https://postgrespro.ru/docs/postgresql/16/sql-grant

Page 10: Привилегии

Source slide page 10: Привилегии{#source-figure-010 caption=“Source slide page 10: Привилегии”}

Табличные пространства, базы данных, схемы

  • Последовательности
    • SELECT
    • currval
    • UPDATE
    • nextval
    • setval
    • USAGE
    • currval
    • nextval

Комментарии преподавателя

Возможно, несколько неожиданный набор привилегий имеют последовательности. Выбирая нужные, можно разрешить или запретить доступ к трем управляющим функциям.

Для табличных пространств есть привилегия CREATE, разрешающая создание объектов в этом пространстве.

Для баз данных привилегия CREATE разрешает создавать схемы в этой БД, а для схемы привилегия CREATE разрешает создавать объекты в этой схеме.

Поскольку точное имя схемы для временных объектов заранее неизвестно, привилегия на создание временных таблиц вынесена на уровень БД (TEMPORARY).

Привилегия USAGE схемы разрешает обращаться к объектам в этой схеме.

Привилегия CONNECT базы данных разрешает подключение к этой БД.

Page 11: Категории ролей

Суперпользователи

  • полный доступ ко всем объектам — проверки не выполняются
  • Владельцы
    • изначально все привилегии для объекта (можно отозвать)
    • действия со своими объектами, не регламентируемые привилегиями,
    • например: удаление, выдача и отзыв привилегий и т. п.
  • Остальные роли
    • доступ в рамках выданных привилегий

Комментарии преподавателя

В целом можно сказать, что доступ роли к объекту определяется привилегиями. Но имеет смысл выделить три категории ролей и рассмотреть их по отдельности:

  1. Роли с атрибутом SUPERUSER (суперпользователи). Такие роли могут делать все, что угодно — для них проверки разграничения доступа не выполняются.

  2. Владелец объекта. Изначально это роль, создавшая объект, хотя потом его можно сменить. Владельцем считается не только сама роль-владелец, но и любая другая роль, включенная в нее. Владелец объекта сразу получает полный набор привилегий для этого объекта.

В принципе, эти привилегии можно отозвать, но владелец объекта обладает также неотъемлемым правом совершать действия, не регламентируемые привилегиями. В частности, он может выдавать и отзывать привилегии (в том числе и себе самому), удалять объект и т. п.

  1. Все остальные роли имеют доступ к объекту только в рамках выданных им привилегий.

Чтобы проверить, есть ли у роли необходимая привилегия в отношении некоторого объекта, можно воспользоваться функциями has_*_privilege:

https://postgrespro.ru/docs/postgresql/16/functions-info

Page 12: Управление привилегиями

Выдача привилегий

alice: GRANT привилегии ON объект TO bob;
  • alice
  • bob
  • привилегии
  • на объект
  • Отзыв привилегий
alice: REVOKE привилегии ON объект FROM bob;

Комментарии преподавателя

Право выдачи и отзыва привилегий на объект имеет владелец этого объекта (и суперпользователь).

Синтаксис команд GRANT и REVOKE достаточно сложен и позволяет указывать как отдельные, так и все возможные привилегии; как отдельные объекты, так и группы объектов, входящие в определенные схемы и т. п.

https://postgrespro.ru/docs/postgresql/16/sql-grant

https://postgrespro.ru/docs/postgresql/16/sql-revoke

Page 13: Привилегии

Source slide page 13: Привилегии{#source-figure-013 caption=“Source slide page 13: Привилегии”}

Алиса смогла подключиться к базе данных. Теперь она хочет создать для себя схему и несколько объектов в ней.

alice=> CREATE SCHEMA alice;
ERROR:  permission denied for database access_overview

В чем проблема? У Алисы нет привилегии для создания схем в БД. Выдадим ее:

student=# GRANT CREATE ON DATABASE access_overview TO alice;
GRANT

Пробуем еще раз:

alice=> CREATE SCHEMA alice;
CREATE SCHEMA

Теперь, поскольку Алиса является владельцем своей схемы, она имеет все привилегии на нее и может создавать в ней любые объекты. По умолчанию будет использоваться именно эта схема:

alice=> SELECT current_schemas(false);
 current_schemas
-----------------
 {alice,public}
(1 row)

Алиса создает две таблицы.

alice=> CREATE TABLE t1(n numeric);
CREATE TABLE
alice=> INSERT INTO t1 VALUES (1);
INSERT 0 1
alice=> CREATE TABLE t2(n numeric, who text DEFAULT current_user);
CREATE TABLE
alice=> INSERT INTO t2(n) VALUES (1);
INSERT 0 1

А суперпользователь создает другую роль для пользователя Боба, который будет обращаться к объектам, принадлежащим Алисе.

student=# CREATE ROLE bob LOGIN PASSWORD 'bobpass';
CREATE ROLE
student$ psql 'host=localhost user=bob dbname=access_overview password=bobpass'

Боб попробует обратиться к таблице t1.

bob=> SELECT * FROM alice.t1;
ERROR:  permission denied for schema alice
LINE 1: SELECT * FROM alice.t1;
                      ^

В чем причина ошибки? Нет доступа к схеме, так как Боб не суперпользователь и не владелец этой схемы. Проверить права на доступ к схеме можно так (столбец Access privileges):

alice=> \x \dn+ \x

Page 14: Page 14

Source slide page 14: Page 14{#source-figure-014 caption=“Source slide page 14: Page 14”}

Expanded display is on.
List of schemas
-[ RECORD 1 ]-----+---------------------------------------
Name              | alice
Owner             | alice
Access privileges |
Description       |
-[ RECORD 2 ]-----+---------------------------------------
Name              | public
Owner             | pg_database_owner
Access privileges | pg_database_owner=UC/pg_database_owner+
                  | =U/pg_database_owner
Description       | standard public schema
Expanded display is off.

Привилегии отображаются в формате: роль=привилегии/кем_предоставлены. Каждая привилегия кодируется одним символом, в частности для схем: U = usage; C = create. Если имя роли опущено (как в последней строке), то имеется в виду псевдороль public. Обратите внимание, что на схему public у псевдороли public есть лишь привилегия usage. Здесь pg_database_owner соответствует владельцу базы данных. Если же опущено все поле (как в первой строке), то имеются в виду привилегии по умолчанию: alice имеет обе доступные привилегии на собственную схему, а остальные роли не имеют никаких. Предоставим доступ к схеме для Боба. Это может сделать Алиса как владелец.

alice=> GRANT CREATE, USAGE ON SCHEMA alice TO bob;
GRANT

Боб снова пробует обратиться к таблице:

bob=> SELECT * FROM alice.t1;
ERROR:  permission denied for table t1

Снова ошибка. В чем причина? На этот раз у Боба есть доступ к схеме, но нет доступа к самой таблице. Вот как проверить доступ:

alice=> \dp alice.t1
                            Access privileges
 Schema | Name | Type  | Access privileges | Column privileges | Policies
--------+------+-------+-------------------+-------------------+----------
 alice  | t1   | table |                   |                   |
(1 row)

И снова видим пустое поле: доступ есть только у владельца, то есть у Алисы. Алиса предоставляет Бобу доступ на чтение и изменение:

alice=> GRANT SELECT, UPDATE ON alice.t1 TO bob;
GRANT

А для второй таблицы — доступ на вставку и чтение одного столбца:

alice=> GRANT SELECT(n), INSERT ON alice.t2 TO bob;
GRANT

Посмотрим, как изменились привилегии:

alice=> \dp alice.*
                             Access privileges
 Schema | Name | Type  |  Access privileges  | Column privileges | Policies
--------+------+-------+---------------------+-------------------+----------
 alice  | t1   | table | alice=arwdDxt/alice+|                   |
        |      |       | bob=rw/alice        |                   |
 alice  | t2   | table | alice=arwdDxt/alice+| n:               +|
        |      |       | bob=a/alice         |   bob=r/alice     |
(2 rows)

Теперь пустое поле «проявилось», и мы видим, что в нем находится полный перечень привилегий. Ниже — обозначения,

Page 15: Page 15

Source slide page 15: Page 15{#source-figure-015 caption=“Source slide page 15: Page 15”}

не все они вполне очевидные: a = insert r = select w = update d = delete D = truncate x = reference t = trigger Привилегии для столбцов отображаются отдельно (столбец Column privileges). На этот раз попытки Боба увенчаются успехом. Чтобы не указывать каждый раз имя схемы, Боб добавляет его к своему пути поиска.

bob=> ALTER ROLE bob SET search_path = public, alice;
ALTER ROLE

Теперь путь поиска будет устанавливаться в каждом сеансе Боба.

bob=> \c
You are now connected to database "access_overview" as user "bob".
bob=> SHOW search_path;
  search_path
---------------
 public, alice
(1 row)
bob=> UPDATE t1 SET n = n + 1;
UPDATE 1
bob=> SELECT * FROM t1;
 n
---
 2
(1 row)

Но другие операции по-прежнему запрещены:

bob=> DELETE FROM t1;
ERROR:  permission denied for table t1

И вторая таблица:

bob=> INSERT INTO t2(n) VALUES (100);
INSERT 0 1
bob=> SELECT n FROM t2;
  n
-----
   1
 100
(2 rows)

А чтение другого столбца запрещено:

bob=> SELECT * FROM t2;
ERROR:  permission denied for table t2

Page 16: Включение роли в роль

Source slide page 16: Включение роли в роль{#source-figure-016 caption=“Source slide page 16: Включение роли в роль”}

Включение в роль

alice: GRANT dba TO bob;
  • alice
  • bob
  • dba
  • псевдороль public неявно включает
  • в себя все остальные роли
  • Исключение из роли
alice: REVOKE dba FROM bob;

Комментарии преподавателя

В любую роль могут быть включены другие роли. В таком случае роль выступает в качестве группы. Отдельного понятия «группа» в PostgreSQL нет.

Роль может быть включена в несколько ролей; включенная роль может, в свою очередь, включать в себя третьи роли, но циклы при этом не допускаются.

По умолчанию роль наследует привилегии роли, в которую она включена. Это поведение можно изменить, указав роли атрибут NOINHERIT — тогда, чтобы воспользоваться привилегиями включающей роли, надо будет явно переключиться на нее с помощью SET ROLE. Атрибуты ролей не наследуются, но можно переключиться на включающую роль и воспользоваться ее атрибутами.

Роли, включающие другие роли, обычно имеют атрибут NOLOGIN и называются «групповыми». Фактически это именованный набор привилегий, который удобно «выдать» обычной роли точно так же, как выдается одиночная привилегия. Это упрощает управление доступом и администрирование.

Существует псевдороль public, которая неявно включает в себя все остальные роли. Если выдать какие-либо привилегии роли public, эти привилегии получат вообще все роли.

https://postgrespro.ru/docs/postgresql/16/role-membership

Page 17: Предопределенные роли

pg_monitor

  • pg_read_all_settings чтение всех параметров сервера
  • pg_read_all_stats доступ к статистике pg_stat_scan_tables мониторинг и блокировки таблиц
  • pg_read_all_data чтение данных из всех таблиц pg_write_all_data изменение данных во всех таблицах
  • pg_read_server_files чтение файлов на сервере pg_write_server_files запись в файлы на сервере
  • pg_execute_server_programs выполнение программ на сервере
  • …

Комментарии преподавателя

В PostgreSQL имеется набор предопределенных ролей, имеющих доступ к часто востребованным, но не общедоступным функциям и данным. Членство в этих ролях может быть предоставлено обычным пользователям для решения ряда задач администрирования — чтобы избежать наделения этих пользователей суперпользовательскими полномочиями.

Количество предопределенных ролей увеличивается с каждой новой версией PostgreSQL. Полный список всех ролей, включая предопределенные, можно просмотреть с помощью команды \duS в psql.

https://postgrespro.ru/docs/postgresql/16/predefined-roles

Можно создавать и свои собственные «административные» роли, например, для выполнения задач резервного копирования.

Page 18: Групповые роли

Source slide page 18: Групповые роли{#source-figure-018 caption=“Source slide page 18: Групповые роли”}

Вспомним, что Боб не смог прочитать второй столбец таблицы:

bob=> SELECT * FROM t2;
ERROR:  permission denied for table t2

Суперпользователь включает Боба в предопределенную роль pg_read_all_data:

student=# GRANT pg_read_all_data TO bob;
GRANT ROLE

Теперь Боб сможет получить доступ на чтение ко всем таблицам так, как будто ему были выданы привилегии SELECT на все таблицы и USAGE на все схемы:

bob=> SELECT * FROM t2;
  n  |  who
-----+-------
   1 | alice
 100 | bob
(2 rows)

Получить информацию о членстве в ролях можно с помощью команды \drg утилиты psql:

student=# \drg
                  List of role grants
 Role name |    Member of     |   Options    | Grantor
-----------+------------------+--------------+----------
 bob       | pg_read_all_data | INHERIT, SET | postgres
(1 row)

Столбец Options содержит информацию об атрибутах, описывающих включение. Здесь для роли bob, включенной в pg_read_all_data, SET означает возможность переключения на групповую роль, а INHERIT — использование привилегий группы без явного переключения на нее. Исключим пользователя bob из группы:

student=# REVOKE pg_read_all_data FROM bob;
REVOKE ROLE

Page 19: Подпрограммы

Единственная привилегия для функций и процедур

  • EXECUTE
  • выполнение
  • Характеристики безопасности
    • SECURITY INVOKER
    • выполняется с правами вызывающего
    • (по умолчанию)
    • SECURITY DEFINER
    • выполняется с правами владельца

Комментарии преподавателя

Для функций и процедур есть единственная привилегия EXECUTE, разрешающая выполнение этой подпрограммы.

Тонкий момент связан с тем, от имени какого пользователя будет выполняться подпрограмма. Если подпрограмма объявлена как SECURITY INVOKER (по умолчанию), она выполняется с правами вызывающего пользователя. В этом случае операторы внутри подпрограммы смогут обращаться только к тем объектам, к которым у вызывающего пользователя есть доступ.

Если же указать фразу SECURITY DEFINER, подпрограмма работает с правами ее владельца. Это способ позволить другим пользователям выполнять определенные действия над объектами, к которым у них нет непосредственного доступа.

https://postgrespro.ru/docs/postgresql/16/sql-createfunction

https://postgrespro.ru/docs/postgresql/16/sql-createprocedure

Page 20: Привилегии по умолчанию

Привилегии псевдороли public

  • подключение к любой базе данных
  • доступ к системному каталогу
  • выполнение любых подпрограмм
  • привилегии выдаются автоматически для каждого нового объекта
  • Настраиваемые привилегии по умолчанию
    • возможность дополнительно выдать или отозвать привилегии
    • для вновь создаваемого объекта

Комментарии преподавателя

Как уже говорилось, псевдороль public включает в себя все остальные роли, которые, таким образом, пользуются всеми привилегиями, выданными для public.

При этом public имеет довольно широкий спектр привилегий по умолчанию. В частности:

● право подключения к любой базе данных (именно поэтому роль alice смогла подключиться к базе данных несмотря на то, что привилегия CONNECT не выдавалась ей явно);

● доступ к системному каталогу;

● выполнение любых подпрограмм.

Это, с одной стороны, позволяет комфортно работать, не задумываясь о привилегиях, а с другой — создает определенные сложности, если разграничение доступа действительно необходимо.

Описанные выше привилегии появляются у public автоматически при создании новых объектов. То есть недостаточно, например, просто отозвать у public привилегию EXECUTE на все подпрограммы: как только появится новая, public немедленно получает привилегию на ее выполнение.

Однако существует специальный механизм «привилегий по умолчанию», который позволяет автоматически выдавать и отзывать привилегии при создании нового объекта. Этот механизм можно использовать и для отзыва права выполнения функций у псевдороли public.

https://postgrespro.ru/docs/postgresql/16/sql-alterdefaultprivileges

Page 21: Подпрограммы и привилегии по умолчанию

Source slide page 21: Подпрограммы и привилегии по умолчанию{#source-figure-021 caption=“Source slide page 21: Подпрограммы и привилегии по умолчанию”}

Алиса создает функцию:

alice=> CREATE FUNCTION foo() RETURNS SETOF t2
AS $$
SELECT * FROM t2;
$$ LANGUAGE sql STABLE;
CREATE FUNCTION

Сможет ли Боб вызвать ее, если Алиса не выдала ему привилегию EXECUTE?

bob=> SELECT foo();
ERROR:  permission denied for table t2
CONTEXT:  SQL function "foo" statement 1

Вызвать — да, но Боб не сможет получить доступ к объектам, на которые у него нет соответствующих привилегий. Если Боб создаст свою таблицу t2 в схеме public, функция будет работать для обоих пользователей — но с разными таблицами, поскольку у Алисы и Боба разные пути поиска. Чтобы Боб мог создать таблицу в схеме public, потребуется явно выдать ему привилегию CREATE на схему (начиная с версии PostgreSQL 15):

student=# GRANT CREATE ON SCHEMA public TO bob;
GRANT
bob=> CREATE TABLE t2(n numeric, who text DEFAULT current_user);
CREATE TABLE
bob=> INSERT INTO t2(n) VALUES (42);
INSERT 0 1
bob=> SELECT foo();
   foo
----------
 (42,bob)
(1 row)
alice=> SELECT foo();
    foo
-----------
 (1,alice)
 (100,bob)
(2 rows)

Другой доступный вариант — объявить функцию как работающую с правами ее владельца:

alice=> ALTER FUNCTION foo() SECURITY DEFINER;
ALTER FUNCTION

В этом случае функция всегда будет выполняться с правами Алисы, независимо от того, кто ее вызывает. Боб удаляет свою таблицу…

bob=> DROP TABLE t2;
DROP TABLE

…и получает доступ к содержимому таблицы Алисы:

bob=> SELECT foo();
    foo
-----------
 (1,alice)
 (100,bob)
(2 rows)

В этом случае Алисе надо внимательно следить за выданными привилегиями. Скорее всего, потребуется отозвать

Page 22: Page 22

Source slide page 22: Page 22{#source-figure-022 caption=“Source slide page 22: Page 22”}

EXECUTE у роли public и выдавать ее явно только нужным ролям.

alice=> REVOKE EXECUTE ON ALL ROUTINES IN SCHEMA alice FROM public;
REVOKE
bob=> SELECT foo();
ERROR:  permission denied for function foo

Но дело осложняется тем, что по умолчанию привилегия на выполнение автоматически выдается роли public на каждую вновь создаваемую функцию. Для того чтобы конкретные пользователи получали или, наоборот, лишались тех или иных привилегий на вновь создаваемые объекты, можно настроить привилегии по умолчанию:

alice=> ALTER DEFAULT PRIVILEGES
FOR ROLE alice
REVOKE EXECUTE ON ROUTINES FROM public;
ALTER DEFAULT PRIVILEGES
alice=> ALTER DEFAULT PRIVILEGES
FOR ROLE alice
GRANT EXECUTE ON ROUTINES TO bob;
ALTER DEFAULT PRIVILEGES
alice=> \ddp
           Default access privileges
 Owner | Schema |   Type   | Access privileges
-------+--------+----------+-------------------
 alice |        | function | alice=X/alice    +
       |        |          | bob=X/alice
(1 row)

Теперь Боб сразу получает привилегию на выполнение подпрограмм, создаваемых Алисой, а остальные пользователи не смогут их выполнять.

alice=> CREATE FUNCTION bar() RETURNS integer
LANGUAGE sql IMMUTABLE SECURITY DEFINER
RETURN 1;
CREATE FUNCTION
bob=> SELECT bar();
 bar
-----
   1
(1 row)

Page 23: Итоги

Роли, атрибуты и привилегии — гибкий механизм, позволяющий по-разному организовать работу

  • можно легко разрешить все всем
  • можно строго разграничить доступ, если это необходимо
  • При создании новых ролей надо позаботиться о возможности их подключения к серверу

Page 24: Практика

    1. Создайте две роли (пароль должен совпадать с именем): – employee — сотрудник магазина, – buyer — покупатель.
  • Убедитесь, что созданные роли могут подключиться к БД. 2. Отзовите у роли public права выполнения всех функций и подключения к БД. 3. Разграничьте доступ таким образом, чтобы: – сотрудник мог только заказывать книги, а также – добавлять авторов и книги, – покупатель мог только приобретать книги.
    • Проверьте выполненные настройки в приложении.

Комментарии преподавателя

  1. Сотрудник — внутренний пользователь приложения, аутентификация выполняется на уровне СУБД.

Покупатель — внешний пользователь. В реальном интернет-магазине управление такими пользователями ложится на приложение, а все запросы поступают в СУБД от одной «обобщенной» роли (buyer). Идентификатор конкретного покупателя может передаваться как параметр (но в нашем приложении мы этого не делаем).

  1. Вообще говоря, разграничение доступа должно быть заложено, в том числе, в приложение. В нашем учебном приложении разграничение не сделано специально: вместо этого на веб-странице можно явно выбрать роль, от имени которой пойдет запрос в СУБД. Это позволяет посмотреть, как поведет себя серверная часть при некорректной работе приложения.

Итак, пользователям нужно выдать:

● Право подключения к БД bookstore и доступ к схеме bookstore.

● Доступ к представлениям, к которым происходит непосредственное обращение.

● Доступ к функциям, которые вызываются как часть API. Если оставить функции SECURITY INVOKER, придется выдавать доступ и ко всем «нижележащим» объектам (таблицам, другим функциям). Однако удобнее просто объявить API-функции как SECURITY DEFINER.

Разумеется, ролям нужно выдать привилегии только на те объекты, доступ к которым у них должен быть.

Page 25: 1. Создание ролей

=> CREATE ROLE employee LOGIN PASSWORD 'employee';
CREATE ROLE
=> CREATE ROLE buyer LOGIN PASSWORD 'buyer';
CREATE ROLE

Настройки по умолчанию разрешают подключение с локального адреса по паролю. Нас это устраивает.

2. Привилегии public

У роли public надо отозвать лишние привилегии.

=> REVOKE EXECUTE ON ALL FUNCTIONS IN SCHEMA bookstore FROM public;
REVOKE
=> REVOKE CONNECT ON DATABASE bookstore FROM public;
REVOKE

3. Разграничение доступа

Функции с правами создавшего.

=> ALTER FUNCTION get_catalog(text,text,boolean) SECURITY DEFINER;
ALTER FUNCTION
=> ALTER FUNCTION update_catalog() SECURITY DEFINER;
ALTER FUNCTION
=> ALTER FUNCTION add_author(text,text,text) SECURITY DEFINER;
ALTER FUNCTION
=> ALTER FUNCTION add_book(text,integer[]) SECURITY DEFINER;
ALTER FUNCTION
=> ALTER FUNCTION buy_book(integer) SECURITY DEFINER;
ALTER FUNCTION
=> ALTER FUNCTION book_name(integer,text,integer) SECURITY DEFINER;
ALTER FUNCTION
=> ALTER FUNCTION authors(books) SECURITY DEFINER;
ALTER FUNCTION

Привилегии покупателя: покупатель должен иметь доступ к поиску книг и их покупке.

=> GRANT CONNECT ON DATABASE bookstore TO buyer;
GRANT
=> GRANT USAGE ON SCHEMA bookstore TO buyer;
GRANT
=> GRANT EXECUTE ON FUNCTION get_catalog(text,text,boolean) TO buyer;
GRANT
=> GRANT EXECUTE ON FUNCTION buy_book(integer) TO buyer;
GRANT

Привилегии сотрудника: сотрудник должен иметь доступ к просмотру и добавлению книг и авторов, а также к каталогу для заказа книг.

=> GRANT CONNECT ON DATABASE bookstore TO employee;
GRANT

Page 26: Page 26

=> GRANT USAGE ON SCHEMA bookstore TO employee;
GRANT
=> GRANT SELECT,UPDATE(onhand_qty) ON catalog_v TO employee;
GRANT
=> GRANT SELECT ON authors_v TO employee;
GRANT
=> GRANT SELECT ON operations_v TO employee;
GRANT
=> GRANT EXECUTE ON FUNCTION book_name(integer,text,integer) TO employee;
GRANT
=> GRANT EXECUTE ON FUNCTION authors(books) TO employee;
GRANT
=> GRANT EXECUTE ON FUNCTION author_name(text,text,text) TO employee;
GRANT
=> GRANT EXECUTE ON FUNCTION add_book(text,integer[]) TO employee;
GRANT
=> GRANT EXECUTE ON FUNCTION add_author(text,text,text) TO employee;
GRANT

Page 27: Практика+

  1. Зарегистрируйте пользовательские роли alice и bob. 2. Отредактируйте файл pg_hba.conf так, чтобы беспарольный вход был разрешен лишь для пользователей postgres и student. Убедитесь, что доступ для alice и bob запрещен. 3. Для пользователей alice и bob включите разрешение входить в сеанс, используя метод peer. Проверьте невозможность входа в сеанс без создания отображения на пользователя ОС. Для alice создайте такое отображение. 4. Проверьте возможность использовать одно и то же отображение для разных ролей.

Комментарии преподавателя

  1. С помощью текстового редактора вставьте перед первой незакомментированной директивой в pg_hba.conf local all postgres,student trust

Перечитайте конфигурацию.

  1. Отредактируйте вставленную в pg_hba.conf директиву, заменив ее следующим: local all postgres,student trust local all alice,bob peer

В конец файла pg_ident.conf добавьте: stmap student alice

  1. Чтобы роли alice и bob использовали одно отображение, содержимое pg_ident.conf должно быть следующим: stmap student alice stmap student bob

Page 28: 1. Добавление ролей

Зарегистрируем роли с правом входа в сеанс.

=> CREATE ROLE alice LOGIN;
CREATE ROLE
=> CREATE ROLE bob LOGIN;
CREATE ROLE

2. Ограничение использования trust

Отредактируем содержимое pg_hba.conf, разрешив метод trust лишь для postgres и student.

student$ sudo sed -i 's/^local.*all.*all.*trust.*$/local all postgres,student trust\n/'
/etc/postgresql/16/main/pg_hba.conf

Вот что получилось:

=> SELECT type,database,user_name,address,auth_method,error
FROM pg_hba_file_rules
ORDER BY rule_number;
 type  |   database    |     user_name      |  address  |  auth_method  | error
-------+---------------+--------------------+-----------+---------------+-------
 local | {all}         | {postgres,student} |           | trust         |
 host  | {all}         | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {all}         | {all}              | ::1       | scram-sha-256 |
 local | {replication} | {all}              |           | trust         |
 host  | {replication} | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {replication} | {all}              | ::1       | scram-sha-256 |
(6 rows)

Применим конфигурацию.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Теперь alice и bob не могут подключиться:

student$ psql -l -U alice
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  no pg_hba.conf entry for host "[local]", user "alice", database "postgres", no
encryption
student$ psql -l -U bob
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  no pg_hba.conf entry for host "[local]", user "bob", database "postgres", no
encryption

3. Метод аутентификации peer

Используя текстовый редактор, добавим еще одну строку с методом аутентификации peer, чтобы разрешить подключение пользователям alice и bob.

student$ sudo sed -i '/^local.*all.*postgres,student.*$/alocal all alice,bob peer'
/etc/postgresql/16/main/pg_hba.conf

Содержимое pg_hba.conf:

=> SELECT type,database,user_name,address,auth_method,error
FROM pg_hba_file_rules
ORDER BY rule_number;

Page 29: Page 29

 type  |   database    |     user_name      |  address  |  auth_method  | error
-------+---------------+--------------------+-----------+---------------+-------
 local | {all}         | {postgres,student} |           | trust         |
 local | {all}         | {alice,bob}        |           | peer          |
 host  | {all}         | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {all}         | {all}              | ::1       | scram-sha-256 |
 local | {replication} | {all}              |           | trust         |
 host  | {replication} | {all}              | 127.0.0.1 | scram-sha-256 |
 host  | {replication} | {all}              | ::1       | scram-sha-256 |
(7 rows)

Перечитаем конфигурацию.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Однако по-прежнему эти учетные записи не могут войти в сеанс, хотя сообщение об ошибке изменилось.

student$ psql -l -U alice
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  Peer authentication failed for user "alice"
student$ psql -l -U bob
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  Peer authentication failed for user "bob"

Метод peer требует совпадения учетной записи в ОС с именем роли в PostgreSQL. Создадим отображение роли alice на пользователя ОС student, добавив строку в файл pg_ident.conf. Для роли bob отображение задавать не будем.

student$ echo 'stmap student alice' | sudo tee -a /etc/postgresql/16/main/pg_ident.conf
stmap student alice

И допишем в добавленную строку параметр map, задающий имя отображения.

student$ sudo sed -i 's/peer.*$/peer map=stmap/' /etc/postgresql/16/main/pg_hba.conf

Содержимое pg_hba.conf:

=> SELECT type,database,user_name,address,auth_method,options,error
FROM pg_hba_file_rules
ORDER BY rule_number;
 type  |   database    |     user_name      |  address  |  auth_method  |   options   |
error
-------+---------------+--------------------+-----------+---------------+-------------+---
----
 local | {all}         | {postgres,student} |           | trust         |             |
 local | {all}         | {alice,bob}        |           | peer          | {map=stmap} |
 host  | {all}         | {all}              | 127.0.0.1 | scram-sha-256 |             |
 host  | {all}         | {all}              | ::1       | scram-sha-256 |             |
 local | {replication} | {all}              |           | trust         |             |
 host  | {replication} | {all}              | 127.0.0.1 | scram-sha-256 |             |
 host  | {replication} | {all}              | ::1       | scram-sha-256 |             |
(7 rows)

Перечитаем конфигурацию.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Теперь alice может подключиться к базе данных и выполнить команды.

student$ psql -c '\conninfo' -U alice -d student
You are connected to database "student" as user "alice" via socket in
"/var/run/postgresql" at port "5432".

А bob — нет.

student$ psql -c '\conninfo' -U bob -d student

Page 30: 4. Одно отображение для нескольких ролей

psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed:
FATAL:  Peer authentication failed for user "bob"

Разрешим пользователю bob входить в сеанс на таких же условиях, как alice.

student$ echo 'stmap student bob' | sudo tee -a /etc/postgresql/16/main/pg_ident.conf
stmap student bob

Добавленные строки в pg_ident.conf:

student$ sudo tail -n2 /etc/postgresql/16/main/pg_ident.conf
stmap student alice
stmap student bob

Перечитаем конфигурацию.

=> SELECT pg_reload_conf();
 pg_reload_conf
----------------
 t
(1 row)

Теперь и alice, и bob смогут подключиться.

student$ psql -c '\conninfo' -U alice -d student
You are connected to database "student" as user "alice" via socket in
"/var/run/postgresql" at port "5432".
student$ psql -c '\conninfo' -U bob -d student
You are connected to database "student" as user "bob" via socket in "/var/run/postgresql"
at port "5432".

20 Занятие 20: Резервное копирование / Логическое резервирование

Исходная страница в нативном Markdown: Резервное копирование / Логическое резервирование
Примечание

Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev1_20_backup_logical.html Archive: DEV1-handouts-16.zip Страниц PDF: 24

Page 1: Резервное копирование / Логическое резервирование

Авторские права

© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)

Использование материалов курса

Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.

Обратная связь

Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru

Отказ от ответственности

Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.

Page 2: Темы

  • Логические и физические резервные копии
  • Копирование и восстановление таблицы
  • Копирование и восстановление базы данных
  • Копирование и восстановление кластера

Page 3: Логическая копия

Команды SQL для создания объектов и наполнения данными

    • можно сделать копию отдельного объекта или отдельной базы
    • можно восстановиться на другой версии или архитектуре
    • (не требуется двоичная совместимость)
    • простота использования
  • − невысокая скорость работы
  • − восстановление только на момент создания резервной копии

Комментарии преподавателя

Логическая резервная копия — набор команд SQL, восстанавливающих кластер (или отдельную базу данных, или отдельную таблицу) с нуля: создаются необходимые объекты и наполняются данными.

Команды можно выполнить на другой версии СУБД (при наличии совместимости на уровне команд) или на другой платформе и архитектуре (не требуется двоичная совместимость).

В частности, логическую резервную копию можно использовать для долговременного хранения: ее можно будет восстановить и после обновления сервера на новую версию.

Создание логической резервной копии — относительно несложный процесс. Обычно он требует выполнения одной команды или запуска одной утилиты.

Однако при восстановлении большой базы команды могут выполняться очень долго. Восстановить систему из логической копии можно ровно на момент начала резервного копирования.

https://postgrespro.ru/docs/postgresql/16/backup-dump

Page 4: Физическая копия

Копия файловой системы кластера баз данных

    • быстрее, чем логическое резервирование
    • восстанавливается статистика
  • − можно восстановиться только на совместимой системе
  • − и на той же самой основной версии PostgreSQL
  • − выборочная копия невозможна, копируется весь кластер
  • Архив журнала предзаписи
      • возможность восстановления на определенный момент времени

Комментарии преподавателя

Физическое резервирование подразумевает копирование всех файлов, относящихся к кластеру БД, то есть создание полной двоичной копии.

Копирование файлов работает быстрее, чем выгрузка SQL-команд при логическом резервировании; запустить сервер из созданной физической копии — дело нескольких минут, в отличие от восстановления из логической копии. Кроме того, нет необходимости заново собирать статистику — она также восстанавливается из копии.

Но есть и минусы. Из физической резервной копии можно восстановить систему только на совместимой платформе (та же ОС, та же разрядность, тот же порядок байтов в представлении чисел и т. п.) и только на той же основной версии PostgreSQL. Кроме того, невозможно сделать физическую копию отдельных баз данных кластера, выполняется копирование всего экземпляра целиком.

Как правило, создание физических резервных копий дополняют ведением архива файлов журнала предзаписи. Это позволяет восстанавливать систему не только на момент создания резервной копии, но и на произвольный момент времени.

https://postgrespro.ru/docs/postgresql/16/backup-file

https://postgrespro.ru/docs/postgresql/16/continuous-archiving

Физическое резервирование — обычный способ создания периодических резервных копий любой сколько-нибудь серьезной системы. Создание таких копий — ответственность администратора баз данных.

Page 5: Копия таблицы в SQL

Source slide page 5: Копия таблицы в SQL{#source-figure-005 caption=“Source slide page 5: Копия таблицы в SQL”}

YKS   Якутск      (129.770
MJZ   Мирный      (114.039
KHV   Хабаровск-Новый   (1
PKC   Елизово     (158.453
UUS   Хомутово    (142.718
VVO   Владивосток       (1
LED   Пулково     (30.2625
KGD   Храброво    (20.5925
KEJ   Кемерово    (86.1072
CEK   Челябинск   (61.5033
MQF   Магнитогорск      (5
PEE   Пермь       (56.0211
SGC   Сургут      (73.4018
  • файл в ФС сервера и доступен владельцу экземпляра PostgreSQL можно ограничить столбцы (или использовать произвольный запрос) при восстановлении строки добавляются к имеющимся в таблице

Комментарии преподавателя

Если требуется сохранить только содержимое одной таблицы, можно воспользоваться командой COPY.

Команда позволяет записать таблицу (или часть столбцов таблицы, или даже результат произвольного запроса) либо в файл, либо на консоль, либо отправить на вход какой-либо программе. При этом можно указать ряд параметров, таких как формат (текстовый, CSV или двоичный), разделитель полей, текстовое представление NULL и т. п.

Другой вариант команды, наоборот, считывает из файла или из консоли строки с полями и записывает их в таблицу. Таблица при этом не очищается, новые строки добавляются к уже существующим.

Команда COPY работает существенно быстрее, чем аналогичные команды INSERT — клиенту не нужно много раз обращаться к серверу, а серверу не нужно много раз анализировать команды.

Тонкость: при выполнении команды COPY FROM не применяются правила (rules), хотя ограничения целостности и триггеры выполняются.

https://postgrespro.ru/docs/postgresql/16/sql-copy

Page 6: Копия таблицы в psql

Source slide page 6: Копия таблицы в psql{#source-figure-006 caption=“Source slide page 6: Копия таблицы в psql”}

YKS   Якутск      (129.770
MJZ   Мирный      (114.039
KHV   Хабаровск-Новый   (1
PKC   Елизово     (158.453
UUS   Хомутово    (142.718
VVO   Владивосток       (1
LED   Пулково     (30.2625
KGD   Храброво    (20.5925
KEJ   Кемерово    (86.1072
CEK   Челябинск   (61.5033
MQF   Магнитогорск      (5
PEE   Пермь       (56.0211
SGC   Сургут      (73.4018
  • файл в ФС клиента и доступен пользователю ОС, запустившему psql происходит пересылка данных между клиентом и сервером синтаксис и возможности аналогичны команде COPY

Комментарии преподавателя

В psql существует клиентский вариант команды COPY с аналогичным синтаксисом.

Указание имени файла в SQL-команде COPY соответствует файлу на сервере БД. У пользователя, под которым работает PostgreSQL (обычно postgres), должен быть доступ к этому файлу.

В клиентском же варианте команды файл располагается на клиенте, что позволяет сохранить локальную копию данных, даже не имея доступа к файловой системе сервера. Содержимое таблиц автоматически пересылается между клиентом и сервером.

https://postgrespro.ru/docs/postgresql/16/app-psql

Page 7: Команда COPY

Создадим базу данных и таблицу.

=> CREATE DATABASE backup_logical_dev_1;
CREATE DATABASE
=> \c backup_logical_dev_1
You are now connected to database "backup_logical_dev_1" as user "student".
=> CREATE TABLE t(
    id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    s text
);
CREATE TABLE
=> INSERT INTO t(s) VALUES ('Привет, мир!'), (''), (NULL);
INSERT 0 3

Вот что показывает команда COPY (выдаем на консоль, а не в файл):

=> COPY t TO stdout;
1
Привет, мир!
2
3
\N

Видно, как различаются в выводе пустые строки и неопределенные значения. Формат вывода настраивается достаточно гибко. Можно изменить разделитель, представление неопределенных значений, добавить строку с названиями столбцов и т.п. Например:

=> COPY t TO stdout WITH (NULL '<NULL>', DELIMITER ',', HEADER);
id,s
1,Привет\, мир!
2,
3,<NULL>

Обратите внимание, что символ-разделитель внутри строки был экранирован (символ для экранирования тоже настраивается). Вместо таблицы можно указать произвольный запрос.

=> COPY (SELECT * FROM t WHERE s IS NOT NULL) TO stdout;
1
Привет, мир!
2

Таким образом можно сохранить результат запроса, данные представления и т. п. Команда может работать и с форматом CSV, который поддерживается множеством программ.

=> COPY t TO stdout WITH (FORMAT csv);
1,"Привет, мир!"
2,""
3,

Аналогично работает и ввод данных из файла или с консоли. При вводе с консоли требуется маркер конца файла — обратная косая черта с точкой. В обычном файле он не нужен. Все параметры при вводе должны совпадать с теми, что были указаны при выводе.

=> TRUNCATE TABLE t;
TRUNCATE TABLE
=> COPY t FROM stdin;
1
Привет, мир!
2
3
\N
\.
COPY 3

Page 8: Page 8

Если вводимые данные предваряются строкой-заголовком с именами столбцов, то возможно выполнение проверки на их соответствие столбцам принимающей таблицы.

=> COPY t FROM stdin (HEADER MATCH);
id
s
4
Строка четыре
\.
COPY 1

При несовпадении получим ошибку:

=> COPY t FROM stdin (HEADER MATCH);
Col_1
Col_2
5
Строка пять
\.
ERROR:  column name mismatch in header line field 1: got "Col_1", expected "id"
CONTEXT:  COPY t, line 1: "Col_1
Col_2"

Еще одна попытка добавить строку в таблицу:

=> INSERT INTO t (s) VALUES ('Строка пять');
ERROR:  duplicate key value violates unique constraint "t_pkey"
DETAIL:  Key (id)=(4) already exists.

Эта ошибка возникла из-за того, что успешно выполненная выше команда COPY не передвинула значение последовательности, обслуживающей столбец id. Исправим ситуацию:

=> SELECT pg_catalog.setval('t_id_seq', 4, true);
 setval
--------
      4
(1 row)
=> INSERT INTO t (s) VALUES ('Строка пять');
INSERT 0 1

Вот что загрузилось в таблицу (для наглядности настроим в psql вывод неопределенных значений):

=> \pset null '\\N'
Null display is "\N".
=> SELECT * FROM t;
 id |       s
----+---------------
  1 | Привет, мир!
  2 |
  3 | \N
  4 | Строка четыре
  5 | Строка пять
(5 rows)

Page 9: Копия базы данных

Source slide page 9: Копия базы данных{#source-figure-009 caption=“Source slide page 9: Копия базы данных”}

--
-- PostgreSQL database dum
--
-- Dumped from database ve
-- Dumped by pg_dump versi
SET statement_timeout = 0;
SET lock_timeout = 0;
SET
idle_in_transaction_sessio
SET client_encoding = 'UTF
SET standard_conforming_st
  • формат: команды SQL при выгрузке можно выбрать отдельные объекты базы данных новая база должна быть создана из шаблона template0 заранее должны быть созданы роли и табличные пространства после загрузки имеет смысл выполнить ANALYZE

Комментарии преподавателя

Для создания полноценной резервной копии базы данных используется утилита pg_dump.

Если не указать имя файла (-f, –file), то утилита выведет результат на консоль. А результатом является скрипт, предназначенный для psql, который содержит команды для создания необходимых объектов и наполнения их данными.

Дополнительными ключами утилиты можно ограничить набор объектов: выбрать указанные таблицы, или все объекты в указанных схемах (с учетом установленных расширений), или наложить другие фильтры.

Чтобы восстановить объекты из резервной копии, достаточно выполнить полученный скрипт в psql.

Следует иметь в виду, что базу данных для восстановления надо создавать из шаблона template0, так как все изменения, сделанные в template1, также попадут в резервную копию.

Кроме того, заранее должны быть созданы необходимые роли и табличные пространства. Поскольку эти объекты не относятся к конкретной БД, они не будут выгружены в резервную копию.

После восстановления базы имеет смысл выполнить команду ANALYZE: она соберет статистику, необходимую оптимизатору для планирования запросов.

https://postgrespro.ru/docs/postgresql/16/app-pgdump

Page 10: Формат custom

Source slide page 10: Формат custom{#source-figure-010 caption=“Source slide page 10: Формат custom”}

;
; Archive created at 2017-
;     dbname: demo
;     TOC Entries: 146
;     Compression: -1
;     Dump Version: 1.12-0
;     Format: CUSTOM
;
; Selected TOC Entries:
;
2297; 1262 475453 DATABASE
5; 2615 475454 SCHEMA - bo
2298; 0 0 COMMENT - SCHEMA
  • внутренний формат с оглавлением отдельные объекты базы данных можно выбрать на этапе восстановления возможна загрузка в несколько параллельных потоков

Комментарии преподавателя

Утилита pg_dump позволяет указать формат резервной копии. По умолчанию это plain — простые команды для psql.

Формат custom (-F c, –format=custom) – формат резервной копии, содержащий не только объекты, но и оглавление. Наличие оглавления позволяет выбирать объекты для восстановления не при создании копии, а непосредственно при восстановлении.

Файл формата custom по умолчанию сжат.

Для восстановления потребуется другая утилита — pg_restore. Она читает файл и преобразует его в команды psql. Если не указать явно имя базы данных (в ключе -d), то команды будут выведены на консоль. Если же база данных указана, утилита соединится с этой БД и выполнит команды без участия psql.

Чтобы восстановить только часть объектов, можно воспользоваться одним из двух подходов. Во-первых, можно ограничить объекты аналогично тому, как они ограничиваются в pg_dump. Вообще, утилита pg_restore понимает многие параметры из репертуара pg_dump.

Во-вторых, можно получить из оглавления список объектов, содержащихся в резервной копии (ключ –list). Затем этот список можно отредактировать вручную, удалив ненужное и передать измененный список на вход pg_restore (ключ –use-list).

https://postgrespro.ru/docs/postgresql/16/app-pgrestore.html

Page 11: Формат directory

Source slide page 11: Формат directory{#source-figure-011 caption=“Source slide page 11: Формат directory”}

каталог с оглавлением и отдельными файлами на каждый объект базы отдельные объекты базы данных можно выбрать на этапе восстановления и выгрузка, и загрузка возможны в несколько параллельных потоков

Комментарии преподавателя

Еще один формат резервной копии — directory. В таком случае будет создан не один файл, а каталог, содержащий объекты и оглавление. По умолчанию файлы внутри каталога будут сжаты.

Преимущество перед форматом custom состоит в том, что такая резервная копия может создаваться параллельно в несколько потоков (количество указывается в ключе -j, –jobs).

Разумеется, несмотря на параллельное выполнение, копия будет содержать согласованные данные. Это обеспечивается общим снимком данных для всех параллельно работающих процессов.

https://postgrespro.ru/docs/postgresql/16/functions-admin.html#FUNCTION S-SNAPSHOT-SYNCHRONIZATION

Восстановление также возможно в несколько потоков (это работает и для формата custom).

В остальном возможности по работе с форматом directory не отличаются от ранее рассмотренных: поддерживаются те же ключи и подходы.

Page 12: Сравнение форматов

plain custom directory tar

  • утилита для восстановления psql pg_restore
  • сжатие zlib
  • выборочное восстановление да да да
  • параллельное резервирование да
  • параллельное восстановление да да

Комментарии преподавателя

В приведенной таблице разные форматы сравниваются с точки зрения предоставляемых ими возможностей.

Отметим, что имеется и четвертый формат — tar. Он не рассматривался, так как не привносит ничего нового и не дает преимуществ перед другими форматами. Фактически он соответствует созданию tar-файла из каталога в формате directory, но не поддерживает сжатие и параллелизм.

Page 13: Утилита pg_dump

При запуске без дополнительных параметров утилита pg_dump выдает команды SQL, создающие все объекты в базе данных:

student$ pg_dump -d backup_logical_dev_1
--
-- PostgreSQL database dump
--
\restrict 3cutVS0EtJ1DeJVxxgugFk8mX1QevZhuoG0wiq67K1WJ6ykBGHGZw12HrPgPodG
-- Dumped from database version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
-- Dumped by pg_dump version 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
SET default_tablespace = '';
SET default_table_access_method = heap;
--
-- Name: t; Type: TABLE; Schema: public; Owner: student
--
CREATE TABLE public.t (
    id integer NOT NULL,
    s text
);
ALTER TABLE public.t OWNER TO student;
--
-- Name: t_id_seq; Type: SEQUENCE; Schema: public; Owner: student
--
ALTER TABLE public.t ALTER COLUMN id ADD GENERATED ALWAYS AS IDENTITY (
    SEQUENCE NAME public.t_id_seq
    START WITH 1
    INCREMENT BY 1
    NO MINVALUE
    NO MAXVALUE
    CACHE 1
);
--
-- Data for Name: t; Type: TABLE DATA; Schema: public; Owner: student
--
COPY public.t (id, s) FROM stdin;
1
Привет, мир!
2
3
\N
4
Строка четыре
5
Строка пять
\.
--
-- Name: t_id_seq; Type: SEQUENCE SET; Schema: public; Owner: student
--
SELECT pg_catalog.setval('public.t_id_seq', 5, true);

Page 14: Page 14

--
-- Name: t t_pkey; Type: CONSTRAINT; Schema: public; Owner: student
--
ALTER TABLE ONLY public.t
    ADD CONSTRAINT t_pkey PRIMARY KEY (id);
--
-- PostgreSQL database dump complete
--
\unrestrict 3cutVS0EtJ1DeJVxxgugFk8mX1QevZhuoG0wiq67K1WJ6ykBGHGZw12HrPgPodG

Команда \restrict запрещает выполнение других метакоманд psql (которые мог сгенерировать злоумышленник) до соответствующей ей команды \unrestrict. Видно, что pg_dump создал таблицу t, добавил автоматическую генерацию идентификатора, заполнил таблицу с помощью уже рассмотренной нами команды COPY, и наконец добавил ограничение целостности для первичного ключа. Ключ –column-inserts позволяет использовать команды INSERT, но загрузка будет работать существенно дольше. Рассмотрим некоторые полезные ключи. Могут пригодиться при восстановлении копии на системе с другим набором ролей: -O, –no-owner — не генерировать команды для установки владельца объектов; -x, –no-acl — не генерировать команды для установки привилегий. Полезны для выгрузки и загрузки данных частями: -s, –schema-only — выгрузить только определения объектов без данных; -a, –data-only — выгрузить только данные, без создания объектов. Удобны, если восстанавливать копию на системе, в которой уже есть данные (и наоборот, на чистой системе): -c, –clean — генерировать команды DROP для создаваемых объектов; -C, –create — генерировать команды создания БД и подключения к ней. Важный момент: в выгрузку попадают и изменения, сделанные в шаблонной БД template1. Поэтому восстанавливать резервную копию лучше на базе данных, созданной из template0. При использовании ключа –create это учитывается автоматически:

student$ pg_dump --create -d backup_logical_dev_1 | grep 'CREATE DATABASE'
CREATE DATABASE backup_logical_dev_1 WITH TEMPLATE = template0 ENCODING = 'UTF8'
LOCALE_PROVIDER = libc LOCALE = 'en_US.UTF-8';

Команда для создания схемы public не формируется, но если права на схему или ее владелец были изменены, то соответствующие команды появятся в выводе pg_dump:

=> ALTER SCHEMA public OWNER TO student;
ALTER SCHEMA
student$ pg_dump --create -d backup_logical_dev_1 | grep 'SCHEMA'
-- Name: public; Type: SCHEMA; Schema: -; Owner: student
ALTER SCHEMA public OWNER TO student;

Существуют ключи для выбора объектов, которые должны попасть в резервную копию: -n, –schema — шаблон для имен схем; -t, –table — шаблон для имен таблиц. И наоборот, включить в копию все, кроме указанного: -N, –exclude-schema — шаблон для имен схем; -T, –exclude-table — шаблон для имен таблиц. Например, восстановим таблицу t в другой базе данных.

=> CREATE DATABASE backup_logical_dev_2;
CREATE DATABASE
student$ pg_dump --table=t -d backup_logical_dev_1 | psql -d backup_logical_dev_2
SET
SET

Page 15: Page 15

SET
SET
SET
 set_config
------------
(1 row)
SET
SET
SET
SET
SET
SET
CREATE TABLE
ALTER TABLE
ALTER TABLE
COPY 5
 setval
--------
      5
(1 row)
ALTER TABLE

Подключимся к базе данных backup_logical_dev_2 и проверим:

=> \c backup_logical_dev_2
You are now connected to database "backup_logical_dev_2" as user "student".
=> SELECT * FROM t;
 id |       s
----+---------------
  1 | Привет, мир!
  2 |
  3 | \N
  4 | Строка четыре
  5 | Строка пять
(5 rows)

Утилита pg\_dump — формат custom

Серьезное ограничение обычного формата (plain) состоит в том, что выбирать объекты нужно в момент выгрузки. Формат custom позволяет сначала сделать полную копию, а выбирать объекты уже при загрузке.

student$ pg_dump --format=custom -d backup_logical_dev_1 -f /home/student/backup_logical_dev_1.custom

Для восстановления объектов из такой копии предназначена утилита pg_restore. Повторим восстановление таблицы t.

=> DROP TABLE t;
DROP TABLE

Формат резервной копии указывать не обязательно — утилита распознает его сама. Утилита pg_restore понимает те же ключи для фильтрации объектов, что и pg_dump, и даже больше: -I, –index — загрузить определенные индексы; -P, –function — загрузить определенные функции; -T, –trigger — загрузить определенные триггеры.

student$ pg_restore --table=t -d backup_logical_dev_2 /home/student/backup_logical_dev_1.custom
=> SELECT * FROM t;
 id |       s
----+---------------
  1 | Привет, мир!
  2 |
  3 | \N
  4 | Строка четыре
  5 | Строка пять
(5 rows)

Еще один пример: восстановим целиком исходную базу данных backup_logical_dev_1.

=> DROP DATABASE backup_logical_dev_1;

Page 16: Page 16

DROP DATABASE

В ключе -d мы указываем любую существующую базу данных; с ключом –create утилита сама создаст базу, указанную в архиве, и тут же переключится в нее.

student$ pg_restore --create -d student /home/student/backup_logical_dev_1.custom

Проверим:

=> \c backup_logical_dev_1
You are now connected to database "backup_logical_dev_1" as user "student".
=> SELECT * FROM t;
 id |       s
----+---------------
  1 | Привет, мир!
  2 |
  3 | \N
  4 | Строка четыре
  5 | Строка пять
(5 rows)

Резервную копию в обычном (plain) формате при необходимости можно изменить в текстовом редакторе. Резервная копия формата custom хранится в двоичном виде, но и для нее доступны более широкие возможности фильтрации объектов, чем рассмотренные ключи. Утилита pg_restore может сформировать список объектов — оглавление резервной копии:

student$ pg_restore --list /home/student/backup_logical_dev_1.custom
;
; Archive created at 2026-04-10 15:42:31 MSK
;     dbname: backup_logical_dev_1
;     TOC Entries: 10
;     Compression: gzip
;     Dump Version: 1.15-0
;     Format: CUSTOM
;     Integer: 4 bytes
;     Offset: 8 bytes
;     Dumped from database version: 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
;     Dumped by pg_dump version: 16.11 (Ubuntu 16.11-1.pgdg24.04+1)
;
;
; Selected TOC Entries:
;
5; 2615 2200 SCHEMA - public student
216; 1259 16388 TABLE public t student
215; 1259 16387 SEQUENCE public t_id_seq student
3433; 0 16388 TABLE DATA public t student
3440; 0 0 SEQUENCE SET public t_id_seq student
3288; 2606 16394 CONSTRAINT public t t_pkey student

Такой список можно записать в файл, отредактировать и использовать его для восстановления с помощью ключа –use-list.

Утилита pg\_dump — формат directory

Формат directory интересен тем, что позволяет выгружать данные в несколько параллельных потоков. При этом гарантируется согласованность данных: все потоки будут использовать один и тот же снимок данных.

student$ pg_dump --format=directory --jobs=2 -d backup_logical_dev_1 -f
/home/student/backup_logical_dev_1.directory

Заглянем внутрь каталога:

student$ ls -l /home/student/backup_logical_dev_1.directory
total 8
-rw-rw-r-- 1 student student   95 апр 10 15:42 3432.dat.gz
-rw-rw-r-- 1 student student 2286 апр 10 15:42 toc.dat

В нем находится файл оглавления и по одному файлу на каждый выгружаемый объект (у нас он всего один):

student$ zcat /home/student/backup_logical_dev_1.directory/3432.dat.gz
1
Привет, мир!
2
3
\N
4
Строка четыре

Page 17: Page 17

5
Строка пять
\.

Восстановим базу backup_logical_dev_1 из резервной копии, используя два потока. Здесь мы добавляем ключ –clean, который генерирует команду удаления БД, поскольку backup_logical_dev_1 существует. И предварительно надо отключиться от backup_logical_dev_1:

=> \c backup_logical_dev_2
You are now connected to database "backup_logical_dev_2" as user "student".
student$ pg_restore --clean --create --jobs=2 -d student /home/student/backup_logical_dev_1.directory

Page 18: Копия кластера БД

Source slide page 18: Копия кластера БД{#source-figure-018 caption=“Source slide page 18: Копия кластера БД”}

--
-- PostgreSQL database clu
--
SET default_transaction_re
SET client_encoding = 'UTF
SET standard_conforming_st
--
-- Roles
--
CREATE ROLE postgres;
  • формат: команды SQL выгружает весь кластер, включая роли и табличные пространства пользователь должен иметь доступ ко всем объектам кластера не поддерживает параллельную выгрузку

Комментарии преподавателя

Чтобы создать резервную копию всего кластера, включая роли и табличные пространства, можно воспользоваться утилитой pg_dumpall.

Поскольку pg_dumpall требуется доступ ко всем объектам всех БД, запускать ее следует суперпользователю или пользователю, включенному в предустановленную роль pg_read_all_data. Утилита по очереди подключается к каждой БД кластера и выгружает информацию с помощью pg_dump. Кроме того, она сохраняет и данные, относящиеся к кластеру в целом.

Чтобы начать работу, утилите требуется подключиться хотя бы к какой-то базе данных. По умолчанию выбирается postgres или template1, но можно указать и другую.

Результатом работы pg_dumpall является скрипт для psql. Другие форматы не поддерживаются. Это означает, что pg_dumpall не поддерживает параллельную выгрузку данных, что может оказаться проблемой при больших объемах данных. В таком случае можно воспользоваться ключом –globals-only, чтобы выгрузить только роли и табличные пространства, а сами базы данных выгружать отдельно с помощью pg_dump в параллельном режиме.

https://postgrespro.ru/docs/postgresql/16/app-pg-dumpall

Page 19: Утилита pg_dumpall

Утилита pg_dump годится для выгрузки одной базы данных, но никогда не выгружает общие объекты кластера БД, такие, как роли и табличные пространства. Чтобы сделать полную копию кластера, нужна утилита pg_dumpall. Все рассматриваемые в этой теме утилиты не требуют каких-то отдельных привилегий, но у выполняющей их роли должны быть привилегии на чтение (создание) всех затронутых объектов. Утилитой pg_dump может, например, пользоваться владелец базы данных. Но поскольку для копирования кластера утилите pg_dumpall надо иметь доступ ко всем базам данных, будем использовать роль с атрибутом суперпользователя.

student$ pg_dumpall --clean -f /home/student/main.sql

В копию кластера дополнительно попадают такие команды, как:

student$ grep 'ROLE' /home/student/main.sql
DROP ROLE postgres;
DROP ROLE student;
CREATE ROLE postgres;
ALTER ROLE postgres WITH SUPERUSER INHERIT CREATEROLE CREATEDB LOGIN REPLICATION
BYPASSRLS PASSWORD
'SCRAM-SHA-256$4096:VFactPz0/OIk23KN4tl9RA==$0IO5QZttKalKrZAG+gmjjnvTyjk6hqrGSJZk75D8D0s=:
Ddi6HwMT/5s52zLiUJzMbbFRmWegm3UBrdd07IFE8NU=';
CREATE ROLE student;
ALTER ROLE student WITH SUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION
NOBYPASSRLS PASSWORD
'SCRAM-SHA-256$4096:INTKa+M9YgvWbjvGK2Bcyw==$+vlrcD2yKCYXLhg0pZahND/DoBZL7UJoesq76hrH3eM=:
y1U20OYj6esdULduHVmq+mVgAzI67zuO/mgQF/0IYEg=';

Восстановление выполняется с помощью psql — никакой другой формат не поддерживается. Команда для восстановления (мы не будем ее выполнять):

student$ psql -f main.sql

В процессе восстановления могут возникать ошибки из-за существующих объектов — обычно это нормально и не мешает процессу, хотя все сообщения стоит проанализировать.

Page 20: Итоги

Логическое резервирование позволяет сделать копию всего кластера, базы данных или отдельных объектов

  • Хорошо подходит
    • для данных небольшого объема
    • для длительного хранения, за время которого меняется версия сервера
    • для миграции на другую платформу
  • Плохо подходит
    • для восстановления после сбоя с минимальной потерей данных

Page 21: Практика

  1. Создайте резервную копию базы данных bookstore в формате custom; действуйте от лица сотрудника магазина.
  • «Случайно» удалите все записи из таблицы authorship.
  • Проверьте, что приложение перестало отображать названия
  • книг на вкладках «Магазин», «Книги», «Каталог».
  • Используйте резервную копию для восстановления
  • потерянных данных в таблице.
  • Проверьте, что нормальная работа книжного магазина
  • восстановилась.

Комментарии преподавателя

  1. Включите пользователя в предопределенную роль pg_read_all_data. При восстановлении используйте ключ –data-only, чтобы избежать ошибки при попытке создания таблицы.

Page 22: 1. Восстановление потерянных данных

Включим пользователя employee в предопределенную роль pg_read_all_data и от его лица выполним создание резервной копии:

=> GRANT pg_read_all_data TO employee;
GRANT ROLE
student$ pg_dump --format=custom 'host=localhost user=employee dbname=bookstore password=employee' >
/home/student/bookstore.custom

Удаляем строки:

=> DELETE FROM authorship;
DELETE 9

Выполнить восстановление у employee не получится — у него нет прав записи в таблицу, поэтому это делает student:

student$ pg_restore -t authorship --data-only -d bookstore /home/student/bookstore.custom
=> SELECT count(*) FROM authorship;
 count
-------
     9
(1 row)

Page 23: Практика+

  1. Команда psql \copy позволяет направить результат на вход произвольной программы. Воспользуйтесь этим, чтобы открыть результаты какого-нибудь запроса в электронной таблице LibreOffice Calc.

Комментарии преподавателя

  1. Команда должна перенаправить результат в файл, а затем запустить libreoffice, указав этот файл в качестве параметра. Файл должен быть записан в формате CSV.

Конечно, такой способ зависит от платформы и без модификации не будет работать, например, в Windows.

Page 24: 1. Открытие результата запроса в LibreOffice

Создаем таблицу.

=> CREATE DATABASE backup_logical_dev;
CREATE DATABASE
=> \c backup_logical_dev
You are now connected to database "backup_logical_dev" as user "student".
=> CREATE TABLE t(
    id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    s text
);
CREATE TABLE
=> INSERT INTO t(s) VALUES ('foo'), ('bar'), ('baz');
INSERT 0 3

Выгружаем содержимое таблицы в csv-файл:

=> \copy t TO PROGRAM 'cat > t.csv' WITH (format csv);
COPY 3

Если вместо \copy использовать SQL-команду COPY, программа будет запущена на сервере СУБД, что, конечно, неправильно. Открываем /home/student/t.csv…

student$ xdg-open /home/student/t.csv
=> \c postgres
You are now connected to database "postgres" as user "student".
=> DROP DATABASE backup_logical_dev;
DROP DATABASE