↑↓ 选择 ↵ 打开 ⌫ 改范围 完整检索页

pgsql.cc 提供对 postgresql.org 官网内容的中文翻译,由 Pigsty 团队维护。

受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
测试与开发版本: 19 / devel
不受支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3 / 8.2 / 8.1 / 8.0 / 7.4 / 7.3 / 7.2 / 7.1
历史版本PostgreSQL 7.1 已于 2006 年 4 月结束社区维护,本页译文保留供仍在使用旧版本的读者参考。新系统请看当前版本。

13.4. 编译型(C)语言函数 #

用 C 编写的函数可以编译成动态可装载对象(也称共享库),用于实现用户自定义的 SQL 函数。在一个后端会话中,某个可装载对象文件里的用户自定义函数第一次被调用时,动态装载器会把该对象文件装载到内存中,以便能够调用该函数。因此,用户自定义函数的 CREATE FUNCTION 必须为函数指定两项信息:可装载对象文件的名称,以及该对象文件内要调用的特定函数的 C 名称(链接符号)。如果没有显式指定 C 名称,则假定它与 SQL 函数名相同。

注意

动态装载的用户函数在第一次使用后会保留在内存中,同一会话中以后对该函数的调用只会产生符号表查找这一很小的开销。

指定对象文件的字符串(AS 子句中的第一个字符串)应是该函数目标代码文件的完整路径,用单引号括起。如果 AS 子句中给出了链接符号,该链接符号也应用单引号括起,并且应与 C 源代码中函数的名称完全相同。在 Unix 系统上,命令 nm 会打印动态可装载对象中的所有链接符号。

注意

Postgres 不会自动编译函数;它必须在被 CREATE FUNCTION 命令使用之前编译好。更多信息见下文。

目前 C 函数使用两种不同的调用约定。较新的“版本 1”调用约定通过为函数写一个 PG_FUNCTION_INFO_V1() 宏调用来表明,如下所示。没有这样的宏则表示是旧风格(“版本 0”)函数。CREATE FUNCTION 中指定的语言名 在两种情况下都是 'C'。旧风格函数由于可移植性问题和功能欠缺现已弃用,但出于兼容性原因仍受支持。

13.4.1. C 语言函数中的基础类型

下表给出了将要装载到 Postgres 中的 C 函数的参数所需的 C 类型。“定义于”列给出(.../src/backend/ 目录中)实际定义等价 C 类型的头文件。注意,你应该始终先包含 postgres.h,而它又包含 c.h。

表 13.1. 内置 Postgres 类型的等价 C 类型

内置类型 C 类型 定义于
abstime AbsoluteTime utils/nabstime.h
bool bool include/c.h
box (BOX *) utils/geo-decls.h
bytea (bytea *) include/postgres.h
"char" char N/A
cid CID include/postgres.h
datetime (DateTime *) include/c.h 或 include/postgres.h
int2 int2 或 int16 include/postgres.h
int2vector (int2vector *) include/postgres.h
int4 int4 或 int32 include/postgres.h
float4 (float4 *) include/c.h 或 include/postgres.h
float8 (float8 *) include/c.h 或 include/postgres.h
lseg (LSEG *) include/geo-decls.h
name (Name) include/postgres.h
oid oid include/postgres.h
oidvector (oidvector *) include/postgres.h
path (PATH *) utils/geo-decls.h
point (POINT *) utils/geo-decls.h
regproc regproc 或 REGPROC include/postgres.h
reltime RelativeTime utils/nabstime.h
text (text *) include/postgres.h
tid ItemPointer storage/itemptr.h
timespan (TimeSpan *) include/c.h 或 include/postgres.h
tinterval TimeInterval utils/nabstime.h
xid (XID *) include/postgres.h


在内部,Postgres 把基本类型视为一个“内存块”。你针对某个类型定义的用户自定义函数转而定义了 Postgres 能对它进行操作的方式。也就是说,Postgres 只负责在磁盘上存储和检索数据,而使用你的用户自定义函数来输入、处理和输出数据。基本类型可以有以下三种内部格式之一:

  • 传值,定长

  • 传引用,定长

  • 传引用,变长

