如果限制不能JOIN, 该如何优化SQL,缩短查询RT(Response Time)?

185 阅读4分钟

💡在阿里巴巴Java开发手册中,阿里明确指出禁止超过三个表的JOIN查询,那么在开发中,我们不采用JOIN的方式query,如何在代码逻辑实现查询,组装成前端需要的想要数据呢,怎样能最大程度的缩小RT(Response Time)呢?本篇文章主要说说我自己的做法。

image.png

以一个查询的实例来说明整个优化的过程,需求:查询该学员的课表排课记录信息(包含老师信息、课节分类信息、课节的评价条数的评价平均分等信息):

优化前:

Service接口

/**
 * 根据加课规则id、学员id查询该加课规则所绑定的课节信息
 *
 * @author: jacklin
 * @since: 2022/02/7 19:10
 **/
Result<?> queryClassSectionListByStudentIdAndRuleId(Page<SdSchoolCourseStudentScheduleVO> page, String userId, String orderCourseId, String ruleId, String classSectionCategoryId);

Service实现

@Override
public Result<?> queryClassSectionListByStudentIdAndRuleId(Page<SdSchoolCourseStudentScheduleVO> page, String userId, String orderCourseId, String ruleId, String classSectionCategoryId) {
    Result<Object> result = new Result<>();
    List<SdSchoolCourseStudentScheduleVO> courseStudentScheduleVOList = Lists.newArrayList();
    try {

        //课节分类查询下级
        List<String> classSectionCategoryChildrenIdList = new ArrayList<>();
        if (StringUtils.isNotBlank(classSectionCategoryId)) {
            String childIds = sdSchoolClassSectionCategoryService.queryTreeChildIds(classSectionCategoryId);
            //查询当前专业分类以及下级分类的批次数据
            classSectionCategoryChildrenIdList = Arrays.asList(childIds.split(","));
        }

        //根据订单课程ID、加课规则ID、学员ID查询学员该门课程的补充课信息
        courseStudentScheduleVOList = sdSchoolCourseStudentScheduleMapper.selectStudentSuppleClassSectionPageListByRuleId(page, userId, orderCourseId, ruleId, classSectionCategoryChildrenIdList);

        String currentDate = DateUtils.getDate("yyyy-MM-dd");
        if (CollectionUtils.isNotEmpty(courseStudentScheduleVOList)) {
            for (SdSchoolCourseStudentScheduleVO courseStudentScheduleVO : courseStudentScheduleVOList) {
                //查询老师信息(老师头像、老师名称)
                String teacherId = courseStudentScheduleVO.getTeacherId();
                if (StringUtils.isNotBlank(teacherId)) {
                    SdSchoolTeacher sdSchoolTeacher = sdSchoolTeacherService.getById(teacherId);
                    if (sdSchoolTeacher != null) {
                        courseStudentScheduleVO.setTeacherNo(sdSchoolTeacher.getTeacherNo());
                        courseStudentScheduleVO.setTeacherIcon(sdSchoolTeacher.getIcon());
                        courseStudentScheduleVO.setTeacherName(sdSchoolTeacher.getTeacherName());
                    }
                }

                //查询课节分类信息
                String sectionCategoryId = courseStudentScheduleVO.getClassSectionCategoryId();
                if (StringUtils.isNotBlank(sectionCategoryId)) {
                    SdSchoolClassSectionCategory classSectionCategory = sdSchoolClassSectionCategoryService.getById(sectionCategoryId);
                    if (classSectionCategory != null) {
                        courseStudentScheduleVO.setClassSectionCategoryName(classSectionCategory.getSectionCategoryName());
                    }
                }
                
                // 这里获取评价数和评分
                String classSectionId = courseStudentScheduleVO.getClassSectionId(); // 课节id
                if (oConvertUtils.isNotEmpty(classSectionId)) {
                    Map<String, Object> map = sdSchoolClassSectionValuationService.statisticsEvaluationNumAndAvgScoreByClassSectionId(classSectionId);
                    if (map != null) {
                        Integer valuationNum = map.get("valuationNum") == null ? 0 : Integer.parseInt(map.get("valuationNum").toString());
                        Float averageValuationScore = map.get("averageValuationScore") == null ? 0 : Float.parseFloat(map.get("averageValuationScore").toString());
                        courseStudentScheduleVO.setValuationNum(valuationNum);
                        courseStudentScheduleVO.setAverageValuationScore(averageValuationScore);
                    }
                }
            }
        }
        page.setRecords(courseStudentScheduleVOList);
        result.setResult(page);
    } catch (Exception e) {
        log.error("根据加课规则id、学员id查询学员课表课节信息", e);
        result.error500("根据加课规则id、学员id查询学员课表课节信息");
    }
    return result;
}

