支持的版本: 当前 (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文件。

用户自定义类型必须始终具有输入和输出函数。这些函数决定了该类型在字符串中的显示方式(供用户输入和输出给用户)以及该类型在内存中的组织方式。输入函数接受一个以 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类型。首先,我们将其声明为 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来解包传递给它们的任何被 TOAST 的值。(此细节通常通过定义特定于类型的GETARG_DATATYPE_P宏来隐藏。)然后,在运行CREATE TYPE命令时,将内部长度指定为variable,并选择除plain之外的某些合适的存储选项。

如果数据对齐并不重要(无论是仅针对特定函数,还是因为数据类型无论如何都指定了字节对齐),则可以避免PG_DETOAST_DATUM的一些开销。您可以改用PG_DETOAST_DATUM_PACKED(通常通过定义GETARG_DATATYPE_PP宏来隐藏),并使用宏VARSIZE_ANY_EXHDRVARDATA_ANY来访问可能打包的数据。同样,这些宏返回的数据即使数据类型定义指定了对齐,也未对齐。如果对齐很重要,则必须通过常规的PG_DETOAST_DATUM接口进行操作。

注意

较旧的代码通常将vl_len_声明为int32字段,而不是char[4]。只要结构定义具有至少int32对齐的其他字段,这就可以。但是,在处理可能未对齐的数据时使用这样的结构定义是危险的;编译器可能会将其视为许可证,假设数据实际上是对齐的,从而导致在对对齐要求严格的架构上出现核心转储。

通过TOAST支持启用的另一个功能是可能具有展开的内存中数据表示形式,该形式比磁盘上存储的格式更易于使用。常规或扁平的 varlena 存储格式最终只是一个字节 blob;例如,它不能包含指针,因为它可能会被复制到内存中的其他位置。对于复杂的数据类型,使用扁平格式可能非常昂贵,因此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

提交更正

如果您发现文档中的任何内容不正确,与您使用特定功能的体验不符或需要进一步澄清,请使用此表单报告文档问题。