2025年9月25日: PostgreSQL 18 釋出!
支援版本: 當前 (18) / 17 / 16 / 15 / 14 / 13
開發版本: devel
不支援的版本: 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2

41.6. 控制結構 #

控制結構可能是 PL/pgSQL 中最有用(也是最重要的)部分。使用 PL/pgSQL 的控制結構,您可以非常靈活且強大地操作 PostgreSQL 資料。

41.6.1. 從函式返回 #

有兩個命令可用於從函式返回資料: RETURNRETURN NEXT

41.6.1.1. RETURN #

RETURN expression;

RETURN 後面跟表示式,會終止函式並將 expression 的值返回給呼叫者。此形式用於不返回集合的 PL/pgSQL 函式。

在返回標量型別的函式中,表示式的結果將自動轉換為函式的返回型別,如賦值部分所述。但要返回複合(行)值,您必須編寫一個提供完全請求列集的表示式。這可能需要使用顯式轉換。

如果您宣告的函式帶有輸出引數,則只需編寫 RETURN,後面不跟表示式。輸出引數變數的當前值將被返回。

如果您宣告的函式返回 void,則可以使用 RETURN 語句提前退出函式;但不要在 RETURN 後面寫表示式。

函式的返回值不能未定義。如果控制在未遇到 RETURN 語句的情況下到達函式頂層塊的末尾,將發生執行時錯誤。但是,此限制不適用於帶有輸出引數的函式和返回 void 的函式。在這些情況下,如果頂層塊結束,將自動執行 RETURN 語句。

一些例子

-- functions returning a scalar type
RETURN 1 + 2;
RETURN scalar_var;

-- functions returning a composite type
RETURN composite_type_var;
RETURN (1, 2, 'three'::text);  -- must cast columns to correct types

41.6.1.2. RETURN NEXTRETURN QUERY #

RETURN NEXT expression;
RETURN QUERY query;
RETURN QUERY EXECUTE command-string [ USING expression [, ... ] ];

PL/pgSQL 函式宣告為返回 SETOF sometype 時,處理方式略有不同。在這種情況下,要返回的單個項由一系列 RETURN NEXTRETURN QUERY 命令指定,然後使用一個不帶引數的最終 RETURN 命令來指示函式已執行完畢。RETURN NEXT 可與標量和複合資料型別一起使用;對於複合結果型別,將返回整個結果“”。RETURN QUERY 將執行查詢的結果追加到函式的返回集中。RETURN NEXTRETURN QUERY 可以在單個集合返回函式中自由混合使用,在這種情況下,它們的輸出將連線起來。

RETURN NEXTRETURN QUERY 實際上並不會從函式返回——它們只是將零行或多行追加到函式的返回集中。然後,執行將繼續 PL/pgSQL 函式中的下一個語句。隨著連續的 RETURN NEXTRETURN QUERY 命令的執行,結果集將被構建起來。最終的 RETURN(應不帶引數)會導致控制退出函式(或者您可以讓控制到達函式末尾)。

RETURN QUERY 有一個變體 RETURN QUERY EXECUTE,它指定要動態執行的查詢。引數表示式可以透過 USING 插入到計算出的查詢字串中,這與普通 EXECUTE 命令中的方式相同。

如果您宣告的函式帶有輸出引數,則只需編寫 RETURN NEXT,後面不跟表示式。每次執行時,輸出引數變數的當前值將被儲存,以便最終作為結果集中的一行返回。請注意,您必須將函式宣告為返回 SETOF record(當有多個輸出引數時),或 SETOF sometype(當只有一個型別為 sometype 的輸出引數時),才能建立帶有輸出引數的集合返回函式。

以下是一個使用 RETURN NEXT 的函式示例

CREATE TABLE foo (fooid INT, foosubid INT, fooname TEXT);
INSERT INTO foo VALUES (1, 2, 'three');
INSERT INTO foo VALUES (4, 5, 'six');

