分类课程智能体AI
文章
订阅
分类课程AI导师
文章
价格
课程进度
5 / 15
上一节WHERE 过滤与 NULL:为什么数据会悄悄消失下一节集合操作:把多份结果纵向合并
自在学

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

关于我们隐私政策使用条款

© 2025 - 2026 株洲市自在学教育科技有限公司 版权所有

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1

编程SQL 实战:小满商店多表查询:把分散的信息重新拼起来

多表查询:把分散的信息重新拼起来

上一章我们一直在一张表里做筛选:用 WHERE 留下需要的行,用 AND、OR 和括号表达条件,再小心处理 NULL。可真实业务很少把所有信息塞进一张表。小满商店的一笔订单只保存客户编号,客户姓名放在 customers;订单里买了什么放在 order_items;商品名称和当前标价又放在 products。如果我们要回答“苏小满买过哪些商品”,只会过滤还不够,还得把分散的行重新拼起来。

这件事就是连接。连接不神秘,它做的事情可以压缩成一句话:拿左边的一行,按照连接条件去右边找匹配行,再把匹配行的列并排放到同一行中。 真正容易出错的地方,不是记住 JOIN 这个单词,而是弄清楚“为什么这两行应该相遇”“一次会相遇几行”以及“结果变多或变少是不是业务本意”。

本章只集中讲最常用的 INNER JOIN,同时把三表以上连接、派生表、自连接、非等值连接和重复行排查串起来。读完后,你应该能先画出关系路径,再写出连接条件,并能解释结果集中的每一行从哪里来。


先认识小满商店的关系地图

我们先沿用全课统一的小满商店数据。下面只列出本章会频繁使用的列;其余列仍然存在,只是当前查询暂时用不到。

表本章重点列每一行表示什么
customerscustomer_id、customer_name、city、referrer_id一位客户
ordersorder_id、customer_id、order_date、status、total_amount一张订单
order_itemsorder_item_id、order_id、product_id、quantity、unit_price、discount_rate一张订单中的一个商品明细
productsproduct_id、category_id、product_name、unit_price、stock、status一件商品
categoriescategory_id、category_name、parent_id一个商品分类
paymentspayment_id、order_id、paid_at、amount、payment_method、status一次支付尝试
departmentsdepartment_id、department_name一个部门
employeesemployee_id、department_id、manager_id、employee_name一名员工
inventory_movementsmovement_id、product_id、employee_id、quantity、moved_at一次库存变动

你可以把这些表分成两类。customers、products、categories、employees 和 departments 描述相对稳定的对象;orders、order_items、payments、inventory_movements 记录发生过的业务动作。连接通常就是沿着编号,把“动作”与“是谁、是什么、属于哪里”拼在一起。

两张员工与部门表通过部门编号连接键重新拼接信息的示意图

主键回答“这一行是谁”

customers.customer_id 是客户表的主键。主键值在表内不能重复,所以 customer_id = 1 最多定位一位客户。类似地,orders.order_id 唯一确定一张订单,products.product_id 唯一确定一件商品。

主键的唯一性会直接影响连接后的行数。订单表中的一行拿着 customer_id = 1 去客户表查找时,最多找到一行。这种“很多订单指向一位客户”的关系,连接后通常不会把每张订单复制成多行。

外键回答“这一行和谁有关”

orders.customer_id 保存下单客户的编号,它指向 customers.customer_id。前者是关系的起点,后者是被查找的目标。写成连接条件就是:

sql
ON o.customer_id = c.customer_id

这里的 o 和 c 是表别名,稍后会详细讲。先关注等号两边:它们都表达“客户编号”,数据类型也应兼容。列名是否相同不是关键,业务含义一致才是关键。例如 employees.manager_id 指向的不是另一个 manager 表,而是同一张 employees 表中的 employee_id。

外键约束能帮助数据库阻止无效编号进入表中,但“能连接”不等于“必须声明外键”。历史系统中有些关系只存在于业务约定里。写查询时仍要根据数据含义确认连接键,不能看到两个同名列就直接相等。

连接前先说清楚结果的粒度

所谓粒度,就是“结果中的一行代表什么”。这是多表查询最实用的起点。

  • 查询客户与订单时,一行通常代表一张订单。
  • 查询订单与订单明细时,一行通常代表一条商品明细。
  • 查询商品与分类时,一行通常代表一件商品。
  • 查询订单与支付记录时,一行可能代表一次支付尝试,而不再是一张订单。

如果你原本希望“一张订单一行”,却连接了可能有多次记录的 payments,结果出现重复订单并不是数据库在复制数据。结果的粒度已经被支付记录改变了。先写下“一行代表什么”,会比查询结束后再用 DISTINCT 补洞可靠得多。


先看清本章使用的固定数据

小满商店的 9 张表已经在前面的建表与填充步骤中准备好。本章不再另造一套临时数据,而是先用计数确认当前规模:

sql
SELECT
  (SELECT COUNT(*) FROM customers) AS customer_count,
  (SELECT COUNT(*) FROM products) AS product_count,
  (SELECT COUNT(*) FROM orders) AS order_count,
  (SELECT COUNT(*) FROM order_items) AS item_count,
  (SELECT COUNT(*) FROM payments) AS payment_count;
customer_countproduct_countorder_countitem_countpayment_count
1012132211

其余四张表中,categories 有 5 行,departments 有 5 行,employees 有 8 行,inventory_movements 有 12 行。

这些数字马上就会派上用场:13 张订单连接 22 条明细后,结果粒度会变成明细;10 位客户与 13 张订单做交叉连接,会得到 130 种组合。

数据中还保留了几处故意“不整齐”的业务事实:陆野从未下单;订单 107 仍待支付;订单 103 已取消;订单 105 已退款;亚麻抱枕缺货;旧版周计划本已经下架;林知夏没有直属经理。它们会帮助我们观察内连接怎样淘汰未匹配行,以及一对多关系怎样改变结果数量。

还有一个必须保持统一的金额口径:order_items.discount_rate 保存折扣率,0 表示不打折,0.1124 表示减去原价的 11.24%。因此明细折后金额应写成:

sql
quantity * unit_price * (1 - discount_rate)

订单明细中的 unit_price 是下单时成交单价,products.unit_price 是商品当前标价。列名相同不代表含义相同,写查询时必须分清属于哪张表。


笛卡尔积是怎样发生的

