商品和明细也到了:三表链与笛卡尔积事故
明细数据到位(500 商品 / 12 万明细)。你一条查询漏写了连接条件,跑出几十亿行的组合,笔记本风扇狂转--笛卡尔积事故现场。
学 · 35 min
01orders -> order_items -> products 的连接链
场景老板要看「每笔订单里都有什么商品」。明细表 order_items 两头各揣一个外键:order_id 指向订单,product_id 指向商品--它就是把三张表串起来的桥。
报表要的列分布在不同表里:单号在 orders、用户名在 users、商品名在 products、数量单价在 order_items。连接链按业务关系走:orders → order_items(一笔订单含多件商品)→ products(商品详情)。四表连起来,每行就是「某笔订单里的某件商品」。
select o.id as 单号, u.name as 用户, p.name as 商品, i.qty as 数量, i.unit_price as 单价 from orders o join order_items i on i.order_id = o.id join products p on p.id = i.product_id join users u on u.id = o.user_id limit 5;易错连接顺序按链条写、JOIN 一行一个,别把四张表挤在 FROM 里用逗号连接--老式逗号语法没有 ON,漏条件就是笛卡尔积且不报错。
02一对多连接的行数放大:明细表决定结果行数
场景你连了 order_items 统计订单 GMV,总金额凭空翻了倍--数据没坏,是行数被放大了。
orders 和 order_items 是一对多:一笔订单平均挂 2-3 件商品,连接后同一笔订单变成 2-3 行,
sum(o.total_amount)就把它重复累加了。判断连接后该有多少行:沿连接链找「多」的那一端,结果行数 = 多端行数(前提都匹配上)。select count(*) from orders; -- 5 万 select count(*) from order_items; -- ≈12 万 select count(*) from orders o join order_items i on i.order_id = o.id; -- ≈12 万,等于明细行数易错「连了明细表之后聚合数字变大」首先怀疑一对多放大。修法:先按 order_id 聚合明细、再连接回主表(D14 周测专门考)。
03漏写连接条件 = 笛卡尔积(CROSS JOIN)
场景漏写一个 ON,笔记本风扇狂转--你正在计算两表所有行的组合。
没有 ON 的 JOIN 退化为 CROSS JOIN:结果行数 = 两表行数之积。orders 5 万行 × order_items 12 万行 = 60 亿行,数据库真的会一行行算出来。这是新手把库跑挂的第一名原因。
-- 惰性:LIMIT 几行就停,可以看到组合的样子 select o.id, i.id from orders o cross join order_items i limit 5; -- 危险:count(*) 要把 60 亿行数完才返回,别在脑子里跑 -- select count(*) from orders cross join order_items;易错EXPLAIN 里看到两侧都没有过滤条件、估行数 = 两表行数之积,就是在算笛卡尔积--赶紧 Ctrl+C。
04USING 与 NATURAL JOIN 为什么生产不用
场景PG 允许 using (...) 简写,natural join 甚至自动按同名列连接--看起来更省事。
using (列)要求两边的连接列同名;本库里 orders.user_id 对 users.id,名字不同,USING 根本写不了。natural join 更激进:把所有同名列全当连接条件。省几个字的代价是赌表结构永远不变--哪天加了一个同名列,natural join 的连接条件悄悄变了,结果变了,不报任何错。-- 本库写不了 USING(连接列不同名),只能显式 ON: select o.id, u.city from orders o join users u on o.user_id = u.id;易错生产代码里见到 natural join 就重构;显式 ON 多打几个字,换来的是改动安全。
练 · 70 min
- 四表连接出订单明细:单号 / 用户名 / 商品名 / 数量 / 单价
参考答案
连接链按业务关系走:orders -> order_items -> products,users 补用户维度;一行一个 JOIN,谁也别挤在逗号后面。
select o.id as 单号, u.name as 用户, p.name as 商品, i.qty as 数量, i.unit_price as 单价 from orders o join order_items i on i.order_id = o.id join products p on p.id = i.product_id join users u on u.id = o.user_id limit 10; - 验证结果行数 = order_items 行数,解释为什么
参考答案
orders : order_items 是一对多,沿连接链找「多」的一端--结果行数由明细表决定;seed 里每个订单都有明细、每条明细都有商品,所以两个数相等(≈12 万)。
select (select count(*) from order_items) as 明细行数, (select count(*) from orders o join order_items i on i.order_id = o.id) as 连接后行数; - 故意漏写一个连接条件:先笔算会出多少行,再加 LIMIT 跑一遍验证
参考答案
漏 ON 的 JOIN 就退化成 CROSS JOIN,PG 会一行行真算这 60 亿。LIMIT 让它吐 5 行就停,安全看到「组合」的样子;再想验证笔算,看 EXPLAIN 的估行数是不是两表行数之积。
-- 笔算:漏掉 orders -> order_items 的 ON,5 万 × 12 万 = 60 亿行 -- 用 CROSS JOIN 显式写出「没有条件的连接」,加 LIMIT 只看 5 行 select o.id as 单号, i.id as 明细号 from orders o cross join order_items i limit 5; - 统计每笔订单的商品种类数和总件数
参考答案
种类数要用 count(distinct product_id):同一单里同一商品出现两行时,count(*) 会把它数成两种。这题不用连 orders--度量全躺在明细表上,别为了「报表感」白白多连一张表。
select order_id, count(distinct product_id) as 商品种类数, sum(qty) as 总件数 from order_items group by order_id order by order_id limit 10; - 把四表连接写成不带别名的版本,感受可读性差在哪里
参考答案
功能完全等价,差的是评审成本:ON 里每列都拖全名,一行长一倍;起了别名后表改名只动 from 一行。团队协作里几乎一律用别名。
select orders.id as 单号, users.name as 用户, products.name as 商品, order_items.qty as 数量, order_items.unit_price as 单价 from orders join order_items on order_items.order_id = orders.id join products on products.id = order_items.product_id join users on users.id = orders.user_id limit 5;