前言

最近在做的业务场景涉及到了数据库的递归查询。我们公司用的 Oracle ,众所周知,Oracle 自带有递归查询的功能,所以实现起来特别简单。

但是,我记得 MySQL 是没有递归查询功能的,那 MySQL 中应该怎么实现呢?

于是,就有了这篇文章。

文章主要知识点:

  • Oracle 递归查询, start with connect by prior 用法
  • find_in_set 函数
  • concat,concat_ws,group_concat 函数
  • MySQL 自定义函数
  • 手动实现 MySQL 递归查询

Oracle 递归查询

在 Oracle 中是通过 start with connect by prior 语法来实现递归查询的。

按照 prior 关键字在子节点端还是父节点端,以及是否包含当前查询的节点,共分为四种情况。

prior 在子节点端(向下递归)

第一种情况: start with 子节点id = ' 查询节点 ' connect by prior 子节点id = 父节点id

select * from dept start with id='1001' connet by prior id=pid;

这里,按照条件 id='1001' 对当前节点以及它的子节点递归查询。查询结果包含自己及所有子节点。

第二种情况: start with 父节点id= ' 查询节点 ' connect by prior 子节点id = 父节点 id

select * from dept start with pid='1001' connect by prior id=pid;

这里,按照条件 pid='1001' 对当前节点的所有子节点递归查询。查询结果只包含它的所有子节点,不包含自己

其实想一想也对,因为开始条件是以父节点为根节点,且向下递归,自然不包含当前节点。

prior 在父节点端(向上递归)

第三种情况: start with 子节点id= ' 查询节点 ' connect by prior 父节点id = 子节点id

select * from dept start with id='1001' connect by prior pid=id;

这里按照条件 id='1001' ,对当前节点及其父节点递归查询。查询结果包括自己及其所有父节点。

第四种情况: start with 父节点id= ' 查询节点 ' connect by prior 父节点id = 子节点id

select * from dept start with pid='1001' connect by prior pid=id;

这里按照条件 pid='1001',对当前节点的第一代子节点以及它的父节点递归查询。查询结果包括自己的第一代子节点以及所有父节点。(包括自己

其实这种情况也好理解,因为查询开始条件是以 父节点为根节点,且向上递归,自然需要把当前父节点的第一层子节点包括在内。

以上四种情况初看可能会让人迷惑,容易记混乱,其实不然。

我们只需要记住 prior 的位置在子节点端,就向下递归,在父节点端就向上递归。

  • 开始条件若是子节点的话,自然包括它本身的节点。
  • 开始条件若是父节点的话,则向下递归时,自然不包括当前节点。而向上递归,需要包括当前节点及其第一代子节点。

MySQL 递归查询

可以看到,Oracle 实现递归查询非常的方便。但是,在 MySQL 中并没有帮我们处理,因此需要我们自己手动实现递归查询。

为了方便,我们创建一个部门表,并插入几条可以形成递归关系的数据。

DROP TABLE IF EXISTS `dept`;
CREATE TABLE `dept` (
`id` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`pid` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1000', '总公司', NULL);
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1001', '北京分公司', '1000');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1002', '上海分公司', '1000');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1003', '北京研发部', '1001');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1004', '北京财务部', '1001');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1005', '北京市场部', '1001');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1006', '北京研发一部', '1003');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1007', '北京研发二部', '1003');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1008', '北京研发一部一小组', '1006');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1009', '北京研发一部二小组', '1006');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1010', '北京研发二部一小组', '1007');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1011', '北京研发二部二小组', '1007');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1012', '北京市场一部', '1005');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1013', '上海研发部', '1002');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1014', '上海研发一部', '1013');
INSERT INTO `dept`(`id`, `name`, `pid`) VALUES ('1015', '上海研发二部', '1013');

没错,刚才 Oracle 递归,就是用的这张表。

另外,在这之前,我们需要复习一下几个 MYSQL中的函数,后续会用到。

find_in_set 函数

函数语法:find_in_set(str,strlist)

str 代表要查询的字符串 , strlist 是一个以逗号分隔的字符串,如 ('a,b,c')。

此函数用于查找 str 字符串在字符串 strlist 中的位置,返回结果为 1 ~ n 。若没有找到,则返回0。

举个栗子:

select FIND_IN_SET('b','a,b,c,d');

结果返回 2 。因为 b 所在位置为第二个子串位置。

