PL/Python、PL/Perl、PL/v8:PostgreSQL 的多語言程序擴展 | PostgreSQL

2026/07/15
PL/Python、PL/Perl、PL/v8:PostgreSQL 的多語言程序擴展 | PostgreSQL

PostgreSQL 的程序語言架構具有高度可擴展性,除了內建的 PL/pgSQL 之外,還支援 PL/PythonPL/PerlPL/v8(JavaScript) 等多種語言。本文完整解析各語言的安裝、語法、SPI 存取、效能特性與選擇策略。

可擴展程序語言架構

PostgreSQL 採用語言 Handler機制支援多種程序語言。每種語言本質上是一個共享函式庫,透過 CREATE EXTENSION 安裝後,PostgreSQL 即知道如何將函式原始碼委派給對應的執行環境:

使用者呼叫函式
     ↓
PostgreSQL 函式呼叫框架
     ↓
語言 Handler(e.g. plpython3)
     ↓
外部語言直譯器(Python / Perl / V8 Engine)
     ↓
透過 SPI 回呼 PostgreSQL 執行 SQL
     ↓
回傳結果給 PostgreSQL

所有語言函式的呼叫方式對 SQL 層完全一致,使用者感知不到底層語言差異。

支援語言總覽

語言套件名稱信任等級典型應用
PL/pgSQL內建Trusted一般商業邏輯
PL/Python 3plpython3uUntrusted資料科學、ML 整合
PL/Perlplperl / plperlu兩者皆有文字處理、正則表達式
PL/Tclpltcl / pltclu兩者皆有輕量腳本
PL/v8plv8TrustedJSON 處理、JavaScript 生態
PL/JavapljavaUntrusted企業 Java 整合
PL/RplrUntrusted統計運算

Trusted vs Untrusted 安全模型

這是選擇程序語言時最關鍵的概念。

受信任語言(Trusted)

  • 無法存取檔案系統、網路、作業系統呼叫
  • 任何資料庫使用者都可以建立函式
  • 代表:PL/pgSQL、plperl、pltcl、plv8

不受信任語言(Untrusted)

  • 可存取檔案系統、執行系統命令、開啟網路連線
  • 只有 superuser 才能建立函式
  • 名稱後綴加 uplpython3uplperlupltclu
-- 確認語言的信任屬性
SELECT lanname, lanpltrusted
FROM pg_language
ORDER BY lanname;

安全性建議:優先使用受信任語言;不受信任語言的函式務必由 DBA 審查,且設置 SECURITY DEFINER 時必須指定 search_path


PL/Python(plpython3u)

安裝

# Ubuntu / Debian
sudo apt install postgresql-plpython3-16

# 在資料庫中啟用
psql -U postgres -d mydb -c "CREATE EXTENSION plpython3u;"

基本語法

-- 最簡單的 PL/Python 函式
CREATE OR REPLACE FUNCTION py_hello(name text)
RETURNS text
LANGUAGE plpython3u
AS $$
    return f"Hello, {name}!"
$$;

SELECT py_hello('World');
-- 輸出:Hello, World!

-- 使用 Python 標準函式庫
CREATE OR REPLACE FUNCTION py_json_parse(raw_text text)
RETURNS jsonb
LANGUAGE plpython3u
AS $$
    import json
    data = json.loads(raw_text)
    return json.dumps(data)
$$;

存取表資料(plpy.execute)

-- 使用參數化查詢(防止 SQL Injection)
CREATE OR REPLACE FUNCTION get_user_safe(user_id integer)
RETURNS text
LANGUAGE plpython3u
AS $$
    plan = plpy.prepare(
        "SELECT username, email FROM users WHERE id = $1",
        ["integer"]
    )
    result = plpy.execute(plan, [user_id], 1)

    if not result:
        return None

    return result[0]["username"]
$$;

回傳集合(SETOF)

CREATE OR REPLACE FUNCTION py_generate_series(start_val int, end_val int)
RETURNS SETOF integer
LANGUAGE plpython3u
AS $$
    return range(start_val, end_val + 1)
$$;

SELECT * FROM py_generate_series(1, 5);
-- 輸出:1, 2, 3, 4, 5

共享資料(SD 與 GD)

CREATE OR REPLACE FUNCTION py_cached_lookup(key text)
RETURNS text
LANGUAGE plpython3u
AS $$
    # GDGlobal Data):同一 Session 內所有 PL/Python 函式共享
    if "cache" not in GD:
        GD["cache"] = {}

    if key in GD["cache"]:
        return GD["cache"][key]

    result = plpy.execute(
        plpy.prepare("SELECT value FROM config WHERE key = $1", ["text"]),
        [key], 1
    )

    if result:
        GD["cache"][key] = result[0]["value"]
        return result[0]["value"]

    return None
