一、先搞懂核心变化
上篇用C语言开发PostgreSQL用户自定义函数之数据查询篇学的方法需要先手动CREATE TYPE定义返回类型,就像先给“快递盒”定制一个专属尺寸;本篇介绍几种更省事的新方法,基于一套C语言代码,不用提前创建返回类型,通过定义不同的参数类型、返回类型达到目的。
核心逻辑没变:还是用SPI查数据库,用SRF返回多行数据;唯一的区别是数据库里注册函数时的返回类型写法,C语言代码几乎不用改。
二、傻瓜式教程:免CREATE TYPE的完整实现
第一步:C语言代码
PG_FUNCTION_INFO_V1(get_all_empsalary2);
Datum
get_all_empsalary2(PG_FUNCTION_ARGS)
{
ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
Tuplestorestate *tupstore;
TupleDesc result_tupdesc;
SPIPlanPtr plan;
int spi_rc;
PG_TRY();
{
InitMaterializedSRF(fcinfo, 0);
/* 连接到SPI管理器 */
if (SPI_connect() != SPI_OK_CONNECT)
elog(ERROR, "get_all_empsalary2: SPI_connect failed");
/* 准备查询 */
if ((plan = SPI_prepare("SELECT depname, empno, salary, enroll_date FROM "
"empsalary ORDER BY empno",
0, NULL)) == NULL)
elog(ERROR, "get_all_empsalary2: SPI_prepare failed");
/* 执行查询 */
spi_rc = SPI_execute_plan(plan, NULL, NULL, true, 0);
if (spi_rc != SPI_OK_SELECT)
elog(ERROR, "get_all_empsalary2: SPI_execute_plan failed");
/* 检查是否有数据 */
if (SPI_processed == 0) {
SPI_finish();
PG_RETURN_NULL();
}
uint64 proc = SPI_processed;
SPITupleTable *spi_tuptable = SPI_tuptable;
TupleDesc spi_tupdesc = spi_tuptable->tupdesc;
for (uint64 i = 0; i < proc; i++) {
Datum values[4];
bool nulls[4];
memset(values, 0, sizeof(values));
memset(nulls, 0, sizeof(nulls));
HeapTuple tuple = spi_tuptable->vals[i];
char *p = SPI_getvalue(tuple, spi_tupdesc, 1);
if(p != NULL)
{
//返回的是text类型,所以要用CStringGetTextDatum
//这里可千万别用CStringGetDatum
values[0] = CStringGetTextDatum(p);
values[0] = CStringGetTextDatum(p);
}
else
nulls[0] = true;
p = SPI_getvalue(tuple, spi_tupdesc, 2);
if(p != NULL)
{
int32 empno = pg_strtoint32(p);
values[1] = Int32GetDatum(empno);
}
else{
nulls[1] = true;
}
p = SPI_getvalue(tuple, spi_tupdesc, 3);
if(p != NULL)
{
int salary = pg_strtoint16(p);
values[2] = Int16GetDatum(salary);
}
else
nulls[2] = true;
p = SPI_getvalue(tuple, spi_tupdesc, 4);
if(p != NULL)
values[3] = DirectFunctionCall1(date_in, CStringGetDatum(p));
else
nulls[3] = true;
tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
}
SPI_finish();
}
PG_CATCH();
{
ereport(NOTICE, errmsg("catch error in acct_stat"));
PG_RE_THROW();
}
PG_END_TRY();
PG_RETURN_NULL();
}
用到的新函数讲解:
InitMaterializedSRF
这是一个辅助函数,用于构建在物化模式下单次调用场景中使用的集合返回函数的状态。此代码包含对 ReturnSetInfo 的完整性检查,创建与该函数一起使用的 Tuplestore和 TupleDesc,并将它们存储到函数的 ReturnSetInfo 中。
可通过设置 "flags" 为 MAT_SRF_USE_EXPECTED_DESC,来使用来自 expectedDesc的元组描述符(即调用方期望的元组描述符)。也可设置 MAT_SRF_BLESS 以完善与元组描述符相关的信息,这在某些元组描述符来自瞬态 RECORD 数据类型的情况下是必需的。tuplestore_putvalues
这个就不解释了,就是把查询结果塞到tupstore里。
第二步:编译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
第三步:数据库注册函数(关键区别在这!)
不用先CREATE TYPE了,直接在RETURNS table()里写清楚返回的列名和类型,一步到位:
-- 直接创建函数,无需提前建类型!
CREATE OR REPLACE FUNCTION get_all_empsalary2()
RETURNS table (
depname text, -- 列1:名称+类型
empno int, -- 列2:名称+类型(注意和SQL查询的类型匹配)
salary int, -- 列3:名称+类型
enroll_date date -- 列4:名称+类型
)
AS '$libdir/spi_demo.1.0', 'get_all_empsalary2' -- 第二个参数是C函数名
LANGUAGE C STRICT;
第四步:调用函数(和之前一样)
执行SQL就能看到结果,和手动查empsalary表完全一致:
SELECT * FROM get_all_empsalary2();
以上是第二种方法,还有别的方法吗?当然有,C语言代码不变,SQL代码如下:
CREATE OR REPLACE FUNCTION get_all_empsalary3()
RETURNS SETOF record -- 核心:返回匿名记录集合
AS '$libdir/spi_demo.1.0', 'get_all_empsalary2' -- C函数名对应
LANGUAGE C STRICT;
-- 格式:SELECT * FROM 函数名() AS 别名(列名1 类型1, 列名2 类型2,...)
SELECT * FROM get_all_empsalary3() AS emp(
depname text,
empno bigint,
salary int,
enroll_date date
);
以上是第三种方法:RETURNS SETOF record(匿名记录)然后用AS指定列。还有第四种吗?继续往下看:
CREATE OR REPLACE FUNCTION get_all_empsalary4(
OUT depname text, -- OUT参数:声明这是返回列
OUT empno bigint,
OUT salary int,
OUT enroll_date date
)
RETURNS SETOF record -- 搭配返回SETOF record
AS '$libdir/spi_demo.1.0', 'get_all_empsalary4'
LANGUAGE C STRICT;
-- 直接调用就行
SELECT * FROM get_all_empsalary4();
4种SRF返回数据集方法终极对比
一、先给核心结论
「CREATE TYPE+SETOF」「RETURNS TABLE」「SETOF record」「OUT参数+SETOF record」这4种方法,底层都是靠PostgreSQL的SRF(返回集合函数)机制逐行返回数据,C语言代码逻辑完全一致,唯一区别是「在SQL里声明函数返回列结构的方式」。下面用“人话+表格+实操例子”把差异讲透,帮你一眼选对。
二、4种方法核心对比(大白话表格)
| 对比维度 | CREATE TYPE+SETOF | RETURNS TABLE | SETOF record(匿名) | OUT参数+SETOF record |
|---|---|---|---|---|
| 核心写法 | 1. 先建自定义类型 2. 函数返回 SETOF 类型 |
函数直接写RETURNS TABLE(列名 类型,...) |
函数返回SETOF record,调用时指定列 |
1. 函数加OUT参数声明列2. 返回 SETOF record |
| 步骤数量 | 两步(建类型+建函数) | 一步(直接建函数) | 一步(建函数)+ 调用加列 | 一步(直接建函数) |
| 列结构可见性 | 类型名和列结构分离,需查类型定义 | 列结构写在函数里,直观易读 | 列结构仅在调用时可见,函数里无信息 | 列结构写在参数里,直观易读 |
| 调用方式 | 简单:SELECT * FROM 函数() |
简单:SELECT * FROM 函数() |
复杂:SELECT * FROM 函数() AS 别名(列...) |
简单:SELECT * FROM 函数() |
| 复用性 | 高(一个类型可被多个函数复用) | 低(列结构仅属于当前函数) | 无(每次调用都要写列) | 低(列结构仅属于当前函数) |
| 新手友好度 | ★★★☆☆(多一步建类型) | ★★★★★(最优) | ★★☆☆☆(调用易出错) | ★★★★☆(和TABLE几乎一样) |
| 典型适用场景 | 多个函数返回相同列结构(如统一的“员工薪资”结构) | 单个函数专用列结构(90%的新手场景) | 动态列结构/临时测试(极少用) | 习惯用“参数”声明返回列(个人编码风格) |
三、每种方法的实操代码(最简示例)
1. CREATE TYPE+SETOF(复用型)
-- 第一步:先建类型(可被多个函数复用)
CREATE TYPE empsalary_type AS (
depname text,
empno bigint,
salary int,
enroll_date date
);
-- 第二步:创建函数
CREATE OR REPLACE FUNCTION get_all_empsalary1()
RETURNS SETOF empsalary_type
AS '$libdir/spi_demo.1.0', 'get_all_empsalary1'
LANGUAGE C STRICT;
-- 调用
SELECT * FROM get_all_empsalary1();
2. RETURNS TABLE(新手首选)
-- 一步到位,列结构写在函数里
CREATE OR REPLACE FUNCTION get_all_empsalary2()
RETURNS table (
depname text,
empno bigint,
salary int,
enroll_date date
)
AS '$libdir/spi_demo.1.0', 'get_all_empsalary2'
LANGUAGE C STRICT;
-- 调用
SELECT * FROM get_all_empsalary2();
3. SETOF record(匿名型)
-- 仅声明返回匿名记录,无列信息
CREATE OR REPLACE FUNCTION get_all_empsalary3()
RETURNS SETOF record
AS '$libdir/spi_demo.1.0', 'get_all_empsalary3'
LANGUAGE C STRICT;
-- 调用(必须指定列,否则报错)
SELECT * FROM get_all_empsalary3() AS emp(
depname text,
empno bigint,
salary int,
enroll_date date
);
4. OUT参数+SETOF record(参数型)
-- 用OUT参数声明返回列,本质和TABLE一样
CREATE OR REPLACE FUNCTION get_all_empsalary4(
OUT depname text,
OUT empno bigint,
OUT salary int,
OUT enroll_date date
)
RETURNS SETOF record
AS '$libdir/spi_demo.1.0', 'get_all_empsalary4'
LANGUAGE C STRICT;
-- 调用
SELECT * FROM get_all_empsalary4();
👉 选方法的关键
- 是否复用列结构:只有
CREATE TYPE+SETOF能复用列结构,其他3种都是“函数专属”; - 调用是否额外操作:只有
SETOF record需要在调用时指定列,其他3种直接调用即可; - 写法风格差异:
RETURNS TABLE是“把列写在返回值里”,OUT参数是“把列写在参数里”,本质等价,仅编码风格不同。
新手选择建议(不用纠结)
- 优先选 RETURNS TABLE:一步到位、直观易读、调用简单,覆盖90%的场景(比如单个函数返回固定列结构);
- 次选 CREATE TYPE+SETOF:仅当多个函数需要返回相同列结构时用(比如5个函数都返回“员工薪资”4列),避免重复写列;
- OUT参数 作为备选:如果你习惯把返回列当成“输出参数”来写(比如有Java/C#编码习惯),可以用这种方式,和TABLE效果完全一样;
- SETOF record 尽量不用:调用时必须手动写列,容易写错,仅临时测试/动态列场景(比如返回列数不固定)才考虑。
总结
4种方法的核心是“换汤不换药”——C代码逻辑不变,只是SQL声明返回类型的方式不同。对新手来说,不用掌握所有方法,只要吃透RETURNS TABLE,就能应对几乎所有场景;其他方法只是不同场景下的“语法糖”而已。