KPI查询常见错误包括:GROUP BY后SELECT列必须为分组列或聚合列;COUNT(*)与COUNT(字段)对NULL处理不同;窗口函数如LAG可安全实现同比/环比;WHERE过滤行级数据,HAVING过滤分组结果。

GROUP BY 后 SELECT 列必须是聚合列或分组列
这是写 KPI 查询时最常报错的地方,比如 SELECT user_id, COUNT(*) FROM orders GROUP BY status 会直接报错——user_id 既没参与 GROUP BY,也没被聚合函数包裹。数据库不知道该取哪一行的 user_id。
正确做法只有两种:
- 把
user_id加进GROUP BY(适合分析每个用户在各状态下的订单数) - 用聚合函数包裹,比如
MAX(user_id)或ANY_VALUE(user_id)(MySQL 5.7+),但得清楚语义:你并不真需要具体哪个user_id,只是占位或辅助去重
注意 ANY_VALUE() 不是标准 SQL,PostgreSQL 用 MIN()/MAX() 替代更稳妥;SQL Server 要靠子查询或窗口函数绕开。
COUNT(*) vs COUNT(字段) 的语义差异直接影响 KPI 分母
算“下单用户数”还是“支付成功用户数”,差一个 IS NOT NULL 就可能翻车。比如 COUNT(user_id) 会自动跳过 NULL 值,而 COUNT(*) 统计所有行。
常见陷阱:
- 用
COUNT(create_time)算日活,但某些测试订单create_time是NULL,结果漏掉这批数据 - 想统计“有手机号的用户数”,写了
COUNT(phone),结果发现部分老数据存的是空字符串'',它不等于NULL,照样被计入
更安全的写法是显式过滤:COUNT(CASE WHEN phone != '' AND phone IS NOT NULL THEN 1 END)。
用窗口函数补全“同比/环比”这类跨行指标
单纯 GROUP BY 只能算静态聚合,但 KPI 常要“本月销售额 / 上月销售额”。这时候硬写两个子查询 JOIN 很容易漏月、对不齐维度。
推荐直接上窗口函数:
SELECT ym, SUM(amount) AS month_amount, LAG(SUM(amount), 1) OVER (ORDER BY ym) AS last_month_amount, ROUND(SUM(amount) / NULLIF(LAG(SUM(amount), 1) OVER (ORDER BY ym), 0), 2) AS mom_ratio FROM orders GROUP BY ym
关键点:
-
LAG()的第二个参数是偏移行数,写1表示上一行;不写默认也是 1 -
NULLIF(..., 0)防止除零错误,比CASE WHEN ... THEN ... ELSE NULL END更简洁 - 如果按产品线再细分同比,
OVER (PARTITION BY product_line ORDER BY ym)就能分组独立计算
WHERE 和 HAVING 混用导致 KPI 过滤逻辑错乱
想看“客单价 > 200 的城市”,有人写 HAVING AVG(order_amount) > 200,却忘了先用 WHERE 排除测试订单——结果测试单拉低了均值,真实高价值城市被过滤掉了。
记住执行顺序:WHERE → GROUP BY → HAVING。所以:
-
WHERE用于行级过滤(如status = 'paid'、created_at >= '2024-01-01') -
HAVING只能过滤分组后的结果(如COUNT(*) > 10、AVG(price) > 50) - 别在
HAVING里写WHERE能做的事,性能差还易读性低
复杂业务场景下,HAVING 的条件最好和 SELECT 中的聚合列保持命名一致,避免后期维护时搞混哪条是原始过滤、哪条是聚合后筛。
聚合函数本身不难,难的是每一步过滤、分组、排序的边界在哪——KPI 出问题,八成卡在 WHERE/HAVING 分不清,或者 COUNT 没想清 NULL 怎么算。

















