了解MySQL中三个表的联接

Understanding joining of three tables in MySQL

提问人:Cholthi Paul Ttiopic 提问时间:10/23/2023 最后编辑:halferCholthi Paul Ttiopic 更新时间:10/23/2023 访问量:122

问:

下面的数据库架构表示一个名为表的虚构显示数据库。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
SQL MySQL 联接

评论

1赞 Stefan Wuebbe 10/23/2023
我猜读者可能会喜欢一些示例数据(例如 + 语句行),以及对预期结果的描述,以说明“有效”和“无效”对您来说到底意味着什么。另请参阅 dbfiddle.ukCreate Table...Insert Into ...
3赞 nbk 10/23/2023
因为您要排除标题,并且在第二个查询中,您只会删除其中没有喜剧的行,因此不会删除具有多种类型的标题
0赞 Cholthi Paul Ttiopic 10/23/2023
@nbk我对 SQL 联接如何工作的假设是,所有联接都转换为大表,其中包含所有表中所选列的行。稍后,where 条件将应用于大表

答:

-1赞 FriendlyDragon 10/23/2023 #1

我准备了一些例子:https://dbfiddle.uk/mb1_Vlc0

我在表中放置了名称为的附加列。 所以,现在我们有两个同名的电视节目,用于不同的频道:NBC和NBC2。 在 NBC 上,这是喜剧,但在 NBC2 上,这是侦探。 您的第一个示例完全排除了标题“有趣的标题”,但您的第二个查询将为其中一个频道保留此名称。tv_showschannel

现在,如果您需要 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);

我的链接截图仅用于演示。enter image description here

0赞 ValNik 10/23/2023 #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_idtitleWHERE 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

在这里演示

3赞 user1191247 10/23/2023 #3

让我们从第二个查询开始,它没有提供所需的结果:

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'

其次,由于子句应用于最右边的表 (),因此联接将隐式更改为联接。WHEREtv_genres.nameLEFTINNER

正如 nbk 在评论中已经解释的那样,您的子句只是包含所有行 where ,这与“没有喜剧类型的电视节目”不同。WHEREg.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),它有一个额外的好处,即不需要加入子查询:LEFTDISTINCTt.`title` NOT INtitleidtv_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_genresPK(show_id, genre_id)(genre_id, show_id)

评论

1赞 Jonas Metzler 10/23/2023
NOT EXISTS是最好的选择。有很好的文章解释了原因。下面是一个示例:sqlperformance.com/2012/12/t-sql-queries/left-anti-semi-join
0赞 Cholthi Paul Ttiopic 10/23/2023
@user1191247,当在两个表的联接中使用时,右侧外部表上的条件如何工作,就像在第一个解决方案子查询中一样
0赞 user1191247 10/23/2023
@CholthiPaulTtiopic 对不起,我不确定您指的是哪个查询。如果使用 EXPLAIN ANALYZE 运行查询,则应该更好地了解不同查询的工作原理。请注意,对于这种特定情况,和会产生相同的执行计划 - db<>fiddle。您可以使用OPTIMIZER_TRACE进一步探索正在发生的事情。NOT IN (subquery)NOT EXISTS (correlated subquery)