传值类型的长度只能是 1、2 或 4 字节(如果你的机器上 sizeof(Datum) 为 8,也可以是 8 字节)。你应当小心地定义类型,使其在所有体系结构上具有相同的大小(以字节计)。例如,long 类型是危险的,因为它在某些机器上是 4 字节而在另一些机器上是 8 字节;而 int 类型在大多数 Unix 机器上是 4 字节(但在大多数个人电脑上不是)。Unix 机器上 int4 类型的一个合理实现可以是:

/* 4-byte integer, passed by value */
typedef int int4;

另一方面,任何大小的定长类型都可以按传引用方式传递。例如,下面是一个 Postgres 类型的示例实现:

/* 16-byte structure, passed by reference */
typedef struct
{
    double  x, y;
} Point;

在 Postgres 函数中传入和传出这类类型时只能使用指向它们的指针。要返回这种类型的值,用 palloc() 分配适量内存,填充所分配的内存,并返回指向它的指针。(或者,你也可以通过返回同类型输入值的指针来返回该输入值。但绝不 要修改传引用输入值的内容。)

最后,所有变长类型也必须以传引用方式传递。所有变长类型都必须以一个恰好 4 字节的长度字段开始,而该类型中要存储的所有数据都位于紧随该长度字段之后的内存中。长度字段是结构的总长度(即它包含长度字段本身的大小)。我们可以这样定义 text 类型:

typedef struct {
    int4 length;
    char data[1];
} text;

显然,这里所示的 data 字段不足以容纳所有可能的字符串;在 C 中无法声明这样的结构。操作变长类型时,我们必须小心分配正确数量的内存并初始化长度字段。例如,如果我们想在 text 结构中存储 40 字节,可以使用如下代码片段:

#include "postgres.h"
...
char buffer[40]; /* our source data */
...
text *destination = (text *) palloc(VARHDRSZ + 40);
destination->length = VARHDRSZ + 40;
memmove(destination->data, buffer, 40);
...

了解了基础类型所有可能的结构后,就可以看一些实际函数示例。

13.4.2. C 语言函数的版本 0 调用约定

我们先介绍“旧风格”调用约定——虽然此方法现已弃用,但初学者更容易上手。在版本 0 方法中,C 函数的参数和结果就按普通 C 风格声明,只是要小心使用上所示的每种 SQL 数据类型的 C 表示。

下面是一些示例:

#include "postgres.h"
#include <string.h>

/* By Value */
         
int
add_one(int arg)
{
    return arg + 1;
}

/* By Reference, Fixed Length */

float8 *
add_one_float8(float8 *arg)
{
    float8    *result = (float8 *) palloc(sizeof(float8));

    *result = *arg + 1.0;
       
    return result;
}

Point *
makepoint(Point *pointx, Point *pointy)
{
    Point     *new_point = (Point *) palloc(sizeof(Point));

    new_point->x = pointx->x;
    new_point->y = pointy->y;
       
    return new_point;
}

/* By Reference, Variable Length */

text *
copytext(text *t)
{
    /*
     * VARSIZE is the total size of the struct in bytes.
     */
    text *new_t = (text *) palloc(VARSIZE(t));
    VARATT_SIZEP(new_t) = VARSIZE(t);
    /*
     * VARDATA is a pointer to the data region of the struct.
     */
    memcpy((void *) VARDATA(new_t), /* destination */
           (void *) VARDATA(t),     /* source */
           VARSIZE(t)-VARHDRSZ);    /* how many bytes */
    return new_t;
}

text *
concat_text(text *arg1, text *arg2)
{
    int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ;
    text *new_text = (text *) palloc(new_text_size);

    VARATT_SIZEP(new_text) = new_text_size;
    memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ);
    memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ),
           VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ);
    return new_text;
}

假设上面的代码已在文件 funcs.c 中准备好并编译成了共享对象,我们就可以用如下命令向 Postgres 定义这些函数:

CREATE FUNCTION add_one(int4) RETURNS int4
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);

-- note overloading of SQL function name add_one()
CREATE FUNCTION add_one(float8) RETURNS float8
     AS 'PGROOT/tutorial/funcs.so',
        'add_one_float8'
     LANGUAGE 'c' WITH (isStrict);

