SQL: IN condition from array - sql

The best way I can describe my problem is by giving you an example of my problem:
I have a table called "person" with the columns id | name | hobbys
hobbys would be a manyToMany association
So I have this statement: SELECT * FROM person p LEFT JOIN hobbys h ON p.hobby_id = h.id WHERE p.hobby_id IN($array);
The problem here is, it will select all persons that have one of the hobbys in that array, but I want the selected persons MUST have all of the hobbys in that array.
Is there a function in sql?

Use GROUP BY and HAVING.
SELECT p.id, p.name
FROM person p
JOIN hobbys h ON p.hobby_id = h.id
WHERE p.hobby_id IN($array)
GROUP BY p.id, p.name
HAVING count(distinct h.id) = <size_of_array>
There are also other solutions using INTERSECTION, IN, or EXISTS however this one will keep the list of values behind IN.

You have to search each item hobby in person and match them with a AND conjunction with all contents in hobby table
first, You have to get all records persons have in hobby
SELECT * FROM person p LEFT JOIN hobbys h ON p.hobby_id = h.id
Now build the AND conditional statement, and later query it with
SELECT * FROM person p LEFT JOIN hobbys h ON p.hobby_id = h.id
WHERE first_result IN($array) and second_result in ($array) and....;

Related

Postgres do array agg for each row

I have a query which will take jobs_locum_hospital_ids from my doctor table, it will then join this to the hospital table on id and fetch the name, then placing all of these into an array.
so [187,123] --> ("George Eliot Hospital - Acute Services"),("Good Hope Hospital")
select array_agg(t)
from (
select h.name from (select jsonb_array_elements_text(d.jobs_locum_hospital_ids)::int as id from doctor d
where d.id = 11720) as q1
left join hospital h on h.id = q1.id
)t
But this is only performing this for where d.id = 11720
What I'd like to do is do this for each row. So in a way joining to
select * from doctor
left join that thing above
It is a bit hard to figure out your data structure or why you are using json functions for this. From what I can tell, doctors have an array of hospital ids and you want the names:
select d.*,
(select array_agg(h.name)
from unnest(d.jobs_locum_hospital_ids) dh join
hospital h
on dh = h.id
) as hospital_names
from doctors;
Just the fact that you want to do this suggests that you really want a junction table, doctorHospitals with one row per doctor and per hospital.

Left outer join with count, on 3 tables not returning all rows from left table

I have these 3 tables:
Areas - id, name
Persons - id, area_id
Special_Persons - id_person, date
I'd like to produce a list of all Areas, followed by a count of Special Persons in each area, including Areas with no Special Persons.
If I do a left join of Areas and Persons, like this:
select a.id as idArea, count(p.id) as count
from areas a
left join persons p on p.area_id = a.id
group by a.id;
This works just fine; Areas that have no Persons show up, and have a count of 0.
What I am not clear on is how to do the same thing with the special_persons table, which currently only has 2 entries, both in the same Area.
I have tried the following:
select a.id as idArea, count(sp.id_person) as count
from special_persons sp, areas a
left join persons p on p.area_id = a.id
where p.area_id = a.id
and sp.id_person = p.id
group by a.id;
And it only returns 1 row, with the Area that happens to have 2 Special Persons in it, and a count of 2.
To continue getting a list of all areas, do I need to use a sub-query? Another join? I'm not sure how to go about it.
You can add another left join to the Special_Persons table:
select a.id as idArea, count(p.id), count(sp.id_person)
from areas a
left join persons p on p.area_id = a.id
left join special_persons sp on sp.id_person = p.id
group by a.id;

Get some data which corresponds to the maximum date

