Aggregation based on another column values - sql

Assume I have the following table:
+------------+-------------+
| Product_id | customer_id |
+------------+-------------+
| a | c1 |
| a | c2 |
| a | c3 |
| a | c4 |
| b | c1 |
| c | c1 |
| b | c2 |
| d | c2 |
+------------+-------------+
I want to find the number of (a, b, c) products purchases per customer and the number of (a, b, d) products purchases per customer. I tried to use COUNT with GROUP BY but I only managed to the find the number purchases of each customers FIDDLE. Do I need to use CASE WHEN or DECODE? How can I achieve that?
The expected output is something like:
+-------------+-------------+-------------+
| CUSTOMER_ID | ABC_PRODUCT | ABD_PRODUCT |
+-------------+-------------+-------------+
| c1 | 1 | 0 |
| c2 | 0 | 1 |
| c3 | 0 | 0 |
| c4 | 0 | 0 |
+-------------+-------------+-------------+

You can do this with a single aggregation and no subqueries. The key is using a nested case statement with aggregation to count each product for each customer. The following determines whether a customer has each "bundle":
SELECT CUSTOMER_ID,
(case when max(case when product_id = 'a' then 1 else 0 end) +
max(case when product_id = 'b' then 1 else 0 end) +
max(case when product_id = 'c' then 1 else 0 end) = 3
then 1
else 0
end) as ABC,
(case when max(case when product_id = 'a' then 1 else 0 end) +
max(case when product_id = 'b' then 1 else 0 end) +
max(case when product_id = 'd' then 1 else 0 end) = 3
then 1
else 0
end) as ABD
FROM CUSTOMERS_SALES
GROUP BY CUSTOMER_ID;
Now, your question is actually about the number of such purchases. So, I suppose a customer could purchase each item twice, and you would want them counted twice. If so, then the number is the least value of any counts. You can get this as well:
SELECT CUSTOMER_ID,
least(sum(case when product_id = 'a' then 1 else 0 end),
sum(case when product_id = 'b' then 1 else 0 end),
sum(case when product_id = 'c' then 1 else 0 end)
) as ABC,
least(sum(case when product_id = 'a' then 1 else 0 end),
sum(case when product_id = 'b' then 1 else 0 end),
sum(case when product_id = 'd' then 1 else 0 end)
) as ABD
FROM CUSTOMERS_SALES
GROUP BY CUSTOMER_ID;

Please try below query to find customer having the products a, b and c:
SELECT CUSTOMER_ID
FROM CUSTOMERS_SALES
WHERE PRODUCT_ID IN ('a', 'b', 'c')
GROUP BY CUSTOMER_ID
HAVING COUNT(DISTINCT PRODUCT_ID)=3
To get the count try:
SELECT COUNT(*) FROM(
SELECT CUSTOMER_ID
FROM CUSTOMERS_SALES
WHERE PRODUCT_ID IN ('a', 'b', 'd')
GROUP BY CUSTOMER_ID
HAVING COUNT(DISTINCT PRODUCT_ID)=3
)x

Based entirely off #TechDo's example:
SELECT DISTINCT CUSTOMER_ID,
DECODE((SELECT CUSTOMER_ID
FROM CUSTOMERS_SALES CS2
WHERE PRODUCT_ID IN ('A', 'B', 'C')
AND CS2.CUSTOMER_ID = CS.CUSTOMER_ID
GROUP BY CUSTOMER_ID
HAVING COUNT(DISTINCT PRODUCT_ID)=3), NULL, 0, 1) AS ABC_PRODUCT,
DECODE((SELECT CUSTOMER_ID
FROM CUSTOMERS_SALES CS2
WHERE PRODUCT_ID IN ('A', 'B', 'D')
AND CS2.CUSTOMER_ID = CS.CUSTOMER_ID
GROUP BY CUSTOMER_ID
HAVING COUNT(DISTINCT PRODUCT_ID)=3), NULL, 0, 1) AS ABD_PRODUCT
FROM CUSTOMERS_SALES CS
ORDER BY CUSTOMER_ID

