我有一个这种形式的 PostgreSQL 表:
base_id int | mods smallint[]
3 | {7,15,48}
我需要填充这种形式的表格:
combo_id int | base_id int | mods smallint[]
1 | 3 |
2 | 3 | {7}
3 | 3 | {7,15}
4 | 3 | {7,48}
5 | 3 | {7,15,48}
6 | 3 | {15}
7 | 3 | {15,48}
8 | 3 | {48}
我认为我可以使用几乎完全相同的函数来完成此操作,遍历第一个表并将组合写入第二个表: Generate all combinations in SQL
但是,我是一个 Postgres 新手,我一辈子都弄不明白如何使用 plpgsql 来做到这一点。它不需要特别快;它只会在后端定期运行。第一个表有大约 80 条记录,粗略计算表明我们可以预期第二个表有大约 2600 条记录。
至少有人能指出我正确的方向吗?
编辑: Craig:我有 PostgreSQL 9.0。我成功地使用了 UNNEST():
FOR messvar IN SELECT * FROM UNNEST(mods) AS mod WHERE mod BETWEEN 0 AND POWER(2, @n) - 1
LOOP
RAISE NOTICE '%', messvar;
END LOOP;
但不知道下一步该去哪里。
编辑:作为引用,我最终使用了 Erwin 的解决方案,添加了一行以向每个集合添加空结果('{}'),并删除了 Erwin 所指的特殊情况:
CREATE OR REPLACE FUNCTION f_combos(_arr integer[], _a integer[] DEFAULT '{}'::integer[], _z integer[] DEFAULT '{}'::integer[])
RETURNS SETOF integer[] LANGUAGE plpgsql AS
$BODY$
DECLARE
i int;
j int;
_up int;
BEGIN
IF array_length(_arr,1) > 0 THEN
_up := array_upper(_arr, 1);
IF _a = '{}' AND _z = '{}' THEN RETURN QUERY SELECT '{}'::int[]; END IF;
FOR i IN array_lower(_arr, 1) .. _up LOOP
FOR j IN i .. _up LOOP
CASE j-i
WHEN 0,1 THEN
RETURN NEXT _a || _arr[i:j] || _z;
ELSE
RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
RETURN QUERY SELECT *
FROM f_combos(_arr[i+1:j-1], _a || _arr[i], _arr[j] || _z);
END CASE;
END LOOP;
END LOOP;
ELSE
RETURN NEXT _arr;
END IF;
END;
$BODY$
然后,我使用该函数来填充我的表:
INSERT INTO e_ecosystem_modified (ide_ecosystem, modifiers)
(SELECT ide_ecosystem, f_combos(modifiers) AS modifiers FROM e_ecosystem WHERE ecosystemgroup <> 'modifier' ORDER BY ide_ecosystem, modifiers);
我的源表中有 79 行,修饰符数组中最多有 7 个项目,查询用了 250 毫秒来填充我的输出表中的 2630 行。太棒了。
最佳答案
在我沉思之后,我有了一个全新的、更简单、更快的想法:
CREATE OR REPLACE FUNCTION f_combos(_arr anyarray)
RETURNS TABLE (combo anyarray) LANGUAGE plpgsql AS
$BODY$
BEGIN
IF array_upper(_arr, 1) IS NULL THEN
combo := _arr; RETURN NEXT; RETURN;
END IF;
CASE array_upper(_arr, 1)
-- WHEN 0 THEN -- does not exist
WHEN 1 THEN
RETURN QUERY VALUES ('{}'), (_arr);
WHEN 2 THEN
RETURN QUERY VALUES ('{}'), (_arr[1:1]), (_arr), (_arr[2:2]);
ELSE
RETURN QUERY
WITH x AS (
SELECT f.combo FROM f_combos(_arr[1:array_upper(_arr, 1)-1]) f
)
SELECT x.combo FROM x
UNION ALL
SELECT x.combo || _arr[array_upper(_arr, 1)] FROM x;
END CASE;
END
$BODY$;
调用:
SELECT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;
512 行,总运行时间:2.899 毫秒
解释
- 用
NULL
和空数组处理特殊情况。 - 构建两个原始数组的组合。
- 任何更长的数组被分解成:
- 相同长度n-1数组的组合
- 加上所有与元素 n 组合的元素 .. 递归。
非常简单,一旦掌握。
- 适用于以下标 1 开头的一维整数数组(见下文)。
- 速度是旧解决方案的 2-3 倍,扩展性更好。
- 再次适用于任何元素类型(使用多态类型)。
- 在问题中显示的结果中包含空数组(正如@Craig 在评论中向我指出的那样)。
- 更短、更优雅。
假设 array subscripts从 1(默认)开始。如果您不确定自己的值,请像这样调用函数来规范化:
SELECT * FROM f_combos(_arr[array_lower(_arr, 1):array_upper(_arr, 1)]);
不确定是否有更优雅的方法来规范化数组下标。我发布了一个问题:
Normalize array subscripts for 1-dimensional array so they start with 1
旧解决方案(较慢)
CREATE OR REPLACE FUNCTION f_combos2(_arr int[], _a int[] = '{}', _z int[] = '{}')
RETURNS SETOF int[] LANGUAGE plpgsql AS
$BODY$
DECLARE
i int;
j int;
_up int;
BEGIN
IF array_length(_arr,1) > 0 THEN
_up := array_upper(_arr, 1);
FOR i IN array_lower(_arr, 1) .. _up LOOP
FOR j IN i .. _up LOOP
CASE j-i
WHEN 0,1 THEN
RETURN NEXT _a || _arr[i:j] || _z;
WHEN 2 THEN
RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
RETURN NEXT _a || _arr[i:j] || _z;
ELSE
RETURN NEXT _a || _arr[i:i] || _arr[j:j] || _z;
RETURN QUERY SELECT *
FROM f_combos2(_arr[i+1:j-1], _a || _arr[i], _arr[j] || _z);
END CASE;
END LOOP;
END LOOP;
ELSE
RETURN NEXT _arr;
END IF;
END;
$BODY$;
调用:
SELECT * FROM f_combos2('{7,15,48}'::int[]) ORDER BY 1;
适用于一维整数数组。
这可以进一步优化,但对于这个问题的范围来说肯定不需要。
ORDER BY
强加问题中显示的顺序。
提供 NULL 或空数组,因为注释中提到了 NULL
。
已使用 PostgreSQL 9.1 进行测试,但应该适用于任何半现代版本。
array_lower()
and array_upper()
至少从 PostgreSQL 7.4 开始就存在了。只有参数默认值是 8.4 版中的新增功能。可以轻松更换。
性能不错。
SELECT DISTINCT * FROM f_combos('{1,2,3,4,5,6,7,8,9}'::int[]) ORDER BY 1;
511 行,总运行时间:7.729 毫秒
说明
它建立在这种简单形式之上,它只创建相邻元素的所有组合:
CREATE FUNCTION f_combos(_arr int[])
RETURNS SETOF int[] LANGUAGE plpgsql AS
$BODY$
DECLARE
i int;
j int;
_up int;
BEGIN
_up := array_upper(_arr, 1);
FOR i in array_lower(_arr, 1) .. _up LOOP
FOR j in i .. _up LOOP
RETURN NEXT _arr[i:j];
END LOOP;
END LOOP;
END;
$BODY$;
但是对于包含两个以上元素的子数组,这将失败。所以:
对于任何具有 3 个元素的子数组,添加一个仅包含外部两个元素的数组。这是这种提高性能的特殊情况的快捷方式,并非严格需要。
对于超过 3 个元素的任何子数组,我采用外部两个元素并填充由同一函数构建的内部元素的所有组合 递归。
关于postgresql - 如何在 postgres 中编写组合函数?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11997037/