2025年9月25日: PostgreSQL 18 釋出!
支援的版本: 當前 (18) / 17 / 16 / 15 / 14 / 13
開發版本: 開發
不再支援的版本: 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 / 7.1

36.13. 使用者定義型別 #

第 36.2 節中所述,PostgreSQL 可以擴充套件以支援新的資料型別。本節介紹如何定義新的基本型別,這些型別是在低於SQL語言級別的級別定義的。建立新的基本型別需要用低階語言(通常是 C)實現操作該型別的函式。

本節中的示例可以在原始碼分發版的 src/tutorial 目錄下的 complex.sqlcomplex.c 檔案中找到。有關執行示例的說明,請參見該目錄下的 README 檔案。

使用者定義型別必須始終具有輸入和輸出函式。這些函式決定了型別在字串中(供使用者輸入和輸出給使用者)的顯示方式以及型別在記憶體中的組織方式。輸入函式接收一個以 null 結尾的字元字串作為引數,並返回型別的內部(記憶體中)表示。輸出函式接收型別的內部表示作為引數,並返回一個以 null 結尾的字元字串。如果我們希望對型別進行除儲存之外的任何操作,就必須提供額外的函式來執行我們希望為該型別提供的任何操作。

假設我們要定義一個表示複數的 complex 型別。在記憶體中表示覆數的一種自然方式是使用以下 C 結構:

typedef struct Complex {
    double      x;
    double      y;
} Complex;

我們將需要使其成為一個透過引用傳遞的型別,因為它太大,無法放入單個 Datum 值中。

作為型別的外部字串表示,我們選擇 (x,y) 形式的字串。

輸入和輸出函式通常不難編寫,特別是輸出函式。但在定義型別的外部字串表示時,請記住您最終必須編寫一個完整且健壯的解析器來處理該表示形式作為您的輸入函式。例如:

PG_FUNCTION_INFO_V1(complex_in);

Datum
complex_in(PG_FUNCTION_ARGS)
{
    char       *str = PG_GETARG_CSTRING(0);
    double      x,
                y;
    Complex    *result;

    if (sscanf(str, " ( %lf , %lf )", &x, &y) != 2)
        ereport(ERROR,
                (errcode(ERRCODE_INVALID_TEXT_REPRESENTATION),
                 errmsg("invalid input syntax for type %s: \"%s\"",
                        "complex", str)));

    result = (Complex *) palloc(sizeof(Complex));
    result->x = x;
    result->y = y;
    PG_RETURN_POINTER(result);
}

輸出函式可以簡單地是:

PG_FUNCTION_INFO_V1(complex_out);

Datum
complex_out(PG_FUNCTION_ARGS)
{
    Complex    *complex = (Complex *) PG_GETARG_POINTER(0);
    char       *result;

    result = psprintf("(%g,%g)", complex->x, complex->y);
    PG_RETURN_CSTRING(result);
}

您應該注意確保輸入和輸出函式互為逆運算。如果不是這樣,當您需要將資料轉儲到檔案然後讀回時,您將遇到嚴重的問題。當涉及浮點數時,這是尤其常見的問題。

可選地,使用者定義型別可以提供二進位制輸入和輸出例程。二進位制 I/O 通常比文字 I/O 更快,但可移植性較差。與文字 I/O 一樣,定義外部二進位制表示的完全取決於您。大多數內建資料型別都嘗試提供與機器無關的二進位制表示。對於 complex,我們將利用 float8 型別的二進位制 I/O 轉換器。

PG_FUNCTION_INFO_V1(complex_recv);

Datum
complex_recv(PG_FUNCTION_ARGS)
{
    StringInfo  buf = (StringInfo) PG_GETARG_POINTER(0);
    Complex    *result;

    result = (Complex *) palloc(sizeof(Complex));
    result->x = pq_getmsgfloat8(buf);
    result->y = pq_getmsgfloat8(buf);
    PG_RETURN_POINTER(result);
}

PG_FUNCTION_INFO_V1(complex_send);

Datum
complex_send(PG_FUNCTION_ARGS)
{
    Complex    *complex = (Complex *) PG_GETARG_POINTER(0);
    StringInfoData buf;

    pq_begintypsend(&buf);
    pq_sendfloat8(&buf, complex->x);
    pq_sendfloat8(&buf, complex->y);
    PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
}

一旦我們編寫了 I/O 函式並將其編譯為共享庫,我們就可以在 SQL 中定義 complex 型別。首先,我們將其宣告為一個空殼型別:

CREATE TYPE complex;

這充當了一個佔位符,允許我們在定義其 I/O 函式時引用該型別。現在我們可以定義 I/O 函數了:

CREATE FUNCTION complex_in(cstring)
    RETURNS complex
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_out(complex)
    RETURNS cstring
    AS 'filename'
    LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_recv(internal)
   RETURNS complex
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

CREATE FUNCTION complex_send(complex)
   RETURNS bytea
   AS 'filename'
   LANGUAGE C IMMUTABLE STRICT;

最後,我們可以提供資料型別的完整定義:

CREATE TYPE complex (
   internallength = 16,
   input = complex_in,
   output = complex_out,
   receive = complex_recv,
   send = complex_send,
   alignment = double
);

當您定義一個新的基本型別時,PostgreSQL 會自動為該型別的陣列提供支援。陣列型別通常與基本型別同名,並在前面加上下劃線字元(_)。

