35.7. 控制結構

控制結構可能是 PL/pgSQL 中最有用的(以及最重要)的部分了。 利用 PL/pgSQL 的控制結構, 您可以以非常靈活而且強大的方法操縱 PostgreSQL 的資料。

35.7.1. 從函數返回

有兩個命令可以用來從函數中返回資料:RETURNRETURN NEXT

35.7.1.1. RETURN

RETURN expression;

帶資料表達式的 RETURN 是用於終止函數, 然後 expression 的值返回給調用者。

如果返回標量類型,那麼可以使用任何資料表達式。資料表達式的類型將被自動轉換成函數的返回類型, 就像我們在賦值中描述的那樣。 要返回一個復合(行)數值,您必須寫一個記錄或者行變量做 expression

一個函數的返回值不能是未定義。如果控制到達了函數的最頂層的塊而沒有碰到一個 RETURN 語句, 那麼它就會發生一個錯誤。

請注意如果您聲明了該函數返回 void,那麼仍然必須聲明 RETURN 語句;但是,跟在 RETURN 後面的資料表達式是可選的,並且在任何情況下都會被忽略。

35.7.1.2. RETURN NEXT

RETURN NEXT expression;

如果一個 PL/pgSQL 函數聲明為返回 SETOF sometype, 那麼遵循的過程則略有不同。在這種情況下,要返回的獨立的項是在 RETURN NEXT 命令裡聲明的,然後最後有一個不帶參數的 RETURN 命令用於告訴我們這個函數已經完成執行了。 RETURN NEXT 可以用於標量和復合資料類型;對於後者,將返回一個完整的結果"資料表"

使用 RETURN NEXT 的函數應該按照下面的風格調用:

SELECT * FROM some_func();

也就是說,這個函數是用做FROM子句裡面的一個資料表資料源的。

RETURN NEXT 實際上並不從函數中返回; 它只是簡單地把資料表達式的值保存起來。 然後執行繼續執行 PL/pgSQL 函數里的下一條語句。 隨著後繼的 RETURN NEXT 命令的執行, 結果集就建立起來了。最後的一個不需要參數的 RETURN, 導致控制退出該函數。

注意: 目前的 PL/pgSQLRETURN NEXT 實現在從函數返回之前把整個結果集都保存起來,就像上面描述的那樣。 這意味著如果一個 PL/pgSQL 函數生成一個非常大的結果集, 性能可能會很差:資料將被寫到磁盤上以避免內存耗盡, 但是函數在完成整個結果集的生成之前不會退出。將來的 PL/pgSQL 版本可能會允許用戶定義沒有這樣限制的返回集合的函數。 目前,資料開始向磁盤裡寫的時刻是由配置變量 work_mem 控制的。 擁有足夠內存的管理員如果想在內存裡儲存更大的結果集, 則可以考慮把這個參數增大一些。

35.7.2. 條件

IF 語句讓您可以根據某種條件執行命令。 PL/pgSQL有五種形式的IF

35.7.2.1. IF-THEN

IF boolean-expression THEN
    statements
END IF;

IF-THEN語句是IF的最簡單形式。如果條件為真, 在THENEND IF之間的語句將被執行。 否則,將忽略它們。

例子:

IF v_user_id <> 0 THEN
    UPDATE users SET email = v_email WHERE user_id = v_user_id;
END IF;

35.7.2.2. IF-THEN-ELSE

IF boolean-expression THEN
    statements
ELSE
    statements
END IF;

IF-THEN-ELSE語句增加了IF-THEN的分支, 讓您可以聲明在條件計算結果為假的時候執行的語句。

例子:

IF parentid IS NULL OR parentid = ''
THEN 
    RETURN fullname;
ELSE
    RETURN hp_true_filename(parentid) || '/' || fullname;
END IF;

IF v_count > 0 THEN 
    INSERT INTO users_count(count) VALUES(v_count);
    RETURN 't';
ELSE 
    RETURN 'f';
END IF;

35.7.2.3. IF-THEN-ELSE IF

IF語句可以嵌套並且在下面的例子中:

IF demo_row.sex = 'm' THEN
  pretty_sex := 'man';
ELSE
  IF demo_row.sex = 'f' THEN
    pretty_sex := 'woman';
  END IF;
END IF;

如果您使用這種形式,那麼您實際上就是在另外一個IF語句的ELSE 部分嵌套了一個IF語句.因此您需要一個END IF語句 給每個嵌套的IF,另外還要一個給父IF-ELSE用. 這麼干是可以的,但是如果我們有太多候選項需要檢查,那麼就會變得很乏味. 因此有下面的形式。

