用C语言开发PostgreSQL用户自定义函数之基础数据使用--数组篇

本文涉及的产品
PolarClaw,2核4GB
简介: 本文详解如何在C语言中编写PostgreSQL自定义函数操作int32数组,涵盖数组的解构、类型验证、元素插入与重构等核心步骤。通过完整示例展示一维数组的追加、前置和指定位置插入功能,深入解析`deconstruct_array`、`construct_array`等关键函数的使用,结合内存管理、错误处理与编译部署全流程,帮助开发者高效实现高性能数组操作UDF。

在使用C语言编写PostgreSQL用户自定义函数(UDF)时,使用数组是很常见的需求。本篇将基于一个完整的int32数组插入操作示例,详细讲解如何在C语言自定义函数中正确处理PostgreSQL数组类型,包括数组的解构、验证、元素操作和重构等核心技术点。示例程序如下:

#include "postgres.h"
#include "fmgr.h"
#include "utils/array.h"
#include "catalog/pg_type.h"
#include "utils/lsyscache.h"


PG_MODULE_MAGIC;

static Datum
int32_array_insert_at(FunctionCallInfo fcinfo, int32 position, bool is_prepend);


/* 版本1:添加到末尾 */
PG_FUNCTION_INFO_V1(int32_array_append);

Datum
int32_array_append(PG_FUNCTION_ARGS)
{
    return int32_array_insert_at(fcinfo, 0, false);
}

/* 版本2:添加到开头 */
PG_FUNCTION_INFO_V1(int32_array_prepend);

Datum
int32_array_prepend(PG_FUNCTION_ARGS)
{
    return int32_array_insert_at(fcinfo, 1, true);
}

/* 版本3:在指定位置插入 */
PG_FUNCTION_INFO_V1(int32_array_insert);

Datum
int32_array_insert(PG_FUNCTION_ARGS)
{
    int32       position;

    if (PG_ARGISNULL(2))
        position = 1;  /* 默认位置为1 */
    else
        position = PG_GETARG_INT32(2);

    if (position < 1)
        ereport(ERROR,
                (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
                 errmsg("位置必须是正整数")));

    return int32_array_insert_at(fcinfo, position, false);
}

/* 通用插入函数 */
static Datum
int32_array_insert_at(FunctionCallInfo fcinfo, int32 position, bool is_prepend)
{
    ArrayType  *input_array = NULL;
    int32       new_value;
    Datum      *elems = NULL;
    bool       *nulls = NULL;
    int         nelems = 0;
    int         ndim;
    Oid         elem_type;
    int16       elem_width;
    bool        elem_byval;
    char        elem_align;
    ArrayType  *result_array;
    Datum      *new_elems;
    bool       *new_nulls;
    int         i, j;
    int         insert_pos;

    /* 处理数组参数 */
    if (!PG_ARGISNULL(0))
    {
        input_array = PG_GETARG_ARRAYTYPE_P(0);

        /* 验证数组元素类型 */
        elem_type = ARR_ELEMTYPE(input_array);
        if (elem_type != INT4OID)
            ereport(ERROR,
                    (errcode(ERRCODE_DATATYPE_MISMATCH),
                     errmsg("输入数组元素类型必须为integer")));

        /* 获取数组信息 */
        get_typlenbyvalalign(elem_type, &elem_width, &elem_byval, &elem_align);

        /* 解构数组 */
        deconstruct_array(input_array, elem_type, elem_width, 
                          elem_byval, elem_align,
                          &elems, &nulls, &nelems);

        ndim = ARR_NDIM(input_array);

        if (ndim != 1 && ndim != 0)
            ereport(ERROR,
                    (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
                     errmsg("只支持一维数组")));
    }
    else
    {
        /* 没有输入数组,设置默认值 */
        elem_type = INT4OID;
        get_typlenbyvalalign(elem_type, &elem_width, &elem_byval, &elem_align);
    }

    /* 获取要添加的值 */
    if (PG_ARGISNULL(1))
    {
        /* 新值为NULL */
        new_value = 0;
    }
    else
    {
        new_value = PG_GETARG_INT32(1);
    }

    /* 确定插入位置 */
    if (is_prepend)
        insert_pos = 1;  /* 添加到开头 */
    else if (position > 0)
        insert_pos = position;
    else
        insert_pos = nelems + 1;  /* 添加到末尾 */

    /* 调整位置到有效范围 */
    if (insert_pos < 1)
        insert_pos = 1;
    if (insert_pos > nelems + 1)
        insert_pos = nelems + 1;

    /* 分配新数组内存 */
    new_elems = (Datum *) palloc((nelems + 1) * sizeof(Datum));
    new_nulls = (bool *) palloc((nelems + 1) * sizeof(bool));

    /* 复制插入点之前的元素 */
    for (i = 0, j = 0; i < insert_pos - 1; i++, j++)
    {
        if (elems)
        {
            new_elems[j] = elems[i];
            new_nulls[j] = nulls[i];
        }
        else
        {
            break;  /* 没有输入数组 */
        }
    }

    /* 插入新元素 */
    new_elems[j] = Int32GetDatum(new_value);
    new_nulls[j] = PG_ARGISNULL(1);
    j++;

    /* 复制插入点之后的元素 */
    for (; i < nelems; i++, j++)
    {
        new_elems[j] = elems[i];
        new_nulls[j] = nulls[i];
    }

    /* 创建新数组 */
    result_array = construct_array(new_elems, nelems + 1, elem_type,
                                   elem_width, elem_byval, elem_align);

    /* 释放内存 */
    if (elems)
    {
        pfree(elems);
        pfree(nulls);
    }
    pfree(new_elems);
    pfree(new_nulls);

    PG_RETURN_ARRAYTYPE_P(result_array);
}

