Can this sql query be simplified? - sql

I have the following tables:
Person, {"Id", "Name", "LastName"}
Sports, {"Id" "Name", "Type"}
SportsPerPerson, {"Id", "PersonId", "SportsId"}
For my query I want to get all the Persons that excersise a specific Sport whereas I only have the Sports "Name" attribute at my disposal. To retrieve the correct rows I've figured out the following queries:
SELECT *
FROM Person
WHERE Person.Id in
(
SELECT SportsPerPerson.PersonId FROM SportsPerPerson
INNER JOIN Sports on SportsPerPerson.SportsId = Sports.Id
WHERE Sports.Name = 'Tennis'
)
AND Person.Id in
(
SELECT SportsPerPerson.PersonId FROM SportsPerPerson
INNER JOIN Sports on SportsPerPerson.SportsId = Sports.Id
WHERE Sports.Name = 'Soccer'
)
OR
SELECT *
FROM Person
WHERE Id IN
(SELECT PersonId FROM SportsPerPerson WHERE SportsId IN
(SELECT Id FROM Sports WHERE Name = 'Tennis'))
AND Id IN
(SELECT PersonId FROM SportsPerPerson WHERE SportsId IN
(SELECT Id FROM Sports WHERE Name = 'Soccer'))
Now my question is, isn't there an easier way to write this query? Using just OR won't work because I need the person who play 'Tennis' AND 'Soccer'. But using AND also doesn't work because the values aren't on the same row.

You can use another JOIN to avoid the second IN. The sub-select only returns those persons that play both Tennis and Soccer:
SELECT *
FROM Person
WHERE Person.Id IN
(
SELECT spp1.PersonId
FROM SportsPerPerson spp1
JOIN SportsPerPerson spp2 ON ( spp2.PersonId = spp1.PersonId )
JOIN Sports s1 on spp1.SportsId = s1.Id
JOIN Sports s2 on spp2.SportsId = s2.Id
WHERE s1.Name = 'Tennis'
AND s2.Name = 'Soccer'
)

You should use two joins in the query:
SELECT *
FROM Person p INNER JOIN SportsPerPerson spp1 ON (p.PersonId = spp1.PersonId)
INNER JOIN Sports s1 ON (s1.SportsIN = spp1.SportId)
INNER JOIN SportsPerPerson spp2 ON (p.PersonId = spp2.PersonId)
INNER JOIN Sports s2 ON (s2.SportId = spp2.SportId)
WHERE s1.Name = 'Tennis' AND s2.Name='Soccer'

The trick is to use aliases so that you can use the same tables multiple times:
SELECT p.*
FROM Person p
INNER JOIN SportsPerPerson spa
ON p.Id = spa.PersonId
INNER JOIN Sports sa
ON spa.SportsId = sa.Id
INNER JOIN SportsPerPerson spb
ON p.Id = spb.PersonId
INNER JOIN Sports sb
ON spb.SportsId = sb.Id
WHERE
sa.Name = 'Tennis'
AND sb.Name = 'Soccer'

This:
SELECT *
FROM Person p
WHERE (
SELECT COUNT(*)
FROM Sports s
JOIN SportsPerPerson sp
ON sp.SportsID = s.id
WHERE s.name IN ('Tennis', 'Soccer')
AND sp.PersonID = p.id
) = 2
or this:
SELECT p.*
FROM (
SELECT sp.PersonID
FROM Sports s
JOIN SportsPerPerson sp
ON sp.SportsID = s.id
WHERE s.name IN ('Tennis', 'Soccer')
GROUP BY
sp.PersonID
HAVING COUNT(*) = 2
) q
JOIN person p
ON p.id = q.personID
You need to declare a UNIQUE KEY or a PRIMARY KEY on SportsPerPerson (sportsid, personid) for this to work correctly and fast.

The query you need is:
SELECT p.ID, p.Name, p.LastName
FROM Person p
JOIN SportsPerPerson sp ON p.ID = sp.PersonID
JOIN Sports s ON sp.SportsID = s.ID
WHERE s.Name = 'Football'
That said, as an aside, the ID key on the SportsPerPerson table is entirely unnecessary to implement the many to many relationship you have. Using the PersonID and SportID columns as a composite primary key would be enough.

Related

sqlite: count and group by clause gives not the result expected

