数据库冗余字段设计笔记

数据库冗余字段设计笔记

关联问题:删除区域/合作商时的外键约束、tb_emp.region_name 脏数据

涉及表:tb_region、tb_partner、tb_node、tb_emp、tb_vending_machine、tb_channel

一、什么是冗余字段

同一个语义的数据,在多个表里各存了一份。

按第三范式(3NF)的要求,tb_emp 只需要存 region_id,员工属于哪个区域应该去 tb_region 表 JOIN 查出来。如果在 tb_emp 里再存一个 region_name,这个 region_name 就是冗余字段------它是"能推导出来、本不该存"的数据。

判断方法:

这个字段的值,能不能通过关联表推导出来?能,就是冗余。

二、本项目中的冗余字段实例

表 冗余字段 本该来自 为什么冗余
tb_emp region_name tb_region.region_name 员工列表页直接显示区域名,省一次 JOIN
tb_emp role_code、role_name tb_role 同上
tb_vending_machine region_id、partner_id、addr 可由 tb_node 推出 设备按区域/合作商统计时不用绕点位表
tb_channel inner_code tb_vending_machine.inner_code 货道按软编号查设备,避免 JOIN
tb_order_collect partner_name tb_partner.partner_name 报表直接展示 + 历史快照
tb_task_details sku_name、sku_image tb_sku 工单历史快照
tb_order_month_collect partner_name、region_name 关联表 统计表,纯展示

典型坑:tb_emp.region_name

区域改名后,tb_emp.region_name 里的旧名字不会自动跟着变,于是出现:

复制代码
region_id   = 2      (指向"北京市海淀区")
region_name = '北京市朝阳区'   (旧名字,脏数据)

同理,把 tb_emp_ibfk_2 改成 ON DELETE SET NULL 后,region_id 会被置空,但 region_name 仍是旧值,脏数据更明显。

三、什么时候应该设计冗余字段

1. 读多写少,且查询频繁

列表页、详情页每次都要显示关联表名称。低频查询 JOIN 无所谓,但高频接口 + 大表关联时,冗余一个名称字段能省掉整条 JOIN 链。

典型场景:列表页的「所属区域」「所属合作商」。

2. 关联层级太深

A 关联 B,B 关联 C,查 A 需要 JOIN 三层才能拿到 C 的名称。此时把 C 的关键字段冗余到 A 上,查询从"三次 JOIN"变成"单表扫描"。

本项目的 tb_vending_machine 属于这种情况:设备 → 点位 → 区域/合作商,两层关联,所以直接在设备表冗余 region_id、partner_id。

3. 需要保留"当时的快照"(最不容妥协)

订单、工单、流水、报表这类表,冗余是必须的,不是可选项:

  • 商品今天叫「可口可乐」,明天改名「可口可乐 500ml」,三个月前的订单里必须还是「可口可乐」
  • 商品价格从 3 元涨到 3.5 元,历史订单金额不能跟着变

所以 tb_task_details.sku_name、tb_task_details.sku_image 必须冗余。

这类冗余的目的不是性能,而是历史事实不可被后续修改影响 。若只存 sku_id,那就是设计错误。

4. 跨库、分表、微服务拆分

数据不在同一个库,JOIN 做不了,只能冗余;或分表后 JOIN 成本极高。

5. 允许短暂不一致,或有对账机制

如商品表冗余"销量""评论数",通过定时任务或消息异步更新,短时间不准可接受。

四、什么时候不该设计冗余字段

1. 源数据频繁变化

region_name 一天改十次,冗余在 5 张表里就要同步 5 次,漏一次就是脏数据。

变化越频繁,冗余成本越高。

2. 强一致性要求

财务金额、库存数量、账户余额这类数据宁可 JOIN,也不能存两份------两处数字不一致时无法判断哪份是对的。

3. 能用其他手段解决时

优先考虑以下方案,它们不承担一致性维护成本:

  • 加缓存(Redis)
  • 建视图(View)
  • 加合适索引让 JOIN 变快
  • 搜索引擎(ES)做宽表

4. 冗余对象是"一对多"的集合