struct.png

一、PostgreSQL C函数开发基础

在开始数组操作前,先了解C语言编写PostgreSQL函数的核心要素:

1.1 必要的头文件

示例代码中引入的核心头文件各有其用途:

#include "postgres.h"       // PostgreSQL核心定义
#include "fmgr.h"           // 函数调用接口
#include "utils/array.h"    // 数组操作核心函数
#include "catalog/pg_type.h"// 内置类型OID定义(如INT4OID)
#include "utils/lsyscache.h"// 类型信息缓存函数

1.2 模块标识与函数注册

PG_MODULE_MAGIC; // 必须的模块魔术标识,用于版本校验

// 函数注册宏,V1表示使用版本1的调用接口
PG_FUNCTION_INFO_V1(int32_array_append);

二、数组操作核心流程解析

示例代码实现了int32数组的追加、前置和指定位置插入功能,核心逻辑封装在int32_array_insert_at函数中,完整的数组操作流程分为以下步骤:

2.1 数组参数接收与验证

2.1.1 获取数组参数

// 从函数调用信息中获取数组参数(第一个参数)
ArrayType  *input_array = NULL;
if (!PG_ARGISNULL(0)) {
   
    input_array = PG_GETARG_ARRAYTYPE_P(0);
}
  • PG_ARGISNULL(n):检查第n个参数是否为NULL
  • PG_GETARG_ARRAYTYPE_P(n):获取第n个参数的数组指针

2.1.2 验证数组类型与维度

// 获取数组元素类型OID
elem_type = ARR_ELEMTYPE(input_array);
// 验证元素类型是否为int32(INT4OID是int4类型的内置OID)
if (elem_type != INT4OID)
    ereport(ERROR,
            (errcode(ERRCODE_DATATYPE_MISMATCH),
             errmsg("输入数组元素类型必须为integer")));

// 验证数组维度(仅支持一维数组)
ndim = ARR_NDIM(input_array);
if (ndim != 1 && ndim != 0)
    ereport(ERROR,
            (errcode(ERRCODE_ARRAY_SUBSCRIPT_ERROR),
             errmsg("只支持一维数组")));

核心宏说明:

  • ARR_ELEMTYPE(arr):获取数组元素的类型OID
  • ARR_NDIM(arr):获取数组的维度
  • ereport(ERROR, ...):抛出PostgreSQL错误,包含错误码和错误信息

2.2 数组解构(转换为原生C数组)

PostgreSQL的数组是封装的ArrayType类型,需要转换为原生C数组才能操作:

// 获取元素类型的内存属性(长度、是否传值、对齐方式)
get_typlenbyvalalign(elem_type, &elem_width, &elem_byval, &elem_align);

