Mysql闭包表之关于国家区域的一个实践

在电商系统中,我们总是会遇到一些树形结构数据的存储需求。如地理区域、位置信息存储,地理信息按照层级划分,会分为很多层级,就拿中国的行政区域划分为例,简单的省-市-县-镇-村就要五个级别。如果系统涉及到跨境的国际贸易,那么存储的地理信息层级会更加深。那么如何正确合理地存储这些数据,并且又能很好的适应各种查询场景就成了我们需要考虑的问题,这次我们来考虑通过闭包表方案,来达到我们的存储及查询需求。

一、设计闭包表

闭包表由Closure Table翻译而来,通过父节点、子节点、两节点距离来描述一棵树空间换时间的思想,Closure Table,一种更为彻底的全路径结构,分别记录路径上相关结点的全展开形式。能明晰任意两结点关系而无须多余查询,级联删除和结点移动也很方便。但是它的存储开销会大一些,除了表示结点的Meta信息,还需要一张专用的关系表。

区域基础信息表结构如下

CREATE TABLE `area_base` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT ‘自增主键‘,
  `area_name` varchar(50) NOT NULL COMMENT ‘区域名称‘,
  `sequence` int(11) DEFAULT NULL COMMENT ‘排序号,越小越靠前‘,
  `created_by` bigint(20) NOT NULL COMMENT ‘创建人‘,
  `created_time` bigint(20) NOT NULL COMMENT ‘创建时间‘,
  `updated_by` bigint(20) DEFAULT NULL COMMENT ‘更新人‘,
  `updated_time` bigint(20) NOT NULL DEFAULT ‘0‘ COMMENT ‘更新时间‘,
  `is_del` tinyint(2) NOT NULL DEFAULT ‘0‘ COMMENT ‘状态:0 正常,-1 已删除‘,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=56 DEFAULT CHARSET=utf8mb4 COMMENT=‘区域表‘;

区域之间指向关系的闭包表结构如下

