Наследование

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

Давайте начнем с примера: предположим, что мы пытаемся построить модель данных для городов. В каждом штате много городов, но только одна столица. Мы хотим иметь возможность быстро получить столицу для любого конкретного штата. Это можно сделать, создав две таблицы: одну для столиц штатов, другую для городов, которые не являются столицами. Но что происходит, когда мы хотим запросить данные о городе, независимо от того, является он столицей или нет? Возможность наследования может помочь решить эту проблему. Мы определяем таблицу capitals так, чтобы она наследовала от cities:

CREATE TABLE cities (
    name            text,
    population      float,
    elevation       int     -- высота в футах
);

CREATE TABLE capitals (
    state           char(2)
) INHERITS (cities);

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

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

SELECT name, elevation
    FROM cities
    WHERE elevation > 500;

Учитывая пример данных из руководства QHB (см. раздел Введение главы «Язык SQL. Общие сведения»), этот запрос возвращает:

   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Mariposa  |      1953
 Madison   |       845

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

SELECT name, elevation
    FROM ONLY cities
    WHERE elevation > 500;

   name    | elevation
-----------+-----------
 Las Vegas |      2174
 Mariposa  |      1953

Здесь ключевое слово ONLY указывает, что запрос должен применяться только к cities, но не к таблицам ниже ее в иерархии наследования. Многие из команд, которые мы уже рассматривали — SELECT, UPDATE и DELETE, — поддерживают ключевое слово ONLY.

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

SELECT name, elevation
    FROM cities*
    WHERE elevation > 500;

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

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

SELECT c.tableoid, c.name, c.elevation
FROM cities c
WHERE c.elevation > 500;

этот запрос возвращает:

 tableoid |   name    | elevation
----------+-----------+-----------
   139793 | Las Vegas |      2174
   139793 | Mariposa  |      1953
   139798 | Madison   |       845

(Если вы попытаетесь воспроизвести этот пример, вы, вероятно, получите в OID другие числа.) Объединив oid с pg_class, можно увидеть фактические имена таблиц:

SELECT p.relname, c.name, c.elevation
FROM cities c, pg_class p
WHERE c.elevation > 500 AND c.tableoid = p.oid;

этот запрос возвращает:

 relname  |   name    | elevation
----------+-----------+-----------
 cities   | Las Vegas |      2174
 cities   | Mariposa  |      1953
 capitals | Madison   |       845

Другой способ получить тот же эффект — использовать псевдоним типа regclass, который выведет OID таблицы в символьном виде:

SELECT c.tableoid::regclass, c.name, c.elevation
FROM cities c
WHERE c.elevation > 500;

Наследование не распространяет автоматически данные из команд INSERT или COPY в другие таблицы в иерархии наследования. В нашем примере следующий оператор INSERT завершится ошибкой:

INSERT INTO cities (name, population, elevation, state)
VALUES ('Albany', NULL, NULL, 'NY');

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

Все проверочные ограничения и ограничения NOT NULL родительской таблицы автоматически наследуются ее дочерними таблицами, если явно не указаны предложения NO INHERIT. Другие типы ограничений (уникальности, первичного и внешнего ключей) не наследуются.

Таблица может наследоваться от нескольких родительских таблиц, и в этом случае она будет объединять в себе столбцы, определенные родительскими таблицами. К ним добавляются все столбцы, объявленные в определении дочерней таблицы. Если в нескольких родительских таблицах или в определении как родительской, так и дочерней таблицы встречаются столбцы с одинаковым именем, то эти столбцы «сливаются» таким образом, чтобы в дочерней таблице был только один такой столбец. Сливаемые столбцы должны иметь одинаковые типы данных, иначе возникает ошибка. Наследуемые проверочные ограничения и ограничения NOT NULL сливаются аналогичным образом. Так, например, слитный столбец будет помечен как NOT NULL, если любое из определений столбцов, из которых он получен, помечено как NOT NULL. Проверочные ограничения сливаются, если они имеют одинаковые имена, но если их условия различаются, слияние завершится ошибкой.