CREATE FUNCTION makepoint(point, point) RETURNS point
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);
                         
CREATE FUNCTION copytext(text) RETURNS text
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);

CREATE FUNCTION concat_text(text, text) RETURNS text
     AS 'PGROOT/tutorial/funcs.so' LANGUAGE 'c'
     WITH (isStrict);

这里 PGROOT 表示 Postgres 源码树的全路径。注意,视你的系统而定,共享对象的文件名可能不以 .so 结尾,而是 .sl 或其他扩展名;请相应调整。

注意我们把这些函数指定为"严格"的,意思是如果任何输入值为 NULL,系统就应自动假定结果为 NULL。这样做就避免了在函数代码中检查 NULL 输入。不这样做的话,我们就得显式检查 NULL,例如为每个传引用参数检查空指针。(对传值参数,我们甚至没有办法检查!)

虽然这个调用约定使用简单,但可移植性不好;在某些体系结构上,以这种方式传递小于 int 的数据类型存在问题。此外,也没有返回 NULL 结果的简单方法,除了把函数声明为严格之外也无法以任何方式处理 NULL 参数。接下来介绍的版本 1 约定克服了这些缺点。

13.4.3. C 语言函数的版本 1 调用约定

版本-1 的调用约定依赖于宏来屏蔽传递参数和结果的大部分复杂性。版本-1 函数的 C 声明总是:

Datum funcname(PG_FUNCTION_ARGS)

此外,宏调用:

PG_FUNCTION_INFO_V1(funcname);

必须出现在同一个源文件中(按惯例写在函数本身之前)。对 "internal" 语言的函数不需要此宏调用,因为 Postgres 目前假定所有内部函数都是版本 1。但对动态装载的函数它是必需的。

在版本 1 函数中,每个实际参数用与该参数数据类型对应的 PG_GETARG_xxx() 宏取得,结果用与返回类型对应的 PG_RETURN_xxx() 宏返回。

下面我们展示与上面相同的函数,以版本 1 风格编码:

#include "postgres.h"
#include <string.h>
#include "fmgr.h"

/* By Value */

PG_FUNCTION_INFO_V1(add_one);
         
Datum
add_one(PG_FUNCTION_ARGS)
{
    int32   arg = PG_GETARG_INT32(0);

    PG_RETURN_INT32(arg + 1);
}

/* By Reference, Fixed Length */

PG_FUNCTION_INFO_V1(add_one_float8);

Datum
add_one_float8(PG_FUNCTION_ARGS)
{
    /* The macros for FLOAT8 hide its pass-by-reference nature */
    float8   arg = PG_GETARG_FLOAT8(0);

    PG_RETURN_FLOAT8(arg + 1.0);
}

PG_FUNCTION_INFO_V1(makepoint);

Datum
makepoint(PG_FUNCTION_ARGS)
{
    /* Here, the pass-by-reference nature of Point is not hidden */
    Point     *pointx = PG_GETARG_POINT_P(0);
    Point     *pointy = PG_GETARG_POINT_P(1);
    Point     *new_point = (Point *) palloc(sizeof(Point));

    new_point->x = pointx->x;
    new_point->y = pointy->y;
       
    PG_RETURN_POINT_P(new_point);
}

/* By Reference, Variable Length */

PG_FUNCTION_INFO_V1(copytext);

Datum
copytext(PG_FUNCTION_ARGS)
{
    text     *t = PG_GETARG_TEXT_P(0);
    /*
     * VARSIZE is the total size of the struct in bytes.
     */
    text     *new_t = (text *) palloc(VARSIZE(t));
    VARATT_SIZEP(new_t) = VARSIZE(t);
    /*
     * VARDATA is a pointer to the data region of the struct.
     */
    memcpy((void *) VARDATA(new_t), /* destination */
           (void *) VARDATA(t),     /* source */
           VARSIZE(t)-VARHDRSZ);    /* how many bytes */
    PG_RETURN_TEXT_P(new_t);
}

PG_FUNCTION_INFO_V1(concat_text);

