用C语言开发PostgreSQL用户自定义函数之数据查询篇

本文涉及的产品
PolarClaw,2核4GB
简介: 本教程教你用C语言编写PostgreSQL的UDF函数,通过SPI接口执行SQL查询,利用SRF机制返回多行数据。涵盖头文件引入、函数编写、编译部署及SQL调用全流程,并附内存管理与列序号等避坑提示,助你掌握C语言扩展PostgreSQL的核心技术。

一、先搞懂核心概念

本篇教程想做的事情其实很简单:写一个C语言的自定义函数(UDF),这个函数能像SQL查询一样从PostgreSQL数据库里查数据,然后把查到的多行结果完整地返回出来。

  • SPI(Server Programming Interface):就像C语言函数和PostgreSQL数据库之间的“桥梁”,让你的C代码能执行SQL查询、获取结果。
  • SRF(Set-Returning Functions):直译是“返回集合的函数”,简单说就是能返回多行数据的函数(普通函数只能返回一个值,SRF能返回一张“小表”)。
  • UDF:用户自定义函数,就是你自己写的、PostgreSQL能调用的函数。

SPI的完整说明请查阅官方文档:https://www.postgresql.org/docs/18/spi.html

二、从0到1写代码

第一步:准备头文件(抄就行)

首先要把PostgreSQL提供的“工具包”包含进来,这些头文件是写C语言UDF的基础,就像做饭要先准备厨具:

#include "postgres.h"    // PostgreSQL核心头文件,必须有
#include "fmgr.h"        // 函数管理相关,UDF必备
#include "funcapi.h"     // SRF相关功能,返回多行数据靠它
#include "access/htup_details.h"  // 处理数据行(元组)用的
#include "executor/spi.h"        // SPI接口的核心头文件
#include "utils/builtins.h"      // 一些基础工具函数

第二步:必加的“魔法语句”

这行代码是PostgreSQL识别C语言扩展的标志,少了它函数用不了,直接复制:

PG_MODULE_MAGIC;

第三步:声明函数信息

告诉PostgreSQL你的函数叫什么名字,版本是V1(固定写法):

PG_FUNCTION_INFO_V1(get_all_empsalary);  // get_all_empsalary是你的函数名,可改

第四步:写核心函数(分两段理解)

函数整体结构是:第一次调用时初始化(查数据库、准备数据) + 每次调用返回一行数据,就像发传单:第一次先把所有传单准备好,之后每次发一张,发完为止。

完整函数代码(带大白话注释)