CREATE OR REPLACE FUNCTION get_all_foo() RETURNS SETOF foo AS
$BODY$
DECLARE
    r foo%rowtype;
BEGIN
    FOR r IN
        SELECT * FROM foo WHERE fooid > 0
    LOOP
        -- can do some processing here
        RETURN NEXT r; -- return current row of SELECT
    END LOOP;
    RETURN;
END;
$BODY$
LANGUAGE plpgsql;

SELECT * FROM get_all_foo();

以下是一個使用 RETURN QUERY 的函式示例

CREATE FUNCTION get_available_flightid(date) RETURNS SETOF integer AS
$BODY$
BEGIN
    RETURN QUERY SELECT flightid
                   FROM flight
                  WHERE flightdate >= $1
                    AND flightdate < ($1 + 1);

    -- Since execution is not finished, we can check whether rows were returned
    -- and raise exception if not.
    IF NOT FOUND THEN
        RAISE EXCEPTION 'No flight at %.', $1;
    END IF;

    RETURN;
 END;
$BODY$
LANGUAGE plpgsql;

-- Returns available flights or raises exception if there are no
-- available flights.
SELECT * FROM get_available_flightid(CURRENT_DATE);

注意

如上所述,RETURN NEXTRETURN QUERY 的當前實現是在函式返回之前儲存整個結果集。這意味著,如果 PL/pgSQL 函式產生非常大的結果集,效能可能會很差:資料將被寫入磁碟以避免記憶體不足,但函式本身在生成整個結果集之前不會返回。未來版本的 PL/pgSQL 可能會允許使用者定義沒有此限制的集合返回函式。當前,資料開始寫入磁碟的點由 work_mem 配置變數控制。具有足夠記憶體將更大結果集儲存在記憶體中的管理員應考慮增加此引數。

41.6.2. 從過程返回 #

過程沒有返回值。因此,過程可以在沒有 RETURN 語句的情況下結束。如果您希望使用 RETURN 語句提前退出程式碼,則只需編寫 RETURN,後面不跟表示式。

如果過程有輸出引數,則輸出引數變數的最終值將返回給呼叫者。

41.6.3. 呼叫過程 #

PL/pgSQL 函式、過程或 DO 塊可以使用 CALL 呼叫過程。輸出引數的處理方式與普通 SQL 中的 CALL 工作方式不同。過程的每個 OUTINOUT 引數必須對應於 CALL 語句中的一個變數,並且無論過程返回什麼,都會在過程返回後將其分配回該變數。例如

CREATE PROCEDURE triple(INOUT x int)
LANGUAGE plpgsql
AS $$
BEGIN
    x := x * 3;
END;
$$;

DO $$
DECLARE myvar int := 5;
BEGIN
  CALL triple(myvar);
  RAISE NOTICE 'myvar = %', myvar;  -- prints 15
END;
$$;

對應於輸出引數的變數可以是簡單變數,也可以是複合型別變數的欄位。當前,它不能是陣列的元素。

41.6.4. 條件語句 #

IFCASE 語句允許您根據特定條件執行替代命令。PL/pgSQL 有三種形式的 IF

  • IF ... THEN ... END IF

  • IF ... THEN ... ELSE ... END IF

  • IF ... THEN ... ELSIF ... THEN ... ELSE ... END IF

以及兩種形式的 CASE

  • CASE ... WHEN ... THEN ... ELSE ... END CASE

  • CASE WHEN ... THEN ... ELSE ... END CASE

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

41.6.4.2. IF-THEN-ELSE #

IF boolean-expression THEN
    statements
ELSE
    statements
END IF;

IF-THEN-ELSE 語句在 IF-THEN 的基礎上增加了功能,允許您指定當條件不為真時應執行的替代語句集。(請注意,這包括條件評估為 NULL 的情況。)

示例

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;

41.6.4.3. IF-THEN-ELSIF #

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