Datum
concat_text(PG_FUNCTION_ARGS)
{
    text  *arg1 = PG_GETARG_TEXT_P(0);
    text  *arg2 = PG_GETARG_TEXT_P(1);
    int32 new_text_size = VARSIZE(arg1) + VARSIZE(arg2) - VARHDRSZ;
    text *new_text = (text *) palloc(new_text_size);

    VARATT_SIZEP(new_text) = new_text_size;
    memcpy(VARDATA(new_text), VARDATA(arg1), VARSIZE(arg1)-VARHDRSZ);
    memcpy(VARDATA(new_text) + (VARSIZE(arg1)-VARHDRSZ),
           VARDATA(arg2), VARSIZE(arg2)-VARHDRSZ);
    PG_RETURN_TEXT_P(new_text);
}

这些函数的CREATE FUNCTION命令与版本 0 的等价形式相同。

乍一看,版本 1 编码约定可能显得像无谓的故弄玄虚。但它们确实提供了不少改进,因为宏可以隐藏不必要的细节。 例如,在编写 add_one_float8 时,我们不再需要知道 float8 是传引用类型。另一个例子是,变长类型的 GETARG 宏隐藏了处理获取"经 TOAST 处理"(压缩或行外存储)值的需要。上面所示的旧风格 copytext 和 concat_text 函数在存在经 TOAST 处理的值时实际上是错误的,因为它们没有对其输入调用 pg_detoast_datum()。(旧风格动态装载函数的处理器目前会处理这一细节,但效率低于版本 1 函数所能达到的水平。)

版本 1 函数的一大改进是对 NULL 输入和结果的更好处理。宏 PG_ARGISNULL(n) 允许函数测试每个输入是否为 NULL(当然,只有在未声明为“严格”的函数中才有必要这样做)。与 PG_GETARG_xxx() 宏一样,输入参数从零开始计数。要返回 NULL 结果,执行 PG_RETURN_NULL();它在严格和非严格函数中都可用。

版本 1 函数调用约定使得返回“集合”结果、实现触发器函数和过程语言调用处理器成为可能。版本 1 代码也比版本 0 更具可移植性,因为它不违反 ANSI C 对函数调用协议的限制。更多细节见源码发行包中的 src/backend/utils/fmgr/README。

13.4.4. C 语言函数中的复合类型

复合类型没有像 C 结构那样的固定布局。复合类型的实例可能包含空字段。此外,属于某个继承层次的复合类型可能具有与同一继承层次其他成员不同的字段。因此,Postgres 提供了一个从 C 访问复合类型字段的过程式接口。当 Postgres 处理一组行时,每一行都会作为一个 TUPLE 类型的不透明结构传入你的函数。 假设我们想编写一个函数来回答查询

SELECT name, c_overpaid(emp, 1500) AS overpaid
FROM emp
WHERE name = 'Bill' OR name = 'Sam';

在上面的查询中,我们可以把 c_overpaid 定义为:

#include "postgres.h"
#include "executor/executor.h"  /* for GetAttributeByName() */

bool
c_overpaid(TupleTableSlot *t, /* the current row of EMP */
           int32 limit)
{
    bool isnull;
    int32 salary;

    salary = DatumGetInt32(GetAttributeByName(t, "salary", &isnull));
    if (isnull)
        return (false);
    return salary > limit;
}

/* In version-1 coding, the above would look like this: */

PG_FUNCTION_INFO_V1(c_overpaid);

Datum
c_overpaid(PG_FUNCTION_ARGS)
{
    TupleTableSlot  *t = (TupleTableSlot *) PG_GETARG_POINTER(0);
    int32            limit = PG_GETARG_INT32(1);
    bool isnull;
    int32 salary;

    salary = DatumGetInt32(GetAttributeByName(t, "salary", &isnull));
    if (isnull)
        PG_RETURN_BOOL(false);
    /* Alternatively, we might prefer to do PG_RETURN_NULL() for null salary */

    PG_RETURN_BOOL(salary > limit);
}

GetAttributeByName 是 Postgres 的系统函数,返回当前行中的属性。它有三个参数:传给函数的 TupleTableSlot* 类型参数、所需属性的名称,以及一个告知该属性 是否为空的返回参数。GetAttributeByName 返回一个 Datum 值,你可以用适当的 DatumGetXXX() 宏把它转换为正确的数据类型。