一旦資料型別存在,我們就可以宣告額外的函式來提供對資料型別的有用操作。然後可以在函式之上定義運算子,如果需要,還可以建立運算子類來支援資料型別的索引。這些附加層將在後續章節中討論。

如果資料型別的內部表示為可變長度,則其內部表示必須遵循可變長度資料的標準佈局:前四個位元組必須是一個 char[4] 欄位,該欄位永遠不會被直接訪問(通常命名為 vl_len_)。您必須使用 SET_VARSIZE() 宏將資料總大小(包括長度欄位本身)儲存在此欄位中,並使用 VARSIZE() 來檢索它。(這些宏存在是因為長度欄位可能根據平臺進行編碼。)

有關更多詳細資訊,請參閱 CREATE TYPE 命令的描述。

36.13.1. TOAST 考量 #

如果您的資料型別的值大小(內部形式)不同,通常最好使該資料型別TOAST-可(參見第 66.2 節)。即使值總是太小而無法壓縮或外部儲存,您也應該這樣做,因為TOAST也可以透過減少頭開銷來節省小資料空間。

為了支援TOAST儲存,操作資料型別的 C 函式必須始終透過使用 PG_DETOAST_DATUM 來小心地解壓任何被 toasted 的值。(這一細節通常透過定義特定型別的 GETARG_DATATYPE_P 宏來隱藏。)然後,在執行 CREATE TYPE 命令時,將內部長度指定為 variable,並選擇除 plain 之外的適當儲存選項。

如果資料對齊不重要(僅對特定函式而言,或者因為資料型別本身指定了位元組對齊),那麼就可以避免 PG_DETOAST_DATUM 的一些開銷。您可以使用 PG_DETOAST_DATUM_PACKED 代替(通常透過定義 GETARG_DATATYPE_PP 宏來隱藏),並使用 VARSIZE_ANY_EXHDRVARDATA_ANY 宏來訪問可能被 packed 的資料。同樣,這些宏返回的資料即使資料型別定義指定了對齊,也不會對齊。如果對齊很重要,您必須透過常規的 PG_DETOAST_DATUM 介面。

注意

較舊的程式碼經常將 vl_len_ 宣告為 int32 欄位而不是 char[4]。只要結構定義具有至少 int32 對齊的其他欄位,這就沒問題。但是,在使用可能未對齊的資料時使用此類結構定義是危險的;編譯器可能會將其視為允許,從而假設資料實際上是對齊的,這可能導致在對對齊有嚴格要求的體系結構上出現核心轉儲。

TOAST支援啟用的另一項功能是,可以有一個展開的記憶體中資料表示,它比儲存在磁碟上的格式更方便處理。常規或扁平 varlena 儲存格式最終只是一塊位元組;它不能包含指標,因為它可能會被複制到記憶體中的其他位置。對於複雜的資料型別,扁平格式在處理時可能會非常昂貴,因此 PostgreSQL 提供了一種展開扁平格式的方法,將其轉換為更適合計算的表示,然後在此資料型別函式之間傳遞該格式的記憶體表示。

要使用展開儲存,資料型別必須定義一個遵循 src/include/utils/expandeddatum.h 中規則的展開格式,並提供函式來將扁平 varlena 值展開到展開格式,並將展開格式展平回常規 varlena 表示。然後,確保資料型別的所有 C 函式都能接受這兩種表示,可能透過在接收時立即將一種轉換為另一種。這不需要一次性修改所有現有函式,因為標準的 PG_DETOAST_DATUM 宏被定義為將展開的輸入轉換為常規的扁平格式。因此,處理扁平 varlena 格式的現有函式將繼續處理展開的輸入,儘管效率稍低;只有在需要更好的效能時才需要轉換它們。

知道如何處理展開表示的 C 函式通常分為兩類:只能處理展開格式的函式,以及可以處理展開格式或扁平 varlena 輸入的函式。前者更容易編寫,但整體效率可能較低,因為將扁平輸入轉換為展開格式供單個函式使用可能比操作展開格式節省的成本更高。當只需要處理展開格式時,可以將扁平輸入的轉換隱藏在引數獲取宏中,這樣函式就不會比處理傳統 varlena 輸入的函式複雜。為了處理這兩種型別的輸入,請編寫一個引數獲取函式,它將解壓外部、短頭部和壓縮的 varlena 輸入,但不會解壓展開的輸入。此類函式可以定義為返回一個指向扁平 varlena 格式和展開格式的聯合體的指標。呼叫者可以使用 VARATT_IS_EXPANDED_HEADER() 宏來確定他們收到的格式。

TOAST基礎結構不僅允許區分常規 varlena 值和展開值,還區分展開值的讀寫只讀指標。只需要檢查展開值或僅以安全且不影響語義的方式更改展開值的 C 函式,不必關心它們接收到的是哪種型別的指標。允許修改輸入值的 C 函式如果接收到讀寫指標,則可以就地修改展開的輸入值,但如果接收到只讀指標,則不得修改輸入;在這種情況下,它們必須首先複製該值,生成一個新值進行修改。已構造新展開值的 C 函式應始終返回指向它的讀寫指標。同樣,就地修改讀寫展開值的 C 函式應注意在部分失敗時將該值保持在合理的狀態。

有關處理展開值的示例,請參見標準陣列基礎結構,特別是 src/backend/utils/adt/array_expanded.c

提交更正

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