I have these 3 tables:
Table ORG:
Fields:historyid, personid
Table PERSON:
Fields: id
Table HISTORY:
Fields: id,date,personid
Both HISTORY and ORG are linked to PERSON with an 1:N relationship. Also, ORG is linked to HISTORY with an 1:N relationship. I want to get from table ORG for each person just one row: this which corresponds to the HISTORY row with the highest date. The following SQL gives the highest date for a certain person. However, I do not know how to combine this with the above requirement.
SELECT ash1.id
FROM
(SELECT * FROM history a WHERE a.personid=person.id) ash1
LEFT JOIN
(SELECT * FROM history b WHERE b.personid=person.id) ash2
ON ash1.personid=ash2.personid
AND ash1.date < ash2.date
WHERE ash2.date IS NULL
I think you can do it by using MAX() and GROUP BY:
SELECT
o.historyid AS o_hist,
o.personid AS o_per,
h.id AS h_id,
MAX(h.date) AS h_date,
h.personid AS h_person
FROM
org o
LEFT JOIN
person p ON p.id = o.personid
LEFT JOIN
history h ON h.id = o.historyid AND h.personid = p.id
GROUP BY o_per
Try the below query..
;WITH_CTE_HighestHistory
AS (SELECT PersonID,MAX([Date]) HDate
FROM History
GROUP BY PersonID)
SELECT org.*,h.*
FROM org o
LEFT JOIN History h ON o.Historyid=h.Id and o.PersonID=h.PersonId
INNER JOIN WITH_CTE_HighestHistory ch ON h.Personid=ch.Personid and h.[Date]=ch.[Date]
WHERE EXISTS(SELECT 1 FROM Person p WHERE p.Id=o.PersonID )
There are multiple ways to approach this, depending on the database. However, your data structure is awkward. Why does org have historyid? That doesn't really make sense to me.
In any case, based on your description, this should work:
select o.*, h.*
from org o join
history h
on h.personid = o.personid
where h.date = (select max(h2.date)
from history h2
where h2.personid = h.personid
);
You might want to start the from clause as:
from (select distinct personid from org) o
So, you only get one person, if they are repeated in the table.

LEFT JOIN across three tables (with junction table)

In Postgres, is there a way to perform a left join between tables linked by a junction table, with some filtering on the linked table?
Say, I have two tables, humans and pets, and I want to perform a query where I have the human ID, and the pet name. If the human ID exists, but they don't have a pet with that name, I still want the human's row to be returned.
If I had a FK relationship from pets to humans, this would work:
select h.*, p.*
from humans as h
left join pets as p on p.human_id = h.id and p.name = 'fluffy'
where h.id = 13
and I'd get a row with human 13's details, and fluffy's values. In addition, if human 13 didn't have a pet named 'fluffy', I'd get a row with human 13's values, and empty values for the pet's columns.
BUT, I don't have a direct FK relationship, I have a junction table between humans and pets, so I'm trying a query like:
select h.*, p.*
from humans as h
left join humans_pets_junction as j on j.human_id = h.id
left join pets as p on j.pet_id = p.id and p.name = 'fluffy'
where h.id = 13
Which returns rows for all of human 13's pets, with empty columns except for fluffy's row.
If I add p.name = 'fluffy' to the WHERE clause, that filters out all the empty rows, but also means I get 0 rows if human 13 doesn't have a pet named fluffy at all.
Is there a way to replicate the behavior of the FK-style left join, but when used with a junction table?
One method is to do the comparison in the where clause:
select h.*, p.*
from humans as h left join
humans_pets_junction as j
on j.human_id = h.id left join
pets as p
on j.pet_id = p.id and p.name = 'fluffy'
where h.id = 13 and (p.name = 'fluffy' or p.id is null);
Alternatively, join the junction table and the pets table as a subquery or CTE:
select h.*, p.*
from humans h left join
(select j.*
from humans_pets_junction j join
pets p
on j.pet_id = p.id and p.name = 'fluffy'
) pj
on pj.human_id = h.id
where h.id = 13;
In Postgres you can use parentheses to prioritize JOIN order. You do not need a subquery:
SELECT h.*, p.id AS p_id, p.name AS pet_name
FROM humans h
LEFT JOIN (pets p
JOIN humans_pets_junction j ON p.name = 'fluffy'
AND j.pet_id = p.id
AND j.human_id = 13) ON TRUE
WHERE h.id = 13;
Per documentation:
Parentheses can be used around JOIN clauses to control the join order.
In the absence of parentheses, JOIN clauses nest left-to-right.
I added the predicate j.human_id = 13 to the join between your junction table and the pets to eliminate irrelevant rows at the earliest opportunity. The outer LEFT JOIN only needs the dummy condition ON TRUE.
SQL Fiddle.
Aside 1: I assume you are aware that you have a textbook implementation of a n:m (many-to-many) relationship?
How to implement a many-to-many relationship in PostgreSQL?
Aside 2: The unfortunate naming convention in the example makes it necessary to deal out column aliases. Don't use "id" and "name" as column names in your actual tables to avoid such conflicts. Use proper names like "pet_id", "human_id" etc.

