MySQL树状数据的数据库设计

我们在mysql数据库设计的时候,会遇到一种树状的数据。如公司下面分开数个部门,部门下面又各自分开数个科室,以此形成树状的数据。 2017-09-26 13:35:40 Mysql数据库设计树状数据 你的数据库索引是否真的起作用了? 为了加快你的查询,增加正确的索引是必不可少的。 但是过了一段时间,当你的系统增长了,你可能会发现自己大量的索引导致数据库的写操作变慢 -- 由于每一次对表的写操作,都需要在事务中更新索引。 2017-09-26 11:06:15 数据库索引查询 如何提高数据库性能?6招就帮你搞定! 提升数据库性能是你的待办事项列表吗?基础设施建设是不是像一个复杂而昂贵的项目?希望有一个简便的可操作清单以提高数据库性能? 2017-09-26 10:51:55 提高数据库性能 外媒速递:Redis为何能够在缓存领域击败Memcached? 原创 选Redis还是Memcached?在对现代数据库驱动型Web应用进行性能改进时,我们总会面临这样的难题。这两大缓存引擎虽然拥有诸多相似之处,但也存在着一些重要区别。而总结来讲,Redis在现代化水平与通用性方面,往往更胜一筹。 2017-09-26 08:56:33 Redis缓存PaaS 干货 | 98道常见Hadoop面试题及答案解析(一) 这是一篇hadoop的测试题及答案解析,题目种类挺多,一共有98道题,题目难度不大,对于高手来说,90分以上才是你的追求。 2017-09-25 10:00:18 Hadoop面试题答案解析 分布式系统一致性保障方案总结 本文重点来阐述下关于一致性的方案,包括强一致性和最终一致性。 而在互联网领域, 很多情况下都是牺牲强一致性,来达到高可用性, 系 统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。 2017-09-22 12:08:01 数据库分布式系统互联网 Oracle数据库中直方图对执行计划的影响 在Oracle数据库中,CBO会默认目标列的数据在其最小值low_value和最大值high_value之间均匀分布,并按照均匀分布原则,来计算目标列施加查询条件后的可选择率以及结果集的cardinality。

我们在mysql数据库设计的时候,会遇到一种树状的数据。如公司下面分开数个部门,部门下面又各自分开数个科室,以此形成树状的数据。

[[204962]]

0 树状数据的分类

我们在mysql数据库设计的时候,会遇到一种树状的数据。如公司下面分开数个部门,部门下面又各自分开数个科室,以此形成树状的数据。关于树状的数据,按层级数大致可分为一下两类:

分类 特点
固定数量层级 层级数量固定,每一层级都有各自的意义,如集团-分公司-部门-科室,省-市-区等
可变数量层级 层级数量不固定,前几层级可能会有特殊含义,但整体在相当大的范围内是浮动的

前者的优点在于,由于每一层级均有各自含义,数据库的整体设计更为方便,可将某一子节点的不同上级节点均存储在数据库中,同样以某集团为例:

节点code 节点名称 节点层级 父级节点code 1级祖先code 2级祖先cdoe
010000 公司1 1 000000 null null
020000 公司2 1 000000 null null
010300 制造部 2 010000 010000 null
010400 品质部 2 010000 010000 null
010301 前工程制造 3 010300 010000 010300
010303 组装制造 3 010300 010000 010300

这样设计的表格冗余较多,但在各种类型查询的时候效率较高.在插入,更新(含子机构,由于业务逻辑特点,机构之间的更新一般是平行转移),删除(含子机构)的时候,由于冗余信息较多,数据操作时所需进行的查询获得也较简单。根据情况,部分冗余信息也考虑删去,如父级节点code,删去一些设计必然会导致部分查询的效率或复杂度提升,这个就需要根据实际情况来取舍平衡了。

缺点有两个:

  1. 一个是当层级数量较多的时候,需要存储大量的冗余信息.当然也可以考虑节约方案:1)不存储像n级祖先code这样的字段,但这样就无法利用固定层级设计带来的高效查询特性,是不建议这么做的;2)n级存储不使用code而改用id,这样做主要是在数据迁移或者他表利用的时候不方便。
  2. 另一个缺点是,当需求方给出要求,需要对当前机构重新洗牌,变更层级数的时候,你会非常头疼。

