超赞,老外的一种避免递归查询所有子部门的树数据表设计与实现!

超赞,老外的一种避免递归查询所有子部门的树数据表设计与实现!,第1张

| 目录

通常树形结构的存储,是在子节点上存储父节点的编号来确定各节点的父子关系,例如这样的组织结构:[图片上传失败...(image-a14d87-1651886833418)]

与之对应的表数据(department):

[图片上传失败...(image-a73b71-1651886833418)]

部门表结构(department)

这样的方式很不错,可以很直观的体现各个节点之间的关系,通常可以满足大多数需求。但是当业务需求变得多了,数据量庞大了,这样的方式就不再适合用于生产。

例如:PM加了以下需求:

使用指定部门编号,一层一层使用递归往下查,可能是多数人会想到的方法。尽管在mysql8.0支持了 cte(公共表表达式),递归效率比传统递归方式有明显提升,但是查询效率仍会随着部门树层级深度的提高而变差。另外一种方法,一次性查出所有数据,放入内存中处理(数据量少时,可以选用。数据量多,不怕挨打的人也可以选这种)~

递归查询每一层的数量,最后相加。

方法1:可以加字段 isLeaf 的方式,来表示这个节点是否是叶子节点。方法2:直接通过查询parent_id=当前id的count是否大于0,大于0表示不是叶子节点,等于0表示为叶子节点。在日常中,可能会经常使用上述类似方法去解决类似的问题,但我觉得这样的方法在效率上不是最优解。于是乎开始查找更好的方案去解决这些问题。

直到后面查到国外一博客中,见到了所谓的《改进后的先序树遍历》文章(天哪,竟然是一篇2003年发表的文章)~他具体是怎么做的呢?还是回到刚刚的组织架构[图片上传失败...(image-8b11ac-1651886833417)]

我们从根节点开始,给董事长左值设为1,下级部门总经理左值设为2,以此类推地沿着边缘开始遍历,给每个节点加上左值,遇到叶子节点处给节点加上右值,再继续向上沿着边缘继续遍历,遍历结束回到根节点右侧,你将得到类似这样的结构。[图片上传失败...(image-f5f389-1651886833417)]

遍历完后每一个节点都有与之对应的左右值。这个时候可以去除parent_id字段,添加lft,rgt,来存储左右值。

[图片上传失败...(image-278dca-1651886833417)]

数据和结构准备完毕,我们来试试 *** 作解决上面的需求~

根据当前表结构的规律,可以发现,要想查出所有子孙部门,只要查左值在 被查寻部门的左\右数之间的节点,查出来都是他的子节点。例如:查询行政总监的所有子部门,行政总监的左右数是9和18,因此只需要用9和18做lft字段的between查询,查询出的结果就是【被查部门本身数据和所有子孙部门】;

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0pxpadding: 0pxoutline: 0pxmax-width: 100%box-sizing: border-box !importantword-wrap: break-word !importantcaret-color: rgb(34, 34, 34)color: rgb(34, 34, 34)font-size: 17pxfont-style: normalfont-variant-caps: normalfont-weight: 400letter-spacing: 0.5440000295639038pxorphans: autotext-align: justifytext-indent: 0pxtext-transform: nonewidows: autoword-spacing: 0px-webkit-text-size-adjust: auto-webkit-text-stroke-width: 0pxtext-decoration: noneline-height: 27.200000762939453px">

完美~

</pre>

到这里可能会说,需求1都解决了,查总数自然也就解决了,直接上select count就可以了,确实没有错,但是没有那个必要,因为有个简单公式可以直接计算。公式:总数 = (右值 - 左值 - 1) / 2例如:

通过有了上述计算公式算总数的经验后,现在判断是否叶子节点,有的小伙伴已经知道了怎么做,那就是:右值 - 1 == 左值那他就是叶子节点,或者左值 + 1 == 右值那他就是叶子节点,反之则不是叶子节点。例如:设计部,5 - 1 == 4,因此他是叶子节点。董事长,20 - 1 != 1,因此他不是叶子节点。至此已经完美的解决了上述需求问题,接下来再尝试一下业务的基本 *** 作。

当新增一个部门时,需要对新增节点位置的后续边缘进行加2 *** 作,因为每一个节点有左右两个数值。这个 *** 作通常需要放到事务中进行处理。例如:在研发部门下添加一个新部门:[图片上传失败...(image-36a719-1651886833417)]

