ビューとルールシステム

PostgreSQL 8.0.0 文書
前のページ | 巻戻し | 第 33章ルールシステム | 早送り | 次のページ

# 33.2. ビューとルールシステム

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

    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;

の2つのコマンドの間には基本的な違いはありません。 というのは、`CREATE VIEW`コマンドによって内部的に全く同じコマンドが行われるからです。 しかし副作用もあります。 その1つはPostgreSQLシステムカタログのビューについての情報はテーブルの情報と同一なので、パーサにとってもテーブルとビューは同じものになるということです。 これらは同じもの、つまり、リレーションです。

## 33.2.1. `SELECT`ルールの動き

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

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

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

この例では、2つの整数から小さな値を返すちょっとした`min`関数を必要とします。 関数の生成は以下のようにします。

    CREATE FUNCTION min(integer, integer) RETURNS integer AS $$
        SELECT CASE WHEN $1 < $2 THEN $1 ELSE $2 END
    $$ LANGUAGE SQL STRICT;

最初の2つのルールシステムの説明で必要とする実テーブルを以下に示します。

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

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

    CREATE TABLE unit (
        un_name    text,          -- プライマリキー
        un_fact    real           -- 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`のコピーである、1つの問い合わせツリーです。

> 注意: `pg_rewrite`項目の`NEW`と`OLD`（歴史的な理由により、出力用の問い合わせツリーではそれは`*NEW*`や`*CURRENT*`という名前が付いています）に対する2つの特別な範囲テーブル項目は`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;

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

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

さて、店に置いてある靴紐（の色とサイズ）に一致する靴が店にあるか、完全に一致する靴紐の在庫数が2以上あるかどうかを把握する問い合わせを書いてみましょう。

    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,
                   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 shoe_ready.total_avail >= 2;

同じように、`shoe`と`shoelace`用のルールは副問い合わせの範囲テーブルとして代用され、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 shoe_ready.total_avail > 2;

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

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

## 33.2.2. 非`SELECT`文のビュールール

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

SELECTと他のコマンドに対する問い合わせツリーの間には大きな違いはありません。 それらは明らかに違うコマンドタイプを持っていて、`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;

以下に、具体的に示します。

- 範囲テーブルには、テーブル`t1`と`t2`に対する項目があります。

- 目的リストにはテーブル`t2`に対する範囲テーブル項目の`b`列を指し示す1つの変数があります。

- 条件式は、範囲テーブルの両項目の`a`列の等価性を比較します。

- 結合ツリーは`t1`と`t2`の単純な結合を表しています。

結果として、両方の問い合わせツリーは似たような実行計画になります。 それらは共に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の別の詳細説明に入りましょう。 テーブルの行は上書きされませんので、`ROLLBACK`処理は速いのです。 `UPDATE`では、(CTIDを取り除いた後)テーブルに新しい結果行が挿入され、CTIDが指し示す古い行の行ヘッダ内の`cmax`と`xmax`項目は現在のコマンドカウンタと現在のトランザクションIDに設定されます。 このようにして、古い行は隠され、トランザクションがコミットされた後、vacuum掃除機により実際に削除することができます。

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

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

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

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

## 33.2.4. ビューの更新について

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

これを変えるために、こうした種類のコマンドの動作を変えるルールを定義することができます。 それが次節の論題になります。

前のページ | ホーム | 次のページ
ルールシステム | 上に戻る | `INSERT`、`UPDATE`、`DELETE`についてのルール