下面的查询让 Postgres 知道 c_overpaid 函数:

CREATE FUNCTION c_overpaid(emp, int4) 
RETURNS bool
AS 'PGROOT/tutorial/obj/funcs.so' 
LANGUAGE 'c';

虽然有办法在 C 函数内构造新行或修改现有行,但这些 方法过于复杂,本手册不予讨论。

13.4.5. 编写代码

现在我们转向编写编程语言函数这一更困难的任务。请注意:本手册的这一部分不会使你成为程序员。在尝试为 Postgres 编写 C 函数之前,你必须对 C(包括指针和 malloc 内存管理器的使用)有良好的理解。虽然也许可以把用 C 以外语言编写的函数装载到 Postgres,但这通常很困难(即使在可能的时候),因为其他语言(如 FORTRAN 和 Pascal)往往不遵循与 C 相同的调用约定。也就是说,其他语言在函数之间传递参数和返回值的方式不同。因此,我们将假定你的编程语言函数是用 C 编写的。

构建 C 函数的基本规则如下:

  • 相关的头(include)文件安装在 /usr/local/pgsql/include 或等价位置。你可以用 pg_config --includedir 找出它在你(或你的用户将要运行)的系统上的位置。对非常底层的工作,你可能需要有完整的 PostgreSQL 源码树。

  • 分配内存时,使用 Postgres 的例程 palloc 和 pfree,而不是相应的 C 库例程 malloc 和 free。palloc 分配的内存会在每个事务结束时自动释放,从而防止内存泄漏。

  • 总是使用 memset 或 bzero 把结构体的字节清零。有几个例程(如哈希访问方法、哈希连接和排序算法)会计算结构体中原始位的函数。即使你初始化了结构体的所有字段,结构体中仍可能存在若干包含垃圾值的对齐填充字节(结构体中的空洞)。

  • Postgres 的大多数内部类型都在 postgres.h 中声明,而函数管理器接口(PG_FUNCTION_ARGS 等)位于 fmgr.h 中,因此你至少需要包含这两个文件。出于可移植性考虑,最好把 postgres.h 放在最前面,先于任何其他系统或用户头文件。包含 postgres.h 时也会顺带为你包含 c.h、 elog.h 和 palloc.h。

  • 对象文件中定义的符号名不得相互冲突,也不得与 PostgreSQL 服务器可执行文件中定义的符号冲突。如果收到此类错误消息,你必须重命名你的函数或变量。

  • 编译和链接你的目标代码使其能被动态装载到 Postgres 总是需要特殊的标志。关于如何针对你的特定操作系统进行操作,参见 第 13.4.6 节 的详细说明。

13.4.6. 编译和链接动态装载的函数 #

在你能够使用以 C 编写的 PostgreSQL 扩展函数之前, 必须以特殊方式对它们进行编译和链接,以生成一个可由服务器动态装载的文件。 更准确地说,需要创建一个共享库。

若想了解本节未涵盖的信息,你应阅读操作系统的文档,特别是 C 编译器 cc 和链接编辑器 ld 的手册页。 此外,PostgreSQL 源代码在 contrib 目录中包含若干可用的示例。 不过,如果你依赖这些示例,就会使你的模块依赖于 PostgreSQL 源代码是否可用。

创建共享库通常与链接可执行文件类似:先把源文件编译为目标文件, 再把目标文件链接在一起。目标文件需要以位置无关代码 (PIC)形式生成。 从概念上讲,这意味着当它们被可执行文件装载时,可以放在内存中的任意位置。 (面向可执行文件的目标文件通常不会这样编译。) 链接共享库的命令中也包含一些特殊标志,用来把它与链接可执行文件的命令区分开来 (至少理论上如此,某些系统上的实际做法要丑陋得多)。

在下面的示例中,我们假定你的源代码位于文件 foo.c 中, 并将创建共享库 foo.so。除非另有说明, 中间目标文件名为 foo.o。共享库可以包含多个目标文件, 但这里我们只使用一个。

BSD/OS

生成 PIC 的编译器选项是 -fpic。创建共享库时使用的链接器选项是 -shared。