后者的优缺点则与前者的优缺点恰好相反,非固定的层级限制非常灵活,而缺点就是查询及数据操作上两方面的不便,这也是本文所要讲述的重点,即如何设计非固定层级的树状数据。

1 非固定层级树状数据的设计方式--祖先路径

树状数据最简单的一种设计方式是,只增加父级id。但这种设计方式给查询后代节点带来了极大的不便,据我所知,尚没有一种不通过函数/存储过程这样循环遍历的查询方式,来一次获取某个节点的所有后代节点或是祖先节点。(此前找到过一个较复杂的查询后代节点的sql,利用的也是祖先节点的id大于后代节点id的特性,但有可能存在通过更新节点使后代节点id大于祖先节点id,所以也不严谨,在此不进行详述)

对于非固定层级树状数据的一种设计方式是:增加祖先路径(ancestor_path),具体可参考下表:

id | 节点名称 | 父id | 祖先路径

  1. ---|---|---|---
  2. 1|node1|0|0,
  3. 2|node2|0|0,
  4. 3|node1.1|1|0,1,
  5. 4|node1.2|1|0,1,
  6. 5|node2.1|2|0,2,
  7. 6|node1.1.1|3|0,1,3,
  8. 7|node1.1.2|3|0,1,3,
  9. 8|node1.2.1|4|0,1,4,
  10. 9|node2.1.1|5|0,2,5,

实际设计时,还可考虑加入层级这个冗余字段,但我在实际使用的过程中很少用到这个字段。

这样,在加了这个字段之后,任意节点的所有祖先节点信息就都可通过这样一条数据全部获取。

祖先路径的设定具有以下特点:

  1. 没有父节点的根节点,父id默认为'0',祖先路径默认为'0,';
  2. 每增加的一个子节点,祖先路径都是在要增加的子节点的父节点的祖先路径上增加父id和',';参考的表结构如下:
  1. CREATETABLE`t_node`(
  2. `node_id`int(11)NOTNULLAUTO_INCREMENT,
  3. `node_name`varchar(50)NOTNULL,
  4. `p_id`int(11)NOTNULL,
  5. `ancestor_path`varchar(100)NOTNULL,
  6. PRIMARYKEY(`node_id`)
  7. )ENGINE=InnoDBAUTO_INCREMENT=10DEFAULTCHARSET=utf8;

2 祖先路径的查询

设计的树节点的查询,主要有两种,一种是查询某个节点的所有后代节点(与查询祖先节点为某个已知节点的所有节点集合是一个意思),这种也是最常用的一种查询;一种是查询某个节点的所有祖先节点,这种不太常用。

1. 查询某个节点的所有后代节点 参考示例如下:

  1. SELECT*FROMt_node
  2. WHEREancestor_pathLIKECONCAT(
  3. (SELECT*FROM(SELECTancestor_pathFROMt_nodeWHEREnode_id=?)wt),
  4. ?,',%')

以上sql即是对id为?的某个节点的所有后代节点的查询方式一,还可使用以下方式:

  1. SELECT*FROMt_nodeWHEREancestor_pathLIKECONCAT('%,',?,',%')

查询方式二的方式更加简洁。但考虑到查询方式一只用到了右模糊查询,可以使用索引,所以还是建议使用方式一进行查询。

需要注意的是以上两种方式查到的节点集合都不包含子节点,如果需要包含该节点的信息,还需要加上

  1. ...ORnode_id=?

2. 查询某个节点的所有祖先节点

  1. SELECT*FROMt_nodeWHEREnode_idREGEXP
  2. CONCAT('^(',
  3. REPLACE((SELECT*FROM(SELECTancestor_pathFROMt_nodeWHEREnode_id=?)wt),',','|'),
  4. '0)$')

以上方式查询祖先节点的效率确实不是很高,但考虑到该查询本身并不用,便姑且用之了。

3 祖先路径的插入,更新和删除

分别分插入,更新和删除来讲:

1. 插入

  1. INSERTINTOt_node(node_name,p_id,ancestor_path)
  2. VALUE('node?',?,
  3. CONCAT((SELECT*FROM(SELECTancestor_pathFROMt_nodeWHEREnode_id=?)wt),?,','))

sql中的3个?均为要加入父节点的id。

2. 更新(含子节点)

如果更新的时候,父节点的位置没有变化,则不必考虑太多;

