如 第 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
类型。首先,我们将其声明为外壳类型
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可用的(请参阅第 65.2 节)。您应执行此操作,即使这些值总是太小而无法压缩或外部存储,因为TOAST也可以节省小数据的空间,方法是减少标头开销。
要支持TOAST存储,操作数据类型时需要使用 PG_DETOAST_DATUM
来仔细解包所有烤制值,这是 C 语言函数的规定。(此详情通常通过定义特定类型的 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()
宏确定它们收到了哪种格式。
该TOAST基础设施不仅允许区分常规 varlena 值和扩展值,还区分“读写”和“只读”到扩展值的指针。只需要检查扩展值或只会以安全和非语义上可见的方式更改它的 C 函数不必关心它们收到的指针类型。产生输入值修改版本 的 C 函数被允许在就地修改一个扩展输入值(如果它们收到一个读写指针),但是如果它们收到一个只读指针,不能修改输入;在那种情况下,它们首先必须复制该值,产生一个新的值进行修改。构造了一个新扩展值的 C 函数应该始终返回一个指向它的读写指针;另外,就地修改一个读写扩展值的 C 函数应注意,如果它中途失败,将该值保留在正常状态。
有关使用扩展值示例的信息,请参阅标准数组基础架构,尤其是 src/backend/utils/adt/array_expanded.c
。