PostgreSQLにおけるビューはルールシステムを使って実装されています。たとえば以下のようなCREAT VIEW文があるとします。
CREATE VIEW myview AS SELECT * FROM mytab;
実際、これと、以下の2つのコマンドの間にはまったく違いがありません。
CREATE TABLE myview (same attribute list as for mytab); CREATE RULE "_RETmyview" AS ON SELECT TO myview DO INSTEAD SELECT * FROM mytab;
というのは、CREATE VIEWコマンドによって内部的にまったく同じ処理が行われるからです。しかし副作用もあります。その1つは PostgreSQLシステムカタログのビューについての情報はテーブルの情報と同一なので、パーサにとってもテーブルとビューは同じものになるということです。これらは同じもの、つまり、リレーションです。これは現時点の重要課題です。
たとえコマンドが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ルールには関係ありません。
ではここでunit、shoe_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(現時点ではたった1つのテーブル)に対する範囲テーブル項目を処理するときに、構文解析ツリーでルール _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;
しかし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);
同じように、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 int4ge(shoe_ready.total_avail, 2);
プランナはこのツリーを2レベルの問い合わせツリーに縮めます。一番下のselectはバラバラに処理する必要がないので2つ目selectに"引っ張り上げ"られます。しかし2つ目のセレクトは集約関数を持つため、頂点からは区別されます。もしそれらを引っ張り上げてしまうと一番上のselectの動作を変えてしまうことになり、それはしたくありません。しかし、問い合わせツリーを縮めることは書き換えシステムが自分で意識する必要がありません。
Note: ルールシステムにおいて(他のルールは別ですが)ビュールールの再帰処理を中止する機構はありません。この無限ループ(メモリの限界に到達してバックエンドが破壊される)を引き起こすには、テーブルを作成しCREATE RULEにより手作業で、互いを選択し合うようなビュールールを設定することによりのみ可能ですから、さほど深刻な問題ではありません。始めのCREATE VIEWでは、2 番目のリレーションは存在せず第1のビューを第2のビューが選択することはありえませんからこのような状況はCREATE VIEWでは起こり得ません。
これまでのビュールールの説明では構文解析ツリーの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;
範囲テーブルにはテーブル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の別の詳細説明に入りましょう。今のところ、テーブルの行は上書きされないのでABORT TRANSACTION処理は速いのです。UPDATEでは、(CTIDを取り除いたあと)テーブルに新しい結果の行が挿入され、その行のタプルヘッダ内ではcmaxを指し示すCTIDとxmaxの項目が現在のコマンドカウンタと現在のトランザクションIDにセットされます。このようにして、古い行は隠されトランザクションがコミットされたあとにvacuumが実際に削除することができます。
これらの詳細が全部理解できれば、どんなコマンドに対してもまったく同じようにしてビューのルールを簡単に適用することができます。そこには差異がありません。
ここまでで、ビューの諸定義が元の構文解析ツリーにどのように関与するかを解説しました。第2の例では、1つのビューからの単純なSELECTによって、最終的に4つのテーブルを結合する構文解析ツリーが生成されました(単位(unit)は違った名前で2回使われます)。
ビューをルールシステムとともに実装する利点は、プランナが、どのテーブルをスキャンすべきか、それらのテーブル間の関連性、ビューからの制約条件、元の問い合わせ条件、これらを1つのパースツリーに持っていることです。元の問い合わせがすでにビューに対する結合であるときも同様です。プランナはここでどれが問い合わせ処理の最適経路かを決定しなければなりません。情報が多ければ多いほど、プランナはよりよい決定を下すことができます。そして PostgreSQLに実装されているルールシステムはこれが現時点で、提供されているすべての情報であることを保証します。
ビューがINSERT、UPDATE、DELETEなどの目的リレーションとして名付けられた場合はどうなるのでしょうか? 上で説明したような置換をしたあとに、結果リレーションが副問い合わせの範囲テーブル項目を指す問い合わせツリーができます。これでは使えませんので、書き換えシステムがそのようなものを見つけるとエラーを発生します。
これを変えるためには非SELECT問い合わせの動作を変えるルールを定義することができます。それが次の節の論題になります。