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

41.11. PL/pgSQL 內部機制 #

本節討論了一些對 PL/pgSQL 使用者來說經常重要的實現細節。

41.11.1. 變數替換 #

PL/pgSQL 函式中的 SQL 語句和表示式可以引用函式的變數和引數。在後臺,PL/pgSQL 會將查詢引數替換為這些引用。查詢引數只會在語法允許的地方進行替換。極端情況下,考慮這個糟糕的程式設計風格示例:

INSERT INTO foo (foo) VALUES (foo(foo));

foo 的第一次出現語法上必須是一個表名,所以它不會被替換,即使函式有一個名為 foo 的變數。第二次出現必須是該表的列名,所以它也不會被替換。同樣,第三次出現必須是一個函式名,所以它也不會被替換。只有最後一次出現才可能是一個對 PL/pgSQL 函式變數的引用。

另一種理解方式是,變數替換隻能將資料值插入到 SQL 命令中;它不能動態更改命令引用的資料庫物件。(如果您想這樣做,您必須動態構建命令字串,如 第 41.5.4 節中所述。)

由於變數名在語法上與表列名沒有區別,因此在也引用表的語句中可能會出現歧義:給定的名稱是指表列還是變數?讓我們修改之前的例子:

INSERT INTO dest (col) SELECT foo + bar FROM src;

在這裡,destsrc 必須是表名,col 必須是 dest 的一個列,但是 foobar 可以是函式的變數,也可以是 src 的列。

預設情況下,如果 SQL 語句中的一個名稱可能引用變數或表列,PL/pgSQL 會報告一個錯誤。您可以透過重新命名變數或列,或透過限定歧義引用,或透過告訴 PL/pgSQL 偏好哪種解釋來解決此問題。

最簡單的解決方法是重新命名變數或列。一個常見的編碼規則是為 PL/pgSQL 變數使用與列名不同的命名約定。例如,如果您始終將函式變數命名為 v_something,而您的任何列名都不以 v_ 開頭,則不會發生衝突。

或者,您可以限定歧義引用以使其清晰。在上例中,src.foo 將是對錶列的無歧義引用。要建立對變數的無歧義引用,請將其宣告在標記塊中,並使用塊的標籤(請參閱 第 41.2 節)。例如:

<<block>>
DECLARE
    foo int;
BEGIN
    foo := ...;
    INSERT INTO dest (col) SELECT block.foo + bar FROM src;

在這裡,即使 src 中有一個名為 foo 的列,block.foo 也指的是變數。函式引數以及像 FOUND 這樣的特殊變數,可以透過函式名來限定,因為它們隱式地宣告在一個以函式名標記的外部塊中。

有時,修復大量 PL/pgSQL 程式碼中的所有歧義引用是不切實際的。在這種情況下,您可以指定 PL/pgSQL 應將歧義引用解析為變數(這與 PL/pgSQLPostgreSQL 9.0 之前的行為相容),或解析為表列(這與其他系統如 Oracle 相容)。

要更改此係統範圍的行為,請將配置引數 plpgsql.variable_conflict 設定為 erroruse_variableuse_column 之一(其中 error 是工廠預設值)。此引數會影響當前會話中尚未編譯的 PL/pgSQL 函式語句的後續編譯。由於更改此設定可能會導致 PL/pgSQL 函式的行為發生意外變化,因此只能由超級使用者更改。

您也可以透過在函式文字的開頭插入以下特殊命令來逐個函式地設定行為:

#variable_conflict error
#variable_conflict use_variable
#variable_conflict use_column

這些命令僅影響它們所寫的函式,並覆蓋 plpgsql.variable_conflict 的設定。例如:

CREATE FUNCTION stamp_user(id int, comment text) RETURNS void AS $$
    #variable_conflict use_variable
    DECLARE
        curtime timestamp := now();
    BEGIN
        UPDATE users SET last_modified = curtime, comment = comment
          WHERE users.id = id;
    END;
$$ LANGUAGE plpgsql;

UPDATE 命令中,curtimecommentid 將引用函式的變數和引數,而不管 users 是否具有這些名稱的列。請注意,我們在 WHERE 子句中必須限定對 users.id 的引用,以使其引用表列。但是,我們不需要限定對 comment 作為 UPDATE 列表中的目標的引用,因為語法上這必須是 users 的一個列。我們可以編寫相同的函式,而不依賴於 variable_conflict 設定,如下所示:

CREATE FUNCTION stamp_user(id int, comment text) RETURNS void AS $$
    <<fn>>
    DECLARE
        curtime timestamp := now();
    BEGIN
        UPDATE users SET last_modified = fn.curtime, comment = stamp_user.comment
          WHERE users.id = stamp_user.id;
    END;
$$ LANGUAGE plpgsql;

變數替換在傳遞給 EXECUTE 或其變體之一的命令字串中不起作用。如果您需要將可變值插入此類命令,請在構建字串值時進行,或使用 USING,如 第 41.5.4 節中所述。

