Home > database >  Own aggregation function in Oracle
Own aggregation function in Oracle

Time:11-03

Customers in my DB can have (none, one or more) products (products are ordered by importance). Products are ISP (3), SOP (2), MAP (1). The customer's category is defined according to product with the highest importance. So if customer for example has SOP and ISP products, his category is ISP (according to product of highest importance in customer's possession).

To calculate the customer's category I have to define my own proprietary order (weight/importance) of products. How I do it - I assign the importance/weight factor to my products, take MIN (or MAX) from all of them (for a customer), and finally revert back weight/importance factor to the product name:

SELECT
  c.id,

  DECODE(
         MAX(
             DECODE(p.name,'ISP',3,'SOP',2,'MAP',1) --encoding my own order (importance)
         ), --taking product with highest importance
         3,'ISP',2,'SOP',1,'MAP') --decoding MAX important product name back
  AS category

FROM customers c
LEFT JOIN products p ON p.id_customer=c.id
GROUP BY c.id

Since I use this logic in many queries I'd like to define my own aggregation function which would return category according to customer's most important product.

I'd like to have the query like this:

SELECT
   c.id,
   MyOwnCategory(p.name) category
FROM customers c
LEFT JOIN products p ON p.id_customer=c.id
GROUP BY c.id

How to implement such a custom aggregation function?

CodePudding user response:

You don't need a custom aggregation function, you can use MAX with KEEP and apply your ordering there:

SELECT c.id,
       MAX(p.name) KEEP (
         DENSE_RANK FIRST
         ORDER BY DECODE(p.name,'ISP',3,'SOP',2,'MAP',1) DESC NULLS LAST
       ) AS category
FROM   customers c
       LEFT JOIN products p ON p.id_customer=c.id
GROUP BY c.id

If you want to define a function to handle the ordering in a single location then:

CREATE FUNCTION product_name_ordering(
  p_name IN PRODUCTS.NAME%TYPE
) RETURN INT DETERMINISTIC
IS
BEGIN
  RETURN CASE p_name
         WHEN 'ISP' THEN 3
         WHEN 'SOP' THEN 2
         WHEN 'MAP' THEN 1
         ELSE NULL
         END;
END product_name_ordering;
/

Then:

SELECT c.id,
       MAX(p.name) KEEP (
         DENSE_RANK FIRST
         ORDER BY product_name_ordering(p.name) DESC NULLS LAST
       ) AS category
FROM   customers c
       LEFT JOIN products p ON p.id_customer=c.id
GROUP BY c.id

CodePudding user response:

If you really want to solve this with a custom aggregation function, then (borrowing from https://oracle-base.com/articles/misc/string-aggregation-techniques ), you could do something like

CREATE OR REPLACE TYPE t_max_product_agg AS OBJECT
(
  g_max_product  VARCHAR2(32767),

  STATIC FUNCTION ODCIAggregateInitialize(sctx  IN OUT  t_max_product_agg)
    RETURN NUMBER,

  MEMBER FUNCTION ODCIAggregateIterate(self   IN OUT  t_max_product_agg,
                                       value  IN      VARCHAR2 )
     RETURN NUMBER,

  MEMBER FUNCTION ODCIAggregateTerminate(self         IN   t_max_product_agg,
                                         returnValue  OUT  VARCHAR2,
                                         flags        IN   NUMBER)
    RETURN NUMBER,

  MEMBER FUNCTION ODCIAggregateMerge(self  IN OUT  t_max_product_agg,
                                     ctx2  IN      t_max_product_agg)
    RETURN NUMBER
);
/
SHOW ERRORS


CREATE OR REPLACE TYPE BODY t_max_product_agg IS
  STATIC FUNCTION ODCIAggregateInitialize(sctx  IN OUT  t_max_product_agg)
    RETURN NUMBER IS
  BEGIN
    sctx := t_max_product_agg(NULL);
    RETURN ODCIConst.Success;
  END;

  MEMBER FUNCTION ODCIAggregateIterate(self   IN OUT  t_max_product_agg,
                                       value  IN      VARCHAR2 )
    RETURN NUMBER IS
  BEGIN
    IF CASE ODCIAggregateIterate.value WHEN 'ISP' THEN 3 WHEN 'SOP' THEN 2 WHEN 'MAP' THEN 1 ELSE NULL END > CASE SELF.g_max_product WHEN 'ISP' THEN 3 WHEN 'SOP' THEN 2 WHEN 'MAP' THEN 1 ELSE NULL END THEN
      SELF.g_max_product := value;
    ELSIF SELF.g_max_product IS NULL THEN
      SELF.g_max_product := value;
    END IF;
    RETURN ODCIConst.Success;
  END;

  MEMBER FUNCTION ODCIAggregateTerminate(self         IN   t_max_product_agg,
                                         returnValue  OUT  VARCHAR2,
                                         flags        IN   NUMBER)
    RETURN NUMBER IS
  BEGIN
    returnValue := SELF.g_max_product;
    RETURN ODCIConst.Success;
  END;

  MEMBER FUNCTION ODCIAggregateMerge(self  IN OUT  t_max_product_agg,
                                     ctx2  IN      t_max_product_agg)
    RETURN NUMBER IS
  BEGIN
    IF CASE ctx2.g_max_product WHEN 'ISP' THEN 3 WHEN 'SOP' THEN 2 WHEN 'MAP' THEN 1 ELSE NULL END > CASE SELF.g_max_product WHEN 'ISP' THEN 3 WHEN 'SOP' THEN 2 WHEN 'MAP' THEN 1 ELSE NULL END THEN    
      SELF.g_max_product := ctx2.g_max_product;
    ELSIF SELF.g_max_product IS NULL THEN
      SELF.g_max_product := ctx2.g_max_product;
    END IF;
    RETURN ODCIConst.Success;
  END;
END;
/
SHOW ERRORS


CREATE OR REPLACE FUNCTION max_product (p_input VARCHAR2)
RETURN VARCHAR2
PARALLEL_ENABLE AGGREGATE USING t_max_product_agg;
/
SHOW ERRORS
sql>select max_product(product) from (select 'SOP' product from dual union all select 'ISP' from dual);

MAX_PRODUCT(PRODUCT)
___________________________________________________________________________________________________________
ISP

You'd need to write similar code for a MIN version.

CodePudding user response:

I found out how to create custom aggregate function:

CREATE TYPE MyCategory_impl AS OBJECT
(
  cat VARCHAR2(100),
  old_cat_weight NUMBER,
  new_cat_weight NUMBER,
  STATIC FUNCTION ODCIAggregateInitialize(sctx IN OUT MyCategory_impl) RETURN NUMBER,
  MEMBER FUNCTION ODCIAggregateIterate(self IN OUT MyCategory_impl,val IN VARCHAR2) RETURN NUMBER,
  MEMBER FUNCTION ODCIAggregateTerminate(self IN MyCategory_impl,returnvalue OUT VARCHAR2,flags IN NUMBER) RETURN NUMBER,
  MEMBER FUNCTION ODCIAggregateMerge(self IN OUT MyCategory_impl,ctx2 IN MyCategory_impl) RETURN NUMBER
);


CREATE TYPE BODY MyCategory_impl IS

  STATIC FUNCTION ODCIAggregateInitialize(sctx IN OUT MyCategory_impl) RETURN NUMBER IS
  BEGIN
    sctx := MyCategory_impl('MAP',0,0);
    RETURN ODCIConst.Success;
  END;

  MEMBER FUNCTION ODCIAggregateIterate(self IN OUT MyCategory_impl,val IN VARCHAR2) RETURN NUMBER IS
  BEGIN
    SELECT DECODE(self.cat,'ISP',3,'SOP',2,'MAP',1,0) INTO old_cat_weight FROM DUAL;
    SELECT DECODE(val,'ISP',3,'SOP',2,'MAP',1,0) INTO new_cat_weight FROM DUAL;
    IF new_cat_weight > old_cat_weight THEN
       self.cat := val;
    END IF;
    RETURN ODCIConst.Success;
  END;

  MEMBER FUNCTION ODCIAggregateTerminate(self IN MyCategory_impl,returnValue OUT VARCHAR2,flags IN NUMBER) RETURN NUMBER IS
  BEGIN
     returnValue := self.cat;
     RETURN ODCIConst.Success;
  END;

  MEMBER FUNCTION ODCIAggregateMerge(self IN OUT MyCategory_impl,ctx2 IN MyCategory_impl) RETURN NUMBER IS
  BEGIN
    RETURN ODCIConst.Success;
  END;

END;


CREATE FUNCTION MyCategory (input VARCHAR2) RETURN VARCHAR2 
PARALLEL_ENABLE AGGREGATE USING MyCategory_impl;


SELECT
  c.id,
  MyCategory(p.name) category
FROM customers c
LEFT JOIN products p ON p.id_customer=c.id
GROUPY BY c.id
 
  • Related