此外,在对表数据进行查询时,它还有一种用法,如下:

select * from dept where FIND_IN_SET(id,'1000,1001,1002');

结果返回所有 id 在 strlist 中的记录,即 id = '1000' ,id = '1001' ,id = '1002' 三条记录。

看到这,对于我们要解决的递归查询,不知道你有什么启发没。

以向下递归查询所有子节点为例。我想,是不是可以找到一个包含当前节点和所有子节点的以逗号拼接的字符串 strlist,传进 find_in_set 函数。就可以查询出所有需要的递归数据了。

那么,现在问题就转化为怎样构造这样的一个字符串 strlist 。

这就需要用到以下字符串拼接函数了。

concat,concat_ws,group_concat 函数

一、字符串拼接函数中,最基本的就是 concat 了。它用于连接N个字符串,如,

select CONCAT('M','Y','S','Q','L') from dual;

结果为 'MYSQL' 字符串。

二、concat 是以逗号为默认的分隔符,而 concat_ws 则可以指定分隔符,第一个参数传入分隔符,如以下划线分隔。

三、group_concat 函数更强大,可以分组的同时,把字段以特定分隔符拼接成字符串。

用法:group_concat( [distinct] 要连接的字段 [order by 排序字段 asc/desc ] [separator '分隔符'] )

可以看到有可选参数,可以对将要拼接的字段值去重,也可以排序,指定分隔符。若没有指定,默认以逗号分隔。

对于 dept 表,我们可以把表中的所有 id 以逗号拼接。(这里没有用到 group by 分组字段,则可以认为只有一组)

MySQL 自定义函数,实现递归查询

可以发现以上已经把字符串拼接的问题也解决了。那么,问题就变成怎样构造有递归关系的字符串了。

我们可以自定义一个函数,通过传入根节点id,找到它的所有子节点。

以向下递归为例。 (讲解自定义函数写法的同时,讲解递归逻辑)

delimiter $$
drop function if exists get_child_list$$
create function get_child_list(in_id varchar(10)) returns varchar(1000)
begin
declare ids varchar(1000) default '';
declare tempids varchar(1000); set tempids = in_id;
while tempids is not null do
set ids = CONCAT_WS(',',ids,tempids);
select GROUP_CONCAT(id) into tempids from dept where FIND_IN_SET(pid,tempids)>0;
end while;
return ids;
end
$$
delimiter ;

(1) delimiter $$ ,用于定义结束符。我们知道 MySQL 默认的结束符为分号,表明指令结束并执行。但是在函数体中,有时我们希望遇到分号不结束,因此需要暂时把结束符改为一个随意的其他值。我这里设置为 $$,意思是遇到 $$ 才结束,并执行当前语句。

(2)drop function if exists get_child_list$$ 。若函数 get_child_list 已经存在了,则先删除它。注意这里需要用 当前自定义的结束符 $$ 来结束并执行语句。 因为,这里需要和下边的函数体单独区分开来。

(3)create function get_child_list 创建函数。并且参数传入一个根节点的子节点id,需要注意一定要注明参数的类型和长度,如这里是 varchar(10)。returns varchar(1000) 用来定义返回值参数类型。

(4)begin 和 end 中间包围的就是函数体。用来写具体的逻辑。

(5)declare 用来声明变量,并且可以用 default 设置默认值。

这里定义的 ids 即作为整个函数的返回值,是用来拼接成最终我们需要的以逗号分隔的递归串的。

而 tempids 是为了记录下边 while 循环中临时生成的所有子节点以逗号拼接成的字符串。

(6) set 用来给变量赋值。此处把传进来的根节点赋值给 tempids 。

(7) while do ... end while; 循环语句,循环逻辑包含在内。注意,end while 末尾需要加上分号。

循环体内,先用 CONCAT_WS 函数把最终结果 ids 和 临时生成的 tempids 用逗号拼接起来。

然后以 FIND_IN_SET(pid,tempids)>0 为条件,遍历在 tempids 中的所有 pid ,寻找以此为父节点的所有子节点 id ,并且通过 GROUP_CONCAT(id) into tempids 把这些子节点 id 都用逗号拼接起来,并覆盖更新 tempids 。

等下次循环进来时,就会再次拼接 ids ,并再次查找所有子节点的所有子节点。循环往复,一层一层的向下递归遍历子节点。直到判断 tempids 为空,说明所有子节点都已经遍历完了,就结束整个循环。

