一次统计接口踩坑总结:
为什么「其他 = 总数 − 已知分类」反而更靠谱?
场景来自真实政府项目,大屏统计接口,数据量百万级。
一、问题背景
做一个供应商统计饼图,需求很简单:
-
供应商用户总数
-
按类型分类:
- 农批市场
- 大型商超
- 食材配送
- 区域总经销
- 其他
并且要求:
前面几类加起来 = 总数
二、最开始的“直觉写法”(也是坑的开始)
一开始我在 SQL 里直接写:
COUNT(DISTINCT CASE WHEN 条件A THEN su.ID END) AS A,
COUNT(DISTINCT CASE WHEN 条件B THEN su.ID END) AS B,
COUNT(DISTINCT CASE WHEN 条件C THEN su.ID END) AS C,
COUNT(DISTINCT CASE WHEN NOT (A OR B OR C) THEN su.ID END) AS OTHER,
COUNT(DISTINCT su.ID) AS TOTAL
结果发现:
- 有时候 前面加起来 < TOTAL
- 有时候 前面加起来 > TOTAL
- 偶尔还会变
👉 一开始以为是 SQL 写错了,后来发现不是。
三、问题本质(不是 SQL 技巧,是统计逻辑)
1️⃣ 分类条件不是互斥的
SELLER_TYPE_ID是一个维度BUSINESS_CATEGORY又是一个维度
同一个用户可能同时命中多个分类。
2️⃣ SQL 的三值逻辑(TRUE / FALSE / NULL)
NOT (A OR B)
在 Oracle 里:
- 只要里面有
NULL - 结果就可能是
NULL - 而不是
TRUE
👉 一批 BUSINESS_CATEGORY = NULL 的用户
被 TOTAL 算了,但没进任何分类
3️⃣ 这不是 bug,是 SQL 的“正确行为”
但对业务来说是不可接受的。
四、最终方案:SQL 只算“确定的”,其他交给 Java
思路很简单:
SQL 负责算清楚的几类 + 总数
其他 = 总数 − 已明确分类
五、SQL(不算 other)
SELECT
COUNT(DISTINCT CASE
WHEN s.SELLER_TYPE_ID LIKE '%农批%'
THEN su.ID
END) AS farmMarketCnt,
COUNT(DISTINCT CASE
WHEN su.BUSINESS_CATEGORY = '大型商超'
THEN su.ID
END) AS supermarketCnt,
COUNT(DISTINCT CASE
WHEN su.BUSINESS_CATEGORY = '食材配送'
THEN su.ID
END) AS foodDeliveryCnt,
COUNT(DISTINCT CASE
WHEN su.BUSINESS_CATEGORY = '区域总经销'
THEN su.ID
END) AS regionalAgentCnt,
COUNT(DISTINCT su.ID) AS totalCnt
FROM SELLER_USER su
JOIN SELLER s ON s.ID = su.SELLER_ID
WHERE su.STATUS = '1';
六、Service 层兜底计算 other(关键)
@Override
public SupplierVO getSupplierStatistics() {
SupplierVO vo = mapper.getSupplierStatistics();
int total = defaultZero(vo.getTotalCnt());
int farm = defaultZero(vo.getFarmMarketCnt());
int supermarket = defaultZero(vo.getSupermarketCnt());
int food = defaultZero(vo.getFoodDeliveryCnt());
int regional = defaultZero(vo.getRegionalAgentCnt());
int other = total - farm - supermarket - food - regional;
// 防止出现负数,避免大屏事故
vo.setOtherCnt(Math.max(other, 0));
return vo;
}
private int defaultZero(Integer val) {
return val == null ? 0 : val;
}
七、为什么这个方案更稳?
✅ 好处总结
- 避开 SQL 的 NULL / NOT IN 陷阱
- 分类规则清晰
- 总数口径唯一
- 饼图一定闭合
- Java 层可兜底、不炸 UI
八、经验总结
统计类接口:
SQL 负责“事实”,
Java 负责“兜底”。
不要试图在一条 SQL 里解决所有业务逻辑,
尤其是 政府大屏 / 领导看板。