在窗口/聚合函数中调用非纯函数是否安全?以nextval为例
我想给source表的每组行分配一个序列生成的ID,后续要把这些行插入一张表,同时把每组的聚合数据插入另一张表——前者通过外键关联后者,这个ID作为聚合表的主键。我不想先创建聚合数据,因为这样得再次关联source表获取ID,实际项目里涉及的表达式很复杂。
我想到的办法是:不给每组分配ID,而是给source表的每一行都取序列ID,再通过分区取最小值。ID浪费一些我不在乎:
create temporary sequence foo_seq start 1000; create temporary table source(id, data) as ( values (1, 'a'), (2, 'b'), (3, 'b'), (4, 'c'), (5, 'c'), (6, 'c')); select id, data, min(nextval('foo_seq'::regclass)) over (partition by data) as group_id from source;
这种操作安全吗?能不能保证同一分组里的每行,min(nextval('foo_seq'::regclass))输出的都是同一个值?我担心某些场景下min会重新计算全组,而nextval是非纯函数(有副作用),纯函数就没这问题。示例里看起来是可行的:
id | data | group_id ----+------+---------- 1 | a | 1000 2 | b | 1001 3 | b | 1001 4 | c | 1003 5 | c | 1003 6 | c | 1003
这种方法不安全,无法保证同一分组内的group_id值完全一致。
PostgreSQL在处理窗口函数时,对于非纯函数(比如nextval)的执行时机没有严格的确定性规范。虽然测试案例里看起来正常,但实际执行时,数据库可能会因为查询优化、并行执行或其他内部逻辑,多次调用nextval计算窗口函数的值,导致同一分组内的min(nextval(...))得到不同结果,甚至出现分组内group_id不一致的情况。
nextval是有副作用的函数,每次调用都会消耗序列的一个值;而窗口函数的计算逻辑中,数据库可能为了效率或其他原因重复计算窗口内的函数值,这会导致同一分组生成多个不同的序列值——哪怕min会取最小的那个,不仅会浪费更多ID,更关键的是无法保证分组内所有行拿到同一个group_id。
更稳妥的方案是先给每个分组分配唯一序列ID,比如通过GROUP BY获取所有分组,生成ID后再关联回原表:
create temporary sequence foo_seq start 1000; create temporary table source(id, data) as ( values (1, 'a'), (2, 'b'), (3, 'b'), (4, 'c'), (5, 'c'), (6, 'c')); -- 先给每个分组分配ID create temporary table group_ids as select data, nextval('foo_seq'::regclass) as group_id from source group by data; -- 关联原表获取每行对应的group_id select s.id, s.data, g.group_id from source s join group_ids g on s.data = g.data;
这种方式能确保每个分组只有一个唯一的group_id,逻辑稳定可靠,适合复杂场景。
内容的提问来源于stack exchange,提问作者Feuermurmel

