34.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;

之間絕對沒有區別,因為這就是CREATE VIEW命令在內部實際執行的內容。 這樣做有一些負作用。其中之一就是在 PostgreSQL 系統表裡的視圖的信息與一般表的信息完全一樣。 所以對于查詢分析器來說,表和視圖之間完全沒有區別。 它們是同樣的事物:關系。

34.2.1. SELECT規則如何運轉

ON SELECT 的規則在最後一步應用于所有查詢, 哪怕給出的命令是一條INSERTUPDATEDELETE。 而且與其他(規則)有不同的語意, 那就是它們在現場修改查詢樹而不是創建一個新的(查詢樹)。 所以我們先介紹SELECT的規則。

目前,一個 ON SELECT 規則裡只能有一個動作(action), 而且它必須是一個無條件的 INSTEAD (取代)的 SELECT 動作。 有這個限制是為了令規則安全到普通用戶也可以打開它們, 並且它對真正的視圖規則做 ON SELECT 規則限制。

本文檔的例子是兩個連接視圖, 它們做一些運算並且因此會涉及到更多視圖的使用。 這兩個視圖之一稍後將利用對INSERTUPDATEDELETE 操作附加規則的方法客戶化, 這樣做最終的結果就會是這個視圖表現得象一個具有一些特殊功能的真正的表。 這個例子可不是適合于開始的簡單易懂的例子, 從這個例子開始講可能會讓我們的講解變得有些難以理解。 但是我們認為用一個覆蓋所有關鍵點的例子來一步一步討論要比舉很多例子搞亂思維好多了。

比如,我們需要一個小巧的 min 函數用于返回兩個整數值中的小的那個。 我們用下面方法創建它

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

我們頭兩個規則系統要用到的真實的表的描述如下:

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	  -- 轉換成釐米的系數
);

你可以看到,這些表代表鞋店的數據。

視圖創建為

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;

創建shoelaceCREATE VIEW命令(也是我們用到的最簡單的一個) 將創建一個關系 shoelace 並且在 pg_rewrite 表裡增加一個記錄,告訴系統有一個重寫規則應用于所有範圍表裡引用了 shoelace 關系的查詢。該規則沒有規則條件(將在非 SELECT 規則討論,因為目前的 SELECT 規則不可能有這些東西)並且它是 INSTEAD(取代)型的。 要注意規則條件與查詢條件不一樣。我們的規則動作(action)有一個查詢條件。 規則的動作是一個查詢樹,這個查詢是樹視圖創建命令中的 SELECT 語句的一個拷貝。

注意: 你在表 pg_rewrite 裡看到的兩個額外的用于 NEWOLD 的範圍表記錄(因歷史原因, 在打印出來的查詢樹裡叫 *NEW**CURRENT* ) 對 SELECT 規則不感興趣。

現在我們填充 unitshoe_datashoelace_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;

然後把這些交給規則系統。規則系統把範圍表(rangetable)過濾一遍, 檢查一下在有沒有適用任何關系的任意規則。當為 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_dataunit 的重寫規則, 所以重寫結束並且上面的就是給規劃器的最終結果。

現在我們想寫這麼一個查詢:這個查詢找出目前在店裡有配對鞋帶的鞋子,並且配對的鞋帶數大于或等于二。

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;

與上面類似,用于 shoeshoelace 的規則替換到子查詢範圍表裡, 生成一個最終的三層查詢樹:

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;

最後規劃器會把這個樹壓縮成一個兩層查詢樹: 最下層的 SELECT"拖到"中間的 SELECT 中, 因為沒有必要分別處理它們。但是中間的 SELECT 仍然和頂層的分開,因為它包含聚集函數。如果我們把它們也拉進來, 那它就會修改最頂層的 SELECT 的行為,那可不是我們想要的。 不過,壓縮查詢樹是重寫系統自己不需要關心的優化操作。

注意: 目前規則系統中沒有用于視圖規則遞歸終止機制(只有用于其他規則的)。 這一點不會造成太大的損害, 因為把這個(規則)無限循環(不斷膨脹後端,直到耗盡內存)的唯一方法是創建表然後後手工用CREATE RULE命令創建視圖規則, 這個規則是這樣的:一個從其他地方來的選擇(select)選擇(select)了這個視圖。 如果使用了CREATE VIEW,這一點是永遠不會發生的, 因為第二個關系不存在,所以第一個CREATE VIEW 不能從第二個視圖裡面選擇(select)。

34.2.2. 非SELECT語句的視圖規則

有兩個查詢樹的細節我們在上面的視圖規則中沒有涉及到。 就是命令類型和結果關系。實際上,視圖規則不需要這些信息。

一個SELECT的查詢樹和用于其他命令的查詢樹只有少數幾個區別。 顯然,它們不同的命令類型並且對于SELECT之外的命令, 結果關系指向結果將前往的範圍表入口。任何其它東西都完全是一樣的。 所以如果有兩個表 t1t2 分別有字段 ab, 下面兩個語句的查詢樹

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 裡去。 但是現有的記錄中的哪一行要被新行取代呢?

要解決這個問題, 在UPDATEDELETE語句的目標列表裡面增加了另外一個入口。 當前的元組 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 指向的舊的數據行的元組頭裡面的 cmaxxmax 設置為當前命令計數器和當前事務 ID。這樣舊的行就被隱藏起來並且在事務提交之後, vacuum 清理器就可以真正把它們刪除掉。

知道了這些,我們就可以簡單的把視圖的規則應用到任意命令中。 它們(視圖和命令)沒有區別。

34.2.3. PostgreSQL裡視圖的強大能力

上面演示了規則系統如何融合到視圖定義的初始查詢樹中去。 在第二個例子裡,一個簡單的對視圖的SELECT 創建了一個 4 個表聯合的查詢樹(unit 以不同的名稱用了兩次)。

在規則系統裡實現視圖的好處是,規劃器在一個查詢樹裡擁有所有信息: 應該掃描哪個表 + 表之間的關系+ 視圖的資格限制 + 初始查詢的資格(條件)。 並且仍然是在最初的查詢已經是一個視圖的聯合的情況下。 現在規劃器必須決定執行查詢的最優路徑。 規劃器擁有越多信息,它的決策就越好。並且 PostgreSQL 裡的規則系統的實現保證這些信息是此時能獲得的有關該查詢的所有信息。

34.2.4. 更新一個視圖

如果視圖是INSERTUPDATE,或者 DELETE 的目標關系會怎樣? 在完成我們上面描述的替換之後, 我們就有一個這樣的查詢樹:結果關系指向一個是子查詢的範圍表記錄。 這樣可不能運行,所以如果重寫器看到自己生成這麼一個東西, 它就拋出一個錯誤。

要修改這個特性,我們可以定義修改這些命令行為的規則。 這是下一節的主題。