有時會存在兩個以上的選擇。IF-THEN-ELSIF 提供了一種方便的方法來依次檢查多個選項。IF 條件按順序測試,直到找到第一個為真的條件。然後執行關聯的語句,之後控制傳遞到 END IF 之後的下一個語句。(任何後續的 IF 條件將 不會 進行測試。)如果所有 IF 條件都不為真,則執行 ELSE 塊(如果存在)。

以下是一個例子

IF number = 0 THEN
    result := 'zero';
ELSIF number > 0 THEN
    result := 'positive';
ELSIF number < 0 THEN
    result := 'negative';
ELSE
    -- hmm, the only other possibility is that number is null
    result := 'NULL';
END IF;

關鍵字 ELSIF 也可以拼寫為 ELSEIF

另一種完成相同任務的方法是巢狀 IF-THEN-ELSE 語句,如下面的示例所示

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

然而,這種方法需要為每個 IF 編寫匹配的 END IF,因此當存在許多選項時,它比使用 ELSIF 更加麻煩。

41.6.4.4. 簡單 CASE #

CASE search-expression
    WHEN expression [, expression [ ... ]] THEN
      statements
  [ WHEN expression [, expression [ ... ]] THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

簡單形式的 CASE 基於運算元的相等性提供條件執行。search-expression 被(僅一次)計算,並依次與 WHEN 子句中的每個 expression 進行比較。如果找到匹配項,則執行相應的 statements,然後控制傳遞到 END CASE 之後的下一個語句。(後續的 WHEN 表示式不會被評估。)如果未找到匹配項,則執行 ELSE statements;但如果 ELSE 不存在,則會引發 CASE_NOT_FOUND 異常。

以下是一個簡單的例子

CASE x
    WHEN 1, 2 THEN
        msg := 'one or two';
    ELSE
        msg := 'other value than one or two';
END CASE;

41.6.4.5. 搜尋式 CASE #

CASE
    WHEN boolean-expression THEN
      statements
  [ WHEN boolean-expression THEN
      statements
    ... ]
  [ ELSE
      statements ]
END CASE;

搜尋式形式的 CASE 基於布林表示式的真偽提供條件執行。WHEN 子句的每個 boolean-expression 按順序評估,直到找到一個產生 true 的表示式。然後執行相應的 statements,然後控制傳遞到 END CASE 之後的下一個語句。(後續的 WHEN 表示式不會被評估。)如果沒有找到真結果,則執行 ELSE statements;但如果 ELSE 不存在,則會引發 CASE_NOT_FOUND 異常。

以下是一個例子

CASE
    WHEN x BETWEEN 0 AND 10 THEN
        msg := 'value is between zero and ten';
    WHEN x BETWEEN 11 AND 20 THEN
        msg := 'value is between eleven and twenty';
END CASE;

這種形式的 CASEIF-THEN-ELSIF 完全等價,除了規則是:到達一個省略的 ELSE 子句會引發錯誤,而不是什麼都不做。

41.6.5. 簡單迴圈 #

使用 LOOPEXITCONTINUEWHILEFORFOREACH 語句,您可以安排您的 PL/pgSQL 函式重複執行一系列命令。

41.6.5.1. LOOP #

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

LOOP 定義一個無條件迴圈,該迴圈將無限重複,直到被 EXITRETURN 語句終止。可選的 label 可以被巢狀迴圈內的 EXITCONTINUE 語句使用,以指定這些語句引用哪個迴圈。

41.6.5.2. EXIT #

EXIT [ label ] [ WHEN boolean-expression ];

如果沒有給出 label,則終止最內層迴圈,並接著執行 END LOOP 之後的語句。如果給出了 label,則它必須是當前或某個外層巢狀迴圈或塊的標籤。然後,指定的迴圈或塊將被終止,並且控制將繼續執行迴圈/塊的相應 END 之後的語句。

如果指定了 WHEN,則僅當 boolean-expression 為真時才會發生迴圈退出。否則,控制將傳遞到 EXIT 之後的語句。

EXIT 可用於所有型別的迴圈;它不限於用於無條件迴圈。

當與 BEGIN 塊一起使用時,EXIT 將控制傳遞到塊末尾之後的下一個語句。請注意,為此目的必須使用標籤;未標記的 EXIT 永遠不會被視為匹配 BEGIN 塊。(這是對 PostgreSQL 8.4 之前的版本的更改,當時允許未標記的 EXIT 匹配 BEGIN 塊。)

示例

LOOP
    -- some computations
    IF count > 0 THEN
        EXIT;  -- exit loop
    END IF;
END LOOP;

LOOP
    -- some computations
    EXIT WHEN count > 0;  -- same result as previous example
END LOOP;

<<ablock>>
BEGIN
    -- some computations
    IF stocks > 100000 THEN
        EXIT ablock;  -- causes exit from the BEGIN block
    END IF;
    -- computations here will be skipped when stocks > 100000
END;

41.6.5.3. CONTINUE #

CONTINUE [ label ] [ WHEN boolean-expression ];

如果沒有給出 label,則開始最內層迴圈的下一次迭代。也就是說,迴圈體中剩餘的所有語句都將被跳過,並且控制將返回到迴圈控制表示式(如果存在)以確定是否需要另一次迴圈迭代。如果存在 label,則它指定了將繼續執行的迴圈的標籤。

如果指定了 WHEN,則僅當 boolean-expression 為真時才會開始迴圈的下一次迭代。否則,控制將傳遞到 CONTINUE 之後的語句。

CONTINUE 可用於所有型別的迴圈;它不限於用於無條件迴圈。

示例

LOOP
    -- some computations
    EXIT WHEN count > 100;
    CONTINUE WHEN count < 50;
    -- some computations for count IN [50 .. 100]
END LOOP;

41.6.5.4. WHILE #

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

WHILE 語句在 boolean-expression 評估為真時重複執行一系列語句。表示式在每次進入迴圈體之前進行檢查。

例如

WHILE amount_owed > 0 AND gift_certificate_balance > 0 LOOP
    -- some computations here
END LOOP;

WHILE NOT done LOOP
    -- some computations here
END LOOP;

41.6.5.5. FOR(整數變體) #

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

此形式的 FOR 建立一個迴圈,該迴圈在整數值範圍內迭代。變數 name 被自動定義為 integer 型別,並且僅在迴圈內部存在(在迴圈內部,忽略該變數名的任何現有定義)。給出範圍的下限和上限的兩個表示式在進入迴圈時被評估一次。如果未指定 BY 子句,則迭代步長為 1,否則為 BY 子句中指定的值,該值在迴圈進入時被評估一次。如果指定了 REVERSE,則在每次迭代後減去步長值,而不是加。

整數 FOR 迴圈的一些示例

FOR i IN 1..10 LOOP
    -- i will take on the values 1,2,3,4,5,6,7,8,9,10 within the loop
END LOOP;

FOR i IN REVERSE 10..1 LOOP
    -- i will take on the values 10,9,8,7,6,5,4,3,2,1 within the loop
END LOOP;

FOR i IN REVERSE 10..1 BY 2 LOOP
    -- i will take on the values 10,8,6,4,2 within the loop
END LOOP;

如果下限大於上限(在 REVERSE 的情況下小於),則根本不執行迴圈體。不會引發錯誤。

如果 label 附加到 FOR 迴圈,則可以使用帶標籤的限定名稱引用整數迴圈變數。

41.6.6. 遍歷查詢結果迴圈 #

使用不同型別的 FOR 迴圈,您可以遍歷查詢結果並相應地操作這些資料。語法是

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

target 是一個記錄變數、行變數或逗號分隔的標量變數列表。target 被依次賦值 query 返回的每一行,並且迴圈體為每一行執行。例如

CREATE FUNCTION refresh_mviews() RETURNS integer AS $$
DECLARE
    mviews RECORD;
BEGIN
    RAISE NOTICE 'Refreshing all materialized views...';

    FOR mviews IN
       SELECT n.nspname AS mv_schema,
              c.relname AS mv_name,
              pg_catalog.pg_get_userbyid(c.relowner) AS owner
         FROM pg_catalog.pg_class c
    LEFT JOIN pg_catalog.pg_namespace n ON (n.oid = c.relnamespace)
        WHERE c.relkind = 'm'
     ORDER BY 1
    LOOP

        -- Now "mviews" has one record with information about the materialized view

        RAISE NOTICE 'Refreshing materialized view %.% (owner: %)...',
                     quote_ident(mviews.mv_schema),
                     quote_ident(mviews.mv_name),
                     quote_ident(mviews.owner);
        EXECUTE format('REFRESH MATERIALIZED VIEW %I.%I', mviews.mv_schema, mviews.mv_name);
    END LOOP;

    RAISE NOTICE 'Done refreshing materialized views.';
    RETURN 1;
END;
$$ LANGUAGE plpgsql;

如果迴圈被 EXIT 語句終止,則最後一個賦值的行值在迴圈之後仍然可訪問。

此型別 FOR 語句中使用的 query 可以是任何返回行給呼叫者的 SQL 命令:SELECT 是最常見的情況,但您也可以使用帶有 RETURNING 子句的 INSERTUPDATEDELETEMERGE。某些實用程式命令,如 EXPLAIN,也將起作用。

PL/pgSQL 變數被查詢引數替換,並且查詢計劃被快取以供重用,具體細節請參見 Section 41.11.1Section 41.11.2

FOR-IN-EXECUTE 語句是遍歷行的另一種方式

[ <<label>> ]
FOR target IN EXECUTE text_expression [ USING expression [, ... ] ] LOOP
    statements
END LOOP [ label ];

這與前一種形式類似,只是源查詢被指定為一個字串表示式,該表示式在每次進入 FOR 迴圈時進行評估和重新計劃。這允許程式設計師像使用普通 EXECUTE 語句一樣,選擇預計劃查詢的速度或動態查詢的靈活性。與 EXECUTE 一樣,可以透過 USING 將引數值插入到動態命令中。

指定應遍歷其結果的查詢的另一種方法是將其宣告為遊標。這在 Section 41.7.4 中進行了描述。

41.6.7. 遍歷陣列迴圈 #

FOREACH 迴圈非常類似於 FOR 迴圈,但它不是遍歷 SQL 查詢返回的行,而是遍歷陣列值的元素。(通常,FOREACH 用於迴圈遍歷複合值表示式的元件;未來可能會新增用於迴圈遍歷除陣列以外的複合型別的變體。)用於遍歷陣列的 FOREACH 語句是

[ <<label>> ]
FOREACH target [ SLICE number ] IN ARRAY expression LOOP
    statements
END LOOP [ label ];

不帶 SLICE,或者如果指定了 SLICE 0,則迴圈遍歷透過評估 expression 生成的陣列的單個元素。target 變數按順序被賦值每個元素的值,並且迴圈體為每個元素執行。以下是一個遍歷整數陣列元素的示例

CREATE FUNCTION sum(int[]) RETURNS int8 AS $$
DECLARE
  s int8 := 0;
  x int;
BEGIN
  FOREACH x IN ARRAY $1
  LOOP
    s := s + x;
  END LOOP;
  RETURN s;
END;
$$ LANGUAGE plpgsql;

無論陣列維度如何,元素都按儲存順序訪問。雖然 target 通常只是一個變數,但在遍歷複合值(記錄)陣列時,它可以是一個變數列表。在這種情況下,對於每個陣列元素,變數將從複合值的連續列中賦值。

SLICE 為正值時,FOREACH 遍歷陣列的切片而不是單個元素。SLICE 值必須是一個不大於陣列維數的整數常量。target 變數必須是一個數組,它接收陣列值的連續切片,其中每個切片都具有 SLICE 指定的維數。以下是一個遍歷一維切片的示例

CREATE FUNCTION scan_rows(int[]) RETURNS void AS $$
DECLARE
  x int[];
BEGIN
  FOREACH x SLICE 1 IN ARRAY $1
  LOOP
    RAISE NOTICE 'row = %', x;
  END LOOP;
END;
$$ LANGUAGE plpgsql;

SELECT scan_rows(ARRAY[[1,2,3],[4,5,6],[7,8,9],[10,11,12]]);

NOTICE:  row = {1,2,3}
NOTICE:  row = {4,5,6}
NOTICE:  row = {7,8,9}
NOTICE:  row = {10,11,12}

41.6.8. 捕獲錯誤 #

預設情況下,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,然後控制傳遞到 END 之後的下一個語句。但是,如果在 statements 中發生錯誤,則會放棄對 statements 的進一步處理,並將控制傳遞給 EXCEPTION 列表。列表將被搜尋以查詢與發生的錯誤匹配的第一個 condition。如果找到匹配項,則執行相應的 handler_statements,然後控制傳遞到 END 之後的下一個語句。如果沒有找到匹配項,則錯誤將像沒有 EXCEPTION 子句一樣傳播出去:錯誤可以被帶有 EXCEPTION 的巢狀塊捕獲,如果沒有,則會中止函式的處理。

condition 名稱可以是 Appendix A 中顯示的任何名稱。類別名稱匹配其類別內的任何錯誤。特殊條件名稱 OTHERS 匹配除 QUERY_CANCELEDASSERT_FAILURE 之外的所有錯誤型別。(透過名稱捕獲這兩個錯誤型別是可能的,但通常不明智。)條件名稱不區分大小寫。此外,可以透過 SQLSTATE 程式碼指定錯誤條件;例如,以下是等效的

WHEN division_by_zero THEN ...
WHEN SQLSTATE '22012' THEN ...

如果在選定的 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 的遞增值,但 UPDATE 命令的效果將被回滾。但是,在塊之前的 INSERT 命令不會被回滾,因此最終結果是資料庫包含 Tom Jones 而不是 Joe Jones

提示

包含 EXCEPTION 子句的塊在進入和退出時比沒有它的塊要昂貴得多。因此,請不要在不需要時使用 EXCEPTION

示例 41.2. 帶有 UPDATE/INSERT 的異常

此示例使用異常處理來執行 UPDATEINSERT(視情況而定)。建議應用程式使用帶有 ON CONFLICT DO UPDATEINSERT,而不是實際使用此模式。此示例主要用於說明 PL/pgSQL 控制流結構的使用。

CREATE TABLE db (a INT PRIMARY KEY, b TEXT);

CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
    LOOP
        -- first try to update the key
        UPDATE db SET b = data WHERE a = key;
        IF found THEN
            RETURN;
        END IF;
        -- not there, so try to insert the key
        -- if someone else inserts the same key concurrently,
        -- we could get a unique-key failure
        BEGIN
            INSERT INTO db(a,b) VALUES (key, data);
            RETURN;
        EXCEPTION WHEN unique_violation THEN
            -- Do nothing, and loop to try the UPDATE again.
        END;
    END LOOP;
END;
$$
LANGUAGE plpgsql;

SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

此程式碼假定 unique_violation 錯誤是由 INSERT 引起的,而不是由表上的觸發器函式中的 INSERT 等引起的。它也可能表現不佳,如果表上有多個唯一索引,因為它將重試操作,而不管哪個索引導致了錯誤。透過使用接下來討論的功能來檢查捕獲到的錯誤是否是預期的錯誤,可以獲得更高的安全性。


41.6.8.1. 獲取錯誤資訊 #

異常處理程式經常需要識別發生的特定錯誤。PL/pgSQL 中獲取當前異常資訊有兩種方法:特殊變數和 GET STACKED DIAGNOSTICS 命令。

在異常處理程式中,特殊變數 SQLSTATE 包含與引發的異常相對應的錯誤程式碼(有關可能錯誤程式碼的列表,請參閱 Table A.1)。特殊變數 SQLERRM 包含與異常相關的錯誤訊息。這些變數在異常處理程式外部是未定義的。

在異常處理程式中,還可以透過使用 GET STACKED DIAGNOSTICS 命令來檢索當前異常的資訊,該命令的形式是

GET STACKED DIAGNOSTICS variable { = | := } item [ , ... ];

每個 item 都是一個關鍵字,用於標識要分配給指定 variable 的狀態值(該變數應具有接收它的正確資料型別)。當前可用的狀態項顯示在 Table 41.2 中。

Table 41.2. 錯誤診斷項

名稱 型別 描述
RETURNED_SQLSTATE text 異常的 SQLSTATE 錯誤程式碼
COLUMN_NAME text 與異常相關的列名
CONSTRAINT_NAME text 與異常相關的約束名稱
PG_DATATYPE_NAME text 與異常相關的資料型別名稱
MESSAGE_TEXT text 異常主訊息的文字
TABLE_NAME text 與異常相關的表名
SCHEMA_NAME text 與異常相關的模式名
PG_EXCEPTION_DETAIL text 異常詳細訊息的文字(如果有)
PG_EXCEPTION_HINT text 異常提示訊息的文字(如果有)
PG_EXCEPTION_CONTEXT text 描述異常發生時呼叫堆疊的文字行(參見 Section 41.6.9

如果異常未為某個項設定值,則返回空字串。

以下是一個例子

DECLARE
  text_var1 text;
  text_var2 text;
  text_var3 text;
BEGIN
  -- some processing which might cause an exception
  ...
EXCEPTION WHEN OTHERS THEN
  GET STACKED DIAGNOSTICS text_var1 = MESSAGE_TEXT,
                          text_var2 = PG_EXCEPTION_DETAIL,
                          text_var3 = PG_EXCEPTION_HINT;
END;

41.6.9. 獲取執行位置資訊 #

前面在 Section 41.5.5 中描述的 GET DIAGNOSTICS 命令用於檢索有關當前執行狀態的資訊(而上面討論的 GET STACKED DIAGNOSTICS 命令報告的是關於先前錯誤發生時的執行狀態的資訊)。其 PG_CONTEXT 狀態項對於識別當前執行位置非常有用。PG_CONTEXT 返回一個文字字串,其中包含描述呼叫堆疊的文字行。第一行指的是當前函式和當前正在執行的 GET DIAGNOSTICS 命令。第二行及後續行指代呼叫堆疊更上層的呼叫函式。例如

CREATE OR REPLACE FUNCTION outer_func() RETURNS integer AS $$
BEGIN
  RETURN inner_func();
END;
$$ LANGUAGE plpgsql;

CREATE OR REPLACE FUNCTION inner_func() RETURNS integer AS $$
DECLARE
  stack text;
BEGIN
  GET DIAGNOSTICS stack = PG_CONTEXT;
  RAISE NOTICE E'--- Call Stack ---\n%', stack;
  RETURN 1;
END;
$$ LANGUAGE plpgsql;

SELECT outer_func();

NOTICE:  --- Call Stack ---
PL/pgSQL function inner_func() line 5 at GET DIAGNOSTICS
PL/pgSQL function outer_func() line 3 at RETURN
CONTEXT:  PL/pgSQL function outer_func() line 3 at RETURN
 outer_func
 ------------
           1
(1 row)

GET STACKED DIAGNOSTICS ... PG_EXCEPTION_CONTEXT 返回相同的堆疊跟蹤,但描述的是檢測到錯誤的位置,而不是當前位置。

提交更正

如果您在文件中看到任何不正確、與您對特定功能的體驗不符或需要進一步澄清的內容,請使用 此表單 報告文件問題。