"If one-to-many table has value" as column in SELECT - sql

I have one table with cars, and another table with fuel types. A third table tracks which cars can use which fuel types. I need to select all data for all cars, including which fuel types they can use:
Car table has Car_ID, Car_Name, etc
Fuel table has Fuel_ID, Fuel_Name
Car_Fuel table has Car_ID, Fuel_ID (one car can have multiple Fuel options)
What I want to return:
SELECT
*
, Can_Use_Gas
, Can_Use_Diesel
, Can_Use_Electric
FROM Car
The Can_Use columns are a BIT value, indicating if the car has a matching Fuel entry in the Car_Fuel table.
I can do this with multiple SELECT statements, but this looks painfully messy (and possibly very inefficient?). I'm hoping there's a better way:
SELECT
c.*
, (SELECT COUNT(*) FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 1) AS Can_Use_Gas
, (SELECT COUNT(*) FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 2) AS Can_Use_Diesel
, (SELECT COUNT(*) FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 3) AS Can_Use_Electric
FROM Car c

Presumably you have no duplicates in Car_fuel, so you don't need aggregation. Hence you can do:
SELECT c.*,
ISNULL((SELECT TOP 1 1 FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 1), 0) AS Can_Use_Gas
ISNULL((SELECT TOP 1 1 FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 2), 0) AS Can_Use_Diesel
ISNULL((SELECT TOP 1 1 FROM Car_Fuel f WHERE f.Car_ID = c.Car_ID AND f.Fuel_ID = 3), 0) AS Can_Use_Electric
FROM Car c;
This is one case where ISNULL() has a performance advantage over COALESCE(), because COALESCE() evaluates the first argument twice.

Although not a perfect solution, you could use the pivot clause:
select *
from ( select car_name, fuel_name
from Car
inner join Car_Fuel on Car.car_id = Car_Fuel.car_id
inner join Fuel on Car_Fuel.fuel_id = Fuel.fuel_id
) as data
pivot (
count(fuel_name)
for fuel_name in (Gas, Diesel, Electric)
) as pivot_table;
See this fiddle, which outputs a table like this:
| car_name | Gas | Diesel | Electric |
|----------|-----|--------|----------|
| Jaguar | 0 | 1 | 0 |
| Mercedes | 0 | 1 | 1 |
| Volvo | 1 | 0 | 1 |
The SQL statement still has the hard-coded list in the for clause of the pivot part, but when the number of fuel types increases, this might be easier to manage and have better performance.
Generating the SQL dynamically
If you use an application server, you could first execute this query:
SELECT stuff( ( SELECT ',' + fuel_name
FROM Fuel FOR XML PATH('')
), 1, 1, '') columns
This will return the list of columns as one comma-separated value, for example:
Gas,Diesel,Electric
You would grab that result and inject it in the first query in the FOR clause.

I would suspect using counts would be inefficient as there would be a large number of sub queries running to total all the counts.
Below is an alternative using self joins. It's not as short as your example but may be easier to maintain and read and should be more efficient.
select car.car_id, car.car_name,
-- Select fuel variables
CASE WHEN lpg.fuel_id IS NULL THEN 0 ELSE 1 END AS LPG,
CASE WHEN unleaded.fuel_id IS NULL THEN 0 ELSE 1 END AS Unleaded,
CASE WHEN electric.fuel_id IS NULL THEN 0 ELSE 1 END AS Electric,
CASE WHEN diesel.fuel_id IS NULL THEN 0 ELSE 1 END AS Diesel
FROM car
-- Self Join fuel records
LEFT join car_fuel as lpg on car.car_id = lpg.car_id and lpg.fuel_id = 1
LEFT join car_fuel as unleaded on car.car_id = unleaded.car_id and unleaded.fuel_id = 2
LEFT join car_fuel as electric on car.car_id = electric.car_id and electric.fuel_id = 3
LEFT join car_fuel as diesel on car.car_id = diesel.car_id and diesel.fuel_id = 4
The self join will return a NULL if the car doesn't use that fuel type. The CASE returns 1 if the join found a record for that car/fuel and 0 if it didn't.
I hope this help.