很多人的第一个多表查询会写成这样:

sql
SELECT c.customer_name, o.order_id
FROM customers AS c
CROSS JOIN orders AS o
ORDER BY c.customer_id, o.order_id;

CROSS JOIN 不寻找客户编号,它会把每位客户与每张订单都组合一次。当前有 10 位客户、13 张订单,因此结果有 130 行。

前 10 行看起来是这样:

customer_nameorder_id
苏小满101
苏小满102
苏小满103
苏小满104
苏小满105
苏小满106
苏小满107
苏小满108
苏小满109
苏小满110

这里只展示按客户编号、订单编号排序后的前 10 行,规律已经很清楚:苏小满先被配上所有订单,接着顾言也会被配上所有订单。只有其中少数组合碰巧符合真实关系。

若左表有 NNN 行,右表有 MMM 行,交叉连接的结果行数就是:

N×MN \times MN×M

当 customers 有 20 万行、orders 有 300 万行时,理论组合数会膨胀到 6000 亿。数据库不一定真的把所有中间行完整写到磁盘上,但这条查询表达的逻辑范围就是如此巨大。

漏掉连接条件也会制造笛卡尔积

下面这种逗号写法与交叉连接表达了相同的组合意图:

sql
SELECT c.customer_name, o.order_id
FROM customers AS c, orders AS o;

还有一种更隐蔽的情况:查询已经写了一个正确连接,却漏掉了第三张表的连接条件。

sql
SELECT o.order_id, c.customer_name, p.product_name
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
CROSS JOIN products AS p;

订单与客户的部分没有问题,但每张订单又与 12 件商品全部组合,13 张订单会扩成 156 行。多表查询出现“行数突然大很多”时,不要只盯着最后一个 WHERE 条件,先检查是否每加入一张表,就随之写了清楚的连接条件。

订单表13行与商品表12行无连接条件产生156行笛卡尔积示意图

交叉连接本身不是错误。生成日期与时段的全部组合、制作尺码与颜色矩阵时,它很有用。危险的是无意中得到笛卡尔积,却把它当成业务关系继续计算。

用计数先验证膨胀规模

遇到可疑查询,可以暂时不选业务列,先数行:

sql
SELECT COUNT(*) AS row_count
FROM customers AS c
CROSS JOIN orders AS o;
row_count
130

再分别数两张表:

sql
SELECT
  (SELECT COUNT(*) FROM customers) AS customer_count,
  (SELECT COUNT(*) FROM orders) AS order_count;
customer_countorder_count
1013

10 × 13 = 130,这就说明结果是完整组合,而不是正常的一对多展开。


INNER JOIN 只保留真正匹配的行

我们真正想要的是“订单属于哪位客户”。订单表已经保存 customer_id,因此连接条件应比较两边的客户编号:

sql
SELECT
  o.order_id,
  o.order_date,
  c.customer_name,
  c.city,
  o.total_amount
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
ORDER BY o.order_id;

订单明细表与商品表内连接仅保留双方匹配商品编号的示意图

order_idorder_datecustomer_namecitytotal_amount
1012025-06-18 10:05:00苏小满杭州118.90
1022025-07-02 21:10:00顾言上海188.00
1032025-08-15 13:20:00白露成都87.00
1042025-09-09 09:20:00江行苏州183.90
1052025-10-12 16:40:00苏小满杭州159.00
1062025-11-01 11:00:00余晴广州140.00
1072025-12-20 19:30:00乔木北京137.00
1082026-01-11 12:40:00程一南京190.00
1092026-02-14 20:10:00顾言上海150.00
1102026-03-08 18:25:00安禾西安99.00
1112026-04-01 08:30:00苏小满杭州99.90
1132026-06-18 10:15:00江行苏州227.00
1142026-07-07 07:50:00夏初武汉86.00

数据库可以把这个过程理解为:取订单 101 的 customer_id = 1,在客户表中找到 customer_id = 1 的苏小满;取订单 102 的 customer_id = 2,找到顾言;如此继续。陆野在客户表里存在,但没有订单行来与他匹配,因此不会出现在结果中。

为什么叫“内”连接

INNER JOIN 只保留连接条件结果为真的行对。左侧有、右侧没有,或者右侧有、左侧没有,都不会被补进结果。它取的是两边关系相交的那一部分。

假设历史数据中出现一张 customer_id = 99 的订单,而客户表没有 99 号客户,这张订单也会从上面的内连接结果中消失。内连接回答的是“哪些订单能找到客户”,不保证“订单表每一行都出现”。当前数据有外键约束,所以这种孤立订单无法正常写入;旧数据迁移或暂时关闭约束时仍要懂得这种行为。

这与上一章讲的 NULL 也连得上。如果 orders.customer_id 为 NULL,条件 o.customer_id = c.customer_id 不会得到真值,自然匹配不到任何客户。

INNER 可以省略,但意图不要省略

下面两种写法在这里含义相同:

sql
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
sql
FROM orders AS o
JOIN customers AS c
  ON o.customer_id = c.customer_id

我建议学习阶段保留 INNER。当一条查询同时出现内连接和外连接时,明确写出连接类型更容易检查。团队已有统一规范时,再按规范选择即可。

ON 可以包含多个连接条件

有些关系只靠一列无法准确识别。虽然小满商店统一结构里的主外键大多只需一列,我们仍要知道 ON 可以使用 AND 组合多个条件:

下面的 table_a、table_b 只是用来推导复合连接键写法的抽象占位符,不是小满商店新增的业务表。

sql
SELECT ...
FROM table_a AS a
INNER JOIN table_b AS b
  ON a.key_part_1 = b.key_part_1
 AND a.key_part_2 = b.key_part_2;

复合连接键少写一部分,往往不会报语法错误,只会让一行匹配到更多行。这类错误比漏写整个 ON 更难发现,所以看到复合主键或复合唯一键时,要逐列核对。


别名不是为了少打几个字

多表查询里经常有同名列。orders 和 payments 都有 order_id、status,order_items 和 products 都有 unit_price。如果只写:

sql
SELECT order_id, status
FROM orders
INNER JOIN payments
  ON order_id = order_id;

这段 SQL 至少有两个问题。数据库无法确定 SELECT 中的 order_id 和 status 来自哪张表;ON order_id = order_id 也没有表达左右两张表的关系。某些环境会直接报告列名歧义,即使侥幸执行,意图也完全不可审查。

