• SQL动态表的设计思路总结
  • 发布于 1周前
  • 44 热度
    0 评论
一、文档概述
1.1 项目背景
筛选数据 随着筛选维度的增加,目前的方案需要不断增加表。短期可以维持原方案来支撑业务。长期需要结合中台思想,考虑更好的扩展性、通用性,快速支撑业务。

1.2 项目目标
在延迟可接受的前提下,提高筛选数据功能的扩展性和可维护性。

1.3 名词解释

名词

解释

静态表

固定字段的数据表

动态表

通过配置key-value对,行转列的数据表

元数据

元数据是关于数据的数据‌。它描述了数据的属性、结构、关系以及数据的背景和上下文信息

二、方案对比
2.1 结论
长期来看,动态表方案更符合技术下沉,做成通用基础设施的目标。

2.2 说明
按照PaaS的思想,上层服务会按照domain独立部署,要保持轻量级,尽量减少独立部署组件。另外,这种模式,每个应用的数据量级不高,因此这里不考虑使用ES等外部组件实现的方案。

2.3 对比

静态表方案

动态表方案

对比

易读性

静态表的字段是固定的,数据结构清晰明了

需要依靠相对复杂的查询

静态表方案

★★★★

动态表方案

★★★

效率

由于字段固定,数据库可以对这些字段进行优化,如索引、预计算等,查询效率较高

由于字段不固定,数据库无法对这些字段进行优化,查询效率通常较低

静态表方案

★★★★

动态表方案

★★★

数据一致性

固定字段可以确保数据的一致性和完整性,因为每个字段都有明确的定义和类型

动态表中的数据结构不固定,可能会导致数据一致性和完整性问题,需要额外的逻辑来保证

静态表方案

★★★★

动态表方案

★★★

可维护性

结构简单,易于进行数据库维护和备份

动态表的结构复杂,维护和备份相对困难,需要更多的开发和运维工作

静态表方案

★★★★

动态表方案

★★★

灵活性

当业务需求变化时,如果需要增加或删除字段,可能需要修改数据库结构,这可能会导致较大的改动和风险

动态表通过key-value对存储数据,可以根据业务需求动态添加或删除字段,灵活性非常高

静态表方案

★★

动态表方案

★★★★

存储效率

如果某些字段的数据量很少,但仍然需要占用固定的存储空间,可能会导致存储效率低下

只有实际存在的数据才会占用存储空间,避免了静态表中可能存在的空字段浪费存储空间的问题

静态表方案

★★

动态表方案

★★★★

扩展性

随着业务的发展,如果需要存储更多类型的数据,静态表可能无法灵活应对

动态表可以轻松应对业务需求的变化,无需修改数据库结构,扩展性非常好

静态表方案

★★

动态表方案

★★★★★

2.4 综述
静态表适用于数据结构固定、查询频繁、数据一致性要求高的场景。
动态表适用于数据结构不固定、灵活性要求高、扩展性要求强的场景。

三 设计总体思路
3.1 核心思路
考虑使用动态表方案时,针对可能出现的问题,如何解决。数据表从大方向上分为两类。一类是元数据信息,这类信息与具体请求无关,以metadata_开头。另一类维持现有规则

