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

17.2.1. Postgresにおけるビューの実装

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

    CREATE VIEW myview AS SELECT * FROM mytab;
と、ふたつのコマンド
    CREATE TABLE myview (same attribute list as for mytab);
    CREATE RULE "_RETmyview" AS ON SELECT TO myview DO INSTEAD
        SELECT * FROM mytab;
の間には全く差異がありません。というのは、CREATE VIEW コマンドによって 内部的に全く同じ処理が行われるからです。しかし副作用もあります。 その一つはPostgresシステムカタログのビュー についての情報はテーブルの情報を同一ですので、パーサにとっても テーブルとビューは同じものになります。リレーションについても同じです。 現時点の重要課題です。

17.2.2. SELECT ルールの動き

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

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

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

例として操作するデータベースの名前は al_bundy です。なぜこのような 名前なのかはすぐにわかるでしょう。下位の二つの整数値を返すちょっとした 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';

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

    CREATE TABLE shoe_data (
        shoename   char(10),      -- primary key
        sh_avail   integer,       -- available # of pairs
        slcolor    char(10),      -- preferred shoelace color
        slminlen   float,         -- miminum shoelace length
        slmaxlen   float,         -- maximum shoelace length
        slunit     char(8)        -- length unit
    );

    CREATE TABLE shoelace_data (
        sl_name    char(10),      -- primary key
        sl_avail   integer,       -- available # of pairs
        sl_color   char(10),      -- shoelace color
        sl_len     float,         -- shoelace length
        sl_unit    char(8)        -- length unit
    );

    CREATE TABLE unit (
        un_name    char(8),       -- the primary key
        un_fact    float          -- 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,
               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 に作ります。ルールはルール条件を持たない INSTEAD です。( SELECT ルール は現在規定されていないので、非 SELECT ルールのところで取り上げられ ます。)ルールの条件は、問い合わせの条件とは異なることに注意して下さい! ルールアクションは条件を持っています。

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

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

ではここでunitshoe_data およびshoelace_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に対する範囲テーブル項目を 処理するときに構文解析ツリーでルール '_RETshoelace' を見つけ出します。
    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;
しかしひとつだけ違いがあります。副問い合わせの範囲テーブルは 二つの余分な項目 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 はバラバラに処理する必要がないのでまん中の select に"引っ張り上げ"られます。しかしまん中のセレクトは集約関数 を持つため、頂点からは区別されます。もしそれらを引っ張り上げてしまうと 一番上の select の動作を変えてしまうことになり、それはしたくありません。 しかし、問い合わせツリーを縮めることは書き換えシステムが自分で 意識する必要がありません。

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

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

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

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

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

    UPDATE t1 SET b = t2.b WHERE t1.a = t2.a;
はほとんど同等です。

結果として、両方の構文解析ツリーが似たような実行プランになります。 それらは共に二つのテーブルの結合です。 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 にはちょっとした 問題があります。エグゼキュータは、結合が行う処理の結果が何を意味 しているかに関与しません。エグゼキュータは単に結果となる行の セットを作成するだけです。一つは SELECT コマンドでもう一つは 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;
ではPostgresの別の詳細説明に入ります。 今のところ、テーブルの行は上書きされませんので ABORT TRANSACTION 処理は速いのです。UPDATE では(ctid を取り除いたあと)テーブルに 新しい結果の行が挿入され、その行のタプルヘッダ内では cmax を指し示す ctid と xmax の項目が現在のコマンドカウンタと現在のトランザクション ID にセットされます。このようにして、旧行は隠されトランザクションが コミットされたあとに vacuum が実際に削除することができます。

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

17.2.4. Postgresにおけるビューの能力

ここまでで、ビューの諸定義がオリジナルの構文解析ツリーにどのように 関与するかを解説しました。第二の例では、最終的四つのテーブルを結合 するパースツリーを生成するような view からの単純な SELECT を使います。 (事例は違った名前で二度使われます。)

17.2.4.1. 利点

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

17.2.5. ビューの更新については?

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

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