How to GROUP BY CASE with aggregate function [duplicate] - sql

This question already has answers here:
Count based on condition in SQL Server
(4 answers)
Closed 4 years ago.
I am attempting to get the count of a.clmNo based on the a.stat value displayed on one row. I am currently getting 3 rows returned since I have a count of 5 a.clmNo over 3 different a.stat values, which makes sense since I am grouping by s.stat. How can I change my query so that I don't have to group by s.stat and instead get the results returned on one row?
Current results:
+-------------+--------------+-----------------+-----------+---------------+
| pend_claims | assnd_claims | qa_ready_claims | qa_claims | closed_claims |
+-------------+--------------+-----------------+-----------+---------------+
| 0 | 3 | 0 | 0 | 0 |
+-------------+--------------+-----------------+-----------+---------------+
| 0 | 0 | 0 | 1 | 0 |
+-------------+--------------+-----------------+-----------+---------------+
| 1 | 0 | 0 | 0 | 0 |
+-------------+--------------+-----------------+-----------+---------------+
Desired results:
+------------+--------------+-----------------+-----------+--------------+
|pend_claims | assnd_claims | qa_ready_claims | qa_claims | closed_claims|
+------------+--------------+-----------------+-----------+--------------+
| 1 | 3 | 0 | 1 | 0 |
+------------+--------------+-----------------+-----------+--------------+
Current query:
SELECT ISNULL(case when s.stat = 'Pending Assignment' then count(a.clmNo) end,0) as pend_claims,
ISNULL(case when s.stat = 'Assigned' then count(a.clmNo) end,0) as assnd_claims,
ISNULL(case when s.stat = 'QA Ready' then count(a.clmNo) end,0) as qa_ready_claims,
ISNULL(case when s.stat = 'In QA' then count(a.clmNo) end,0) as qa_claims,
ISNULL(case when s.stat = 'Closed' then count(a.clmNo) end,0) as closed_claims
FROM assnmts a
inner join assnmtStats astats
on a.assnmtIdPk = astats.assnmtIdFk
inner join stats s
on astats.aStatId = s.statIdPk
inner join repAssnmts ra
on a.assnmtIdPk = ra.assnmtIdFk
inner join aspnetusers anu
on ra.repId = anu.Id
inner join clients c
on a.clientIdFk = c.clientIdPk
inner join carrs
on a.carrierId = carrs.carrIdPk
inner join (SELECT a2.assnmtIdPk, MAX(astats2.asCrtdDt) as MaxDate
FROM assnmts a2
INNER JOIN assnmtStats astats2
on a2.assnmtIdPk = astats2.assnmtIdFk
GROUP BY a2.assnmtIdPk
) mdt
on a.assnmtIdPk = mdt.assnmtIdPk
and astats.asCrtdDt = mdt.MaxDate
inner join (select a3.assnmtIdPk, MAX(ra2.raCrtdDt) as MaxRepDate
from assnmts a3
inner join repAssnmts ra2
on a3.assnmtIdPk = ra2.assnmtIdFk
group by a3.assnmtIdPk
) mrepdt
on a.assnmtIdPk = mrepdt.assnmtIdPk
and ra.raCrtdDt = mrepdt.MaxRepDate
group by s.stat

You want conditional aggregation. Remove the GROUP BY and rephrase the SELECT:
SELECT SUM(case when s.stat = 'Pending Assignment' then 1 else 0 end) as pend_claims,
SUM(case when s.stat = 'Assigned' then 1 else 0 end) as assnd_claims,
SUM(case when s.stat = 'QA Ready' then 1 else 0 end) as qa_ready_claims,
SUM(case when s.stat = 'In QA' then 1 else 0 end) as qa_claims,
SUM(case when s.stat = 'Closed' then 1 else 0 end) as closed_claims

Related

Order by but one value is always last

