#DevOps

数据库范式与反范式:什么时候该打破规则

第三范式是教科书答案,但生产系统经常故意反范式(冗余/预计算)来换性能。本文讲清 1NF/2NF/3NF/BCNF 各解决什么问题、反范式的典型场景(计数器 count 字段、宽表/读优化)、以及"先范式后反范式"的工程判断——先设计干净的范式,再按热点查询有选择地反范式。

本文要点

  • 主题聚焦:DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年2月22日·更新于 2026年7月23日

数据库范式与反范式:什么时候该打破规则

面试时被问"数据库设计遵循第几范式?",标准答案是"第三范式"。但如果你去看任何一个高并发生产系统的真实 schema,你会发现到处都是反范式:订单表里冗余了商品快照、用户表里直接存了 post_count、首页 feed 是一张预 JOIN 好的宽表。

这不是开发者不懂范式,而是他们在范式之上做了有意识的取舍。范式解决的是"数据一致性"问题,反范式解决的是"读性能"问题——两者的权衡,是每一个做后端、做架构的人绕不开的功课。

这篇文章我想把这件事讲透:范式到底解决了什么、每一级范式在消除什么异常、什么时候你应该主动反范式、反范式的代价是什么、以及最重要的——"先范式后反范式"这条工程铁律。

一、范式为什么存在

先回到问题本源:为什么需要范式?

考虑一张"反例"表,几乎所有教科书都会拿它当靶子:

text
orders_denormalized
+----+-----------+-------------+-------------+------------+-----------+----------+
| id | order_no  | customer_id | customer_nm | product_id | product_nm| qty      |
+----+-----------+-------------+-------------+------------+-----------+----------+
| 1  | A001      | C100        | 张三        | P500       | 键盘      | 2        |
| 2  | A002      | C100        | 张三        | P501       | 鼠标      | 1        |
| 3  | A003      | C101        | 李四        | P500       | 键盘      | 5        |
+----+-----------+-------------+-------------+------------+-----------+----------+

这张表把订单、客户、商品三件事揉到了一起,看起来"方便",但带出三类经典问题,统称为更新异常(Update Anomalies):

  1. 插入异常(Insertion Anomaly):如果一个新客户还没下过单,你根本没法把他的信息存进这张表——因为主键是 id(订单号),客户信息没有独立位置。
  2. 更新异常(Update Anomaly):张三改名成"张三丰",你要同时更新第 1、2 两行。漏改一行,数据就不一致了。冗余越多,更新成本越高、出错概率越大。
  3. 删除异常(Deletion Anomaly):如果把订单 3(李四唯一一单)删掉,李四这个客户就从系统里"消失"了——客户信息和订单信息耦合在一起。

范式存在的意义,就是用一组规则把这些"耦合"拆开,让每一份事实只在一个地方存储(Single Source of Truth)。这样:

  • 减少冗余:客户名只存一次,商品名只存一次。
  • 消除更新异常:改一处即可,不会漏。
  • 保证一致性:不存在"同一个客户有两个名字"的尴尬。

代价是查询时要 JOIN,写起来啰嗦,性能上也有成本。范式的整个演进史,本质就是"用 JOIN 换一致性"的过程。

二、从 1NF 到 BCNF:逐级解决什么问题

范式有 1NF、2NF、3NF、BCNF、4NF、5NF 等好几级,但工程上有意义的就前四级。每一级都针对一种特定的"依赖关系异常"。理解它们,不要背定义,要背"它在消灭什么异常"。

1NF:字段必须是原子的

第一范式要求每个字段只存原子值,不能在一个单元格里塞数组、逗号分隔的列表。

text
-- ❌ 违反 1NF
users_bad
+-----+----------+----------------------+
| id  | name     | tags                 |
+-----+----------+----------------------+
| 1   | 张三     | java,go,python       |
+-----+----------+----------------------+

-- ✅ 符合 1NF:拆出关联表
users(id, name)
user_tags(user_id, tag)

为什么不能存数组?因为你一旦想查"谁会 go",就得用 LIKE '%go%',不仅慢还会误匹配(golang 也算 go);你想给某用户加一个 tag,要读出整个字符串、改完再写回,并发场景下还会丢失更新。1NF 本质是把"集合语义"显式化,让数据库能用索引和 JOIN 来处理。

