热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

优雅的统计订单收益(二)

mysql教程栏目今天介绍如何优雅的统计订单收益,减少烦恼。
mysql教程栏目今天介绍如何优雅的统计订单收益,减少烦恼。

引言

上篇文章详细说明了异构出收益日报表的方案.接下来我们来解决聚合需求多的情况下如何优化聚合SQL的问题.

需求

在如何优雅统计订单收益(一)中已经详细说明,大概就是些日/月/年的收益统计.

思考

目标

  • 尽量减少聚合SQL的查询次数
  • 给前端方便展示的API数据,表现在如果某一天的数据为空值时,后端处理成收益为0数据给前端
  • 方法函数尽量通用提高代码质量

思路

初步实现

建立在已经通过canal异构出收益日统计表的情况下:

  1. 单日统计(例如今日,昨日,精确日期)可以直接通过日期锁定一条数据返回.
  2. 月统计也可以通过时间过滤出当月的数据进行聚合统计.
  3. 年统计也通过日期区间查询出所在年份的统计实现.
  4. 各项收益也可以分别进行聚合查询

这样看来日统计表的异构是有价值的,至少可以解决当前的所有需求. 如果需要今日/昨日/上月/本月的收益统计,用SQL直接聚合查询,则需要分别查询今日,昨日以及跨度为整月的数据集然后通过SUM聚合实现.

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='用户收益日统计'

这种写法如果接口需要返回今日/昨日/上月/本月的收益统计时,就需要查询4次SQL才可以实现.写法没问题,但是不是最优解?可以用更少的SQL查询么?

观察

通过观察分析,今日/昨日/上月/本月统计存在共同的交集,它们都处于同一个时间区间(上月一号-本月月末),那我们可以通过SQL直接查出这两个月的数据,再通过程序聚合就可以轻松得出我们想要的数据.

优化实现

补充一下收益日统计表设计

select * from t_user_income_daily where day_time BETWEEN '上月一号' AND '本月月末' and user_id=xxx

查询出两个月的收益

select * from t_user_income

为了减少表的数据量,如果当日没有收益变动是不会创建当日的日统计数据的,所以这里只能查询出某时间区间用户有收益变动的收益统计数据.如果处理某一天数据为空的情况则还需要再程序中特殊处理.此处有小妙招,在数据库中生成一张时间辅助表.以天为单位,存放各种格式化后的时间数据,辅助查询详细操作可见这篇博文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进行聚合. 举例说明一些例子,先从简单的开始

常用静态方法封装

/**
     * @description: 本月的第一天
     * @author: chenyunxuan
     */
    public static LocalDate getThisMonthFirstDay() {
        return LocalDate.of(LocalDate.now().getYear(), LocalDate.now().getMonthValue(), 1);
    }

    /**
     * @description: 本月的最后一天
     * @author: chenyunxuan
     */
    public static LocalDate getThisMonthLastDay() {
        return LocalDate.now().with(TemporalAdjusters.lastDayOfMonth());
    }

    /**
     * @description: 上个月第一天
     * @author: chenyunxuan
     */
    public static LocalDate getLastMonthFirstDay() {
        return LocalDate.of(LocalDate.now().getYear(), LocalDate.now().getMonthValue() - 1, 1);
    }

    /**
     * @description: 上个月的最后一天
     * @author: chenyunxuan
     */
    public static LocalDate getLastMonthLastDay() {
        return getLastMonthFirstDay().with(TemporalAdjusters.lastDayOfMonth());
    }
    
    /**
     * @description: 今年的第一天
     * @author: chenyunxuan
     */
    public static LocalDate getThisYearFirstDay() {
        return LocalDate.of(LocalDate.now().getYear(), 1, 1);
    }
    
    /**
     * @description: 分转元,不支持负数
     * @author: chenyunxuan
     */
    public static String fenToYuan(Integer money) {
        if (mOney== null) {
            return "0.00";
        }
        String s = money.toString();
        int len = s.length();
        StringBuilder sb = new StringBuilder();
        if (s != null && s.trim().length() > 0) {
            if (len == 1) {
                sb.append("0.0").append(s);
            } else if (len == 2) {
                sb.append("0.").append(s);
            } else {
                sb.append(s.substring(0, len - 2)).append(".").append(s.substring(len - 2));
            }
        } else {
            sb.append("0.00");
        }
        return sb.toString();
    }

指定月份收益列表(按时间倒序)

