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

相关推荐
l***370915 小时前
spring 跨域CORS Filter
java·后端·spring
aiopencode15 小时前
APP 公钥与 MD5 信息在工程中的价值 一次签名排查过程带来的经验总结
后端
ServBay16 小时前
Django 6.0 发布,新增原生任务队列与 CSP 支持
后端·python·django
用户21903265273516 小时前
Spring Boot 4.0 整合 RabbitMQ 注解方式使用指南
后端
PPPPickup16 小时前
easychat---创建,获取,获取详细,退群,解散,添加与移除群组
java·开发语言·后端·maven
Dxy123931021617 小时前
MySQL连表更新
数据库·mysql
回家路上绕了弯17 小时前
大表优化实战指南:从千万到亿级数据的性能蜕变
分布式·后端
Home17 小时前
23 种设计模式--桥接(Bridge)模式(结构型模式二)
java·后端
对 酒 当 歌 人 生 几 何17 小时前
Mysql多表连接
数据库·sql·mysql
IT教程资源C17 小时前
(N_128)基于springboot,vue酒店管理系统
mysql·vue·前后端分离·酒店管理系统·springboot酒店