Skip to content

第 7 章 关联表查询 ​

从一个生活场景说起 ​

上一章我们把"用户"和"文章"拆成了两张表,用外键关联。现在要回答一个问题:"列出每个作者写了哪些文章"。

这像极了查户口:民警手里有两本册子——"居民名册"和"房产证登记册"。想知道"张三名下有几套房",就要拿着居民名册上的身份证号,去房产册里逐条对照,最后把两本册子的信息并排誊写到一张新表上。

这个"按某个编号把两张表对齐拼接"的动作,就是数据库里的 JOIN(连接查询)。

核心概念 ​

JOIN 的两种基本口味 ​

  • INNER JOIN(内连接):只保留两边都能对上号的记录。用户没写过文章?不出现在结果里;文章找不到作者?也不出现。
  • LEFT JOIN(左外连接):左边(用户表)全员保留,右边(文章表)对不上号的填 NULL。没写过文章的用户也会出现,只是标题一栏是空的。

用集合的话说:INNER JOIN 是"交集",LEFT JOIN 是"左边全集 + 交集"。

SQLAlchemy 里的写法 ​

JOIN 查询有两个关键部分:

  1. .select_from(左表.join(右表, 连接条件)):说明从哪张"拼接出来的大表"里取数;
  2. 连接条件:user_table.c.id == article_table.c.author_id,即"用户表的 id 对上文章表的 author_id"。

示例代码(逐行讲解) ​

python
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, ForeignKey, select

engine = create_engine(
    "mysql+pymysql://root:123456@localhost:3306/demo_db?charset=utf8mb4",
    echo=True,
)
metadata = MetaData()

user_table = Table(
    "user", metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("name", String(50), nullable=False),
)
article_table = Table(
    "article", metadata,
    Column("id", Integer, primary_key=True, autoincrement=True),
    Column("title", String(200), nullable=False),
    Column("author_id", Integer, ForeignKey("user.id")),
)

with engine.connect() as conn:
    # ① INNER JOIN:只保留两边都匹配的记录
    stmt = (
        select(user_table.c.name, article_table.c.title)
        .select_from(
            user_table.join(article_table,
                            user_table.c.id == article_table.c.author_id)
        )
    )
    for row in conn.execute(stmt):
        print(row.name, "写了文章:", row.title)

    # ② LEFT JOIN:左边全部保留,右边没有则为 None
    stmt = (
        select(user_table.c.name, article_table.c.title)
        .select_from(
            user_table.outerjoin(article_table,
                                 user_table.c.id == article_table.c.author_id)
        )
    )
    for row in conn.execute(stmt):
        print(row.name, "→", row.title or "(还没写过文章)")

第一段 INNER JOIN 翻译成 SQL 是:

sql
SELECT user.name, article.title
FROM user
JOIN article ON user.id = article.author_id

如果张三写了 2 篇文章、李四没写,那么:

  • INNER JOIN 结果:张三出现 2 次(每篇文章一行),李四不出现;
  • LEFT JOIN 结果:张三出现 2 次,李四出现 1 次且 title 为 NULL。

关键知识点 ​

JOIN 类型含义
join() / INNER JOIN只返回两表都匹配的行
outerjoin() / LEFT JOIN左表全保留,右表不匹配补 NULL

深入理解:小心笛卡尔积 ​

如果 JOIN 时不写连接条件(或写错条件),数据库会把左表的每一行和右表的每一行都配对一次——这叫笛卡尔积。100 个用户 × 1000 篇文章 = 10 万行无意义结果,足以拖垮一次查询。

JOIN 条件的列必须是有逻辑对应关系的列(通常是主键 ↔ 外键),这是 JOIN 的生命线。

进阶内容 ​

1. 同名列冲突与别名。 两张表都有 id 列时,结果里会分不清谁是谁,可以用 .label() 起别名:

python
select(
    user_table.c.id.label("user_id"),
    article_table.c.id.label("article_id"),
    ...
)

2. 一次 JOIN 多张表。 链式调用即可,比如"用户 → 文章 → 评论":

python
.select_from(
    user_table
    .join(article_table, user_table.c.id == article_table.c.author_id)
    .join(comment_table, article_table.c.id == comment_table.c.article_id)
)

3. JOIN 的性能来自索引。 JOIN 时 MySQL 要拿左表的值去右表里"查找",查找快不快取决于右表被匹配的列有没有索引。MySQL 会自动为外键列建索引,所以主键↔外键的 JOIN 通常很快。

4. Core 写 JOIN 有点啰嗦? 是的。后面 ORM 章节里,relationship 会让你直接写 user.articles 就拿到所有文章,SQLAlchemy 自动生成 JOIN——这是 ORM 最香的特性之一。

注意事项 ​

  • JOIN 条件写错(如写错列)会造成"笛卡尔积",结果行数爆炸。
  • 多表 JOIN 时注意给列起别名,避免同名列冲突:.label("别名")。
  • JOIN 的性能依赖索引:确保 JOIN 条件用到的列(尤其是外键列)有索引,MySQL 对外键会自动建索引。
  • 想保留左表全部记录(包括没匹配上的)用 outerjoin();只想看两边都有的用 join()。选错会得到莫名其妙缺失或多出的行。
  • Core 风格的 JOIN 写起来繁琐,这正是后面 ORM 章节要解决的问题——ORM 可以通过 relationship 直接"点"出关联对象。

本章小结 ​

  • JOIN = 按外键关系把两张表拼接成一张大表取数;
  • INNER JOIN 取交集,LEFT JOIN 保留左表全部;
  • 连接条件是生命线,写错就是笛卡尔积灾难;
  • 觉得繁琐?ORM 的 relationship 在第 11 章等你。