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