第 7 章 关联表查询
从一个生活场景说起
上一章我们把"用户"和"文章"拆成了两张表,用外键关联。现在要回答一个问题:"列出每个作者写了哪些文章"。
这像极了查户口:民警手里有两本册子——"居民名册"和"房产证登记册"。想知道"张三名下有几套房",就要拿着居民名册上的身份证号,去房产册里逐条对照,最后把两本册子的信息并排誊写到一张新表上。
这个"按某个编号把两张表对齐拼接"的动作,就是数据库里的 JOIN(连接查询)。
核心概念
JOIN 的两种基本口味
- INNER JOIN(内连接):只保留两边都能对上号的记录。用户没写过文章?不出现在结果里;文章找不到作者?也不出现。
- LEFT JOIN(左外连接):左边(用户表)全员保留,右边(文章表)对不上号的填 NULL。没写过文章的用户也会出现,只是标题一栏是空的。
用集合的话说:INNER JOIN 是"交集",LEFT JOIN 是"左边全集 + 交集"。
SQLAlchemy 里的写法
JOIN 查询有两个关键部分:
.select_from(左表.join(右表, 连接条件)):说明从哪张"拼接出来的大表"里取数;- 连接条件:
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 章等你。