// 解构数组为原生C数组
Datum      *elems = NULL;  // 存储元素值的数组
bool       *nulls = NULL;  // 存储元素是否为NULL的数组
int         nelems = 0;    // 元素个数
deconstruct_array(input_array, elem_type, elem_width, 
                  elem_byval, elem_align,
                  &elems, &nulls, &nelems);
  • get_typlenbyvalalign:获取类型的内存布局信息,为数组解构做准备
  • Datum:PostgreSQL中存储任意数据类型的通用容器
  • nulls:布尔数组,标记对应位置的元素是否为NULL
  • deconstruct_array:用于从数组对象提取数据的简单方法,说明如下:
    • input_array: 待解析的数组对象(必须非空)
    • elem_type: 元素数据类型信息
    • elem_width: 元素类型长度
    • elem_byval: 是否按值传递
    • elem_align: 内存对齐方式
    • elems: 返回值,指向已分配内存的数据值数组
    • nulls: 返回值,指向已分配内存的空值标记数组
    • nelems: 返回值,设置为提取的元素数量
      若调用方不支持数组中的空值,可传入 nullsp == NULL。注意这将产生信息量较少的错误提示,因此仅在确实不会出现空值的场景下使用。
      若数组元素为按引用传递的数据类型,返回的 Datum 值将指向数组对象内部。

2.3 数组元素操作(插入新元素)

2.3.1 确定插入位置

// 处理插入位置逻辑,确保位置在有效范围(1 ~ nelems+1)
if (insert_pos < 1)
    insert_pos = 1;
if (insert_pos > nelems + 1)
    insert_pos = nelems + 1;

PostgreSQL数组默认是1-based(从1开始计数),而C数组是0-based,需要注意转换。

2.3.2 内存分配与元素复制

// 分配新数组内存(原数组长度+1)
new_elems = (Datum *) palloc((nelems + 1) * sizeof(Datum));
new_nulls = (bool *) palloc((nelems + 1) * sizeof(bool));

// 复制插入点之前的元素
for (i = 0, j = 0; i < insert_pos - 1; i++, j++) {
   
    new_elems[j] = elems[i];
    new_nulls[j] = nulls[i];
}

// 插入新元素
new_elems[j] = Int32GetDatum(new_value);  // 将int32转换为Datum
new_nulls[j] = PG_ARGISNULL(1);           // 标记新元素是否为NULL
j++;

// 复制插入点之后的元素
for (; i < nelems; i++, j++) {
   
    new_elems[j] = elems[i];
    new_nulls[j] = nulls[i];
}

关键要点:

  • 使用palloc而非malloc:PostgreSQL的内存分配函数,内存会在事务结束时自动释放
  • Int32GetDatum:将int32类型转换为Datum类型(PostgreSQL通用数据类型)
  • 分三段复制:插入点前 → 新元素 → 插入点后

2.4 重构数组(转换回PostgreSQL数组)

操作完成后,需要将原生C数组转换回PostgreSQL的ArrayType

// 构建新的PostgreSQL数组
result_array = construct_array(new_elems, nelems + 1, elem_type,
                               elem_width, elem_byval, elem_align);
  • construct_array:将Datum数组重构为PostgreSQL数组,参数说明如下:
    • new_elems: 组成数组内容的数据项数组(不支持空元素值)
    • nelems: 元素个数
    • elem_type: 元素数据类型信息
    • elem_width: 元素类型长度
    • elem_byval: 是否按值传递
    • elem_align: 内存对齐方式
      本函数会构造并返回一个已分配内存的一维数组对象。注意:即使元素是按引用传递的类型,其值也会被复制到数组对象中。另请注意:若 nelems = 0,返回的将是0维数组而非1维数组。

2.5 内存释放与结果返回

// 释放解构后的原数组内存
if (elems) {
   
    pfree(elems);
    pfree(nulls);
}
// 释放新数组的临时内存
pfree(new_elems);
pfree(new_nulls);

// 返回结果数组
PG_RETURN_ARRAYTYPE_P(result_array);
  • pfree:释放palloc分配的内存
  • PG_RETURN_ARRAYTYPE_P:将数组指针作为函数结果返回

