能用,但多数场景下不推荐作为主逻辑——join或窗口函数更稳、更易读、更易维护;子查询仅适合“一次性取基准值”等轻量操作,且须确保索引、类型、过滤条件完全匹配业务规则。

子查询在跨月账单轧差中该不该用?
直接说结论:能用,但多数场景下不推荐作为主逻辑——JOIN 或窗口函数更稳、更易读、更易维护。子查询容易写成相关子查询(correlated subquery),一查一个月,再嵌套一层查上月,性能会断崖式下跌。真正适合子查询的,是「一次性取基准值」这类轻量动作,比如取上月总余额、取某客户首笔账单日期。
用子查询取上月余额时怎么避免全表扫描
常见错误是写成:SELECT ..., (SELECT SUM(amount) FROM bills WHERE customer_id = b.customer_id AND bill_month = DATE_SUB(b.bill_month, INTERVAL 1 MONTH)) AS last_month_balance FROM bills b。这个语句每行都触发一次子查询,且 bill_month 若无索引或类型不匹配(比如存为字符串'2024-03'),MySQL 就没法走索引。
实操建议:
- 确保
customer_id和bill_month组合有联合索引,顺序为(customer_id, bill_month) - 把
bill_month存为DATE类型(如'2024-03-01'),而非字符串;否则DATE_SUB()计算后无法命中索引 - 若只查最近两期,优先用
LEFT JOIN自关联替代子查询,执行计划更可控
为什么窗口函数比子查询更适合轧差累计
轧差本质是「按客户+时间排序,逐行计算当前余额与上期余额的差额」,这正是 LAG() 的典型场景。用子查询模拟 LAG() 不仅冗长,还容易在分组边界出错(比如客户A的2024-02和客户B的2024-02混排)。
示例(MySQL 8.0+):
SELECT customer_id, bill_month, balance, balance - LAG(balance) OVER (PARTITION BY customer_id ORDER BY bill_month) AS diff FROM bills;
注意点:
-
PARTITION BY customer_id必须显式写出,否则跨客户错位 -
ORDER BY bill_month要确保顺序严格(避免同月多条记录时无序) - 如果
bill_month是字符串,ORDER BY可能按字典序排错('2024-1'>'2024-10'),必须转成标准格式
真实账单表结构下子查询最容易踩的坑
生产环境账单表往往带状态字段(如 status IN ('posted', 'reversed'))、多币种(currency)、甚至分账主体(ledger_id)。子查询若漏加这些过滤条件,轧差结果就完全失真。
比如只写:(SELECT SUM(amount) FROM bills WHERE customer_id = b.customer_id AND bill_month = ...),却没加 AND status = 'posted',那已冲正的单子仍被计入,差额永远对不上。
关键检查项:
- 子查询里的
WHERE条件必须和主查询的业务过滤逻辑完全一致 - 涉及多币种时,不能直接
SUM(amount),得先统一换算,否则子查询里漏掉汇率字段就会出错 - 测试时一定要用含「跨月、冲正、补录」的真实数据集跑,光用干净测试数据看不出问题
复杂点从来不在语法,而在业务规则是否被完整映射到每一层查询里——子查询越深,漏条件的概率越高。