tb_emp.region_name 冗余的是单个值,尚可控;若在区域表里存"该区域所有点位名称",则绝对不能做。

五、冗余的代价

收益 代价
查询少 JOIN,性能好 存储空间增加
代码简单,不用拼多表 一致性风险:源数据变了,冗余数据不会自己变
历史数据可追溯 更新逻辑变复杂,需同步改多处

一致性维护方式(按可靠性排序)

  1. 同一事务内同步更新 (最常用)------改区域名时,事务里同时更新 tb_emp.region_name
  2. 不冗余易变字段------只冗余"商品快照"这类天然不该变的数据
  3. 定时任务/对账脚本校准------接受最终一致
  4. 数据库触发器------能实现,但业务逻辑藏进数据库,排查困难,一般不推荐

六、本项目落地建议

当前 Region / Partner / Node 三个模块都没有做冗余字段的同步更新,具体表现:

冗余关系 隐患
tb_emp.region_name 区域改名后不同步
tb_vending_machine.region_id / partner_id / addr 点位改所属区域后,设备表不会跟着变

按项目定位选择处理方式:

练手项目

不处理也可以,知道它是冗余、知道有隐患即可。

正式项目

  • tb_emp.region_name 这类纯展示用的名称冗余 :建议去掉,改由后端返回时根据 region_id 组装,或前端查字典,彻底消灭不一致。
  • tb_vending_machine.region_id 这类用于统计和筛选的冗余:建议保留,但在"点位改区域"的 Service 里加同步更新设备的逻辑,保证同一事务内改完。

七、速查口诀

读多写少 + 值相对稳定 + 不一致可接受(或本就是历史快照)→ 可以冗余

写多、值易变、要求强一致 → 老老实实 JOIN

决策清单

设计字段前依次自问:

  • 这个值能从别的表推导出来吗?能 → 它才是"冗余"
  • 查询频率高吗?关联层级深吗?
  • 是历史快照吗?(订单/工单/流水 → 必须冗余)
  • 源字段一年改几次?频繁 → 不冗余
  • 业务能接受短暂不一致吗?不能 → 不冗余
  • 有缓存/视图/索引等替代方案吗?有 → 优先用替代方案
  • 如果决定冗余,更新逻辑在哪?是否在同一事务内同步?

八、配套注意:冗余字段与外键的关系

冗余字段和外键约束是两件事,别混淆:

  • 外键 保证"引用的 id 真实存在"(如 tb_node.region_id 指向的 tb_region 必须存在)
  • 冗余字段 是"另存的一份展示数据"(如 tb_emp.region_name),数据库不会校验它与源表是否一致

所以:

现象 原因
删除区域报 Cannot delete or update a parent row 外键 RESTRICT,子表还有引用
员工列表显示的区域名和区域表不一致 冗余字段未同步,数据库不管

删除区域时想区分是"点位"还是"员工"在拦,可在 GlobalExceptionHandler 中解析外键异常里的子表名,翻译成中文提示(参考 dkd-framework/.../GlobalExceptionHandler.java)。

相关推荐
工具派1 小时前
PDF签名贴上去为什么发虚?尺寸、DPI与坐标的边界实测
java·python·pdf
萧瑟余晖1 小时前
Spring Data综合实战与调优详解
java·后端·spring
滴啦嘟啦哒1 小时前
【具身笔记】【原理探究】一、对PI05源码的实践笔记
笔记·python·vla·具身
瑞克-桑切斯2 小时前
4、MySQL 索引与事务
数据库·mysql
yuniko-n2 小时前
【JUC】原子类 LongAdder
java
二宝哥2 小时前
【问题】本地启动springboot项目启动flowable引擎会提示数据库脚本错误,同事本地启动没有报错(MySQL表明大小写敏感参数)
数据库·mysql
John_Hui2 小时前
第 5 篇:Ubuntu 安装 Redis
数据库·redis·缓存
TDengine (老段)2 小时前
TDengine IDMP 十大实用功能(2)
大数据·数据库·chrome·时序数据库·tdengine·idmp·有问有答