3.2 表结构设计
表一
CREATE TABLE `metadata_prop_config`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',
  `uniqe_key` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '唯一标识一个属性,允许遵循字段命名规范来定义,遵循驼峰命名法',
  `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '属性名,页面上的表示名',
  `parent_key` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '-' COMMENT '关联本表父节点的uniqe_key',
  `can_null` int(11) NOT NULL DEFAULT 0 COMMENT '是否必填:0 必填 1非必填',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `uniqe_key`(`uniqe_key`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '存放数据定义的配置表' ROW_FORMAT = Dynamic;
比如

存储到数据库后就是
# 堆代码 duidaima.com
select * from metadata_prop_config WHERE name='机动车' or parent_key in (SELECT uniqe_key from metadata_prop_config WHERE name='机动车' or parent_key in (SELECT uniqe_key from metadata_prop_config WHERE name='机动车'))

表二
CREATE TABLE `metadata_prop_enum`  (
  `id` int(11) NOT NULL COMMENT '自增主键',
  `prop_key` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '关联metadata_prop_config表的uniqe_key属性',
  `prop_enum` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '属性值的枚举',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `uniq_all_field`(`prop_key`, `prop_enum`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '存放数据项的取值范围约束' ROW_FORMAT = Dynamic;
比如咱们需要录入居民 使用的手机,手机品牌是可以枚举的,不能选择市场上没有的。比如市场上买不到光头强用的【山寨手机】这个品牌。字段定义如下:

取值可以限定以下范围,在页面上,可以通过提供下拉框来选择实现,减少用户手动输入的填错风险,便于归类:
select * from metadata_prop_enum WHERE prop_key='phoneBrand'


注意:不是所有的属性都需要写这张表来限定类型,比如车牌号就不可枚举。

表三
CREATE TABLE `resident_prop_relation`  (
  `id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '主键ID',
  `prop_key` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '属性键',
  `prop_value` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL DEFAULT '' COMMENT '属性值',
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `key_all_field`(`resident_id`, `domain_id`, `prop_key`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '存放居民的属性信息' ROW_FORMAT = DYNAMIC;
一个居民可能会有以下属性:

3.3 交互设计
3.3.1 设计要求
接口设计需要支持同时满足and 和 or两种查询组合
如筛选居民居住类型和性别 (and)
如筛选 机动车信息 或者 电动车信息 的居民(or) 

3.3.2 既有设计形式
and 条件的数据用扩展字段做,or多条信息用列表做
3.3.1 中的例子接口入参示例如下
@Data
public class ResidentInfo {
    private String 居住类型;
    private String 性别;
    private List<Vehicle> 车辆;

    @Data
    public static class Vehicle {
        private String 车牌号;
        private String 车牌颜色;//针对电动车这个字段值都为空
        private String 车辆类型;
        private String 车辆品牌;
        private String 电瓶类型;//针对机动车这个字段值都为空
    }
}
此方案有个缺点,针对上面注释的【针对电动车这个字段值都为空】、【针对机动车这个字段值都为空】的情况,字段冗余较多,可维护性差
3.3.3 改进方案设计形式
and 条件的数据用扩展字段做,or多条信息用map列表做
3.3.1 中的例子接口入参示例如下:
@Data
public class ResidentInfo {
    private String 居住类型;
    private String 性别;
    private List<Vehicle> 车辆;

    @Data
    public static class Vehicle {
        private static Map<String, String> propMap;
//        车辆1:机动车
//        propMap.put("车牌号","京A888888");
//        propMap.put("车辆类型","SUV");
//        propMap.put("车辆品牌","奔驰");
//        propMap.put("车辆颜色","黑色");
//        车辆2:电动车
//        propMap.put("车牌号","京N111111");
//        propMap.put("车辆类型","Q豆锂电版");
//        propMap.put("车辆品牌","爱玛");
//        propMap.put("电瓶类型","12V");
    }
}
此方案弥补了既有方案的缺点,增加了灵活性,但产生了新的问题:propMap到底能传哪些没有具体限定,可能会造成滥用,需要前后端充分沟通。
为了解决问题3,可以提供属性范围校验接口
/config/get?propKey=机动车
返回结果

前端校验在属性范围内才可以传入,需要提供通用组件来做此校验
3.4 实现要点
这个方案最大的问题是基于关系型数据库查询的效率问题。上面的例子,需要把sql分成两部分。一部分是筛选既有字段,如居住类型、性别,这些保留现有逻辑单独筛选,将筛选后的居民作为入参再传给第二部分。

第二部分的SQL语句是动态生成,核心是每个prop_key一个SQL语句,在内存中用线程池并发向数据库中获取数据后内存计算。SQL语句样例如下:
SELECT * FROM resident_prop_relation
 WHERE prop_key='car' AND prop_value='京A888888' AND id in
MVP版本实现 3.3 交互设计 中案例条件,执行耗时800ms内。
四、未来规划
基于PaaS的思想,将来可以基于这个设计,用配置代替开发,后续针对同种类型的问题,可以采用低代码甚至无代码方式支持。
用户评论