CREATE TABLE `area_closure` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘自增长Id‘,
`ancestor` bigint(20) NOT NULL COMMENT ‘祖先‘,
`descendant` bigint(20) NOT NULL COMMENT ‘后代‘,
`distance` int(11) DEFAULT NULL COMMENT ‘祖先到后代之间的距离‘,
PRIMARY KEY (`id`),
UNIQUE KEY `id_ancedesc` (`ancestor`,`descendant`) USING BTREE,
KEY `idx_ancestor` (`ancestor`,`distance`) USING BTREE,
KEY `idx_descendant` (`descendant`,`distance`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=259 DEFAULT CHARSET=utf8mb4 COMMENT=‘区域的树形结构闭包表‘;

模拟一些示范数据,如下所示

mysql> select * from area_base;
+----+-----------+----------+------------+----------------+------------+---------------+--------+
| id | area_name | sequence | created_by | created_time   | updated_by | updated_time  | is_del |
+----+-----------+----------+------------+----------------+------------+---------------+--------+
|  1 | 根节点    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 29 | 亚洲      |       96 |        123 | 15679841561561 |        990 | 1540031478909 |      0 |
| 30 | 美洲      |       33 |        123 | 15679841561561 |        990 | 1540031478923 |      0 |
| 31 | 欧洲      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 35 | 中国      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 36 | 日本      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 37 | 朝鲜      |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 38 | 广东省    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 39 | 新疆省    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 40 | 广西省    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 41 | 深圳市    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 42 | 广州市    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
| 43 | 佛山市    |        0 |        123 | 15679841561561 |        990 | 1539175879690 |      0 |
+----+-----------+----------+------------+----------------+------------+---------------+--------+
13 rows in set

二、闭包表中的递归操作

如何递归构造出一颗全区域的返回树

    public AreaTreeResponse getAreaTree(Long areaId) {
        String cacheKey = BasicConst.Cache.AREA_TREE_KEY + BasicConst.AreaInfo.ROOT_NODE_ID;
        AreaTreeResponse areaTreeResponse = cache.get(cacheKey);
        if(areaTreeResponse != null){
            return areaTreeResponse;
        }
        // 递归生成
        areaTreeResponse = newAreaTreeByRecur(areaId);
        // 加入缓存,并设置超时时间
        cache.set(cacheKey, areaTreeResponse, BasicConst.Cache.AREA_CACHE_TTL);
        return areaTreeResponse;
    }
    /**
     * 根据父节点构造返回子树
     *
     * @param parentId
     * @return
     */
    private AreaTreeResponse newAreaTreeByRecur(Long parentId){
        // 初始化返回结果
        AreaTreeResponse areaTree = new AreaTreeResponse();
        // 获取直接子节点
        List<AreaTree> areaChildList = areaClosureMapper.getAreaTree(parentId, 1);
        if(areaChildList == null || areaChildList.size() == 0){
            return areaTree;
        } else {
            // 初始化当前节点的id和name
            Long curNodeId = null;
            String curNodeName = null;
            // 初始化当前节点对应的childList
            List<AreaTreeResponse> childList = new ArrayList<>();
            for (AreaTree areaChildNode : areaChildList) {
                curNodeId = areaChildNode.getParentId();
                curNodeName = areaChildNode.getParentName();
                // 递归,将子节点当成父节点向下递归
                AreaTreeResponse child = newAreaTreeByRecur(areaChildNode.getChildrenId());
                // 叶子节点设置child
                child.setAreaId(areaChildNode.getChildrenId());
                child.setAreaName(areaChildNode.getChildrenName());
                childList.add(child);
            }
            // 将childList传给上一节点
            areaTree.setAreaId(curNodeId);
            areaTree.setAreaName(curNodeName);
            areaTree.setChildren(childList);
            return areaTree;
        }
    }

写一个测试用例进行测试

@Test
public void getCurrentNodeTree(){
    AreaTreeResponse areaTreeResponse = areaService.getAreaTree(1L);
    // 模拟返回树
    String jsonObject = JSONObject.toJSONString(areaTreeResponse);
    System.out.println("lingyejun test result :"+jsonObject);
}

递归生成的树状Json如下

{
    "areaId":1,
    "areaName":"根节点",
    "children":[
        {
            "areaId":31,
            "areaName":"欧洲"
        },
        {
            "areaId":30,
            "areaName":"美洲"
        },
        {
            "areaId":29,
            "areaName":"亚洲",
            "children":[
                {
                    "areaId":35,
                    "areaName":"中国",
                    "children":[
                        {
                            "areaId":38,
                            "areaName":"广东省",
                            "children":[
                                {
                                    "areaId":41,
                                    "areaName":"深圳市"
                                },
                                {
                                    "areaId":42,
                                    "areaName":"广州市"
                                },
                                {
                                    "areaId":43,
                                    "areaName":"佛山市"
                                }
                            ]
                        },
                        {
                            "areaId":39,
                            "areaName":"新疆省"
                        },
                        {
                            "areaId":40,
                            "areaName":"广西省"
                        }
                    ]
                },
                {
                    "areaId":36,
                    "areaName":"日本"
                },
                {
                    "areaId":37,
                    "areaName":"朝鲜"
                }
            ]
        }
    ]
}

参考文章:https://www.biaodianfu.com/closure-table.html  

原文地址:https://www.cnblogs.com/lingyejun/p/9824987.html

时间: 2024-08-03 17:57:31

Mysql闭包表之关于国家区域的一个实践的相关文章

mysql分表和表分区详解

为什么要分表和分区? 日常开发中我们经常会遇到大表的情况,所谓的大表是指存储了百万级乃至千万级条记录的表.这样的表过于庞大,导致数据库在查询和插入的时候耗时太长,性能低下,如果涉及联合查询的情况,性能会更加糟糕.分表和表分区的目的就是减少数据库的负担,提高数据库的效率,通常点来讲就是提高表的增删改查效率. 什么是分表? 分表是将一个大表按照一定的规则分解成多张具有独立存储空间的实体表,我们可以称为子表,每个表都对应三个文件,MYD数据文件,.MYI索引文件,.frm表结构文件.这些子表可以分布在

MySQL 高性能表设计规范

良好的逻辑设计和物理设计是高性能的基石, 应该根据系统将要执行的查询语句来设计schema, 这往往需要权衡各种因素. 一.选择优化的数据类型 MySQL支持的数据类型非常多, 选择正确的数据类型对于获得高性能至关重要. 更小的通常更好 更小的数据类型通常更快, 因为它们占用更少的磁盘. 内存和CPU缓存, 并且处理时需要的CPU周期也更少. 简单就好 简单数据类型的操作通常需要更少的CPU周期. 例如, 整型比字符操作代价更低, 因为字符集和校对规则(排序规则 )使字符比较比整型比较更复杂.

【mysql】mysql分表和表分区详解

为什么要分表和分区? 日常开发中我们经常会遇到大表的情况,所谓的大表是指存储了百万级乃至千万级条记录的表.这样的表过于庞大,导致数据库在查询和插入的时候耗时太长,性能低下,如果涉及联合查询的情况,性能会更加糟糕.分表和表分区的目的就是减少数据库的负担,提高数据库的效率,通常点来讲就是提高表的增删改查效率. 什么是分表? 分表是将一个大表按照一定的规则分解成多张具有独立存储空间的实体表,我们可以称为子表,每个表都对应三个文件,MYD数据文件,.MYI索引文件,.frm表结构文件.这些子表可以分布在

Python数据库操作 Mysql数据库表引擎与字符集#学习猿地

# Mysql数据库表引擎与字符集 ![](./imgs/752951346A5F4E7EBDE362FA97107707.png) ### 1.服务器处理客户端请求 其实不论客户端进程和服务器进程是采用哪种方式进行通信,最后实现的效果都是:**客户端进程向服务器进程发送一段文本(MySQL语句),服务器进程处理后再向客户端进程发送一段文本(处理结果).**那服务器进程对客户端进程发送的请求做了什么处理,才能产生最后的处理结果呢?客户端可以向服务器发送增删改查各类请求,我们这里以比较复杂的查询请

导航猫(NaviCat for MySql)建立表的方法

我知道在客户端上建立表一共了三种(我自己知道,不知道还有木有别的方法,如果有请留言) 1.右键某数据库建立一张表,自己填写字段 2.导入.sql文件(sql语句已经写好),如下图: 右键数据点运行SQL文件就出现了上面的截图部分,执行完sql,刷新一下数据库,表就建立好了.(前提是sql文件中的语句是对的). 3.点击查询->新建查询->此时写sql语句,如下图: 我就知道这三种了,其实sql文件应该是可以生成的(如果切换数据,应该可以在原来的数据库中生成),我建议还是第三种可以联系一下sql

mysql单表限制

mysql单表的限制 一.MySQL数据库的MyISAM存储 引擎单表大小限制已经不是有MySQL数据库本身来决定(限制扩大到64pb),而是由所在主机的OS上面的文件系统来决定了. 在mysql5.0版本之前,myisam存储引擎默认表的大小4Gb,可以用一下命令来查看: [[email protected] test]# cd /data/mysql/mysql_3306/data/test [[email protected] test]# myisamchk -dv t2 MyISAM

mysql分表方法-----MRG_MyISAM引擎分表法

一般来说,当我们的数据库的数据超过了100w记录的时候就应该考虑分表或者分区了,这次我来详细说说分表的一些方法.目前我所知道的方法都是MYISAM的,INNODB如何做分表并且保留事务和外键,我还不是很了解. 首先,我们需要想好到底分多少个表,前提当然是满足应用.这里我使用了一个比较简单的分表方法,就是根据自增id的尾数来分,也就是说分0-9一共10个表,其取值也很好做,就是对10进行取模.另外,还可以根据某一字段的md5值取其中几位进行分表,这样的话,可以分的表就很多了. 好了,先来创建表吧,

关于Mysql删除表数据的两种方式对比

1.delete from table_name 一行一行删除,只删除表数据,auto_increament仍停留在最后一天数据的下一个值. 2.truncate table_name 快捷删除表数据.先删除整个表,然后重新建表结构.auto_increament从1开始. 关于Mysql删除表数据的两种方式对比,布布扣,bubuko.com

python3 mysql 多表查询

python3 mysql 多表查询 一.准备表 创建二张表: company.employee company.department #建表 create table department( id int, name varchar(20) ); create table employee( id int primary key auto_increment, name varchar(20), sex enum('male','female') not null default 'male'