这里,用 '1000' 来举例,即是:(参看图1的表数据关系)

第一次循环:
tempids=1000 ids=1000 tempids=1001,1002 (1000的所有子节点)
第二次循环:
tempids=1001,1002 ids=1000,1001,1002 tempids=1003,1004,1005,1013 (1001和1002的所有子节点)
第三次循环:
tempids=1003,1004,1005,1013
ids=1000,1001,1002,1003,1004,1005,1013
tempids=1003和1004和1005及1013的所有子节点
...
最后一次循环,因找不到子节点,tempids=null,就结束循环。

(8)return ids; 用于把 ids 作为函数返回值返回。

(9)函数体结束以后,记得用结束符 $$ 来结束整个逻辑,并执行。

(10)最后别忘了,把结束符重新设置为默认的结束符分号 。

自定义函数做好之后,我们就可以用它来递归查询我们需要的数据了。如,我查询北京研发部的所有子节点。

以上是向下递归查询所有子节点的,并且包括了当前节点,也可以修改逻辑为不包含当前节点,我就不演示了。

手动实现递归查询(向上递归)

相对于向下递归来说,向上递归比较简单。

因为向下递归时,每一层递归一个父节点都对应多个子节点。

而向上递归时,每一层递归一个子节点只对应一个父节点,关系比较单一。

同样的,我们可以定义一个函数 get_parent_list 来获取根节点的所有父节点。

delimiter $$
drop function if exists get_parent_list$$
create function get_parent_list(in_id varchar(10)) returns varchar(1000)
begin
declare ids varchar(1000);
declare tempid varchar(10); set tempid = in_id;
while tempid is not null do
set ids = CONCAT_WS(',',ids,tempid);
select pid into tempid from dept where id=tempid;
end while;
return ids;
end
$$
delimiter ;

查找北京研发二部一小组,以及它的递归父节点,如下:

注意事项

我们用到了 group_concat 函数来拼接字符串。但是,需要注意它是有长度限制的,默认为 1024 字节。可以通过 show variables like "group_concat_max_len"; 来查看。

注意,单位是字节,不是字符。在 MySQL 中,单个字母占1个字节,而我们平时用的 utf-8下,一个汉字占3个字节。

这个对于递归查询还是非常致命的。因为一般递归的话,关系层级都比较深,很有可能超过最大长度。(尽管一般拼接的都是数字字符串,即单字节)

所以,我们有两种方法解决这个问题:

  1. 修改 MySQL 配置文件 my.cnf ,增加 group_concat_max_len = 102400 #你要的最大长度

  2. 执行以下任意一个语句。SET GLOBAL group_concat_max_len=102400; 或者 SET SESSION group_concat_max_len=102400;

    他们的区别在于,global是全局的,任意打开一个新的会话都会生效,但是注意,已经打开的当前会话并不会生效。而 session 是只会在当前会话生效,其他会话不生效。

    共同点是,它们都会在 MySQL 重启之后失效,以配置文件中的配置为准。所以,建议直接修改配置文件。102400 的长度一般也够用了。假设一个id的长度为10个字节,也能拼上一万个id了。

除此之外,使用 group_concat 函数还有一个限制,就是不能同时使用 limit 。如,

本来只想查5条数据来拼接,现在不生效了。

不过,如果需要的话,可以通过子查询来实现,