public ResponseResult selectIncomeDetailThisMonth(int userId, Integer year, Integer month) {
        ResponseResult respOnseResult= ResponseResult.newSingleData();
        String startTime;
        String endTime;
        //不是指定月份
        if (null == year && null == month) {
            //如果时间为当月则只显示今日到当月一号
            startTime = DateUtil.getThisMonthFirstDay().toString();
            endTime = LocalDate.now().toString();
        } else {
            //如果是指定年份月份,用LocalDate.of构建出需要查询的月份的一号日期和最后一天的日期
            LocalDate localDate = LocalDate.of(year, month, 1);
            startTime = localDate.toString();
            endTime = localDate.with(TemporalAdjusters.lastDayOfMonth()).toString();
        }
        //查询用通用的SQL传入用户id和开始结束时间
        List userIncomeDailyList = selectIncomeByTimeInterval(userId, startTime, endTime);
        /给前端的数据需要把数据库存的分转为字符串,如果没有相关需求可跳过直接返回
        List userIncomeStatisticalList = userIncomeDailyList.stream()
                .map(item -> UserIncomeStatisticalVO.builder()
                        .affiliateMemberIncome(Tools.fenToYuan(item.getAffiliateMemberIncome()))
                        .memberIncome(Tools.fenToYuan(item.getMemberIncome()))
                        .effectiveOrderNum(item.getEffectiveOrderNum())
                        .shareIncome(Tools.fenToYuan(item.getShareIncome()))
                        .totalIncome(Tools.fenToYuan(item.getTotalIncome()))
                        .dayTimeStr(item.getDayTimeStr())
                        .selfPurchaseIncome(Tools.fenToYuan(item.getSelfPurchaseIncome())).build()).collect(Collectors.toList());
        responseResult.setData(userIncomeStatisticalList);
        return responseResult;
    }

今日/昨日/上月/本月收益

    public Map getPersonalIncomeMap(int userId) {
        Map resultMap = new HashMap<>(4);
        LocalDate localDate = LocalDate.now();
        //取出上个月第一天和这个月最后一天
        String startTime = DateUtil.getLastMonthFirstDay().toString();
        String endTime = DateUtil.getThisMonthLastDay().toString();
        //这条查询就是上面优化过的SQL.传入开始和结束时间获得这个时间区间用户的收益日统计数据
        List userIncomeDailyList = selectIncomeByTimeInterval(userId, startTime, endTime);
        //因为这里需要取的都是总收益,所以封装了returnTotalIncomeSum方法,用于传入条件返回总收益聚合
        //第二个参数就是筛选条件,只保留符合条件的部分.(此处都是用的LocalDate的API)
        int today = returnTotalIncomeSum(userIncomeDailyList, n -> localDate.toString().equals(n.getDayTimeStr()));
        int yesterday = returnTotalIncomeSum(userIncomeDailyList, n -> localDate.minusDays(1).toString().equals(n.getDayTimeStr()));
        int thisMOnth= returnTotalIncomeSum(userIncomeDailyList, n ->
                n.getDayTime() >= Integer.parseInt(DateUtil.getThisMonthFirstDay().toString().replace("-", ""))
                        && n.getDayTime() <= Integer.parseInt(DateUtil.getThisMonthLastDay().toString().replace("-", "")));
        int lastMOnth= returnTotalIncomeSum(userIncomeDailyList, n ->
                n.getDayTime() >= Integer.parseInt(DateUtil.getLastMonthFirstDay().toString().replace("-", ""))
                        && n.getDayTime() <= Integer.parseInt(DateUtil.getLastMonthLastDay().toString().replace("-", "")));
        //因为客户端显示的是两位小数的字符串,所以需要用Tools.fenToYuan把数值金额转换成字符串
        resultMap.put("today", Tools.fenToYuan(today));
        resultMap.put("yesterday", Tools.fenToYuan(yesterday));
        resultMap.put("thisMonth", Tools.fenToYuan(thisMonth));
        resultMap.put("lastMonth", Tools.fenToYuan(lastMonth));
        return resultMap;
    }
    
    //传入收益集合以及过滤接口,返回对应集合数据,Predicate接口是返回一个boolean类型的值,用于筛选
    private int returnTotalIncomeSum(List userIncomeDailyList, Predicate predicate) {
        return userIncomeDailyList.stream()
                //过滤掉不符合条件的数据
                .filter(predicate)
                //把流中对应的总收益字段取出
                .mapToInt(UserIncomeDailyVO::getTotalIncome)
                //聚合总收益
                .sum();
    }