You could use conditional aggregation.
Do an outer join to the Car_Fuel table, and do a GROUP BY Car_ID to collapse the rows.
For each row from Car_Fuel, return a 1 if the Fuel_ID matches the one you are checking for, otherwise return a 0. And use a MAX() aggregate to filter the rows, finding out if any of them returned a 1.
For example:
SELECT c.Car_ID
, c.Car_Name
, MAX(CASE WHEN f.Fuel_ID=1 THEN 1 ELSE 0 END) AS Can_Use_Gas
, MAX(CASE WHEN f.Fuel_ID=2 THEN 1 ELSE 0 END) AS Can_Use_Diesel
, MAX(CASE WHEN f.Fuel_ID=3 THEN 1 ELSE 0 END) AS Can_Use_Electric
FROM Car c
LEFT
JOIN Car_Fuel f
ON f.Car_ID = c.Car_ID
GROUP
BY c.Car_ID
, c.Car_Name
With SQL Server, you'd need to repeat every non-aggregate expression in the SELECT list in the GROUP BY clause. If you add more columns from the Car table to SELECT list, you'll have to copy those down to the GROUP BY.
If that's too painful, you could do the aggregation in an inline view instead, and then do the JOIN. To make sure a NULL doesn't get returned, you can replace a NULL value with a 0, in the outer query:
For example:
SELECT c.Car_ID
, c.Car_Name
, ISNULL(u.Can_Use_Gas,0) AS Can_Use_Gas
, ISNULL(u.Can_Use_Diesel,0) AS Can_Use_Diesel
, ISNULL(u.Can_Use_Electric,0) AS Can_Use_Electric
FROM Car c
LEFT
JOIN ( SELECT f.Car_ID
, MAX(CASE WHEN f.Fuel_ID=1 THEN 1 ELSE 0 END) AS Can_Use_Gas
, MAX(CASE WHEN f.Fuel_ID=2 THEN 1 ELSE 0 END) AS Can_Use_Diesel
, MAX(CASE WHEN f.Fuel_ID=3 THEN 1 ELSE 0 END) AS Can_Use_Electric
FROM Car_Fuel f
GROUP BY f.Car_ID
) u
ON u.Car_ID = c.Car_ID

Related

Use one select query instead of query + subquery

SQL Server 2014, data has been changed to protect the data. I hope the below makes sense.
I have to search a table for all categories with a Moderate risk. Then I need to go into another table (test) and retrieve those Moderate categories where only the Toby test has failed. Categories are unique in that first table.
So in the sample data below, categories 4 and 5 both are Moderate risk, and both also have Toby with a result of Fail in the test table.
However I want to exclude category 4 from my final output because that Category also has the Bill test that failed.
My goal is only to show Category 5 as an output. I can do this with a query and sub-query, where the sub-query returns categories 4 and 5, and the main query filters on that. But can I achieve the same thing with a single query somehow?
Update:
My current query is below. I've had to munge it a bit for this post, I hope it's sufficient. Basically the sub-query pulls in all categories that have any failed test for a Moderate category, and the main query filters out any categories with other failures.
select tt.category, tt.[name]
from test_table mt
where tt.category in (select mt.category
from main_table mt
inner join test_table tt on tt.category = mt.category
where mt.risk= 'Moderate' and tt.result = 'Fail')
and tt.[name] <> 'Toby'
and tt.[result] = 'Fail'
Output:
category risk
----------------------
1 Minimal
2 Critical
3 Elevated
4 Moderate
5 Moderate
category name result
-------------------------------
1 Mark Pass
1 Bill No Result
1 John Pass
1 Toby Pass
2 Mark Pass
2 Bill No Result
2 John Fail
2 Toby Pass
3 Mark Pass
3 Bill No Result
3 John Pass
3 Toby Pass
4 Mark Pass
4 Bill Fail
4 John Pass
4 Toby Fail
5 Mark Pass
5 Bill Pass
5 John Pass
5 Toby Fail
Join the tables, group by category and set the conditions in the HAVING clause:
select c.category
from categories c inner join test t
on c.category = t.category
where c.risk = 'Moderate'
group by c.category
having
sum(case when t.name = 'Toby' and t.result = 'Fail' then 1 else 0 end) > 0
and
sum(case when t.name <> 'Toby' and t.result = 'Fail' then 1 else 0 end) = 0
or:
select c.category
from categories c inner join test t
on c.category = t.category
where c.risk = 'Moderate' and t.result = 'Fail'
group by c.category
having count(distinct t.name) = 1 and max(t.name) = 'Toby'
See the demo.
Results:
> | category |
> | -------: |
> | 5 |
A pretty direct reading of your question suggests exists/not exists:
select c.*
from categories c
where c.risk = 'Moderate' and
exists (select 1
from tests t
where t.category = c.category and
t.name = 'Toby' and
t.result = 'Fail'
) and
not exists (select 1
from tests t
where t.category = c.category and
t.name <> 'Toby' and
t.result = 'Fail'
);
Conditional aggregation is also a viable solution -- and a solution that I prefer when the filter is on only one table. However, the filtering here is on two tables, and this will use of an index on test(category, result, name) and avoid the outer aggregation.

