【问题标题】:An aggregate function that only allows one unique input只允许一个唯一输入的聚合函数
【发布时间】:2011-05-23 22:07:46
【问题描述】:

我经常发现自己在group by 子句中添加了我确信是独一无二的表达式。有时结果证明我错了 - 因为我的 SQL 中的错误或错误的假设,而那个表达式并不是真正唯一的。

在很多情况下,我宁愿这会产生一个 SQL 错误,而不是默默地扩展我的结果集,有时甚至非常巧妙。

我希望能够做类似的事情:

select product_id, unique description from product group by product_id

但显然我自己无法实现 - 但可以在某些数据库上使用用户定义的聚合来实现几乎同样简洁的东西。

只允许一个唯一输入值的特殊聚合通常对所有版本的 SQL 都有帮助吗?如果是这样,现在可以在大多数数据库上实现这样的事情吗? null 值应该像任何其他值一样被考虑 - 与内置聚合 avg 通常工作的方式不同。 (我已经添加了针对 postgres 和 Oracle 的实现方式的答案。)

以下示例旨在说明如何使用聚合,但这是一个简单的情况,其中很明显哪些表达式应该是唯一的。实际使用更有可能是在较大的查询中,这样更容易对唯一性做出错误的假设

表格:

 product_id | description
------------+-------------
          1 | anvil
          2 | brick
          3 | clay
          4 | door

 sale_id | product_id |  cost
---------+------------+---------
       1 |          1 | £100.00
       2 |          1 | £101.00
       3 |          1 | £102.00
       4 |          2 |   £3.00
       5 |          2 |   £3.00
       6 |          2 |   £3.00
       7 |          3 |  £24.00
       8 |          3 |  £25.00

查询:

> select * from product join sale using (product_id);

 product_id | description | sale_id |  cost
------------+-------------+---------+---------
          1 | anvil       |       1 | £100.00
          1 | anvil       |       2 | £101.00
          1 | anvil       |       3 | £102.00
          2 | brick       |       4 |   £3.00
          2 | brick       |       5 |   £3.00
          2 | brick       |       6 |   £3.00
          3 | clay        |       7 |  £24.00
          3 | clay        |       8 |  £25.00

> select product_id, description, sum(cost) 
  from product join sale using (product_id) 
  group by product_id, description;

 product_id | description |   sum
------------+-------------+---------
          2 | brick       |   £9.00
          1 | anvil       | £303.00
          3 | clay        |  £49.00

> select product_id, solo(description), sum(cost) 
  from product join sale using (product_id) 
  group by product_id;

 product_id | solo  |   sum
------------+-------+---------
          1 | anvil | £303.00
          3 | clay  |  £49.00
          2 | brick |   £9.00

错误案例:

> select solo(description) from product;
ERROR:  This aggregate only allows one unique input

【问题讨论】:

  • 你用的是哪个数据库mysql,oracle,mysql有没有solo函数
  • @Rahul 这是一个一般性的 SQL 问题 - 我希望得到我不熟悉的数据库(postgres 和 Oracle)的答案

标签: sql mysql sql-server oracle postgresql


【解决方案1】:

这是我对 postgres 的实现(经过编辑以将 null 也视为唯一值):

create function solo_sfunc(inout anyarray, anyelement) 
       language plpgsql immutable as $$
begin
  if $1 is null then
    $1[1] := $2;
  else
    if ($1[1] is not null and $2 is null) 
         or ($1[1] is null and $2 is not null) 
         or ($1[1]!=$2) then 
      raise exception 'This aggregate only allows one unique input'; 
    end if;
  end if;
  return;
end;$$;

create function solo_ffunc(anyarray) returns anyelement 
       language plpgsql immutable as $$
begin
  return $1[1];
end;$$;

create aggregate solo(anyelement)
                     (sfunc=solo_sfunc, stype=anyarray, ffunc=solo_ffunc);

用于测试的示例表:

create table product(product_id integer primary key, description text);

insert into product(product_id, description)
values (1, 'anvil'), (2, 'brick'), (3, 'clay'), (4, 'door');

create table sale( sale_id serial primary key, 
                   product_id integer not null references product, 
                   cost money not null );

insert into sale(product_id, cost)
values (1, '100'::money), (1, '101'::money), (1, '102'::money),
       (2, '3'::money), (2, '3'::money), (2, '3'::money),
       (3, '24'::money), (3, '25'::money);