on sqlite, I have the tables
papers: rero_id, doi, year
writtenby: rero_id, authorid, instid
authors: author_id, name, firstname
inst: inst_id, name, see_id
inst is a table of Institutions: Universities and so on.
Each line in writtenby gives a paper, an author, an institution this author was attached at that time. There can be more then one institution and the couple paper, authorid is repeated for each institution.
For a given author, I want a list and a count of the institutions he has cohautored paper with.
For a list I tried
SELECT inst.name as loc
FROM (
(authors INNER JOIN writtenby ON authors.authorid =
writtenby.authorid)
INNER JOIN writtenby AS writtenby_1 ON writtenby.rero_id =
writtenby_1.rero_id
)
INNER JOIN authors AS auth_1 ON writtenby_1.authorid =
auth_1.authorid
inner join inst on writtenby_1.instid = inst.inst_id
WHERE (authors.name) ="Doe" AND (authors.firstname)= "Joe"
ORDER BY loc
I got a list that seems ok.
Now, I would like to regroup these institution names and have a count.
I tried
SELECT inst.name, count(inst.name)
FROM (
(authors INNER JOIN writtenby ON authors.authorid =
writtenby.authorid)
INNER JOIN writtenby AS writtenby_1 ON writtenby.rero_id =
writtenby_1.rero_id
)
INNER JOIN authors AS auth_1 ON writtenby_1.authorid =
auth_1.authorid
inner join inst on writtenby_1.instid = inst.inst_id
GROUP BY inst.name
HAVING (authors.name) ="Doe" AND (authors.firstname)= "John"
I have only three line and not a count of the institutions listed from the first query.
Thanks for correcting me !
François
Try using where instead of having
SELECT inst.name, count(inst.name)
FROM (
(authors INNER JOIN writtenby ON authors.authorid =
writtenby.authorid)
INNER JOIN writtenby AS writtenby_1 ON writtenby.rero_id =
writtenby_1.rero_id
)
INNER JOIN authors AS auth_1 ON writtenby_1.authorid =
auth_1.authorid
inner join inst on writtenby_1.instid = inst.inst_id
where authors.name ='Doe' AND authors.firstname= 'John'
GROUP BY inst.name
I got this that works,
SELECT inst.name as loc, count(*) as c
FROM (
(authors INNER JOIN writtenby ON authors.authorid = writtenby.authorid)
INNER JOIN writtenby AS writtenby_1 ON writtenby.rero_id =
writtenby_1.rero_id
inner join inst on writtenby_1.instid = inst.inst_id
)
INNER JOIN authors AS auth_1 ON writtenby_1.authorid = auth_1.authorid
WHERE (authors.name) ="Doe" AND (authors.firstname)= "John"
GROUP BY inst.name
ORDER BY c DESC
I still can use a where clause, and that's not the same as having...
And thanks to fa6 who gave the answer below
F.

Write the SQL code that will list Physician-Person appointments only ONCE

This one is confusing me?
There are three tables. Appointments(Appointment_ID, Physician_ID and Person_ID) , Physician(Physician_ID) and a Person(Person_ID and Physician_ID).
This is what I have so far :
SELECT DISTINCT Appointment_date_time FROM Appointment
INNER JOIN Person
ON Appointment.Person_ID = Person.Person_ID
INNER JOIN Physician
ON Physician.Physician_ID = Person.Physician_ID
HAVING COUNT(*) < 1
There are three tables. Appointments(Appointment_ID, Physician_ID and Person_ID) , Physician(Physician_ID) and a Person(Person_ID and Physician_ID).
select *
from Appointments a
inner join Person p
on a.Person_ID = p.Person_ID
inner join Physician ph
on a.Physician_ID = ph.Physician_ID

SQL Query / Herigate data join

