Most efficient SQL for this example - sql

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

Related

Oracle sql - referencing tables

My school task was to get names from my movie database actors which play in movies with highest ratings
I made it this way and it works :
select name,surname
from actor
where ACTORID in(
select actorid
from actor_movie
where MOVIEID in (
select movieid
from movie
where RATINGID in (
select ratingid
from rating
where PERCENT_CSFD = (
select max(percent_csfd)
from rating
)
)
)
);
the output is :
Gary Oldman
Sigourney Weaver
...but I'd like to also add to this select mentioned movie and its rating. It accessible in inner selects but I don't know how to join it with outer select in which i can work just with rows found in Actor Table.
Thank you for your answers.
You just need to join the tables properly. Afterwards you can simply add the columns you´d like to select. The final select could be looking like this.
select ac.name, ac.surname, -- go on selecting from the different tables
from actor ac
inner join actor_movie amo
on amo.actorid = ac.actorid
inner join movie mo
on amo.movieid = mo.movieid
inner join rating ra
on ra.ratingid = mo.ratingid
where ra.PERCENT_CSFD =
(select max(percent_csfd)
from rating)
A way to get your result with a slightly different method could be something like:
select *
from
(
select name, surname, percent_csfd, row_number() over ( order by percent_csfd desc) as rank
from actor
inner join actor_movie
using (actorId)
inner join movie
using (movieId)
inner join rating
using(ratingId)
(
where rank = 1
This uses row_number to evaluate the "rank" of the movie(s) and then filter for the movie(s) with the highest rating.

How to have IN and NOT IN at same time

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.

Using advance SELECT statement for SQL QUERY

I'm trying to use sql query to show name_id and name attribute for all the people who have only grown tomato (veg_grown) and the result are show ascending order of name attribute.
CREATE TABLE people
(
name_id# CHAR(4) PRIMARY KEY,
name VARCHAR2(20) NOT NULL,
address VARCHAR2(80) NOT NULL,
tel_no CHAR(11) NOT NULL
)
CREATE TABLE area
(
area_id# CHAR(5) PRIMARY KEY,
name_id# REFRENCES people,
area_location_adress VARCHAR2(80) NOT NULL
)
CREATE TABLE area_use
(
area_id# REFERENCES area,
veg_grown VARCHAR (20) NOT NULL
)
but the veg_grown attribute has no relation to the people table but the people and area_use table are linked through area table so I tried using INNER JOIN like this which I confused my-self and didn't even work:
SELECT
name, name_id
FROM
people
INNER JOIN
area USING (name_id)
SELECT area_id
FROM area
INNER JOIN area_use USING (area_id)
WHERE veg_grown = 'tomato'
ORDER BY name ASC;
Surely there must be a way to select name_id and name who has only grown tomato in SQL query
I will take any help or advice :) thanks
SELECT p.name, p.name_id
FROM people p
JOIN area a
ON p.name_id = a.name_id
JOIN area_use au
ON a.area_id = au.area_id
AND au.veg_grown = 'tomato'
LEFT JOIN area_use au2
ON a.area_id = au2.area_id
AND au2.veg_grown <> 'tomato'
WHERE au2.area_id IS NULL;
This will use a LEFT JOIN to find people that only grow tomatoes. To find people that grow tomatoes and possibly anything else too, remove the LEFT JOIN part and everything below it.
An SQLfiddle to test with.
EDIT: If your field names contain # in the actual table, you'll need to quote the identifiers and add the #, I left them out in this sample.
AFAICT you only want entries where all info is available, so there are no left/right joins.
SELECT p.name_id, p.name
FROM people p
JOIN area a
ON p.name_id = a.name_id
JOIN area_use au
ON a.area_id = au.area_id
WHERE au.veg_grown = 'tomato'
ORDER BY p.name ASC
I'm not 100% sure of your data model, but this seems to be what you're trying to do.
SELECT name, people.name_id
FROM people, area, area_use
WHERE area.area_id = area_use.area_id
AND veg_grown = 'tomato'
AND area.name_id = people.name_id
ORDER BY name ASC;

Better way to demand, in SQL, that a column contains every specified value

Imagine you have two tables, with a one to many relationship.
For this example, I will suggest that there are two tables: Person, and Homes.
The person table holds a persons name, and gives them an ID. The homes table, holds the association of homes to a person. PID joins to "Person.ID"
And, in this tiny DB, a person can have no homes, or many homes.
I hope I drew that right.
How do I write a select, that returns everyone with every specified house type?
Let's say these are valid "Types" in the homes table:
Cottage, Main, Mansion, Spaceport.
I want to return everyone, in the Person table, who has a spaceport and a Cottage.
The best I could come up with was this:
SELECT DISTINCT( p.name ) AS name
FROM person p
INNER JOIN homes h ON h.pid = p.id
WHERE 'spaceport' in (
SELECT DISTINCT( type ) AS type
FROM homes
WHERE pid = p.id
)
AND 'cottage' in (
SELECT DISTINCT( type ) AS type
FROM homes
WHERE pid = p.id
)
When I wrote that, it works, but I'm pretty sure there has to be a better way.
The HAVING clause here will guarantee that the persons returned have both types, not just one or the other.
SELECT p.name
FROM person p
INNER JOIN homes h
ON p.id = h.pid
AND h.type IN ('spaceport', 'cottage')
GROUP BY p.name
HAVING COUNT(DISTINCT h.type) = 2
select * from homes;
home_id person_id type
--
1 1 cottage
2 1 mansion
3 2 cottage
4 3 mansion
5 4 cottage
6 4 cottage
To find the id numbers of every person who has both a cottage and a mansion, group by the id number, restrict the output to cottages and mansions, and count the distinct types.
select person_id
from homes
where type in ('cottage','mansion')
group by person_id
having count(distinct type) = 2;
person_id
--
1
You can use this query in a join to get all the columns from the person table.
select person.*
from person
inner join (select person_id
from homes
where type in ('cottage','mansion')
group by person_id
having count(distinct type) = 2) T
on person.person_id = T.person_id;
Thanks to Joe for pointing out an error in my count().
Not sure about the performance on this one, but here goes:
SELECT PID FROM (
SELECT PID, COUNT(PID) cnt FROM (
SELECT DISTINCT PID, Type FROM Homes
WHERE Type IN ('Type1', 'Type2', 'Type3')
) a
GROUP BY PID
) b
WHERE b.cnt = 3
You'd have to dynamically generate your IN clause as well as the WHERE b.CNT clause.

SQL query for finding row with same column values that was created most recently

If I have three columns in my MySQL table people, say id, name, created where name is a string and created is a timestamp.. what's the appropriate query for a scenario where I have 10 rows and each row has a record with a name. The names could have a unique id, but a similar name none the less. So you can have three Bob's, two Mary's, one Jack and 4 Phil's.
There is also a hobbies table with the columns id, hobby, person_id.
Basically I want a query that will do the following:
Return all of the people with zero hobbies, but only check by the latest distinct person created, if that makes sense. Meaning if there is a Bob person that was created yesterday, and one created today.. I only want to know if the Bob created today has zero hobbies. The one from yesterday is no longer relevant.
select pp.id
from people pp, (select name, max(created) from people group by name) p
where pp.name = p.name
and pp.created = p.created
and id not in ( select person_id from hobbies )
SELECT latest_person.* FROM (
SELECT p1.* FROM people p1
WHERE NOT EXISTS (
SELECT * FROM people p2
WHERE p1.name = p2.name AND p1.created < p2.created
)
) AS latest_person
LEFT OUTER JOIN hobbies h ON h.person_id = latest_person.id
WHERE h.id IS NULL;
Try This:
Select *
From people p
Where timeStamp =
(Select Max(timestamp)
From people
Where name = p.Name
And not exists
(Select * From hobbies
Where person_id = p.id))