例外:PostgreSQL 的 int[]、jsonb 数组类型,从"存储"看不原子,但配合 GIN 索引仍然可查询。这是工程上的折中,不是范式失效——你仍然要清楚,把一个会频繁独立修改、需要单独查询的属性塞进数组,迟早会出事。

2NF:消除部分函数依赖(针对复合主键)

第二范式要求:在复合主键的表里,非主属性必须依赖整个主键,而不是主键的一部分。

典型例子是订单明细表:

text
-- ❌ 违反 2NF
order_items_bad
+----------+------------+------------+-----------+----------+
| order_id | product_id | product_nm | unit_price| qty      |
+----------+------------+------------+-----------+----------+
| A001     | P500       | 键盘       | 199       | 2        |
| A002     | P501       | 鼠标       | 89        | 1        |
+----------+------------+------------+-----------+----------+
主键:(order_id, product_id)

这里 product_nm 和 unit_price 只依赖 product_id,不依赖 order_id——这就是部分函数依赖。后果是:同一个商品出现在多笔订单里时,商品名和价格会被重复存储;商品改名要扫全表。

text
-- ✅ 符合 2NF:拆开
order_items(order_id, product_id, qty)           -- 主键 (order_id, product_id)
products(product_id, product_nm, unit_price)     -- 商品独立成表

注意一个细节:单价 unit_price 在范式化后只属于 products 表。但真实业务里,订单发生时的成交价和商品当前售价是两件事——前者是历史快照,不该随后者变化。这时候 order_items.unit_price 反而是必要的(它依赖整个订单明细主键,是"该笔交易的价格")。这也是后面"反范式"会反复出现的主题:快照 ≠ 冗余。

3NF:消除传递函数依赖

第三范式要求:非主属性不能依赖另一个非主属性(即不能存在 A → B → C 这种传递依赖)。

text
-- ❌ 违反 3NF
employees_bad
+-----+--------+-----------+-------------+--------------+
| id  | name   | dept_id   | dept_name   | dept_location|
+-----+--------+-----------+-------------+--------------+
| 1   | 张三   | D01       | 工程部      | 北京         |
| 2   | 李四   | D01       | 工程部      | 北京         |
+-----+--------+-----------+-------------+--------------+
依赖关系:id → dept_id → dept_name, dept_location

这里 dept_name 和 dept_location 依赖的是 dept_id,而 dept_id 又依赖 id(员工主键),形成传递依赖。后果:工程部改名或搬迁,要更新所有该部门员工行。

text
-- ✅ 符合 3NF:部门独立成表
employees(id, name, dept_id)
departments(dept_id, dept_name, dept_location)

3NF 是绝大多数业务系统的设计终点。它有个等价说法——"非主属性既不部分依赖也不传递依赖于候选键",这条原则被 SQL Boy 们口口相传为"每个事实只出现一次"。

BCNF:3NF 的加强版(处理主属性内部的依赖)

BCNF(Boyce-Codd Normal Form)比 3NF 更严格:所有非平凡函数依赖 X → Y 中,X 必须是超键(候选键的超集)。3NF 允许 Y 是主属性时例外,BCNF 不允许。

经典例子是"一课一师,一师一课"的约束:

text
-- ❌ 满足 3NF,但违反 BCNF
course_teacher_bad
+----------+-----------+------------+
| course   | teacher   | room       |
+----------+-----------+------------+
| 数学     | 张老师    | R101       |
| 物理     | 李老师    | R102       |
+----------+-----------+------------+
候选键:(course, teacher)、(course, room)
约束:teacher → room(每个老师固定在某个教室上课)

这里 teacher → room,但 teacher 不是候选键,所以违反 BCNF。问题在于:如果张老师改到 R201,要更新所有他教的课的行。BCNF 的解法是把表拆成 (course, teacher) 和 (teacher, room)。

工程上 BCNF 比较少见,因为它要求你承认"主属性之间也有依赖",这通常意味着业务约束比较特殊。多数团队把"3NF + 业务约束用 UNIQUE/CK 实现"当作事实上的终点。

一张表总结各级范式

级别解决的问题一句话口诀
1NF字段不原子别把数组塞一个格子
2NF部分函数依赖(复合主键)非主属性要依赖整个主键
3NF传递函数依赖非主属性不能依赖另一个非主属性
BCNF主属性间依赖决定因素必须是候选键