I was wandering if it was possible to Order By a column but have a value always come last.
For instance, I'm doing a COUNT to see how many sales our employees have, differentiated by type of employee.
This is what I'm getting:
+-----------+------------+------------+
| Status | Associate | Internal |
+-----------+------------+------------+
Agended | 5 | 13
Canceled | 0 | 1
Instaled | 10 | 24
TOTAL | 15 | 38
And this is my goal:
+-----------+------------+------------+
| Status | Associate | Internal |
+-----------+------------+------------+
Canceled | 0 | 1
Agended | 5 | 13
Instaled | 10 | 24
TOTAL | 15 | 38
My code is
SELECT CASE WHEN bo.stateofcontract IS NULL THEN ISNULL(bo.stateofcontract,'TOTAL') ELSE bo.stateofcontract END "Status"
, COUNT(CASE WHEN bo.stateofcontract IN ('INSTALED','AGENDED','CANCELED')AND CM3.func = 'ASSOCIATE' THEN 1 END) "Associate"
, COUNT(CASE WHEN bo.stateofcontract IN ('INSTALED','AGENDED','CANCELED')AND CM3.func <> 'ASSOCIATE' THEN 1 END) "Internal"
FROM BO
JOIN CM3 ON CM3.cm = BO.employee
WHERE (bo.stateofcontract IN ('INSTALED','AGENDED','CANCELED'))
GROUP BY ROLLUP (bo.stateofcontract)
You can simplify your query a bit by removing some conditions from the CASE expressions. There is also a little trick in SQL Server using CHARINDEX() which usually works for such ordering:
SELECT COALESCE(bo.stateofcontract, 'TOTAL') as status,
COUNT(CASE WHEN CM3.func = 'ASSOCIATE' THEN 1 END) Associate,
COUNT(CASE WHEN AND CM3.func <> 'ASSOCIATE' THEN 1 END) as Internal
FROM BO JOIN
CM3
ON CM3.cm = BO.employee
WHERE bo.stateofcontract IN ('INSTALED', 'AGENDED', 'CANCELED'))
GROUP BY ROLLUP bo.stateofcontract
ORDER BY CHARINDEX(status, 'Canceled,Agended,Instaled,Total');
You can ORDER BY a CASE expression, or add an ordering column in the SELECT. EG:
SELECT CASE WHEN bo.stateofcontract IS NULL THEN ISNULL(bo.stateofcontract,'TOTAL') ELSE bo.stateofcontract END "Status"
, COUNT(CASE WHEN bo.stateofcontract IN ('INSTALED','AGENDED','CANCELED')AND CM3.func = 'ASSOCIATE' THEN 1 END) "Associate"
, COUNT(CASE WHEN bo.stateofcontract IN ('INSTALED','AGENDED','CANCELED')AND CM3.func <> 'ASSOCIATE' THEN 1 END) "Internal"
FROM BO
JOIN CM3 ON CM3.cm = BO.employee
WHERE (bo.stateofcontract IN ('INSTALED','AGENDED','CANCELED'))
GROUP BY ROLLUP (bo.stateofcontract)
ORDER BY case when Status = 'Canceled' then 1
when Status = 'Agended' then 2
when Status = 'Instaled' then 3
when Status = 'Total' then 4
end

Nested self join and creating multiple sums

