13.3. ビューとルールシステム

13.3.1. PostgreSQLにおけるビューの実装

PostgreSQLにおけるビューはルールシステムを使って実装されています。

CREATE VIEW myview AS SELECT * FROM mytab;

事実上、上記のコマンドと、以下の 2 つのコマンドは、まったく同じ役割を果たします。

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

というのは、CREATE VIEWコマンドによって内部的にまったく同じ処理が行われるからです。しかし副作用もあります。 その 1 つは PostgreSQL システムカタログのビューについての情報はテーブルの情報と同一なので、パーサにとってもテーブルとビューは同じものになるということです。これらは同じもの、つまり、リレーションです。これは現時点の重要課題です。

13.3.2. SELECTルールの動き

たとえコマンドがINSERT、UPDATE、DELETEなどであっても、ON SELECTルールはすべての問い合わせに対し最後に適用されます。そして、それぞれは他と異なるセマンティックを持っていて構文解析ツリーを新規に生成せずに、そこにあるものを修正します。したがってSELECTルールが一番初めに評価されなければいけません。

現在のところ、ON SELECTルールでは1つのアクションしか許されず、それはINSTEADである無条件のSELECTアクションでなければいけません。 この制約は、一般のユーザが何をしても、ルールシステムが堅牢であるために必要であり、ON SELECT のルールは、実質的に view のルールのみに限定されます。

このドキュメントの例として挙げているのは、ちょっとした演算をする2つの結合のビューと、次にこれらの機能を利用するいくつかのビューです。最終結果が何らかの魔法の機能によりあたかも実テーブルのように振る舞うビューになるように、始めの2つのビューの内の1つは、INSERT、UPDATE、DELETE操作に対するルールを後で追加することでカスタマイズされます。 始めて学ぶための例としては決して簡単ではなく先に進むことを躊躇させるかもしれませんが、多くの別々の例を持ち出して頭の混乱を招くよりも、すべての論点をステップごとに追う1つの例を挙げるほうがおいでしょう。

例として操作するデータベースの名前はal_bundyです。なぜこのような名前なのかはすぐにわかるでしょう。2つの整数から小さな値を返すちょっとしたmin()関数を必要としますので、手続き言語PL/pgSQLがインストールされている必要があります。関数の生成は以下のようにします。

CREATE FUNCTION min(integer, integer) RETURNS integer AS '
    BEGIN
        IF $1 < $2 THEN
            RETURN $1;
        END IF;
        RETURN $2;
    END;
' LANGUAGE plpgsql;

始めの2つのルールシステムの説明に必要な実際のテーブルは以下のものです。

CREATE TABLE shoe_data (
    shoename   char(10),      -- プライマリキー
    sh_avail   integer,       -- 在庫
    slcolor    char(10),      -- 望ましい靴紐の色
    slminlen   float,         -- 靴紐の最短サイズ
    slmaxlen   float,         -- 靴紐の最長サイズ
    slunit     char(8)        -- 長さのサイズ
);

CREATE TABLE shoelace_data (
    sl_name    char(10),      -- プライマリキー
    sl_avail   integer,       -- 在庫
    sl_color   char(10),      -- 靴紐の色
    sl_len     float,         -- 靴紐の長さ
    sl_unit    char(8)        -- 長さの単位
);