Наследование между таблицами обычно устанавливается при создании дочерней таблицы с помощью предложения INHERITS оператора CREATE TABLE. Как вариант, к таблице, которая уже определена сообразным способом, можно добавить новое родительское отношение, используя вариант ALTER TABLE с предложением INHERIT. Для этого новая дочерняя таблица уже должна включать столбцы с теми же именами и типами, что и у столбцов родительской таблицы. Кроме того она должна включать проверочные ограничения с теми же именами и проверочными выражениями, что и у родительской. Схожим образом можно удалить наследственную связь из дочерней таблицы, используя вариант ALTER TABLE с предложением NO INHERIT. Динамическое добавление и удаление таких наследственных связей может быть полезно, когда отношение наследования используется для партиционирования таблиц (см. раздел Партиционирование таблиц).

Одним из удобных способов создания совместимой таблицы, которая впоследствии станет новым потомком, является использование предложения LIKE в команде CREATE TABLE. Это создает новую таблицу с теми же столбцами, что и у исходной таблицы. Если в исходной таблице есть какие-либо проверочные ограничения, следует указать в LIKE параметр INCLUDING CONSTRAINTS, поскольку чтобы считаться совместимым, новый потомок должен иметь ограничения, соответствующие родительским.

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

Команда ALTER TABLE будет распространять вниз по иерархии наследования любые изменения в определениях данных столбцов и в проверочных ограничениях. Опять же, удаление столбцов, зависящих от других таблиц, возможно только с помощью параметра CASCADE. ALTER TABLE следует тем же правилам слияния и отклонения дубликатов столбцов, которые применяются при выполнении CREATE TABLE.

Унаследованные запросы выполняют проверки прав доступа только для родительской таблицы. Так, например, предоставление разрешения UPDATE для таблицы cities подразумевает также разрешение на изменение строк в таблице capitals при обращении к ним через cities. Это сохраняет видимость того, что данные находятся (также) в родительской таблице. Но напрямую таблицу capitals нельзя изменить без дополнительных прав. Аналогичным образом политики защиты строк родительской таблицы (см. раздел Политики защиты строк) применяются к строкам, приходящим из дочерних таблиц при выполнении унаследованного запроса. Политики дочерней таблицы, если таковые имеются, применяются только в том случае, если ее имя явно указано в запросе; при этом любые политики, связанные с ее родительскими таблицами, игнорируются.

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



Ограничения применимости

Обратите внимание, что не все команды SQL способны работать в иерархиях наследования. Команды, используемые для запроса или изменения данных либо изменения схемы (например, SELECT, UPDATE, DELETE, большинство вариантов ALTER TABLE, но не INSERT или ALTER TABLE ... RENAME), обычно по умолчанию включают дочерние таблицы и поддерживают указание ONLY, чтобы исключить их. Команды, выполняющие обслуживание и настройку базы данных (например, REINDEX, VACUUM), обычно работают только с отдельными физическими таблицами и не поддерживают рекурсию по иерархиям наследования. Соответствующее поведение каждой отдельной команды описывается на ее справочной странице (глава Команды SQL).

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

  • Если мы объявили cities.name с ограничением UNIQUE или PRIMARY KEY, это не помешает таблице capitals иметь строки с именами, дублирующими строки в cities. И эти дублирующиеся строки по умолчанию будут отображаться в запросах из cities. На самом деле по умолчанию таблица capitals вообще не имеет ограничения уникальности и поэтому может содержать несколько строк с одинаковыми именами. Можно добавить для capitals ограничение уникальности, но это не предотвратит дублирование относительно cities.

  • Точно так же, если бы мы указали, что cities.name ссылается (REFERENCES) на какую-то другую таблицу, это ограничение не стало бы автоматически распространяться на capitals. В этом случае ситуацию можно исправить, вручную добавив такое же ограничение REFERENCES в capitals.

  • Указание на то, что столбец другой таблицы ссылается (REFERENCES) на cities(name), позволит этой другой таблице содержать названия городов, но не столиц. Для этого случая хорошего решения нет.

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