SELECT CUSTOMERS_SALES.CUSTOMER_ID,NVL(MAX(abc.CUSTOMER_ID),0) as ABC_PRODUCT ,NVL(MAX(abd.CUSTOMER_ID),0) as ABD_PRODUCT FROM CUSTOMERS_SALES
LEFT JOIN
(SELECT CUSTOMER_ID
FROM CUSTOMERS_SALES
WHERE PRODUCT_ID IN ('a', 'b', 'd')
GROUP BY CUSTOMER_ID
HAVING COUNT(DISTINCT PRODUCT_ID)=3) abd
ON abd.CUSTOMER_ID=CUSTOMERS_SALES.CUSTOMER_ID
LEFT JOIN
(SELECT CUSTOMER_ID
FROM CUSTOMERS_SALES
WHERE PRODUCT_ID IN ('a', 'b', 'c')
GROUP BY CUSTOMER_ID
HAVING COUNT(DISTINCT PRODUCT_ID)=3) abc
ON abc.CUSTOMER_ID=CUSTOMERS_SALES.CUSTOMER_ID
GROUP BY CUSTOMERS_SALES.CUSTOMER_ID
ORDER BY CUSTOMERS_SALES.CUSTOMER_ID;
FIDDLE

Related

Select sum based on value of other column

I have a table with values like
ID | CODE | QUANTITY
====================
1 | 2 | 20
2 | 2 | 40
3 | 5 | 10
4 | 6 | 15
5 | 5 | 20
6 | 6 | 50
7 | 6 | 10
8 | 7 | 20
9 | 8 | 100
I have a requirement to get the sum of all quantities with "CODE" = 2. However, if the sum is 0
then return the sum of all quantities where "CODE" in (5,6). The idea is to ignore all other codes except 2, 5, and 6, with 2 as the first preference for sum.
I have tried this
WITH CTE AS(
SELECT
SUM(CASE WHEN CODE = '2' THEN QUANTITY ELSE 0 END) AS QUANTITY1,
SUM(CASE WHEN CODE IN ('5', '6') THEN QUANTITY ELSE 0 END) AS QUANTITY2
FROM TABLE1
)
SELECT CASE
WHEN QUANTITY1 <> 0 THEN QUANTITY1
ELSE QUANTITY2
END
FROM CTE
It does work but I feel it can be improved and can be done in minimum steps. How can I improve it?
Edit1: The value of QUANTITY column can be 0 in TABLE1
Edit2: sqlfiddle
For the sum of quantities with CODE = '2' use ELSE 0 in the CASE expression and NULLIF(), so that the result is NULL even if the sum is 0:
SELECT COALESCE(
NULLIF(SUM(CASE WHEN CODE = '2' THEN QUANTITY ELSE 0 END), 0),
SUM(CASE WHEN CODE IN ('5', '6') THEN QUANTITY END)
)
FROM TABLE1
You can use ELSE for quantities with CODE IN ('5', '6') too:
SELECT COALESCE(
NULLIF(SUM(CASE WHEN CODE = '2' THEN QUANTITY ELSE 0 END), 0),
SUM(CASE WHEN CODE IN ('5', '6') THEN QUANTITY ELSE 0 END)
)
FROM TABLE1
See the demo.
If quantity is always greater than 0 in the underlying table, you can use COALESCE():
SELECT COALESCE(SUM(CASE WHEN CODE = '2' THEN QUANTITY END) AS QUANTITY1,
SUM(CASE WHEN CODE IN ('5', '6') THEN QUANTITY END),
0) AS QUANTITY2
FROM TABLE1

Sum rows with same Id based on type and exlude where SUM = 0