$$;

錯誤處理

CREATE OR REPLACE FUNCTION py_safe_divide(a numeric, b numeric)
RETURNS numeric
LANGUAGE plpython3u
AS $$
    try:
        if b == 0:
            plpy.error(
                "Division by zero",
                detail="Divisor cannot be zero",
                hint="Use NULLIF(b, 0) to handle this"
            )
        return a / b
    except Exception as e:
        plpy.warning(f"Unexpected error: {e}")
        return None
$$;

PL/Perl

安裝

# Ubuntu / Debian
sudo apt install postgresql-plperl-16

# 啟用受信任版本
psql -c "CREATE EXTENSION plperl;"

字串處理與正則表達式

PL/Perl 的強項在於 Perl 強大的字串處理能力:

-- 正則表達式提取 email
CREATE OR REPLACE FUNCTION extract_emails(text_content text)
RETURNS text[]
LANGUAGE plperl
AS $$
    my $content = $_[0];
    my @emails;

    while ($content =~ /([a-zA-Z0-9._%+-]+\@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})/g) {
        push @emails, $1;
    }

    return \@emails;
$$;

SELECT extract_emails('Contact info@example.com or support@company.org');

-- 字串轉 slug
CREATE OR REPLACE FUNCTION perl_slugify(input text)
RETURNS text
LANGUAGE plperl
AS $$
    my $str = lc($_[0]);
    $str =~ s/[^a-z0-9\s-]//g;
    $str =~ s/\s+/-/g;
    $str =~ s/-+/-/g;
    $str =~ s/^-|-$//g;
    return $str;
$$;

SELECT perl_slugify('Hello World! This is a Test.');
-- 輸出:hello-world-this-is-a-test

SPI 存取

CREATE OR REPLACE FUNCTION perl_find_user(uid integer)
RETURNS text
LANGUAGE plperl
AS $$
    my $plan = spi_prepare(
        'SELECT username FROM users WHERE id = $1',
        'INTEGER'
    );
    my $rv = spi_exec_prepared($plan, $_[0]);

    if ($rv->{processed} > 0) {
        return $rv->{rows}[0]->{username};
    }
    return undef;  # 回傳 SQL NULL
$$;

回傳集合

CREATE OR REPLACE FUNCTION perl_split_words(sentence text)
RETURNS SETOF text
LANGUAGE plperl
AS $$
    my @words = split(/\s+/, $_[0]);
    for my $word (@words) {
        return_next($word);
    }
    return;  # 必須以空 return 結束
$$;

SELECT * FROM perl_split_words('the quick brown fox');

PL/v8(JavaScript)

安裝

# Ubuntu
sudo apt install postgresql-16-pgv8

# 啟用
psql -c "CREATE EXTENSION plv8;"

基本語法

CREATE OR REPLACE FUNCTION js_hello(name text)
RETURNS text
LANGUAGE plv8
AS $$
    return `Hello, ${name}!`;
$$;

-- JavaScript 物件操作
CREATE OR REPLACE FUNCTION js_parse_config(config_str text)
RETURNS jsonb
LANGUAGE plv8
AS $$
    const config = JSON.parse(config_str);
    const defaults = { timeout: 30, retry: 3, debug: false };
    return Object.assign({}, defaults, config);
$$;

JSON 處理優勢

PL/v8 最突出的優勢在於 JSON/JSONB 的原生處理能力:

-- 複雜 JSON 扁平化
CREATE OR REPLACE FUNCTION js_flatten_json(nested jsonb, prefix text DEFAULT '')
RETURNS jsonb
LANGUAGE plv8
AS $$
    function flatten(obj, prefix, result) {
        result = result || {};
        for (let key in obj) {
            if (!obj.hasOwnProperty(key)) continue;
            const fullKey = prefix ? `${prefix}.${key}` : key;
            const val = obj[key];
            if (val !== null && typeof val === 'object' && !Array.isArray(val)) {
                flatten(val, fullKey, result);
            } else {
                result[fullKey] = val;
            }
        }
        return result;
    }
    return flatten(nested, prefix || '');
$$;

SELECT js_flatten_json(
    '{"user": {"name": "Alice", "addr": {"city": "Taipei"}}}'
);
-- 輸出:{"user.name": "Alice", "user.addr.city": "Taipei"}

SPI 存取

