N+1查询

今天面试的时候面试官对于一个查询场景提出了一系列问题。

现有以下表

复制代码
CREATE TABLE `sys_dept`  (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `pid` bigint(20) NULL DEFAULT NULL COMMENT '上级ID',
  `pids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '所有上级ID,用逗号分开',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名称',
  `sort` int(10) UNSIGNED NULL DEFAULT NULL COMMENT '排序',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  `create_date` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `updater` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `update_date` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_pid`(`pid`) USING BTREE,
  INDEX `idx_sort`(`sort`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '部门管理' ROW_FORMAT = DYNAMIC;
​
CREATE TABLE `sys_user`  (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码',
  `real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '姓名',
  `head_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像',
  `gender` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '性别   0:男   1:女    2:保密',
  `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '邮箱',
  `mobile` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '手机号',
  `dept_id` bigint(20) NULL DEFAULT NULL COMMENT '部门ID',
  `super_admin` tinyint(3) UNSIGNED NULL DEFAULT NULL COMMENT '超级管理员   0:否   1:是',
  `status` tinyint(4) NULL DEFAULT NULL COMMENT '状态  0:停用   1:正常',
  `creator` bigint(20) NULL DEFAULT NULL COMMENT '创建者',
  `create_date` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `updater` bigint(20) NULL DEFAULT NULL COMMENT '更新者',
  `update_date` datetime NULL DEFAULT NULL COMMENT '更新时间',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `uk_username`(`username`) USING BTREE,
  INDEX `idx_create_date`(`create_date`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '系统用户' ROW_FORMAT = DYNAMIC;
​

要查询用户列表,模糊匹配姓名,模糊匹配部门名称、准确匹配年龄。

面试官要求不能使用联合查询,不能引入其他依赖,只能使用 Mybatis-plus 查询数据。

开始我的查询逻辑是先查部门表,通过模糊查询将所有部门查出来存入 Map,key 为部门 id,value 为部门名称。

之后通过条件,查询出对应的 user 信息,然后将再按 Map 中的 key-value 为最后的返回值填入部门名称。

这时候面试官提出问题了,先查部门,如果部门有1000多万的话,内存中存这样一个 Map 可行吗。

他这么一问,成功把我唬住了,哪家公司能有1000多万部门。

不过总体还是了解了一下 N+1 问题这个知识点。

N+1 问题是当你进行一次查询,后面因为关联查询的需求,你需要再遍历查询出来的表,每次遍历的时候再进行一次相关 MySQL 查询。造成巨大的查询压力。

解决该问题的方法有:

  1. 使用 JOIN,但是这次面试官不允许使用关联查询

    复制代码
    SELECT u.id, u.username, d.name AS deptName
    FROM sys_user u
    LEFT JOIN sys_dept d ON u.dept_id = d.id;

    这样一条 sql 就可以解决该问题。

  2. 一次性批量查询,将数据暂存本地,之后直接本地访问。用空间换时间。

为什么面试官会给出一个不能使用关联查询的条件呢。因为可能有以下失效场景:

  • 笛卡儿积,关联查询其实就是笛卡尔积的应用,一张表的查询操作可能会很快,但是多张表联查就会非常慢,因为他们的数据量是n*m,所以有时候采用连接查询,还不如分成多次查询来的快。

  • 分库分表,如果系统的数据库采用的是分库分表,这个时候有些表是不能够进行连接查询,我们只能分多次查询,然后组装到一起。

  • 数据来源不一致,如果订单的数据是从第三方接口获取的,那我们自然没办法进行连表查询。

复制代码
public SysUserVo querySysUser(SysUserDto dto) {
​
    SysUserDto.Condition cond = dto.getCondition();
​
    // 用户名为空可选择返回空 或 查询全部,看系统需求,但是题干没给
    if (!StringUtils.hasText(cond.getUserName())) {
        return new SysUserVo().setTotal(0).setList(Collections.emptyList());
    }
​
    /* ---------- 1. 查询部门 ---------- */
​
    LambdaQueryWrapper<SysDept> deptQuery = Wrappers.lambdaQuery(SysDept.class)
            .select(SysDept::getId, SysDept::getName);
​
    if (StringUtils.hasText(cond.getDeptName())) {
        deptQuery.like(SysDept::getName, cond.getDeptName());
    }
​
    List<SysDept> sysDepts = sysDeptMapper.selectList(deptQuery);
    List<Long> deptIds = sysDepts.stream().map(SysDept::getId).collect(Collectors.toList());
    Map<Long, String> deptNameMap = sysDepts.stream()
            .collect(Collectors.toMap(SysDept::getId, SysDept::getName));
​
    /* ---------- 2. 查询用户 ---------- */
​
    Page<SysUser> page = new Page<>(dto.getPage().getCurrent(), dto.getPage().getSize());
    LambdaQueryWrapper<SysUser> userQuery = Wrappers.lambdaQuery(SysUser.class)
            .like(SysUser::getUsername, cond.getUserName());
​
    if (cond.getGender() != null) {
        userQuery.eq(SysUser::getGender, cond.getGender());
    }
​
    if (!deptIds.isEmpty()) {
        userQuery.in(SysUser::getDeptId, deptIds);
    }
​
    Page<SysUser> sysUserPage = sysUserMapper.selectPage(page, userQuery);
    List<SysUser> sysUsers = sysUserPage.getRecords();
​
    /* ---------- 3. 补全部门信息 ---------- */
​
    if (!sysUsers.isEmpty()) {
        List<Long> userDeptIds = sysUsers.stream().map(SysUser::getDeptId).collect(Collectors.toList());
        List<Long> missing = userDeptIds.stream()
                .filter(id -> !deptNameMap.containsKey(id))
                .collect(Collectors.toList());
​
        if (!missing.isEmpty()) {
            List<SysDept> missingDepts = sysDeptMapper.selectBatchIds(missing);
            missingDepts.forEach(d -> deptNameMap.put(d.getId(), d.getName()));
        }
    }
​
    /* ---------- 4. 组装结果 ---------- */
​
    List<SysUserVo.SysUserVoElement> list = sysUsers.stream().map(u -> {
        SysUserVo.SysUserVoElement e = new SysUserVo.SysUserVoElement();
        e.setId(u.getId());
        e.setUsername(u.getUsername());
        e.setGender(u.getGender());
        e.setRealName(u.getRealName());
        e.setDeptName(deptNameMap.get(u.getDeptId()));
        return e;
    }).collect(Collectors.toList());
​
    return new SysUserVo()
            .setList(list)
            .setTotal(sysUserPage.getTotal());
}

下面看一下我按笔试题规则构建的部分 DTO 和 VO

复制代码
@Data
@Accessors(chain = true)
public class SysUserDto {
​
    private Condition condition;
    private Page page;
​
    @Data
    public static class Condition {
        private String deptName;
        private int gender;
        private String userName;
    }
​
    @Data
    public static class Page {
        private int size;
        private int current;
        private int total;
    }
}
复制代码
@Data
@Accessors(chain = true)
public class SysUserVo {
​
    private List<SysUserVoElement> list;
    private long total;
​
    @Data
    public static class SysUserVoElement {
        private String username;
        private int gender;
        private String realName;
        private long id;
        private String deptName;
    }
}

这两个用到了静态内部类,我们知道,一般使用非静态内部类是用来表示内部类和外部类的生命周期强绑定。内部类不能单独作为一个独立类出现。而静态内部类的生命周期不受外部类作用,使用他主要的意图是强聚合,表明该静态内部类只在对应的 dto 和 vo 中才有意义。单独使用并没有任何意义。JDK 源码中也大量使用了内部类。其实还有一种类内部接口,最典型的就是 JDK Map 中的 Entry 了,表示这个 Entry 规范的方法只能被 Map 使用,也可以看作是对类功能的进一步补充。

例如:

复制代码
public class Fruits {
 
    public interface Apple {
        public String info();
    }
 
}
​
public class Test {
 
    public static void main(String[] args) {
        Fruits.Apple apple = new Fruits.Apple() {
            @Override
            public String info() {return "I'm an Apple";}
        };
        System.out.println(apple.info());
    }
}

具体还不能言说其意。

相关推荐
子夜江寒2 小时前
MySQL 表创建与数据导入导出
数据库·mysql
我要添砖java2 小时前
《JAVAEE》网络编程-什么是网络?
java·网络·java-ee
CoderYanger2 小时前
动态规划算法-01背包问题:50.分割等和子集
java·算法·leetcode·动态规划·1024程序员节
菜鸟小九2 小时前
redis基础(安装配置redis)
数据库·redis·缓存
保定公民2 小时前
达梦数据库使用cp备份集恢复报错分析与解决
数据库
Caster_Z3 小时前
WinServer安装VM虚拟机运行Linux-(失败,云服务器不支持虚拟化)
linux·运维·服务器
菜鸟233号3 小时前
力扣513 找树左下角的值 java实现
java·数据结构·算法·leetcode
Neoest4 小时前
【EasyExcel 填坑日记】“Syntax error on token )“: 一次编译错误在逃 Runtime 的灵异事件
java·eclipse·编辑器
自在极意功。4 小时前
Web开发中的分层解耦
java·microsoft·web开发·解耦