SQL one-to-many relationship, how to select parent containing unique set of children - sql

I have a routine one-to-many schema, similar to this simple example:
PERSON
person_id (PK)
PERSON_TRAIT
person_id (FK)
trait_id (FK)
quantity
TRAIT
trait_id (PK)
name
//other attributes
Given a set of traits ("friendly, funny"), how would I return the associated person_id and quantity recordset.
At first glance I was tempted to use this but it's not this simple:
select person_id, quantity
from trait t
inner join person_trait pt on t.trait_id = pt.trait_id
where name in ('friendly', 'funny')
This isn't correct because I could have a person that contains those traits plus more ("friendly, funny, skinny") and it would be returned.
To take it a step further, if there isn't a person that contains all of the traits exactly, how would I aggregate the traits from multiple different persons and return a recordset of those person_id and quantity values?
Using SQL Server 2005.

This will give you a list of people who have only the two traits indicated.
SELECT person_id, quantity
FROM PERSON_TRAIT
WHERE person_id IN
(
SELECT person_id
FROM PERSON_TRAIT pt
LEFT OUTER JOIN TRAIT t ON pt.trait_id = t.trait_id
AND t.name IN ('friendly','funny')
GROUP BY person_id
HAVING COUNT(*) = COUNT(t.trait_id)
)

Try:
select person_id, sum(quantity)
from trait t
inner join person_trait pt on t.trait_id = pt.trait_id
where name in ('friendly', 'funny')
having count(distinct name) = 2
Change the having number to be the number of distinct names to be selected - so if 'friendly', 'funny' and 'clever' are required, change the having clause to be having count(distinct name) = 3.

Related

Return everything from many-to-many relationship with only one query

I'll give an example to better clarify what I want:
Suppose I have the following classes in my programming language:
Class Person(
int id,
string name,
List<Car> cars
);
Class Car(
int id,
string name,
string brand
)
I want to save that in a PostgreSQL database, so I'll have the following tables:
CREATE TABLE person(
id SERIAL,
name TEXT
);
CREATE TABLE car(
id SERIAL,
name TEXT,
brand TEXT
)
CREATE TABLE person_car(
person_id int,
car_id int,
CONSTRAINT fk_person
FOREIGN KEY (person_id)
REFERENCES person(id),
CONSTRAINT fk_car
FOREIGN KEY (car_id)
REFERENCES car(id)
)
Then, I want to select all people with their cars from DB. I can select all people, then for each person, select their cars. But supposing I have 1000 people, I will have to query the DB 1001 times (one to select all people, and one for each person, to get their cars).
Is there an efficient way to bring all people, each with all their cars in a single query, so that I can fill my classes with the correct data without querying the DB a lot of times?
If you want to return a hierarchical dataset, you can use subqueries with COALESCE, for example :
SELECT
p.id
p.name,
COALESCE((SELECT
json_agg(json_build_object(
'id', c.id,
'name', c.name,
'brand', c.brand
))
FROM car AS c
JOIN person_car pc ON c.id = pc.car_id
WHERE pc.person_id = p.id), '[]'::json) AS cars
FROM person AS p;
You are joining person and car to person_car based on their respective ID’s.
SELECT
person.name,
person.id as person_id,
car.name,
car.brand,
car.id as car_id
FROM
person
JOIN
person_car
ON
person.id = person_car.person_id
JOIN
car
ON
car.id = person_car.car_id

Mapping table join query

I have three tables as follows,
student
category
student_category
Student table has the following columns:
studentid, student name, studenttype
Category table has the following tables:
categoryid, categoryname, ..
student_category table has the following columns:
id, studentid, categoryid
Now I have 2 input parameters categoryid and studenttype so now I need to get the all the student details which are associated with respective categoryid and whose student type is studenttype
I tried as follows which is not giving correct result,
SELECT
s.*
FROM
student s
JOIN
student_category sc ON sc.categoryid = 1;
Also I also need to filter student whose studenttype is 'someinput'
I am working on PostgreSQL. Any suggestions please
You should add a where clause and also use the appropiate join condition.
SELECT s.*
FROM student s
JOIN student_category sc ON sc.studentid = s.studentid
where s.studenttype = 'studenttype' --or your desired value
and sc.categoryid = 1

Find the minimum value of a SQL table attribute and create a new table of attributes?

