如第 36.2 节中所述,PostgreSQL 可以扩展以支持新的数据类型。本节介绍如何定义新的基本类型,这些类型是在低于SQL语言级别的级别定义的。创建新的基本类型需要用低级语言(通常是 C)实现操作该类型的函数。
本节中的示例可以在源代码分发版的 src/tutorial
目录下的 complex.sql
和 complex.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 命令的描述。
如果您的数据类型的值大小(内部形式)不同,通常最好使该数据类型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_EXHDR
和 VARDATA_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
。
如果您在文档中看到任何不正确之处、与您对特定功能的体验不符之处或需要进一步澄清之处,请使用此表单报告文档问题。