38.2. Представления и система правил#

38.2. Представления и система правил

38.2. Представления и система правил

Представления в Tantor SE-1C реализованы с использованием системы правил. Фактически, нет существенной разницы между:

CREATE VIEW myview AS SELECT * FROM mytab;

сравнивается с двумя командами:

CREATE TABLE myview (same column list as mytab);
CREATE RULE "_RETURN" AS ON SELECT TO myview DO INSTEAD
    SELECT * FROM mytab;

Потому что это именно то, что команда CREATE VIEW делает внутри. Это имеет некоторые побочные эффекты. Один из них заключается в том, что информация о представлении в системных каталогах Tantor SE-1C точно такая же, как и для таблицы. Для парсера нет никакой разницы между таблицей и представлением. Они являются одним и тем же: отношениями.

38.2.1. Как работают правила SELECT

Правила ON SELECT применяются ко всем запросам на последнем этапе, даже если указанная команда - это INSERT, UPDATE или DELETE. И они имеют отличную семантику от правил для других типов команд, поскольку они изменяют дерево запроса на месте, а не создают новое. Поэтому сначала описываются правила для SELECT.

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

Примеры для этой главы - это два представления объединения, которые выполняют некоторые вычисления, а также еще несколько представлений, использующих их в свою очередь. Одно из двух первых представлений настраивается позже путем добавления правил для операций INSERT, UPDATE и DELETE, чтобы конечный результат был представлением, которое ведет себя как настоящая таблица с некоторой магической функциональностью. Это не такой простой пример для начала, и это затрудняет понимание. Но лучше иметь один пример, который пошагово охватывает все рассматриваемые моменты, чем иметь много разных примеров, которые могут запутать в голове.

Реальные таблицы, которые нам нужны в описаниях первых двух систем правил, это:

CREATE TABLE shoe_data (
    shoename   text,          -- primary key
    sh_avail   integer,       -- available number of pairs
    slcolor    text,          -- preferred shoelace color
    slminlen   real,          -- minimum shoelace length
    slmaxlen   real,          -- maximum shoelace length
    slunit     text           -- length unit
);

CREATE TABLE shoelace_data (
    sl_name    text,          -- primary key
    sl_avail   integer,       -- available number of pairs
    sl_color   text,          -- shoelace color
    sl_len     real,          -- shoelace length
    sl_unit    text           -- length unit
);

CREATE TABLE unit (
    un_name    text,          -- primary key
    un_fact    real           -- factor to transform to cm
);

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

Представления создаются следующим образом:

CREATE VIEW shoe AS
    SELECT sh.shoename,
           sh.sh_avail,
           sh.slcolor,
           sh.slminlen,
           sh.slminlen * un.un_fact AS slminlen_cm,
           sh.slmaxlen,
           sh.slmaxlen * un.un_fact AS slmaxlen_cm,
           sh.slunit
      FROM shoe_data sh, unit un
     WHERE sh.slunit = un.un_name;

CREATE VIEW shoelace AS
    SELECT s.sl_name,
           s.sl_avail,
           s.sl_color,
           s.sl_len,
           s.sl_unit,
           s.sl_len * u.un_fact AS sl_len_cm
      FROM shoelace_data s, unit u
     WHERE s.sl_unit = u.un_name;

CREATE VIEW shoe_ready AS
    SELECT rsh.shoename,
           rsh.sh_avail,
           rsl.sl_name,
           rsl.sl_avail,
           least(rsh.sh_avail, rsl.sl_avail) AS total_avail
      FROM shoe rsh, shoelace rsl
     WHERE rsl.sl_color = rsh.slcolor
       AND rsl.sl_len_cm >= rsh.slminlen_cm
       AND rsl.sl_len_cm <= rsh.slmaxlen_cm;

Команда CREATE VIEW для представления shoelace (самого простого из всех) создаст отношение shoelace и запись в pg_rewrite, которая указывает, что существует правило перезаписи, которое должно применяться каждый раз, когда отношение shoelace упоминается в диапазоне таблицы запроса. Правило не имеет квалификации (обсуждается позже, с не-SELECT правилами, так как правила SELECT в настоящее время не могут иметь их) и оно является INSTEAD. Обратите внимание, что квалификации правил не являются тем же самым, что и квалификации запроса. Действие нашего правила имеет квалификацию запроса. Действие правила представляет собой одно дерево запроса, которое является копией оператора SELECT в команде создания представления.

