提问人:Cholthi Paul Ttiopic 提问时间:10/23/2023 最后编辑:halferCholthi Paul Ttiopic 更新时间:10/23/2023 访问量:122
了解MySQL中三个表的联接
Understanding joining of three tables in MySQL
问:
下面的数据库架构表示一个名为表的虚构显示数据库。hbtn_0d_tvshows
tv_shows |
---|
编号 |
标题 |
tv_genres |
---|
编号 |
名字 |
tv_show_genres |
---|
编号 |
show_id |
genre_id |
是一个链接tv_shows表和tv_genres表的数据透视表。现在,我接到了一个任务,要检索没有喜剧类型的行。一位朋友提出了以下查询,它有效。tv_show_genres
SELECT DISTINCT `title`
FROM `tv_shows` AS t
LEFT JOIN `tv_show_genres` AS s
ON s.`show_id` = t.`id`
LEFT JOIN `tv_genres` AS g
ON g.`id` = s.`genre_id`
WHERE t.`title` NOT IN
(SELECT `title`
FROM `tv_shows` AS t
INNER JOIN `tv_show_genres` AS s
ON s.`show_id` = t.`id`
INNER JOIN `tv_genres` AS g
ON g.`id` = s.`genre_id`
WHERE g.`name` = "Comedy")
ORDER BY `title`;
我的问题是,当我们可以在外部查询中放置这样的 where 子句时,为什么我们需要子查询。由于所有表都已由联接加载,因此筛选器应该可以正常工作。如下 SQL:where g.name != "Comedy"
SELECT DISTINCT `title`
FROM `tv_shows` AS t
LEFT JOIN `tv_show_genres` AS s
ON s.`show_id` = t.`id`
LEFT JOIN `tv_genres` AS g
ON g.`id` = s.`genre_id`
WHERE g.`name` != "Comedy";
也许我遗漏了一些东西,但这对我来说是一个困惑的原因,也是我学习SQL的绊脚石。
预期结果
title
Better Call Saul
Breaking Bad
Dexter
Game of Thrones
Homeland
House
答:
我准备了一些例子:https://dbfiddle.uk/mb1_Vlc0
我在表中放置了名称为的附加列。
所以,现在我们有两个同名的电视节目,用于不同的频道:NBC和NBC2。
在 NBC 上,这是喜剧,但在 NBC2 上,这是侦探。
您的第一个示例完全排除了标题“有趣的标题”,但您的第二个查询将为其中一个频道保留此名称。tv_shows
channel
现在,如果您需要 sercond 查询的第一个逻辑,则需要进行设计。
数据模型:
insert into tv_shows values
(1, 'First title', 'NBC'),
(2, 'Second title', 'SomeTVShowChannel'),
(3, 'Some title', 'NBC'),
(4, 'Funny title', 'NBC'),
(5, 'Funny title', 'NBC2');
insert into tv_genres values
(1, 'Comedy'),
(2, 'Detective');
insert into tv_show_genres values
(1, 1, 1),
(2, 2, 2),
(3, 3, 2),
(4, 4, 1),
(5, 5, 2);
使用测试数据和查询
select *
from tv_shows as s
left join tv_show_genres as sg on sg.show_id=s.id
left join tv_genres g on g.id=sg.genre_id
输出为
编号 | 标题 | show_id | genre_id | 编号 | 名字 |
---|---|---|---|---|---|
1 | 房子 | 1 | 1 | 1 | 戏剧 |
1 | 房子 | 1 | 2 | 2 | 神秘 |
2 | 权力的游戏 | 2 | 3 | 3 | 奇遇 |
2 | 权力的游戏 | 2 | 1 | 1 | 戏剧 |
2 | 权力的游戏 | 2 | 4 | 4 | 幻想 |
3 | 生活大爆炸理论 | 3 | 5 | 5 | 喜剧 |
4 | 新女孩 | 4 | 5 | 5 | 喜剧 |
5 | 硅谷 | 5 | 5 | 5 | 喜剧 |
6 | 绝命毒师 | 6 | 6 | 6 | 犯罪 |
6 | 绝命毒师 | 6 | 1 | 1 | 戏剧 |
6 | 绝命毒师 | 6 | 7 | 7 | 悬念 |
6 | 绝命毒师 | 6 | 8 | 8 | 惊悚 |
7 | 最好打电话给扫罗 | 零 | 零 | 零 | 零 |
8 | 德克斯特 | 8 | 6 | 6 | 犯罪 |
8 | 德克斯特 | 8 | 1 | 1 | 戏剧 |
8 | 德克斯特 | 8 | 2 | 2 | 神秘 |
8 | 德克斯特 | 8 | 7 | 7 | 悬念 |
8 | 德克斯特 | 8 | 8 | 8 | 惊悚 |
9 | 家园 | 零 | 零 | 零 | 零 |
10 | 地球上的最后一个人 | 10 | 5 | 5 | 喜剧 |
10 | 地球上的最后一个人 | 10 | 1 | 1 | 戏剧 |
A) 正如 @nbk 所评论的那样,一些具有多种类型的标题不会因条件而被淘汰WHERE g.`name` != "Comedy"
B) 如果未为某些分配,则条件等于条件 所以: “喜剧”=null - 是假的 “喜剧”!=null - 是假的,或者
g.name = null - 是假的 g.name != null - 是假
的
genre_id
title
WHERE g.`name` != "Comedy"
WHERE g.`name` != null
这种情况不能按预期工作。
C) 你的第一个查询可能最好这样写
SELECT title
FROM `tv_shows` AS t
WHERE id not in
(
SELECT show_id
FROM `tv_show_genres` AS s
INNER JOIN `tv_genres` AS g
ON s.`genre_id` = g.`id`
WHERE g.`name` = "Comedy"
)
ORDER BY `title`;
以避免出现多行相同
且此查询具有更好性能的情况。Title
让我们从第二个查询开始,它没有提供所需的结果:
SELECT DISTINCT `title`
FROM `tv_shows` AS t
LEFT JOIN `tv_show_genres` AS s
ON s.`show_id` = t.`id`
LEFT JOIN `tv_genres` AS g
ON g.`id` = s.`genre_id`
WHERE g.`name` != "Comedy";
首先,不要对字符串文字使用(双引号)。它可以工作(使用默认的MySQL配置),但非标准,应避免使用。请改用(单引号)。"Comedy"
'Comedy'
其次,由于子句应用于最右边的表 (),因此联接将隐式更改为联接。WHERE
tv_genres.name
LEFT
INNER
正如 nbk 在评论中已经解释的那样,您的子句只是包含所有行 where ,这与“没有喜剧类型的电视节目”不同。WHERE
g.name != 'Comedy'
现在,让我们跳回第一个查询:
SELECT DISTINCT `title`
FROM `tv_shows` AS t
LEFT JOIN `tv_show_genres` AS s
ON s.`show_id` = t.`id`
LEFT JOIN `tv_genres` AS g
ON g.`id` = s.`genre_id`
WHERE t.`title` NOT IN
(SELECT `title`
FROM `tv_shows` AS t
INNER JOIN `tv_show_genres` AS s
ON s.`show_id` = t.`id`
INNER JOIN `tv_genres` AS g
ON g.`id` = s.`genre_id`
WHERE g.`name` = "Comedy"'Comedy')
ORDER BY `title`;
外部查询中的两个联接什么也没做,所以我们可以摆脱它们。这也意味着不再需要。我们应该使用 ,而不是在 上加入 (for),它有一个额外的好处,即不需要加入子查询:LEFT
DISTINCT
t.`title` NOT IN
title
id
tv_shows
SELECT `title`
FROM `tv_shows` AS t
WHERE t.`id` NOT IN (
SELECT `show_id`
FROM `tv_show_genres` AS s
INNER JOIN `tv_genres` AS g
ON g.`id` = s.`genre_id`
WHERE g.`name` = 'Comedy'
)
ORDER BY `title`;
这可以重写为:LEFT JOIN ... IS NULL
SELECT `title`
FROM `tv_shows` AS t
LEFT JOIN `tv_show_genres` AS s
ON s.`show_id` = t.`id`
AND s.`genre_id` = (SELECT id FROM `tv_genres` WHERE `name` = 'Comedy')
WHERE s.`genre_id` IS NULL
ORDER BY `title`;
或:NOT EXISTS
SELECT *
FROM tv_shows s
WHERE NOT EXISTS (
SELECT 1
FROM tv_show_genres sg
JOIN tv_genres g ON sg.genre_id = g.id
WHERE sg.show_id = s.id
AND g.name = 'Comedy'
)
ORDER BY title;
这是一个 db<>小提琴。
请注意,我已经更改了 db<>fiddle 中的索引。除了少数例外,数据透视表需要双向复合指数 - 和次要 .tv_show_genres
PK(show_id, genre_id)
(genre_id, show_id)
评论
NOT EXISTS
是最好的选择。有很好的文章解释了原因。下面是一个示例:sqlperformance.com/2012/12/t-sql-queries/left-anti-semi-join
EXPLAIN ANALYZE
运行查询,则应该更好地了解不同查询的工作原理。请注意,对于这种特定情况,和会产生相同的执行计划 - db<>fiddle。您可以使用OPTIMIZER_TRACE进一步探索正在发生的事情。NOT IN (subquery)
NOT EXISTS (correlated subquery)
评论
Create Table...
Insert Into ...