三、函数编译与部署

3.1 编译命令

使用CMake构建,CMake配置文件如下:

cmake_minimum_required(VERSION 3.25)
project(add_c C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_BUILD_TYPE debug)

list(APPEND flags "-fPIC")

find_program(PG_CONFIG pg_config REQUIRED)

execute_process(COMMAND ${PG_CONFIG} --includedir-server
    OUTPUT_VARIABLE POSTGRESQL_INCLUDE_DIR
    OUTPUT_STRIP_TRAILING_WHITESPACE)

execute_process(COMMAND ${PG_CONFIG} --libdir
    OUTPUT_VARIABLE POSTGRESQL_LIB_DIR
    OUTPUT_STRIP_TRAILING_WHITESPACE)

execute_process(COMMAND ${PG_CONFIG} --libs
    OUTPUT_VARIABLE POSTGRESQL_LIBS
    OUTPUT_STRIP_TRAILING_WHITESPACE)

add_library(add_c SHARED add.c)

# 设置生成的共享库名称,去掉lib前缀
set_target_properties(add_c PROPERTIES PREFIX "")
# 最终so文件名
set_target_properties(add_c PROPERTIES OUTPUT_NAME add_c)

# 安装目录
install(TARGETS add_c LIBRARY DESTINATION ${POSTGRESQL_LIB_DIR})

target_compile_options(add_c PUBLIC "-fPIC" "-g3" "-O0")
target_link_options(add_c PUBLIC "-shared")
target_include_directories(add_c PUBLIC ${POSTGRESQL_INCLUDE_DIR})
target_link_directories(add_c PUBLIC ${POSTGRESQL_LIB_DIR})

build.png

3.2 注册函数

在PostgreSQL中执行以下SQL注册函数:

-- 追加元素到数组末尾
CREATE OR REPLACE FUNCTION int32_array_append(integer[], integer)
RETURNS integer[]
AS '/path/to/array_functions.so', 'int32_array_append'
LANGUAGE C STRICT;

-- 前置元素到数组开头
CREATE OR REPLACE FUNCTION int32_array_prepend(integer[], integer)
RETURNS integer[]
AS '/path/to/array_functions.so', 'int32_array_prepend'
LANGUAGE C STRICT;

-- 在指定位置插入元素
CREATE OR REPLACE FUNCTION int32_array_insert(integer[], integer, integer)
RETURNS integer[]
AS '/path/to/array_functions.so', 'int32_array_insert'
LANGUAGE C STRICT;

3.3 测试函数

-- 测试追加
SELECT int32_array_append(ARRAY[1,2,3], 4);  -- 返回 {1,2,3,4}

-- 测试前置
SELECT int32_array_prepend(ARRAY[1,2,3], 0); -- 返回 {0,1,2,3}

-- 测试指定位置插入
SELECT int32_array_insert(ARRAY[1,2,3], 99, 2); -- 返回 {1,99,2,3}

test.png

四、要点说明

4.1 内存管理

  1. 始终使用palloc/pfree而非malloc/free:PostgreSQL的内存上下文会自动管理这些内存
  2. 及时释放临时内存:避免内存泄漏,尤其是在循环或复杂逻辑中
  3. 处理NULL参数:必须检查PG_ARGISNULL,避免空指针访问

4.2 错误处理

  1. 使用ereport抛出标准化错误:包含错误码(errcode)和用户友好的错误信息(errmsg)
  2. 验证输入参数:包括数组类型、维度、位置合法性等
  3. 处理边界情况:如空数组、插入位置超出范围等

4.3 性能优化

  1. 复用类型信息:使用get_typlenbyvalalign缓存类型属性,避免重复查询
  2. 减少内存拷贝:尽量在原数组上操作,避免不必要的数组复制
  3. 使用适当的数据类型:根据需求选择int2/int4/int8,避免类型转换开销
