数据库字段冗余,真的只是用空间换时间吗?

这两天实习的时候,研究了一下测试库的表,发现很多表都是同时记录部门代码部门名称字段的。

对于这一类问题,我过去的理解一直很模糊。因为此前考虑时,只粗糙地思考过两种情况:

  1. 只记录 部门代码。节省存储空间,但是查询时需要联表查询。

  2. 同时记录部门代码部门名称,查询性能好,但是空间占用略大。

就是这么的粗糙简单......

也就是说,我一直把它理解成一个简单的 空间与时间 的选择问题。

但仔细看这些表后发现,它们大多是:

  • 收货单
  • 结算单
  • 采购单
  • 业务流水

它们记录的是已经发生的业务事实,也就是一种 业务快照

一、关键不是 JOIN,而是"这个值会不会随时间变化"

假设部门信息是:

复制代码
部门代码:1001
部门名称:采购部

后来部门改名:

复制代码
部门代码:1001
部门名称:供应链采购部

现在有两种场景。

员工表

员工表只保存:

复制代码
employee.department_id

查询员工当前部门时 JOIN:

csharp 复制代码
select e.name, d.name
from employee e
left join department d
    on e.department_id = d.id;

这是合理的。

因为我们关心的是:

这个员工现在属于什么部门?

部门改名以后,员工页面也应该显示新名称。

所以这里:

复制代码
部门名称 = 当前属性

适合 JOIN。


收货单

假设一张历史收货单生成时:

复制代码
部门代码:1001
部门名称:采购部

几年后部门改名。

如果收货单只保存部门代码,再 JOIN 当前部门表,那么历史单据会变成:

复制代码
部门名称:供应链采购部

这就改变了历史。

因此这类表同时保存:

复制代码
department_code
department_name

是合理的。

这里的 department_name 表示:

单据生成那一刻的部门名称。

它不是普通字段冗余,而是 历史快照


所以核心不是:

这个字段能不能从其他表查出来?

而是:

我要的是当前状态,还是当时的状态?


二、派生字段要不要存?

另一种容易纠结的是:

ini 复制代码
amount = qty * price
age = today - birthday
profit = revenue - cost

它们都可以通过其他字段计算出来。

但"能算出来"并不意味着"一定不存"。


1. 可以稳定重算:通常不存

例如:

ini 复制代码
age = today - birthday
year = YEAR(order_date)

只要输入确定,结果就可以随时重新得到。

特别是年龄、逾期天数这种数据:

即使数据库没有发生任何修改,它也会随着时间失效。

所以应该保存:

复制代码
birthday
due_date

而不是:

复制代码
age
overdue_days

2. 最终业务结果:应该存

假设:

ini 复制代码
qty = 3
price = 9.99

理论金额:

复制代码
29.97

但由于:

  • 折扣
  • 优惠
  • 税费
  • 尾差
  • 人工调整

最终结算金额可能是:

复制代码
29.96

这时 29.96 已经不是简单的计算值,而是:

最终确认的业务结果。

这种数据应该保存。

例如:

复制代码
实际结算金额
最终成交金额
人工调整后的金额
绩效最终得分

所以:

能计算出来 ≠ 不应该保存。


3、可以用"重算测试"

面对一个派生字段 X,可以问:

如果把 X 删除,以后还能不能根据其他数据 100% 重建出相同结果?

能,而且计算简单

例如:

ini 复制代码
year = YEAR(order_date)

→ 不存。

能,但计算很贵

例如:

scss 复制代码
用户累计消费 = SUM(历史订单)

每次都扫描几十万条订单不现实。

→ 可以保存为 缓存 / 预计算字段

以后未必能重建

例如绩效算法发生变化:

ini 复制代码
2025:
score = sales * 0.7 + rating * 0.3

2026:
score = sales * 0.5 + rating * 0.5

如果保存的是:

yaml 复制代码
2025 年最终绩效

→ 应该存。

根本无法重建

例如金额经过人工调整。

→ 必须存。


三、当冗余只是为了性能

例如:

markdown 复制代码
article
-------
id
author_id

用户昵称在:

markdown 复制代码
user
----
id
nickname

正常来说应该 JOIN。