Use Count function inside conditional part of Case Expression

I have two tables with the following sample records in oracle database
1. staffs
inst_name name sid
ABC John 1
PQR Sam 2
ABC Tom 3
ABC Amit 4
PQR Jack 5
2. staffaccounts
sid account_no
1 4587
1 4588
2 4589
3 4581
3 4582
5 4583
5 4585
4 4586
Where I want the result like
inst_name account_type total
PQR SINGLE 1
ABC SINGLE 1
PQR DOUBLE 1
ABC DOUBLE 2
This can be achieved by a outer query, but I want to write a query where there is no outer query. Want to accomplish it in one single query.
SELECT
A .inst_name,
(
CASE COUNT (b.ac_no)
WHEN 1 THEN
'Single'
WHEN 2 THEN
'Double'
END
) account_type,
COUNT (A . NAME)
FROM
staffs A,
staffaccounts b
WHERE
A . s_id = b.s_id
GROUP BY
A .inst_name
The above query gives error ORA-00907: missing right parenthesis. Can it be done in single query or is outer query the only way out.
Oracle Version is 10g
May be something like this would work.
SELECT
A.inst_name,
CASE COUNT (b.account_no)
WHEN 1 THEN
'Single'
WHEN 2 THEN
'Double'
END account_type,
COUNT (A.name)
FROM
staffs A JOIN
staffaccounts b
ON
A.SID = b.sid
GROUP BY
A.inst_name , a.sid
ORDER BY 3;
You are grouping by inst_name, but this is not what you actually want, because you don't want a result row per inst_name, but per inst_name and account_type.
select
s.inst_name,
sa.account_type,
count(*) as total
from staffs s
join
(
select
sid,
case when count(*) = 1 then 'SINGLE' else 'DOUBLE' end as account_type
from staffaccounts
group by sid
having count(*) <= 2
) sa on sa.sid = s.sid
group by sa.account_type, s.inst_name
order by sa.account_type, s.inst_name;
You should learn how to properly use JOIN syntax. I prefer the explicit comparison syntax for CASE.
This may be what you want:
SELECT s.inst_name,
(CASE WHEN COUNT(sa.ac_no) = 1 THEN 'Single'
WHEN COUNT(sa.ac_no) = 2 THEN 'Double'
END) as account_type,
COUNT(*)
FROM staffs s JOIN
staffaccounts sa
ON s.SID = sa.sid
GROUP BY s.inst_name;
EDIT:
Now I see what you want:
SELECT s.inst_name,
(CASE WHEN cnt = 1 THEN 'Single'
WHEN cnt = 2 THEN 'Double'
END) as account_type,
COUNT(*)
FROM (SELECT s.*, COUNT(*) as cnt
FROM staffs s JOIN
staffaccounts sa
ON s.SID = sa.sid
GROUP BY s.id
) s
GROUP BY s.inst_name,
(CASE WHEN cnt = 1 THEN 'Single'
WHEN cnt = 2 THEN 'Double'
END);
I got only the way by using subquery but is in the easy way (more easier and readable) to achieve your requirement
SELECT inst_name, account_type, count(total) as total
FROM (
SELECT
a.inst_name,
CASE
WHEN COUNT (b.account_no) = 1 THEN 'Single'
WHEN COUNT (b.account_no) = 2 THEN 'Double'
END AS account_type,
COUNT (a.name) AS total
FROM staffs a
INNER JOIN staffaccounts b ON A . SID = b.sid
GROUP BY a.inst_name, a.sid) t GROUP BY inst_name, account_type
OUTPUT:
inst_name account_type total
ABC Double 2
PQR Double 1
ABC Single 1
PQR Single 1

