Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CN/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、视图只读]
** xref:master/oracle_compatibility/with_function_procedure.adoc[21、WITH FUNCTION/PROCEDURE]
** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、索引 ONLINE 参数]
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG 函数]
* 容器化与云服务
** 容器化指南
*** xref:master/containerization/k8s_deployment.adoc[K8S部署]
Expand Down Expand Up @@ -101,6 +102,7 @@
**** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context]
**** xref:master/oracle_builtin_functions/userenv.adoc[userenv]
**** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex]
**** xref:master/oracle_builtin_functions/stragg.adoc[stragg]
*** xref:master/gb18030.adoc[国标GB18030]
* 参考指南
** xref:master/tools_reference.adoc[工具参考]
Expand Down
139 changes: 139 additions & 0 deletions CN/modules/ROOT/pages/master/oracle_builtin_functions/stragg.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
:sectnums:
:sectnumlevels: 5

:imagesdir: ./_images

= STRAGG 聚合函数的实现

== 目的

IvorySQL 在 `contrib/ivorysql_ora` 扩展中新增 `sys.stragg(text)` 聚合函数,
实现与 Oracle 同名函数一致的字符串聚合行为:将组内所有非 NULL 值用逗号连接为一个字符串。

== 实现说明

=== 状态布局设计

STRAGG 的聚合状态复用 PostgreSQL 内置 `string_agg` 所使用的 `StringInfo` 布局,
从而可以直接引用 `string_agg_finalfn`、`string_agg_combine`、
`string_agg_serialize`、`string_agg_deserialize` 四个内置函数,
无需另行实现 finalize、并行合并及序列化逻辑。

`StringInfo` 状态的约定如下:

[source,c]
----
/*
* data = "," + val1 + "," + val2 + ...
* 首元素前也预置一个逗号,便于 finalfn 统一处理
* cursor = 1
* 记录前导分隔符的字节长度(逗号占 1 字节)
* string_agg_finalfn 返回 &data[cursor],即自动去掉前导逗号
*/
----

=== 转换函数(`stragg_transfn`)

转换函数位于
`contrib/ivorysql_ora/src/builtin_functions/misc_functions.c`。

[source,c]
----
PG_FUNCTION_INFO_V1(stragg_transfn);

Datum
stragg_transfn(PG_FUNCTION_ARGS)
{
StringInfo state;
MemoryContext aggcontext;
MemoryContext oldcontext;

if (!AggCheckCallContext(fcinfo, &aggcontext))
elog(ERROR, "stragg_transfn called in non-aggregate context");

state = PG_ARGISNULL(0) ? NULL : (StringInfo) PG_GETARG_POINTER(0);

/* 跳过 NULL 输入,与 Oracle STRAGG 行为一致 */
if (!PG_ARGISNULL(1))
{
text *value = PG_GETARG_TEXT_PP(1);

if (state == NULL)
{
oldcontext = MemoryContextSwitchTo(aggcontext);
state = makeStringInfo();
MemoryContextSwitchTo(oldcontext);

/* 首个值:预置分隔符,cursor 记录其长度 */
appendStringInfoChar(state, ',');
state->cursor = 1;
}
else
{
appendStringInfoChar(state, ',');
}

appendBinaryStringInfo(state, VARDATA_ANY(value), VARSIZE_ANY_EXHDR(value));
}

if (state)
PG_RETURN_POINTER(state);
PG_RETURN_NULL();
}
----

关键设计点:

* 状态在聚合上下文(`aggcontext`)中分配,生命周期覆盖整个聚合过程。
* `StringInfo` 内部缓冲区按需翻倍扩容,追加操作均摊 O(1),总时间复杂度 O(N),
优于纯 SQL 拼接方案的 O(N²)。
* NULL 输入由 `PG_ARGISNULL(1)` 判断并跳过,状态不受污染。

=== SQL 定义