I have a basic parent / child scheme for expenditures:
The underling data is the same so I just added a category column and parent_id. These have child records:
I am trying to aggregate the totals form the orders, related orders and difference between the two like this:
Which is grouped by the orders overall then I am also looking for something like this:
I can get the order_amount no problem either way. That's a simple JOIN and SUM.
I am stuck on the secondary JOINS given that I have to JOIN the invoices expenditures to the orders then JOIN the invoice expenditure items and SUM that up.
I am looking for direction on the correct JOIN or if there is a better way to approach this with some sort of subquery etc.
To sump up by order, one solution would be to use a conditional aggregate query. A trick is to check the category to decide whether to use the value from column expenditures.id or from column expenditures.parent_id as grouping criteria:
SELECT
CASE WHEN e.category = 'order' THEN e.id ELSE e.parent_id END expenditure_id,
SUM(CASE WHEN e.category = 'order' THEN i.amount ELSE 0 END) order_amount,
SUM(CASE WHEN e.category = 'invoice' THEN i.amount ELSE 0 END) order_amount,
SUM(CASE WHEN e.category = 'order' THEN i.amount ELSE 0 END)
- SUM(CASE WHEN e.category = 'invoice' THEN i.amount ELSE 0 END) balance
FROM expenditures e
LEFT JOIN expenditure_items i ON e.id = i.expenditure_id
GROUP BY CASE WHEN e.category = 'order' THEN e.id ELSE e.parent_id END
ORDER BY expenditure_id
Demo on DB Fiddle:
| expenditure_id | order_amount | order_amount | balance |
| -------------- | ------------ | ------------ | ------- |
| 1 | 3740 | 0 | 3740 |
| 2 | 11000 | 9350 | 1650 |
The second query, that sums up by item code, basically follows the same logic, but groups by idem code instead:
SELECT
i.code,
SUM(CASE WHEN e.category = 'order' THEN i.amount ELSE 0 END) order_amount,
SUM(CASE WHEN e.category = 'invoice' THEN i.amount ELSE 0 END) order_amount,
SUM(CASE WHEN e.category = 'order' THEN i.amount ELSE 0 END)
- SUM(CASE WHEN e.category = 'invoice' THEN i.amount ELSE 0 END) balance
FROM expenditures e
LEFT JOIN expenditure_items i ON e.id = i.expenditure_id
GROUP BY i.code
ORDER BY i.code;
Demo:
| code | order_amount | order_amount | balance |
| ---- | ------------ | ------------ | ------- |
| a | 13400 | 8500 | 4900 |
| b | 1340 | 850 | 490 |

Rewriting SQL query to get record id and customer number