但为了查询性能,也可以复制:

css 复制代码
article.author_nickname

但是这里和业务快照完全不同。

用户昵称修改以后:

复制代码
Tom → Thomas

文章中的昵称也必须同步修改,当用户有几十万篇文章,那简直是灾难.....


这时,真正要权衡的其实不是"多存一个字段占多少空间",而是:

少一次 JOIN,值不值得换来额外的数据同步成本?

还是以 article.author_nickname 为例。

假设用户把昵称从 Tom 改成 Thomas

如果文章表只保存:

复制代码
author_id

那么只需要修改一次:

ini 复制代码
update user
set nickname = 'Thomas'
where id = 1;

之后所有文章通过 JOIN 都能立即得到最新昵称。

但如果为了查询性能,在文章表中冗余了:

复制代码
author_nickname

事情就变成了:

css 复制代码
user.nickname 修改
        ↓
找到这个用户的所有文章
        ↓
同步 article.author_nickname

如果这个用户只有 10 篇文章,更新 10 行可能无所谓。

但如果有 100 万条内容,一次改昵称就可能产生 100 万行更新。此时不仅要考虑 UPDATE 本身,还有日志、复制、IO,以及同步失败后的数据不一致。

不过反过来,如果这个字段:

复制代码
每天读取几亿次
一年只修改几次

而且实际压测已经证明 JOIN 是主要瓶颈,那么用一次偶尔发生的批量同步,换取大量查询的性能提升,也可能是值得的。

甚至还可以接受短暂的不一致:

ini 复制代码
user.nickname    = Thomas
article.nickname = Tom

通过异步任务几秒后再同步完成。

所以,性能冗余更适合这样的数据:

sql 复制代码
读非常多
改非常少
允许短暂不一致
JOIN 确实已经成为瓶颈

而如果只是因为:

"感觉 JOIN 会慢。"

就提前把名称复制到各张表里,通常只是在用未来的数据一致性问题,解决一个还没有发生的性能问题。

在真正冗余字段之前,更合理的顺序通常是:

sql 复制代码
先确认 JOIN 是否真的慢
        ↓
检查索引和 SQL
        ↓
考虑缓存或批量查询
        ↓
仍然存在明确瓶颈
        ↓
再考虑反范式化

因此,性能冗余应该是一种有数据依据的优化,而不是数据库设计时的默认选择。


总结

现在再看"一个字段该不该多存一份",我更倾向于先判断它的业务语义,而不是先想 JOIN 和存储空间。

简单来说:

  • 当前状态:优先 JOIN,保持单一数据源。
  • 历史事实:保存快照,避免未来修改影响历史。
  • 派生结果:能稳定、低成本重算就不存;已经成为最终业务结果则保存。
  • 性能冗余:只有确认 JOIN 确实是瓶颈后再做,并接受随之而来的一致性成本。

所以,数据库里的"冗余"并不一定是不合理设计。

真正需要搞清楚的是:

这个字段为什么要存,以及哪份数据才是真正的数据源。


欢迎讨论、补充、纠正

相关推荐
沉下去,苦磨练!1 小时前
Human‑in‑the‑Loop
数据库·oracle
吠品1 小时前
Java中实现拓扑排序的两种方式:Kahn算法和DFS
java·数据库·notepad++
轻揉小乔 真新人1 小时前
数据库架构的升级和变更
数据库·数据库架构
禁默2 小时前
信创实战:Python + SQLAlchemy 接入金仓数据库(从驱动安装到完整 CRUD)
开发语言·数据库·python
范什么特西2 小时前
知识总结04(redis)
数据库·redis·缓存
小黑技术栈2 小时前
web前端基础到入门——14day
前端·数据库·oracle
福大大架构师每日一题3 小时前
agno v2.8.7 发布:顾问模型、精准路线、调度能力全面升级,10项关键修复一次看懂
java·开发语言·数据库
深念Y3 小时前
老Flyme 官方 root 开启 ADB TCP 方案
linux·数据库·网络协议·tcp/ip·adb·智能手机·emmc
c2385611 小时前
MySQL 基础用法(上):库表管理与数据增删改
c语言·数据库·c++·mysql