转换函数和聚合定义位于
`contrib/ivorysql_ora/src/builtin_functions/builtin_functions--1.0.sql`。

[source,sql]
----
CREATE FUNCTION sys.stragg_transfn(internal, text)
RETURNS internal
AS 'MODULE_PATHNAME', 'stragg_transfn'
LANGUAGE C
CALLED ON NULL INPUT
PARALLEL SAFE;

CREATE AGGREGATE sys.stragg(text) (
SFUNC = sys.stragg_transfn,
STYPE = internal,
FINALFUNC = string_agg_finalfn,
COMBINEFUNC = string_agg_combine,
SERIALFUNC = string_agg_serialize,
DESERIALFUNC = string_agg_deserialize,
PARALLEL = SAFE
);
----

`FINALFUNC`、`COMBINEFUNC`、`SERIALFUNC`、`DESERIALFUNC` 均直接引用 PostgreSQL
内置的 `string_agg` 系列函数,因为 STRAGG 与 `string_agg` 使用完全相同的
`StringInfo` 状态格式。

=== 并行聚合支持

通过指定 `COMBINEFUNC = string_agg_combine` 和序列化/反序列化函数,
STRAGG 支持 PostgreSQL 的并行聚合执行路径。各并行工作进程独立维护局部
`StringInfo` 状态,最终由 leader 进程通过 `string_agg_combine` 合并。

=== 回归测试

测试文件:`contrib/ivorysql_ora/sql/ora_stragg.sql`,
对应预期输出:`contrib/ivorysql_ora/expected/ora_stragg.out`。

在 `Makefile` 的 `ORA_REGRESS` 列表中已加入 `ora_stragg`,
可通过以下命令运行:

[source,bash]
----
cd contrib/ivorysql_ora
make installcheck
----
172 changes: 172 additions & 0 deletions CN/modules/ROOT/pages/master/oracle_compatibility/compat_stragg.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
:sectnums:
:sectnumlevels: 5

:imagesdir: ./_images

= STRAGG 聚合函数

== 目的

本文档说明 IvorySQL 中 `sys.stragg` 聚合函数的功能,该函数以兼容 Oracle 的方式
将组内多行字符串值拼接为一个逗号分隔的字符串。

== 功能说明

* `sys.stragg(text)` 是一个聚合函数,将同一分组中所有非 NULL 的文本值用逗号(`,`)连接。
* NULL 值被自动忽略,不出现在结果中,也不产生多余的逗号。
* 分组内所有值均为 NULL 或分组为空集时,返回 `NULL`(而非空字符串)。
* 不保证输出顺序;如需确定顺序,可在聚合调用中使用 `ORDER BY` 子句(PostgreSQL 扩展语法)。
* 函数定义于 `sys` 模式,在 Oracle 兼容模式(不需要sys前缀)和 PG 模式下均可使用。

== 语法

[source,sql]
----
sys.stragg(expr [ORDER BY sort_expr [ASC | DESC] [, ...]])
----

[cols="1,3"]
|===
|参数 |说明

|`expr`
|任意可隐式转换为 `text` 的表达式;NULL 值将被跳过。

|`ORDER BY`
|可选,指定组内拼接顺序。省略时顺序不确定。
|===

返回类型:`text`

== 测试用例

=== 测试环境准备

[source,sql]
----
CREATE TABLE stragg_test (dept TEXT, name TEXT);
INSERT INTO stragg_test VALUES ('HR', 'Alice');
INSERT INTO stragg_test VALUES ('HR', 'Bob');
INSERT INTO stragg_test VALUES ('HR', 'Carol');
INSERT INTO stragg_test VALUES ('IT', 'Dave');
INSERT INTO stragg_test VALUES ('IT', 'Eve');
----

=== 基础聚合

[source,sql]
----
-- 单组聚合,使用 ORDER BY 保证结果确定
SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR';
-- 期望:Alice,Bob,Carol