// 函数的标准声明,Datum是PostgreSQL里“任意数据类型”的统称
Datum
get_all_empsalary(PG_FUNCTION_ARGS)
{
   
    // 1. 定义需要用到的变量(先记着,后面会用)
    FuncCallContext *funcctx;  // SRF的上下文,记录“发了多少张传单”
    int call_cntr;             // 已经返回了多少行数据
    int max_calls;             // 总共要返回多少行数据
    AttInMetadata *attinmeta;  // 数据类型转换的辅助工具

    /* === 第一段:第一次调用时执行(准备所有“传单”)=== */
    if (SRF_IS_FIRSTCALL())  // 判断是不是第一次调用这个函数
    {
   
        MemoryContext oldcontext;  // 内存上下文,不用深究,照抄就行
        TupleDesc tupdesc;         // 描述“每行数据有哪些列”
        SPIPlanPtr plan;           // SPI的查询计划
        int spi_rc;                // SPI执行结果的返回码

        // 初始化SRF上下文(固定写法)
        funcctx = SRF_FIRSTCALL_INIT();

        // 切换内存上下文(防止内存泄露,照抄)
        oldcontext = MemoryContextSwitchTo(funcctx->multi_call_memory_ctx);

        // 1. 连接SPI“桥梁”(必须先连,才能查数据库)
        if (SPI_connect() != SPI_OK_CONNECT)
            elog(ERROR, "get_all_empsalary: SPI_connect failed");  // 连接失败就报错

        // 2. 准备要执行的SQL查询(把你要查的SQL写在这里)
        // 参数说明:SQL语句、参数个数(0表示无参数)、参数类型(NULL)
        if ((plan = SPI_prepare("SELECT depname, empno, salary, enroll_date FROM empsalary ORDER BY empno", 
                               0, NULL)) == NULL)
            elog(ERROR, "get_all_empsalary: SPI_prepare failed");  // 准备SQL失败报错

        // 3. 执行SQL查询(true表示要返回结果,0表示返回所有行)
        spi_rc = SPI_execute_plan(plan, NULL, NULL, true, 0);
        if (spi_rc != SPI_OK_SELECT)
            elog(ERROR, "get_all_empsalary: SPI_execute_plan failed");  // 执行SQL失败报错

        // 4. 检查有没有查到数据(没查到就直接结束)
        if (SPI_processed == 0)
        {
   
            SPI_finish();  // 断开SPI连接
            SRF_RETURN_DONE(funcctx);  // 告诉PostgreSQL:没数据要返回
        }

        // 5. 准备返回数据的格式(描述每行有哪些列、什么类型)
        tupdesc = SPI_tuptable->tupdesc;  // 获取查询结果的列信息
        tupdesc = CreateTupleDescCopy(tupdesc);  // 复制一份,防止被销毁
        attinmeta = TupleDescGetAttInMetadata(tupdesc);  // 初始化类型转换工具
        funcctx->attinmeta = attinmeta;  // 保存到上下文,后面要用

        // 6. 保存查询结果(把查到的所有数据存起来,后面慢慢返回)
        funcctx->user_fctx = SPI_tuptable;

        // 7. 记录总共要返回多少行数据(SPI_processed是查到的行数)
        funcctx->max_calls = SPI_processed;

        // 切回原来的内存上下文(固定写法)
        MemoryContextSwitchTo(oldcontext);
    }

    /* === 第二段:每次调用都执行(发一张“传单”)=== */
    funcctx = SRF_PERCALL_SETUP();  // 初始化每次调用的上下文(固定写法)

    call_cntr = funcctx->call_cntr;  // 获取已经返回的行数
    max_calls = funcctx->max_calls;  // 获取总共要返回的行数
    attinmeta = funcctx->attinmeta;  // 获取类型转换工具

    if (call_cntr < max_calls)  // 如果还有数据没返回
    {
   
        char **values;          // 存储一行数据的所有列值
        HeapTuple tuple;        // 要返回的一行数据(元组)
        // 取出之前保存的查询结果
        SPITupleTable *tuptable = (SPITupleTable *) funcctx->user_fctx;
        // 取出当前要返回的那一行数据
        HeapTuple spi_tuple = tuptable->vals[call_cntr];
        TupleDesc tupdesc = tuptable->tupdesc;

        // 1. 分配内存,存储4列数据(你的SQL查了4列,所以是4)
        values = (char **) palloc(4 * sizeof(char *));

        // 2. 逐个取出列的值(数字是列的位置,从1开始)
        values[0] = SPI_getvalue(spi_tuple, tupdesc, 1);  // depname列
        values[1] = SPI_getvalue(spi_tuple, tupdesc, 2);  // empno列
        values[2] = SPI_getvalue(spi_tuple, tupdesc, 3);  // salary列
        values[3] = SPI_getvalue(spi_tuple, tupdesc, 4);  // enroll_date列

        // 3. 把列值组装成PostgreSQL能识别的“行”(元组)
        tuple = BuildTupleFromCStrings(attinmeta, values);

        // 4. 释放临时内存(防止内存泄露)
        pfree(values);

        // 5. 返回这一行数据,同时记录“已经发了多少张传单”
        SRF_RETURN_NEXT(funcctx, HeapTupleGetDatum(tuple));
    }
    else  // 所有数据都返回完了
    {
   
        SPI_finish();  // 断开SPI连接(必须关,不然占资源)
        SRF_RETURN_DONE(funcctx);  // 告诉PostgreSQL:数据返回完了
    }
}

第五步:编译C代码(这里依然使用CMake)