I have two tables, the first with person data
ID
Name
The second has each person relatives
Primary Key Field
Person (references person.id)
RelativeType (mother or father)
Parent (references person.id)
I'm trying to get all the sons and grandsons of a specific person and I'm stuck integrating the grandsons in the query results. I'm using SQL Server.
Any ideas?
The query you need depends highly of how many level you have of the Parent-Child relationship. If you can change the schema, I would recommend you turn into using HierarchyId which is an SQL Server specific data type. Have a look here.
In the following, I assume you only have 2 levels. Father - Son - GrandSon
;WITH Sons AS (
SELECT pdf.Id, pdf.Name, pdd.Id ParentId, pdd.Name Parent FROM PersonData pdf
JOIN PersonRelative pr ON pdf.Id = pr.Parent
JOIN PersonData pdd ON pr.Person = pdd.Id //Selecting all Parents
)
SELECT pd.Name, s.Name Son, 'Son' Type FROM PersonData pd
JOIN Sons s on pd.Id = s.ParentId
UNION
SELECT pd.Name, gs.Name Son, 'GrandSon' Type FROM PersonData pd
JOIN Sons s on pd.Id = s.ParentId
JOIN Sons gs on s.Id = gs.ParentId
Try something like this (untested):
declare #person bigint = 123
;with cte as
(
select p.id, p.name, r.relativetype, r.parent, 0 decendantLevel
from person p
inner join relatives r
on p.id = r.person
where p.id = #person --select the ancestor who's tree we're interested in (if we don't want to include the person themselves change p.id to r.parent)
union all
select p.id, p.name, r.relativetype, r.parent, c.decendantLevel + 1
from person p
inner join relatives r
on p.id = r.person
inner join cte c
on c.id = r.parentid
)
select * from cte order by decendantLevel
Like mentioned recursive CTE is way to achieve this:
DECLARE #PersonToFind INT
SET #PersonToFind = 1
;WITH RCTE AS
(
SELECT Person, CAST('Child' AS NVARCHAR(MAX)) AS Relation
FROM PersonRelations
WHERE Father = #PersonToFind OR Mother = #PersonToFind
UNION ALL
SELECT pr.Person, 'Grand' + r.Relation
FROM PersonRelations pr
INNER JOIN RCTE r ON r.Person = pr.Mother OR r.Person = pr.Father
)
SELECT r.*, p.Name
FROM RCTE r
LEFT JOIN Person p ON r.Person = p.ID
SQLFiddle DEMO
(this is using mother and father columns before question was edited, so just change to Parent where there are Mother OR Father checks)
Thanks for all the answers, CTE was the way to go. I taked parts from every answer and ended with this
with Sons as (select P1.Person as PersonSon from Persons
join relatives as P1 on P1.Parent = Persons.Id
where Persons.Name = 'Mary')
select Id from Persons
join (
select P2.Person as PersonSon from Relatives as P2
join Sons on P2.Parent = Sons.PersonSon ) as P3 on Persons.Id = P3.PersonSon
union all select Sons.PersonSon from Sons

How to select SQL results based on multiple tables

I need to select results from one table based on certain matching values in a couple of other tables. I have the following tables:
person: id, firstname, lastname
team: id, teamname
player: id, person_id(FK), team_id(FK)
coach: id, person_id(FK), team_id(FK)
I need to return all the coaches and players names for each team. I've only ever used inner joins, and it doesn't seem like I can use those here, so any idea how to do this?
This will give you the coach:
SELECT team.Teamname, person.Firstname, person.Lastname
FROM person
JOIN coach ON person.id = coach.person_id
JOIN team ON coach.team_id = team.id
And this will give you the players:
SELECT team.Teamname, person.Firstname, person.Lastname
FROM person
JOIN player ON person.id = player.person_id
JOIN team ON player.team_id = team.id
So, the non-elegant, simple answer is to just toss it all together with UNION.
Just use an OR in the join to Team
SELECT
P.firstname,
P.lastname,
T.teamname
FROM
person p id
LEFT JOIN player pl
ON p.id = pl.person_id
LEFT JOIN coach c
ON p.id = c.person_id
LEFT JOIN team t
ON pl.team_id = t.id
or.c.team_id = t.id
Or if you perfer if and your database has COALESCE
LEFT JOIN team t
ON COALESCE(pl.team_id,c.team_id) = t.id

SQL Query to Count() multiple tables