I have this table MOVEMENTS:
Id | FatherId | MovementType | Quantity |
=================================================
1 | A | IN | 10 |
2 | A | IN | 5 |
3 | A | OUT | 5 |
4 | B | IN | 10 |
5 | B | OUT | 10 |
6 | C | IN | 5 |
I'm trying to get all the FatherId with the SUM of IN - OUT Movments > 0.
So the result would be:
FatherId | Total |
=========================
A | 10 |
C | 5 |
FatherId = B not showing because
SUM(MovementType = IN) - SUM (MovementType = OUT) = 0
I tried with
SELECT FatherId,
(SELECT (
SUM(CASE WHEN MovementType = 'IN' THEN Quantity ELSE 0 END) -
SUM(CASE WHEN MovementType = 'OUT' THEN Quantity ELSE 0 END)
)) AS Total
FROM MOVEMENTS
GROUP BY FatherId
ORDER BY FatherId
That gives me the result grouped by FatherId, but I'm not able to filter with Total > 0, and also, I'm unable to put this query in a Subquery like:
SELECT * FROM MOVEMENTS WHERE FatherId IN (SELECT ....) OFFSET ... FETCH NEXT ... ROWS ONLY
Is this doable without a stored procedure?
Thank you for any help
Why are you using a subquery? This should do what you want:
SELECT FatherId,
(SUM(CASE WHEN MovementType = 'IN' THEN Quantity ELSE 0 END) -
SUM(CASE WHEN MovementType = 'OUT' THEN Quantity ELSE 0 END)
) AS Total
FROM MOVEMENTS
GROUP BY FatherId
HAVING (SUM(CASE WHEN MovementType = 'IN' THEN Quantity ELSE 0 END) -
SUM(CASE WHEN MovementType = 'OUT' THEN Quantity ELSE 0 END)
) > 0;
You can also simplify the logic to use a single SUM():
SELECT FatherId,
SUM(CASE WHEN MovementType = 'IN' THEN Quantity
WHEN MovementType = 'OUT' THEN - Quantity
ELSE 0
END) AS Total
FROM MOVEMENTS
GROUP BY FatherId
HAVING SUM(CASE WHEN MovementType = 'IN' THEN Quantity
WHEN MovementType = 'OUT' THEN - Quantity
ELSE 0
END) > 0
ORDER BY FatherId;

Finding orders where products of both types are present

Consider below table tbl:
ordernr productId productType
1 12 A
2 15 B
2 13 C
2 12 A
3 15 B
3 12 A
3 11 D
How can I get only rows where products of both productType's B and C are present in the order?
The desired output should be below because products of both type B and C are present in the order:
2 15 B
2 13 C
2 12 A
It might be more efficient to use use exists twice:
select t.*
from mytable t
where
exists (select 1 from mytable t1 where t1.ordernr = t.ordernr and t1.productid = 'B')
and exists (select 1 from mytable t1 where t1.ordernr = t.ordernr and t1.productid = 'C')
This query would take advantage of an index on (ordernr, productid).
One method is using a CTE to get the counts and then filter using those in the outer query:
WITH CTE AS(
SELECT ordernr,
productId,
productType
COUNT(CASE productType WHEN 'B' THEN 1 END) AS BCount,
COUNT(CASE productType WHEN 'C' THEN 1 END) AS CCount
FROM dbo.YourTable)
SELECT ordernr,
productId,
productType
FROM CTE
WHERE BCount > 0
AND CCount > 0;
You can get all the ordernrs that you need with this query:
select ordernr
from tablename
where productType in ('B', 'C')
group by ordernr
having count(distinct productType) = 2
So you can use it with the operator in:
select * from tablename
where ordernr in (
select ordernr
from tablename
where productType in ('B', 'C')
group by ordernr
having count(distinct productType) = 2
)
See the demo.
Results:
> ordernr | productId | productType
> ------: | --------: | :----------
> 2 | 15 | B
> 2 | 13 | C
> 2 | 12 | A

SQL check if column contains specific values

I have a table like this:
id | Values
------------------
1 | a
1 | b
1 | c
1 | d
1 | e
2 | a
2 | a
2 | c
2 | c
2 | e
3 | a
3 | c
3 | b
3 | d
Now I want to know which id contains at least one of a, one of b and one of c.
This is the result I want:
id
--------
1
3
One method is aggregation with having:
select id
from t
where values in ('a', 'b', 'c')
group by id
having count(distinct values) = 3;
If you wanted more flexibility with the counts of each value:
having sum(case when values = 'a' then 1 else 0 end) >= 1 and
sum(case when values = 'b' then 1 else 0 end) >= 1 and
sum(case when values = 'c' then 1 else 0 end) >= 1
You can use grouping:
SELECT id
FROM your_table
GROUP BY id
HAVING SUM(CASE WHEN value = 'a' THEN 1 ELSE 0 END) >= 1
AND SUM(CASE WHEN value = 'b' THEN 1 ELSE 0 END) = 1
AND SUM(CASE WHEN value = 'c' THEN 1 ELSE 0 END) = 1;
or using COUNT:
SELECT id
FROM your_table
GROUP BY id
HAVING COUNT(CASE WHEN value = 'a' THEN 1 END) >= 1
AND COUNT(CASE WHEN value = 'b' THEN 1 END) = 1
AND COUNT(CASE WHEN value = 'c' THEN 1 END) = 1;