对应sql:

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0pxpadding: 0pxoutline: 0pxmax-width: 100%box-sizing: border-box !importantword-wrap: break-word !importantcaret-color: rgb(34, 34, 34)color: rgb(34, 34, 34)font-size: 17pxfont-style: normalfont-variant-caps: normalfont-weight: 400letter-spacing: 0.5440000295639038pxorphans: autotext-align: justifytext-indent: 0pxtext-transform: nonewidows: autoword-spacing: 0px-webkit-text-size-adjust: auto-webkit-text-stroke-width: 0pxtext-decoration: noneline-height: 27.200000762939453px">

</pre>

删除部门与新增部门类似,不同的是需要对删除节点的后续边缘节点减2 *** 作。例如:删除刚刚添加的新部门:[图片上传失败...(image-504fd7-1651886833417)]

对应sql

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0pxpadding: 0pxoutline: 0pxmax-width: 100%box-sizing: border-box !importantword-wrap: break-word !importantcaret-color: rgb(34, 34, 34)color: rgb(34, 34, 34)font-size: 17pxfont-style: normalfont-variant-caps: normalfont-weight: 400letter-spacing: 0.5440000295639038pxorphans: autotext-align: justifytext-indent: 0pxtext-transform: nonewidows: autoword-spacing: 0px-webkit-text-size-adjust: auto-webkit-text-stroke-width: 0pxtext-decoration: noneline-height: 27.200000762939453px">

</pre>

查询某部门的直接子部门(即不包含孙子部门),例如:查询总经理下的直接子部门。正常需要返回产品部和行政总监[图片上传失败...(image-e2c88e-1651886833417)]

对应的sql

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0pxpadding: 0pxoutline: 0pxmax-width: 100%box-sizing: border-box !importantword-wrap: break-word !importantcaret-color: rgb(34, 34, 34)color: rgb(34, 34, 34)font-size: 17pxfont-style: normalfont-variant-caps: normalfont-weight: 400letter-spacing: 0.5440000295639038pxorphans: autotext-align: justifytext-indent: 0pxtext-transform: nonewidows: autoword-spacing: 0px-webkit-text-size-adjust: auto-webkit-text-stroke-width: 0pxtext-decoration: noneline-height: 27.200000762939453px">

</pre>

查询某部门的祖链路径。例如:查询产品部的祖链路径,正常需要返回董事长,总经理

<pre mp-original-font-size="17" mp-original-line-height="27.200000762939453" style="margin: 0pxpadding: 0pxoutline: 0pxmax-width: 100%box-sizing: border-box !importantword-wrap: break-word !importantcaret-color: rgb(34, 34, 34)color: rgb(34, 34, 34)font-size: 17pxfont-style: normalfont-variant-caps: normalfont-weight: 400letter-spacing: 0.5440000295639038pxorphans: autotext-align: justifytext-indent: 0pxtext-transform: nonewidows: autoword-spacing: 0px-webkit-text-size-adjust: auto-webkit-text-stroke-width: 0pxtext-decoration: noneline-height: 27.200000762939453px">

</pre>

在我目前看来,这个方法的唯一缺点就是,每一次的新增或删除, *** 作节点的后续边缘走到的节点都要加/减2 *** 作。

欢迎指正、交流和评论,一起探讨更多解决方案 ...

-- 链接指导:

https://mp.weixin.qq.com/s/GDmrIwo89WVfDyAWYSBWQA

首先创建一个熟悉的机构表

插入几条测试数据:

union all上面的是初始化语句,只会执行一次,查到了 开发部 这一行记录。

接下来下面的join会用初始化的语句去原来的organization表去join获取所有 开发部的子部门 ,然后再用这些 子部门 去join更下面的部门。

执行的结果如下:

如下想查询开发部的所有上级部门的话上面的递归查询语句简单改一下就可以了:

执行结果如下:

Recursive Common Table Expression 'temp' can contain neither

aggregation nor window functions in recursive query block

mysql

mysql对递归的深度是有限制的,默认的递归深度是1000。

可以通过 show variables like 'cte_max_recursion_depth'进行查看

也可以通过select语句最大执行时间对递归加以显示, show variables lile 'max_execution_time'

使用WITH AS提高性能简化嵌套SQL

一.WITH AS的含义

    WITH AS短语,也叫做子查询部分(subquery factoring),可以让你做很多事情,定义一个SQL片断,该SQL片断会

被整个SQL语句所用到。有的时候,是为了让SQL语句的可读性更高些,也有可能是在UNION ALL的不同部分,作为提供数

据的部分。 

特别对于UNION ALL比较有用。因为UNION ALL的每个部分可能相同,但是如果每个部分都去执行一遍的话,则成本太高,