35.7.2.4. IF-THEN-ELSIF-ELSE

IF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
[ ELSIF boolean-expression THEN
    statements
    ...]]
[ ELSE
    statements ]
END IF;

IF-THEN-ELSIF-ELSE提供了一種更方便的方法用於在一條語句中檢查許多候選條件。 形式上它和嵌套的IF-THEN-ELSE-IF-THEN命令相同, 但是只需要一個END IF

這裡是一個例子:

IF number = 0 THEN
    result := 'zero';
ELSIF number > 0 THEN
    result := 'positive';
ELSIF number < 0 THEN
    result := 'negative';
ELSE
    -- 另外一個唯一的可能是它是空值
    result := 'NULL';
END IF;

35.7.2.5. IF-THEN-ELSEIF-ELSE

ELSEIFELSIF 的別名。

35.7.3. 簡單循環

使用LOOPWHILEFOREXIT 語句,您可以控制您的 PL/pgSQL 函數重複一系列命令。

35.7.3.1. LOOP

[<<label>>]
LOOP
    statements
END LOOP;

LOOP 定義一個無條件的循環,無限循環,直到由EXIT或者RETURN語句終止。 可選的標籤可以由EXIT語句使用,用於在嵌套循環中聲明應該結束哪一層循環。

35.7.3.2. EXIT

EXIT [ label ] [ WHEN expression ];

如果沒有給出 label, 那麼退出最內層的循環,然後執行跟在END LOOP後面的語句。 如果給出 label, 那麼它必須是目前或者更高層的嵌套循環塊或者塊的標籤。 然後該命名塊或者循環就會終止,而控制落到對應循環/塊的 END 語句後面的語句上。

如果出現了WHEN,循環退出只發生在聲明的條件為真的時候, 否則控制會落到EXIT後面的語句上。

EXIT 可以用於在所有的循環類型中提前退出; 它並不僅限於在無條件循環中使用。

例子:

LOOP
    -- 一些計算
    IF count > 0 THEN
        EXIT;  -- exit loop
    END IF;
END LOOP;

LOOP
    -- 一些計算
    EXIT WHEN count > 0;
END LOOP;

BEGIN
    -- 一些計算
    IF stocks > 100000 THEN
        EXIT;  -- 導致從 BEGIN 塊裡退出
    END IF;
END;

35.7.3.3. WHILE

[<<label>>]
WHILE expression LOOP
    statements
END LOOP;

只要條件資料表達式為真,WHILE語句就會不停在一系列語句上進行循環. 條件是在每次進入循環體的時候檢查的.

比如:

WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
    -- 可以在這裡做些計算
END LOOP;

WHILE NOT BOOLEAN_expression LOOP
    -- 可以在這裡做些計算
END LOOP;

35.7.3.4. FOR (整數變種)

[<<label>>]
FOR name IN [ REVERSE ] expression .. expression LOOP
    statements
END LOOP;

這種形式的FOR對一定範圍的整數數值進行迭代的循環。 變量name 會自動定義為integer類型並且只在循環裡存在。 給出範圍上下界的兩個資料表達式在進入循環的時候計算一次。 迭代步進值總是為 1,但如果聲明了REVERSE就是 -1。

一些整數FOR循環的例子︰

FOR i IN 1..10 LOOP
  -- 這裡可以放一些資料表達式
    RAISE NOTICE 'i IS %', i;
END LOOP;

FOR i IN REVERSE 10..1 LOOP
    -- 這裡可以放一些資料表達式
END LOOP;

如果下界大於上界(或者是在 REVERSE 情況下是小於),那麼循環體將完全不被執行。 而且不會拋出任何錯誤。

35.7.4. 遍歷命令結果

使用不同類型的FOR循環,您可以遍歷一個命令的結果並且相應的操作哪些資料。語法是:

[<<label>>]
FOR record_or_row IN query LOOP
    statements
END LOOP;

這裡的記錄或者行變量將相繼被賦予所有來自query(必須是一條 SELECT 命令)的行, 並且循環體將為每行執行一次。下面是一個例子:

CREATE FUNCTION cs_refresh_mviews() RETURNS integer AS $$
DECLARE
     mviews RECORD;

BEGIN
     PERFORM cs_log('Refreshing materialized views...');

     FOR mviews IN SELECT * FROM cs_materialized_views ORDER BY sort_key LOOP

         -- 現在 "mviews" 裡有了一條來自 cs_materialized_views 的記錄

        PERFORM cs_log('Refreshing materialized view ' || quote_ident(mviews.mv_name) || ' ...');
        EXECUTE 'TRUNCATE TABLE ' || quote_ident(mviews.mv_name);
        EXECUTE 'INSERT INTO  ' ||  quote_ident(mview.mv_name) || ' ' || mview.mv_query;
     END LOOP;

     PERFORM cs_log('Done refreshing materialized views.');
     RETURN 1;