SQL: Get multiple line entries linked to one item?

I have a table:
ID | ITEMID | STATUS | TYPE
1 | 123 | 5 | 1
2 | 123 | 4 | 2
3 | 123 | 5 | 3
4 | 125 | 3 | 1
5 | 125 | 5 | 3
Any item can have 0 to many entries in this table. I need a query that will tell me if an ITEM has all it's entries in either a state of 5 or 4. For example, in the above example, I would like to end up with the result:
ITEMID | REQUIREMENTS_MET
123 | TRUE --> true because all statuses are either 5 or 4
125 | FALSE --> false because it has a status of 3 and a status of 5.
If the 3 was a 4 or 5, then this would be true
What would be even better is something like this:
ITEMID | MET_REQUIREMENTS | NOT_MET_REQUIREMENTS
123 | 3 | 0
125 | 1 | 1
Any idea how to write a query for that?
Fast, short, simple:
SELECT itemid
,count(status = 4 OR status = 5 OR NULL) AS met_requirements
,count(status < 4 OR status > 5 OR NULL) AS not_met_requirements
FROM tbl
GROUP BY itemid
ORDER BY itemid;
Assuming all columns to be integer NOT NULL.
Builds on basic boolean logic:
TRUE OR NULL yields TRUE
FALSE OR NULL yields NULL
And NULL is not counted by count().
->SQLfiddle demo.
SELECT a.ID FROM (SELECT ID, MIN(STATUS) AS MINSTATUS, MAX(STATUS) AS MAXSTATUS FROM TABLE_NAME AS a GROUP BY ID)
WHERE a.MINSTATUS >= 4 AND a.MAXSTATUS <= 5
One way of doing this would be
SELECT t1.itemid, NOT EXISTS(SELECT 1
FROM mytable t2
WHERE itemid=t1.itemid
AND status NOT IN (4, 5)) AS requirements_met
FROM mytable t1
GROUP BY t1.itemid
UPDATE: for your updated requirement, you can have something like:
SELECT itemid,
sum(CASE WHEN status IN (4, 5) THEN 1 ELSE 0 END) as met_requirements,
sum(CASE WHEN status IN (4, 5) THEN 0 ELSE 1 END) as not_met_requirements
FROM mytable
GROUP BY itemid
simple one:
select
"ITEMID",
case
when min("STATUS") in (4, 5) and max("STATUS") in (4, 5) then 'True'
else 'False'
end as requirements_met
from table1
group by "ITEMID"
better one:
select
"ITEMID",
sum(case when "STATUS" in (4, 5) then 1 else 0 end) as MET_REQUIREMENTS,
sum(case when "STATUS" in (4, 5) then 0 else 1 end) as NOT_MET_REQUIREMENTS
from table1
group by "ITEMID";
sql fiddle demo
WITH dom AS (
SELECT DISTINCT item_id FROM items
)
, yes AS ( SELECT item_id, COUNT(*) AS good_count FROM items WHERE status IN (4,5) GROUP BY item_id
)
, no AS ( SELECT item_id, COUNT(*) AS bad_count FROM items WHERE status NOT IN (4,5) GROUP BY item_id
)
SELECT d.item_id
, COALESCE(y.good_count,0) AS good_count
, COALESCE(n.bad_count,0) AS bad_count
FROM dom d
LEFT JOIN yes y ON y.item_id = d.item_id
LEFT JOIN no n ON n.item_id = d.item_id
;
Can be done with an outer join, too:
WITH yes AS ( SELECT item_id, COUNT(*) AS good_count FROM items WHERE status IN (4,5) GROUP BY item_id)
, no AS ( SELECT item_id, COUNT(*) AS bad_count FROM items WHERE status NOT IN (4,5) GROUP BY item_id)
SELECT COALESCE(y.item_id, n.item_id) AS item_id
, COALESCE(y.good_count,0) AS good_count
, COALESCE(n.bad_count,0) AS bad_count
FROM yes y
FULL JOIN no n ON n.item_id = y.item_id
;
Nevermind, it was actually easy to do:
select ITEM_ID ,
sum (case when STATUS >= 3 then 1 else 0 end ) as met_requirements,
sum (case when STATUS < 3 then 1 else 0 end ) as not_met_requirements
from TABLE as d
group by ITEM_ID