Reputation: 6460
I have a many-to-many mapped via an ORM. I select from the left table and use 2 left outer joins to pull in the right table.
Everything works fine, but I have a case where if the right table contains a specific value, then the query should return nothing.
Here is a contrived, simplified example:
people people_fruits fruits
id | name people_id | fruit_id id | name
---------------------------------------------------
1 | Tom 1 | 1 1 | apple
2 | Paul 1 | 2 2 | banana
3 | Kim 2 | 1 3 | orange
2 | 3
A given query might look like:
SELECT p.*, f.*
FROM people AS p
LEFT OUTER JOIN people_fruits AS pf ON pf.people_id = p.id
LEFT OUTER JOIN fruits AS f ON pf.fruits_id=f.id
WHERE p.id = 1;
Pretty standard. But let's say I want to pretend people associated with oranges simply don't exist (sorry orange fans). I want the following query to return an empty set:
SELECT p.*, f.*
FROM people AS p
LEFT OUTER JOIN people_fruits AS pf ON pf.people_id = p.id
LEFT OUTER JOIN fruits AS f ON pf.fruits_id=f.id
WHERE p.id = 2;
This query should return an empty set.
Queries for Tom
and Kim
should return results, but queries for Paul
should not.
How can such a query be written?
Upvotes: 0
Views: 43
Reputation: 35900
You can use NOT EXISTS
or NOT IN
as following. NOT IN
must be used carefully as it needs to handle null condition along with it.
-- NOT EXISTS --
SELECT p.*, f.*
FROM people AS p
LEFT JOIN people_fruits AS pf ON pf.people_id = p.id
LEFT JOIN fruits AS f ON pf.fruits_id = f.id
-- added this where condition with not exists
WHERE NOT EXISTS (SELECT 1
FROM people_fruits PFIN JOIN fruits FIN
ON PFIN.fruits_id = FIN.id
AND FIN.NAME = 'orange'
and PFIN.people_id = p.id);
-- NOT IN --
SELECT p.*, f.*
FROM people AS p
LEFT JOIN people_fruits AS pf ON pf.people_id = p.id
LEFT JOIN fruits AS f ON pf.fruits_id = f.id
-- added this where condition with not IN
WHERE p.id not in (SELECT PFIN.people_id
FROM people_fruits PFIN JOIN fruits FIN
ON PFIN.fruits_id = FIN.id
AND FIN.NAME = 'orange');
Cheers!!
Upvotes: 1
Reputation: 521028
One option would be to aggregate by person and assert that he is never associated with oranges:
WITH cte AS (
SELECT p.id
FROM people AS p
LEFT JOIN people_fruits AS pf ON pf.people_id = p.id
LEFT JOIN fruits AS f ON pf.fruits_id = f.id
GROUP BY p.id
HAVING COUNT(*) FILTER (WHERE f.name = 'orange') = 0
)
SELECT p.*, f.*
FROM people AS p
LEFT JOIN people_fruits AS pf ON pf.people_id = p.id
LEFT JOIN fruits AS f ON pf.fruits_id = f.id
WHERE p.id IN (SELECT id FROM cte);
Upvotes: 0