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;

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

33.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)。

33.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 清理器就可以真正把它們刪除掉。

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

33.2.3. PostgreSQL裡視圖的強大能力

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

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

33.2.4. 更新一個視圖

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

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