同事问我MySQL怎么递归查询,我懵逼了的更多相关文章

  1. 之前同事问到的一道python题目

    Python面试题 之前同事问了一道Python题目如下,暂时归类为面试题 题目:把类似'123.456'的字符串转换成浮点型数据 方法一: >>> print '{:.3f}'.f ...

  2. 互联网公司面试必问的mysql题目(下)

    这是mysql系列的下篇,上篇文章地址我附在文末. 什么是数据库索引?索引有哪几种类型?什么是最左前缀原则?索引算法有哪些?有什么区别? 索引是对数据库表中一列或多列的值进行排序的一种结构.一个非常恰 ...

  3. 面试必问的MySQL锁与事务隔离级别

    之前多篇文章从mysql的底层结构分析.sql语句的分析器以及sql从优化底层分析, 还有工作中常用的sql优化小知识点.面试各大互联网公司必问的mysql锁和事务隔离级别,这篇文章给你打神助攻,一飞 ...

  4. 企业面试题|最常问的MySQL面试题集合(二)

    MySQL的关联查询语句 六种关联查询 交叉连接(CROSS JOIN) 内连接(INNER JOIN) 外连接(LEFT JOIN/RIGHT JOIN) 联合查询(UNION与UNION ALL) ...

  5. 企业面试题|最常问的MySQL面试题集合(一)

    问题1:char.varchar的区别是什么?varchar是变长而char的长度是固定的.如果你的内容是固定大小的,你会得到更好的性能. 问题2: TRUNCATE和DELETE的区别是什么?DEL ...

  6. 常问的MySQL面试题集合

    关注「开源Linux」,选择"设为星标" 回复「学习」,有我为您特别筛选的学习资料~ 除了基础题部分,本文还收集整理的MySQL面试题还包括如下知识点或题型: MySQL高性能索引 ...

  7. 一句SQL实现MYSQL的递归查询

    众所周知,目前的mysql版本中并不支持直接的递归查询,但是通过递归到迭代转化的思路,还是可以在一句SQL内实现树的递归查询的.这个得益于Mysql允许在SQL语句内使用@变量.以下是示例代码. 创建 ...

  8. 互联网公司面试必问的mysql题目(上)

    又到了招聘的旺季,被要求准备些社招.校招的题库.(如果你是应届生,尤其是东北的某大学,绝对福利哦) 介绍:MySQL是一个关系型数据库管理系统,目前属于 Oracle 旗下产品.虽然单机性能比不上or ...

  9. 企业面试题|最常问的MySQL面试题集合(三)

    分区表的原理 分库分表的原理 延伸: MySQL的复制原理及负载均衡 分区表的工作原理 对用户而言,分区表是一个独立的逻辑表,但是底层MySQL将其分成了多个物理子表,这对用户来说是透明的,每一个分区 ...

随机推荐

  1. C++中复杂声明和定义的辨析

    0x00 前言 c++中的复杂声明往往令人无法下手,经常使人搞错这到底声明的是一个指针还是指针函数.但其实c++对于复杂声明是遵循一定的规则的,叫做变量名—>右--左-右规则. 0x01 规则解 ...

  2. redis源码学习之工作流程初探

    目录 背景 环境准备 下载redis源码 下载Visual Studio Visual Studio打开redis源码 启动过程分析 调用关系图 事件循环分析 工作模型 代码分析 动画演示 网络模块 ...

  3. JVM 专题十八:垃圾回收(二)垃圾回收相关算法

    1. 标记阶段 1.1 引用计数算法 1.1.1 对象存活判断 在堆里存放着几乎所有的Java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象.只有被标记为己 ...

  4. java 面向对象(十一):关键字:package/import

    1.1 使用说明: * 1.为了更好的实现项目中类的管理,提供包的概念 * 2.使用package声明类或接口所属的包,声明在源文件的首行 * 3.包,属于标识符,遵循标识符的命名规则.规范(xxxy ...

  5. dataframe一次小实验

    老师搞不清,一个ndarray[df['columns']==a],返回的是什么,目前看来应该是df[] == a 的索引

  6. Shader-内轮廓自发光效果

    需求 1 基于涅菲尔反射的变形 原理 (近处的反射少,远处反射多) 1)公式(近似):F = Fscale + (1-Fscale)(1-v·n)^5 利用fresnel做边缘发光,代码 fixed ...

  7. JavaScript动画实例:旋转的正三角形

    给定一个正三角形的重心坐标为(x0,y0),高为h,可以用如下的语句绘制一个底边水平的正三角形. ctx.beginPath(); ctx.moveTo(x0,y0-h*2/3); ctx.lineT ...

  8. Python Ethical Hacking - BACKDOORS(2)

    Refactoring - Creating a Listener Class #!/usr/bin/env python import socket class Listener: def __in ...

  9. JVM调优工具Arthas的使用

    Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱.在线排查问题,无需重启:动态跟踪Java代码:实时监控JVM状态. Arthas 支持JDK6+,支持Linux/Mac/Wind ...

  10. 深入浅出Semaphore源码解析

    Semaphore通过permits的值来限制线程访问临界资源的总数,属于有限制次数的共享锁,不支持重入. 前提条件 在理解Semaphore时需要具备一些基本的知识: 理解AQS的实现原理 之前有写 ...