CREATE TABLE unit (
    un_name    char(8),       -- プライマリキー
    un_fact    float          -- 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,
           min(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;

shoelaceビュー(今ある一番簡単なビュー)用のCREATE VIEWコマンドは、shoelaceリレーションと、問い合わせ範囲テーブルの中でshoelaceリレーションが参照されるときはいつでも、適用されるべき書き換えルールの存在を示す項目をpg_rewriteに作ります。ルールはルール条件(SELECTルールは現在持つことができませんので、非SELECTルールのところで取りあげられます)を持たないINSTEADです。ルールの条件は、問い合わせの条件とは異なることに注意してください!ルールアクションは問い合わせ条件を持っています。

ルールアクションはビュー生成コマンドのSELECT文とまったく同じ問い合わせツリーです。

注意: pg_rewrite項目のNEWとOLD(歴史的な理由により、出力用の問い合わせツリーではそれは*NEW*や*CURRENT*という名前が付いています)に対する2つの特別な範囲テーブル項目はSELECTルールには関係ありません。

ではここでunitshoe_datashoelace_dataにデータを入れます。 アルはここで人生初のSELECTを入力します。

al_bundy=> INSERT INTO unit VALUES ('cm', 1.0);
al_bundy=> INSERT INTO unit VALUES ('m', 100.0);
al_bundy=> INSERT INTO unit VALUES ('inch', 2.54);
al_bundy=>
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy->     ('sh1', 2, 'black', 70.0, 90.0, 'cm');
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy->     ('sh2', 0, 'black', 30.0, 40.0, 'inch');
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy->     ('sh3', 4, 'brown', 50.0, 65.0, 'cm');
al_bundy=> INSERT INTO shoe_data VALUES
al_bundy->     ('sh4', 3, 'brown', 40.0, 50.0, 'inch');
al_bundy=>
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl1', 5, 'black', 80.0, 'cm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl2', 6, 'black', 100.0, 'cm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl3', 0, 'black', 35.0 , 'inch');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl4', 8, 'black', 40.0 , 'inch');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl5', 4, 'brown', 1.0 , 'm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl6', 0, 'brown', 0.9 , 'm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl7', 7, 'brown', 60 , 'cm');
al_bundy=> INSERT INTO shoelace_data VALUES
al_bundy->     ('sl8', 1, 'brown', 40 , 'inch');
al_bundy=>
al_bundy=> 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;

この操作はルールシステムに伝えられます。ルールシステムは範囲テーブルを参照し、何らかのリレーションに対してルールがpg_rewriteに存在するか調べます。 shoelace (現時点ではたった 1 つのテーブル) に対する範囲テーブル項目を処理する際、構文解析ツリーで _RETURN ルールを検出します。

SELECT s.sl_name, s.sl_avail,
       s.sl_color, s.sl_len, s.sl_unit,
       float8mul(s.sl_len, u.un_fact) AS sl_len_cm
  FROM shoelace *OLD*, shoelace *NEW*,
       shoelace_data s, unit u
 WHERE bpchareq(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;

しかし1つだけ違いがあります。副問い合わせの範囲テーブルが2つの余分な項目shoelace *OLD*とshoelace *NEW*を持っていることです。これらの項目は副問い合わせの結合ツリーや目的リストに参照されないので、直接問い合わせでは使われません。書き換えシステムではそれらを、ビューを参照した範囲テーブルの項目にもともと存在したアクセス権限確認情報を格納するために使います。この方法で、書き換えられた問い合わせで直接ビューを使用していなくても、エクゼキュータはユーザがそのビューにアクセスするための正しい権限を持っているか確認します。

これが最初に適用されるルールです。 ルールシステムは始めの問い合わせ(この例ではそれ以上はありません)の残りの範囲テーブルの項目をチェックし続けます。 そしてルールシステムは、追加された副問い合わせの項目がビューを参照するかを再帰的に確認します(しかし*OLD*や*NEW*は展開しません。 そうでなければ無限再帰になってしまいます!)。この例ではshoelace_dataやunitのための書き換えルールはありません。 ですから書き換えは完結し、上記がプランナに渡される最終的な結果となります。

ここで、ブルースブラザーズが靴を買いにお店にやって来て、ブルースブラザーズですから、同じ靴を履きたいと言います。すぐに履きたいので靴紐も必要です。

店に置いてある靴紐(の色とサイズ)に一致する靴がどれで、完全に一致するものの在庫が2組以上あるかどうかをアルは知らなければいけません。アルはデータベースに問い合わせることが必要です。

al_bundy=> 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)

アルは靴についての専門家ですので、sh1型の靴が該当することがわかります(sl7の靴紐は茶色で、それに合う茶色の靴はブルースブラザーズの好みではありません)。

今回のパーサの出力は以下の構文解析ツリーです。

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 int4ge(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,
               min(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 int4ge(shoe_ready.total_avail, 2);

同じように、shoeshoelaceのルールは副問い合わせの範囲テーブルとして代用され、レベル3の最終問い合わせツリーへと導きます。

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,
               min(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 int4ge(shoe_ready.total_avail, 2);

プランナはこのツリーを2レベルの問い合わせツリーに縮めます。一番下のselectはバラバラに処理する必要がないので2つ目selectに"引っ張り上げ"られます。しかし2つ目のセレクトは集約関数を持つため、頂点からは区別されます。もしそれらを引っ張り上げてしまうと一番上のselectの動作を変えてしまうことになり、それはしたくありません。 しかし、問い合わせツリーを縮めることを、書き換えシステムが自分で意識する必要はありません。

注意: ルールシステムにおいて(他のルールは別ですが)ビュールールの再帰処理を中止する機構はありません。この無限ループ(メモリの限界に到達してバックエンドが破壊される)を引き起こすには、テーブルを作成しCREATE RULEにより手作業で、互いを選択し合うようなビュールールを設定することによりのみ可能ですから、さほど深刻な問題ではありません。始めのCREATE VIEWでは、2番目のリレーションは存在せず第1のビューを第2のビューが選択することはありえませんからこのような状況はCREATE VIEWでは起こり得ません。

13.3.3. 非SELECT文のビュールール

これまでのビュールールの説明では構文解析ツリーの2つの詳細について触れませんでした。それらは、コマンドタイプと結果リレーションです。実際、ビューのルールはこれらの情報を必要としません。

SELECTと他のコマンドに対する構文解析ツリーの間には大きな違いはありません。それらは明らかに違うコマンドタイプを持っていて、今回は結果リレーションは結果がどこに行くのかを示す範囲テーブルの項目を指し示します。それ以外ではまったく同じです。ですから、a と b の属性を持つテーブル t1、t2 に対する以下の 2 つの命令文の構文解析ツリーは、ほとんど同じです。

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

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

結果として、両方の構文解析ツリーは似たような実行計画になります。それらは共に2つのテーブルの結合です。UPDATEでは t1から抜けている列はプランナが目的リストに追加し、最終の構文解析ツリーは、以下のようになります。

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

結合を実行したエクゼキュータは、

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

の結果セットと全く同じ結果セットを作成します。 とはいってもUPDATEにはちょっとした問題があります。エクゼキュータは、結合が行う処理の結果が何を意味しているかに関与しません。エクゼキュータは単に結果となる行のセットを作成するだけです。1つはSELECTコマンドでもう1つはUPDATEコマンドですが、違いはエクゼキュータを呼び出す側で扱われることです。呼び出し側は(構文解析ツリーを見て)、これがUPDATEであるとわかっていて、この結果がテーブルt1に入らなければいけないことを知っています。しかし、どの行が新しい行によって置換されなければならないのでしょうか。

この問題を解決するため、UPDATE文(DELETE文も)の目的リストに別の項目が付け加えられます。 それは、現在のタプル ID (CTID) です。これはファイルのブロック番号とその行のブロック中の位置を持つシステム属性です。 テーブルがわかっている場合、CTID を元の t1 行を抽出して更新するために使うことができます。 CTID を目的リストに追加すると、問い合わせは以下のようになります。

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

では、PostgreSQLの別の詳細説明に入りましょう。今のところ、テーブルの行は上書きされないのでABORT TRANSACTION処理は速いのです。 UPDATEでは、(CTID を取り除いた後) テーブルに新しい結果行が挿入され、その行のタプルヘッダ内では、cmaxxmax 項目を指す CTID が、現在のコマンドカウンタと現在のトランザクション ID にセットされます。 このようにして、古い行は隠され、トランザクションがコミットされた後、vacuum により実際に削除することができます。

これらの詳細が全部理解できれば、どんなコマンドに対してもまったく同じようにしてビューのルールを簡単に適用することができます。そこには差異がありません。

13.3.4. PostgreSQLにおけるビューの能力

ここまでで、ビューの諸定義が元の構文解析ツリーにどのように関与するかを解説しました。第2の例では、1つのビューからの単純なSELECTによって、最終的に4つのテーブルを結合する構文解析ツリーが生成されました(単位(unit)は違った名前で2回使われます)。

13.3.4.1. 利点

ビューをルールシステムとともに実装する利点は、プランナが、どのテーブルをスキャンすべきか、それらのテーブル間の関連性、ビューからの制約条件、元の問い合わせ条件、これらを1つのパースツリーに持っていることです。元の問い合わせがすでにビューに対する結合であるときも同様です。プランナはここでどれが問い合わせ処理の最適経路かを決定しなければなりません。情報が多ければ多いほど、プランナはよりよい決定を下すことができます。そしてPostgreSQLに実装されているルールシステムはこれが現時点で、提供されているすべての情報であることを保証します。

13.3.5. ビューの更新について

ビューがINSERT、UPDATE、DELETEなどの目的リレーションとして名付けられた場合はどうなるのでしょうか?上で説明したような置換をしたあとに、結果リレーションが副問い合わせの範囲テーブル項目を指す問い合わせツリーができます。これでは使えませんので、書き換えシステムがそのようなものを見つけるとエラーを発生します。

これを変えるためには非SELECT問い合わせの動作を変えるルールを定義することができます。それが次の節の論題になります。