如果需要更新所在父节点,相比于最简单的树节点设计模式,增加祖先路径的方式除了在更新当前节点本身的父id外,还需要修改对应的祖先路径,这个步骤通过存储过程实现,是一种比较简单的方式,在此不再详述。仅对不使用存储过程的方式进行描述。

  1. UPDATEt_nodeSETp_id=?_pWHEREnode_id=?_n;
  2. UPDATEt_nodeSETancestor_path=CONCAT((SELECT*FROM(SELECTancestor_pathFROMt_nodeWHEREnode_id=?_p)wt2),?_p,',',SUBSTR(ancestor_path,LENGTH(@PPath)+1))
  3. WHEREancestor_pathLIKECONCAT((SELECT*FROM(SELECT@ppath:=ancestor_pathFROMt_nodeWHEREnode_id=?_n)wt),?_n,',%')
  4. ORnode_id=?_n;

其中?_n表示要修改的节点的id,?_p表示要修改的节点的新父节点的id。

注:使用该sql一定要先更新子节点的祖先路径,再更新本节点的祖先路径,如果是使用存储过程的话就可以无视这一点了。

3. 删除(含子节点)

  1. DELETEFROMt_node
  2. WHEREancestor_pathLIKECONCAT(
  3. (SELECT*FROM(SELECTancestor_pathFROMt_nodeWHEREnode_id=?)wt),
  4. ?,',%')

删除的核心在于where,和获取所有后代节点的where可以说是完全一样的。

同样要主要先删除所有后代节点,再删除本节点;

4 祖先路径的重置

有可能你此前的某个数据库表格没有使用过祖先路径,但已经积累了一定量的数据,或者之前使用了祖先路径,但由于某种原因导致祖先路径的一些数据更新错误。因为祖先路径本质上是一个冗余字段,所以还是可以通过父id的方式将之还原重置。

