SQL聚合函数选择唯一值

Posted

技术标签:

【中文标题】SQL聚合函数选择唯一值【英文标题】:SQL aggregation function to choose the only value 【发布时间】:2020-05-09 21:58:31 【问题描述】:

我有一个包含两列的行集:technical_idnatural_id。行集实际上是复杂查询的结果。假定列值之间的映射是双射的(即,对于具有相同 technical_id 的两行,natural_ids 也相同,对于不同的 technical_ids,natural_ids 也是不同的)。由于原始查询中的连接,(technical_id,natural_id) 对在行集中不是唯一的。示例:

with t (technical_id, natural_id, val) as (values
  (1, 'a', 1),
  (1, 'a', 2),
  (2, 'b', 3),
  (2, 'b', 2),
  (3, 'c', 0),
  (3, 'c', 1),
  (4, 'd', 1)
)

不幸的是,双射仅由应用程序逻辑强制执行。 natural_id 实际上是从多个表中收集的,并使用基于coalesce 的表达式组合而成,因此它的唯一性几乎无法通过 db 约束来强制执行。

假设natural_id 是唯一的,我需要通过technical_id 聚合行集的行。如果不是(例如,如果将元组 (4, 'x', 1) 添加到示例数据中),则查询应该失败。在理想的 SQL 世界中,我会使用一些假设的聚合函数:

select technical_id, only(natural_id), sum(val)
from t
group by technical_id;

我知道 SQL 中没有这样的功能。是否有一些替代方案或解决方法? Postgres 特有的解决方案也可以。

请注意,group by technical_id, natural_idselect technical_id, max(natural_id) - 尽管在愉快的情况下工作得很好 - 都是不可接受的(首先因为 technical_id 在所有情况下的结果中都必须是唯一的,其次因为该值可能是随机的并且掩盖了数据不一致)。

感谢您的提示:-)

更新:预期的答案是

technical_id,v,sum
1,a,3
2,b,5
3,c,1
4,d,1

4,x,1 也存在时失败。

【问题讨论】:

您能否通过显示示例数据的预期结果来扩展问题? 我不太愿意回答 - 但感觉您可能正在寻找 HAVING COUNT() 子句 【参考方案1】:

你可以使用

SELECT technical_id, max(natural_id), count(natural_id)
...
GROUP BY technical_id;

当计数不为 1 时抛出错误。

如果您想保证对数据库的约束,您可以执行以下操作之一:

    取消人工主键。

    做一些像这样复杂的事情:

    CREATE TABLE id_map (
       technical_id bigint UNIQUE NOT NULL,
       natural_id text UNIQUE NOT NULL,
       PRIMARY KEY (technical_id, natural_id)
    );
    
    ALTER TABLE t
       ADD FOREIGN KEY (technical_id, natural_id) REFERENCES id_map;
    

【讨论】:

谢谢,Laurenz,我同意保护唯一性的基于应用程序的解决方案仍在发挥作用,我只是对是否有一些纯 SQL 解决方案感兴趣。 t CTE 实际上不是一个表,所以我不能从中引用任何人工表,不考虑它会给应用程序带来维护另一个表 id_map 的负担。 那么我的第一个建议应该可以解决问题,对吧?另一种方法是在输出中抑制此类结果。 对不起,我更喜欢不需要更改数据库结构的解决方案。 我的意思是SELECTcount。这不需要您更改数据库中的任何内容。 我明白了,我的误解(我提到了你的第 1 点)。 Select with count 需要使用一些额外的逻辑对结果进行后处理。该查询是 ETL 管道的一部分,我不确定在出现两种不同的纯 SQL 解决方案后是否要更改它。【参考方案2】:

您只能使用以下方法获取“唯一”自然 ID:

select technical_id, max(natural_id), sum(val)
from t
group by technical_id
having min(natural_id) = max(natural_id);

如果您希望查询实际失败,这有点难以保证。这是一个 hacky 方法:

select technical_id, max(natural_id), sum(val)
from t
group by technical_id
having (case when min(natural_id) = max(natural_id) then 0 else 1 / (count(*) - count(*)) end) = 0;

还有一个 dbfiddle 说明 this。

【讨论】:

谢谢,戈登,是的,min=max 解决方案会将technical_ids 从结果中排除,我也不想要。但是除零技巧真的很酷!现在我赞成它,直到早上考虑是否我敢在代码可理解性方面将这种黑客添加到拉取请求中。【参考方案3】:

您可以创建自己的聚合。 ONLY 是关键字,因此最好不要将其用作聚合的名称。不愿意花太多时间做决定,我只称它为2。

CREATE OR REPLACE FUNCTION public.only_agg(anyelement, anyelement)
 RETURNS anyelement
 LANGUAGE plpgsql
 IMMUTABLE
AS $function$
BEGIN 
  if $1 is null then return $2; end if; 
  if $2 is null then return $1; end if; 
  if $1=$2 then return $1; end if; 
  raise exception 'not only';  
END $function$;

create aggregate only2 (anyelement) ( sfunc = only_agg, stype = anyelement);

它可能无法使用 NULL 输入做你想要的事情,但我不知道在这种情况下你想要什么。

【讨论】:

谢谢,jjanes,这绝对是一个有吸引力的解决方案,我会试试的。 为什么不使用language SQL 函数来执行简单的select coalesce($1, $2);?将比 PL/pgSQL 函数快很多(但如果两者都为空,则不能让它“失败”并出现异常) @a_horse_with_no_name 这是我的第一次尝试,但你不能从 SQL 抛出一个真正的异常(我知道)。甚至都很难除以零,因为不断折叠会在错误的时间发生这种情况。 @jjanes:为什么要抛出异常? 这是问题规范的一部分:“或者在 4,x,1 也存在时失败。”否则我只会使用 min 或 max,或者命名为“first”。【参考方案4】:

似乎我终于找到了基于 select 子句中相关子查询的单行基数的解决方案:

select technical_id,
       (select v from unnest(array_agg(distinct natural_id)) as u(v)) as natural_id,
       sum(val)
from t
group by technical_id;

这是我目前情况的最简单解决方案,所以我将诉诸自我接受。无论如何,如果出现一些缺点,我会在这里描述它们并重新接受其他答案。我感谢所有其他建议,并相信它们对任何人也很有价值。

【讨论】:

以上是关于SQL聚合函数选择唯一值的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server聚合函数

sql - 使用聚合函数(最小值/最大值)作为选择语句的一部分

不使用聚合函数 SQL 的最小值 [关闭]

SQL Server报错:选择列表中的列无效,因为该列没有包含在聚合函数或 GROUP BY 子句中

SQL Server 索引视图:无法创建聚集索引,因为选择列表包含聚合函数结果的表达式

sql server中啥是聚合函数