【讨论】:

  • 不错的函数,但它似乎返回一个输入类型的数组(低于 9.5),有什么方法可以让它准确返回输入类型?
【解决方案2】:

这是我对 Oracle 的实现 - 不幸的是,我认为每种基本类型都需要一个实现:

create type SoloNumberImpl as object
(
  val number, 
  flag char(1), 
  static function ODCIAggregateInitialize(sctx in out SoloNumberImpl) 
         return number,
  member function ODCIAggregateIterate( self in out SoloNumberImpl, 
                                        value in number )
         return number,
  member function ODCIAggregateTerminate( self in SoloNumberImpl, 
                                          returnValue out number, 
                                          flags in number ) 
         return number,
  member function ODCIAggregateMerge( self in out SoloNumberImpl, 
                                      ctx2 in SoloNumberImpl ) 
         return number
);
/

create or replace type body SoloNumberImpl is 
static function ODCIAggregateInitialize(sctx in out SoloNumberImpl)
       return number is 
begin
  sctx := SoloNumberImpl(null, 'N');
  return ODCIConst.Success;
end;

member function ODCIAggregateIterate( self in out SoloNumberImpl, 
                                      value in number ) 
       return number is
begin
  if self.flag='N' then
    self.val:=value;
    self.flag:='Y';
  else
    if (self.val is null and value is not null) 
         or (self.val is not null and value is null) 
         or (self.val!=value) then
      raise_application_error( -20001, 
                               'This aggregate only allows one unique input' );
    end if;
  end if;
  return ODCIConst.Success;
end;

member function ODCIAggregateTerminate( self in SoloNumberImpl, 
                                        returnValue out number, 
                                        flags in number )  
       return number is
begin
  returnValue := self.val;
  return ODCIConst.Success;
end;

member function ODCIAggregateMerge( self in out SoloNumberImpl, 
                                    ctx2 in SoloNumberImpl ) 
       return number is
begin
  if self.flag='N' then
    self.val:=ctx2.val;
    self.flag=ctx2.flag;
  elsif ctx2.flag='Y' then
    if (self.val is null and ctx2.val is not null) 
          or (self.val is not null and ctx2.val is null) 
          or (self.val!=ctx2.val) then
      raise_application_error( -20001, 
                               'This aggregate only allows one unique input' );
    end if;
  end if;
  return ODCIConst.Success;
end;
end;
/

create function SoloNumber (input number) 
return number aggregate using SoloNumberImpl;
/

【讨论】:

    【解决方案3】:

    ORACLE 解决方案是

    select product_id, 
           case when min(description) != max(description) then to_char(1/0) 
                else min(description) end description, 
           sum(cost) 
      from product join sale using (product_id) 
      group by product_id;
    

    而不是 to_char(1/0) [引发 DIVIDE_BY_ZERO 错误),您可以使用一个简单的函数

    CREATE OR REPLACE FUNCTION solo (i_min IN VARCHAR2, i_max IN VARCHAR2) 
    RETURN VARCHAR2 IS
    BEGIN
      IF i_min != i_max THEN
        RAISE_APPLICATION_ERROR(-20001, 'Non-unique value specified');
      ELSE
        RETURN i_min;
      END;
    END;
    /
    select product_id, 
           solo(min(description),max(description)) end description, 
           sum(cost) 
    from product join sale using (product_id) 
    group by product_id;
    

    您可以使用用户定义的聚合,但我担心在 SQL 和 PL/SQL 之间切换会影响性能。

    【讨论】:

    • 谢谢,我没有想到这种巧妙的方法。我真的在寻找能够使 SQL 尽可能简洁的东西——理想情况下,我想要一些像 select id, unique description from product group by product_id 这样的新语法——但没有供应商会添加它,除非它在我猜的标准中。我也想不出如何更改您的方法以强制 null 像我即将发布的 Oracle 答案中那样算作唯一值。
    【解决方案4】:

    您应该在 (product_id, description) 上定义一个 UNIQUE 约束,这样您就不必担心一个产品有两个描述。

    【讨论】:

    • “真正的使用更有可能是在较大的查询中,这样更容易对唯一性做出错误的假设”。您的“表”可能是任何数据源,例如带有聚合的子查询,在其中添加数据库约束以强制唯一性并不容易
    • 无论如何,(product_id, description) 已经是唯一的,因为 product_id 是
    猜你喜欢
    • 2012-01-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-05
    • 2016-03-28
    • 1970-01-01
    • 2017-12-09
    • 1970-01-01
    相关资源
    最近更新 更多