SQL: найти пересечение в двойном отношении многие ко многим - PullRequest
3 голосов
/ 13 марта 2019

Ниже приведена упрощенная версия моей схемы и данных:

Пользователи:

id | name
 1 | Peter
 2 | Max
 3 | Susan

рестораны:

id | name
 1 | Mario
 2 | Ali
 3 | Alfonzo
 4 | BurgerQueen

посуда:

id | name
 1 | Burger
 2 | Pizza
 3 | Salad

users_dishes:

user_id | dish_id
      1 | 1
      2 | 1
      2 | 2
      3 | 2
      3 | 3

restaurants_dishes:

restaurant_id | dish_id
            1 | 2
            1 | 3
            2 | 1
            2 | 3
            3 | 1
            3 | 2
            3 | 3
            4 | 1

Итак, у меня есть три объекта: пользователи , рестораны и блюда . И два отношения многие ко многим .

  • Отношение users-dish определяет, что пользователь может есть.
  • Отношение рестораны-блюда определяет то, что ресторан может служить.

В качестве ввода у меня есть список идентификаторов пользователей. Теперь мне нужно найти все рестораны, где все пользователи из списка могут есть то, что им нравится.

Рассмотрим следующий запрос:

select u.name as user, group_concat(distinct r.name) as dishes
from users u
join users_dishes ud on ud.user_id = u.id
join restaurants_dishes rd on rd.dish_id = ud.dish_id
join restaurants r on r.id = rd.restaurant_id
group by u.id

Здесь показаны все рестораны, которые может посетить каждый пользователь.

user  | restaurants
Peter | Alfonzo,Ali,BurgerQueen
Max   | Alfonzo,Ali,BurgerQueen,Mario
Susan | Alfonzo,Ali,Mario

Итак, мне нужно пересечение множеств. Вы уже можете видеть, что все три пользователя могут перейти к Альфонсо и Али. Но Петр не может пойти к Марьяо. И Сьюзен не может пойти в BurgerQueen.

Результат (для идентификаторов пользователя 1,2,3) должен быть:

id | name
 2 | Ali
 3 | Alfonzo

Для идентификаторов 1, 2 это должно быть

id | restaurant
 2 | Ali
 3 | Alfonzo
 4 | BurgerQueen

Для идентификаторов 2, 3 это должно быть

id | restaurant
 1 | Mario
 2 | Ali
 3 | Alfonzo

Вы можете создать схему и пример данных с помощью следующего сценария SQL:

CREATE TABLE users (id INT AUTO_INCREMENT,name varchar(100),PRIMARY KEY (id));
INSERT INTO users(name) VALUES ('Peter'),('Max'),('Susan');
CREATE TABLE restaurants (id INT AUTO_INCREMENT,name varchar(100),PRIMARY KEY (id));
INSERT INTO restaurants(name) VALUES ('Mario'),('Ali'),('Alfonzo'),('BurgerQueen');
CREATE TABLE dishes (id INT AUTO_INCREMENT,name varchar(100),PRIMARY KEY (id));
INSERT INTO dishes(name) VALUES ('Burger'),('Pizza'),('Salad');
CREATE TABLE users_dishes (user_id INT,dish_id INT,PRIMARY KEY (user_id, dish_id),INDEX (dish_id, user_id));
INSERT INTO users_dishes(user_id, dish_id) VALUES (1,1),(2,1),(2,2),(3,2),(3,3);
CREATE TABLE restaurants_dishes (restaurant_id INT,dish_id INT,PRIMARY KEY (restaurant_id, dish_id),INDEX (dish_id, restaurant_id));
INSERT INTO restaurants_dishes(restaurant_id, dish_id) VALUES (1,2),(1,3),(2,1),(2,3),(3,1),(3,2),(3,3),(4,1);

Я также подготовил SQL-скрипту на db-fiddle.com .

Следует также упомянуть, что мне нужно решение, совместимое с MySQL 5.7 и MariaDB 10.1

Ответы [ 3 ]

4 голосов
/ 13 марта 2019

Классик реляционное деление . Вот один из самых «простых» подходов:

select *
from restaurants r
where not exists (
  select *
  from users u
  where not exists (
    select *
    from users_dishes ud
    join restaurants_dishes rd on ud.dish_id = rd.dish_id
    where ud.user_id = u.id
    and rd.restaurant_id = r.id
  )
  and u.id in (1, 2, 3)
)

Демо здесь . Другими словами, если был пользователь, для которого в данном ресторане не было блюдо, то этот ресторан не мог вместить всех пользователей. Итак, мы хотим получить рестораны, для которых нет пользователя, для которых в этом ресторане нет блюда.

3 голосов
/ 13 марта 2019

Я изменил ваш запрос, чтобы сгруппировать по названию ресторана и подсчитать количество пользователей, которые могут есть на каждом из них, и добавил условие, имеющее:

select r.id, r.name as restaurant
from users u
join users_dishes ud on ud.user_id = u.id
join restaurants_dishes rd on rd.dish_id = ud.dish_id
join restaurants r on r.id = rd.restaurant_id
group by r.id, r.name
having count(distinct u.id) = (select count(*) from users);

Результаты:

| id  | restaurant |
| --- | ---------- |
| 2   | Ali        |
| 3   | Alfonzo    |

См. демо Вы можете добавить условие для проверки списка пользователей, например:

select r.id, r.name as restaurant
from users u
join users_dishes ud on ud.user_id = u.id
join restaurants_dishes rd on rd.dish_id = ud.dish_id
join restaurants r on r.id = rd.restaurant_id
where u.id in (1, 2, 3)
group by r.id, r.name
having count(distinct u.id) = 3;
1 голос
/ 13 марта 2019

Давайте перефразируем проблему: найдите рестораны, которые подают хотя бы одно блюдо для каждого пользователя.Это может быть выражено как:

SELECT *
FROM restaurants
WHERE id IN (
    SELECT restaurants_dishes.restaurant_id
    FROM restaurants_dishes
    JOIN users_dishes ON restaurants_dishes.dish_id = users_dishes.dish_id
    WHERE users_dishes.user_id IN (1, 2, 3)         -- <--------------+
    GROUP BY restaurants_dishes.restaurant_id       --                |
    HAVING COUNT(DISTINCT users_dishes.user_id) = 3 -- this matches --+
)
...