如第36.2节所述,PostgreSQL 可以扩展以支持新的数据类型。本节介绍如何定义新的基本类型,即在语言级别以下定义的数据类型。SQL创建新的基本类型需要实现函数以使用低级语言(通常为 C)对类型进行操作。
本节中的示例可以在源代码分发版的 src/tutorial
目录中的 complex.sql
和 complex.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
,我们将借用类型 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 命令的描述。
如果您的数据类型的值在大小(在内部形式中)上有所不同,则通常希望使数据类型TOAST-able(请参阅第65.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_EXHDR
和 VARDATA_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
。
如果您在文档中看到任何不正确的内容,与您对特定功能的体验不符,或者需要进一步说明,请使用此表单报告文档问题。