相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍如何基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
4月前
|
SQL 存储 缓存
PL/pgSQL 入门教程(六):从避坑到吃透,聊聊事务、错误处理和底层那些事儿
本文深度解析PL/pgSQL开发避坑指南:详解RAISE多级错误处理与USING增强提示、EXCEPTION事务恢复机制、变量替换限制与计划缓存陷阱,并分享美元符引号、CREATE OR REPLACE调试、extra_warnings预警等实战技巧,助你写出健壮高效存储过程。
|
4月前
|
SQL 监控 关系型数据库
PL/pgSQL 入门教程(五):触发器
PostgreSQL触发器是数据库的“自动服务员”,可在INSERT/UPDATE/DELETE等操作时自动执行校验、日志记录、汇总更新等逻辑。支持BEFORE/AFTER/INSTEAD OF时机,ROW/STATEMENT级别,配合NEW/OLD变量实现灵活数据管控,大幅提升数据一致性与运维效率。
|
4月前
|
数据库 C++ Perl
PL/pgSQL 入门教程(三):控制结构
本文详解PL/pgSQL核心编程:函数返回(RETURN单值、RETURN NEXT/QUERY多行)、条件判断(IF/CASE)、循环控制(LOOP/WHILE/FOR/FOREACH)及异常处理(EXCEPTION),附丰富示例与最佳实践,助你写出健壮高效的数据库逻辑。
|
4月前
|
SQL 缓存 安全
PL/pgSQL 入门教程(二):表达式和基础语句
本文详解PL/pgSQL核心语法:表达式由主SQL引擎以参数化SELECT执行,支持计划缓存;基础语句涵盖赋值(:=/=)、静态/动态SQL执行(INTO/PERFORM/EXECUTE)、结果处理(STRICT模式)、状态获取(FOUND/GET DIAGNOSTICS)及空操作NULL。
|
4月前
|
SQL 存储 关系型数据库
PL/pgSQL 入门教程(一):语法篇
本教程为PL/pgSQL入门首篇,系统讲解其核心基础与语法规则。涵盖函数创建、块结构、变量声明、参数传递、返回类型及排序规则等关键知识点,助你掌握在PostgreSQL中编写高效存储过程与函数的必备技能,提升数据库逻辑处理能力。
|
4月前
|
SQL 关系型数据库 数据库
用C语言开发PostgreSQL用户自定义函数之数据查询篇
本教程教你用C语言编写PostgreSQL的UDF函数,通过SPI接口执行SQL查询,利用SRF机制返回多行数据。涵盖头文件引入、函数编写、编译部署及SQL调用全流程,并附内存管理与列序号等避坑提示,助你掌握C语言扩展PostgreSQL的核心技术。
|
4月前
|
SQL 存储 关系型数据库
PostgreSQL SQL函数语法详解
本文深入讲解PostgreSQL中SQL语言函数的编写,涵盖参数引用、返回类型(基类型/复合类型/集合)、输出参数、可变参数、默认值、多态函数及排序规则等核心特性,系统阐述其语法、行为与最佳实践。
|
5月前
|
SQL 关系型数据库 数据库
Postgresql入门之psql用法详解(三)- 元命令详解(\dconfig-\if)
psql元命令以反斜杠开头,由psql客户端直接解析执行,用于增强数据库管理与脚本操作。支持参数引用、变量插值、shell命令执行及SQL语句联动,涵盖连接控制、对象查看、数据导入导出等功能,是PostgreSQL交互操作的重要工具。
|
4月前
|
搜索推荐 关系型数据库 大数据
PL/pgSQL 入门教程(四):使用游标(cursor)
游标是PostgreSQL中“按需取数”的数据指针,避免大查询内存溢出;支持逐行处理、动态查询、精准更新/删除及函数返回大结果集。分未绑定(灵活)与绑定(固定)两类,核心操作为声明→打开→FETCH/MOVE/UPDATE→关闭,FOR循环可自动简化遍历。
|
5月前
|
SQL 关系型数据库 Shell
Postgresql入门之psql用法详解(四)- 高级功能
psql 是 PostgreSQL 的交互式命令行工具,支持模式匹配、变量替换、SQL 插值、自定义提示符及行编辑功能。通过 `\d` 等元命令可按名称模式查看对象,支持通配符与正则表达式。变量可动态设置并安全插值到 SQL 中,提升脚本灵活性。提供丰富的环境变量与配置文件(如 `.psqlrc`)来自定义行为,兼容不同终端与编码环境,适用于本地或远程数据库管理。