Use one select query instead of query + subquery - sql

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.

Related

CASE expression sum - issue when joining to other tables

I have a case expression in a stored procedure summing an account field, and then inserting into a user id. The logic works... until joining to another table.
I tried adding distinct counts, and additional tables to the query, but still when I join to another table it applies the 1 value when I want it to be 0 to the account.
This is the calculation in the stored proc;
INSERT INTO #SUMMARY_TEMP (USER_ID,FSN_CNT )
(SELECT USER_ID,
SUM(CASE WHEN A_GROUP_CD = 'RED' AND A_TYPE_CD = 'FSN' THEN REC_COUNT ELSE 0 END)
) AS 'FSN_CNT',
FROM (SELECT A_ACCOUNT_NBR,
A_USER_ID,
A_GROUP_CD,
A_TYPE_CD,
COUNT(*) AS REC_COUNT
FROM EXCEPTION_DETAIL
INNER JOIN #STAFF ON A_REPORT_DT = #REPORT_DT
AND (A_USER = B_USER_ID)
GROUP BY A_ACCOUNT_NBR,
A_USER_ID_ID,
A_GROUP_CD,
A_TYPE_CD) EXCEPTIONS
GROUP BY A_USER_ID,
A_ACCOUNT_NBR)
This is the result which is what I expect for 2 USER Ids
A_ACCOUNT_NBR USER_ID FSN_CNT
123456 HENRY 0
123498 HENRY 1
374933 JOE 1
474930 JOE 0
but when I join to another table the data looks like
A_ACCOUNT_NBR USER_ID FSN_CNT
123456 HENRY 1
123498 HENRY 1
374933 JOE 1
474930 JOE 1
Its applying the 1 value to account 123456 & 474930 when it should be 0.
I think its because the other table does not have the ACCOUNT_NBR column - I am joining on USER_ID and so it applies the 1 to all ACCOUNT_NBR from table A.
Thanks for all the suggestions, I tried using a CTE, and the counts now look good, but its created duplicate rows as shown below. Any suggestions on how to remove the duplication, below is the join I am using for the CTE;
select cte.*, jt.USER_ID
from cte
join EXCEPTION_DETAIL jt on cte.USER_ID=jt.USER_ID
USER ACCOUNT_NBR FSN_CNT
HENRY 123456 0
HENRY 123456 0
HENRY 123498 1
HENRY 123498 1
JOE 374933 1
JOE 374933 1
JOE 474930 0
JOE 474930 0
you can separate the 1st query by using cte and join with it next level like below
with cte as
(
(SELECT USER_ID,
SUM(CASE WHEN A_GROUP_CD = 'RED' AND A_TYPE_CD = 'FSN' THEN REC_COUNT ELSE 0 END)
) AS 'FSN_CNT',
FROM (SELECT A_ACCOUNT_NBR,
A_USER_ID,
A_GROUP_CD,
A_TYPE_CD,
COUNT(*) AS REC_COUNT
FROM EXCEPTION_DETAIL
INNER JOIN #STAFF ON A_REPORT_DT = #REPORT_DT
AND (A_USER = B_USER_ID)
GROUP BY A_ACCOUNT_NBR,
A_USER_ID_ID,
A_GROUP_CD,
A_TYPE_CD) EXCEPTIONS
GROUP BY A_USER_ID,
A_ACCOUNT_NBR)
) select cte.*,jt.USER_ID from cte join jointable_name jt on cte.USER_ID=jt.USER_ID

SQL list users from table one and count / compare in the other table where value is NULL

I have a table 'users':
id | name
1 Jane
2 David
3 Olivia
The other table 'general' :
ida | iduser | discription | motivation
1 2 bla bla Because we need it
2 3 bla again NULL
3 3 test We do not have this yet
4 2 another item certainly necessary
...
The result I want to achive is a list of the users from table 'users' and look up in the other table 'general' that for user 2 there is no result NULL in the column motivation so the end result for user 2 is 'TRUE' and for user 3 there is a result with NULL so the end result for this is 'FALSE'.
So I want to have a result that loop out something like this:
name | motivation
Jane TRUE
David TRUE
Olivia FALSE
....
How can I achieve the result?
The answer by Gordon (+1) might be the most efficient, but we can also try aggregating on the general table and joining to that:
SELECT
u.name,
CASE WHEN t.iduser IS NULL THEN 'TRUE' ELSE 'FALSE' END AS motivation
FROM users u
LEFT JOIN
(
SELECT iduser
FROM general
GROUP BY iduser
HAVING SUM(CASE WHEN motivation IS NULL THEN 1 ELSE 0 END)
) t
ON u.id = t.iduser;
Demo
I would use not exists:
select u.*,
(case when exists (select 1
from general g
where g.iduser = u.id and g.motivation is null
)
then 0 else 1
end) as demotivated_flag
from u;
for sql server:
select u.*, ISNULL(c.truefalse, 'true') motivation
from users u
outer apply (
select top 1 'false' truefalse
from general g
where g.id = u.id
and g.motivation is null
) c

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

Find all books where availability is = 0 or unknown

I have a table Books which has many properties. Properties are stored as key and value.
So if Books are:
1 LOTR
2 Harry Potter 1
3 Harry Potter 2
And properties are
id book_id key value
1 1 available 0
2 2 available 10
3 2 author Rowling
4 3 author Rowling
I'd like to get the results as:
1 LOTR
3 Harry Potter 2
since Book id 1 has 0 availability, and 2 has 10, and 3 does not have any availability info.
I know I can work with anti join, but I am not sure how to use it. I'm kind of new to anti joins.
Any help is appreciated.
I'm not 100% sure I'm understanding your question, but assuming you want to return all books that have no availability in the properties table, then here's one option using an outer join:
select b.*
from books b
left join properties p on b.id = p.book_id and p.key = 'available' and p.value > 0
where p.id is null
Depending on your database, you may need to cast the value column in the join.
Try this:
SELECT b.book_id, a.key, a.value
FROM Books AS B INNER JOIN AnotherTable AS A B.book_id = a.book_id
WHERE a.key = 'available' and (a.value = 0 OR a.value is null)
SELECT book_id, title
FROM Books as B
WHERE B.book_id NOT IN (
SELECT P.book_id
FROM properties as P
WHERE P.key = available AND P.value <> 0)
Note that <> means NOT EQUAL

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

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