上篇文章详细说明了异构出收益日报表的方案.接下来我们来解决聚合需求多的情况下如何优化聚合SQL的问题.
在如何优雅统计订单收益(一)中已经详细说明,大概就是些日/月/年的收益统计.
建立在已经通过canal异构出收益日统计表的情况下:
单日统计(例如今日,昨日,精确日期)可以直接通过日期锁定一条数据返回.月统计也可以通过时间过滤出当月的数据进行聚合统计.年统计也通过日期区间查询出所在年份的统计实现.各项收益也可以分别进行聚合查询这样看来日统计表的异构是有价值的,至少可以解决当前的所有需求. 如果需要今日/昨日/上月/本月的收益统计,用SQL直接聚合查询,则需要分别查询今日,昨日以及跨度为整月的数据集然后通过SUM聚合实现.
//单日收益 select 分销收益,自购收益,... from t_user_income_daily where day_time='日期' and user_id=xxx //时间区间收益(周,月,年统计均可使用) 1.select sum(分销收益),sum(自购收益),sum(...) from t_user_income_daily where day_time BETWEEN '起始日期' AND '结束日期' and user_id=xxx这种写法如果接口需要返回今日/昨日/上月/本月的收益统计时,就需要查询4次SQL才可以实现.写法没问题,但是不是最优解?可以用更少的SQL查询么?
通过观察分析,今日/昨日/上月/本月统计存在共同的交集,它们都处于同一个时间区间(上月一号-本月月末),那我们可以通过SQL直接查出这两个月的数据,再通过程序聚合就可以轻松得出我们想要的数据.
补充一下收益日统计表设计
CREATE TABLE `t_user_income_daily` ( `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', `user_id` int(11) NOT NULL COMMENT '用户id', `day_time` date NOT NULL COMMENT '日期', `self_purchase_income` int(11) DEFAULT '0' COMMENT '自购收益', `member_income` int(11) DEFAULT '0' COMMENT '一级分销收益', `affiliate_member_income` int(11) DEFAULT '0' COMMENT '二级分销收益', `share_income` int(11) DEFAULT '0' COMMENT '分享收益', `effective_order_num` int(11) DEFAULT '0' COMMENT '有效订单数', `total_income` int(11) DEFAULT '0' COMMENT '总收益', `update_time` datetime DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8 COMMENT='用户收益日统计'查询出两个月的收益
select * from t_user_income_daily where day_time BETWEEN '上月一号' AND '本月月末' and user_id=xxx为了减少表的数据量,如果当日没有收益变动是不会创建当日的日统计数据的,所以这里只能查询出某时间区间用户有收益变动的收益统计数据.如果处理某一天数据为空的情况则还需要再程序中特殊处理.此处有小妙招,在数据库中生成一张时间辅助表.以天为单位,存放各种格式化后的时间数据,辅助查询详细操作可见这篇博文Mysql生成时间辅助表.有了这张表就可以进一步优化这条SQL.时间辅助表的格式如下,也可修改存储过程,加入自己个性化的时间格式.
SELECT a.DAY_ID day_time, a.MONTH_ID month_time, a.DAY_SHORT_DESC day_time_str, CASE when b.user_id is null then #{userId} else b.user_id end user_id, CASE when b.self_purchase_income is null then 0 else b.self_purchase_income end self_purchase_income, CASE when b.member_income is null then 0 else b.member_income end member_income, CASE when b.affiliate_member_income is null then 0 else b.affiliate_member_income end affiliate_member_income, CASE when b.share_income is null then 0 else b.share_income end share_income, CASE when b.effective_order_num is null then 0 else b.effective_order_num end effective_order_num, CASE when b.total_income is null then 0 else b.total_income end total_income FROM t_day_assist a LEFT JOIN t_user_income_daily b ON b.user_id = #{userId} AND a.DAY_SHORT_DESC = b.day_time WHERE STR_TO_DATE( a.DAY_SHORT_DESC, '%Y-%m-%d' ) BETWEEN #{startTime} AND #{endTime} ORDER BY a.DAY_ID DESC思路很简单,用时间辅助表左关联需要查询的收益日统计表,关联字段就是day_time时间,如果没有当天的收益数据,SQL中也会有日期为那一天但是统计数据为空的数据,用casewhen判空赋值给0,最后通过时间倒序,便可以查询出一套完整时间区间统计.
以SQL查询出的数据为基础.在程序中用stream进行聚合. 举例说明一些例子,先从简单的开始
扩展returnTotalIncomeSum函数,mapToInt支持传入ToIntFunction参数的值.
private int returnTotalIncomeSum(List<UserIncomeDailyVO> userIncomeDailyList, Predicate<UserIncomeDailyVO> predicate,ToIntFunction<UserIncomeDailyVO> function) { return userIncomeDailyList.stream() //过滤掉不符合条件的数据 .filter(predicate) //把流中对应的字段取出 .mapToInt(function) //聚合收益 .sum(); 例如: 今日分享的金额,function参数传入`UserIncomeDailyVO::getShareIncome` 今日自购和分享的金额,funciton参数传入`userIncomeDailyVO->userIncomeDailyVO.getShareIncome()+userIncomeDailyVO.getSelfPurchaseIncome()` }我们先来了解一下stream的聚合 语法糖:
list.stream().collect( Collectors.groupingBy(分组字段, Collectors.collectingAndThen(Collectors.toList(), list -> {分组后的操作}) ));流程图: 代码实例:
public ResponseResult selectIncomeDetailThisYear(int userId) { ResponseResult responseResult = ResponseResult.newSingleData(); List<UserIncomeStatisticalVO> incomeStatisticalList = new LinkedList<>(); //开始时间为今年的第一天 String startTime = DateUtil.getThisYearFirstDay.toString(); //区间最大时间为今日 String endTime = LocalDate.now().toString(); //通用SQL List<UserIncomeDailyVO> userIncomeDailyList = selectIncomeByTimeInterval(userId, startTime, endTime); //运用了stream的聚合,以月份进行分组,分组后用LinkedHashMap接收防止分组后月份顺序错乱,完毕后再把得到的每个月的收益集合流进行聚合并组装成最终的实体返回 Map<Integer, UserIncomeStatisticalVO> resultMap = userIncomeDailyList.parallelStream() .collect(Collectors.groupingBy(UserIncomeDailyVO::getMonthTime, LinkedHashMap::new, Collectors.collectingAndThen(Collectors.toList(), item -> UserIncomeStatisticalVO.builder() .affiliateMemberIncome(Tools.fenToYuan(item.stream().mapToInt(UserIncomeDailyVO::getAffiliateMemberIncome).sum())) .memberIncome(Tools.fenToYuan(item.stream().mapToInt(UserIncomeDailyVO::getMemberIncome).sum())) .effectiveOrderNum(item.stream().mapToInt(UserIncomeDailyVO::getEffectiveOrderNum).sum()) .shareIncome(Tools.fenToYuan(item.stream().mapToInt(UserIncomeDailyVO::getShareIncome).sum())) .totalIncome(Tools.fenToYuan(item.stream().mapToInt(UserIncomeDailyVO::getTotalIncome).sum())) .monthTimeStr(item.stream().map(time -> { String timeStr = time.getMonthTime().toString(); return timeStr.substring(0, timeStr.length() - 2).concat("-").concat(timeStr.substring(timeStr.length() - 2)); }).findFirst().get()) .selfPurchaseIncome(Tools.fenToYuan(item.stream().mapToInt(UserIncomeDailyVO::getSelfPurchaseIncome).sum())).build())) ); resultMap.forEach((k, v) -> incomeStatisticalList.add(v)); responseResult.setData(incomeStatisticalList); return responseResult; }本文主要介绍了在统计收益时,一些SQL的优化小技巧和JDK中stream聚合. 总结下来就是在业务量逐渐增大时,尽量避免多次大数量量表的查询聚合,可以分析思考后用尽量少的聚合查询完成,一些简单的业务也可以直接程序聚合.避免多次数据库查询的开销.在客户端返回接口需要时间完整性时,可以考虑时间辅助表进行关联,可以减少程序计算空值判空操作,优化代码的质量.