I have three SQL tables. The first is SCUBA_CLASSES with attributes CLASSID and CLASSNAME. Second is a table called STUDENTS with attributes STUDENTID and AGE. Third is a table TAKES_CLASS with attributes STUDENTID and COURSEID.
How do I create a new table that displays the CLASSID and CLASSNAME of the youngest student in the group?
I have:
CREATE TABLE YOUNGEST_STUDENT_ENROLLMENT
SELECT SCUBA_CLASSES.CLASSID AS CLASSID, SCUBA_CLASSES.CLASSNAME AS CLASSNAME
FROM SCUBA_CLASSES, STUDENTS, TAKES_CLASS
WHERE MIN(STUDENTS.AGE)
Not sure what to do from here. I have to find the youngest student and output the CLASSID and CLASSNAME of all the classes they are enrolled in.
It would seem a VIEW would be a better fit than a table. The view will update automatically when new students are enrolled;
CREATE VIEW YOUNGEST_STUDENT_ENROLLMENT AS
WITH cte AS (
SELECT SC.CLASSID, SC.CLASSNAME, S.NAME, S.AGE,
ROW_NUMBER() OVER (PARTITION BY SC.CLASSID ORDER BY AGE) RN
FROM SCUBA_CLASSES SC
JOIN TAKES_CLASS TC ON SC.CLASSID = TC.CLASSID
JOIN STUDENTS S ON S.STUDENTID = TC.STUDENTID
)
SELECT CLASSID,CLASSNAME,NAME,AGE FROM cte WHERE RN=1;
This view will always contain the person with the lowest age per class. If you want it to contain only the lowest aged person no matter which class, you can remove the PARTITION BY SC.CLASSID clause.
An SQLfidde to test with.
Try this nested query:-
CREATE TABLE YOUNGEST_STUDENT_ENROLLMENT AS (
SELECT SCUBA_CLASSES.CLASSID AS CLASSID, SCUBA_CLASSES.CLASSNAME AS CLASSNAME
FROM SCUBA_CLASSES
WHERE SCUBA_CLASSES.CLASSID IN (SELECT TAKES_CLASS.CLASSID FROM TAKES_CLASS
WHERE TAKES_CLASS.STUDENTID IN (SELECT STUDENTS.STUDENTID FROM STUDENTS
WHERE STUDENTS.AGE IN (SELECT MIN(STUDENTS.AGE) as AGE FROM STUDENTS))));

SQL to select a table through a generic relationship

In a relational database I have three tables. Using SQL Server.
person(id, type)
student(id, person_id, type, student specific fields)
teacher(id, person_id, type, teacher specific fields)
Student and teacher are both people, therefore a student will have a record in both the person and student tables, as will the teacher. Student and teacher have foreign keys to person. Student and teacher have different field definitions therefore a union will NOT work.
Now I have the person's id and depending on whether the person is a student or teacher I would like to select * from the relevant table (not person).
For example, if the person is a student I would like my query to select the student table.
I can think of a few inefficient methods but I am looking for the optimum one.
I would suggest a UNION
SELECT student.*
FROM student
WHERE person_id= #id
UNION
SELECT teacher.*
FROM teacher
WHERE person_id= #id
if exists(select person_id from student where person_id = #id)
select * from student where person_id = #id
else
if exists(select person_id from teacher where person_id = #id)
select * from teacher where person_id = #id
If your RDBMS is SQLServer, then I would abstract a view along the lines of podiluska's union, mapping out specific fields in Student and Teacher to common names, and padding with NULLs where no mapping is possible
And assuming that Students and Teachers inherit from person (i.e. both are 0..1 to 1 with Person), then they can share the same primary key, i.e. no need for new surrogates keys on Teacher and Student.
I've assumed that person.type determines whether the person is a Student(S) or Teacher(T).
CREATE VIEW SubClassesOfPerson AS
SELECT p.id as PersonId,
p.name as PersonName,
p.OtherBaseFieldsHere,
s.SomeStudentSpecificField AS MappedField1,
s.SomeStudentSpecificFieldX AS MappedFieldX,
s.SomeStudentSpecificField as MappedFieldForStudentOnly,
NULL as MappedFieldForTeacherOnly -- Pad this because it can't be mapped
FROM person p
INNER JOIN student s
on s.person_id = p.id AND p.type = 'S'
UNION
SELECT p.id as PersonId,
p.name as PersonName,
p.OtherBaseFieldsHere,
t.SomeTeacherSpecificField AS MappedField1,
t.SomeTeacherSpecificFieldX AS MappedFieldX,
NULL as MappedFieldForStudentOnly, -- Pad this because it can't be mapped
t.SomeTeacherSpecificField as MappedFieldForTeacherOnly
FROM person p
INNER JOIN teacher t
on t.person_id = p.id AND p.type = 'T'

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.