Примечание

Два дополнительных записи таблицы диапазона для NEW и OLD, которые вы можете видеть в записи pg_rewrite, не представляют интереса для правил SELECT.

Теперь мы заполняем unit, shoe_data и shoelace_data и выполняем простой запрос на представление:

INSERT INTO unit VALUES ('cm', 1.0);
INSERT INTO unit VALUES ('m', 100.0);
INSERT INTO unit VALUES ('inch', 2.54);

INSERT INTO shoe_data VALUES ('sh1', 2, 'black', 70.0, 90.0, 'cm');
INSERT INTO shoe_data VALUES ('sh2', 0, 'black', 30.0, 40.0, 'inch');
INSERT INTO shoe_data VALUES ('sh3', 4, 'brown', 50.0, 65.0, 'cm');
INSERT INTO shoe_data VALUES ('sh4', 3, 'brown', 40.0, 50.0, 'inch');

INSERT INTO shoelace_data VALUES ('sl1', 5, 'black', 80.0, 'cm');
INSERT INTO shoelace_data VALUES ('sl2', 6, 'black', 100.0, 'cm');
INSERT INTO shoelace_data VALUES ('sl3', 0, 'black', 35.0 , 'inch');
INSERT INTO shoelace_data VALUES ('sl4', 8, 'black', 40.0 , 'inch');
INSERT INTO shoelace_data VALUES ('sl5', 4, 'brown', 1.0 , 'm');
INSERT INTO shoelace_data VALUES ('sl6', 0, 'brown', 0.9 , 'm');
INSERT INTO shoelace_data VALUES ('sl7', 7, 'brown', 60 , 'cm');
INSERT INTO shoelace_data VALUES ('sl8', 1, 'brown', 40 , 'inch');

SELECT * FROM shoelace;

 sl_name   | sl_avail | sl_color | sl_len | sl_unit | sl_len_cm
-----------+----------+----------+--------+---------+-----------
 sl1       |        5 | black    |     80 | cm      |        80
 sl2       |        6 | black    |    100 | cm      |       100
 sl7       |        7 | brown    |     60 | cm      |        60
 sl3       |        0 | black    |     35 | inch    |      88.9
 sl4       |        8 | black    |     40 | inch    |     101.6
 sl8       |        1 | brown    |     40 | inch    |     101.6
 sl5       |        4 | brown    |      1 | m       |       100
 sl6       |        0 | brown    |    0.9 | m       |        90
(8 rows)

Это самый простой SELECT, который вы можете выполнить на наших представлениях, поэтому мы воспользуемся этой возможностью, чтобы объяснить основы правил представлений. Запрос SELECT * FROM shoelace был интерпретирован парсером и сформировал дерево запроса:

SELECT shoelace.sl_name, shoelace.sl_avail,
       shoelace.sl_color, shoelace.sl_len,
       shoelace.sl_unit, shoelace.sl_len_cm
  FROM shoelace shoelace;

и это передается системе правил. Система правил проходит через таблицу диапазонов и проверяет, есть ли правила для каких-либо отношений. При обработке записи таблицы диапазонов для shoelace (единственной на данный момент) она находит правило _RETURN с деревом запроса:

SELECT s.sl_name, s.sl_avail,
       s.sl_color, s.sl_len, s.sl_unit,
       s.sl_len * u.un_fact AS sl_len_cm
  FROM shoelace old, shoelace new,
       shoelace_data s, unit u
 WHERE s.sl_unit = u.un_name;

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

SELECT shoelace.sl_name, shoelace.sl_avail,
       shoelace.sl_color, shoelace.sl_len,
       shoelace.sl_unit, shoelace.sl_len_cm
  FROM (SELECT s.sl_name,
               s.sl_avail,
               s.sl_color,
               s.sl_len,
               s.sl_unit,
               s.sl_len * u.un_fact AS sl_len_cm
          FROM shoelace_data s, unit u
         WHERE s.sl_unit = u.un_name) shoelace;

Однако есть одно отличие: в таблице диапазона подзапроса есть две дополнительные записи shoelace old и shoelace new. Эти записи не участвуют непосредственно в запросе, так как они не ссылается на дерево соединений или список целей подзапроса. Переписыватель использует их для хранения информации о проверке прав доступа, которая изначально присутствовала в записи таблицы диапазона, которая ссылалась на представление. Таким образом, исполнитель все равно проверит, что у пользователя есть соответствующие привилегии для доступа к представлению, даже если в переписанном запросе нет прямого использования представления.