在代码中可以看到,当不采用JOIN关联查的方式,正常就是先查询出courseStudentScheduleVOList一个基础的数据List集合,通过遍历courseStudentScheduleVOList根据外键id查询对应的老师信息、课节分类信息、课节评价总数和平均分统计,这种方式是典型的用次数单表查询换取JOIN的查询瓶颈,但是当courseStudentScheduleVOList的size达到几十个量甚至几百的时候,我们的查询次数就是上百次了,即便你的单次查询0.1毫秒,几百次加起来的RT肯定也是挺大的,因此,我们遇到这种场景的query查询优化方式是:采用一次性根据idList查询方式(IN的方式查询出Map,然后再根据id匹配),可以明显缩小我们的查询响应时间。

优化后:

/**
 * 最新版本查询性能优化,采用一次性查询的当时
 *
 * @since: 2022/06/17 11:00
 * @author: jacklin
 **/
@Override
public Result<?> queryClassSectionListByStudentIdAndRuleId(Page<SdSchoolCourseStudentScheduleVO> page, String userId, String orderCourseId, String ruleId, String classSectionCategoryId) {
    Result<Object> result = new Result<>();
    try {

        //课节分类查询下级
        List<String> classSectionCategoryChildrenIdList = new ArrayList<>();
        if (StringUtils.isNotBlank(classSectionCategoryId)) {
            String childIds = sdSchoolClassSectionCategoryService.queryTreeChildIds(classSectionCategoryId);
            //查询当前专业分类以及下级分类的批次数据
            classSectionCategoryChildrenIdList = Arrays.asList(childIds.split(","));
        }
        //根据订单课程ID、加课规则ID、学员ID查询学员该门课程的补充课信息
        List<SdSchoolCourseStudentScheduleVO> courseStudentScheduleVOList = sdSchoolCourseStudentScheduleMapper.selectStudentSuppleClassSectionPageListByRuleId(page, userId, orderCourseId, ruleId, classSectionCategoryChildrenIdList);

        //查询老师信息
        List<String> teacherIds = CollectionUtil.getFieldValues(courseStudentScheduleVOList, "teacherId", String.class);
        if (CollectionUtil.isNotEmpty(teacherIds)) {
            Map<String, SdSchoolTeacherVO> teacherMap = sdSchoolTeacherService.getTeacherMap(teacherIds);
            if (teacherMap != null) {
                courseStudentScheduleVOList.stream().filter(c -> oConvertUtils.isNotEmpty(c.getTeacherId())).forEach((c) -> {
                    SdSchoolTeacherVO teacherVO = teacherMap.get(c.getTeacherId());
                    if (teacherVO != null) {
                        c.setTeacherNo(teacherVO.getTeacherNo());
                        c.setTeacherName(teacherVO.getTeacherName());
                        c.setTeacherIcon(teacherVO.getIcon());
                    }
                });
            }
        }

        //查询课节分类信息
        List<String> classSectionCategoryIds = CollectionUtil.getFieldValues(courseStudentScheduleVOList, "classSectionCategoryId", String.class);
        if (CollectionUtil.isNotEmpty(classSectionCategoryIds)) {
            Map<String, SdSchoolClassSectionCategoryVO> classSectionCategoryMap = sdSchoolClassSectionCategoryService.getSectionCategoryMap(classSectionCategoryIds);
            if (classSectionCategoryMap != null) {
                courseStudentScheduleVOList.stream().filter(c -> oConvertUtils.isNotEmpty(c.getClassSectionCategoryId())).forEach((c) -> {
                    SdSchoolClassSectionCategoryVO classSectionCategoryVO = classSectionCategoryMap.get(c.getClassSectionCategoryId());
                    if (classSectionCategoryVO != null) {
                        c.setClassSectionCategoryName(classSectionCategoryVO.getSectionCategoryName());
                    }
                });
            }
        }

        //查询课节的评分信息
        List<String> classSectionIds = CollectionUtil.getFieldValues(courseStudentScheduleVOList, "classSectionId", String.class);
        if (CollectionUtil.isNotEmpty(classSectionIds)) {
            Map<String, SdSchoolClassSectionEvaluateDetailsVO> evaluationNumAndAvgScoreMap = sdSchoolClassSectionValuationService.getClassSectionEvaluationNumAndAvgScoreMap(classSectionIds);
            if (evaluationNumAndAvgScoreMap != null) {
                courseStudentScheduleVOList.stream().filter(c -> oConvertUtils.isNotEmpty(c.getClassSectionId()))
                        .forEach((c) -> {
                            SdSchoolClassSectionEvaluateDetailsVO evaluateDetailsVO = evaluationNumAndAvgScoreMap.get("classSectionId");
                            if (evaluateDetailsVO != null) {
                                //课节评价次数
                                Integer valuationNum = oConvertUtils.isEmpty(evaluateDetailsVO.getValuationNum()) ? 0 : evaluateDetailsVO.getValuationNum();
                                //课节评价评价分
                                Float averageValuationScore = oConvertUtils.isEmpty(evaluateDetailsVO.getAverageValuationScore()) ? 0 : evaluateDetailsVO.getAverageValuationScore();
                                c.setValuationNum(valuationNum);
                                c.setAverageValuationScore(averageValuationScore);
                            }else {
                                c.setValuationNum(0);
                                c.setAverageValuationScore(0f);
                            }
                        });
            }
        }
        
        page.setRecords(courseStudentScheduleVOList);
        result.setResult(page);

    } catch (Exception e) {
        log.error("补充课-查询加课规则课节信息异常", e);
        result.error500("补充课-查询加课规则课节信息异常");
    }
    return result;
}

