从依赖 JOIN 表的子查询中获取 SUM 值
结论
先按 customer_id 分别汇总销售额和付款额,再把两份汇总结果连接到 customers。不要在同一层同时连接销售明细和付款明细。两边都是一对多关系,直接连接会产生重复行,导致 SUM() 重复计算。
SELECT
c.id,
c.company,
COALESCE(s.total_sales, 0) AS total_sales,
COALESCE(p.total_payments, 0) AS total_payments
FROM customers AS c
LEFT JOIN (
SELECT
i.customer_id,
SUM(t.amount) AS total_sales
FROM invoices AS i
JOIN invoice_transactions AS t
ON t.invoice_id = i.id
GROUP BY i.customer_id
) AS s
ON s.customer_id = c.id
LEFT JOIN (
SELECT
ap.customer_id,
SUM(t.amount) AS total_payments
FROM account_payments AS ap
JOIN invoice_transactions AS t
ON t.id = ap.transaction_id
GROUP BY ap.customer_id
) AS p
ON p.customer_id = c.id;
错误原因
下面这段写法:
(SELECT SUM(amount) FROM sales)
不会引用外层查询中名为 sales 的表别名。SQL 会把 FROM sales 解释为查询一张实际存在且名为 sales 的表,因此 MySQL 会报错:
#1146 - Table 'db.sales' doesn't exist
这和子查询是否先于 JOIN 执行无关。表别名只在其所属的查询块中有效,不能当作临时表写入另一个查询块的 FROM 子句。
相关子查询可以引用外层别名的列,例如 sales.amount,但不能写成 FROM sales。
为什么不能直接在外层求和
即使把查询改成:
SUM(sales.amount),
SUM(payments.amount)
并补上 GROUP BY,结果仍可能不正确。
假设某个客户有:
- 3 条销售交易
- 2 条付款交易
同时连接这两组明细后,会得到 3 × 2 = 6 行。每笔销售出现两次,每笔付款出现三次,两个总额都会被放大。
所以,两类数据都要先汇总成“每位客户一行”,再进行连接。
关联关系
销售额的关联路径是:
customers.id
← invoices.customer_id
← invoice_transactions.invoice_id
对应的连接条件是:
t.invoice_id = i.id
付款额的关联路径是:
customers.id
← account_payments.customer_id
→ account_payments.transaction_id
→ invoice_transactions.id
对应的连接条件是:
t.id = ap.transaction_id
原查询使用了:
ON i.invoice_id = sales.id
但这与给出的表结构不一致,因为 invoices 中列出的是 id,不是 invoice_id。如果实际数据库使用了不同的字段名,应以真实字段为准进行调整。
注意事项
使用 LEFT JOIN 可以保留没有销售记录或付款记录的客户。没有匹配记录时,聚合结果是 NULL,可以通过:
COALESCE(total, 0)
将其转换为 0。
为提高连接和聚合效率,通常应为以下列建立索引:
invoices.customer_id
invoice_transactions.invoice_id
account_payments.customer_id
account_payments.transaction_id
如果 invoices.id 和 invoice_transactions.id 是主键,通常已经有索引。
这段查询还假定每条销售交易只通过一个 invoice_id 归属客户,并且每条付款交易不会被 account_payments 重复引用。如果数据中允许重复关联,需要先确定业务上的去重规则。不能直接使用 SUM(DISTINCT amount),因为两笔金额相同的合法交易也会被错误地合并。
备注:内容仅供参考。