三、反范式:什么时候故意冗余

如果你的 schema 完美符合 3NF,恭喜你,数据很干净。但上线一周后产品经理提了两个需求:首页要展示"每个用户发了多少篇文章",订单列表要展示"用户名 + 商品名 + 单价 + 数量"。你一查慢查询日志,发现都是 COUNT(*) 和四表 JOIN 把数据库 CPU 打满了。

这时候你要做反范式(Denormalization)——故意引入冗余,用空间换时间,用一致性成本换读性能。下面是几个高频场景。

场景 1:计数器字段(count 字段)

users 表上直接挂一个 post_count、follower_count、like_count:

sql
CREATE TABLE users (
  id            BIGINT PRIMARY KEY,
  name          VARCHAR(64),
  email         VARCHAR(128),
  post_count    BIGINT DEFAULT 0,   -- 反范式:冗余计数
  follower_count BIGINT DEFAULT 0,
  like_sum      BIGINT DEFAULT 0
);

范式版本应该是 SELECT COUNT(*) FROM posts WHERE user_id = ?,但首页要展示 100 个用户的发帖数,就是 100 次聚合查询,性能爆炸。冗余一个 post_count 字段,读变成 O(1) 的列读取。

代价:每次发帖/删帖,要同步更新 users.post_count。高并发下 UPDATE users SET post_count = post_count + 1 会让这一行成为热点(行锁竞争)。生产做法:

  • 用应用层事务包住"插入 post + 更新 count"两步;
  • 用 Redis 做缓冲,定时刷回数据库(牺牲强一致,换吞吐);
  • 或者把计数单独拆到一张 user_counters(user_id, counter_name, value) 表,避免污染主表。

场景 2:宽表预 JOIN(读多写少)

订单列表是反范式重灾区。范式版本需要四表 JOIN:

sql
-- 范式版本
SELECT o.id, o.order_no, o.created_at,
       c.name AS customer_name,
       p.name AS product_name, oi.qty, oi.unit_price
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.created_at > NOW() - INTERVAL '7 days';

JOIN 在小表上很便宜,但订单数据量一大,优化器会选择 Hash Join 或 Nested Loop,磁盘 IO 和 CPU 都会爆。反范式做法是把读路径直接拍平成一张宽表:

sql
CREATE TABLE order_list_wide (
  order_id      BIGINT,
  order_no      VARCHAR(32),
  created_at    TIMESTAMP,
  customer_id   BIGINT,
  customer_name VARCHAR(64),   -- 反范式:冗余用户名快照
  product_id    BIGINT,
  product_name  VARCHAR(128),  -- 反范式:冗余商品名快照
  qty           INT,
  unit_price    DECIMAL(10,2)  -- 反范式:成交价快照
);

读变成 SELECT * FROM order_list_wide WHERE created_at > ...,一次扫描搞定。代价是写入时要维护这张表,通常用 ETL / Canal 监听 binlog / 定时任务刷新。

注意:宽表里冗余的是快照(下单那一刻的用户名、商品名、单价),而不是"当前值"。这是个微妙但重要的区分——订单历史不该因为用户改名而变化,所以这里的冗余反而是业务正确性要求的,不仅仅是性能优化。

场景 3:读多写少的预聚合

各种报表、榜单、首页 Feed 都属于这一类。比如"过去 24 小时每分类销量 TOP10",范式版要 GROUP BY category, product_id ORDER BY sum(qty) DESC,跑一次几秒。反范式做法是定时任务每 5 分钟跑一次,结果写到 category_hot_products(category, product_id, sold_qty, updated_at),前端读这张表,毫秒级返回。

场景 4:跨服务/跨库的副本

微服务架构下,订单服务需要展示商品名,但商品库在商品服务里。直接跨库 JOIN 是反模式。常见做法是在订单库里冗余一份 product_snapshot(product_id, name, price, ...),由商品服务发事件、订单服务消费更新。这本质上也是反范式——为了服务自治,故意让数据有多份副本。

四、反范式的代价

反范式不是免费午餐,它把你从"读"的麻烦挪到了"写"和"一致性"上。在你决定冗余一个字段之前,先想清楚下面四笔账。