优化思路: 先通过CollectionUtil.getFieldValues()获取courseStudentScheduleVOList对应的ids信息,然后通过子查询查询出返回一个Map<String, xxxVO>的Map集合,通过代码根据id匹配逻辑,设置对应的字段数据,优化方法主要看:

//查询老师信息,先获取courseStudentScheduleVOList中的索引teacherIds信息
List<String> teacherIds = CollectionUtil.getFieldValues(courseStudentScheduleVOList, "teacherId", String.class);
if (CollectionUtil.isNotEmpty(teacherIds)) {
    //通过一次性查询出老师的信息
    Map<String, SdSchoolTeacherVO> teacherMap = sdSchoolTeacherService.getTeacherMap(teacherIds);
    if (teacherMap != null) {
        courseStudentScheduleVOList.stream().filter(c -> oConvertUtils.isNotEmpty(c.getTeacherId())).forEach((c) -> {
            SdSchoolTeacherVO teacherVO = teacherMap.get(c.getTeacherId());
            if (teacherVO != null) {
                //设置老师属性信息
                c.setTeacherNo(teacherVO.getTeacherNo());
                c.setTeacherName(teacherVO.getTeacherName());
                c.setTeacherIcon(teacherVO.getIcon());
            }
        });
    }
}

getTeacherMap

@Override
public Map<String, SdSchoolTeacherVO> getTeacherMap(List<String> ids) {
    List<SdSchoolTeacherVO> teacherVOList = baseMapper.getTeacherMap(ids);
    Map<String, SdSchoolTeacherVO> map = new HashMap<>();
    for (SdSchoolTeacherVO teacherVO : teacherVOList) {
        map.put(teacherVO.getId(), teacherVO);
    }
    return map;
}

经过10次的查询统计,优化前的RT信息如下:

优化前查询响应时间(单位毫秒)
第一次415ms
第二次358ms
第三次343ms
第四次395ms
第五次359ms
第六次330ms
第七次350ms
第八次360ms
第九次335ms
第十次365ms

平均响应时间:361ms

优化后的RT信息如下:

优化后查询响应时间(单位毫秒)
第一次238ms
第二次255ms
第三次237ms
第四次235ms
第五次220ms
第六次234ms
第七次245ms
第八次230ms
第九次247ms
第十次244ms

平均响应时间:238ms

🌈可以明显看出,优化后查询RT比优化后的RT小了大约 120 毫秒,查询效率明显高于优化前的,这个数据只是一个大体的统计,不同的场景和不同的开发环境肯定存在数据的偏差,而且RT也会受courseStudentScheduleVOList大小的影响,仅供查考,因此类似遇到这样的场景可以采用一次性查询的方式来减少我们的响应时间!