Get separate count for each condition within group

I am trying to get a view of table information from and Oracle 10g table that lists the counts of specific values of a column in their own columns with each row being the group value.
for example:
The first select is :
SELECT processed_by, count(priority) as P2
FROM agreement_activity
WHERE priority = '2'
GROUP BY processed_by
Which outputs:
PROCESSED_BY P2
------------------------------ ----------
Alicia 2
Christine 2
The second select is:
SELECT processed_by, count(priority) as P1
FROM agreement_activity
WHERE priority = '1'
GROUP BY processed_by
Which outputs:
PROCESSED_BY P1
------------------------------ ----------
Bonita 2
Alicia 6
Christine 2
What I am looking for is to output those values as the following:
PROCESSED_BY P1 P2
------------------------------ ---------- ----------
Bonita 2
Alicia 6 2
Christine 2 2
Is that possible?
You can use sum with the case expression to get conditional count:
select processed_by
, sum(case when priority = 1 then 1 else 0 end) as P1
, sum(case when priority = 2 then 1 else 0 end) as P2
from agreement_activity
group by processed_by
P.S. If you don't care that P1 or P2 maybe null instead of 0 you can omit else in both expressions.
this is how I implement the sql. I'm using firebird code but I think you can convert the code into your sql
SELECT
a.equipmentid,
a.name equipname,
w1.countwarranty1 ,
w2.countwarranty2
FROM TBL_EQUIPMENTMST a
inner JOIN
(select c.equipmentid, count(c.WARRANTYID) countwarranty1 from tbl_equipwarranty c where c.serviceproduct='1' group by c.equipmentid) w1
ON w1.equipmentid = a.equipmentid
inner JOIN
(select d.equipmentid, count(d.WARRANTYID) countwarranty2 from tbl_equipwarranty d where d.serviceproduct='2' group by d.equipmentid) w2
ON w2.equipmentid = a.equipmentid
inner JOIN
(select e.equipmentid, count(e.equiplocationid) countlocation from tbl_equiplocation e group by e.equipmentid) w3
ON w3.equipmentid = a.equipmentid
this is the output
My warranty table has only 2 warranty for equipment each that's why its only showing 2.
you can also inner join to the same table with different count.
as you can see my warranty has multiple warranty where serviceproduct is different on each table
if I edit your code it will be like this
SELECT a.processed_by, b.priority as p2, c.priority as p1
FROM agreement_activity a
inner join
(
SELECT w1.processed_by, count(w1.priority) as P2 FROM agreement_activity w1
WHERE w1.priority = '2' GROUP BY w1.processed_by
) b
on a.processed_by = b.processed_by
inner join
(
SELECT w2.processed_by, count(w2.priority) as P2 FROM w2.agreement_activity
WHERE w2.priority = '1' GROUP BY w2.processed_by
) c
on a.processed_by = c.processed_by
test it

SQL: Count each row value in a column and return count into multiple column