END;
$$ LANGUAGE plpgsql;

如果循環是用一個EXIT語句終止的,那麼在循環之後您仍然可以訪問最後賦值的行。

FOR-IN-EXECUTE語句是遍歷所有行的另外一種方法:

[<<label>>]
FOR record_or_row IN EXECUTE text_expression LOOP
    statements
END LOOP;

這個例子類似前面的形式,只不過源SELECT語句聲明為了一個字串資料表達式, 這樣它在每次進入FOR循環的時候都會重新計算和生成執行計劃。 這樣就允許程序員在一個預先規劃好了的命令所獲得的速度,和一個動態命令所獲得的靈活性(就像一個簡單的EXECUTE語句那樣)之間進行選擇。

注意: PL/pgSQL 分析器目前區分兩種類型的FOR循環(整數或者返回記錄的): 方法是檢查是否有任何 .. 出現在 INLOOP 之間的圓括弧之外。 如果沒有看到 ..,那麼這個循環就是在資料行上的循環。 如果誤敲了 .. 就很可能會導致像下面這樣的錯誤訊息: "loop variable of loop over rows must be a record or row variable", 而不是我們以為會看到的簡單的語法錯誤。

35.7.5. 捕獲錯誤

預設時,一個在 PL/pgSQL 函數里發生的錯誤退出函數的執行, 並且實際上是其周圍的交易也會退出。您可以使用一個帶有 EXCEPTION 子句的 BEGIN 塊捕獲錯誤並且從中恢復。 其語法是正常的 BEGIN 塊語法的一個擴展:

[ <<label>> ]
[ DECLARE
    declarations ]
BEGIN
    statements
EXCEPTION
    WHEN condition [ OR condition ... ] THEN
        handler_statements
    [ WHEN condition [ OR condition ... ] THEN
          handler_statements
      ... ]
END;

如果沒有發生錯誤,這種形式的塊只是簡單地執行所有 statements, 但是如果在 statements 裡發生了一個錯誤, 則對 statements 的進一步處理將廢棄, 控制傳遞到了 EXCEPTION 列資料表。 系統搜索這個列資料表,尋找匹配發生的錯誤的第一個元素。如果找到匹配, 則執行對應的 handler_statements,然後控制傳遞到 END 之後的下一個語句。 如果沒有找到匹配,該錯誤就會廣播出去,就好像根本沒有 EXCEPTION 子句一樣: 該錯誤可以被一個包圍塊用 EXCEPTION 捕獲,如果沒有包圍塊,則退出函數的處理。

condition 名字可以是 Appendix A 裡顯示的任何名字。 一個範疇名匹配任意該範疇裡的錯誤。特殊的條件名 OTHERS 匹配除了 QUERY_CANCELED 之外的所有錯誤類型。 (我們可以用名字捕獲 QUERY_CANCELED,不過通常是不明智的。)條件名是大小寫無關的。

如果在選中的 handler_statements 裡發生了新錯誤, 那麼它不能被這個 EXCEPTION 子句捕獲,而是傳播出去。 一個外層的 EXCEPTION 子句可以捕獲它。

如果一個錯誤被 EXCEPTION 捕獲,PL/pgSQL 函數的局部變量保持錯誤發生的時候的原值, 但是所有該塊中想固化在資料庫中的狀態都回滾。作為一個例子,讓我們看看下面片斷:

    INSERT INTO mytab(firstname, lastname) VALUES('Tom', 'Jones');
    BEGIN
        UPDATE mytab SET firstname = 'Joe' WHERE lastname = 'Jones';
        x := x + 1;
        y := x / 0;
    EXCEPTION
        WHEN division_by_zero THEN
            RAISE NOTICE 'caught division_by_zero';
            RETURN x;
    END;

當控制到達給 y 賦值的地方的時候,它會帶著一個 division_by_zero 錯誤失敗。 這個錯誤將被 EXCEPTION 子句捕獲。而在 RETURN 語句裡返回的數值將是 x 的增量值。 但是,在該塊之前的 INSERT 將不會回滾,因此最終的結果是資料庫包含 Tom Jones 而 不是 Joe Jones

提示: 進入和退出一個包含 EXCEPTION 子句的塊要比不包含的塊開銷大的多。 因此,不必要的時候不要使用 EXCEPTION