變數替換目前僅在 SELECTINSERTUPDATEDELETE 和包含其中任何一項的命令(如 EXPLAINCREATE TABLE ... AS SELECT)中工作,因為主 SQL 引擎只允許在這些命令中使用查詢引數。要在其他語句型別(通常稱為實用程式語句)中使用非常量名稱或值,您必須將實用程式語句構建為字串並使用 EXECUTE 執行它。

41.11.2. 計劃快取 #

首次呼叫函式時(在每個會話中),PL/pgSQL 直譯器會解析函式原始碼並生成一個內部二進位制指令樹。該指令樹完全翻譯了 PL/pgSQL 語句結構,但函式中使用的單個SQL表示式和SQL命令不會立即翻譯。

當函式中的每個表示式和SQL命令首次執行時,PL/pgSQL 直譯器會解析並分析該命令,使用SPI管理器(manager)的 SPI_prepare 函式來建立準備好的語句。後續訪問該表示式或命令時會重用準備好的語句。因此,具有很少訪問的程式碼路徑的函式將永遠不會承擔分析當前會話中從不執行的命令的開銷。一個缺點是特定表示式或命令中的錯誤直到執行到該部分時才能被檢測到。(細微的語法錯誤將在初始解析過程中被檢測到,但更深層次的問題直到執行時才能被檢測到。)

PL/pgSQL(或更準確地說,SPI管理器)還可以嘗試快取與任何特定準備好的語句相關的執行計劃。如果未使用的快取計劃,則在每次訪問語句時都會生成新的執行計劃,並且可以使用當前引數值(即 PL/pgSQL 變數值)來最佳化所選計劃。如果語句沒有引數,或者執行次數很多,SPI管理器將考慮建立一個不依賴於引數值的通用計劃,並將其快取以備重用。這通常只會在執行計劃對其中引用的 PL/pgSQL 變數的值不敏感時發生。如果敏感,每次都生成計劃會帶來淨收益。有關準備好的語句行為的更多資訊,請參閱 PREPARE

由於 PL/pgSQL 以這種方式儲存準備好的語句,有時甚至是執行計劃,因此直接出現在 PL/pgSQL 函式中的 SQL 命令在每次執行時都必須引用相同的表和列;也就是說,您不能在 SQL 命令中使用引數作為表或列的名稱。要繞過此限制,您可以使用 PL/pgSQL EXECUTE 語句來構建動態命令 — 但代價是在每次執行時都要進行新的解析分析和構建新的執行計劃。

記錄變數的可變性在這個方面帶來了另一個問題。當記錄變數的欄位用在表示式或語句中時,欄位的資料型別從函式的一次呼叫到下一次呼叫都不能改變,因為每個表示式都將使用在表示式首次到達時存在的資料型別進行分析。必要時可以使用 EXECUTE 來解決此問題。

如果同一個函式用作多個表的觸發器,PL/pgSQL 會為每個這樣的表獨立地準備和快取語句 — 也就是說,對於每個觸發器函式和表的組合都有一個快取,而不僅僅是每個函式。這緩解了資料型別變化帶來的一些問題;例如,即使 key 列在不同表中具有不同的型別,觸發器函式也能夠成功地處理名為 key 的列。

同樣,具有多型引數型別的函式為它們被呼叫的每種實際引數型別組合都有一個單獨的語句快取,這樣資料型別差異就不會導致意外的失敗。

語句快取有時會對時間敏感值的解釋產生意想不到的影響。例如,以下兩個函式的操作是不同的:

CREATE FUNCTION logfunc1(logtxt text) RETURNS void AS $$
    BEGIN
        INSERT INTO logtable VALUES (logtxt, 'now');
    END;
$$ LANGUAGE plpgsql;

CREATE FUNCTION logfunc2(logtxt text) RETURNS void AS $$
    DECLARE
        curtime timestamp;
    BEGIN
        curtime := 'now';
        INSERT INTO logtable VALUES (logtxt, curtime);
    END;
$$ LANGUAGE plpgsql;

logfunc1 的情況下,PostgreSQL 主解析器在解析 INSERT 時知道字串 'now' 應該被解釋為 timestamp,因為 logtable 的目標列就是該型別。因此,當 INSERT 被解析時,'now' 將被轉換為一個 timestamp 常量,然後在會話的整個生命週期內用於 logfunc1 的所有呼叫。不用說,這不是程式設計師想要的。更好的方法是使用 now()current_timestamp 函式。

logfunc2 的情況下,PostgreSQL 主解析器不知道 'now' 應該變成什麼型別,因此它返回一個型別為 text 的資料值,其中包含字串 now。在隨後的對區域性變數 curtime 的賦值過程中,PL/pgSQL 直譯器透過呼叫 textouttimestamp_in 函式進行轉換,將此字串轉換為 timestamp 型別。因此,計算出的時間戳會在每次執行時更新,正如程式設計師所期望的那樣。儘管這碰巧如預期般工作,但效率不高,所以使用 now() 函式仍然是更好的選擇。

提交更正

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