Это было первое примененное правило. Система правил будет продолжать проверку оставшихся записей в таблице диапазонов в верхнем запросе (в этом примере их больше нет), и рекурсивно будет проверять записи в таблице диапазонов в добавленном подзапросе, чтобы увидеть, ссылаются ли они на представления. (Но она не будет разворачивать old или new — иначе у нас была бы бесконечная рекурсия!) В этом примере нет правил перезаписи для shoelace_data или unit, поэтому перезапись завершена, и вышеуказанное является окончательным результатом, предоставленным планировщику.

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

SELECT * FROM shoe_ready WHERE total_avail >= 2;

 shoename | sh_avail | sl_name | sl_avail | total_avail
----------+----------+---------+----------+-------------
 sh1      |        2 | sl1     |        5 |           2
 sh3      |        4 | sl7     |        7 |           4
(2 rows)

Результатом работы парсера на этот раз является дерево запроса:

SELECT shoe_ready.shoename, shoe_ready.sh_avail,
       shoe_ready.sl_name, shoe_ready.sl_avail,
       shoe_ready.total_avail
  FROM shoe_ready shoe_ready
 WHERE shoe_ready.total_avail >= 2;

Первым применяемым правилом будет правило для представления shoe_ready, и результатом будет дерево запроса:

SELECT shoe_ready.shoename, shoe_ready.sh_avail,
       shoe_ready.sl_name, shoe_ready.sl_avail,
       shoe_ready.total_avail
  FROM (SELECT rsh.shoename,
               rsh.sh_avail,
               rsl.sl_name,
               rsl.sl_avail,
               least(rsh.sh_avail, rsl.sl_avail) AS total_avail
          FROM shoe rsh, shoelace rsl
         WHERE rsl.sl_color = rsh.slcolor
           AND rsl.sl_len_cm >= rsh.slminlen_cm
           AND rsl.sl_len_cm <= rsh.slmaxlen_cm) shoe_ready
 WHERE shoe_ready.total_avail >= 2;

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

SELECT shoe_ready.shoename, shoe_ready.sh_avail,
       shoe_ready.sl_name, shoe_ready.sl_avail,
       shoe_ready.total_avail
  FROM (SELECT rsh.shoename,
               rsh.sh_avail,
               rsl.sl_name,
               rsl.sl_avail,
               least(rsh.sh_avail, rsl.sl_avail) AS total_avail
          FROM (SELECT sh.shoename,
                       sh.sh_avail,
                       sh.slcolor,
                       sh.slminlen,
                       sh.slminlen * un.un_fact AS slminlen_cm,
                       sh.slmaxlen,
                       sh.slmaxlen * un.un_fact AS slmaxlen_cm,
                       sh.slunit
                  FROM shoe_data sh, unit un
                 WHERE sh.slunit = un.un_name) rsh,
               (SELECT s.sl_name,
                       s.sl_avail,
                       s.sl_color,
                       s.sl_len,
                       s.sl_unit,
                       s.sl_len * u.un_fact AS sl_len_cm
                  FROM shoelace_data s, unit u
                 WHERE s.sl_unit = u.un_name) rsl
         WHERE rsl.sl_color = rsh.slcolor
           AND rsl.sl_len_cm >= rsh.slminlen_cm
           AND rsl.sl_len_cm <= rsh.slmaxlen_cm) shoe_ready
 WHERE shoe_ready.total_avail > 2;

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

38.2.2. Смотреть правила в не-SELECT выражениях

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

Есть только несколько различий между деревом запроса для команды SELECT и для любой другой команды. Очевидно, что они имеют различный тип команды, и для команды, отличной от SELECT, отношение результата указывает на запись таблицы диапазона, куда должен быть помещен результат. Все остальное абсолютно одинаково. Итак, имея две таблицы t1 и t2 с колонками a и b, деревья запросов для двух операторов:

SELECT t2.b FROM t1, t2 WHERE t1.a = t2.a;

UPDATE t1 SET b = t2.b FROM t2 WHERE t1.a = t2.a;

