支援的版本:目前 (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 / 7.1

36.13. 使用者定義型別 #

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

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

使用者定義型別必須始終具有輸入和輸出函數。 這些函數決定了型別在字串中的顯示方式(用於使用者輸入和輸出給使用者)以及型別在記憶體中的組織方式。 輸入函數採用一個以空字元結尾的字串作為其引數,並傳回型別的內部(在記憶體中)表示形式。 輸出函數採用型別的內部表示形式作為引數,並傳回一個以空字元結尾的字串。 如果我們想要對該型別執行任何操作,而不僅僅是儲存它,我們必須提供額外的函數來實作我們想要用於該型別的任何操作。

假設我們要定義一個代表複數的 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,我們將 piggy-back 在 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 型別。 首先,我們將其宣告為 shell 型別

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-able (參見第 65.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 來存取可能壓縮的 datum。同樣,即使資料類型定義指定了對齊,這些巨集傳回的資料也是未對齊的。如果對齊很重要,您必須使用常規的 PG_DETOAST_DATUM 介面。

注意

較舊的程式碼通常將 vl_len_ 宣告為 int32 欄位,而不是 char[4]。只要結構定義具有其他至少具有 int32 對齊的欄位,這就沒有問題。但在處理可能未對齊的 datum 時使用這樣的結構定義是很危險的;編譯器可能會將其視為授權,假設 datum 實際上已對齊,從而在對對齊要求嚴格的架構上導致核心轉儲。

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

要使用展開的儲存,資料類型必須定義遵循 src/include/utils/expandeddatum.h 中給出的規則的展開格式,並提供函數來將扁平的 varlena 值展開為展開格式,以及將展開的格式扁平化回常規的 varlena 表示形式。 然後確保資料類型的所有 C 函數都可以接受任一表示形式,可能是在收到後立即將一個轉換為另一個。 這不需要一次性修復資料類型的所有現有函數,因為標準的 PG_DETOAST_DATUM 巨集被定義為將展開的輸入轉換為常規的扁平格式。 因此,使用扁平 varlena 格式的現有函數將繼續與展開的輸入一起工作,儘管效率略低; 除非更好的效能很重要,否則它們不需要轉換。

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

TOAST基礎架構不僅允許將常規 varlena 值與展開的值區分開來,還可以區分指向展開值的讀寫唯讀指標。 只需要檢查展開值,或僅以安全且在語義上不可見的方式更改它的 C 函數,不需要關心它們收到哪種類型的指標。 產生輸入值的修改版本的 C 函數如果收到讀寫指標,則可以就地修改展開的輸入值,但如果收到唯讀指標,則不得修改輸入; 在這種情況下,他們必須先複製該值,產生一個新值來修改。 構造了一個新的展開值的 C 函數應始終傳回指向它的讀寫指標。 此外,就地修改讀寫展開值的 C 函數應注意在部分失敗的情況下將該值保持在健全狀態。

有關使用展開值的範例,請參閱標準陣列基礎架構,特別是 src/backend/utils/adt/array_expanded.c

提交更正

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