MySQL树形表查询优化

背景

在项目中可能会有这么一个场景

一个代理可以有很多子代理,然后子代理又有子代理,业务要求父代理能看到所有子代理的数据

层级数量不做控制,理论上可以有无限的层级(这里不去考虑法律法规限制)

  • 如何查询出一个父代理名下所有的子代理列表
  • 如何统计出一个父代理名下所有子代理的日订单数据

这是一个典型的树形结构,结合上述两个问题,让我们先来建立一张代理商表 与 订单表

DROP TABLE IF EXISTS `group`;
CREATE TABLE `group`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `group_name` varchar(70) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '代理商名称',
  `parent_id` bigint NOT NULL DEFAULT 0 COMMENT '上级代理id',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `oss_group_parent_id_index`(`parent_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COMMENT = '代理商表' ROW_FORMAT = DYNAMIC;

DROP TABLE IF EXISTS `order`;
CREATE TABLE `order`  (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32)  NOT NULL COMMENT '订单号',
  `agent_id` bigint NULL DEFAULT 1 COMMENT '代理商Id',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_agent_id`(`agent_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COMMENT = '订单表' ROW_FORMAT = DYNAMIC;

实战开始

查询某代理名下所有子代理

解决方案
  • 因为前面有提到业务代理可以有无限的层级,那么就不能用 inner join 的硬编码方式来查询

这种硬编码方式极不推荐,若后续业务变更将会非常的痛苦,不给自己留下技术债

  • 那么在我们java代码中是否能通过递归的方式循环查询MySQL,从而将代理商整个层级结构捞出

此方式是可以实现的,但是性能较差,也不是很推荐

  • 思考MySQL是否有提供 递归函数 来处理此场景呢?

    在MySQL8.0,引入了 with recursive,下面我们来看看相关语法

    with recursive temp as (
    select * from group p where id= 2
    union all
    select t.* from group t inner join temp on temp.id = t.parent_id
    )
    select id,group_name from temp order by temp.id

但这种方式也有个弊端,那就是有的项目上还没用上MySQL8.0,那该如何处理呢?请接着看第四个通用方案

  • 为 group 表新增一个 id_path 字段,用来记录当前代理商的层级关系

    举例:现有代理ID 1、2,他们之间关系为:2是1的下级

    那么代理商ID为1的 id_path 为0. 代理商ID为2的 id_path 为0.1.(注:id_path 也可将自身id也拼接上)

    最新表结构如下

    DROP TABLE IF EXISTS group;
    CREATE TABLE group (
    id bigint NOT NULL AUTO_INCREMENT,
    group_name varchar(70) CHARACTER SET utf8mb4 NOT NULL DEFAULT '' COMMENT '代理商名称',
    parent_id bigint NOT NULL DEFAULT 0 COMMENT '上级代理id',
    id_path varchar(190) CHARACTER SET utf8mb4 NULL DEFAULT '0.' COMMENT '从当前节点到根节点的id路径',
    PRIMARY KEY (id) USING BTREE,
    INDEX oss_group_parent_id_index(parent_id ASC) USING BTREE,
    INDEX idx_oss_group_id_path(id_path ASC) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COMMENT = '代理商表' ROW_FORMAT = DYNAMIC;

    查询代理商ID 1 名下所有子代理

    select * from oss_group where id_path like '0.1.%';

看到这里,第一个问题大家没有什么疑问了,那我们继续看第二个问题

统计某代理名下所有子代理的交易笔数

背景:group表五千多,order表千万出头,现需要查一位名下有三百多代理在某一个月的总订单数

解决方案
  • 采用 IN (subGroupList)

这种方案挺常见,在数据量少的时候可以采用,但是量一旦大了以后呢,性能变差 且 通过Mybatis拼接完SQL以后可能会超长(而通过多线程分批后聚合编码又较为复杂)

SELECT
    count( id ) orderTotal     
FROM
    order
WHERE
    agent_id IN ( #{三百多个ID} )
    AND pay_rev_time BETWEEN '2024-03-01 00:00:00' 
    AND '2024-03-31 23:59:59' ;
    
# 最终执行耗时:23.9s  24.5s  24.6s
  • 采用MySQL8.0提供的 with recursive 递归查询

这种方式本质上还是通过 agent_id in 一大堆,优点:可以避免SQL超长从而只能分批查询

WITH recursive temp AS (
    SELECT
    id 
    FROM
    oss_group p 
    WHERE
    id = #{根ID} UNION ALL
    SELECT
    t.id 
    FROM
    oss_group t
    INNER JOIN temp ON temp.id = t.parent_id 
    ) SELECT
    count( id ) orderTotal 
FROM
    order_rent
WHERE
    agent_id IN ( SELECT id FROM temp ORDER BY temp.id )
    AND pay_rev_time BETWEEN '2024-03-01 00:00:00' 
    AND '2024-03-31 23:59:59' ;
    
# 最终执行耗时:22.6s  22.9s  22.8s
  • 采用 id_path 查询

    SELECT
    count( t.id ) orderTotal
    FROM
    order_rent t
    INNER JOIN oss_group gr ON t.agent_id = gr.id
    WHERE
    ( gr.id_path LIKE concat('根代理id_path','%') )
    AND t.pay_rev_time BETWEEN '2024-03-01 00:00:00'
    AND '2024-03-31 23:59:59'

    最终执行耗时:19.9s 19.76s 19.86s

为了让测试结果凸出差异化 并且 保证公正性,故在测试中以上SQL都走的不是最佳索引(日报表定时任务优化历程),并且在每次SQL执行都间隔一段时间 且 执行了多次其他全表扫描的SQL来刷缓存

结语

根据上述两个问题引出了几种解决方案,我个人比较推崇的是:id_path 方案来处理两个问题。

因为其他方案本质都是通过 IN 来查询数据,虽然还可以进一步对 IN 方案进行优化(例如:业务上多线程分批IN,EXISTS等),但还是略逊一筹

参考文章

https://blog.csdn.net/weixin_57535055/article/details/132783354

相关推荐
颜淡慕潇22 分钟前
【K8S问题系列 |1 】Kubernetes 中 NodePort 类型的 Service 无法访问【已解决】
后端·云原生·容器·kubernetes·问题解决
尘浮生1 小时前
Java项目实战II基于Spring Boot的光影视频平台(开发文档+数据库+源码)
java·开发语言·数据库·spring boot·后端·maven·intellij-idea
尚学教辅学习资料1 小时前
基于SpringBoot的医药管理系统+LW示例参考
java·spring boot·后端·java毕业设计·医药管理
monkey_meng2 小时前
【Rust中的迭代器】
开发语言·后端·rust
余衫马3 小时前
Rust-Trait 特征编程
开发语言·后端·rust
monkey_meng3 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
瓜牛_gn4 小时前
mysql特性
数据库·mysql
paopaokaka_luck7 小时前
【360】基于springboot的志愿服务管理系统
java·spring boot·后端·spring·毕业设计
码农小旋风9 小时前
详解K8S--声明式API
后端
Peter_chq9 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端