给表起别名后,关系就清楚了:

sql
SELECT
  o.order_id,
  o.status AS order_status,
  p.payment_id,
  p.status AS payment_status
FROM orders AS o
INNER JOIN payments AS p
  ON o.order_id = p.order_id
WHERE o.order_id IN (101, 105, 110)
ORDER BY o.order_id, p.payment_id;
order_idorder_statuspayment_idpayment_status
101已完成1成功
105已退款4已退款
110已支付8成功

一套稳定的别名习惯

短别名没有唯一标准,但要让读者能快速反推表名。

表推荐别名记忆方式
customersccustomer
ordersoorder
order_itemsoiorder item
productspproduct
categoriescatcategory
paymentspaypayment
employeeseemployee
departmentsddepartment
inventory_movementsiminventory movement

有两点比别名长短更重要:同一条语句中不要让一个别名承担两个角色;一旦声明别名,后续就始终用别名限定列。

sql
SELECT o.order_id, c.customer_name
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id;

不要前面定义 orders AS o,后面又写 orders.customer_id。别名生效后,它就是这次查询中该表引用的名字。

输出列也要说明含义

表别名解决“输入列来自哪里”,列别名解决“输出列表示什么”。订单状态与支付状态都叫 status,如果原样输出,应用程序或导出的表格很难区分。因此应写成:

sql
o.status AS order_status,
pay.status AS payment_status

连接越复杂,越不要依赖 SELECT *。明确列出所需字段,既能防止同名列冲突,也能避免表结构增加一列后接口结果悄悄改变。


三张表以上:沿着关系路径逐段连接

现在回答开头的问题:“每张订单买了什么,购买者是谁?” 所需信息分布在四张表:

text
customers → orders → order_items → products

连接路径不是按表名猜出来的,而是由键一段一段接起来:

text
customers.customer_id = orders.customer_id
orders.order_id = order_items.order_id
order_items.product_id = products.product_id

客户表、订单表、订单明细表和商品表沿三个连接键重新拼接的四表路径图

把这三段关系翻译成 SQL:

sql
SELECT
  o.order_id,
  c.customer_name,
  p.product_name,
  oi.quantity,
  oi.unit_price,
  oi.discount_rate,
  ROUND(
    oi.quantity * oi.unit_price * (1 - oi.discount_rate),
    2
  ) AS line_amount
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON oi.product_id = p.product_id
WHERE o.order_id IN (101, 108, 114)
ORDER BY o.order_id, oi.order_item_id;
order_idcustomer_nameproduct_namequantityunit_pricediscount_rateline_amount
101苏小满白瓷马克杯139.900.000039.90
101苏小满原木托盘189.000.112479.00
108程一65W 氮化镓充电器1169.000.0847154.69
108程一编织数据线139.000.094935.30
114夏初编织数据线239.000.000078.00

这里有 3 张订单,却得到 5 行,因为粒度已经从“一张订单”变成“一条订单明细”。订单 101 与 108 各有两条明细,所以自然各出现两次。客户姓名跟随每条明细重复,也是正确结果的一部分。

为什么不能直接从订单跳到商品

下面这段条件看似都在比较编号,实际上关系是错的:

sql
SELECT o.order_id, p.product_name
FROM orders AS o
INNER JOIN products AS p
  ON o.order_id = p.product_id;

order_id 和 product_id 都是整数,不代表它们可以相等。即使数据里恰好同时出现某个相同数字,那也只是编号碰巧相同,不是业务关系。订单与商品是多对多关系:一张订单能有多件商品,一件商品也能出现在多张订单中。order_items 正是把这两个方向连接起来的中间表。

写多表连接时逐段验收

不要一口气写完六张表再执行。更稳的方式是每加一张表就验证一次:

先连接 orders 与 customers,确认一张订单最多对应一位客户,并观察订单数是否符合预期。

再加入 order_items,预期行数变成明细数。若订单重复,先确认这是不是一单多明细造成的正常展开。

最后连接 products,由于 product_id 在商品表中唯一,每条明细应最多找到一件商品,行数通常不再增加。

可以用下面的计数查询辅助验证:

sql
SELECT COUNT(*) AS joined_rows
FROM orders AS o
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id;
joined_rows
22

它与当前 order_items 的 22 行一致。若连接商品后突然超过 22 行,就要检查 products.product_id 是否真的唯一,或者 ON 是否误用了 category_id。

五表查询仍然只是继续沿路走

若还要展示商品分类,只需从商品继续连接分类:

sql
SELECT
  o.order_id,
  c.customer_name,
  p.product_name,
  cat.category_name,
  oi.quantity
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON oi.product_id = p.product_id
INNER JOIN categories AS cat
  ON p.category_id = cat.category_id
WHERE o.order_id IN (101, 108, 114)
ORDER BY o.order_id, oi.order_item_id;
order_idcustomer_nameproduct_namecategory_namequantity
101苏小满白瓷马克杯厨房用品1
101苏小满原木托盘厨房用品1
108程一65W 氮化镓充电器数码配件1
108程一编织数据线数码配件1
114夏初编织数据线数码配件2

这里 WHERE 只留下示范用的三张订单。连接条件回答表怎么拼,过滤条件回答拼好以后要哪些行。如果改成过滤订单状态,连接路径本身不需要改变。


派生表:先把一个查询结果当成表

FROM 后面不一定只能写真实表,也可以放一个带括号的 SELECT。这个临时查询结果叫派生表。它只在当前语句中存在,但在外层查询看来,它和普通表一样有行、有列,也能参与连接。

假设运营想查看“单次购买至少两件的明细”,并补上订单客户与商品名称。可以先在派生表中筛选明细:

sql
SELECT
  o.order_id,
  c.customer_name,
  p.product_name,
  bulk_item.quantity,
  bulk_item.unit_price
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN (
  SELECT
    order_id,
    product_id,
    quantity,
    unit_price
  FROM order_items
  WHERE quantity >= 2
) AS bulk_item
  ON o.order_id = bulk_item.order_id
INNER JOIN products AS p
  ON bulk_item.product_id = p.product_id
ORDER BY o.order_id;
order_idcustomer_nameproduct_namequantityunit_price
109顾言亚麻抱枕279.00
113江行四季手账本249.00
114夏初编织数据线239.00

派生表必须有名字