所以可以使用WITH AS短语,则只要执行一遍即可。如果WITH AS短语所定义的表名被调用两次以上,则优化器会自动将

WITH AS短语所获取的数据放入一个TEMP表里,如果只是被调用一次,则不会。而提示materialize则是强制将WITH AS

短语里的数据放入一个全局临时表里。很多查询通过这种方法都可以提高速度。

二.使用方法

先看下面一个嵌套的查询语句:

select * from person.StateProvince where CountryRegionCode in 

         (select CountryRegionCode from person.CountryRegion where Name like 'C%') 上面的查询语句使用了一个子查询。虽然这条SQL语句并不复杂,但如果嵌套的层次过多,会使SQL语句非常难以阅

读和维护。因此,也可以使用表变量的方式来解决这个问题。

SQL语句如下:

declare @t table(CountryRegionCode nvarchar(3)) 

insert into @t(CountryRegionCode)  (select CountryRegionCode from person.CountryRegion where Name like 'C%') 

select * from person.StateProvince where CountryRegionCode 

                     in (select * from @t) 

    虽然上面的SQL语句要比第一种方式更复杂,但却将子查询放在了表变量@t中,这样做将使SQL语句更容易维护,但又

会带来另一个问题,就是性能的损失。由于表变量实际上使用了临时表,从而增加了额外的I/O开销,因此,表变量的方式

并不太适合数据量大且频繁查询的情况。为此,在SQL Server 2005中提供了另外一种解决方案,这就是公用表表达式(CTE),使用CTE,可以增加SQL语句的可维护性,同时,CTE要比表变量的效率高得多。

下面是CTE的语法:

[ WITH <common_table_expression> [ ,n ] ] 

<common_table_expression>::= 

        expression_name [ ( column_name [ ,n ] ) ] 

    AS 

        ( CTE_query_definition )

现在使用CTE来解决上面的问题,SQL语句如下:

with 

cr as 

    select CountryRegionCode from person.CountryRegion where Name like 'C%' 

select * from person.StateProvince where CountryRegionCode in (select * from cr) 

    其中cr是一个公用表表达式,该表达式在使用上与表变量类似,只是SQL Server 2005在处理公用表表达式的方式上有

所不同。

在使用CTE时应注意如下几点:

1. CTE后面必须直接跟使用CTE的SQL语句(如select、insert、update等),否则,CTE将失效。如下面的SQL语句将无法正

常使用CTE:

with 

cr as 

    select CountryRegionCode from person.CountryRegion where Name like 'C%' 

select * from person.CountryRegion  -- 应将这条SQL语句去掉

-- 使用CTE的SQL语句应紧跟在相关的CTE后面-- 

select * from person.StateProvince where CountryRegionCode in (select * from cr) 

2. CTE后面也可以跟其他的CTE,但只能使用一个with,多个CTE中间用逗号(,)分隔,如下面的SQL语句所示:

with 

cte1 as 

    select * from table1 where name like 'abc%' 

), 

cte2 as 

    select * from table2 where id > 20 

), 

cte3 as 

    select * from table3 where price < 100 

select a.* from cte1 a, cte2 b, cte3 c where a.id = b.id and a.id = c.id 

3. 如果CTE的表达式名称与某个数据表或视图重名,则紧跟在该CTE后面的SQL语句使用的仍然是CTE,当然,后面的SQL语句

使用的就是数据表或视图了,如下面的SQL语句所示:

--  table1是一个实际存在的表

with 

table1 as 

    select * from persons where age < 30 

select * from table1  --  使用了名为table1的公共表表达式

select * from table1  --  使用了名为table1的数据表

4. CTE 可以引用自身,也可以引用在同一WITH 子句中预先定义的CTE。不允许前向引用。

5. 不能在CTE_query_definition 中使用以下子句:

(1)COMPUTE 或COMPUTE BY

(2)ORDER BY(除非指定了TOP 子句)

(3)INTO

(4)带有查询提示的OPTION 子句

(5)FOR XML

(6)FOR BROWSE

6. 如果将CTE 用在属于批处理的一部分的语句中,那么在它之前的语句必须以分号结尾,如下面的SQL所示:

declare @s nvarchar(3) 

set @s = 'C%' 

  -- 必须加分号

with 

t_tree as 

    select CountryRegionCode from person.CountryRegion where Name like @s 

select * from person.StateProvince where CountryRegionCode in (select * from t_tree)


欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/8674028.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2023-04-19
下一篇 2023-04-19

发表评论

登录后才能评论

评论列表(0条)

保存