SQL: 在双重多对多关系中查找交集
SQL: Find intersection in a double many-to-many relatiion
以下是我的架构和数据的简化版本:
用户:
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-dishes 定义了用户可以吃什么。
- 关系 餐厅菜肴 定义了餐厅可以提供的服务。
作为输入,我有一个用户 ID 列表。
我现在需要的是找到列表中所有用户都可以吃他们喜欢吃的所有餐厅。
考虑以下查询:
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
所以我需要的是集合的交集。
您已经可以看到所有三个用户都可以转到 Alfonzo 和 Ali。
但是彼得不能去找马里奥。而苏珊不能去BurgerQueen。
结果(对于用户 ID 1、2、3)应为:
id | name
2 | Ali
3 | Alfonzo
对于 ID 1、2,它应该是
id | restaurant
2 | Ali
3 | Alfonzo
4 | BurgerQueen
对于 ID 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-fiddle on db-fiddle.com。
我还应该提到我需要一个兼容 MySQL 5.7 和 MariaDB 10.1
的解决方案
经典relational division。 "easiest" 方法之一是:
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)
)
Demo here。换句话说,如果有一个用户在给定的餐厅没有菜,那么给定的餐厅就不能容纳所有的用户。所以,我们想获取没有用户的餐厅,该餐厅没有菜。
我修改了您的查询以按餐厅名称分组并计算可以在每个餐厅用餐的用户并添加了一个条件:
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 |
见demo
您可以添加一个条件来检查这样的用户列表:
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;
让我们重新表述问题:找到至少为每位用户提供一道菜的餐厅。可以表示为:
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 --+
)
以下是我的架构和数据的简化版本:
用户:
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-dishes 定义了用户可以吃什么。
- 关系 餐厅菜肴 定义了餐厅可以提供的服务。
作为输入,我有一个用户 ID 列表。 我现在需要的是找到列表中所有用户都可以吃他们喜欢吃的所有餐厅。
考虑以下查询:
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
所以我需要的是集合的交集。 您已经可以看到所有三个用户都可以转到 Alfonzo 和 Ali。 但是彼得不能去找马里奥。而苏珊不能去BurgerQueen。
结果(对于用户 ID 1、2、3)应为:
id | name
2 | Ali
3 | Alfonzo
对于 ID 1、2,它应该是
id | restaurant
2 | Ali
3 | Alfonzo
4 | BurgerQueen
对于 ID 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-fiddle on db-fiddle.com。
我还应该提到我需要一个兼容 MySQL 5.7 和 MariaDB 10.1
的解决方案经典relational division。 "easiest" 方法之一是:
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)
)
Demo here。换句话说,如果有一个用户在给定的餐厅没有菜,那么给定的餐厅就不能容纳所有的用户。所以,我们想获取没有用户的餐厅,该餐厅没有菜。
我修改了您的查询以按餐厅名称分组并计算可以在每个餐厅用餐的用户并添加了一个条件:
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 |
见demo
您可以添加一个条件来检查这样的用户列表:
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;
让我们重新表述问题:找到至少为每位用户提供一道菜的餐厅。可以表示为:
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 --+
)