37.11. 用户定义的类型

Section 37.2中所述,可以扩展 PostgreSQL 以支持新的数据类型。本节介绍如何定义新的基本类型,这些基本类型是在 SQL 语言级别之下定义的数据类型。创建新的基本类型需要实现一些功能,以使用低级语言(通常为 C)对该类型进行操作。

本节中的示例可以在源发行版的src/tutorial目录的complex.sqlcomplex.c中找到。有关运行示例的说明,请参见该目录中的README文件。

用户定义的类型必须始终具有 Importing 和输出功能。这些函数确定类型如何以字符串形式出现(供用户 Importing 并输出给用户),以及如何在内存中组织类型。Importing 函数以一个以零结尾的字符串作为参数,并返回该类型的内部(在内存中)表示形式。输出函数将类型的内部表示形式作为参数,并返回以 null 终止的字符串。如果我们想对类型做更多的事情而不仅仅是存储类型,我们必须提供其他功能来实现我们想要对类型进行的任何操作。

假设我们要定义表示复数的complex类型。下面的 C 结构是在内存中表示复数的自然方式:

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

我们将需要使其成为按引用传递类型,因为它太大而无法容纳单个Datum值。

作为类型的外部字符串表示形式,我们选择形式为(x,y)的字符串。

Importing 和输出功能通常不难编写,尤其是输出功能。但是,在定义类型的外部字符串表示形式时,请记住,您最终必须为该表示形式编写一个完整且健壮的解析器作为 Importing 函数。例如:

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 complex: \"%s\"",
                        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);
}

您应该小心使 Importing 和输出函数彼此相反。如果不这样做,则需要将数据转储到文件中然后再读回时会遇到严重的问题。当涉及浮点数时,这是一个特别常见的问题。

可选地,用户定义的类型可以提供二进制 Importing 和输出例程。二进制 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命令的描述。

37 .11.1. 吐司注意事项

如果数据类型的值的大小(内部形式)有所不同,通常需要使数据类型为 TOAST-able(请参见Section 66.2)。即使值始终太小而不能压缩或存储在外部,也应该这样做,因为 TOAST 可以通过减少头开销来节省小数据的空间。

为了支持 TOAST 存储,在数据类型上运行的 C 函数必须始终小心解压使用PG_DETOAST_DATUM传递的所有烘烤值。 (通常,通过定义特定于类型的GETARG_DATATYPE_P宏来隐藏此详细信息.)然后,在运行CREATE TYPE命令时,将内部长度指定为variable并选择plain以外的一些适当的存储选项。

如果数据对齐方式不重要(或者仅针对特定功能,或者因为数据类型始终指定字节对齐方式),则可以避免PG_DETOAST_DATUM的一些开销。您可以改用PG_DETOAST_DATUM_PACKED(通常通过定义GETARG_DATATYPE_PP宏将其隐藏),并使用宏VARSIZE_ANY_EXHDRVARDATA_ANY来访问可能打包的基准。同样,即使数据类型定义指定对齐方式,这些宏返回的数据也不会对齐。如果对齐方式很重要,则必须通过常规的PG_DETOAST_DATUM接口。

Note

较旧的代码经常将vl_len_声明为int32字段,而不是char[4]。只要 struct 定义中的其他字段至少具有int32对齐,就可以了。但是在处理可能未对齐的基准时使用这样的结构定义是危险的。编译器可能会以此为准,假设数据实际上已对齐,从而导致对对齐要求严格的体系结构上发生核心转储。

TOAST 支持所支持的另一项功能是具有扩展的内存中数据表示的可能性,它比存储在磁盘上的格式更易于使用。常规的或“固定的” varlena 存储格式最终只是字节的一滴;例如,它不能包含指针,因为它可能会被复制到内存中的其他位置。对于复杂的数据类型,使用平面格式可能会非常昂贵,因此 PostgreSQL 提供了一种将平面格式“扩展”为更适合计算的表示形式,然后在内存的函数之间传递该格式的方式。数据类型。

要使用扩展存储,数据类型必须定义一个遵循src/include/utils/expandeddatum.h中给出的规则的扩展格式,并提供函数以将平面 varlena 值“扩展”为扩展格式,并将该扩展格式“扩展”为常规 varlena 表示形式。然后,确保该数据类型的所有 C 函数都可以接受任何一种表示形式,可能是在收到后立即将其转换为另一种表示形式。这不需要立即修复该数据类型的所有现有功能,因为定义了标准PG_DETOAST_DATUM宏可将扩展的 Importing 转换为常规的平面格式。因此,使用平面 varlena 格式的现有功能将在扩展 Importing 的情况下 continue 工作,尽管效率略低。除非更好的性能很重要,否则不需要转换它们。

知道如何使用扩展表示形式的 C 函数通常分为两类:只能处理扩展格式的函数和可以处理扩展或平面 varlenaImporting 的函数。前者更容易编写,但整体效率可能较低,因为将平面 Importing 转换为扩展格式以供单个功能使用可能比通过扩展格式进行操作所节省的成本更高。当仅需要处理扩展格式时,可以将平面 Importing 转换为扩展形式隐藏在参数获取宏中,从而使该函数看起来比使用传统 varlenaImporting 的函数更复杂。要处理两种类型的 Importing,请编写一个参数获取函数,该函数将取消外部,短标题和压缩的 varlenaImporting 的作用,但不扩展 Importing 的作用。可以将此类函数定义为返回指向平面 varlena 格式和扩展格式的并集的指针。呼叫者可以使用VARATT_IS_EXPANDED_HEADER()宏来确定他们接收的格式。

TOAST 基础结构不仅允许将常规 varlena 值与扩展值区分开,而且还可以区分指向扩展值的“读写”和“只读”指针。仅需要检查扩展值或仅以安全且非语义上可见的方式更改其扩展值的 C 函数无需关心它们接收的指针类型。产生 Importing 值修改版本的 C 函数如果接收到读写指针,则可以就地修改扩展的 Importing 值,但如果接收到只读指针,则不得修改 Importing。在这种情况下,他们必须先复制该值,然后产生一个新值进行修改。构造了新的扩展值的 C 函数应始终返回对其的读写指针。同样,正在修改读写扩展值的 C 函数应该小心,如果在途中失败,则将其保持为正常状态。

有关使用扩展值的示例,请参见标准数组基础结构,尤其是src/backend/utils/adt/array_expanded.c