09 Занятие 09: Расширяемость / Пользовательские типы данных
Эта страница извлечена из официального PDF в нативный Markdown. Текст слайдов, код и комментарии сохранены в порядке чтения; исходные PDF и HTML остаются эталоном макета. HTML source: dev2_09_ext_datatypes.html Archive: DEV2-handouts-16.zip Страниц PDF: 29
Page 1: Расширяемость / Пользовательские типы данных
Авторские права
© Postgres Professional, 2017–2024 Авторы: Егор Рогов, Павел Лузанов, Илья Баштанов, Игорь Гнатюк Фото: Олег Бартунов (монастырь Пху и пик Бхрикути, Непал)
Использование материалов курса
Некоммерческое использование материалов курса (презентации, демонстрации) разрешается без ограничений. Коммерческое использование возможно только с письменного разрешения компании Postgres Professional. Запрещается внесение изменений в материалы курса.
Обратная связь
Отзывы, замечания и предложения направляйте по адресу: edu@postgrespro.ru
Отказ от ответственности
Компания Postgres Professional не несет никакой ответственности за любые повреждения и убытки, включая потерю дохода, нанесенные прямым или непрямым, специальным или случайным использованием материалов курса. Компания Postgres Professional не предоставляет каких-либо гарантий на материалы курса. Материалы курса предоставляются на основе принципа «как есть» и компания Postgres Professional не обязана предоставлять сопровождение, поддержку, обновления, расширения и изменения.
Page 2: Темы
- Составные типы (краткое повторение)
- Типы перечислений
- Диапазонные типы и мультидиапазоны
- Базовые типы
- Домены
- Приведение типов, операторы
Page 3: Расширяемость типов
Богатый набор существующих типов
- числа, строки, даты, слабоструктурированные, геометрия, …
- массивы
- Средства для создания новых типов
- простые: на основе уже имеющихся типов
- сложные: новый тип «с нуля»
Комментарии преподавателя
PostgreSQL имеет множество встроенных типов данных (и поддержку массивов элементов любых типов данных).
https://postgrespro.ru/docs/postgresql/16/datatype
Вместе с тем имеются и средства для создания своих собственных типов данных.
В простом случае новые типы создаются по одной из предопределен-ных схем из других, уже имеющихся, типов. Это составные типы, перечисления, диапазоны, домены.
Если же необходимый тип данных не может быть выражен с помощью этих простых средств, можно создать новый тип «с нуля», детально описав его поведение на низком уровне.
Page 4: Составные типы
Набор именованных атрибутов (табличная строка)
- Создание нового типа
- вручную: CREATE TYPE AS
- автоматически при создании отношения
Комментарии преподавателя
Составные типы представляют собой набор именованных атрибутов каких-либо других типов данных. В разных языках программирования такие типы могут называться структурами или записями.
По сути, составной тип описывает табличную строку и поэтому создается автоматически при создании любой таблицы или представления.
Новый составной тип можно создать и вручную.
Составные типы определены в стандарте SQL. Они подробно рассматриваются в курсе DEV1, поэтому здесь мы не будем на них останавливаться.
https://postgrespro.ru/docs/postgresql/16/rowtypes
https://postgrespro.ru/docs/postgresql/16/functions-comparisons#COMPOSI TE-TYPE-COMPARISON
Page 5: Типы перечислений
Упорядоченный набор значений
- значения выглядят как текстовые строки
- хранятся как 4-байтовые числа (oid)
- Создание нового типа
- только вручную: CREATE TYPE AS ENUM
Комментарии преподавателя
Тип перечисления создается простым перечислением всех допустимых значений.
Значения задаются и используются в виде текстовых строк, которые хранятся в системном каталоге как упорядоченный набор значений. При использовании перечисления, например, в качестве типа столбца таблицы, каждое его значение будет представлено в виде 4-байтного целого числа (фактически, тип oid). Это позволяет использовать длинные (до 63 символов) описательные значения, не экономя на месте в таблице.
Стандартных типов перечисления в PostgreSQL нет.
https://postgrespro.ru/docs/postgresql/16/datatype-enum
https://postgrespro.ru/docs/postgresql/16/functions-enum
Page 6: Типы перечислений
Создадим новые типы перечислений для старшинства карт:
И для мастей:
Напомним, что тип данных — отдельный объект базы данных, который хранится в системном каталоге:
Чтобы вспомнить и составные типы, определим еще тип для карты:
Обратите внимание, что названия значений перечислимого типа регистрозависимы. Все значения в перечислении упорядочены, и этим можно пользоваться:
Однако сами типы перечислений считаются уникальными и поэтому значения разных перечислений нельзя сравнивать, даже если они определены одинаково:
Page 7: Page 7
При необходимости выполнить подобное сравнение придется либо реализовать собственный оператор, либо явно преобразовать типы в запросе, например так:
Можно получить список значений в виде массива (функция enum_range смотрит только на тип параметра; само значение может быть любым):
Кажется, мы забыли валета. Не беда, его можно добавить к перечислению:
А вот удалить значение из типа невозможно. Единственный вариант — удалить тип и создать его заново. Если мы заглянем в системный каталог, то увидим, что упорядоченность значений перечисления обеспечивает столбец типа real. Обратите внимание на его дробное значение для добавленного нами валета:
Page 8: Диапазонные типы
Непрерывный отрезок значений от начальной до конечной точки
- открытый (>, <), закрытый (≤, ≥), неограниченный
- Создание нового типа
- CREATE TYPE AS RANGE
- уже есть для integer, bigint, numeric, timestamp, date
- Операции
- сравнение
- проверка на включение диапазона или элемента, на пересечение
- объединение, пересечение, вычитание
- и др.
Комментарии преподавателя
Диапазонный тип представляет непрерывный отрезок значений скалярного типа. Концы отрезка могут быть открытыми (граничное значение не включается) или закрытыми (наоборот, включается), или отрезок может быть бесконечным, то есть не иметь одной или обеих границ.
Для диапазонов определено довольно много удобных операций, в том числе:
● проверки на включение одного диапазона в другой, на принадлежность значения диапазона, на пересечение двух диапазонов;
● обычные операции над множествами: объединение, пересечение, вычитание.
Диапазоны предопределены для различных числовых типов и дат (например, int4range для integer и др.). Можно создать и свой собственный диапазонный тип.
https://postgrespro.ru/docs/postgresql/16/rangetypes
https://postgrespro.ru/docs/postgresql/16/functions-range
Page 9: Мультидиапазоны
Множество диапазонов
- не должны пересекаться
- Создание нового типа
- автоматически при создании диапазона
- Операции
- сравнение
- проверка на включение мультидиапазона, диапазона или элемента
- проверка на пересечение
- объединение, пересечение, вычитание
- и др.
Комментарии преподавателя
Для каждого диапазонного типа определен соответствующий мультидиапазонный тип. Такой тип по сути представляет собой массив отдельных диапазонов. Диапазоны внутри мультидиапазонного типа не должны пересекаться между собой, но между диапазонами могут быть пропуски. Большинство диапазонных операторов работают и с мультидиапазонами. Кроме того, есть функции для работы именно с мультидиапазонными типами.
Мультидиапазонный тип определяется автоматически при определении диапазонного типа. По умолчанию имя мультидиапазонного типа генерируется PostgreSQL, но можно задать его и явно.
Page 10: Диапазонные типы
Начнем с нескольких примеров работы с уже имеющимися диапазонными типами. Диапазон целых чисел от 1 до 9, обе границы включаются (об этом говорят квадратные скобки). Что будет выведено?
Результат выводится в каноническом виде, в котором левая граница включается, а правая — нет (круглая скобка). Конечно, значение правой границы при этом увеличивается на единицу. Канонический вид определен не для всех типов. Диапазон может быть и открытым. Проверим, принадлежит ли число диапазону:
Точно так же можно проверить включение одного диапазона в другой:
Удобны функции, позволяющие работать с диапазонами как с множествами. Например, можно легко найти пересечение (общие точки) двух диапазонов:
При создании нового диапазонного типа указываются: базовый тип, значения которого должны быть сортируемыми; функция разности двух значений, которая должна возвращать результат типа double precision. Например, в PostgreSQL нет типа для диапазонов времени (без даты). Чтобы определить его, нам потребуется функция, возвращающая разность между двумя временами. Обычная разность возвращает значение типа interval:
Но мы можем воспользоваться функцией extract epoch, которая вернет длину диапазона в секундах:
Page 11: Page 11
Теперь можно определить тип диапазона, а вместе с ним — и мультидиапазона:
Последний параметр — необязательный, в его отсутствие соответствующий мультидиапазонный тип будет создан автоматически. Новый тип можно использовать аналогично уже существующим. Например, найдем объединение (здесь показаны два способа записи констант диапазонного типа):
Заметьте, что диапазон не приводится к каноническому виду. Чтобы это работало, необходимо реализовать соответствующую функцию и указать ее в определении типа. Мы не будем этого делать. Новый мультидиапазонный тип позволяет представить несколько диапазонов как одно значение:
Обратите внимание, что пересекающиеся диапазоны были объединены.
Page 12: Интервалы дат и времени
Интервал значений определенной длины
- Операции
- арифметика дат и времени
- функции корректировки
Комментарии преподавателя
В PostgreSQL есть тип interval, на первый взгляд похожий на диапазон. Но, в отличие от диапазонных типов, интервал определяется длиной, а не начальной и конечной точкой. Кроме того, интервалы существуют только для дат и времени.
Отметим также, что при работе с этим типом есть ряд тонкостей, обусловленных особенностями хранения значений интервалов. Например, существуют специальные функции для корректировки числа дней и часов при их выходе за обычные границы (одну из них мы увидим в демонстрации).
Создание собственных типов, аналогичных interval, невозможно. Здесь мы показываем этот тип только для полноты картины и не будем останавливаться на нем подробно.
https://postgrespro.ru/docs/postgresql/16/datatype-datetime
https://postgrespro.ru/docs/postgresql/16/functions-datetime
Page 13: Интервалы
Значения типа interval определяют длину временного отрезка, в отличие от диапазонного типа, значения которого состоят из начальной и конечной точек. Интервал появляется естественным образом при вычитании двух моментов времени:
(Хотя та же операция для дат (тип date) возвращает целое число.) Интервалы используются в арифметике дат и времени. Например:
Обратите внимание, что интервал ‘1 month’ может иметь разную длину — от 28 до 31 дней в зависимости от месяца. При необходимости можно преобразовать интервал так, чтобы, например, каждый 24-часовой период считался одним днем:
Page 14: Домены
Ограничение допустимых значений существующего типа
- ограничение NOT NULL
- значение по умолчанию DEFAULT
- проверка CHECK
- Создание нового типа
- только вручную: CREATE DOMAIN
Комментарии преподавателя
Домен создается на базе любого существующего типа данных и служит для ограничения множества допустимых значений этого типа.
Определяя домен, можно запретить неопределенные значения (и определить значение по умолчанию), можно также указать произвольную проверку допустимости значения.
По сути, это похоже на ограничения целостности, накладываемые на столбцы при создании таблицы. Создав тип домена, его можно использовать в нескольких таблицах, не указывая каждый раз одни и те же ограничения.
Домены определены в стандарте SQL.
https://postgrespro.ru/docs/postgresql/16/domains
https://postgrespro.ru/docs/postgresql/16/sql-createdomain
Page 15: Домены
Создадим доменный тип, ограничивающий временной диапазон рабочими часами и запрещающий неопределенные значения:
Его можно использовать при создании таблицы:
А так получится?
Нет — нарушено условие проверки. А так?
Нет — неопределенные значения не допускаются. А если так?
А так получится. Запрет неопределенных значений на уровне домена работает не так, как на уровне столбца. Ограничение NOT NULL надежнее указывать при создании таблицы. Еще одна проблема связана с тем, что при любых операциях значения приводятся к базовому типу. Это не позволяет использовать домены для контроля типов в операциях. Вот пример:
Можно ли складывать метры с граммами?
Оказывается, можно. Поэтому практическое применение доменных типов довольно ограничено.
Page 16: Базовые типы
Тип, не сводящийся к комбинации существующих
- Создание нового типа
- CREATE TYPE, требует программирования на языке C
- множество типов предоставляются расширениями
Комментарии преподавателя
Иногда может возникнуть потребность в совершенно новом типе, который нельзя представить как комбинацию уже существующих.
В этом случае потребуется определить все низкоуровневые детали внутреннего устройства типа, что можно сделать только на языке C. Поэтому мы не будем рассматривать эту возможность.
Впрочем, множество различных типов данных доступны в виде расширений (в том числе сторонних), так что вероятность того, что потребуется совершенно новый тип, очень мала.
https://postgrespro.ru/docs/postgresql/16/sql-createtype
Page 17: Действия над типами
Функции, принимающие или возвращающие значения типа
- Приведение типов
- преобразование значения одного типа к другому типу
- Операторы
- унарные (префиксные)
- бинарные (инфиксные)
Комментарии преподавателя
Понятно, что мало определить новый тип данных — надо уметь его как-то использовать.
Написанию функций, принимающих или возвращающих значения произвольных (в том числе определенных пользователем) типов посвящен курс DEV1, поэтому на этом мы не будем специально останавливаться.
На функциях основаны и другие важные объекты — приведения типов и операторы, — которые могут определяться пользователем.
Приведения типов позволяют преобразовывать значения одного типа к другому.
Операторы позволяют вместо вызова функции f(x,y) использовать инфиксную нотацию, например x+y, что позволяет сделать код проще и нагляднее. Кроме того, как мы увидим в следующей теме «Классы операторов», операторы играют важную роль в индексировании.
Приведение типов в документации:
https://postgrespro.ru/docs/postgresql/16/sql-createcast
Операторы:
https://postgrespro.ru/docs/postgresql/16/xoper
https://postgrespro.ru/docs/postgresql/16/xoper-optimization
https://postgrespro.ru/docs/postgresql/16/sql-createoperator
Page 18: Приведение типов
В качестве примера рассмотрим приведение типа timerange к типу interval. Сработает ли такое приведение само по себе?
Увы, нет. Чтобы создать такое приведение, сначала напишем функцию:
Теперь можно создать приведение типов. Здесь мы указываем AS IMPLICIT, чтобы приведение срабатывало и неявно.
Операторы
Пусть требуется узнать, сколько раз заданный интервал (тип interval) содержится в другом интервале. Например, сколько в интервале часов, или минут, или пятисекундных отрезков. Иными словами, нужно деление одного интервала на другой. Но в PostgreSQL для интервалов определен только оператор деления на целое число. Напишем соответствующую функцию:
Теперь мы можем узнать, сколько раз можно послушать композицию Your Latest Trick, длящуюся 6 минут 33 секунды, если у нас есть полтора часа времени:
Определим бинарный оператор, используя эту функцию:
Page 19: Page 19
Имя оператора можно составлять только из специальных символов, список которых приведен в документации к команде CREATE OPERATOR. Если не указать LEFTARG, будет определен префиксный оператор. Постфиксные операторы не поддерживаются начиная с PostgreSQL 14. Теперь можно пользоваться удобной инфиксной формой записи (здесь также показан другой способ записи интервалов):
Page 20: Итоги
PostgreSQL позволяет создавать новые типы данных
- составные, перечисления, диапазоны и мультидиапазоны, домены
- базовые типы
- Для работы со значениями типов могут создаваться функции, операторы и приведения типов
Page 21: Практика
- Реализуйте в приложении возможность установки розничной цены книг с указанной пользователем даты (сейчас дата просто игнорируется). Предыдущая история изменений цен должна сохраняться. Для этого добавьте в таблицу retail_prices информацию о периоде действия цены и внесите необходимые изменения в функции get_retail_price и set_retail_price. 2. Создайте составной тип для формата издания, состоящего из размеров типографского листа и доли листа. Также создайте приведение для этого нового типа к текстовому. Замените тип данных столбца format таблицы books и убедитесь, что интерфейс с приложением не изменился.
Комментарии преподавателя
- Интервал действия розничной цены можно представить по-разному:
● двумя столбцами типа timestamptz («дата с» и «дата по»);
● одним столбцом типа timestamptz («дата с»), при этом цена считается действующей до следующей даты в другой строке таблицы;
● одним столбцом диапазонного типа tstzrange.
Реализуйте последний вариант. Обратите внимание:
● В функции set_retail_price придется изменять две строки таблицы, и это должно корректно работать в случае, если несколько пользователей устанавливают цену одновременно.
● Функция get_retail_price должна получать цену на текущий момент. Используйте функцию current_timestamp (возвращающую время начала транзакции), а не clock_timestamp. Это будет важно при последующих изменениях.
- Формат издания записывается в виде WxH/N, где W и H — ширина и высота типографского листа, с которым работает печатная машина, и который разрезается потом на N одинаковых частей (страниц книги). Поэтому N как правило представляет собой степень двойки.
Несмотря на то, что отношение длины страницы к ширине может быть разным, форматы книг можно упорядочить, сравнивая площади страниц, которые равны W*H/N.
При замене типа столбца обратите внимание на блокировку, которая при этом удерживается.
Page 22: 1. Розничная цена на книги
Добавим в таблицу retail_prices столбец с диапазоном дат. Значение по умолчанию — неограниченный диапазон.
Функция получения текущей цены должна выбрать тот диапазон, в который входит текущая дата:
Функция установки цены должна изменить одну строку и добавить одну новую. Использование уровня изоляции Read Committed в этом случае может приводить к аномалиям, поэтому сначала заблокируем соответствующую запись в таблице books, чтобы в один момент времени только одна транзакция могла устанавливать цену одной и той же книги.
Проверим.
Page 23: Page 23
Вставка новой цены в конец:
Вставка не в конец:
Цена с уже существующей датой (новый интервал не появляется):
2. Тип данных для формата издания
Тип данных:
Page 24: Page 24
Преобразование в текст:
Проверим:
И обратно:
Проверим:
Обратите внимание, что если написать просто:
то произойдет ошибка: здесь срабатывает не приведение типа text в book_format, а создание типа book_format из литерала, которое использует фиксированный формат для всех составных типов:
Теперь заменим тип столбца. Это тяжелая операция, которая перезаписывает всю таблицу, полностью блокируя работу с ней. Поэтому использовать ее надо с осторожностью, особенно для больших таблиц. Вот в каком файле находятся данные сейчас:
Выполняем замену типа. Преобразование будет выполнено автоматически благодаря созданному ранее приведению типов:
Page 25: Page 25
Операция выполняется в несколько шагов, но при перезаписи таблицы используется исключительная блокировка. В этой же транзакции надо внести изменения и в интерфейсные функции приложения. Формат возвращают две функции: webapi.get_catalog и empapi.get_catalog, но обе они вызывают одну и ту же функцию public.get_catalog. К счастью, в этой функции столбец format явно приводится к типу text, поэтому никаких изменений не требуется.
Теперь данные таблицы находятся в другом файле:
Page 26: Практика+
- Производственные смены каждого сотрудника фабрики хранятся в таблице. Напишите запрос, который покажет, сколько человеко-часов пропадет даром, если в некоторый момент времени на 15 минут будет отключено электричество. 2. В таблице, содержащей много данных, используется столбец статуса, имеющий тип перечисления. Проверьте, какие блокировки будут установлены на таблицу при необходимости добавления нового статуса, и какое время занимает эта операция. Сравните с альтернативным подходом, при котором перечисление реализуется как обычный текстовый тип с ограничением CHECK на уровне столбца или домена.
Комментарии преподавателя
Производственные смены могут храниться в таблице вида: CREATE TABLE shifts ( employee_name text, work_hours tstzrange );
Блокировки можно посмотреть в таблице pg_locks, как говорилось в теме «Обзор блокировок».
Кроме обычной проверки status text CHECK (status IN (…))
рассмотрите также вариант с использованием домена.
Page 27: 1. Человеко-часы
Таблица с производственными сменами:
Добавим несколько записей (рабочий день с перерывом на обед):
Результат:
2. Блокировки при добавлении значения
Тип перечисления и таблица:
Начинаем транзакцию:
Page 28: Page 28
Выведем только блокировки таблицы:
Итак: добавление выполняется быстро; таблица не блокируется. Вариант с проверкой CHECK.
Здесь: добавление выполняется дольше — поскольку требуется перепроверка всех значений в таблице; таблица полностью блокируется на время изменения ограничения. Это плохой вариант.
Page 29: Page 29
Вариант с проверкой на домене.
В этом случае: изменение также выполняется долго; таблица блокируется в более мягком режиме (конфликтует с изменением данных, но разрешает чтение). Это вариант несколько лучше, чем CHECK.