以下为机构表的一个重置存储过程,供以参考:

  1. CREATEDEFINER=`root`@`localhost`PROCEDURE`p_reset_organ_path`(OUTresultMarkvarchar(50))
  2. BEGIN
  3. /*
  4. 使用前的说明:
  5. 1.本存储过程非客户使用,且自己人使用频率同样较低,故过程更方便调试,但效率不是很高;
  6. 2.如果执行SELECT*FROMt_organWHEREorgan_id<parent_organ_id(即父机构产生于子机构之后)后的数据为空,则可以考虑使用分段模式(速度会快一些).
  7. 3.如果2中所述数据不为空,使用分段会使该id对应的机构及其子机构的ancestor_path不正确.结果为partfail.
  8. */
  9. DECLAREintACountINT(11)DEFAULT0;
  10. DECLAREintPCountINT(11)DEFAULT0;
  11. DECLAREintPIndexINT(11)DEFAULT0;
  12. DECLAREintPOrganIdINT(11)DEFAULT0;
  13. DECLAREstrPPathVARCHAR(100)DEFAULT'';
  14. DECLAREintLoopDoneINT(11)DEFAULT0;
  15. DECLAREintRCountINT(11)DEFAULT0;
  16. DECLAREintRIndexINT(11)DEFAULT0;
  17. DECLAREintROrganIdINT(11)DEFAULT0;
  18. DROPTABLEIFEXISTStmp_aOrganIdList;
  19. CREATETEMPORARYTABLEtmp_aOrganIdList(
  20. rowidINT(11)auto_incrementPRIMARYKEY,
  21. organ_idINT(11),
  22. p_organ_idINT(11)
  23. );
  24. DROPTABLEIFEXISTStmp_pOrganIdList;
  25. CREATETEMPORARYTABLEtmp_pOrganIdList(
  26. rowidINT(11)auto_incrementPRIMARYKEY,
  27. organ_idINT(11)
  28. );
  29. /**/
  30. DROPTABLEIFEXISTStmp_cOrganIdList;
  31. CREATETEMPORARYTABLEtmp_cOrganIdList(
  32. rowidINT(11)auto_incrementPRIMARYKEY,
  33. organ_idINT(11)
  34. );
  35. DROPTABLEIFEXISTStmp_rOrganIdList;
  36. CREATETEMPORARYTABLEtmp_rOrganIdList(
  37. rowidINT(11)auto_incrementPRIMARYKEY,
  38. organ_idINT(11),
  39. p_organ_idINT(11),
  40. ancestor_pathVARCHAR(100)
  41. );
  42. INSERTINTOtmp_aOrganIdList(organ_id,p_organ_id)
  43. (SELECTorgan_id,parent_organ_idFROMt_organ);--测试的时候limit:LIMIT0,100
  44. INSERTINTOtmp_pOrganIdList(organ_id)VALUES(0);
  45. INSERTINTOtmp_rOrganIdList(organ_id,p_organ_id,ancestor_path)VALUES(0,-1,'');
  46. WHILE((SELECTCOUNT(1)FROMtmp_aOrganIdList)>0ANDintLoopDone=0)DO--持续循环,当没有organId数据为止(如果中间机构中断,则可能陷入死循环)
  47. SELECTCOUNT(1)FROMtmp_pOrganIdListINTOintPCount;--当前父机构id的缓存区
  48. SETintPIndex=0;
  49. WHILEintPIndex<=intPCountDO--对每个当前查询到的父id进行对应操作
  50. SELECTorgan_idFROMtmp_pOrganIdListLIMITintPIndex,1INTOintPOrganId;
  51. SELECTancestor_pathFROMtmp_rOrganIdListWHEREorgan_id=intPOrganIdINTOstrPPath;
  52. INSERTINTOtmp_cOrganIdList(organ_id)(SELECTorgan_idFROMtmp_aOrganIdListWHEREp_organ_id=intPOrganId);--次级机构id的缓存区
  53. --SELECTCOUNT(1)FROMtmp_pOrganIdListINTOintDelCount;
  54. INSERTINTOtmp_rOrganIdList(organ_id,p_organ_id,ancestor_path)
  55. (SELECTorgan_id,intPOrganId,CONCAT(strPPath,intPOrganId,',')FROMtmp_aOrganIdListWHEREp_organ_id=intPOrganId);
  56. DELETEFROMtmp_aOrganIdListWHEREp_organ_id=intPOrganId;
  57. SETintPIndex=intPIndex+1;
  58. ENDWHILE;
  59. DELETEFROMtmp_pOrganIdList;
  60. IF(SELECTCOUNT(1)FROMtmp_cOrganIdList)>0THEN
  61. INSERTINTOtmp_pOrganIdList(organ_id)(SELECTorgan_idFROMtmp_cOrganIdList);
  62. DELETEFROMtmp_cOrganIdList;
  63. ELSE
  64. SETintLoopDone=1;
  65. ENDIF;
  66. --SELECT*FROMtmp_pOrganIdList;
  67. --SELECTCOUNT(1)FROMtmp_aOrganIdList;
  68. --SELECTintLoopDone;
  69. ENDWHILE;
  70. --SELECT*FROMtmp_rOrganIdList;--想要查看测试的结果,请看此表
  71. SELECTCOUNT(1)FROMtmp_rOrganIdListINTOintRCount;
  72. WHILEintRIndex<=intRCountDO
  73. SELECTorgan_id,ancestor_pathFROMtmp_rOrganIdListLIMITintRIndex,1INTOintROrganId,strPPath;
  74. UPDATEt_organSETancestor_path=strPPathWHEREorgan_id=intROrganId;
  75. SETintRIndex=intRIndex+1;
  76. ENDWHILE;
  77. IF(SELECTCOUNT(1)FROMtmp_aOrganIdList)=0THEN
  78. SETresultMark='perfect';
  79. ELSE
  80. SETresultMark='partfail';
  81. ENDIF;
  82. END

©本文为清一色官方代发,观点仅代表作者本人,与清一色无关。清一色对文中陈述、观点判断保持中立,不对所包含内容的准确性、可靠性或完整性提供任何明示或暗示的保证。本文不作为投资理财建议,请读者仅作参考,并请自行承担全部责任。文中部分文字/图片/视频/音频等来源于网络,如侵犯到著作权人的权利,请与我们联系(微信/QQ:1074760229)。转载请注明出处:清一色财经

(0)
打赏 微信扫码打赏 微信扫码打赏 支付宝扫码打赏 支付宝扫码打赏
清一色的头像清一色管理团队
上一篇 2023年5月6日 09:51
下一篇 2023年5月6日 09:51

相关推荐

发表评论

登录后才能评论

联系我们

在线咨询:1643011589-QQbutton

手机:13798586780

QQ/微信:1074760229

QQ群:551893940

工作时间:工作日9:00-18:00,节假日休息

关注微信