9.3 9.4 9.5 9.6 10 11 12 13 14 Current(15)
阿里云PostgreSQL 问题报告 纠错本页面

35.11. 用户定义的类型

第 35.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 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);
}

你应当让输入和输出函数互为彼此的逆函数。如果不这样做,当你需要把数据转储到一个文件并且以后将它重新读入时会遇到很严重的问题。在涉及到浮点数时这是一个特别常见的问题。

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

35.11.1. TOAST 注意事项

如果你的数据类型值的尺寸(内部形式)是可变的, 通常希望让该数据类型是可TOAST的(见第 63.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_EXHDR以及VARDATA_ANY来访问一个可能包装过的数据。此外,即使数据类型定义指定了一种对齐方式,这些宏返回的数据也不是对齐过的。如果对齐对你很重要,你必须使用常规的PG_DETOAST_DATUM接口。

注意: 老的代码经常声明vl_len_为一个int32域而不是char[4]。只要结构定义含有其他具有至少int32对齐的域,这就是 OK 的。但是在使用可能未对齐的数据时,使用这样一种结构定义就是危险的,编译器可能会把它当作一个授权来假定数据实际上已经被对齐,在对于对齐很严格的架构上会导致核心转储。

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

要使用扩展存储,数据类型必须定义遵循 src/include/utils/expandeddatum.h 中给出的规则的扩展格式, 并提供" 扩展"一个flat varlena值为扩展格式,和"平面化"扩展格式回到常规varlena表示的函数。 然后确保该数据类型的所有C函数可以接受任一表示,可能通过在接收时立即将其转换为另一个。 这不需要立即修复数据类型的所有现有函数,因为定义了标准 PG_DETOAST_DATUM 宏,可以将扩展输入转换为常规平面格式。 因此,使用平面varlena格式的现有函数将继续工作,尽管效率稍低,直到需要更好的性能,使用扩展输入; 它们不需要被转换

知道如何使用扩展表示的C函数通常分为两类:只能处理扩展格式的函数,以及可以处理扩展或平面varlena输入的函数。 前者更容易书写,但整体效率可能较低,因为将平面输入转换为扩展形式以供单个功能使用可能比通过在扩展格式上操作节省的成本更高。 当只需要处理扩展格式时,平面输入到扩展形式的转换可以隐藏在参数获取宏中,以使该函数不会比使用传统varlena输入的函数复杂。 要处理这两种类型的输入,请编写一个参数获取函数,该函数将detoast外部,short-header和压缩的varlena输入,但不扩展输入。这样的函数可以被定义为返回指向平面varlena格式和扩展格式的联合的指针。 调用者可以使用 VARATT_IS_EXPANDED_HEADER()宏来确定它们接收的格式。

TOAST 基础设施不仅允许将常规varlena 值与扩展值区分开,也能区分指向扩展值的"读写""只读"指针。 C函数只需要检查扩展的值,或者将只以安全和非语义可见的方式更改它们,不需要关心它们接收哪种类型的指针。 如果产生输入值的修改版本的C函数接收到读写指针,则允许就地修改扩展输入值,但是如果它们接收到只读指针,则不能修改输入; 在这种情况下,他们必须首先复制该值,产生一个新的值来修改。 已经构造了新的扩展值的C函数应该总是返回一个读写指针。 此外,正在修改读写扩展值的C函数应该注意,如果该值在中途失败,则应该将该值保持在正常状态。

有关使用扩展值的示例,请参见标准数组基础设施,特别是 src/backend/utils/adt/array_expanded.c.

<
/BODY >