MySQL:如何在多表查询中计算每个 Trip 的 Payments 总额?
结论
先按 trips_tripID 汇总 payments.amount,再将汇总结果连接到 Trip。由于 seminar 是可选关系,从 people_has_trips 连接到 seminar 时应使用 LEFT JOIN。
建议先聚合 Payments。这样即使其他一对多表中有多条相关记录,付款金额也不会被重复计算:
SELECT
pht.people_PersonID,
pht.trips_tripID,
s.seminarname,
ar.agent,
COALESCE(pay.total_amount, 0) AS Amount
FROM people_has_trips AS pht
INNER JOIN trips AS t
ON t.tripID = pht.trips_tripID
LEFT JOIN seminar AS s
ON s.seminarID = pht.seminar_seminarID
INNER JOIN agentref AS ar
ON ar.agentID = t.agentref_agentID
LEFT JOIN (
SELECT
trips_tripID,
SUM(amount) AS total_amount
FROM payments
GROUP BY trips_tripID
) AS pay
ON pay.trips_tripID = t.tripID;
为什么要先汇总 Payments
payments 与 trips 通常是一对多关系。一条 Trip 可以对应多笔付款,直接连接这两个表时,每笔付款都会产生一行结果。
下面的子查询先按 Trip 汇总付款:
SELECT
trips_tripID,
SUM(amount) AS total_amount
FROM payments
GROUP BY trips_tripID
汇总后,每个 Trip 只保留一行。外层查询连接这个结果时,Amount 就是该 Trip 的付款总额。
使用 LEFT JOIN 可以保留还没有付款记录的 Trip。此时汇总金额为 NULL,因此需要使用:
COALESCE(pay.total_amount, 0)
将其显示为 0。
让没有 Seminar 的记录也出现在结果中
可选外键应使用 LEFT JOIN:
LEFT JOIN seminar AS s
ON s.seminarID = pht.seminar_seminarID
这样会保留所有 people_has_trips 记录。没有关联 Seminar 时,seminarname 为 NULL,person 和 trip 仍会出现在结果中。
如果需要筛选 Seminar,要注意 WHERE 条件的位置。例如:
WHERE s.seminarname = 'Example'
这个条件会排除没有 Seminar 的记录,使 LEFT JOIN 的实际效果变成 INNER JOIN。如果筛选条件只用于限制参与连接的 Seminar,应将其写在连接条件中:
LEFT JOIN seminar AS s
ON s.seminarID = pht.seminar_seminarID
AND s.seminarname = 'Example'
也可以直接使用 GROUP BY
如果查询中没有其他可能产生重复行的一对多连接,也可以直接聚合:
SELECT
pht.people_PersonID,
pht.trips_tripID,
s.seminarname,
ar.agent,
COALESCE(SUM(p.amount), 0) AS Amount
FROM people_has_trips AS pht
INNER JOIN trips AS t
ON t.tripID = pht.trips_tripID
LEFT JOIN seminar AS s
ON s.seminarID = pht.seminar_seminarID
INNER JOIN agentref AS ar
ON ar.agentID = t.agentref_agentID
LEFT JOIN payments AS p
ON p.trips_tripID = t.tripID
GROUP BY
pht.people_PersonID,
pht.trips_tripID,
s.seminarname,
ar.agent;
这种写法更短,但要确认其他连接不会导致付款记录成倍增加。
特别注意 agentnotes
原查询还连接了 agentnotes:
INNER JOIN agentnotes
ON trips.tripID = agentnotes.Trips_tripID
如果一个 Trip 有多条 agentnotes,每笔 Payment 都会被复制多次。此时直接执行 SUM(payments.amount),得到的金额会偏大。
如果结果中既不需要备注字段,也不需要按备注筛选,最好删除这个连接。如果只是要求 Trip 至少有一条备注,可以改用 EXISTS,以免付款记录被复制:
WHERE EXISTS (
SELECT 1
FROM agentnotes AS an
WHERE an.Trips_tripID = t.tripID
)
如果必须同时返回多条备注,就需要单独聚合 Payments,并明确结果是一行对应一个 Trip,还是一行对应一条备注。对于“一人参加了哪些 Trip,以及每个 Trip 总共花了多少钱”这一需求,先按 Trip 聚合 Payments 更稳妥。
备注:内容仅供参考。