外层查询需要一个名字来引用派生结果,因此括号后要写别名:

sql
(
  SELECT ...
) AS bulk_item

如果省掉 bulk_item,外层的 ON 与 SELECT 就没有稳定的限定名。派生表内部选出的列也要避免重名,因为外层会把它们当作一张表的列来使用。

派生表适合表达“先得到什么”

上面的逻辑也可以直接把 oi.quantity >= 2 写在外层 WHERE 中,结果相同:

sql
SELECT
  o.order_id,
  c.customer_name,
  p.product_name,
  oi.quantity,
  oi.unit_price
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON oi.product_id = p.product_id
WHERE oi.quantity >= 2
ORDER BY o.order_id;

所以,派生表不是“写了就一定更快”的技巧。数据库可能把它合并进外层查询,也可能采用中间结果。使用它的主要理由应是让查询的阶段更清楚,或者先完成一个必须独立计算的分组、去重、排名,再与其他表连接。

不要把每张表都包进派生表。层层括号会隐藏关系路径,让列来自哪里更难追踪。能用直接连接清楚表达时,就保持直接。

派生表里少选一列会发生什么

下面的派生表没有输出 product_id,外层却拿它连接商品:

sql
SELECT p.product_name, bulk_item.quantity
FROM (
  SELECT order_id, quantity
  FROM order_items
  WHERE quantity >= 2
) AS bulk_item
INNER JOIN products AS p
  ON bulk_item.product_id = p.product_id;

数据库会报告 bulk_item.product_id 不存在。派生表是一道边界:内部表有多少列不重要,外层只能看到内部 SELECT 明确输出的列。排查这类错误时,先看派生表的选择列表,不要只看原始表结构。


同一张表出现两次:每次都扮演一个角色

小满商店的 customers.referrer_id 保存推荐人的客户编号。它指向同一张表的 customer_id。若要显示“谁由谁推荐”,必须把 customers 放入查询两次:一次代表新客户,一次代表推荐人。

sql
SELECT
  new_customer.customer_name AS new_customer_name,
  referrer.customer_name AS referrer_name
FROM customers AS new_customer
INNER JOIN customers AS referrer
  ON new_customer.referrer_id = referrer.customer_id
ORDER BY new_customer.customer_id;
new_customer_namereferrer_name
顾言苏小满
白露苏小满
江行顾言
余晴白露
夏初江行

这就是自连接。数据库没有把表复制一份;两个别名只是让同一张表在同一条语句中扮演两个可区分的角色。

为什么不能只用一个别名

如果写成:

sql
SELECT c.customer_name
FROM customers AS c
INNER JOIN customers AS c
  ON c.referrer_id = c.customer_id;

两个表实例都叫 c,数据库无法判断每个 c 指向哪一边。正确做法是用角色命名:new_customer 与 referrer 比 c1 与 c2 更容易阅读。

员工与直属上级也是自连接

employees.manager_id 指向 employees.employee_id。查询员工和直属上级时,可以写:

sql
SELECT
  e.employee_name AS employee_name,
  m.employee_name AS manager_name,
  e.job_title
FROM employees AS e
INNER JOIN employees AS m
  ON e.manager_id = m.employee_id
ORDER BY e.employee_id;

这里仍然是内连接,所以 manager_id 为 NULL 的最高负责人不会出现。不是这位员工被删除了,而是他找不到满足 e.manager_id = m.employee_id 的另一行。后续学习外连接时,我们会解决“左边没有匹配也要保留”的需求。

分类父子关系同样适用

categories.parent_id 也指向本表的 category_id。我们可以列出子分类与父分类:

sql
SELECT
  child.category_name AS child_category,
  parent.category_name AS parent_category
FROM categories AS child
INNER JOIN categories AS parent
  ON child.parent_id = parent.category_id
ORDER BY child.category_id;
child_categoryparent_category
厨房用品居家生活

根分类的 parent_id 为 NULL,因此不会出现在内连接结果中。自连接很适合查“一层关系”;若要一次展开不确定深度的整棵层级树,则需要后面课程中的递归查询思路。


等值连接与非等值连接

我们目前写过的关系大多使用等号:

sql
ON o.customer_id = c.customer_id

这种连接叫等值连接。主键与外键天然适合等值连接,因为我们要找的就是编号相同的那一行。

等值连接不等于“一对一”

等号只规定“如何匹配”,并不保证“一边只有一行”。例如一张订单可以拥有多条订单明细:

sql
SELECT o.order_id, oi.order_item_id, oi.product_id
FROM orders AS o
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
WHERE o.order_id = 101
ORDER BY oi.order_item_id;
order_idorder_item_idproduct_id
10111
10122

订单 101 会匹配两条明细。连接条件明明是等号,结果仍然是一对多。是否展开取决于连接列在两侧是不是唯一,不取决于用了哪个比较符。

非等值连接用范围或顺序建立关系

连接条件也可以使用 <、>、<=、>= 或范围表达式。最直观的例子是生成不重复的商品对比组合。我们想把每两件在售商品配成一组,但不允许商品和自己配对,也不希望“咖啡—茶”与“茶—咖啡”重复出现:

sql
SELECT
  cheaper.product_name AS cheaper_product,
  cheaper.unit_price AS cheaper_price,
  dearer.product_name AS dearer_product,
  dearer.unit_price AS dearer_price
FROM products AS cheaper
INNER JOIN products AS dearer
  ON cheaper.unit_price < dearer.unit_price
WHERE cheaper.status = '在售'
  AND dearer.status = '在售'
ORDER BY cheaper.unit_price, dearer.unit_price;

部分结果如下:

cheaper_productcheaper_pricedearer_productdearer_price
香樟衣柜挂片25.00雾蓝中性笔套装29.90
香樟衣柜挂片25.00编织数据线39.00
香樟衣柜挂片25.00白瓷马克杯39.90
香樟衣柜挂片25.00四季手账本49.00
香樟衣柜挂片25.00原木托盘89.00
香樟衣柜挂片25.00轻量折叠伞99.00
香樟衣柜挂片25.00暖光阅读灯129.00
香樟衣柜挂片25.00保温随行杯139.00
香樟衣柜挂片25.0065W 氮化镓充电器169.00
雾蓝中性笔套装29.90编织数据线39.00