Getting individual counts of a tables column after joining other tables

I'm having problems getting an accurate count of a column after joining others. When a column is joined I would still like to have a DISTINCT count of the table that it is being joined on.
A restaurant has multiple meals, meals have multiple food groups, food groups have multiple ingredients.
Through the restaurants id I want to be able to calculate how many of meals, food groups, and ingrediants the restaurant has.
When I join the food_groups the count for meals increases as well (I understand this is natural behavior I just don't understand how to get what I need due to it.) I have tried DISTINCT and other things I have found, but nothing seems to do the trick. I would like to keep this to one query rather than splitting it up into multiple ones.
SELECT
COUNT(meals.id) AS countMeals,
COUNT(food_groups.id) AS countGroups,
COUNT(ingrediants.id) AS countIngrediants
FROM
restaurants
INNER JOIN
meals ON restaurants.id = meals.restaurant_id
INNER JOIN
food_groups ON meals.id = food_groups.meal_id
INNER JOIN
ingrediants ON food_groups.id = ingrediants.food_group_id
WHERE
restaurants.id='43'
GROUP BY
restaurants.id
Thanks!
The DISTINCT goes inside the count
SELECT
COUNT(DISTINCT meals.id) AS countMeals,
COUNT(DISTINCT food_groups.id) AS countGroups,
COUNT(DISTINCT ingrediants.id) AS countIngrediants
FROM
restaurants
INNER JOIN
meals ON restaurants.id = meals.restaurant_id
INNER JOIN
food_groups ON meals.id = food_groups.meal_id
INNER JOIN
ingrediants ON food_groups.id = ingrediants.food_group_id
WHERE
restaurants.id='43'
GROUP BY
restaurants.id
You're going to have to do subqueries, I think. Something like:
SELECT
(SELECT COUNT(1) FROM meals m WHERE m.restaurant_id = r.id) AS countMeals,
(SELECT COUNT(1) FROM food_groups fg WHERE fg.meal_id = m.id) AS countGroups,
(SELECT COUNT(1) FROM ingrediants i WHERE i.food_group_id = fg.id) AS countGroups
FROM restaurants r
Where were you putting your DISTINCT and on which columns? When using COUNT() you need to do the distinct inside the parentheses and you need to do it over a single column that is distinct for what you're trying to count. For example:
SELECT
COUNT(DISTINCT M.id) AS count_meals,
COUNT(DISTINCT FG.id) AS count_food_groups,
COUNT(DISTINCT I.id) AS count_ingredients
FROM
Restaurants R
INNER JOIN Meals M ON M.restaurant_id = R.id
INNER JOIN Food_Groups FG ON FG.meal_id = M.id
INNER JOIN Ingredients I ON I.food_group_id = FG.id
WHERE
R.id='43'
Since you're selecting for a single restaurant, you shouldn't need the GROUP BY. Also, unless this is in a non-English language, I think you misspelled ingredients.