扩展returnTotalIncomeSum函数,mapToInt支持传入ToIntFunction参数的值.

     private int returnTotalIncomeSum(List userIncomeDailyList, Predicate predicate,ToIntFunction 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 incomeStatisticalList = new LinkedList<>();
        //开始时间为今年的第一天
        String startTime = DateUtil.getThisYearFirstDay.toString();
        //区间最大时间为今日
        String endTime = LocalDate.now().toString();
        //通用SQL
        List userIncomeDailyList = selectIncomeByTimeInterval(userId, startTime, endTime);
        //运用了stream的聚合,以月份进行分组,分组后用LinkedHashMap接收防止分组后月份顺序错乱,完毕后再把得到的每个月的收益集合流进行聚合并组装成最终的实体返回
        Map 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聚合. 总结下来就是在业务量逐渐增大时,尽量避免多次大数量量表的查询聚合,可以分析思考后用尽量少的聚合查询完成,一些简单的业务也可以直接程序聚合.避免多次数据库查询的开销.在客户端返回接口需要时间完整性时,可以考虑时间辅助表进行关联,可以减少程序计算空值判空操作,优化代码的质量.

相关免费学习推荐:mysql教程(视 频)

以上就是优雅的统计订单收益(二)的详细内容,更多请关注 第一PHP社区 其它相关文章!


推荐阅读
  • 本文详细解析了MySQL中常见的几种错误,并提供了具体的解决方法,帮助开发者快速定位和解决问题。 ... [详细]
  • 本文探讨了如何在PHP与MySQL环境中实现高效的分页查询,包括基本的分页实现、性能优化技巧以及高级的分页策略。 ... [详细]
  • 本文介绍了如何通过安装 sqlacodegen 和 pymysql 来根据现有的 MySQL 数据库自动生成 ORM 的模型文件(model.py)。此方法适用于需要快速搭建项目模型层的情况。 ... [详细]
  • 搭建个人博客:WordPress安装详解
    计划建立个人博客来分享生活与工作的见解和经验,选择WordPress是因为它专为博客设计,功能强大且易于使用。 ... [详细]
  • 在Android应用开发过程中,开发者经常遇到诸如CPU使用率过高、内存泄漏等问题。本文将介绍几种常用的命令及其应用场景,帮助开发者有效定位并解决问题。 ... [详细]
  • H5技术实现经典游戏《贪吃蛇》
    本文将分享一个使用HTML5技术实现的经典小游戏——《贪吃蛇》。通过H5技术,我们将探讨如何构建这款游戏的两种主要玩法:积分闯关和无尽模式。 ... [详细]
  • 软件测试行业深度解析:迈向高薪的必经之路
    本文深入探讨了软件测试行业的发展现状及未来趋势,旨在帮助有志于在该领域取得高薪的技术人员明确职业方向和发展路径。 ... [详细]
  • 如何在Django框架中实现对象关系映射(ORM)
    本文介绍了Django框架中对象关系映射(ORM)的实现方式,通过ORM,开发者可以通过定义模型类来间接操作数据库表,从而简化数据库操作流程,提高开发效率。 ... [详细]
  • 二维码的实现与应用
    本文介绍了二维码的基本概念、分类及其优缺点,并详细描述了如何使用Java编程语言结合第三方库(如ZXing和qrcode.jar)来实现二维码的生成与解析。 ... [详细]
  • CRZ.im:一款极简的网址缩短服务及其安装指南
    本文介绍了一款名为CRZ.im的极简网址缩短服务,该服务采用PHP和SQLite开发,体积小巧,约10KB。本文还提供了详细的安装步骤,包括环境配置、域名解析及Nginx伪静态设置。 ... [详细]
  • 本文详细介绍了PostgreSQL与MySQL在SQL语法上的主要区别,包括如何使用COALESCE替代IFNULL、金额格式化的方法、别名处理以及日期处理等关键点。 ... [详细]
  • 从CodeIgniter中提取图像处理组件
    本指南旨在帮助开发者在未使用CodeIgniter框架的情况下,如何独立使用其强大的图像处理功能,包括图像尺寸调整、创建缩略图、裁剪、旋转及添加水印等。 ... [详细]
  • HTML:  将文件拖拽到此区域 ... [详细]
  • 在处理大数据量的SQL分页查询时,通常需要执行两次查询来分别获取数据和总记录数。本文介绍了一种优化方法,通过单次查询同时返回分页数据和总记录数,从而提高查询效率。 ... [详细]
  • Bootstrap Paginator 分页插件详解与应用
    本文深入探讨了Bootstrap Paginator这款流行的JavaScript分页插件,提供了详细的使用指南和示例代码,旨在帮助开发者更好地理解和利用该工具进行高效的数据展示。 ... [详细]
author-avatar
郭楠v
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有