WHERE子句中不能直接使用聚合函数,因执行时未分组聚合;应改用HAVING(配合GROUP BY)或子查询先计算值再比较。

WHERE子句里不能直接用聚合函数对比跨月数据
财务系统里想比「上月营收 vs 本月营收」,很多人第一反应是写 WHERE SUM(amount) > (SELECT SUM(amount) FROM sales WHERE month = '2024-08') —— 这会报错:ERROR: aggregate functions are not allowed in WHERE。因为 WHERE 执行时还没分组聚合,SUM() 根本不可用。
真正能用的时机是 HAVING(配合 GROUP BY)或放到子查询里先算出值再比较。比如要查「本月营收超过上月110%的部门」,得把上月总和单独拎成子查询:
SELECT dept, SUM(amount) AS this_month FROM sales WHERE month = '2024-09' GROUP BY dept HAVING SUM(amount) > ( SELECT SUM(amount) * 1.1 FROM sales WHERE month = '2024-08' );
日期字段类型不一致会导致跨月查询全空
财务表里 month 字段常见三种存法:CHAR(7)(如 '2024-09')、DATE(如 '2024-09-01')、INT(如 202409)。混用时看似能查,实际可能漏数据——比如用 WHERE month = '2024-09' 查 DATE 类型字段,MySQL 会隐式转成 '2024-09-00',结果为 NULL。
- 统一用
DATE类型 +YEAR_MONTH()或DATE_FORMAT(date_col, '%Y-%m')提取年月 - 若只能用字符串,确保所有查询都加
TRIM()防空格,且大小写一致(尤其 Oracle 对大小写敏感) - 测试时随手加
SELECT DISTINCT month, LENGTH(month), DUMP(month) FROM sales LIMIT 5看真实存储格式
LEFT JOIN子查询比WHERE IN更稳地处理零值月份
做「连续12个月各月营收 vs 同期去年」对比时,如果某月没数据,WHERE month IN ('2024-01', '2024-02', ...) 会直接跳过该月;但财务报表必须显示「0」而非消失。这时候得用驱动表+LEFT JOIN:
SELECT m.month, COALESCE(t1.amount, 0) AS curr_year, COALESCE(t2.amount, 0) AS last_year FROM (SELECT '2024-01' AS month UNION ALL SELECT '2024-02' ...) m LEFT JOIN ( SELECT DATE_FORMAT(sale_date, '%Y-%m') AS month, SUM(amount) AS amount FROM sales WHERE sale_date >= '2024-01-01' GROUP BY DATE_FORMAT(sale_date, '%Y-%m') ) t1 ON m.month = t1.month LEFT JOIN ( SELECT DATE_FORMAT(sale_date, '%Y-%m') AS month, SUM(amount) AS amount FROM sales WHERE sale_date BETWEEN '2023-01-01' AND '2023-12-31' GROUP BY DATE_FORMAT(sale_date, '%Y-%m') ) t2 ON m.month = DATE_FORMAT(DATE_ADD(STR_TO_DATE(t2.month, '%Y-%m'), INTERVAL 1 YEAR), '%Y-%m');
注意:Oracle 要用 ADD_MONTHS(),PostgreSQL 用 (t2.month::date + INTERVAL '1 year')::text,别硬套 MySQL 写法。
嵌套太深会让执行计划崩掉,优先用CTE拆解
当需要「月度同比、环比、滚动3月均值、预算完成率」四层嵌套时,MySQL 5.7 或 SQL Server 2016 以下版本会拒绝优化,直接走全表扫描。观察 EXPLAIN 输出里如果出现 type: ALL 和 rows 是百万级,基本就是嵌套惹的祸。
改用 CTE 把每层逻辑分开,既可读又可控执行顺序:
WITH monthly_sum AS ( SELECT DATE_FORMAT(sale_date, '%Y-%m') AS month, SUM(amount) AS amt FROM sales GROUP BY DATE_FORMAT(sale_date, '%Y-%m') ), last_year AS ( SELECT month, amt AS ly_amt FROM monthly_sum WHERE month < '2024-01' ), this_year AS ( SELECT month, amt AS ty_amt FROM monthly_sum WHERE month >= '2024-01' ) SELECT t.month, t.ty_amt, l.ly_amt, (t.ty_amt - l.ly_amt)/NULLIF(l.ly_amt, 0) AS yoy_rate FROM this_year t JOIN last_year l ON t.month = DATE_FORMAT(DATE_ADD(STR_TO_DATE(l.month, '%Y-%m'), INTERVAL 1 YEAR), '%Y-%m');
CTE 不是语法糖,它是明确的物化点——数据库会先算完 monthly_sum,再分别过滤生成 last_year 和 this_year,避免重复扫描原始表。
跨月对比最麻烦的从来不是写法,而是日期字段的实际存储形态和索引覆盖是否匹配查询条件。上线前务必用生产数据量级跑一遍 EXPLAIN ANALYZE,别信本地小表测试结果。

