почти идентичны. В частности:

  • В диапазонных таблицах содержатся записи для таблиц t1 и t2.

  • В целевых списках содержится одна переменная, которая указывает на столбец b записи таблицы t2.

  • Выражения квалификации сравнивают столбцы a обоих записей таблицы диапазона на равенство.

  • Деревья соединений показывают простое соединение между t1 и t2.

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

UPDATE t1 SET a = t1.a, b = t2.b FROM t2 WHERE t1.a = t2.a;

и, таким образом, исполнитель, работающий над объединением, будет производить точно такой же набор результатов, как:

SELECT t1.a, t2.b FROM t1, t2 WHERE t1.a = t2.a;

Есть небольшая проблема в UPDATE: часть плана исполнителя, которая выполняет объединение, не заботится о том, для чего предназначены результаты объединения. Она просто создает набор результатов строк. Факт того, что одна команда SELECT, а другая - UPDATE, обрабатывается выше, в исполнителе, где он знает, что это UPDATE, и он знает, что этот результат должен быть помещен в таблицу t1. Но какую из имеющихся строк нужно заменить новой строкой?

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

SELECT t1.a, t2.b, t1.ctid FROM t1, t2 WHERE t1.a = t2.a;

Теперь поговорим об еще одной особенности Tantor SE-1C. Старые строки таблицы не перезаписываются, и поэтому ROLLBACK выполняется быстро. В UPDATE новая строка результата вставляется в таблицу (после удаления CTID) и в заголовок старой строки, на которую указывал CTID, записываются значения cmax и xmax текущего счетчика команд и текущего идентификатора транзакции. Таким образом, старая строка скрывается, и после коммита транзакции процесс очистки может уже удалить мертвую строку.

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

38.2.3. Сила представлений в Tantor SE-1C

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

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

38.2.4. Обновление представления

Что произойдет, если представление названо целевым отношением для команды INSERT, UPDATE или DELETE? Если выполнить описанные выше замены, то получится дерево запроса, в котором результат отношения указывает на подзапрос в таблице диапазона, что не будет работать. Однако Tantor SE-1C может поддерживать обновление представления несколькими способами. По мере увеличения сложности для пользователя эти способы следующие: автоматическая замена базовой таблицы представления, выполнение пользовательского триггера или переписывание запроса согласно пользовательскому правилу. Ниже рассматриваются эти варианты.

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

Альтернативно, операция может быть обработана пользовательским триггером INSTEAD OF на представлении (см. CREATE TRIGGER). Переписывание работает немного иначе в этом случае. Для INSERT переписыватель вообще ничего не делает с представлением, оставляя его в качестве результирующего отношения для запроса. Для UPDATE и DELETE все еще необходимо раскрыть запрос представления, чтобы получить строки old, которые команда будет пытаться обновить или удалить. Таким образом, представление раскрывается как обычно, но в запрос добавляется еще одна не раскрытая запись диапазона, представляющая представление в качестве результирующего отношения.

Проблема, которая теперь возникает, заключается в том, как идентифицировать строки, которые должны быть обновлены в представлении. Напомним, что когда результатом является таблица, в целевой список добавляется специальная запись CTID, чтобы идентифицировать физические местоположения строк, которые должны быть обновлены. Это не работает, если результатом является представление, потому что представление не имеет никакого CTID, так как его строки не имеют фактических физических местоположений. Вместо этого, для операций UPDATE или DELETE, в целевой список добавляется специальная запись wholerow, которая расширяется, чтобы включить все столбцы из представления. Исполнитель использует это значение для предоставления старой строки триггеру INSTEAD OF. Триггеру предстоит определить, что обновлять на основе старых и новых значений строки.

Другая возможность для пользователя - определить правила INSTEAD, которые указывают заменить действия для команд INSERT, UPDATE и DELETE на представлении. Эти правила переписывают команду, обычно в команду, которая обновляет одну или несколько таблиц, а не представления. Это тема Раздел 38.4.

Обратите внимание, что правила сначала оцениваются, переписывая исходный запрос перед его планированием и выполнением. Поэтому, если у представления есть триггеры INSTEAD OF а также правила на INSERT, UPDATE или DELETE, то правила будут оцениваться первыми, и в зависимости от результата триггеры могут не быть использованы вообще.

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

Если для представления нет правил INSTEAD или триггеров INSTEAD OF, и переписыватель не может автоматически переписать запрос как обновление базового отношения, будет сгенерирована ошибка, поскольку исполнитель не может обновить представление как таковое.