CREATE OR REPLACE FUNCTION js_aggregate_sales(product_id integer)
RETURNS jsonb
LANGUAGE plv8
AS $$
    const rows = plv8.execute(
        'SELECT order_date, quantity, unit_price FROM order_items WHERE product_id = $1',
        [product_id]
    );

    const summary = rows.reduce((acc, row) => {
        const month = row.order_date.toISOString().substring(0, 7);
        if (!acc[month]) acc[month] = { quantity: 0, revenue: 0 };
        acc[month].quantity += row.quantity;
        acc[month].revenue += row.quantity * row.unit_price;
        return acc;
    }, {});

    return summary;
$$;

PL/Tcl

PL/Tcl 是 PostgreSQL 最早支援的外部程序語言之一,通常已包含在 postgresql-contrib 中:

CREATE EXTENSION pltcl;

CREATE OR REPLACE FUNCTION tcl_hello(name text)
RETURNS text
LANGUAGE pltcl
AS $$
    return "Hello, $1!"
$$;

CREATE OR REPLACE FUNCTION tcl_word_count(sentence text)
RETURNS integer
LANGUAGE pltcl
AS $$
    set words [split $1]
    return [llength $words]
$$;

除非既有技術棧包含大量 Tcl 程式碼,否則不建議選擇 PL/Tcl 作為新專案的程序語言。

跨語言效能比較

語言啟動開銷執行效率SQL 互動開銷記憶體使用
C 函式極低最高最低
PL/pgSQL
PL/Perl中等中高中等中等
PL/v8中高中高中等中高
PL/Python中等中等

效能關鍵觀察

  • Python 啟動成本高:高頻小型函式呼叫場景影響顯著,用 GD 快取已初始化物件
  • SPI 呼叫有開銷:盡量使用 plpy.prepare() / spi_prepare() 預編譯
  • 型別轉換成本:大型 JSONB 或陣列傳遞時尤為明顯
  • 直譯器複用:同一 backend process 生命週期內共享,第一次呼叫最慢

語言選擇決策表

需求情境推薦語言理由
一般業務邏輯PL/pgSQL最佳效能、無額外依賴
資料科學、ML 推論PL/Pythonnumpy、scipy、sklearn 生態
複雜文字處理、正則PL/PerlPerl 正則最強大
JSON/JSONB 複雜轉換PL/v8JavaScript 原生 JSON
需要受信任環境PL/pgSQL / plperl / plv8無需 superuser
需要存取檔案/外部 APIplpython3u / plperluUntrusted 允許系統存取
統計分析PL/RR 語言統計生態
高頻效能關鍵路徑PL/pgSQL 或 C最低執行開銷

選擇流程

  1. 優先考慮 PL/pgSQL,覆蓋 80% 以上場景
  2. 若需特定語言函式庫或生態,再評估對應語言
  3. 安全性優先時選擇 Trusted 語言
  4. 效能瓶頸時考慮 C 語言函式

常見陷阱

陷阱說明解法
PL/Python GIL 限制不支援多執行緒,高並行場景可能瓶頸減少函式呼叫頻率
大型模組載入慢每次呼叫 import 開銷高GD 快取已載入模組
Perl undef 與 NULLundef 對應 SQL NULLdefined 檢查
PL/v8 記憶體管理GD 中大型物件佔用整個 Session只快取輕量查詢計畫
SECURITY DEFINER 風險以函式擁有者權限執行必須設定 search_path
版本相依性PG 升級後共享函式庫路徑可能改變定期檢查 pg_available_extensions

版本演進

PostgreSQL 版本重要變更
9.xplpython3u 正式取代 plpython2u
11Stored Procedure(CALL)支援事務控制
14plpython 對 Python 3.x 相容性全面改善
16函式 search_path 預設行為更嚴格
17PL/v8 社群持續維護以跟上 V8 引擎更新

總結

PostgreSQL 的多語言程序擴展讓你根據需求選擇最合適的語言:

  1. PL/pgSQL 是首選——效能最佳、無需額外安裝、完整 SQL 整合
  2. PL/Python 適合資料科學場景——但注意 Untrusted 等級與啟動開銷
  3. PL/Perl 在正則表達式和文字處理上無可匹敵
  4. PL/v8 是 JSON 處理的最佳選擇——且為 Trusted 語言
  5. Trusted vs Untrusted 決定了誰能建立函式以及能存取什麼資源

下一篇,我們將探討 PostgreSQL 的安裝與初始化——從原始碼編譯到套件管理器安裝、initdb 初始化、pg_hba.conf 設定到第一次連線的完整流程。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。