38.13. 用户定义类型#
如第 38.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命令的说明。
38.13.1. TOAST 注意事项#
如果数据类型的值大小(内部形式)各不相同,通常需要使数据类型可TOAST(请参阅第 73.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 存储格式最终只是一大堆字节;例如,它不能包含指针,因为它可能会被复制到内存中的其他位置。对于复杂的数据类型,扁平格式可能非常昂贵,因此PostgreSQL提供了一种将扁平格式“扩展”到更适合于计算的表示中的方法,然后在数据类型的函数之间以内存中格式传递该格式。
要使用扩展存储,数据类型必须定义一个扩展格式,该格式遵循src/include/utils/expandeddatum.h
中给出的规则,并提供将扁平 varlena 值“扩展”到扩展格式和将扩展格式“扁平化”回到常规 varlena 表示的函数。然后确保数据类型的所有 C 函数都可以接受任一表示,可能在收到后立即将一个表示转换为另一个表示。这不需要立即修复数据类型的所有现有函数,因为标准PG_DETOAST_DATUM
宏被定义为将扩展输入转换为常规扁平格式。因此,使用扁平 varlena 格式的现有函数将继续使用扩展输入工作,尽管效率稍低;它们不需要被转换,除非更好的性能很重要。
知道如何使用扩展表示的 C 函数通常分为两类:只能处理扩展格式的函数和可以处理扩展或扁平 varlena 输入的函数。前者更容易编写,但总体效率可能较低,因为将扁平输入转换为扩展形式以供单个函数使用可能比在扩展格式上操作节省的成本更多。当只需要处理扩展格式时,可以将扁平输入转换为扩展形式的转换隐藏在参数获取宏中,这样该函数看起来并不比使用传统 varlena 输入的函数更复杂。要处理这两种类型的输入,请编写一个参数获取函数,该函数将对外部、短头和压缩的 varlena 输入进行解压缩,但不会对扩展输入进行解压缩。可以将这样的函数定义为返回指向扁平 varlena 格式和扩展格式的并集的指针。调用者可以使用VARATT_IS_EXPANDED_HEADER()
宏来确定他们收到了哪种格式。
除了允许将常规 varlena 值与扩展值区分开来之外,TOAST基础设施还区分指向扩展值的“读写”和“只读”指针。只需检查扩展值或只会以安全且语义不可见的方式更改扩展值的 C 函数不必关心它们接收的是哪种类型的指针。生成输入值修改版本 C 函数如果接收到读写指针,则允许就地修改扩展输入值,但如果接收到只读指针,则不能修改输入;在这种情况下,它们必须先复制值,生成要修改的新值。构造新扩展值的 C 函数应始终返回指向它的读写指针。此外,就地修改读写扩展值的 C 函数应注意,如果它在中途失败,则将值保留在正常状态。
有关使用扩展值示例,请参阅标准数组基础设施,尤其是src/backend/utils/adt/array_expanded.c
。