cheaper.unit_price < dearer.unit_price 一次解决了两个问题:相同商品的价格不小于自己,因此不会与自己配对;每对商品只保留“低价在左、高价在右”的方向,因此不会出现反向重复。

如果两个商品同价,上面的条件不会把它们配成一组。若业务要包含同价但不同商品,可以把关系写得更精确:

sql
ON cheaper.unit_price < dearer.unit_price
OR (
     cheaper.unit_price = dearer.unit_price
     AND cheaper.product_id < dearer.product_id
   )

非等值连接的匹配范围通常比主外键连接更宽,一行可能与许多行相遇。动手前最好估算结果规模,并确认相应的范围是否重叠。


ON 与 WHERE 各自负责什么

把它们记成两句话就够了:

  • ON 说明两侧的行怎样才算有关联。
  • WHERE 说明关联完成后,最终想留下哪些行。

例如查找已支付订单及客户:

sql
SELECT
  o.order_id,
  c.customer_name,
  o.total_amount
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
WHERE o.status = '已支付'
ORDER BY o.order_id;

o.customer_id = c.customer_id 是关系,因此放在 ON;o.status = '已支付' 是业务筛选,因此放在 WHERE。

order_idcustomer_nametotal_amount
110安禾99.00

在内连接里挪位置,结果可能相同

下面把订单状态也放进 ON:

sql
SELECT
  o.order_id,
  c.customer_name,
  o.total_amount
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
 AND o.status = '已支付'
ORDER BY o.order_id;

对于当前内连接,它与上一条查询返回相同的一行。因为内连接最终只保留匹配成功的行,优化器也可能对条件重新安排。但是“结果碰巧一样”不代表两种写法同样清楚。第一种一眼就能区分关系和业务筛选,加入更多表后更容易维护。

错把筛选当连接会掩盖真正关系

看下面这个错误:

sql
SELECT o.order_id, p.product_name
FROM orders AS o
INNER JOIN products AS p
  ON p.status = '在售';

p.status = '在售' 只筛选商品,没有说明订单与商品怎样关联。每张订单会与每件在售商品组合,本质上是过滤后的笛卡尔积。正确路径必须经过 order_items:

sql
SELECT o.order_id, p.product_name
FROM orders AS o
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON oi.product_id = p.product_id
WHERE p.status = '在售';

ON 中出现条件,不代表它就是连接条件。判断标准不是放在哪个子句,而是它有没有描述左右两边的关系。只引用一侧表的条件通常是筛选;如果整个 ON 没有把两侧列联系起来,就要警惕笛卡尔积。

为后续外连接留下一条清晰边界

内连接里某些条件放 ON 或 WHERE 结果相同,但到了外连接,两者会改变“未匹配行是否保留”。因此从现在就养成职责分离的习惯:关系条件写在对应的 ON,最终结果过滤写在 WHERE。这样学习外连接时,不需要推翻现有写法。


连接基数决定结果会增加多少行

连接基数描述的是左边一行与右边多少行匹配。我们不用先背理论名词,可以直接问两个问题:

  1. 左侧连接列是否唯一?
  2. 右侧连接列是否唯一?

一对一:两边的连接键都唯一

若两边连接列都唯一,一行最多匹配一行,连接通常不会增加行数。统一数据中没有专门拆分的一对一扩展表,但“订单与仅保留每单一条的派生结果”可以形成一对一关系。

多对一:明细指向唯一商品

order_items.product_id 可以重复,因为很多明细能购买同一商品;products.product_id 是主键,不能重复。因此每条明细最多匹配一件商品:

sql
SELECT COUNT(*) AS item_rows
FROM order_items;
item_rows
22
sql
SELECT COUNT(*) AS joined_rows
FROM order_items AS oi
INNER JOIN products AS p
  ON oi.product_id = p.product_id;
joined_rows
22

在没有孤立外键的前提下,连接前后都是 22 行。

一对多:订单指向多条明细

orders.order_id 唯一,order_items.order_id 可重复。一张订单会展开为它拥有的每条明细。订单 101 因此出现两行,订单 111 会出现三行。

多对多:两侧的连接值都可能重复

真实多对多关系通常通过中间表表达,例如订单与商品由 order_items 连接。也可能因为选错键,无意间制造多对多。

下面错误地用 category_id 连接订单明细与商品:

sql
SELECT oi.order_item_id, p.product_id, p.product_name
FROM order_items AS oi
INNER JOIN products AS p
  ON oi.product_id = p.category_id;

当前商品编号与分类编号都从较小整数开始,所以这条错误语句甚至能返回一些行:例如明细中的 product_id = 2 会匹配所有 category_id = 2 的商品。结果看起来有商品名,却没有一件是依据明细真正购买的商品匹配出来的。连接键检查不能只看结果有没有数据。


“重复行”通常是关系在说话

多表查询一出现重复,很多人会立刻加 DISTINCT:

sql
SELECT DISTINCT o.order_id, c.customer_name
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id;

这确实会把订单 101 的两条明细压回一行,但也把“为什么会重复”藏起来了。若查询本来就只需要订单编号和客户名,那么根本不必连接 order_items;若后面要计算件数或金额,提前 DISTINCT 还可能让结果失真。

先判断重复发生在哪个键上

查询每张订单在连接后出现几次:

sql
SELECT
  o.order_id,
  COUNT(*) AS rows_after_join
FROM orders AS o
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
GROUP BY o.order_id
HAVING COUNT(*) > 1
ORDER BY o.order_id;
order_idrows_after_join
1012
1022
1042
1062
1082
1113
1133

再看明细表本身:

sql
SELECT
  order_id,
  COUNT(*) AS item_count
FROM order_items
GROUP BY order_id
HAVING COUNT(*) > 1;
order_iditem_count
1012
1022
1042
1062
1082
1113
1133

两边计数一致,说明重复来自正常的一单多明细,不是连接条件错误。

检查你以为唯一的列是否真的唯一

若你认为 payments.order_id 每单只会有一行,先验证:

sql
SELECT
  order_id,
  COUNT(*) AS payment_count
FROM payments
GROUP BY order_id
HAVING COUNT(*) > 1;

当前查询不返回任何行,说明这批数据中每张有支付记录的订单恰好只有一条支付。不过,payments.order_id 没有唯一约束,未来完全可能因失败重试而出现多行。数据现状是唯一的,不等于表结构保证它永远唯一。

