支援的版本:目前 (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 中有一個欄位 fooblock.foo 也表示變數。函數參數以及諸如 FOUND 之類的特殊變數可以使用函數的名稱來限定,因為它們隱式地在一個帶有函數名稱標籤的外部區塊中宣告。

有時,修復大量 PL/pgSQL 程式碼中的所有有歧義的引用是不切實際的。在這種情況下,您可以指定 PL/pgSQL 應將有歧義的引用解析為變數(這與 PostgreSQL 9.0 之前的 PL/pgSQL 的行為相容),或解析為表欄位(這與某些其他系統(例如 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 指令中,無論 users 是否有名為 curtimecommentid 的欄位,curtimecommentid 都會參考函式的變數和參數。請注意,我們必須限定 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管理員的 SPI_prepare 函式建立預備語句。 後續對該表示式或指令的訪問會重複使用該預備語句。 因此,具有很少訪問的條件程式碼路徑的函式將永遠不會產生分析那些在目前會期中從未執行的指令的開銷。 缺點是,在執行到達函式的該部分之前,無法偵測到特定表示式或指令中的錯誤。 (微不足道的語法錯誤將在初始剖析過程中偵測到,但任何更深層的錯誤都將在執行之前無法偵測到。)

PL/pgSQL(或更準確地說,SPI 管理員)可以進一步嘗試快取與任何特定預備語句相關聯的執行計畫。 如果未使用快取的計畫,則每次訪問該語句時都會產生一個新的執行計畫,並且可以使用目前的參數值(即,PL/pgSQL 變數值)來優化所選的計畫。 如果該語句沒有參數,或執行多次,則 SPI 管理員將考慮建立一個不依賴於特定參數值的泛型計畫,並快取該計畫以供重複使用。 通常,只有在執行計畫對其中引用的 PL/pgSQL 變數的值不是很敏感時,才會發生這種情況。 如果是,則每次產生一個計畫都是淨勝利。 有關預備語句行為的更多資訊,請參閱PREPARE

由於 PL/pgSQL 以這種方式儲存預備語句,有時也儲存執行計畫,因此直接出現在 PL/pgSQL 函式中的 SQL 指令每次執行都必須參考相同的資料表和欄位;也就是說,您不能使用參數作為 SQL 指令中資料表或欄位的名稱。 若要解決此限制,您可以使用 PL/pgSQL EXECUTE 語句建構動態指令 — 代價是在每次執行時執行新的剖析分析並建構新的執行計畫。

記錄變數的可變性在本質上提出了另一個問題。 在表示式或語句中使用記錄變數的欄位時,欄位的資料類型不得從函式的一次呼叫更改為下一次呼叫,因為每個表示式都將使用首次到達該表示式時存在的資料類型進行分析。 必要時可以使用 EXECUTE 來解決此問題。

如果相同的函式用作多個資料表的觸發程序,則 PL/pgSQL 會為每個這樣的資料表獨立準備和快取語句 — 也就是說,每個觸發函式和資料表組合都有一個快取,而不僅僅是每個函式都有一個快取。 這減輕了變動資料類型的一些問題;例如,即使名為 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() 函數仍然會是更好的選擇。

提交更正

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