MySQL SELECT 按 payment method 和 status 汇总销售额
结论
这里应使用“条件聚合”,也就是把 CASE 放在 SUM() 内部。还需要先按 basket_id 汇总付款金额,避免 basket_payment 中存在多条记录时重复累计 amount_due。
假设付款方式 ID 如下:
1:CASH2:Credit/Debit Card4:Cash on Delivery
查询可以写成:
WITH payment_totals AS (
SELECT basket_id,
payment_method_id,
SUM(amount_paid) AS amount_paid
FROM basket_payment
GROUP BY basket_id, payment_method_id
)
SELECT DATE(bv.sale_time) AS sale_date,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 1
AND bt.payment_status IN ('paid', 'partial')
THEN COALESCE(p.amount_paid, 0)
WHEN p.payment_method_id = 4
AND bt.payment_status = 'paid'
THEN COALESCE(p.amount_paid, 0)
ELSE 0
END
), 2) AS total_cash_sales,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 1
AND bt.payment_status IN ('due', 'partial')
THEN GREATEST(
bv.amount_due - COALESCE(p.amount_paid, 0),
0
)
WHEN p.payment_method_id = 4
AND bt.payment_status = 'due'
THEN GREATEST(
bv.amount_due - COALESCE(p.amount_paid, 0),
0
)
ELSE 0
END
), 2) AS total_credit_sales,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 4
AND bt.payment_status IN ('paid', 'due')
THEN bv.amount_due
ELSE 0
END
), 2) AS total_cash_on_delivery_sales,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 2
AND bt.payment_status = 'paid'
THEN COALESCE(p.amount_paid, 0)
ELSE 0
END
), 2) AS total_card_sales
FROM basket_amount_due AS bv
JOIN basket AS bt
ON bt.basket_id = bv.basket_id
LEFT JOIN payment_totals AS p
ON p.basket_id = bv.basket_id
WHERE bv.sale_time >= CURDATE()
AND bv.sale_time < CURDATE() + INTERVAL 1 DAY
GROUP BY DATE(bv.sale_time);
按照题目提供的数据,结果应为:
| total_cash_sales | total_credit_sales | total_cash_on_delivery_sales | total_card_sales |
|---|---|---|---|
| 3160.00 | 1570.50 | 1688.40 | 2070.00 |
计算逻辑
total_cash_sales 是实际收到的现金:
1170.00 + 1000.00 + 990.00 = 3160.00
具体包括:
- CASH 且状态为
paid时,计入已付款金额。 - CASH 且状态为
partial时,只计入已经支付的部分。 - Cash on Delivery 且状态为
paid时,计入已付款金额。
total_credit_sales 实际统计的是尚未收取的金额:
756.60 + (1115.50 - 1000.00) + 698.40 = 1570.50
部分付款订单按下面的公式计算:
未付款金额 = amount_due - amount_paid
total_cash_on_delivery_sales 按题目规则统计 Cash on Delivery 的订单总额,不区分当前状态是 paid 还是 due:
990.00 + 698.40 = 1688.40
total_card_sales 统计银行卡已支付的金额:
2070.00
为什么原查询得不到正确结果
原查询使用了下面的结构:
CASE
WHEN ...
THEN SUM(amount_due)
ELSE 0
END
这种写法先按付款方式分组,再判断当前分组属于哪种类型,因此每个 payment_method_id 都会返回一行,无法直接得到每天一行、包含多个汇总列的结果。
条件聚合应该写成:
SUM(
CASE
WHEN 条件 THEN 金额
ELSE 0
END
)
每条销售记录先根据条件确定计入多少金额,再由 SUM() 统一汇总。
原查询中的:
GROUP BY p.payment_method_id
还会把 CASH、Card 和 Cash on Delivery 拆成多行。目标结果是每天一行,因此只需要:
GROUP BY DATE(bv.sale_time)
日期条件的写法
不建议这样写:
WHERE DATE(bv.sale_time) = CURDATE()
对 sale_time 调用 DATE(),可能导致普通的 sale_time 索引无法用于范围查找。使用半开区间更合适:
WHERE bv.sale_time >= CURDATE()
AND bv.sale_time < CURDATE() + INTERVAL 1 DAY
查询指定日期时,可以写成:
WHERE bv.sale_time >= '2021-09-18'
AND bv.sale_time < '2021-09-19'
MySQL 5.7 写法
MySQL 5.7 不支持 CTE,可以把付款汇总改为派生表:
SELECT DATE(bv.sale_time) AS sale_date,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 1
AND bt.payment_status IN ('paid', 'partial')
THEN COALESCE(p.amount_paid, 0)
WHEN p.payment_method_id = 4
AND bt.payment_status = 'paid'
THEN COALESCE(p.amount_paid, 0)
ELSE 0
END
), 2) AS total_cash_sales,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 1
AND bt.payment_status IN ('due', 'partial')
THEN GREATEST(
bv.amount_due - COALESCE(p.amount_paid, 0),
0
)
WHEN p.payment_method_id = 4
AND bt.payment_status = 'due'
THEN GREATEST(
bv.amount_due - COALESCE(p.amount_paid, 0),
0
)
ELSE 0
END
), 2) AS total_credit_sales,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 4
AND bt.payment_status IN ('paid', 'due')
THEN bv.amount_due
ELSE 0
END
), 2) AS total_cash_on_delivery_sales,
ROUND(SUM(
CASE
WHEN p.payment_method_id = 2
AND bt.payment_status = 'paid'
THEN COALESCE(p.amount_paid, 0)
ELSE 0
END
), 2) AS total_card_sales
FROM basket_amount_due AS bv
JOIN basket AS bt
ON bt.basket_id = bv.basket_id
LEFT JOIN (
SELECT basket_id,
payment_method_id,
SUM(amount_paid) AS amount_paid
FROM basket_payment
GROUP BY basket_id, payment_method_id
) AS p
ON p.basket_id = bv.basket_id
WHERE bv.sale_time >= CURDATE()
AND bv.sale_time < CURDATE() + INTERVAL 1 DAY
GROUP BY DATE(bv.sale_time);
注意事项
这段查询有一个前提:每个 basket_id 只能对应一种付款方式。如果订单允许同时使用现金和银行卡拆分付款,就不能按付款方式直接重复归类 amount_due。此时需要在 basket_payment 中保存各付款方式分别承担的金额,再按该金额汇总。
这里的 total_credit_sales 表示“尚未收款的销售额”,并不是银行卡销售额。为了避免业务含义混淆,实际项目中可以将该列改名为:
total_outstanding_sales
金额字段应优先使用 DECIMAL,不要使用 FLOAT 或 DOUBLE,以免出现不必要的浮点精度误差。
备注:内容仅供参考。