代价 1:更新要写多个地方

post_count 加一,不再是 INSERT posts 一条语句,而是 INSERT posts + UPDATE users 两步。这两步要么在同一个事务里(强一致,但有锁开销),要么接受最终一致(异步、消息队列)。代码复杂度立刻上升。

代价 2:一致性维护要选模型

  • 强一致:数据库事务包裹,所有冗余一起成功/一起失败。简单但慢,热点行会拖垮吞吐。
  • 触发器:数据库层自动同步,开发者省心,但触发器难以调试、难以跨库、容易成为隐藏的"魔法"。
  • 应用层同步:代码里显式更新多处,灵活可测试,但容易漏写(尤其是删除/软删场景)。
  • CDC / 消息队列:监听 binlog 异步广播,解耦最好但引入最终一致延迟和幂等性挑战。

没有银弹。计数器字段我倾向应用层事务或 Redis 缓冲;跨服务副本必须用 CDC;宽表用定时刷新就够。

代价 3:触发器是隐藏的技术债

sql
-- 看起来很优雅
CREATE TRIGGER sync_post_count
AFTER INSERT ON posts
FOR EACH ROW
BEGIN
  UPDATE users SET post_count = post_count + 1 WHERE id = NEW.user_id;
END;

但触发器有几个坑:1) 难以调试,出问题不看日志根本不知道;2) 性能开销隐藏在写入路径里,容易被忽视;3) 数据迁移、批量导入时触发器可能误触发或成为瓶颈;4) 跨库根本没法用。我的建议是触发器只在数据完整性约束上用(比如审计表),不用来做反范式同步——那种事让应用层或 CDC 显式做。

代价 4:冗余字段越多,schema 越难演进

加字段还好,改语义就痛苦了。比如 post_count 当初只算"原创",现在产品要改成"原创 + 转发",你需要写一次性数据回填脚本,可能还要停服。冗余越多,这种迁移越痛。

一句话总结代价:反范式把"读时的复杂"换成了"写时的复杂和运维时的复杂"。读的复杂数据库优化器替你扛,写的复杂只能你自己扛。

五、先范式,后反范式

讲了这么多,那么到底该怎么设计?我的结论是一句工程铁律:

先做干净的 3NF,再按真实热点查询有选择地反范式。

这条原则反过来的意思更重要:不要一开始就反范式。

很多新手或"性能焦虑型"开发者,一上来就把表设计成宽表,理由是"反正以后查询要 JOIN,干脆先冗余了"。这是错的,原因有三:

  1. 你不知道热点在哪。没上线、没流量数据,你猜的"热点查询"很可能不是真的热点。先把范式做干净,上线后用 APM / 慢查询日志看清楚哪几个查询真的慢、真的频繁,再针对它们反范式。这叫数据驱动优化,不是拍脑袋。
  2. 范式是退路。一旦反范式出问题(一致性问题、迁移困难),干净的范式设计让你随时能回到 SSOT。一开始就反范式,等于烧掉退路。
  3. 范式化设计本身性能不会差太多。现代数据库优化器很强,索引设计合理的情况下,三表 JOIN 在百万级数据量下毫秒级返回。很多"JOIN 慢"其实是因为没建索引、没分区、没分库,不是范式本身的锅。

标准流程

我推荐的实操流程:

  1. 设计阶段:严格按 3NF 拆表。每一类实体一张表,关系用外键关联。画出 ER 图,让团队 review。
  2. 上线观察:APM 收集真实的慢查询,统计每条 SQL 的调用频率、平均耗时、P99。
  3. 识别热点:找出 Top 10 性能瓶颈。大概率是几条 COUNT、几条多表 JOIN、几条 ORDER BY + LIMIT。
  4. 针对性反范式:每个热点引入一个反范式手段(计数器 / 宽表 / 预聚合),并明确同步策略(事务 / CDC / 定时刷新)。
  5. 加监控:每个冗余字段配一致性校验任务(比如每天对一次 post_count 和真实 COUNT(*),差异超阈值告警)。