From this table
D_value
-------
A
B
B
C
C
C
To display count D_Value into this table
A | B | C
---------
1 | 2 | 3
To get a single row, use this.
SELECT
COUNT(CASE WHEN D_Value = 'A' THEN 1 END) AS A,
COUNT(CASE WHEN D_Value = 'B' THEN 1 END) AS B,
COUNT(CASE WHEN D_Value = 'C' THEN 1 END) AS C
FROM
MyTable
This works for a finite and fixed number of values to count
If you do not know how many different values, then you need to do a simple aggregate and make a single row in the client code.
SELECT D_value, COUNT(*) FROM MyTable GROUP BY D_value;
However, this won't give zero counts for values that do not exist. So you'd need a lookup table and LEFT JOIN. I won't go there yet...
select D_value,count(*) from table group by D_value;

SQL - Group By Distinct Values

My question, is there a faster way to the following query?
I'm using ORACLE 10g
Say i have a table Manufacturer and Car, and i want to count all occurrences of the column 'Car.Name'. here is How i'd do it:
SELECT manuf.Name, COUNT(car1.Name), COUNT(car2.Name), COUNT(car3.Name)
FROM Manufacturer manuf
LEFT JOIN (SELECT * FROM Car c where c.Name = 'Ferrari1') car1 ON manuf.PK = car1.ManufPK
LEFT JOIN (SELECT * FROM Car c where c.Name = 'Ferrari2') car2 ON manuf.PK = car2.ManufPK
LEFT JOIN (SELECT * FROM Car c where c.Name = 'Ferrari3') car3 ON manuf.PK = car3.ManufPK
GROUP BY manuf.Name
Wanted Results:
Manufacturer | Ferrari1 | Ferrari2 | Ferrari3
----------------------------------------------
Fiat | 1 | 0 | 5
Ford | 2 | 3 | 0
I tried this with few LEFT JOINs, and it worked fine. But when i added a lot (like 90+), it was ultra slow (more than 1 minute).
My question, is there a faster way to do this query?
If you are happy to see the cars counted down the page, try:
select m.Name manufacturer_name,
c.Name car_name,
count(*)
from Manufacturer m
left join Car c
on m.PK = c.ManufPK and c.Name in ('Ferrari1','Ferrari2','Ferrari3')
group by m.Name, c.Name
If you need to see individual cars across the page, try:
select m.Name manufacturer_name,
sum(case c.Name when 'Ferrari1' then 1 else 0 end) Ferrari1_Count,
sum(case c.Name when 'Ferrari2' then 1 else 0 end) Ferrari2_Count,
sum(case c.Name when 'Ferrari3' then 1 else 0 end) Ferrari3_Count
from Manufacturer m
left join Car c
on m.PK = c.ManufPK and c.Name in ('Ferrari1','Ferrari2','Ferrari3')
group by m.Name
SELECT manuf.Name, COUNT(DISTINCT c.Name)
FROM Manufacturer manuf
LEFT JOIN Car c ON manuf.PK = c.ManufPK
GROUP BY manuf.Name
OR depending on your needs
SELECT manuf.Name, c.Name, COUNT(*) Cnt
FROM Manufacturer manuf
LEFT JOIN Car c ON manuf.PK = c.ManufPK
GROUP BY manuf.Name, c.Name
PS: Your question is not very clear. Provide some wanted resultset to refine the answer
You can also try this:
SELECT manuf.Name
, car1.cnt AS Ferrari1
, car2.cnt AS Ferrari2
, car3.cnt AS Ferrari3
FROM
Manufacturer AS manuf
LEFT JOIN
( SELECT ManufPK, COUNT(*) AS cnt
FROM Car
WHERE Name = 'Ferrari1'
GROUP BY ManufPK
) AS car1
ON car1.ManufPK = manuf.PK
LEFT JOIN
( SELECT ManufPK, COUNT(*) AS cnt
FROM Car
WHERE Name = 'Ferrari2'
GROUP BY ManufPK
) AS car2
ON car2.ManufPK = manuf.PK
LEFT JOIN
( SELECT ManufPK, COUNT(*) AS cnt
FROM Car
WHERE Name = 'Ferrari3'
GROUP BY ManufPK
) AS car3
ON car3.ManufPK = manuf.PK
ORDER BY manuf.Name