SQL Query / Herigate data join - sql

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

Related

How to select only 1 record from a group with a unique condition

I have the following query. This query allows me to produce a list of children and their familymember contacts (contactpupilID).
select s.studentnr,pc.pupilid, pc.contactpupilid, p2.mainmail
from student s
join pupil p on p.id = s.pupilid
join pupilcontact pc on pc.pupilid = p.id
join pupil p2 on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
order by pc.pupilid
Each child can have 0 to 3 familycontacts (0 because no contact has been added during registration).
Each familycontact (contactpupilid) has an email field. However there are cases where all familycontacts have an email or 1 of them or none of them.
My list needs to select a child with a familycontact(contactpupilid) that has an email. The familycontact that is selected should be the one that has an email.
If none of the familycontacts have an email then it should select the 1st familycontact by default.
This is how it needs to look like
How would I complete this task?
I don't know what you mean by "first record" because SQL tables are unordered. I can assume you mean the one with the smallest contactpupilid.
What you have described is what distinct on does:
select distinct on (s.studentnr) s.studentnr, pc.pupilid, pc.contactpupilid, p2.mainmail
from student s join
pupil p
on p.id = s.pupilid join
pupilcontact pc
on pc.pupilid = p.id join
pupil p2
on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
order by s.studentnr, (p2.mainmail is not null) desc;
Use ROW_NUMBER() window function in your query to rank the rows that contain an email first:
with cte as (
select s.studentnr, pc.pupilid, pc.contactpupilid, p2.mainmail,
row_number() over (partition by s.studentnr order by p2.mainmail is not null desc, pc.contactpupilid) rn
from student s
join pupil p on p.id = s.pupilid
join pupilcontact pc on pc.pupilid = p.id
join pupil p2 on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
)
select studentnr, pupilid, contactpupilid, mainmail
from cte
where rn = 1
order by pupilid;
You can do it with CTE like this
with temp as (
select s.studentnr,pc.pupilid, pc.contactpupilid, p2.mainmail,row_number() over (partition by pupilid order by pupilid) as row_number
from student s
join pupil p on p.id = s.pupilid
join pupilcontact pc on pc.pupilid = p.id
join pupil p2 on p2.id = pc.contactpupilid
where CURRENT_DATE between pc.validfrom and pc.validuntil
order by pc.pupilid
)
select *
from temp
where row_number = 1

SQL: display children and brothers/sisters

I have a database that basically stores a family tree made of several families.
I have two tables:
"persons": id, name, lname
"relationships": parent, child
Displaying all parents is ok, I did this:
SELECT DISTINCT p.fname, p.lname
FROM persons p
INNER JOIN relationships ON p.id = relationships.p
INNER JOIN persons c ON c.id = relationships.c;
But how could I display comprehensively each child and its respective brothers/sisters?
I tried a select within a select among many things but it's not working:
SELECT DISTINCT
c1.name,
c1.lname,
(
SELECT
c2.firstname,
c2.lastname
FROM
persons p2
INNER JOIN relations ON p2.id = relations.parent
INNER JOIN persons c2 ON c2.id = relations.child
WHERE
p.id = p2.id
)
FROM
persons p
INNER JOIN relations ON p.id = relations.parent
INNER JOIN persons c1 ON c1.id = relations.child;
I think I'm on the wrong path.
Thanks for your help.
If I understand correctly, this is just a self join:
select r.child, r2.child as sibling
from relationships r join
relationships r2
on r.parent = r2.parent and
r.child <> r2.child
order by r.child, r2.child;
For the names from the other table you need more joins:
select p.*, p2.*
from relationships r join
relationships r2
on r.parent = r2.parent and
r.child <> r2.child join
persons p
on p.id = r.child join
persons p2
on p2.id = r2.child
order by r.child, r2.child;

How to display Mother and Father of a person separately from same table in psql

Person table
PersonParent table(relation)
How to display all the records of person table, with their parents, whether or not having parents?
Output
that would be :
here is how to join to get the parents information alongside with person info , however this way , the person row will repeat for each parent :
select p.*, concat_ws(',',parent.firstname , parent_lastname) as ParentName
from persons p
cross join (select *
left join PersonParent pp
on p.personId = pp.personId
left join persons parent
on pp.parentId = parent.personid
if you want to show only one row per person, you can use query below, I used gender column to distinguish between mother and father:
select * from persons p
left join lateral(
select max(concat_ws(',', parents.firstname, parents.lastname)) filter (where parents.gender = 'm') as fatherrname
, max(concat_ws(',', parents.firstname, parents.lastname)) filter (where parents.gender = 'f') as mothername
from personparents pp
join persons parents on pp.parentid = parents.id
where pp.personid = p.id
group by pp.personid
) pp on true

SQL: How to save order in sql query?

I have PostgreSQL database and I try to print all my users (Person).
When I execute this query
-- show owners
-- sorted by maximum cars amount
SELECT p.id
FROM car c JOIN person p ON c.person_id = p.id
GROUP BY p.id
ORDER BY COUNT(p.name) ASC;
I get all owners sorted by cars amount
Output: 3 2 4 1
And all order goes wrong when I try to link owner id.
SELECT *
FROM person p
WHERE p.id IN (
SELECT p.id
FROM car c JOIN person p ON c.person_id = p.id
GROUP BY p.id
ORDER BY COUNT(p.name) ASC);
Output: 1 2 3 4 and other data
You see than order is wrong. So here is my question how can I save that order?
Instead Of subquery use join. Try this.
SELECT p.*
FROM person p
JOIN (SELECT p.id,
Count(p.NAME)cnt
FROM car c
JOIN person p
ON c.person_id = p.id
GROUP BY p.id) b
ON p.id = b.id
ORDER BY cnt ASC
Untangle the mess. Aggregate first, join later:
SELECT p.*
FROM person p
JOIN (
SELECT person_id, count(*) AS ct
FROM car
GROUP BY person_id
) c ON c.person_id = p.id
ORDER BY c.cnt;
No need to join to person twice. This should be fastest if you count most or all rows.
For a small selection, correlated subqueries are faster:
SELECT p.*
FROM person p
ORDER BY (SELECT count(*) FROM car c WHERE c.person_id = p.id)
WHERE p.id BETWEEN 10 AND 20; -- some very selective predicate
As for your original: IN takes a set on the right hand, order of elements is ignored, so ORDER BY is pointless in the subuery.

Can this sql query be simplified?

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.