举个真实例子:我之前服务过一个内容平台,最初设计是干净的 3NF(users / posts / comments / likes),上线后慢查询排名第一的是首页 feed 的"用户头像 + 文章摘要 + 点赞数 + 评论数"四表 JOIN + 两次 COUNT。我们没有推翻原设计,而是新增了一张 post_stats(post_id, like_count, comment_count, ...) 表,由点赞/评论服务通过消息队列异步维护,feed 查询从 800ms 降到 30ms。原始 posts 表依然干净,反范式是叠加层的、可丢弃的。

这个思路的核心是:把反范式当作"读优化层",而不是"基础设计"。基础是范式,优化是反范式,两层分开,演进和回滚都容易。

六、物化视图与缓存层:结构化反范式的工程手段

反范式如果到处乱撒——这里加个字段、那里加张表——很快就会失控。工程上的成熟做法是用结构化的机制承载反范式,主要手段是物化视图和缓存层。

物化视图(Materialized View)

PostgreSQL、Oracle、SQL Server 都原生支持物化视图——它本质是一张"由查询定义、被持久化存储"的表,可以定期 REFRESH。

sql
-- PostgreSQL:创建每分类销量榜的物化视图
CREATE MATERIALIZED VIEW category_sales_mv AS
SELECT c.name AS category_name,
       p.id   AS product_id,
       p.name AS product_name,
       SUM(oi.qty) AS total_qty
FROM order_items oi
JOIN products p ON p.id = oi.product_id
JOIN categories c ON c.id = p.category_id
GROUP BY c.name, p.id, p.name;

CREATE UNIQUE INDEX ON category_sales_mv(product_id);

-- 定时刷新(REFRESH CONCURRENTLY 不阻塞读)
REFRESH MATERIALIZED VIEW CONCURRENTLY category_sales_mv;

物化视图的好处是声明式:你描述"我要什么样的冗余数据",刷新机制由数据库负责,比手写 ETL 干净得多。代价是刷新有延迟(不是实时),且部分数据库(如 PG)的物化视图不能自动增量刷新,全量刷新在大表上很贵。

MySQL 没有原生物化视图,业内常用 Flexviews 或自己用定时任务 + INSERT ... SELECT 模拟。或者干脆把这套逻辑放到数仓里(ClickHouse / Doris)做。

缓存层

Redis、Memcached 是另一条反范式通道——把数据库的查询结果或预计算结果缓存起来,读路径优先打缓存。这本质上也是"用冗余换性能",只不过冗余的位置从"数据库里的另一张表"换成了"另一个进程的内存"。

text
读路径:  请求 → Redis(post_stats)→ 命中返回
                              ↓ miss
                  MySQL(posts JOIN likes JOIN comments)
                              ↓ 回填
                          Redis(带 TTL)

缓存层和物化视图的关键区别:

  • 位置:物化视图在数据库内,缓存在外部进程。
  • 一致性:物化视图由数据库刷新,相对内聚;缓存需要在应用层显式维护(写时失效 / 读时回填),容易出现缓存与数据库不一致的经典难题。
  • 粒度:物化视图适合聚合查询,缓存适合单对象查询。
  • 持久性:物化视图持久化到磁盘,缓存通常易失(重启要预热)。

实战里两者经常配合:物化视图承载重聚合,Redis 承载热点单 key。比如 feed 列表用物化视图,每条 post 的实时计数用 Redis(因为变更太频繁,写库扛不住)。

CQRS:反范式的极致形态

把"读模型和写模型分离"做到极致就是 CQRS(Command Query Responsibility Segregation):写侧是干净的 3NF(保证一致性),读侧是一系列针对查询优化的反范式投影(宽表、列存、搜索引擎)。两者通过事件总线同步。

这是大型系统处理复杂读路径的终极方案,也是反范式思想的系统化应用。代价是架构复杂度——只有当你的读路径真的复杂到值得这套设施时才上,否则杀鸡用牛刀。

七、NoSQL 的"天然反范式"

讲完关系数据库,回头看 NoSQL。文档数据库(MongoDB)、键值数据库(DynamoDB)、宽列存储(Cassandra)这些设计哲学跟关系库完全相反——它们从根上就是反范式的,鼓励你把相关数据塞到一条记录里。

文档数据库的 embed 设计

MongoDB 经典的"博客 + 评论"建模:

javascript
// 范式派(MongoDB 不推荐)
db.posts.insertOne({ _id: 1, title: "Hello", author_id: 42 })
db.comments.insertOne({ post_id: 1, user_id: 7, text: "不错" })