如果报告只想看成功支付,仍应把需求明确写进查询:

sql
SELECT
  o.order_id,
  c.customer_name,
  pay.payment_method,
  pay.paid_at
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN payments AS pay
  ON o.order_id = pay.order_id
WHERE pay.status = '成功'
  AND o.order_id IN (101, 108, 114)
ORDER BY o.order_id;
order_idcustomer_namepayment_methodpaid_at
101苏小满微信2025-06-18 10:07:00
108程一微信2026-01-11 12:42:00
114夏初微信2026-07-07 07:52:00

这里结果一单一行,是因为样例中每单恰好只有一条成功记录。若业务允许退款后再次支付,或者数据异常产生两条成功记录,仍可能重复。真正要求“每单最新一条成功支付”时,需要先在派生表中明确选出最新记录,而不是假设状态过滤后必然唯一。

同时连接两个一对多方向会相乘

这是最容易被误判的一类重复。商品 5 是“65W 氮化镓充电器”,它出现在 4 条订单明细中,也有 3 条库存流水。如果同时把明细与库存流水直接连接到商品:

sql
SELECT
  p.product_id,
  oi.order_item_id,
  im.movement_id
FROM products AS p
INNER JOIN order_items AS oi
  ON p.product_id = oi.product_id
INNER JOIN inventory_movements AS im
  ON p.product_id = im.product_id
WHERE p.product_id = 5
ORDER BY oi.order_item_id, im.movement_id;
product_idorder_item_idmovement_id
534
536
538
564
566
568
584
586
588
5124
5126
5128

4 条明细乘 3 条库存流水,得到 12 行。数据库无法自动知道“某条销售明细应该对应哪条库存流水”,因为统一结构里二者只有共同的商品编号,没有逐条对应的业务键。

如果直接对这 12 行求销售金额,每条明细会被重复计算 3 次;若统计库存变动,每条流水又会重复 4 次。解决方向通常有三个:先分别汇总到“一件商品一行”再连接;只保留业务上需要的某个子集;或者拆成两个查询分别展示不同粒度的信息。选哪一个取决于报表想让一行代表什么。

DISTINCT 只能删除最终选择列表完全相同的行,不能修复错误的连接关系。看到重复时先检查基数、连接键和目标粒度,最后才判断是否真的需要去重。


一套可复用的连接排错流程

多表查询结果不对,通常表现为三种情况:行太多、行太少、列值看似正常但来自错误对象。可以按下面的顺序排查。

先画关系路径,不急着写 SELECT

例如要查“库存变动由谁操作、涉及什么商品、员工属于哪个部门”,路径应是:

text
products ← inventory_movements → employees → departments

对应连接键:

text
inventory_movements.product_id = products.product_id
inventory_movements.employee_id = employees.employee_id
employees.department_id = departments.department_id

如果画不出路径,说明业务关系还没弄清楚。此时继续堆 SQL 只会增加偶然得到错误结果的概率。

从最小连接开始数行

sql
SELECT COUNT(*)
FROM inventory_movements AS im
INNER JOIN products AS p
  ON im.product_id = p.product_id;

确认合理后再加员工:

sql
SELECT COUNT(*)
FROM inventory_movements AS im
INNER JOIN products AS p
  ON im.product_id = p.product_id
INNER JOIN employees AS e
  ON im.employee_id = e.employee_id;

每加一张表都记录行数变化。哪一步发生异常,问题通常就在那一步新加入的关系上。

暂时把键都选出来

业务报表可能只展示名称,但排错时应把编号暴露出来:

sql
SELECT
  im.movement_id,
  im.product_id AS movement_product_id,
  p.product_id AS matched_product_id,
  im.employee_id AS movement_employee_id,
  e.employee_id AS matched_employee_id
FROM inventory_movements AS im
INNER JOIN products AS p
  ON im.product_id = p.product_id
INNER JOIN employees AS e
  ON im.employee_id = e.employee_id;

这样可以直接检查等号两边是否符合预期。只看商品名和员工名,很容易被“碰巧像对的”结果骗过去。

分别检查两侧重复值

如果连接后行数异常增加,对两侧连接列分别执行:

sql
SELECT product_id, COUNT(*) AS duplicate_count
FROM products
GROUP BY product_id
HAVING COUNT(*) > 1;

主键列正常时不应返回任何行。再检查允许重复的业务列:

sql
SELECT product_id, COUNT(*) AS movement_count
FROM inventory_movements
GROUP BY product_id
HAVING COUNT(*) > 1;

这里返回多行不一定是问题,因为同一商品本来就可能发生多次库存变动。关键是你是否预期结果粒度变成“一次库存变动”。

检查行太少是不是内连接淘汰了未匹配数据

内连接只保留两边都能匹配的行。可以分别计数:

sql
SELECT COUNT(*) AS order_count
FROM orders;
sql
SELECT COUNT(*) AS matched_order_count
FROM orders AS o
INNER JOIN payments AS pay
  ON o.order_id = pay.order_id;

后一条不是“有支付的订单数”,而是“订单与支付的匹配行数”;一次订单有两次支付就会计两行。若要确认有多少不同订单匹配到支付,可写:

sql
SELECT COUNT(DISTINCT o.order_id) AS paid_attempt_order_count
FROM orders AS o
INNER JOIN payments AS pay
  ON o.order_id = pay.order_id;
paid_attempt_order_count
11

总共 13 张订单,只有 11 张至少有一条支付记录。已取消的订单 103 与待支付的订单 107 没有支付记录,所以在内连接中消失。这是连接类型的行为,不一定是数据丢失。

最后才恢复完整业务列和排序

键、基数、行数都确认以后,再加入名称、金额、表达式和 ORDER BY。排错阶段越小,越容易定位;完整查询应该是验证后的关系逐步累积出来的结果。


常见错误,逐条拆开看

错误:只按同名列连接

sql
SELECT ...
FROM order_items AS oi
INNER JOIN products AS p
  ON oi.unit_price = p.unit_price;

两张表都有 unit_price,但一个是成交单价,一个是当前标价。价格相等不能证明该明细购买的就是这件商品,而且调价后原本的商品还可能匹配不上。正确的关系键是 oi.product_id = p.product_id。

错误:把查询展示名当成连接键

sql
SELECT ...
FROM customers AS c
INNER JOIN employees AS e
  ON c.customer_name = e.employee_name;

