Join query with only columns that have all values

2020-02-14 19:59发布

I'm creating a simple filtering system for my website. I have a many to many relationship between venues and amenities. Here are my tables.

NOTE: all ids are uuids. Making them short for simplicity

venues:

| id    |      name      |
_________________________
| 'aaa' |  'first venue' |
| 'bbb' | 'second venue' |
| 'ccc' | 'third venue'  |

amenities:

| id    |      name        |
___________________________
| 'aaa' |  'first amenity' |
| 'bbb' | 'second amenity' |
| 'ccc' | 'third amenity'  |

amenity_venue:

| amenity_id  |    venue_id  |
______________________________
| 'aaa'       |  'aaa'       |
| 'bbb'       | 'aaa'        |
| 'ccc'       | 'aaa'        |
| 'aaa'       | 'bbb'        |
| 'bbb'       | 'ccc'        |

I'm trying to write a query to return the venues that have at least all the passed in amenity_ids. For example passing in amenity_ids aaa and bbb.

Output I'm looking for when the amenity ids passed in are aaa and bbb.

| id    |      name      |
_________________________
| 'aaa' |  'first venue' |

Initially I tried this query

select * from venues 
INNER JOIN amenity_venue ON amenity_venue.venue_id = venues.id
where amenity_id in ('aaa', 'bbb');

This returns all the venues that have either amenity_id aaa or bbb

| id    |      name      |
_________________________
| 'aaa' |  'first venue' |
| 'bbb' | 'second venue' |
| 'ccc' | 'third venue'  |

so then naively I tried

select * from venues 
INNER JOIN amenity_venue ON amenity_venue.venue_id = venues.id
where amenity_id = 'aaa'
  and amenity_id = 'bbb';

Which returns nothing. I'm trying to write a query where if amenity_ids aaa and bbb are passed in only venue aaa is returned since its the only venue that has a relationship with both amenities. Also the number of amenities is dynamic from query to query.

4条回答
可以哭但决不认输i
2楼-- · 2020-02-14 20:34

Is this what you are looking for?

select * from venues 
where  exists (
    select venue_id from amenity_venue 
    where venues.id = amenity_venue.venue_id and amenity_id in ('aaa', 'bbb')
    group by venue_id
    having count(*) = 2
  )

Working Solution

查看更多
看我几分像从前
3楼-- · 2020-02-14 20:34
select * from venues where id in(
 select venue_id
 from amenity_venue
 where amenity_id in('aaa','bbb')
 group by venue_id
 having count(1) = 2
)

Right:

  • The essential part of this is to only return venue_ids for the outer select.
  • You need group by to facilitate using having.
  • Having is an aggregate form of where clause.
  • The having 2 makes sure both aaa AND bbb are present to return a venue id in the inner select, rather than the default OR.
  • count(1) - you can use column numbers instead of asterisk or column name too for aggregate functions.

Proof the code works:

https://rextester.com/TXQB38528

I made some minor tweaks and added this version too.

with amenities as (select 'aaa' as amenity_id UNION select 'bbb'),
ac as (select count(amenity_id) as tally from amenities)
select * from venues where id in(
 select venue_id
 from amenity_venue
 where amenity_id in(select amenity_id from amenities)
 group by venue_id
 having count(1) = (select tally from ac)
);

That way you aren't constrained by maintaining counts of amenities. You can see it here. https://rextester.com/TKRF28879

查看更多
家丑人穷心不美
4楼-- · 2020-02-14 20:43

I think you are looking for

SELECT v.*
FROM venues v
WHERE v.name IN (/* list of venues names */)
  AND NOT EXISTS (
         SELECT 1
         FROM amenities AS a
         WHERE a.name IN (/* list of amenity names */)
           AND NOT EXISTS (
                  SELECT 1
                  FROM amenity_venue AS av
                  WHERE av.venut_id = v.id
                    AND av.amenity_id = a.id
               )
      );

That should work independent of how many amenities there are.

You can add conditions in the places I indicated to limit the query to only a certain subset of amenities or venues.

查看更多
三岁会撩人
5楼-- · 2020-02-14 20:45

You can do this by aggregating the IDs into an array and then compare that with the list of intended IDs:

select v.*
from venues v
  join amenity_venue av ON av.venue_id = v.id
group by v.id
having array_agg(av.amenity_id) @> array['aaa', 'bbb'];

The above assumes that venue.id is declared as the primary key (because of the group by).

You don't really need to hardcode the IDs in the query if you would like to just pass the amenity names:

select v.*
from venues v
  join amenity_venue av ON av.venue_id = v.id
group by v.id
having array_agg(av.amenity_id) @> array(select id 
                                         from amenities 
                                         where name in ('first amenity', 'second amenity'));

Online example: https://rextester.com/FNNVXO34389

查看更多
登录 后发表回答