如何在 postgres 中编写组合函数?
Posted
技术标签:
【中文标题】如何在 postgres 中编写组合函数?【英文标题】:How to write combinatorics function in postgres? 【发布时间】:2012-08-16 23:50:31 【问题描述】:我有一个这种形式的 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 行。太棒了。
【问题讨论】:
这是用于排列,但很接近:wiki.postgresql.org/wiki/Permutations 在尝试任何事情之前:您使用的是哪个 PostgreSQL 版本?对于这样的问题至关重要,其中某些功能仅在较新版本中可用。例如,如果您没有 CTE 或 array_unnest,那么我会说“安装 plperl;用 plperl 编写”。 OK,9.0是合理的;您可以使用窗口函数、递归 CTE、数组取消嵌套等等。还有几个问题:集合总是 3 个元素吗?或者它是可变大小的?另外,是否需要以任何方式订购组合 ID? 感谢克雷格的帮助。数组是可变大小的,可以为空(在这种情况下,函数应该只写入一条记录,设置 base_id 但 mods 为空)。 combo_id 应该是一个序列号(我认为——我们在其他数据库中称之为自动增量)。 好的,我的意思是:combo_id 是否必须从每组组合的 1 开始并向上计数,就像在您的示例中一样?或者它可以是 any 唯一编号吗?如果它必须计数,你需要一个row_number
窗口函数;如果它必须是唯一的,可以使用普通的serial
。
【参考方案1】:
睡过之后,我有了一个全新的、更简单、更快的想法:
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 在 cmets 中向我指出的那样)。 更短,更优雅。这假定 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 或空数组,因为在 cmets 中提到了 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 个以上元素的子数组,我将 外部两个元素 并用由同一函数 构建的 内部元素的所有组合 填充递归。
【讨论】:
有趣。我们采用的两种方法在我的 9.1 系统上的性能相同,误差在 5% 以内,与元素数量的缩放比例完全相同。 @CraigRinger:在我的测试中,plpgsql 函数稍微快了约 10%。对于完全不同的方法,这非常接近。也许我放弃了多态类型以允许在函数定义中将空数组作为默认值这一事实有助于提高性能 - 以减少通用性为代价。除了您另外返回的空数组外,结果是相同的。检查SELECT * FROM combinations('1,2,3,4,5,6,7,8,9'::int[]) c(x) where x not IN ( SELECT * FROM f_combos('1,2,3,4,5,6,7,8,9'::int[]))
我故意返回空数组,因为
是一个有效的组合(并且'因为问题就是这样)。两者之间的另一个区别是使用空数组作为输入;你返回 ,我返回 null,所以你的更正确。至于速度,您的 PL/PgSQL 版本比这里快 5% 左右。速度的相似性简直是疯了。
@CraigRinger:我运行了更多的测试,性能随着数组元素数量的增加而不同。 20 个元素创建 1048575 组合,这在服务器上开始变得困难。几乎不再与问题的规格相关。这很有趣,该睡觉了,编码愉快!
我喜欢!同意,很好的改进。我使用相同的数据集进行了测试,并在 109 毫秒内得到了相同的结果。将其标记为答案,并且是一个非常好的答案!【参考方案2】:
一种方法是使用递归 CTE。不过,Erwin 更新后的递归函数明显更快且可扩展性更好,因此这作为一种有趣的不同方法非常有用。 Erwin 的更新版本更加实用。
我尝试了一种位计数方法(见最后),但没有一种快速的方法从数组中提取任意元素,结果证明它比递归方法慢。
递归 CTE 组合函数
CREATE OR REPLACE FUNCTION combinations(anyarray) RETURNS SETOF anyarray AS $$
WITH RECURSIVE
items AS (
SELECT row_number() OVER (ORDER BY item) AS rownum, item
FROM (SELECT unnest($1) AS item) unnested
),
q AS (
SELECT 1 AS i, $1[1:0] arr
UNION ALL
SELECT (i+1), CASE x
WHEN 1 THEN array_append(q.arr,(SELECT item FROM items WHERE rownum = i))
ELSE q.arr END
FROM generate_series(0,1) x CROSS JOIN q WHERE i <= array_upper($1,1)
)
SELECT q.arr AS mods
FROM q WHERE i = array_upper($1,1)+1;
$$ LANGUAGE 'sql';
它是一个多态函数,因此它适用于任何类型的数组。
逻辑是使用工作表迭代未嵌套输入集中的每个项目。从工作表中的一个空数组开始,代号为 1。对于输入集中的每个条目,将两个新数组插入到工作表中,并增加代号。两者中的一个是上一代输入数组的副本,另一个是输入数组中附加了输入集中第 (generation-number) 项的输入数组。当世代数超过输入集中的项数时,返回上一代。
用法
您可以使用combinations(smallint[])
函数产生您想要的结果,将它用作与row_number
窗口函数结合的集合返回函数。
-- assuming table structure
regress=# \d comb
Table "public.comb"
Column | Type | Modifiers
---------+------------+-----------
base_id | integer |
mods | smallint[] |
SELECT base_id, row_number() OVER (ORDER BY mod) AS mod_id, mod
FROM (SELECT base_id, combinations(mods) AS mod FROM comb WHERE base_id = 3) x
ORDER BY mod;
结果
regress=# SELECT base_id, row_number() OVER (ORDER BY mod) AS mod_id, mod
regress-# FROM (SELECT base_id, combinations(mods) AS mod FROM comb WHERE base_id = 3) x
regress-# ORDER BY mod;
base_id | mod_id | mod
---------+--------+-----------
3 | 1 |
3 | 2 | 7
3 | 3 | 7,15
3 | 4 | 7,15,48
3 | 5 | 7,48
3 | 6 | 15
3 | 7 | 15,48
3 | 8 | 48
(8 rows)
Time: 2.121 ms
零元素数组产生空结果。如果你想让combinations()
返回一行,那么
UNION ALL
和 就可以完成这项工作。
理论
您似乎想要the k-combinations for all k in a k-multicombination,而不是简单的组合。见number of combinations with repetition。
换句话说,您需要集合中元素的所有 k 组合,对于从 0 到 n 的所有 k,其中 n 是集合大小。
相关的 SO 问题:SQL - Find all possible combination,其中有关于位计数的非常有趣的答案。
Bit operations 存在于 Pg 中,因此应该可以使用 bit counting 方法。您希望它更高效,但由于从数组中选择分散的元素子集非常慢,它实际上运行起来更慢。
CREATE OR REPLACE FUNCTION bitwise_subarray(arr anyarray, elements integer)
RETURNS anyarray AS $$
SELECT array_agg($1[n+1])
FROM generate_series(0,array_upper($1,1)-1) n WHERE ($2>>n) & 1 = 1;
$$ LANGUAGE sql;
COMMENT ON FUNCTION bitwise_subarray(anyarray,integer) IS 'Return the elements from $1 where the corresponding bit in $2 is set';
CREATE OR REPLACE FUNCTION comb_bits(anyarray) RETURNS SETOF anyarray AS $$
SELECT bitwise_subarray($1, x)
FROM generate_series(0,pow(2,array_upper($1,1))::integer-1) x;
$$ LANGUAGE 'sql';
如果你能找到一种更快的方法来编写bitwise_subarray
,那么comb_bits
会非常快。比如,一个小的 C 扩展函数,但是 I'm only crazy enough to write one of those for an SO answer。
【讨论】:
@Erwin:感谢您的解决方案!像我这样的新手可以理解。注意我添加了IF _a = '' AND _z = '' THEN RETURN QUERY SELECT ''::int[]; END IF;
为每个集合添加一个空结果('')。另外,我发现当我移除特殊的 3 元素外壳时,我得到了相同的性能,所以我决定移除它。
@Kim:很有趣,感谢您的反馈。请考虑我的新解决方案。我想出了一个全新的、更简单的想法。以上是关于如何在 postgres 中编写组合函数?的主要内容,如果未能解决你的问题,请参考以下文章