姓名会重复,也会修改,更没有客户与员工之间的直接业务关系。除非需求明确要求做同名匹配,否则这不是可靠连接。

错误:少写一段关系

sql
SELECT o.order_id, p.product_name
FROM orders AS o
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON o.order_id = oi.order_id;

第二个 ON 仍在重复订单与明细的条件,没有把新加入的 products AS p 接进来。由于条件对商品表没有限制,每条已匹配明细会与所有商品组合。正确条件是 oi.product_id = p.product_id。

错误:把历史成交价换成当前商品价

sql
SELECT
  oi.quantity * p.unit_price AS line_amount
FROM order_items AS oi
INNER JOIN products AS p
  ON oi.product_id = p.product_id;

连接本身是对的,计算语义却可能错。订单金额通常要使用下单时记录在 order_items.unit_price 的成交价,而不是商品表中的当前价格。正确表达应根据业务规则使用:

sql
oi.quantity * oi.unit_price * (1 - oi.discount_rate)

多表查询不只要防“拼错行”,还要防“从正确的行里拿错列”。

错误:用 WHERE 补救错误连接

如果 ON 错了,随后增加更多日期、状态过滤,只会让错行变少,不会让关系变对。正确顺序是先验证连接键与基数,再验证业务过滤。


练习:把关系说清楚再动手

下面练习都沿用本章的小满商店表与样例数据。建议先写下“结果一行代表什么”和关系路径,再展开答案。

练习:订单客户清单

查询所有已支付订单,显示订单编号、客户姓名、城市、订单金额,按订单编号升序排列。

sql
SELECT
  o.order_id,
  c.customer_name,
  c.city,
  o.total_amount
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
WHERE o.status = '已支付'
ORDER BY o.order_id;
order_idcustomer_namecitytotal_amount
110安禾西安99.00

结果一行代表一张已支付订单。ON 负责订单与客户的编号关系,WHERE 负责订单状态。

练习:已支付订单的商品明细

显示已支付订单的订单编号、客户姓名、商品名称、数量与明细金额。

sql
SELECT
  o.order_id,
  c.customer_name,
  p.product_name,
  oi.quantity,
  ROUND(
    oi.quantity * oi.unit_price * (1 - oi.discount_rate),
    2
  ) AS line_amount
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON oi.product_id = p.product_id
WHERE o.status = '已支付'
ORDER BY o.order_id, oi.order_item_id;
order_idcustomer_nameproduct_namequantityline_amount
110安禾轻量折叠伞199.00

结果一行代表一条已支付订单明细。当前只有订单 110 的状态恰好是“已支付”;“已完成”和“已发货”是其他订单状态,不能在没有说明口径时自动混进来。

练习:成功支付方式

查询成功支付的订单编号、客户姓名、支付方式和支付时间。

sql
SELECT
  o.order_id,
  c.customer_name,
  pay.payment_method,
  pay.paid_at
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN payments AS pay
  ON o.order_id = pay.order_id
WHERE pay.status = '成功'
ORDER BY o.order_id;

连接支付表后,一行代表一次成功支付。不要把订单状态与支付状态混为一谈,所以输出与过滤都要使用明确别名。

练习:分类与父分类

列出有父分类的分类名称及其父分类名称。

sql
SELECT
  child.category_name AS category_name,
  parent.category_name AS parent_category_name
FROM categories AS child
INNER JOIN categories AS parent
  ON child.parent_id = parent.category_id
ORDER BY child.category_id;
category_nameparent_category_name
厨房用品居家生活

这是自连接。child 与 parent 指向同一张表,但承担不同角色。

练习:找出订单与明细连接后的重复来源

下面查询为什么让订单 101 出现两行?

sql
SELECT o.order_id, oi.order_item_id, oi.product_id
FROM orders AS o
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
WHERE o.order_id = 101;

order_items.order_id 不唯一。订单 101 有白瓷马克杯和原木托盘两条明细,因此一张订单匹配两行。两行分别代表两个真实的商品明细:

sql
SELECT o.order_id, oi.order_item_id, p.product_name
FROM orders AS o
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON oi.product_id = p.product_id
WHERE o.order_id = 101
ORDER BY oi.order_item_id;

不要先加 DISTINCT。如果目标粒度是订单明细,这两行都应该保留;如果只要订单清单,则不应为了取客户名而连接明细表。

练习:修复漏掉的连接

下面查询有什么问题?

sql
SELECT c.customer_name, o.order_id, p.product_name
FROM customers AS c
INNER JOIN orders AS o
  ON c.customer_id = o.customer_id
INNER JOIN products AS p
  ON c.customer_id = o.customer_id;

第二个 ON 重复了客户与订单关系,新加入的商品表没有被连接,因此每张订单会与全部商品组合。订单到商品必须经过订单明细:

sql
SELECT c.customer_name, o.order_id, p.product_name
FROM customers AS c
INNER JOIN orders AS o
  ON c.customer_id = o.customer_id
INNER JOIN order_items AS oi
  ON o.order_id = oi.order_id
INNER JOIN products AS p
  ON oi.product_id = p.product_id;

练习:限定数量的派生表

使用派生表先找出购买数量至少为 2 的明细,再连接订单与客户,显示订单编号、客户姓名和数量。

sql
SELECT
  o.order_id,
  c.customer_name,
  bulk_item.quantity
FROM orders AS o
INNER JOIN customers AS c
  ON o.customer_id = c.customer_id
INNER JOIN (
  SELECT order_id, quantity
  FROM order_items
  WHERE quantity >= 2
) AS bulk_item
  ON o.order_id = bulk_item.order_id
ORDER BY o.order_id;

派生表必须有别名 bulk_item,而外层需要的 order_id 与 quantity 必须出现在派生表的选择列表里。

概念检测

1
orders 有 13 行,products 有 12 行,二者 CROSS JOIN 会产生多少行?
2
结果希望一行代表一条订单明细,哪条连接路径正确?
3
连接后订单重复,哪些检查有助于找到原因?
4
INNER JOIN 使用等号时,结果就一定是一对一。
5
派生表的内部原表有 product_id,外层就一定可以直接使用 product_id。

从“横向拼列”走向“纵向合并”

这一章处理的问题有一个共同特点:相关信息分散在不同表的不同列里。我们沿着主键和外键把它们横向拼到一行,用 INNER JOIN 留下匹配成功的行;表多了就逐段连接;同一张表承担两个角色时使用自连接;结果变多时回到粒度与连接基数,而不是急着去重。

