【PGCCC】Postgresql 编写自定义 C 函数
前言
postgresql 支持自定义函数,并且还支持多种语言进行编写, 极大的提高了可扩展性。postgresql 支持使用 pgSQL(postgresql提供的类sql语言),python 和 c 语言来编写函数,本篇主要讲解 c 语言,因为它的性能是最好的。
示例
编写 c 函数分为三部分:
- 编写 c 文件,定义函数实现
- 编译程序,需要编译成共享库
- 在 postgresql 中执行 CREATE FUNCTION注册函数
下面展示一个简单的函数,用于两数相加。通过这个简单的例子,来看看操作流程。
编写函数
创建一个my_add_func.c 文件,内容如下
#include "postgres.h"
#include "fmgr.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(my_add_func);
Datum my_add_func(PG_FUNCTION_ARGS)
{
int32 a = PG_GETARG_INT32(0);
int32 b = PG_GETARG_INT32(1);
int64 result = a + b;
PG_RETURN_INT64(result);
}
这里面的函数很简单,只不过调用了一些莫名其妙的宏,所以会觉得疑惑。这些宏的原理在后面会讲到,这里先简单的介绍下作用。
PG_MODULE_MAGIC宏必须要使用,后面编译生成的库才可以被 postgresql 加载。
PG_FUNCTION_INFO_V1宏声明了函数的版本,这个也必须要使用,目前postgresql 只支持 v1 版本的函数。
PG_FUNCTION_ARGS宏定义了参数类型和名称,等于FunctionCallInfo fcinfo,这个函数名称会在取参数值会用到。
PG_GETARG_INT32宏表示取出指定位置的参数,并且转换为 int32 类型。
PG_RETURN_INT64宏表示将 int64 类型的数值,转换为Datum并且返回。
编译函数
上述的 c 文件,包含了 postgresql 里的头文件,所以在编译的时候需要指定头文件的位置。通过pg_config --includedir-server命令就可以找到。我是通过 yum 在 Centos 上装的 postgresql,执行情况如下:
[root@host1]# /usr/pgsql-9.6/bin/pg_config --includedir-server
/usr/pgsql-9.6/include/server
然后执行编译命令,记住这个生成共享库(so 结尾的文件)的路径,在创建函数时会用到
gcc -fPIC -c my_add_func.c -I/usr/pgsql-9.6/include/server/ # 生成my_add_func.o文件
gcc -shared -o my_add_func.so my_add_func.o # 生成共享库
创建函数
然后在 postgresql 命令行执行CREATE FUNCTION命令
CREATE FUNCTION my_add(a integer, b integer) RETURNS integer
AS '/var/lib/pgsql/my_add_func.so', 'my_add_func' LANGUAGE C STRICT;
接下来我们尝试使用 my_add 函数
test=# select my_add(1, 2);
my_add
--------
3
(1 row)
test=# select my_add(id, price), id, price from orders ;
my_add | id | price
--------+----+-------
124 | 1 | 123
(1 row)
数据类型
通过上面的例子,可以看到创建一个 c 函数并不难,不过还有些细节需要注意到,就是数据是如何在 postgresql 和函数之间传递的。
postgresql 支持的数据类型有多种,基本类型 int,char,double 等,还有复杂类型,比如字符串,结构体等。这些数据传递时都是由Datum类型表示,也就是指针类型。指针长度根据平台不同而不一样,有32位或64位。那么它是如何能够表示这么多类型的数据呢。下面分为两种情形来分析,基本数据类型和复杂数据类型。
基本数据传递
基本数据类型包含 int8,int16,int32,int64,float等数值型。因为Datum的长度根据系统的不同,可能是 4个字节 或 8个字节。如果基本类型的长度小于等于 Datum,那么将它的值直接存储在Datum。如果大于 Datum,就需要分配堆内存,然后将内存地址存储在Datum。
我们以 int16 类型的数值 -1 为例,它的二进制位1111 1111 1111 1111。这里假设Datum的长度为 4 个字节,将其转换为Datum之后的二进制位0000 0000 0000 0000 1111 1111 1111 1111。这里只是在高位填充0。当从Datum转换 int16 类型时,直接取对应的低位。这里转换规则和我们平时接触的不太一样,在 c 语言中如果是负数,会在高位填充1,正数才会填充0,目前还不太清楚原因。
再来看看 int64 数据类型,Int64GetDatum定义了转换规则。如果Datum的长度等于 8 个字节,那么就定义了USE_FLOAT8_BYVAL宏,也就是直接存储到Datum里。如果Datum的长度小于 8 个字节,那么则分配堆内存,然后将内存地址存储在Datum。
#ifdef USE_FLOAT8_BYVAL
#define Int64GetDatum(X) ((Datum) (X))
#else
extern Datum Int64GetDatum(int64 X);
#endif
Datum Int64GetDatum(int64 X)
{
// 使用palloc分配堆内存,palloc的作用同malloc一样,在它基础之上增加了内存管理
int64 *retval = (int64 *) palloc(sizeof(int64));
// 设置堆内存值
*retval = X;
// 将内存地址存储在Datum
return PointerGetDatum(retval);
}
相关宏
这里再来回顾下PG_FUNCTION_ARGS宏,它定义函数参数名称fcinfo,类型为FunctionCallInfo。FunctionCallInfo是一个结构体,里面有个args成员,表示参数数组。
#define PG_FUNCTION_ARGS FunctionCallInfo fcinfo
postgresql 为我们提供了提取参数的方法,下面列举其中一些
// 提取指定位置的参数
#define PG_GETARG_DATUM(n) (fcinfo->args[n].value)
// 将指定位置的参数转换为uint16
#define PG_GETARG_UINT16(n) DatumGetUInt16(PG_GETARG_DATUM(n))
// 将指定位置的参数转换为int32
#define PG_GETARG_INT32(n) DatumGetInt32(PG_GETARG_DATUM(n))
同样 postgresql 也为我们提供了返回结果的方法,
// 将int32转换为Datum类型,并且返回
#define PG_RETURN_INT32(x) return Int32GetDatum(x)
// 将uint32转换为Datum类型,并且返回
#define PG_RETURN_UINT32(x) return UInt32GetDatum(x)
复合数据传递
上述介绍的my_add_func例子,只是接收和返回基本类型。postgresql 还可以接收行数据,或者自定义的数据类型(比如postgis插件的 POINT 类型),这种情况叫做复合类型数据,它在 c 语言中 由 tuple 表示,下面演示一个实例:
c 文件内容
#include "postgres.h"
#include "funcapi.h"
PG_MODULE_MAGIC;
PG_FUNCTION_INFO_V1(my_tuple_func);
Datum my_tuple_func(PG_FUNCTION_ARGS)
{
// 第一个参数为复合类型
HeapTupleHeader tuple = PG_GETARG_HEAPTUPLEHEADER(0);
// 取出返回数据的类型,由TupleDesc表示
TupleDesc tupdesc;
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
elog(ERROR, "return type must be a row type");
// 构建返回值
Datum values[2];
values[0] = PointerGetDatum(cstring_to_text("hello"));
values[1] = Int32GetDatum(1);
// nulls 数组表示值是否为空
bool nulls[2];
memset(nulls, 0, sizeof(nulls));
// 构建返回数据,由HeapTuple表示
HeapTuple result = heap_form_tuple(tupdesc, values, nulls);
PG_RETURN_DATUM(HeapTupleGetDatum(result));
}
创建函数语句如下,定义了接收参数类型为 record, 返回结果有两列(text,int)
CREATE FUNCTION my_tuple_func(tuple record)
RETURNS TABLE (message text ,num int)
AS '/var/lib/pgsql/my_tuple_func', 'my_tuple_func'
LANGUAGE C STRICT;
执行语句,返回的数据为复合类型,可以通过.*符号展示所有列
test=# select my_tuple_func(orders) from orders where id = 1;
my_tuple_func
---------------
(hello,1)
(1 row)
test=# select (my_tuple_func(orders)).* from orders where id = 1;
message | num
---------+-----
hello | 1
通过上面的例子,可以看到可以接收行数据,参数为对应的表名即可。它由HeapTuple类表示,关于HeapTuple的原理,可以参考后续的文件。
在获取参数时,我们又看到了一个新的宏PG_GETARG_HEAPTUPLEHEADER,它负责提取参数并且转换为HeapTuple类。
在构建返回结果时,我们需要调用get_call_result_type函数获取返回结果的类型,也就是TupleDesc。返回结果的类型是在我们执行CREATE FUNCTION时候创建的,通过RETURNS TABLE 语句定义的。postgresql 在调用函数时,会将结果类型保存到参数FunctionCallInfo里。
还需要注意一点,上述函数返回的第一列是TEXT类型,它并不等于 C 语言的字符串,所以需要使用cstring_to_text函数转换。
返回多行数据
下面展示一个函数,用于生成指定数量的 斐波那契数列。每行数据包括两列(num int, value int)。
c 语言文件
#include "postgres.h"
#include "funcapi.h"
PG_MODULE_MAGIC;
// 计算斐波那契数列,需要记录前两个结果的值
typedef struct OlderValues {
uint64 value1;
uint64 value2;
} OlderValues;
PG_FUNCTION_INFO_V1(my_records_func);
Datum my_records_func(PG_FUNCTION_ARGS)
{
FuncCallContext *funcctx;
// 判断是否第一次执行
if (SRF_IS_FIRSTCALL())
{
// 进行此次函数第一次初始化
funcctx = SRF_FIRSTCALL_INIT();
// 切换内存分配器
MemoryContext oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);
// 提取参数,保存到FuncCallContext的max_calls属性里
funcctx->max_calls = PG_GETARG_UINT32(0);
// 分配自定义上下文变量
OlderValues * mydata = palloc(sizeof(OlderValues));
mydata->value1 = 0;
mydata->value2 = 1;
funcctx->user_fctx = mydata;
//切回内存分配器
MemoryContextSwitchTo(oldcontext);
}
// 获取最新的FuncCallContext
funcctx = SRF_PERCALL_SETUP();
uint64 call_cntr = funcctx->call_cntr;
uint64 max_calls = funcctx->max_calls;
if (call_cntr < max_calls)
{
TupleDesc tupdesc;
if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
elog(ERROR, "return type must be a row type");
// 构建返回值,使用调用次数作为第一列的值
Datum values[2];
values[0] = UInt64GetDatum(call_cntr);
// 计算此次斐波那契的值
uint64 value;
if (call_cntr == 0) {
value = 0;
}
if (call_cntr == 1) {
value = 1;
}
if (call_cntr > 1) {
// 取出上下文的变量,也就是前两个斐波那契的值
OlderValues *mydata = (OlderValues *) funcctx->user_fctx;
value = mydata->value1 + mydata->value2;
mydata->value1 = mydata->value2;
mydata->value2 = value;
}
values[1] = UInt64GetDatum(value);
// nulls 数组表示值是否为空
bool nulls[2];
memset(nulls, 0, sizeof(nulls));
// 构建返回数据,由HeapTuple表示
HeapTuple tuple = heap_form_tuple(tupdesc, values, nulls);
Datum result = HeapTupleGetDatum(tuple);
// 返回数据
SRF_RETURN_NEXT(funcctx, result);
}
// 数据已经全部返回
SRF_RETURN_DONE(funcctx);
}
创建函数
CREATE FUNCTION my_records_func(number_limit int)
RETURNS TABLE (num int ,value int)
AS '/var/lib/pgsql/my_records_func.so', 'my_records_func'
LANGUAGE C STRICT;
执行语句
test=# select * from my_records_func(5);
num | value
-------+-----
0 | 0
1 | 1
2 | 1
3 | 2
4 | 3
(5 rows)
函数调用上下文
因为要返回多行数据,所以会执行函数多次。这里需要一个上下文变量,来保存需要在执行多次之间传递的变量,比如执行的次数等。FuncCallContext结构体,作为函数调用的内置上下文变量,下面只是列举常见的成员
typedef struct FuncCallContext
{
uint64 call_cntr; // 已经返回结果的次数
void *user_fctx; // 用于保存自定义上下文
MemoryContext multi_call_memory_ctx; // 用于分配内存
// .......
} FuncCallContext;
相关宏
上述的程序调用了多个宏,接下来简单介绍下它们的用途:
SRF_IS_FIRSTCALL负责判断是否这是第一次执行。
SRF_FIRSTCALL_INIT负责初始化FuncCallContext,它会设置call_cntr为 0,然后在fn_mcxt内存管理器下,分配一个子管理器,负责这个函数执行时的内存申请。使用内存管理器有一个好处,它会记录所有的申请内存,在所有的行数据返回完之后,会释放掉执行期间申请的所有内存。
SRF_PERCALL_SETUP负责返回最新的FuncCallContext。
SRF_RETURN_NEXT负责返回数据,会更新call_cntr自增,设置isDone为ExprMultipleResult,表示还有更多的行数据。
SRF_RETURN_NEXT_NULL负责返回空数据,执行过程同SRF_RETURN_NEXT相同。
SRF_RETURN_DONE负责释放函数执行期间申请的所有内存,并且设置isDone为ExprEndResult,表示所有的行数据已经返回完了。
作者:zhmin
链接:https://zhmin.github.io/posts/postgresql-c-function/
PG证书#PG考试#postgresql培训#postgresql考试#postgresql认证