Составной тип представляет структуру строки или записи; по существу, это просто список имён полей и их типов данных. Digital Q.DataBase позволяет использовать составные типы во многих случаях, где можно использовать простые типы. Например, столбец таблицы может быть объявлен как составной тип.
Вот два простых примера определения составных типов:
CREATE TYPE complex AS (
r double precision,
i double precision
);
CREATE TYPE inventory_item AS (
name text,
supplier_id integer,
price numeric
);
Синтаксис сравним с CREATE TABLE, за исключением того, что можно
указывать только имена полей и их типы; ограничения (такие как NOT
NULL) в настоящее время включать нельзя. Обратите внимание, что ключевое слово
AS существенно; без него система решит, что подразумевается другая разновидность
команды CREATE TYPE, и вы получите странные синтаксические ошибки.
Определив типы, мы можем использовать их для создания таблиц:
CREATE TABLE on_hand (
item inventory_item,
count integer
);
INSERT INTO on_hand VALUES (ROW('fuzzy dice', 42, 1.99), 1000);
или функций:
CREATE FUNCTION price_extension(inventory_item, integer) RETURNS numeric AS 'SELECT $1.price * $2' LANGUAGE SQL; SELECT price_extension(item, 10) FROM on_hand;
Всякий раз, когда вы создаёте таблицу, автоматически создаётся и составной тип с тем же именем, что и у таблицы, для представления строчного типа таблицы. Например, если бы мы написали:
CREATE TABLE inventory_item (
name text,
supplier_id integer REFERENCES suppliers,
price numeric CHECK (price > 0)
);
то составной тип inventory_item, показанный выше,
возник бы как побочный продукт и мог бы использоваться так же, как описано выше.
Однако обратите внимание на важное ограничение текущей реализации:
поскольку с составным типом не связано ограничений, ограничения, указанные в определении таблицы,
не применяются к значениям составного типа
за пределами таблицы. (Чтобы обойти это, создайте
домен поверх составного
типа и примените нужные ограничения как ограничения CHECK
домена.)
Чтобы записать составное значение в виде литеральной константы, заключите значения полей в круглые скобки и разделите их запятыми. Вы можете заключать любое значение поля в двойные кавычки и должны делать это, если оно содержит запятые или круглые скобки. (Более подробно это описано ниже.) Таким образом, общий формат составной константы следующий:
'(val1,val2, ... )'
Пример:
'("fuzzy dice",42,1.99)'
Это будет допустимым значением типа inventory_item,
определённого выше. Чтобы сделать поле NULL, не пишите вообще никаких символов
на его позиции в списке. Например, эта константа указывает
третье поле как NULL:
'("fuzzy dice",42,)'
Если вам нужна пустая строка, а не NULL, напишите двойные кавычки:
'("",42,)'
Здесь первое поле — не-NULL пустая строка, третье — NULL.
(Эти константы на самом деле являются лишь частным случаем общих констант типов, обсуждаемых в Раздел 2.1.1.2.7. Константа изначально обрабатывается как строка и передаётся подпрограмме преобразования ввода составного типа. Может потребоваться явное указание типа, чтобы сообщить, в какой тип преобразовать константу.)
Синтаксис выражения ROW также может использоваться для
конструирования составных значений. В большинстве случаев это значительно
проще в использовании, чем синтаксис строкового литерала, поскольку вам не нужно
беспокоиться о нескольких уровнях кавычек. Мы уже использовали этот
метод выше:
ROW('fuzzy dice', 42, 1.99)
ROW('', 42, NULL)
Ключевое слово ROW на самом деле необязательно, если в выражении более одного поля, поэтому эти примеры можно упростить до:
('fuzzy dice', 42, 1.99)
('', 42, NULL)
Синтаксис выражения ROW подробнее обсуждается в Раздел 2.1.2.13.
Чтобы получить доступ к полю составного столбца, пишут точку и имя поля,
очень похоже на выбор поля из имени таблицы. На самом деле, это настолько
похоже на выбор из имени таблицы, что часто приходится использовать круглые скобки,
чтобы не запутать анализатор. Например, вы можете попытаться выбрать
некоторые подполя из нашей примера таблицы on_hand с помощью чего-то вроде:
SELECT item.name FROM on_hand WHERE item.price > 9.99;
Это не сработает, потому что имя item, согласно правилам синтаксиса SQL,
воспринимается как имя таблицы, а не как имя столбца on_hand.
Вы должны написать это так:
SELECT (item).name FROM on_hand WHERE (item).price > 9.99;
или, если вам также нужно использовать имя таблицы (например, в многотабличном запросе), так:
SELECT (on_hand.item).name FROM on_hand WHERE (on_hand.item).price > 9.99;
Теперь объект в скобках правильно интерпретируется как ссылка на
столбец item, и затем из него может быть выбрано подполе.
Подобные синтаксические вопросы возникают всякий раз, когда вы выбираете поле из составного значения. Например, чтобы выбрать только одно поле из результата функции, возвращающей составное значение, вам нужно написать что-то вроде:
SELECT (my_func(...)).field FROM ...
Без дополнительных скобок это вызовет синтаксическую ошибку.
Специальное имя поля * означает «все поля», как
дополнительно объясняется в Раздел 2.5.16.5.
Вот несколько примеров правильного синтаксиса для вставки и обновления составных столбцов. Сначала вставка или обновление всего столбца:
INSERT INTO mytab (complex_col) VALUES((1.1,2.2)); UPDATE mytab SET complex_col = ROW(1.1,2.2) WHERE ...;
В первом примере ROW опущено, во втором используется; мы
могли бы сделать в любом из этих вариантов.
Мы можем обновить отдельное подполе составного столбца:
UPDATE mytab SET complex_col.r = (complex_col).r + 1 WHERE ...;
Обратите внимание, что здесь нам не нужно (и действительно нельзя)
ставить скобки вокруг имени столбца, появляющегося сразу после
SET, но нам нужны скобки при ссылке на тот же
столбец в выражении справа от знака равенства.
И мы также можем указывать подполя в качестве целей для INSERT:
INSERT INTO mytab (complex_col.r, complex_col.i) VALUES(1.1, 2.2);
Если бы мы не предоставили значения для всех подполей столбца, оставшиеся подполя были бы заполнены значениями NULL.
Существуют различные специальные правила синтаксиса и особенности поведения, связанные с составными типами в запросах. Эти правила предоставляют полезные сокращения, но могут сбивать с толку, если вы не знаете логику, стоящую за ними.
В Digital Q.DataBase ссылка на имя таблицы (или псевдоним)
в запросе фактически является ссылкой на составное значение текущей строки
таблицы. Например, если бы у нас была таблица
inventory_item, как показано
выше, мы могли бы написать:
SELECT c FROM inventory_item c;
Этот запрос производит один составной столбец, поэтому мы могли бы получить вывод, подобный:
c
------------------------
("fuzzy dice",42,1.99)
(1 row)
Однако обратите внимание, что простые имена сопоставляются с именами столбцов до имён таблиц,
поэтому этот пример работает только потому, что в таблицах запроса нет столбца
с именем c.
Обычный синтаксис квалифицированного имени столбца
имя_таблицы.имя_столбца
можно понимать как применение выбора поля
к составному значению текущей строки таблицы.
(По соображениям эффективности это на самом деле реализовано не так.)
Когда мы пишем
SELECT c.* FROM inventory_item c;
тогда, согласно стандарту SQL, мы должны получить содержимое таблицы, развёрнутое в отдельные столбцы:
name | supplier_id | price
------------+-------------+-------
fuzzy dice | 42 | 1.99
(1 row)
как если бы запрос был
SELECT c.name, c.supplier_id, c.price FROM inventory_item c;
Digital Q.DataBase будет применять это поведение развёртывания к
любому составному выражению, хотя, как показано выше, вам нужно писать скобки
вокруг значения, к которому применяется .*, всякий раз, когда это не простое имя таблицы.
Например, если myfunc() — это функция,
возвращающая составной тип со столбцами a,
b и c, то эти два запроса дают одинаковый
результат:
SELECT (myfunc(x)).* FROM some_table; SELECT (myfunc(x)).a, (myfunc(x)).b, (myfunc(x)).c FROM some_table;
Digital Q.DataBase обрабатывает развёртывание столбцов путём
фактического преобразования первой формы во вторую. Таким образом, в этом
примере myfunc() будет вызываться три раза для каждой строки
при любом синтаксисе. Если это дорогая функция, вы можете захотеть
избежать этого, что можно сделать с помощью запроса вида:
SELECT m.* FROM some_table, LATERAL myfunc(x) AS m;
Размещение функции в
элементе LATERAL FROM предотвращает её
вызов более одного раза на строку. m.* по-прежнему
разворачивается в m.a, m.b, m.c, но теперь эти переменные
являются просто ссылками на вывод элемента FROM.
(Ключевое слово LATERAL здесь необязательно, но мы показываем его,
чтобы прояснить, что функция получает x
из some_table.)
Синтаксис составное_значение.* приводит к
развёртыванию столбцов такого рода, когда он появляется на верхнем уровне
списка вывода SELECT,
списка RETURNING
в INSERT/UPDATE/DELETE/MERGE,
предложения VALUES или
конструктора строк.
Во всех остальных контекстах (включая вложенность внутри одной из этих
конструкций) присоединение .* к составному значению не
изменяет значение, поскольку это означает «все столбцы», и поэтому
снова создаётся то же составное значение. Например,
если somefunc() принимает аргумент составного значения,
эти запросы одинаковы:
SELECT somefunc(c.*) FROM inventory_item c; SELECT somefunc(c) FROM inventory_item c;
В обоих случаях текущая строка inventory_item
передаётся в функцию как единственный аргумент составного значения.
Даже если .* ничего не делает в таких случаях, его использование — это хороший
стиль, поскольку он даёт понять, что подразумевается составное значение. В
частности, анализатор будет считать c в c.*
ссылкой на имя таблицы или псевдоним, а не на имя столбца, так что нет
неоднозначности; тогда как без .* не ясно,
означает ли c имя таблицы или имя столбца, и на самом деле
интерпретация как имени столбца будет предпочтительнее, если есть столбец
с именем c.
Другой пример, демонстрирующий эти концепции, заключается в том, что все эти запросы означают одно и то же:
SELECT * FROM inventory_item c ORDER BY c; SELECT * FROM inventory_item c ORDER BY c.*; SELECT * FROM inventory_item c ORDER BY ROW(c.*);
Все эти предложения ORDER BY указывают составное
значение строки, что приводит к сортировке строк согласно правилам, описанным
в Раздел 2.6.25.6. Однако,
если inventory_item содержит столбец
с именем c, первый случай будет отличаться от
остальных, так как это будет означать сортировку только по этому столбцу. Учитывая приведённые ранее
имена столбцов, эти запросы также эквивалентны приведённым выше:
SELECT * FROM inventory_item c ORDER BY ROW(c.name, c.supplier_id, c.price); SELECT * FROM inventory_item c ORDER BY (c.name, c.supplier_id, c.price);
(В последнем случае используется конструктор строк с опущенным ключевым словом ROW.)
Ещё одна особенная синтаксическая особенность, связанная с составными значениями, —
это возможность использовать функциональную нотацию для извлечения поля
составного значения. Простой способ объяснить это — что
обозначения
и field(таблица)
взаимозаменяемы. Например, эти запросы эквивалентны:
таблица.field
SELECT c.name FROM inventory_item c WHERE c.price > 1000; SELECT name(c) FROM inventory_item c WHERE price(c) > 1000;
Более того, если у нас есть функция, принимающая один аргумент составного типа, мы можем вызвать её с любой нотацией. Эти запросы все эквивалентны:
SELECT somefunc(c) FROM inventory_item c; SELECT somefunc(c.*) FROM inventory_item c; SELECT c.somefunc FROM inventory_item c;
Эта эквивалентность между функциональной нотацией и нотацией поля
позволяет использовать функции для составных типов для реализации
«вычисляемых полей».
Приложение, использующее последний запрос выше, не должно быть напрямую
осведомлено, что somefunc не является реальным столбцом таблицы.
Из-за такого поведения неразумно давать функции, принимающей единственный
аргумент составного типа, то же имя, что и у любого из полей этого
составного типа. При неоднозначности будет выбрана интерпретация как имени поля,
если используется синтаксис имени поля, тогда как функция будет выбрана,
если используется синтаксис вызова функции. Однако в
версиях Digital Q.DataBase до 11 всегда выбиралась
интерпретация имени поля, если только синтаксис вызова не требовал, чтобы это был вызов функции.
Один из способов принудительно использовать интерпретацию как функции
в старых версиях — квалифицировать имя функции схемой, то есть написать
.
схема.функция(составное_значение)
Внешнее текстовое представление составного значения состоит из элементов,
которые интерпретируются согласно правилам преобразования ввода/вывода для отдельных
типов полей, плюс оформления, указывающего на составную структуру.
Оформление состоит из круглых скобок (( и ))
вокруг всего значения, плюс запятых (,) между соседними
элементами. Пробелы вне скобок игнорируются, но внутри скобок
они считаются частью значения поля и могут быть либо значимыми, либо нет
в зависимости от правил преобразования ввода для типа данных поля.
Например, в:
'( 42)'
пробелы будут проигнорированы, если тип поля integer, но не если это text.
Как показано ранее, при записи составного значения вы можете заключать в двойные кавычки любое отдельное значение поля. Вы должны делать это, если значение поля может запутать анализатор составных значений. В частности, поля, содержащие круглые скобки, запятые, двойные кавычки или обратные слеши, должны заключаться в двойные кавычки. Чтобы поместить двойную кавычку или обратный слеш в заключённое в кавычки значение составного поля, предварите его обратным слешем. (Также пара двойных кавычек внутри значения поля, заключённого в двойные кавычки, воспринимается как символ двойной кавычки, по аналогии с правилами для одинарных кавычек в строковых литералах SQL.) Альтернативно, вы можете избежать кавычек и использовать экранирование обратным слешем для защиты всех символов данных, которые в противном случае были бы восприняты как синтаксис составного значения.
Полностью пустое значение поля (вообще никаких символов между запятыми
или скобками) представляет NULL. Чтобы записать значение, которое является пустой
строкой, а не NULL, напишите "".
Подпрограмма вывода составного значения будет заключать значения полей в двойные кавычки, если они являются пустыми строками или содержат круглые скобки, запятые, двойные кавычки, обратные слеши или пробелы. (Делать это для пробелов не обязательно, но способствует читаемости.) Двойные кавычки и обратные слеши, встроенные в значения полей, будут удвоены.
Помните, что то, что вы пишете в команде SQL, сначала будет интерпретировано
как строковый литерал, а затем как составное значение. Это удваивает количество
обратных слешей, которые вам нужны (при условии использования синтаксиса строк с экранированием).
Например, чтобы вставить поле типа text,
содержащее двойную кавычку и обратный слеш, в составное
значение, вам нужно написать:
INSERT ... VALUES ('("\"\\")');
Обработчик строковых литералов удаляет один уровень обратных слешей, так что
тому, что поступает в анализатор составных значений, выглядит как
("\"\\"). В свою очередь, строка,
подаваемая на вход подпрограмме ввода типа данных text,
становится "\. (Если бы мы работали
с типом данных, чья входная подпрограмма также специально обрабатывает обратные слеши,
например, bytea, нам могло бы потребоваться до восьми обратных слешей
в команде, чтобы получить один обратный слеш в сохранённом составном поле.)
Долларовые кавычки (см. Раздел 2.1.1.2.4) могут
использоваться, чтобы избежать необходимости удваивать обратные слеши.
Синтаксис конструктора ROW обычно проще в использовании,
чем синтаксис составного литерала при записи составных значений в командах SQL.
В ROW отдельные значения полей записываются так же,
как они записывались бы, не являясь членами составного значения.