支援的版本:目前 (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.5. 基本陳述式 #

在本節和接下來的章節中,我們將描述 PL/pgSQL 明確理解的所有陳述式類型。 任何未被識別為這些陳述式類型之一的內容,都會被假定為 SQL 指令,並被傳送到主資料庫引擎執行,如第 41.5.2 節所述。

41.5.1. 賦值 #

將值賦予 PL/pgSQL 變數的寫法如下:

variable { := | = } expression;

如先前所述,此類陳述式中的運算式是透過傳送到主資料庫引擎的 SQL SELECT 指令來評估的。 該運算式必須產生單一值(如果變數是列或記錄變數,則可能是一個列值)。 目標變數可以是簡單變數(可選擇使用區塊名稱限定)、列或記錄目標的欄位,或陣列目標的元素或切片。 等於 (=) 可以用來代替符合 PL/SQL 規範的 :=

如果運算式的結果資料類型與變數的資料類型不符,則該值將被強制轉換,如同透過賦值轉換一樣(請參閱第 10.4 節)。 如果所涉及的資料類型配對沒有已知的賦值轉換,則 PL/pgSQL 直譯器將嘗試以文字方式轉換結果值,也就是先套用結果類型的輸出函式,然後套用變數類型的輸入函式。 請注意,如果結果值的字串形式對於輸入函式而言不可接受,則可能會導致輸入函式產生執行階段錯誤。

範例

tax := subtotal * 0.06;
my_record.user_id := 20;
my_array[j] := 20;
my_array[1:3] := array[1,2,3];
complex_array[n].realpart = 12.3;

41.5.2. 執行 SQL 指令 #

一般來說,任何不傳回列的 SQL 指令都可以透過撰寫指令在 PL/pgSQL 函式中執行。 例如,您可以透過撰寫以下內容來建立和填入表格:

CREATE TABLE mytable (id int primary key, data text);
INSERT INTO mytable VALUES (1,'one'), (2,'two');

如果指令確實傳回列(例如 SELECT,或具有 RETURNINGINSERT/UPDATE/DELETE/MERGE),則有兩種處理方式。 當指令最多傳回一列時,或者您只關心輸出的第一列時,請照常撰寫指令,但新增一個 INTO 子句來擷取輸出,如第 41.5.3 節中所述。 若要處理所有輸出列,請將指令撰寫為 FOR 迴圈的資料來源,如第 41.6.6 節中所述。

通常,僅僅執行靜態定義的 SQL 指令是不夠的。 通常,您會希望指令使用不同的資料值,甚至以更基本的方式變化,例如在不同的時間使用不同的表格名稱。 同樣,根據情況有兩種處理方式。

在可最佳化的 SQL 指令中,PL/pgSQL 變數值可以自動插入,這些指令包括 SELECTINSERTUPDATEDELETEMERGE,以及某些包含上述指令的工具指令,例如 EXPLAINCREATE TABLE ... AS SELECT。在這些指令中,指令文本中出現的任何 PL/pgSQL 變數名稱都會被替換為查詢參數,然後在執行時提供變數的目前值作為參數值。這與前面描述的表達式處理方式完全相同;詳細資訊請參閱第 41.11.1 節

當以這種方式執行可最佳化的 SQL 指令時,PL/pgSQL 可能會快取並重複使用該指令的執行計畫,如第 41.11.2 節所述。

不可最佳化的 SQL 指令(也稱為工具指令)無法接受查詢參數。因此,在這些指令中,PL/pgSQL 變數的自動替換不起作用。若要將非常數文本包含在從 PL/pgSQL 執行的工具指令中,您必須將該工具指令建構為字串,然後 EXECUTE 它,如第 41.5.4 節所述。

如果您想以某種方式修改指令,而不是僅提供資料值(例如變更表名),則也必須使用 EXECUTE

有時,評估表達式或 SELECT 查詢但捨棄結果會很有用,例如在呼叫具有副作用但沒有可用結果值的函式時。若要在 PL/pgSQL 中執行此操作,請使用 PERFORM 陳述式。

PERFORM query;

這會執行 query 並捨棄結果。以您撰寫 SQL SELECT 指令的方式撰寫 query,但將初始關鍵字 SELECT 替換為 PERFORM。對於 WITH 查詢,請使用 PERFORM,然後將查詢放在括號中。(在這種情況下,查詢只能傳回一列。)PL/pgSQL 變數將被替換到查詢中,如上所述,並且計畫以相同的方式快取。此外,如果查詢產生至少一列,則特殊變數 FOUND 設定為 true;如果未產生任何列,則設定為 false(請參閱第 41.5.5 節)。

注意

人們可能會期望直接撰寫 SELECT 就能完成此結果,但目前唯一接受的方式是 PERFORM。如果 SQL 指令可以傳回列,例如 SELECT,則除非它具有如下一節所述的 INTO 子句,否則將被拒絕並產生錯誤。

範例

PERFORM create_mv('cs_session_page_requests_mv', my_query);

41.5.3. 執行具有單列結果的指令 #

產生單列(可能具有多個欄位)的 SQL 指令的結果可以指派給記錄變數、列類型變數或純量變數列表。這是透過撰寫基本 SQL 指令並新增 INTO 子句來完成的。例如:

SELECT select_expressions INTO [STRICT] target FROM ...;
INSERT ... RETURNING expressions INTO [STRICT] target;
UPDATE ... RETURNING expressions INTO [STRICT] target;
DELETE ... RETURNING expressions INTO [STRICT] target;
MERGE ... RETURNING expressions INTO [STRICT] target;

其中 target 可以是記錄變數、列變數,或以逗號分隔的簡單變數和記錄/列欄位列表。PL/pgSQL 變數將被替換到指令的其餘部分(即除了 INTO 子句之外的所有內容)中,如上所述,並且計畫以相同的方式快取。這適用於 SELECTINSERT/UPDATE/DELETE/MERGERETURNING,以及傳回列集的某些工具指令,例如 EXPLAIN。除了 INTO 子句之外,SQL 指令與在 PL/pgSQL 之外撰寫的指令相同。

提示

請注意,對帶有 INTOSELECT 的這種解讀與 PostgreSQL 的常規 SELECT INTO 指令完全不同,在常規指令中,INTO 目標是新建立的資料表。如果您想在 PL/pgSQL 函式中從 SELECT 結果建立資料表,請使用語法 CREATE TABLE ... AS SELECT

如果使用列變數或變數列表作為目標,則指令的結果欄位必須與目標的結構完全匹配,包括數量和資料類型,否則會發生執行階段錯誤。當記錄變數是目標時,它會自動將自身配置為指令結果欄位的列類型。

INTO 子句幾乎可以出現在 SQL 指令中的任何位置。習慣上,它會寫在 SELECT 指令中 select_expressions 列表的前面或後面,或者寫在其他指令類型的指令結尾。建議您遵循此慣例,以防 PL/pgSQL 解析器在未來的版本中變得更嚴格。

如果未在 INTO 子句中指定 STRICT,則 target 將設定為指令傳回的第一列,如果指令未傳回任何列,則設定為 null。(請注意,除非您使用了 ORDER BY,否則 第一列 沒有明確定義。)第一列之後的任何結果列都會被捨棄。您可以檢查特殊的 FOUND 變數(請參閱第 41.5.5 節)以確定是否傳回了一列。

SELECT * INTO myrec FROM emp WHERE empname = myname;
IF NOT FOUND THEN
    RAISE EXCEPTION 'employee % not found', myname;
END IF;

如果指定了 STRICT 選項,則指令必須恰好傳回一列,否則會報告執行階段錯誤,即 NO_DATA_FOUND(無列)或 TOO_MANY_ROWS(多於一列)。如果您希望捕捉錯誤,可以使用例外處理區塊,例如:

BEGIN
    SELECT * INTO STRICT myrec FROM emp WHERE empname = myname;
    EXCEPTION
        WHEN NO_DATA_FOUND THEN
            RAISE EXCEPTION 'employee % not found', myname;
        WHEN TOO_MANY_ROWS THEN
            RAISE EXCEPTION 'employee % not unique', myname;
END;

成功執行帶有 STRICT 的指令始終會將 FOUND 設定為 true。

對於帶有 RETURNINGINSERT/UPDATE/DELETE/MERGE,即使未指定 STRICTPL/pgSQL 也會報告多於一列的錯誤。這是因為沒有像 ORDER BY 這樣的選項可以用來確定應傳回哪個受影響的列。

如果為函式啟用了 print_strict_params,則當因為不符合 STRICT 的要求而擲出錯誤時,錯誤訊息的 DETAIL 部分將包含有關傳遞給指令的參數的資訊。您可以透過設定 plpgsql.print_strict_params 來變更所有函式的 print_strict_params 設定,但只有後續的函式編譯會受到影響。您也可以使用編譯器選項在每個函式的基礎上啟用它,例如:

CREATE FUNCTION get_userid(username text) RETURNS int
AS $$
#print_strict_params on
DECLARE
userid int;
BEGIN
    SELECT users.userid INTO STRICT userid
        FROM users WHERE users.username = get_userid.username;
    RETURN userid;
END;
$$ LANGUAGE plpgsql;

失敗時,此函式可能會產生如下錯誤訊息:

ERROR:  query returned no rows
DETAIL:  parameters: username = 'nosuchuser'
CONTEXT:  PL/pgSQL function get_userid(text) line 6 at SQL statement

注意

STRICT 選項的行為與 Oracle PL/SQL 的 SELECT INTO 和相關語法相同。

41.5.4. 執行動態指令 #

通常,您會希望在 PL/pgSQL 函數中產生動態指令,也就是每次執行時會涉及不同資料表或不同資料類型的指令。PL/pgSQL 嘗試為指令快取計畫的一般做法(如第 41.11.2 節所討論的)在這種情況下不起作用。為了處理這類問題,提供了 EXECUTE 語法

EXECUTE command-string [ INTO [STRICT] target ] [ USING expression [, ... ] ];

其中 command-string 是一個產生字串(text 類型)的表示式,該字串包含要執行的指令。 可選的 target 是一個記錄變數、一個列變數或一個以逗號分隔的簡單變數和記錄/列欄位列表,指令的結果將儲存在其中。 可選的 USING 表示式提供要插入到指令中的值。

不會對計算出的指令字串進行 PL/pgSQL 變數的替換。 任何需要的變數值都必須在建構指令字串時插入其中;或者您可以使用如下所述的參數。

此外,對於透過 EXECUTE 執行的指令,沒有計畫快取。 相反地,每次執行語法時都會計畫該指令。 因此,可以在函數中動態建立指令字串,以對不同的資料表和欄位執行動作。

INTO 子句指定應該將傳回列的 SQL 指令的結果指派到哪裡。 如果提供了列變數或變數列表,它必須與指令結果的結構完全匹配;如果提供了記錄變數,它將自動設定為與結果結構匹配。 如果傳回多個列,則只會將第一個列指派給 INTO 變數。 如果沒有傳回列,則會將 NULL 指派給 INTO 變數。 如果未指定 INTO 子句,則會捨棄指令結果。

如果給定了 STRICT 選項,除非指令產生剛好一列,否則會回報錯誤。

指令字串可以使用參數值,這些參數值在指令中以 $1$2 等表示。 這些符號指的是 USING 子句中提供的值。 這種方法通常比將資料值作為文字插入指令字串中更好:它避免了將值轉換為文字再轉換回來的執行時額外負擔,而且它更不容易受到 SQL 注入攻擊,因為不需要引號或跳脫字元。 例如:

EXECUTE 'SELECT count(*) FROM mytable WHERE inserted_by = $1 AND inserted <= $2'
   INTO c
   USING checked_user, checked_date;

請注意,參數符號只能用於資料值 — 如果您想要使用動態決定的資料表或欄位名稱,則必須以文字方式將它們插入指令字串中。 例如,如果需要針對動態選擇的資料表執行上述查詢,您可以這樣做:

EXECUTE 'SELECT count(*) FROM '
    || quote_ident(tabname)
    || ' WHERE inserted_by = $1 AND inserted <= $2'
   INTO c
   USING checked_user, checked_date;

一種更簡潔的方法是使用 format()%I 規範來插入具有自動引號的資料表或欄位名稱:

EXECUTE format('SELECT count(*) FROM %I '
   'WHERE inserted_by = $1 AND inserted <= $2', tabname)
   INTO c
   USING checked_user, checked_date;

(這個範例依賴於 SQL 規則,即以換行符分隔的字串字面值會隱含地串連在一起。)

參數符號的另一個限制是,它們僅在可最佳化的 SQL 指令中起作用(SELECTINSERTUPDATEDELETEMERGE 以及包含其中一個指令的特定指令)。 在其他語法類型(通常稱為公用程式語法)中,即使它們只是資料值,您也必須以文字方式插入值。

具有簡單常數字串指令和一些 USING 參數的 EXECUTE(如上面的第一個範例)在功能上等同於直接在 PL/pgSQL 中編寫指令,並允許自動替換 PL/pgSQL 變數。 重要的區別在於,EXECUTE 會在每次執行時重新計畫指令,產生一個特定於目前參數值的計畫; 而 PL/pgSQL 否則可能會建立通用計畫並快取它以供重複使用。 在最佳計畫強烈依賴參數值的情況下,使用 EXECUTE 可以積極地確保不會選擇通用計畫,這會很有幫助。

SELECT INTO 目前在 EXECUTE 中不受支援;相反地,執行簡單的 SELECT 指令,並將 INTO 指定為 EXECUTE 本身的一部分。

注意

PL/pgSQLEXECUTE 語法與 PostgreSQL 伺服器支援的 EXECUTE SQL 語法無關。 伺服器的 EXECUTE 語法不能直接在 PL/pgSQL 函數中使用(而且不需要)。

範例 41.1. 在動態查詢中加上值引號

在使用動態指令時,您通常必須處理單引號的跳脫。 在您的函數主體中加上固定文字引號的建議方法是使用錢字符號引號。 (如果您有未使用錢字符號引號的舊版程式碼,請參閱 第 41.12.1 節中的概述,這可以在將上述程式碼翻譯為更合理的架構時節省您的一些精力。)

動態值需要仔細處理,因為它們可能包含引號字元。 一個使用 format() 的範例(這假設您正在使用錢字符號引號來引用函數主體,因此不需要將引號加倍):

EXECUTE format('UPDATE tbl SET %I = $1 '
   'WHERE key = $2', colname) USING newvalue, keyvalue;

也可以直接呼叫引號函數:

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = '
        || quote_literal(newvalue)
        || ' WHERE key = '
        || quote_literal(keyvalue);

這個範例示範了 quote_identquote_literal 函數的用法(請參閱第 9.4 節)。 為了安全起見,包含欄位或資料表識別碼的表示式應在插入動態查詢之前透過 quote_ident 傳遞。 包含應該是已建構指令中的字串字面值的值的表示式應透過 quote_literal 傳遞。 這些函數會採取適當的步驟來傳回以雙引號或單引號分別括住的輸入文字,並適當地跳脫任何內嵌的特殊字元。

因為 quote_literal 被標記為 STRICT,所以當使用空引數呼叫它時,它總是會傳回 null。 在上面的範例中,如果 newvaluekeyvalue 為 null,則整個動態查詢字串將變為 null,從而導致 EXECUTE 發生錯誤。 您可以使用 quote_nullable 函數來避免這個問題,該函數的工作方式與 quote_literal 相同,不同之處在於,當使用 null 引數呼叫它時,它會傳回字串 NULL。 例如:

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = '
        || quote_nullable(newvalue)
        || ' WHERE key = '
        || quote_nullable(keyvalue);

如果您要處理可能為 null 的值,您通常應該使用 quote_nullable 代替 quote_literal

與往常一樣,必須小心確保查詢中的 null 值不會傳遞意想不到的結果。 例如,WHERE 子句

'WHERE key = ' || quote_nullable(keyvalue)

如果 keyvalue 為 null,則永遠不會成功,因為使用等號運算子 = 與 null 運算元的結果始終為 null。 如果您希望 null 像普通鍵值一樣工作,您需要將上述內容重寫為:

'WHERE key IS NOT DISTINCT FROM ' || quote_nullable(keyvalue)

(目前,IS NOT DISTINCT FROM 的處理效率遠低於 =,所以除非您必須,否則不要這樣做。 有關 null 和 IS DISTINCT 的更多資訊,請參閱第 9.2 節。)

請注意,使用 dollar quoting 只適用於引用固定文字。試圖以下列方式撰寫範例會是個非常糟糕的主意:

EXECUTE 'UPDATE tbl SET '
        || quote_ident(colname)
        || ' = $$'
        || newvalue
        || '$$ WHERE key = '
        || quote_literal(keyvalue);

因為如果 newvalue 的內容碰巧包含 $$,就會發生錯誤。對於您可能選擇的任何其他 dollar quoting 分隔符號,也會有相同的問題。因此,為了安全地引用事先未知的文字,您必須使用 quote_literalquote_nullablequote_ident(視情況而定)。

也可以使用 format 函數安全地建構動態 SQL 陳述式(請參閱第 9.4.1 節)。例如:

EXECUTE format('UPDATE tbl SET %I = %L '
   'WHERE key = %L', colname, newvalue, keyvalue);

%I 相當於 quote_ident%L 相當於 quote_nullableformat 函數可以與 USING 子句結合使用:

EXECUTE format('UPDATE tbl SET %I = $1 WHERE key = $2', colname)
   USING newvalue, keyvalue;

這種形式更好,因為變數是以其原生資料類型格式處理,而不是無條件地將它們轉換為文字並透過 %L 引用。它也更有效率。


範例 41.10 中可以看到一個更大的動態命令和 EXECUTE 範例,它建構並執行一個 CREATE FUNCTION 命令來定義一個新的函數。

41.5.5. 取得結果狀態 #

有幾種方法可以確定命令的效果。第一種方法是使用 GET DIAGNOSTICS 命令,其形式如下:

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

此命令允許檢索系統狀態指示器。CURRENT 是一個無意義的詞(但也請參閱第 41.6.8.1 節中的 GET STACKED DIAGNOSTICS)。每個item 是一個關鍵字,用於識別要指派給指定的 variable 的狀態值(應該是正確的資料類型才能接收它)。目前可用的狀態項目顯示在表 41.1 中。冒號等號 (:=) 可以用來代替 SQL 標準的 = 符號。一個例子:

GET DIAGNOSTICS integer_var = ROW_COUNT;

表 41.1. 可用的診斷項目

名稱 類型 描述
ROW_COUNT bigint 最近的命令所處理的列數SQL命令
PG_CONTEXT text 描述目前呼叫堆疊的文字行(請參閱第 41.6.9 節
PG_ROUTINE_OID oid 目前函數的 OID

確定命令效果的第二種方法是檢查名為 FOUND 的特殊變數,它的類型為 booleanFOUND 在每個 PL/pgSQL 函數呼叫中都以 false 開始。它由以下每種類型的陳述式設定:

  • SELECT INTO 陳述式會在指派列時將 FOUND 設定為 true,如果沒有傳回列則設定為 false。

  • PERFORM 陳述式如果產生(並捨棄)一或多列,則將 FOUND 設定為 true,如果沒有產生列則設定為 false。

  • UPDATEINSERTDELETEMERGE 陳述式如果至少影響一列,則將 FOUND 設定為 true,如果沒有影響列則設定為 false。

  • FETCH 陳述式如果傳回列,則將 FOUND 設定為 true,如果沒有傳回列則設定為 false。

  • MOVE 陳述式如果成功地重新定位游標,則將 FOUND 設定為 true,否則設定為 false。

  • FORFOREACH 陳述式如果迭代一或多次,則將 FOUND 設定為 true,否則設定為 false。當迴圈結束時,FOUND 會以這種方式設定;在迴圈執行期間,FOUND 不會被迴圈陳述式修改,但可能會被迴圈主體內的其他陳述式變更。

  • RETURN QUERYRETURN QUERY EXECUTE 陳述式如果查詢傳回至少一列,則將 FOUND 設定為 true,如果沒有傳回列則設定為 false。

其他 PL/pgSQL 陳述式不會變更 FOUND 的狀態。特別要注意的是,EXECUTE 會變更 GET DIAGNOSTICS 的輸出,但不會變更 FOUND

FOUND 是每個 PL/pgSQL 函數中的區域變數;對它的任何變更只會影響目前的函數。

41.5.6. 什麼都不做 #

有時,一個什麼都不做的佔位符陳述式很有用。例如,它可以表明 if/then/else 鏈的一個分支是有意為空的。為此,請使用 NULL 陳述式:

NULL;

例如,以下兩個程式碼片段是等效的:

BEGIN
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN
        NULL;  -- ignore the error
END;
BEGIN
    y := x / 0;
EXCEPTION
    WHEN division_by_zero THEN  -- ignore the error
END;

哪個更好取決於個人喜好。

注意

在 Oracle 的 PL/SQL 中,不允許空的陳述式清單,因此對於這種情況,必須使用 NULL 陳述式。PL/pgSQL 允許您直接什麼都不寫。

提交更正

如果您在文件中發現任何不正確、與您使用特定功能時的經驗不符,或需要進一步說明的內容,請使用此表單來報告文件問題。