2010-12-11 33 views
4

Je me trouve souvent en train d'ajouter des expressions dans la clause group by qui, j'en suis sûr, sont uniques. Il s'avère parfois que je me trompe - à cause d'une erreur dans mon SQL ou d'une supposition erronée, et cette expression n'est pas vraiment unique.Une fonction d'agrégat qui n'autorise qu'une entrée unique

Il y a beaucoup de cas où je préférerais que cela génère une erreur SQL plutôt que d'étendre mon jeu de résultats en mode silencieux et parfois très subtilement.

J'aimerais pouvoir faire quelque chose comme:

select product_id, unique description from product group by product_id 

mais il est évident que je ne peux pas mettre en œuvre moi-même - mais quelque chose presque aussi concis peut être mis en oeuvre avec des agrégats définis par l'utilisateur sur certaines bases de données.

Un agrégat spécial qui n'accepte qu'une valeur d'entrée unique peut-il être utile dans toutes les versions de SQL? Si oui, une telle chose pourrait-elle être implémentée maintenant sur la plupart des bases de données? Les valeurs null doivent être considérées comme n'importe quelle autre valeur - contrairement à la façon dont l'agrégat avg fonctionne généralement. (J'ai ajouté des réponses pour implémenter ceci pour postgres et Oracle.)

L'exemple suivant est destiné à montrer comment l'agrégat serait utilisé, mais est un cas simple où il est évident que les expressions doivent être uniques. l'utilisation réelle serait plus probable dans les grandes questions où il est plus facile de faire des hypothèses erronées au sujet de l'unicité

tables:

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 

requêtes:

> 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 

cas d'erreur:

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

quelle base de données vous utilisez MySQL, Oracle, est-il est une fonction solo dans une base MySQL – XMen

+0

@Rahul ceci une question SQL générale - j'espère obtenir des réponses pour les bases de données que je ne connais pas (postgres et Oracle) –

Répondre

3

Voici ma mise en œuvre pour postgres (édité pour traiter null comme un v unique aleur aussi):

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); 

tables exemple pour les tests:

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); 
+0

Belle fonction mais il semble qu'il retourne un tableau du type d'entrée (sous 9.5) y at-il un moyen de pour retourner exactement le type d'entrée? – MarHoff

1

Vous devez définir une contrainte UNIQUE sur (product_id, description), alors vous ne devez jamais vous soucier d'être là deux descriptions pour un produit .

+1

"L'utilisation réelle serait plus susceptible d'être dans des requêtes plus importantes où il est plus facile de faire des hypothèses erronées sur l'unicité". Vos 'tables' peuvent être n'importe quelle source de données, comme une sous-requête avec un agrégat, où il n'est pas si facile d'ajouter des contraintes de base de données pour renforcer l'unicité –

+0

De toute façon, (product_id, description) est déjà unique car product_id est –

7

Une solution 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; 

Plutôt que la to_char (1/0) [qui soulève une erreur de DIVIDE_BY_ZERO), vous pouvez utiliser une fonction simple qui ne

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; 

Vous pouvez utiliser un agrégat défini par l'utilisateur, mais je serais inquiet de l'impact sur les performances de la commutation entre SQL et PL/SQL.

+0

merci, je n'ai pas J'ai pensé à cette approche ingénieuse. Je cherche vraiment quelque chose qui gardera le SQL aussi concis que possible - idéalement, je voudrais un peu de nouvelle syntaxe comme 'select id, description unique du groupe de produits par product_id' - mais aucun fournisseur ne l'ajoutera à moins que ce soit dans la norme je suppose. Je ne peux pas non plus imaginer comment votre méthode pourrait être modifiée pour forcer 'null' à compter comme valeur unique comme dans la réponse d'Oracle que je suis sur le point de publier. –

1

Et voici ma mise en œuvre pour Oracle - malheureusement, je pense que vous avez besoin d'une mise en œuvre pour chaque type de base:

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; 
/