商品和明细也到了:三表链与笛卡尔积事故

明细数据到位(500 商品 / 12 万明细)。你一条查询漏写了连接条件,跑出几十亿行的组合,笔记本风扇狂转--笛卡尔积事故现场。

学 35 min
练 70 min
盘 15 min
共 120 分钟

学 · 35 min

  1. 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,漏条件就是笛卡尔积且不报错。

  2. 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 周测专门考)。

  3. 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。

  4. 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

  1. 四表连接出订单明细:单号 / 用户名 / 商品名 / 数量 / 单价
    参考答案

    连接链按业务关系走: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;
  2. 验证结果行数 = 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 连接后行数;
  3. 故意漏写一个连接条件:先笔算会出多少行,再加 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;
  4. 统计每笔订单的商品种类数和总件数
    参考答案

    种类数要用 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;
  5. 把四表连接写成不带别名的版本,感受可读性差在哪里
    参考答案

    功能完全等价,差的是评审成本: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;
过关标准 四表连接一次写对,且结果行数与 order_items 行数一致(能解释为什么)。