下一类问题看起来相似,方向却不同。比如小满商店要把“杭州客户名单”和“上海客户名单”叠成一份名单,两边返回的是相同结构的行,我们不是继续向右增加列,而是把第二个查询的行接到第一个查询下面。这个动作不靠连接键,而要使用 UNION、UNION ALL 等集合操作。

进入下一章前,先记住这条分界线:JOIN 主要解决列分散的问题,集合操作主要解决行分散的问题。 判断清楚要横向拼列还是纵向叠行,SQL 的方向就不会走偏。

  • 先认识小满商店的关系地图
    • 主键回答“这一行是谁”
    • 外键回答“这一行和谁有关”
    • 连接前先说清楚结果的粒度
  • 先看清本章使用的固定数据
  • 笛卡尔积是怎样发生的
    • 漏掉连接条件也会制造笛卡尔积
    • 用计数先验证膨胀规模
  • INNER JOIN 只保留真正匹配的行
    • 为什么叫“内”连接
    • `INNER` 可以省略,但意图不要省略
    • `ON` 可以包含多个连接条件
  • 别名不是为了少打几个字
    • 一套稳定的别名习惯
    • 输出列也要说明含义
  • 三张表以上:沿着关系路径逐段连接
    • 为什么不能直接从订单跳到商品
    • 写多表连接时逐段验收
    • 五表查询仍然只是继续沿路走
  • 派生表:先把一个查询结果当成表
    • 派生表必须有名字
    • 派生表适合表达“先得到什么”
    • 派生表里少选一列会发生什么
  • 同一张表出现两次:每次都扮演一个角色
    • 为什么不能只用一个别名
    • 员工与直属上级也是自连接
    • 分类父子关系同样适用
  • 等值连接与非等值连接
    • 等值连接不等于“一对一”
    • 非等值连接用范围或顺序建立关系
  • ON 与 WHERE 各自负责什么
    • 在内连接里挪位置,结果可能相同
    • 错把筛选当连接会掩盖真正关系
    • 为后续外连接留下一条清晰边界
  • 连接基数决定结果会增加多少行
    • 一对一:两边的连接键都唯一
    • 多对一:明细指向唯一商品
    • 一对多:订单指向多条明细
    • 多对多:两侧的连接值都可能重复
  • “重复行”通常是关系在说话
    • 先判断重复发生在哪个键上
    • 检查你以为唯一的列是否真的唯一
    • 同时连接两个一对多方向会相乘
  • 一套可复用的连接排错流程
    • 先画关系路径,不急着写 SELECT
    • 从最小连接开始数行
    • 暂时把键都选出来
    • 分别检查两侧重复值
    • 检查行太少是不是内连接淘汰了未匹配数据
    • 最后才恢复完整业务列和排序
  • 常见错误,逐条拆开看
    • 错误:只按同名列连接
    • 错误:把查询展示名当成连接键
    • 错误:少写一段关系
    • 错误:把历史成交价换成当前商品价
    • 错误:用 WHERE 补救错误连接
  • 练习:把关系说清楚再动手
    • 练习:订单客户清单
    • 练习:已支付订单的商品明细
    • 练习:成功支付方式
    • 练习:分类与父分类
    • 练习:找出订单与明细连接后的重复来源
    • 练习:修复漏掉的连接
    • 练习:限定数量的派生表
    • 概念检测
  • 从“横向拼列”走向“纵向合并”

目录

  • 先认识小满商店的关系地图
    • 主键回答“这一行是谁”
    • 外键回答“这一行和谁有关”
    • 连接前先说清楚结果的粒度
  • 先看清本章使用的固定数据
  • 笛卡尔积是怎样发生的
    • 漏掉连接条件也会制造笛卡尔积
    • 用计数先验证膨胀规模
  • INNER JOIN 只保留真正匹配的行
    • 为什么叫“内”连接
    • `INNER` 可以省略,但意图不要省略
    • `ON` 可以包含多个连接条件
  • 别名不是为了少打几个字
    • 一套稳定的别名习惯
    • 输出列也要说明含义
  • 三张表以上:沿着关系路径逐段连接
    • 为什么不能直接从订单跳到商品
    • 写多表连接时逐段验收
    • 五表查询仍然只是继续沿路走
  • 派生表:先把一个查询结果当成表
    • 派生表必须有名字
    • 派生表适合表达“先得到什么”
    • 派生表里少选一列会发生什么
  • 同一张表出现两次:每次都扮演一个角色
    • 为什么不能只用一个别名
    • 员工与直属上级也是自连接
    • 分类父子关系同样适用
  • 等值连接与非等值连接
    • 等值连接不等于“一对一”
    • 非等值连接用范围或顺序建立关系
  • ON 与 WHERE 各自负责什么
    • 在内连接里挪位置,结果可能相同
    • 错把筛选当连接会掩盖真正关系
    • 为后续外连接留下一条清晰边界
  • 连接基数决定结果会增加多少行
    • 一对一:两边的连接键都唯一
    • 多对一:明细指向唯一商品
    • 一对多:订单指向多条明细
    • 多对多:两侧的连接值都可能重复
  • “重复行”通常是关系在说话
    • 先判断重复发生在哪个键上
    • 检查你以为唯一的列是否真的唯一
    • 同时连接两个一对多方向会相乘
  • 一套可复用的连接排错流程
    • 先画关系路径,不急着写 SELECT
    • 从最小连接开始数行
    • 暂时把键都选出来
    • 分别检查两侧重复值
    • 检查行太少是不是内连接淘汰了未匹配数据
    • 最后才恢复完整业务列和排序
  • 常见错误,逐条拆开看
    • 错误:只按同名列连接
    • 错误:把查询展示名当成连接键
    • 错误:少写一段关系
    • 错误:把历史成交价换成当前商品价
    • 错误:用 WHERE 补救错误连接
  • 练习:把关系说清楚再动手
    • 练习:订单客户清单
    • 练习:已支付订单的商品明细
    • 练习:成功支付方式
    • 练习:分类与父分类
    • 练习:找出订单与明细连接后的重复来源
    • 练习:修复漏掉的连接
    • 练习:限定数量的派生表
    • 概念检测
  • 从“横向拼列”走向“纵向合并”