-- 配合 GROUP BY 按部门分组
SELECT dept, sys.stragg(name ORDER BY name)
FROM stragg_test
GROUP BY dept
ORDER BY dept;
-- 期望:
-- HR | Alice,Bob,Carol
-- IT | Dave,Eve
----

=== NULL 值处理

[source,sql]
----
-- 插入一行 NULL 值
INSERT INTO stragg_test VALUES ('HR', NULL);

-- NULL 被忽略,结果与无 NULL 时相同
SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR';
-- 期望:Alice,Bob,Carol

-- 所有值均为 NULL 时返回 NULL
SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE name IS NULL;
-- 期望:t
----

=== 空集处理

[source,sql]
----
-- 无匹配行时返回 NULL
SELECT sys.stragg(name) IS NULL FROM stragg_test WHERE dept = 'NONE';
-- 期望:t
----

=== 单值

[source,sql]
----
-- 组内只有一个值时,结果中不含逗号
SELECT sys.stragg(name) FROM stragg_test WHERE dept = 'IT' AND name = 'Dave';
-- 期望:Dave
----

=== PG 兼容模式下使用

[source,sql]
----
SET ivorysql.compatible_mode = pg;
SELECT sys.stragg(name ORDER BY name) FROM stragg_test WHERE dept = 'HR';
-- 期望:Alice,Bob,Carol
RESET ivorysql.compatible_mode;
----

=== 测试环境清理

[source,sql]
----
DROP TABLE stragg_test;
----

== 与 Oracle 的行为差异

[cols="1,2,2"]
|===
|场景 |Oracle STRAGG |IvorySQL sys.stragg

|空集
|`NULL`
|`NULL`(一致)

|全 NULL 分组
|`NULL`
|`NULL`(一致)

|NULL 值
|忽略
|忽略(一致)

|拼接顺序
|不确定
|不确定;可用 `ORDER BY` 子句指定

|`ORDER BY` 子句
|不支持(官方无此语法)
|支持(PostgreSQL 聚合扩展语法)
|===

== 与 LISTAGG 的比较

[cols="1,2,2"]
|===
|特性 |`LISTAGG`(Oracle / IvorySQL)|`STRAGG`(Oracle / IvorySQL)

|分隔符
|可指定任意分隔符
|固定为逗号

|`ORDER BY`
|通过 `WITHIN GROUP (ORDER BY ...)` 指定
|不保证顺序(IvorySQL 支持 PostgreSQL 聚合 `ORDER BY`)

|结果长度限制
|Oracle 限制 4000 字节(IvorySQL 通过 `ora_listagg_check` 检查)
|无限制

|使用场景
|需要精确控制分隔符和顺序的场合
|快速拼接,历史遗留代码迁移
|===
2 changes: 2 additions & 0 deletions EN/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
** xref:master/oracle_compatibility/compat_read_only_view.adoc[20、Read Only View]
** xref:master/oracle_compatibility/with_function_procedure_en.adoc[21、WITH FUNCTION/PROCEDURE]
** xref:master/oracle_compatibility/compat_create_index_online.adoc[22、ONLINE Parameter for CREATE INDEX]
** xref:master/oracle_compatibility/compat_stragg.adoc[23、STRAGG function]
* Containerization and Cloud Service
** Containerization
*** xref:master/containerization/k8s_deployment.adoc[K8S deployment]
Expand Down Expand Up @@ -101,6 +102,7 @@
*** xref:master/oracle_builtin_functions/sys_context.adoc[sys_context]
*** xref:master/oracle_builtin_functions/userenv.adoc[userenv]
*** xref:master/oracle_builtin_functions/rawtohex.adoc[rawtohex]
*** xref:master/oracle_builtin_functions/stragg.adoc[stragg]
** xref:master/gb18030.adoc[GB18030 Character Set]
* Reference
** xref:master/tools_reference.adoc[Tool Reference]
Expand Down
Loading
Loading