gcc -fpic -c foo.c
ld -shared -o foo.so foo.o

这从 BSD/OS 4.0 版起适用。

FreeBSD

生成 PIC 的编译器选项是 -fpic。创建共享库时使用的编译器选项是 -shared。

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o

这从 FreeBSD 3.0 版起适用。

HP-UX

生成 PIC 的系统编译器选项是 +z。使用 GCC 时则是 -fpic。用于共享库的链接器选项是 -b。因此:

cc +z -c foo.c

or:

gcc -fpic -c foo.c

and then:

ld -b -o foo.sl foo.o

与大多数其他系统不同,HP-UX 使用 .sl 作为共享库扩展名。

Irix

PIC 是默认行为,无须特殊的编译器选项。 生成共享库时使用的链接器选项是 -shared。

cc -c foo.c
ld -shared -o foo.so foo.o
Linux

生成 PIC 的编译器选项是 -fpic。在某些平台的某些情况下,如果 -fpic 不起作用,则必须使用 -fPIC。更多信息请参阅 GCC 手册。创建共享库的 编译器选项是 -shared。完整示例如下:

cc -fpic -c foo.c
cc -shared -o foo.so foo.o
NetBSD

生成 PIC 的编译器选项是 -fpic。对于 ELF 系统,使用 带 -shared 选项的编译器来链接共享库。在较旧 的非 ELF 系统上,则使用 ld -Bshareable。

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o
OpenBSD

生成 PIC 的编译器选项是 -fpic。链接共享库使用 ld -Bshareable。

gcc -fpic -c foo.c
ld -Bshareable -o foo.so foo.o
Digital Unix/Tru64 UNIX

PIC 是默认值,因此编译命令就是常规的那条。链接时需要使用带特殊选项的ld。

cc -c foo.c
ld -shared -expect_unresolved '*' -o foo.so foo.o

使用 GCC 代替系统编译器时过程相同;不需要特殊选项。

Solaris

使用 Sun 编译器时,生成 PIC 的编译器选项是 -KPIC;使用 GCC 时则是 -fpic。要链接共享库,两种编译器都使用编译器 选项 -G,或者在使用 GCC 时 改用 -shared。

cc -KPIC -c foo.c
cc -G -o foo.so foo.o

or

gcc -fpic -c foo.c
gcc -G -o foo.so foo.o
Unixware

生成 PIC 的编译器选项,对于 SCO 编译器是 -K PIC,而 -fpic 则用于 GCC。 链接共享库时,编译器选项对于 SCO 编译器是 -G, 对于 GCC 则是 -shared。

cc -K PIC -c foo.c
cc -G -o foo.so foo.o

或者

gcc -fpic -c foo.c
gcc -shared -o foo.so foo.o

提示

如果你想把扩展模块打包以便广泛分发,应该考虑使用 GNU Libtool 来构建共享库。它把平台差异封装到一个通用而强大的接口之后。严肃的打包还需要考虑库版本管理、符号解析方法以及其他问题。

生成的共享库文件随后就可以装载到 Postgres 中。 在向 CREATE FUNCTION 命令指定文件名时, 必须给出共享库文件名(以 .so 结尾),而不是简单的目标文件。

注意

实际上,Postgres 并不关心你把文件命名成什么, 只要它是一个共享库文件即可。

传给 CREATE FUNCTION 命令的路径必须是绝对路径 (即以 / 开头),并且指向运行 Postgres 服务器的机器上可见的目录。相对路径 实际上也能工作,但它是相对于数据库所在目录的(前端应用通常看不到这个 目录)。显然,让路径相对于用户启动前端应用的目录是没有意义的,因为 服务器可能运行在一台完全不同的机器上!Postgres 服务器运行所用的用户 ID 必须能够遍历传给 CREATE FUNCTION 命令的路径,并且能够读取该共享库文件。(让文件或上层目录对 “postgres” 用户不可读和/或不可执行是一个常见错误。)

提交更正

译文有误、术语不当或页面显示问题,请到译文仓库 pgsty/pgdoc 报告译文问题。 英文原文本身的问题,请在当前版本的对应页面向上游反馈;上游不再修订已结束维护的版本。