// 反范式派(MongoDB 推荐)
db.posts.insertOne({
  _id: 1,
  title: "Hello",
  author: { id: 42, name: "张三", avatar: "..." },  // embed 冗余
  comments: [
    { user_id: 7, user_name: "李四", text: "不错", created_at: ... },
    { user_id: 8, user_name: "王五", text: "+1",   created_at: ... }
  ],
  comment_count: 2  // 冗余计数
})

一条文档就把整篇博客、作者信息、所有评论、评论数都装下了,读路径一次查询拿全部数据,不用 JOIN。MongoDB 甚至压根不支持跨集合 JOIN(后来的 $lookup 是补丁,性能差)。

为什么 NoSQL 天然反范式?三个原因:

  1. 分布式架构:跨节点 JOIN 极其昂贵,干脆不让你做。把相关数据放同一条记录、同一个分片,是分布式友好的唯一选择。
  2. CAP 取舍:NoSQL 普遍偏好 AP(高可用 + 分区容忍),牺牲强一致。这种语境下"通过外键维护一致性"的前提就不成立,反范式的额外一致性成本反而没那么突出。
  3. 读多写少的互联网场景:大部分互联网业务(内容、社交、电商商品展示)都是读远多于写,反范式的"读优化"红利巨大。

反范式代价在 NoSQL 里同样存在

但 NoSQL 的反范式不是没有代价,反而一致性维护更麻烦。DynamoDB 的官方文档花了大篇幅讲"如何处理 embed 数据的更新"——比如评论数组变大(超过 400KB 限制)怎么办、作者改名时怎么找到所有引用他的文档批量更新。

MongoDB 的 16MB 文档大小限制、嵌套数组的查询限制,都会逼你在"embed"和"reference"之间反复权衡。业内经验:

  • 一对少(1-few,< 100)且一起读:embed。比如博客评论、订单明细。
  • 一对多(1-many,> 1000)或独立查询:reference。比如用户的全部订单。
  • 多对多:必须 reference,且通常维护双向引用(冗余 ID 列表)。
  • 频繁更新的属性:reference,避免 embed 后大量文档要更新。

这条经验其实和关系库的"先范式后反范式"是一致的——默认 reference(范式),只在读路径真的需要时 embed(反范式)。换了个名字,底层的工程智慧是相通的。

关系库 vs 文档库:范式光谱

维度关系库(3NF)文档库(embed)
默认设计范式反范式
一致性强(外键、事务)最终一致(应用层维护)
读性能JOIN,需要优化单文档查询,快
写性能多表写,事务开销单文档原子,快
演进成本schema 迁移重schema-less 灵活但易腐坏
适用场景复杂关系、强一致读多写少、文档化、灵活 schema

选型时不要被"快"或"灵活"绑架。如果你的业务核心是资金、订单、库存这种强一致场景,老老实实用关系库 + 3NF;如果是内容、Feed、用户画像这种读多写少、能容忍最终一致的场景,文档库的反范式设计会让你舒服得多。

小结

  1. 范式是为了消除更新异常:1NF 治字段不原子,2NF 治部分依赖,3NF 治传递依赖,BCNF 治主属性间依赖。每一级都在消灭一种"事实存了多份"的耦合,把 Single Source of Truth 做实。

  2. 反范式是为了读性能:计数器字段、宽表预 JOIN、预聚合、跨服务副本——四种典型场景,本质都是"用冗余换 JOIN"。代价是写复杂度、一致性维护、schema 演进成本都上升。

  3. 反范式的同步方式按一致性需求选:强一致走事务、解耦走 CDC、跨服务走事件、低频走定时刷新。触发器能不用就不用。

  4. 物化视图和缓存层是结构化的反范式:把零散的冗余升级为工程化设施,可监控、可演进。读路径复杂到一定程度,CQRS 是终极形态。

  5. 先范式后反范式是铁律:上线前做干净的 3NF,上线后用真实慢查询数据驱动反范式。把反范式当作读优化层、不是基础设计——基础丢了就回不去,优化层可以随时拆掉重建。

金句:范式是给数据的一致性上的保险,反范式是给性能交的过路费——先买全险上路,跑顺了再决定哪段可以省掉保险飙车。

Related

相关推荐