I have a table which has several one to many relationships with other tables. Let's say the main table is a person, and the other tables represent pets, cars and children. I would like a query that returns details of the person,the number of pets, cars and children they have e.g.
Person.Name Count(cars) Count(children) Count(pets)
John Smith 3 2 4
Bob Brown 1 3 0
What is the best way to do this?
Subquery Factoring (9i+):
WITH count_cars AS (
SELECT t.person_id
COUNT(*) num_cars
FROM CARS c
GROUP BY t.person_id),
count_children AS (
SELECT t.person_id
COUNT(*) num_children
FROM CHILDREN c
GROUP BY t.person_id),
count_pets AS (
SELECT p.person_id
COUNT(*) num_pets
FROM PETS p
GROUP BY p.person_id)
SELECT t.name,
NVL(cars.num_cars, 0) 'Count(cars)',
NVL(children.num_children, 0) 'Count(children)',
NVL(pets.num_pets, 0) 'Count(pets)'
FROM PERSONS t
LEFT JOIN count_cars cars ON cars.person_id = t.person_id
LEFT JOIN count_children children ON children.person_id = t.person_id
LEFT JOIN count_pets pets ON pets.person_id = t.person_id
Using inline views:
SELECT t.name,
NVL(cars.num_cars, 0) 'Count(cars)',
NVL(children.num_children, 0) 'Count(children)',
NVL(pets.num_pets, 0) 'Count(pets)'
FROM PERSONS t
LEFT JOIN (SELECT t.person_id
COUNT(*) num_cars
FROM CARS c
GROUP BY t.person_id) cars ON cars.person_id = t.person_id
LEFT JOIN (SELECT t.person_id
COUNT(*) num_children
FROM CHILDREN c
GROUP BY t.person_id) children ON children.person_id = t.person_id
LEFT JOIN (SELECT p.person_id
COUNT(*) num_pets
FROM PETS p
GROUP BY p.person_id) pets ON pets.person_id = t.person_id
you could use the COUNT(distinct x.id) synthax:
SELECT person.name,
COUNT(DISTINCT car.id) cars,
COUNT(DISTINCT child.id) children,
COUNT(DISTINCT pet.id) pets
FROM person
LEFT JOIN car ON (person.id = car.person_id)
LEFT JOIN child ON (person.id = child.person_id)
LEFT JOIN pet ON (person.id = pet.person_id)
GROUP BY person.name
I would probably do it like this:
SELECT Name, PersonCars.num, PersonChildren.num, PersonPets.num
FROM Person p
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Cars ON Cars.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonCars ON PersonCars.PersonID = p.PersonID
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Children ON Children.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonChildren ON PersonChildren.PersonID = p.PersonID
LEFT JOIN (
SELECT PersonID, COUNT(*) as num
FROM Person INNER JOIN Pets ON Pets.PersonID = Person.PersonID
GROUP BY Person.PersonID
) PersonPets ON PersonPets.PersonID = p.PersonID
Note, that it depends on your flavour of RDBMS, whether it supports nested selects like the following:
SELECT p.name AS name
, (SELECT COUNT(*) FROM pets e WHERE e.owner_id = p.id) AS pet_count
, (SELECT COUNT(*) FROM cars c WHERE c.owner_id = p.id) AS world_pollution_increment_device_count
, (SELECT COUNT(*) FROM child h WHERE h.parent_id = p.id) AS world_population_increment
FROM person p
ORDER BY p.name
IIRC, this works at least with PostgreSQL and MSSQL. Not tested, so your mileage may vary.
Using subselects not very good practice, but may be here it will be good
select p.name, (select count(0) from cars c where c.idperson = p.idperson),
(select count(0) from children ch where ch.idperson = p.idperson),
(select count(0) from pets pt where pt.idperson = p.idperson)
from person p
You could do this with three outer joins:
SELECT
Person.Name,
sum(case when cars.id is not null then 1 else 0 end) car_count,
sum(case when children.id is not null then 1 else 0 end) child_count,
sum(case when pets.id is not null then 1 else 0 end) pet_count
FROM
Person
LEFT OUTER JOIN
cars on
Person.id = cars.person_id
LEFT OUTER JOIN
children on
Person.id = children.person_id
LEFT OUTER JOIN
pets on
Person.id = pets.person_id
GROUP BY
Person.Name
I belive that Oracle now supports the case when syntax, but if not you could use a decode.
You'd need to include multiple count statements in the query. Off the top of my head,
SELECT p.Name, COUNT(DISTINCT t.Cars), COUNT(DISTINCT o.Children), Count(DISTINCT p.Pets)
FROM Person p
INNER JOIN Transport t ON p.ID = t.PersonID
LEFT JOIN Offspring o ON p.ID = o.PersonID
LEFT JOIN Pets p ON p.ID = o.OwnerID
GROUP BY p.Name
ORDER BY p.Name