cmake_minimum_required(VERSION 3.25)
project(spi_demo 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(spi_demo SHARED spi_demo.c)

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

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

target_compile_options(spi_demo PUBLIC "-fPIC" "-g3" "-O0")
target_link_options(spi_demo PUBLIC "-shared")
target_include_directories(spi_demo PUBLIC ${POSTGRESQL_INCLUDE_DIR})
target_link_directories(spi_demo PUBLIC ${POSTGRESQL_LIB_DIR})
cmake . -DCMAKE_EXPORT_COMPILE_COMMANDS=1
make

sudo cp spi_demo.so /usr/local/pgsql18/lib/spi_demo.1.0.so

第六步:在数据库里创建函数(SQL语句)

就像给数据库“注册”你的C函数,让它知道怎么调用:

-- 第一步:定义返回类型(和你查询的列对应,列名、类型要一致)
CREATE TYPE empsalary_type AS (
    depname text,    -- 对应SQL里的depname列
    empno bigint,    -- 对应empno列
    salary int,      -- 对应salary列
    enroll_date date -- 对应enroll_date列
);

-- 第二步:创建函数
CREATE OR REPLACE FUNCTION get_all_empsalary()
RETURNS SETOF empsalary_type  -- SETOF表示返回多行,类型是上面定义的empsalary_type
AS '$libdir/spi_demo.1.0', 'get_all_empsalary'  -- 第一个是编译后的文件路径,第二个是C函数名
LANGUAGE C STRICT;  -- 语言是C,STRICT表示参数为NULL时直接返回NULL

prepare.png

第七步:调用函数(验证结果)

执行下面的SQL,就能看到和直接查empsalary表一样的结果:

SELECT * FROM get_all_empsalary();

test_result.png

三、新手避坑指南(重点!)

  1. 列的位置别写错SPI_getvalue的第三个参数是列的位置(从1开始),比如你查的是a,b,c,那a是1,b是2,c是3,错了会返回错误的值;
  2. 内存要释放:用palloc分配的内存,一定要用pfree释放,不然会导致数据库内存泄露;
  3. SPI连接要关:最后一定要调用SPI_finish(),不然会占用数据库连接资源;

思考:本例中返回数据集时在数据库中额外创建了个一类型empsalary_type,有没有更好的办法?

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍如何基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
相关文章
|
3月前
|
SQL 存储 关系型数据库
PL/pgSQL 入门教程(一):语法篇
本教程为PL/pgSQL入门首篇,系统讲解其核心基础与语法规则。涵盖函数创建、块结构、变量声明、参数传递、返回类型及排序规则等关键知识点,助你掌握在PostgreSQL中编写高效存储过程与函数的必备技能,提升数据库逻辑处理能力。
|
3月前
|
SQL 存储 关系型数据库
PostgreSQL SQL函数语法详解
本文深入讲解PostgreSQL中SQL语言函数的编写,涵盖参数引用、返回类型(基类型/复合类型/集合)、输出参数、可变参数、默认值、多态函数及排序规则等核心特性,系统阐述其语法、行为与最佳实践。
|
2月前
|
运维 Linux 应用服务中间件
Linux sort命令实战教程
本文详解Linux `sort`命令:从基础排序、数字/反向/去重/忽略大小写等常用参数,到按字段(-k)、分隔符(-t)、月份(-M)、人类可读大小(-h)等进阶用法,并附日志、CSV、文件大小等实战案例。通俗易懂,示例可直接运行,新手也能快速掌握。
|
3月前
|
SQL 存储 缓存
PL/pgSQL 入门教程(六):从避坑到吃透,聊聊事务、错误处理和底层那些事儿
本文深度解析PL/pgSQL开发避坑指南:详解RAISE多级错误处理与USING增强提示、EXCEPTION事务恢复机制、变量替换限制与计划缓存陷阱,并分享美元符引号、CREATE OR REPLACE调试、extra_warnings预警等实战技巧,助你写出健壮高效存储过程。
|
3月前
|
SQL 监控 关系型数据库
PL/pgSQL 入门教程(五):触发器
PostgreSQL触发器是数据库的“自动服务员”,可在INSERT/UPDATE/DELETE等操作时自动执行校验、日志记录、汇总更新等逻辑。支持BEFORE/AFTER/INSTEAD OF时机,ROW/STATEMENT级别,配合NEW/OLD变量实现灵活数据管控,大幅提升数据一致性与运维效率。
|
3月前
|
数据库 C++ Perl
PL/pgSQL 入门教程(三):控制结构
本文详解PL/pgSQL核心编程:函数返回(RETURN单值、RETURN NEXT/QUERY多行)、条件判断(IF/CASE)、循环控制(LOOP/WHILE/FOR/FOREACH)及异常处理(EXCEPTION),附丰富示例与最佳实践,助你写出健壮高效的数据库逻辑。
|
3月前
|
SQL 缓存 安全
PL/pgSQL 入门教程(二):表达式和基础语句
本文详解PL/pgSQL核心语法:表达式由主SQL引擎以参数化SELECT执行,支持计划缓存;基础语句涵盖赋值(:=/=)、静态/动态SQL执行(INTO/PERFORM/EXECUTE)、结果处理(STRICT模式)、状态获取(FOUND/GET DIAGNOSTICS)及空操作NULL。
|
3月前
|
搜索推荐 关系型数据库 大数据
PL/pgSQL 入门教程(四):使用游标(cursor)
游标是PostgreSQL中“按需取数”的数据指针,避免大查询内存溢出;支持逐行处理、动态查询、精准更新/删除及函数返回大结果集。分未绑定(灵活)与绑定(固定)两类,核心操作为声明→打开→FETCH/MOVE/UPDATE→关闭,FOR循环可自动简化遍历。
|
3月前
|
Windows 自然语言处理
Ollama Modelfile 详细使用手册
想用Ollama打造专属模型?Modelfile就是你的“模型食谱”!本文以做菜为喻,零基础手把手教你写Modelfile:FROM选基模、PARAMETER调温度/记忆、SYSTEM定角色(如马里奥)、TEMPLATE规范格式、MESSAGE给示例。全程无术语,附实操步骤与避坑指南,看完即能创建并运行自己的第一个自定义模型。
|
3月前
|
SQL 关系型数据库 C语言
用C语言开发PostgreSQL用户自定义函数之内部函数调用
本教程详解如何用C语言开发PostgreSQL的Numeric类型自定义函数,涵盖加法、可变参数求和、默认值处理、数组求和及加权求和,深入讲解Datum、ArrayType、内存管理与内置函数调用,提升高性能计算能力。

热门文章

最新文章

下一篇
开通oss服务