How to have IN and NOT IN at same time - sql

Can someone help me to figure out how is the best way to do this?
I have a list of people with cars. I need to execute a query that will return people that have a type of car and don't have another type at the same time.
Here is my example:
ID Name CarType
----------- ---------- ----------
1 John MINI VAN
1 John SUV
2 Mary SUV
2 Mary SEDAN
3 Paul SPORT
3 Paul TRUCK
4 Joe SUV
4 Joe MINI VAN
For instance, I want to display only people that have SUV AND DON'T have MINI VAN. If we try the clause CarType IN ('SUV') AND NOT IN ('MINI VAN'), this will not work, because the second statement is just ignored.
In order to return people that have a type but don't have another type at the same time, I tried the following:
Create a temporary table with the IN clause, let's say #Contains
Create a temporary table with the NOT IN clause, let's say #DoesNotContain
Join table with #Contains, this will do the IN clause
On the where clause, look for IDs that are not in #DoesNotContain table.
The query that I am using is this:
--This is the IN Clause
declare #Contains table(
ID int not null
)
--This is the NOT IN Clause
declare #DoesNotContains table(
ID int not null
)
--Select IN
insert into #Contains
SELECT ID from #temp where CarType = 'SUV'
--Select NOT IN
insert into #DoesNotContains
SELECT ID from #temp where CarType = 'MINI VAN'
SELECT
a.ID, Name
FROM
#temp a
INNER JOIN #Contains b on b.ID = a.ID
WHERE
a.ID NOT IN (SELECT ID FROM #DoesNotContains)
Group by
a.ID, Name
This will return Mary because she has a SUV but does not have a MINI VAN.
Here are my questions:
Is it possible to execute this IN and NOT IN in the query, without temp tables? Is there something new in SQL that does that? (Sorry, last time I worked with SQL was SQL 2005)
Should we use temp tables for this?
If this is the way to go, should I use IN and NOT IN instead of the JOIN?
How to replace the NOT IN clause with a JOIN?
Thank y'all!
EDIT
I just tested the solutions but unfortunately I did not specify that I need a combination of cartypes. My bad :(
For instance, if I want all users that have SUV and MINI VAN but not TRUCK AND NOT SEDAN. In this case it only John is returned.

This is normally accomplished with a single query in standard SQL, using NOT EXISTS:
SELECT *
FROM mytable AS t1
WHERE CarType = 'SUV' AND
NOT EXISTS (SELECT *
FROM mytable AS t2
WHERE t1.Name = t2.Name AND t2.CarType = 'MINI VAN')
The above query will select all people having CarType = 'SUV', but do not have CarType = 'MINI VAN'.

Here's one way
SELECT Id, Name
FROM Cars
WHERE CarType = 'SUV'
EXCEPT
SELECT Id, Name
FROM Cars
WHERE CarType = 'MINI VAN'
Or another
SELECT Id, Name
FROM Cars
WHERE CarType IN ('SUV', 'MINI VAN')
GROUP BY Id, Name
HAVING MIN(CarType) = 'SUV'
Or a more generic version that addresses the different requirement in the comment.
SELECT Id,
NAME
FROM Cars
WHERE CarType IN ( 'SUV', 'MINI VAN', 'TRUCK')
GROUP BY Id,
NAME
HAVING COUNT(DISTINCT CASE
WHEN CarType IN ( 'SUV', 'MINI VAN' ) THEN CarType
END) = 2
AND COUNT(DISTINCT CASE
WHEN CarType IN ( 'TRUCK' ) THEN CarType
END) = 0

Using LEFT JOIN:
SELECT a.ID,
Name
FROM #temp a
INNER JOIN #Contains b ON b.ID = a.ID
LEFT OUTER JOIN #DoesNotContains c ON c.ID = a.ID
WHERE c.ID IS NULL
The INNER JOIN will return records where b.ID and a.ID match.
The LEFT OUTER JOIN returns all records, with NULL where there is no match - adding WHERE c.ID IS NULL returns records from a that don't match to c.

The keyword except is your friend. This is the general idea
where carType in
(select carType
from cars
where you want to include them
except
select carType
from cars
where you want to exclude them)
You can work out the details.

Related

Return first not null result from a column

On SQL Server, I have the following query (minimized):
SELECT A.ID, A.OWNER, B.CAR
FROM TABLE A
LEFT JOIN TABLE B ON A.ID = B.CAR_ID
Which returns the following:
ID Owner Car
01 Bob BMW
02 Bob NULL
03 Bob BMW
04 Andy Audi
05 Andy Audi
I want to Group By Owner with the first not NULL result for car to get:
Owner Car
Bob BMW
Andy Audi
I could do:
SELECT A.OWNER, max(B.CAR) as Car
FROM TABLE A
LEFT JOIN TABLE B ON A.ID = B.CAR_ID
GROUP BY A.OWNER
But, is there way to do this with Coalesce()? Or something else that might work better with a more complex query?
When a car is present, your result-set always associates 'Bob' with 'BMD' and 'Andy' with 'Audi'. I assume, however, that in the real dataset there are owners that can have more than one type of car. So the question then becomes: "which one do you choose?".
If it's really arbitrary and doesn't matter, then your existing approach using 'max' is fine. At least it has a predictable default ordering so that you'll get the same output on every run given the same state of data in the base tables.
However, if something else should count as 'first', such as if you wanted to base the comparison on the 'id' field, then you're going to want to use 'row_number' to order by that field for within each owner, such as in the code below.
select owner, car
from (
select *,
ord = row_number() over(partition by owner order by id)
from [Table A] a
left join [Table B] b on a.id = b.car_id
where b.car is not null
) orderings
where ord = 1
I'm not sure what you mean by first result. If you want to go by the default order, you could do:
If you were ordering by ID, then it would be
SELECT DISTINCT FIRST_VALUE(Owner) OVER(PARTITION BY Owner ORDER BY ID), FIRST_VALUE(Car) OVER(PARTITION BY Owner ORDER BY ID)
FROM Table_Name WHERE Car IS NOT NULL
You could do the following:
SELECT TOP 1 WITH TIES A.OWNER, B.CAR
FROM TABLE A
LEFT JOIN TABLE B ON A.ID = B.CAR_ID
ORDER BY ROW_NUMBER() OVER (PARTITION BY A.OWNER ORDER IIF(B.CAR IS NOT NULL, 0, 1), A.ID)
By splitting the ORDER BY in two, you place all NULL's last, followed by ordering on the given ID in your resultset. Each distinct A.OWNER will receive ROW_NUMBER() 1. Using TOP 1 WITH TIES you're left with all 1's without using a subquery, thus having only one row per A.OWNER.

SQL - Joining a table over itself to find people with same parents

I have table like this:
*Id, Name, Surname, Father Name, Mother Name
---------------------------------------------
*1, John, Green, James, Sue
*2, Michael, Sloan, Barry, Lilly
*3, Sally, Green, Andrew, Molly
*4, Michael, Sloan, Barry, Lilly
*5, Ned, White, James, Sue
I want a query that selects rows with the same father name and mother name for given first names. For the example table, when I want to select Johns and Neds with same parents, query should return
1, John, Green, James, Sue
5, Ned, White, James, Sue
I tried joining table with itself but no matter how I change the where criteria it returned a cartesian product. Any tips?
Use sub-query
SELECT * FROM Table
WHERE (FatherName, MotherName) IN
(SELECT FatherName, MotherName FROM Table WHERE Name='John')
What you need is called relational division. However, it's slightly trickier in your case, since it normally returns aggregated data, and you need all rows from the table. So, indeed, a self join is required:
select t.*
from dbo.Table t
inner join (
select d.FatherName, d.MotherName
from dbo.Table d
group by d.FatherName, d.MotherName
having count(*) > 1
) sq on sq.FatherName = t.FatherName
and sq.MotherName = t.MotherName;
In the subquery, you select only Father+Mother combinations that have more than 1 entry in the table, and then join it with the table again to output everything from these parents' pairs.
You can try this (without group by and count(*))
with fm as
(select fathername, mothername,row_number() over (partition by fathername,
mothername order by id) rownum
from #tmp1
)
select b.*
from #tmp1 b
join fm
on b.fathername = fm.fathername
and b.mothername = fm.mothername
where fm.rownum = 2
Self-join is appropriate method at this case.
SELECT DISTINCT t1.*
FROM MyTable AS t1
INNER JOIN MyTable AS t2
ON t1.FatherName=t2.FatherName
AND t1.MotherName=t2.MotherName
AND t1.Id<>t2.Id
WHERE t1.Name in ('John', 'Ned')

Select duplicated field name

If I have the following scenario
Table that store people
id_person, name, age (...)
And a table that stores address of people
id_address, id_person, city
If I run a query like this
select * from people P left join address A on P.id_person = A.id_person
I'm getting id_person === null in result set (because there IS a person, but no address has been recorded it, which is fine).
The null is comming from the table address. Is it possible to solve this without doing select field1, field2, field3 ... (lots os fields)?
Example
Person
id_person Name
1 John
2 Steve
Address
id_address id_person city
1 1 'AnyCity'
When I run a query like this
select * from people P left join address A on P.id_person = A.id_person
where P.name = 'Steve'
His id_person is returning null
You mean you only want the id_person from the people table, not from the address table (which sometimes is NULL)?
select p.id_person, p.name, p.age, a.id_address, a.city
from people P left join address A ON P.id_person = A.id_person
Is it possible to solve this without doing select field1, field2, field3 ... (lots os fields)
No - you either use * or identify the fields. You could select all fields from one table and then cherry pick from the other table:
select P.*, A.address, A.City, ...
from people P
left join address A where P.id_person = A.id_person

How to fetch the non matching rows in Oracle

Can anyone help me fetch the non matching rows from two tables in Oracle?
Table: Names
Class_id Stud_name
S001 JAMES
S001 PETER
S002 MARK
Table: Course
Course_id Stud_name
S001 JAMES
S001 KEITH
S002 MARK
Output
I need the rows to display as
CLASS ID STUD_NAME_FROM_NAME_TABLE STUD_NAME_FROM_COURSE_TABLE
---------------------------------------------------------------------
S001 PETER KEITH
I have used Oracle joins to fetch the non matching names:
SELECT *
FROM Names, Course
WHERE Names.Class_id=Course.Course_id
AND Names.Stud_name<>Course.Stud_name
This query is returning duplicate rows.
If you insist on Join you can use this one:
SELECT *
FROM Names
FULL OUTER JOIN Course ON Names.Class_id=Course.Course_id
AND Names.Stud_name = Course.Stud_name
WHERE Names.Stud_name IS NULL or Course.Stud_name IS NULL
Fetches unmatched rows in Names table
SELECT * FROM Names
WHERE
NOT EXISTS
(SELECT 'x' from Course
WHERE
Names.Class_id = Course.Course_id AND
Names.Stud_name = Course.Stud_name)
Fetches unmatched rows in Names and Course too!
SELECT Names.Class_id,Names.Stud_name,C1.Stud_name
FROM Names , Course C1
WHERE Names.Class_id = C1.Course_id AND
NOT EXISTS
(SELECT 'x' from Course C2
WHERE
Names.Class_id = C2.Course_id AND
Names.Stud_name = C2.Stud_name);
When you ask for unmatching rows I assume that you want rows that exist in names but not in course.
If this is the case you're probably after
select * from names
where (class_id, stud_name ) not in
(select course_id, stud_name from course);
Your query returned duplicate rows beacuse for each row in names it selected all rows in course that satisfied the where condition.
So, for the row S001, PETER in names it faound that S001, JAMES and S001, KEITH matched that condition, thus, that row was "returned" twice.
EDIT Since it is not clear if stud_name is a primary key, or unique (and on second sight I think it's not), you'd probably want a
select * from names
where not exists (
select 1 from course where
names.class_id = course.course_id and
names.stud_name <> course.stud_name
)
Edit II if you insist on using a join (as per your comment) you might want to try a
select distinct names.* from...
Hope it helps you
with not_in_class as
(select a.*
from Names a
where not exists ( select 'x'
from course b
where b.Course_id = a.class_id
and a.Stud_name = b.Stud_name)),
not_in_course as
(select b.*
from course b
where not exists ( select 'x'
from Names a
where b.Course_id = a.class_id
and a.Stud_name = b.Stud_name))
select x.class_id,
x.Stud_name NOT_IN_CLASS,
y.stud_name NOT_IN_COURSE
from not_in_class x, not_in_course y
where x.class_id = y.course_id
Output
| CLASS_ID | NOT_IN_CLASS | NOT_IN_COURSE |
|----------|--------------|---------------|
| S001 | PETER | KEITH |
Only problem is that if multiple mismatches are there in both the tables for a given id, it works for single mismatch for a particular id. You need to rework if multiple mismatches are there for the same id.
Well, I am not sure if I understand correctly what you are asking. I think you want a list of all IDs where the student list in class table and course table differs. Then you want to show the id and the students that are in class but not in course and the students that are in course but not in class.
To do so you would full outer join the tables. That gives you students that are both in class and course, students that are in class and not in course, and students that are in course and not in class. Filter your results where either class_id or course_id is null then to get the students missing in course or class. At last group by id and list the students.
select coalesce(class.class_id, course.course_id) as id
, listagg(class.stud_name, ',') within group (order by class.stud_name) as missing_in_course
, listagg(course.stud_name, ',') within group (order by course.stud_name) as missing_in_class
from class
full outer join course
on (class.class_id = course.course_id and class.stud_name = course.stud_name)
where class.class_id is null or course.course_id is null
group by coalesce(class.class_id, course.course_id);
Here is the SQL fiddle showing how it works: http://sqlfiddle.com/#!4/8aaaa/2
EDIT: In Oracle 9i there is no listagg. You can use the inofficial function wm_concat instead:
select coalesce(class.class_id, course.course_id) as id
, wm_concat(class.stud_name) as missing_in_course
, wm_concat(course.stud_name) as missing_in_class
from class
full outer join course
on (class.class_id = course.course_id and class.stud_name = course.stud_name)
where class.class_id is null or course.course_id is null
group by coalesce(class.class_id, course.course_id);

Most efficient SQL for this example

Table A: Person: id, name
Table B: Toys: id, person_id, toy_name
I have a search screen that includes a dropdown of fixed toy names.
A search is found if a subset of the total set of toys for a person is matched.
Example, a person name=bob has toys: doll, car, house, hat
A search is done for person name=bob and toys=doll, hat.
I want to return bob and ALL of his toys, not just what toys were searched for(doll, hat).
Bob is found because a subset of his toys are a match.
I don't know what the most efficient/least db calls way to accomplish this.
I can do a search for bob and get all of his toys, then parse through the result set to see if the searched for toys find a match, but that seems wrong, that the db call could return rows for which no match is found (and that seems wrong?).
okay,
select
p.id,
p.name,
t.id as toyid,
t.toy_name
from
person p
join
toys t
on p.id = t.person_id
where
p.id in (
select person_id from toys where toy_name = 'doll'
intersect
select person_id from toys where toy_name = 'hat');
Fiddle Here
If you normalise your schema a little further,
create table Person
(
Id int,
Name varchar(100)
);
create table Toy
(
Id int,
Name varchar(100)
);
create table PersonToy
(
Id int,
PersonId int,
ToyId int
);
It should make the complexity of the problem clearer. It will also save some space. A statement of the form,
select
p.Name PersonName,
t.Name ToyName
from
Person p
join
PersonToy pt
on pt.PersonId = p.Id
join
Toy t
on t.Id = pt.ToyId
where
p.Id in
(
select PersonId from PersonToy where ToyId = 1
intersect
select PersonId from PersonToy where ToyId = 4
);
will work efficiently.
Updated Fiddle
Here's one way to do it using a subquery and checking for the existence of Hat and Doll in the HAVING clause:
select p.id, p.name,
t.id as toyid, t.name as toyname
from person p
inner join toys t on p.id = t.person_id
inner join (
select person_id
from toys
group by person_id
having sum(name = 'hat') > 0 and
sum(name = 'doll') > 0
) t2 on p.id = t2.person_id
SQL Fiddle Demo