Sample data
+-------------------+-------------+-----------------+---------------------+
| RECORD_ID | CUST_NO | IsAccntClosed | Code |
+-------------------+-------------+-----------------+---------------------+
|159045 | 2439123 | N | 13 |
+-------------------+-------------+-----------------+---------------------+
|159048 | 6376150 | Y | 13 |
+-------------------+-------------+-----------------+---------------------+
|159048 | 9513035 | N | 13 |
+-------------------+-------------+-----------------+---------------------+
|159049 | 2398524 | N | 12 |
+-------------------+-------------+-----------------+---------------------+
|159049 | 6349269 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159049 | 6350690 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159049 | 6372163 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159049 | 6393810 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159049 | 6402062 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159050 | 2677512 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159050 | 6349382 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159050 | 6378137 | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
|159051 | 2336197 | N | 12 |
+-------------------+-------------+-----------------+---------------------+
|159051 | 6349293 | N | 12 |
+-------------------+-------------+-----------------+---------------------+
|159051 | 6350682 | N | 12 |
+-------------------+-------------+-----------------+---------------------+
|159051 | 6367895 | N | 12 |
+-------------------+-------------+-----------------+---------------------+
|159060 | yyyyyy | Y | 12 |
+-------------------+-------------+-----------------+---------------------+
IsAccntClosed column indicates if the account is Open (Y) or account is closed (Y).
I need to select Record_ID and cust_no for only those rows for which which Record_Id satisfies one of the below condition :
1. Only one cust account is open , there might be one or multiple closed customers
2. No open customer and only one closed customer
Expected output :
159045 2439123
159048 9513035
159049 2398524
159060 yyyyyy
A query like this would take each row as a single group and the count will come as 1
select RECORD_ID, CUST_NO, IsAccntClosed, count(IsAccntClosed), Code
from table1
group by RECORD_ID, CUST_NO, IsAccntClosed, Code
Any suggestions on how this query could be written to get the expected output?
Being IsAccntClosed a CHAR column, you should be able to get what you want with a query like this:
SELECT a.record_id,
(CASE
WHEN a.CountOpen=1 THEN a.CustNoOpen
ELSE a.CustNoClosed
END) AS cust_no
FROM (
SELECT b.record_id,
MAX(CASE WHEN b.IsAccntClosed='N' THEN b.cust_no ELSE NULL END) AS CustNoOpen ,
SUM(CASE WHEN b.IsAccntClosed='N' THEN 1 ELSE 0 END) AS CountOpen ,
MAX(CASE WHEN b.IsAccntClosed='Y' THEN b.cust_no ELSE NULL END) AS CustNoClosed,
SUM(CASE WHEN b.IsAccntClosed='Y' THEN 1 ELSE 0 END) AS CountClosed
FROM table1 b
GROUP BY b.record_id
) a
WHERE a.CountOpen=1 OR (a.CountOpen=0 AND a.CountClosed=1)
The inner query is grouping the table. It counts the open and closed accounts and takes one (random) cust_no of any of the closed accounts and one (random) of any of the open accounts, per group.
The outer query filters the data and cleans everything up, placing the open or closed cust_no in the output result column.
Notice that the WHERE condition of the outer query has the collateral effect that, since you are looking for records that have just a single open or a single closed account, those random cust_no which have been selected by the inner query, are now significant.
EDIT: I fixed the query and tested it on SQLFiddle.
In the below I added another record:
insert into tbl values (159060, 'zzzzzz', 'N', 12);
to illustrate what would happen if a record_id has just one open cust_no and just one closed cust_no. Note how in the result the cust_no returned is the zzzzz one, because that account is open, which you mentioned wanting to take precendence over closed, in the event of a tie 1:1 (zzzzzz should take over yyyyyy in this case, because yyyyyy is closed whereas zzzzzz is open)
Fiddle: http://sqlfiddle.com/#!4/5cb60/1/0
with one_open as
(select record_id
from tbl
where IsAccntClosed = 'N'
group by record_id
having count(distinct cust_no) = 1),
one_closed as
(select record_id
from tbl
where IsAccntClosed = 'Y'
group by record_id
having count(distinct cust_no) = 1),
bothy as
(select record_id from one_open intersect select record_id from one_closed)
select *
from tbl
where (record_id in (select record_id from one_open) and
IsAccntClosed = 'N')
or (record_id not in (select record_id from one_open) and
record_id in (select record_id from one_closed) and
IsAccntClosed = 'Y' and
record_id not in (select record_id from bothy))
select
record_id,
cust_no
from (
select
record_id,
cust_no,
count(case when isAccntClosed='Y' then 1 else null end)
over (partition by record_id) closed_accounts,
count(case when isAccntClosed='N' then 1 else null end)
over (partition by record_id) open_accounts
from
table1
)
where (open_accounts = 1)
or (open_accounts = 0 and closed_accounts = 1)
You can do this with conditional aggregation:
select RECORD_ID,
(case when sum(case when IsAccntClosed = 'N' then 1 else 0 end) = 1
then max(case when IsAccntClosed = 'N' then max(CUST_NO) end)
else max(CUST_NO)
end) as cust_no
from table1
group by RECORD_ID
having sum(case when IsAccntClosed = 'N' then 1 else 0 end) = 1 or
(sum(case when IsAccntClosed = 'N' then 1 else 0 end) = 0 and
sum(case when IsAccntClosed = 'Y' then 1 else 0 end) = 1
)
It is probably easier to understand the logic using subqueries:
select recordId,
(case when numOpen > 0 then OpenCustNo else closedCustNo end) as CustNo
from (select t1.RecordId, sum(case when IsAccntClosed = 'N' then 1 else 0 end) as numOpen,
sum(case when IsAccntOpen = 'N' then 1 else 0 end) as numClosed,
max(case when IsAccntClosed = 'N' then cust_no end) as OpenCustNo,
max(case when IsAccntClosed = 'Y' then cust_no end) as ClosedCustNo
from table1 t1
group by record_id
) r
where numOpen = 1 or numOpen = 0 and numClosed = 1;

sql how to transform data vertically

I have a 3 datbles Dealer, payment_type and dealer_payment_type
Dealer : dealer_id , dealer_name, dealer_address
1 | test | 123 test lane
2 | abc | abc lane
3 | def | def lane
Payment_type : paymenttype_id , paytype
1 | CHECK
2 | WIRE
3 | CREDIT
Dealer_Payment_type : DPT_id , dealer_id , payment_type_id
1 | 1 | 1
2 | 1 | 2
3 | 1 | 3
4 | 2 | 2
5 | 2 | 3
6 | 3 | 1
7 | 3 | 2
I have to write a query to get payment type info for each dealer , query needs to return data like this:
dealer_id , dealer_name , paytype
1 | test | check,wire,credit
2 | abc | wire,credit
3 | def | check,wire
OR
dealer_id , dealer_name , check , wire , credit
1 | test | true | true | true
2 | abc | false | true | true
3 | def | true | false | true
You did not specify what version of Oracle you are using.
If you are using Oracle 11g, then you can use the following.
To get the values into a single column, then you can use LISTAGG:
select d.dealer_id,
d.dealer_name,
listagg(p.paytype, ',') within group (order by d.dealer_id) as paytype
from dealer d
left join Dealer_Payment_type dp
on d.dealer_id = dp.dealer_id
left join payment_type p
on dp.payment_type_id = p.paymenttype_id
group by d.dealer_id, d.dealer_name;
See SQL Fiddle with demo
To get the values in separate columns, then you can use PIVOT:
select dealer_id, dealer_name,
coalesce("Check", 'false') "Check",
coalesce("Wire", 'false') "Wire",
coalesce("Credit", 'false') "Credit"
from
(
select d.dealer_id,
d.dealer_name,
p.paytype,
'true' flag
from dealer d
left join Dealer_Payment_type dp
on d.dealer_id = dp.dealer_id
left join payment_type p
on dp.payment_type_id = p.paymenttype_id
)
pivot
(
max(flag)
for paytype in ('CHECK' as "Check", 'WIRE' as "Wire", 'CREDIT' as "Credit")
)
See SQL Fiddle with Demo.
If you are not using Oracle 11g, then you can use wm_concat() to concatenate the values into a single row:
select d.dealer_id,
d.dealer_name,
wm_concat(p.paytype) as paytype
from dealer d
left join Dealer_Payment_type dp
on d.dealer_id = dp.dealer_id
left join payment_type p
on dp.payment_type_id = p.paymenttype_id
group by d.dealer_id, d.dealer_name;
To create the separate columns, then you can use an aggregate function with a CASE:
select dealer_id, dealer_name,
max(case when paytype = 'CHECK' then flag else 'false' end) "Check",
max(case when paytype = 'WIRE' then flag else 'false' end) "Wire",
max(case when paytype = 'CREDIT' then flag else 'false' end) "Credit"
from
(
select d.dealer_id,
d.dealer_name,
p.paytype,
'true' flag
from dealer d
left join Dealer_Payment_type dp
on d.dealer_id = dp.dealer_id
left join payment_type p
on dp.payment_type_id = p.paymenttype_id
)
group by dealer_id, dealer_name;
See SQL Fiddle with Demo

SQL query: self join on subquery necessitates creating a separate (non-temporary) table?

I'm working on what is for me a complicated query, and I've managed to get the information I need, but seem to be forced to create a table to accomplish it. I'm using MySQL, so I can't use WITH, I can't use a view because my SELECT contains a subquery in the FROM clause, and I can't use a temporary table because I need to self-join. Am I missing something?
Background:
a reservation can have 1 or more reservation_detail (foreign key rel'p on reservation_id)
a reservation_detail has a quantity and a ticket_type (foreign key rel'p on ticket_type)
Here's the first part of my current solution:
CREATE TABLE
tmp
SELECT
t.reservation_id,
t.ticket_type,
COALESCE(rd.quantity,0) AS qty
FROM (
SELECT *
FROM
(ticket_type tt, reservation r)
) t
LEFT JOIN
reservation_detail rd
ON
t.reservation_id = rd.reservation_id
AND
t.ticket_type = rd.ticket_type;
This gives me a table that looks like the following, where for each combination of a reservation_id and a ticket_type, I have a qty.
+----------------+-------------+------+
| reservation_id | ticket_type | qty |
+----------------+-------------+------+
| 1 | ADULT | 2 |
| 1 | CHILD | 2 |
| 1 | INFANT | 0 |
| 2 | ADULT | 1 |
| 2 | CHILD | 0 |
| 2 | INFANT | 0 |
| 3 | ADULT | 1 |
| 3 | CHILD | 0 |
| 3 | INFANT | 0 |
+----------------+-------------+------+
Now I can self join thrice on this table to get what I'm really looking for...
SELECT
t1.reservation_id,
t1.qty AS num_adults,
t2.qty AS num_children,
t3.qty AS num_infants
FROM
tmp t1
LEFT JOIN
tmp t2
ON
t1.reservation_id = t2.reservation_id
LEFT JOIN
tmp t3
ON
t2.reservation_id = t3.reservation_id
WHERE
t1.ticket_type = 'ADULT'
AND
t2.ticket_type = 'CHILD'
AND
t3.ticket_type = 'INFANT';
...which is one row for each reservation showing the qty for each of the three ticket types.
+----------------+------------+--------------+-------------+
| reservation_id | num_adults | num_children | num_infants |
+----------------+------------+--------------+-------------+
| 1 | 2 | 2 | 0 |
| 2 | 1 | 0 | 0 |
| 3 | 1 | 0 | 0 |
+----------------+------------+--------------+-------------+
I hope this is enough information. Please leave a comment if it's not.
If your query is considering only these 3 types: ADULT, CHILD, INFANT; you don't have to use table ticket_type.
SELECT
r.reservation_id,
COALESCE(rd_adult.quantity,0) AS num_adults,
COALESCE(rd_child.quantity,0) AS num_children,
COALESCE(rd_infant.quantity,0) AS num_infants
FROM
reservation r
LEFT JOIN
reservation_detail rd_adult
ON r.reservation_id = rd_adult.reservation_id
and rd_adult.ticket_type = 'ADULT'
LEFT JOIN
reservation_detail rd_child
ON r.reservation_id = rd_child.reservation_id
and rd_child.ticket_type = 'CHILD'
LEFT JOIN
reservation_detail rd_infant
ON r.reservation_id = rd_infant.reservation_id
and rd_infant.ticket_type = 'INFANT'
Since table reservation_detail contains all the fields you need, you don't need to join the other tables and create a temp table.
Try this:
SELECT distinct
t.reservation_id,
COALESCE(t1.qty,0) AS num_adults,
COALESCE(t2.qty,0) AS num_children,
COALESCE(t3.qty,0) AS num_infants
FROM reservation t
LEFT JOIN reservation_detail t1 ON t.reservation_id = t1.reservation_id AND t1.ticket_type = 'ADULT'
LEFT JOIN reservation_detail t2 ON t.reservation_id = t2.reservation_id AND t2.ticket_type = 'CHILD'
LEFT JOIN reservation_detail t3 ON t.reservation_id = t3.reservation_id AND t3.ticket_type = 'INFANT';
If you want to stick with your first query, you can sub this for the 2nd:
SELECT reservation_id,
SUM(CASE WHEN ticket_type='ADULT' THEN qty ELSE 0 END) AS adults,
SUM(CASE WHEN ticket_type='CHILD' THEN qty ELSE 0 END) AS children,
SUM(CASE WHEN ticket_type='INFANT' THEN qty ELSE 0 END) AS infants,
FROM tmp
GROUP BY reservation_id;
However, I'm wondering a bit about your schema. You are storing qty, a calculated value. Have you considered just having a row for each ticket instance. If you do that then no tmp table is required, though you'd do the pivot similarly to the above.
a simple GROUP BY should be OK:
SELECT t.reservation_id,
SUM(CASE
WHEN ticket_type = 'ADULT' THEN
COALESCE(rd.quantity, 0)
ELSE
0
END) num_adults,
SUM(CASE
WHEN ticket_type = 'CHILD' THEN
COALESCE(rd.quantity, 0)
ELSE
0
END) num_children,
SUM(CASE
WHEN ticket_type = 'INFANT' THEN
COALESCE(rd.quantity, 0)
ELSE
0
END) num_infants
FROM (SELECT * FROM (ticket_type tt, reservation r)) t
LEFT JOIN reservation_detail rd ON t.reservation_id = rd.reservation_id
AND t.ticket_type = rd.ticket_type
GROUP BY t.reservation_id