规范化与反规范化决策
High Contrast
Dark Mode
Light Mode
Sepia
Forest
3 min read567 words

规范化与反规范化决策

数据库设计没有绝对的对错,只有适合当前场景的权衡。规范化(Normalization)减少冗余、保证一致性;反规范化(Denormalization)牺牲一些冗余换取查询性能。了解何时该用哪种策略,是区分经验工程师和初级工程师的重要标志。


为什么要规范化

规范化的目标是消除数据冗余,防止更新异常(Update Anomaly)。

未规范化的问题

-- ❌ 未规范化:用户地址存在 orders 表中
CREATE TABLE orders (
id          BIGSERIAL PRIMARY KEY,
user_id     BIGINT,
user_name   TEXT,          -- 冗余:用户改名,历史订单 user_name 不一致
user_email  TEXT,          -- 冗余:同上
city        TEXT,          -- 问题:是下单时的地址还是当前地址?
address     TEXT,
total       NUMERIC(10, 2)
);

第三范式(3NF)后的设计

-- ✅ 规范化:用户信息在 users 表
-- 订单在 orders 表(只存 user_id 外键)
-- 收货地址独立为 addresses 表(历史快照)
CREATE TABLE users (
id    BIGSERIAL PRIMARY KEY,
name  TEXT NOT NULL,
email TEXT NOT NULL UNIQUE
);
CREATE TABLE addresses (
id       BIGSERIAL PRIMARY KEY,
user_id  BIGINT NOT NULL REFERENCES users(id),
city     TEXT NOT NULL,
address  TEXT NOT NULL,
is_default BOOLEAN DEFAULT false
);
CREATE TABLE orders (
id                BIGSERIAL PRIMARY KEY,
user_id           BIGINT NOT NULL REFERENCES users(id),
shipping_address_snapshot  JSONB,  -- 下单时的地址快照(见后)
total             NUMERIC(12, 2) NOT NULL
);

快照模式:规范化 + 历史记录

某些数据需要"当时的状态",而不是"当前的状态"。典型场景:订单的商品价格、收货地址。

-- 订单明细:存下单时的价格(不能只存 product_id,价格可能变化)
CREATE TABLE order_items (
id           BIGSERIAL PRIMARY KEY,
order_id     BIGINT NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id   BIGINT NOT NULL REFERENCES products(id),
product_name TEXT NOT NULL,            -- ← 快照:下单时的商品名
unit_price   NUMERIC(10, 2) NOT NULL,  -- ← 快照:下单时的价格
quantity     INTEGER NOT NULL
);
-- 订单:存下单时的收货地址快照
CREATE TABLE orders (
id               BIGSERIAL PRIMARY KEY,
user_id          BIGINT NOT NULL REFERENCES users(id),
shipping_address JSONB NOT NULL,        -- ← 地址快照
total            NUMERIC(12, 2) NOT NULL,
created_at       TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- shipping_address 的格式:
-- {
--   "name": "Alice",
--   "phone": "+60-12-345-6789",
--   "line1": "123 Jalan Bukit Bintang",
--   "city": "Kuala Lumpur",
--   "postcode": "50450",
--   "state": "Kuala Lumpur"
-- }

何时考虑反规范化

反规范化是一种有意识的权衡,为了提升读性能而接受一定的数据冗余。常见场景:

场景一:统计计数字段

-- ❌ 每次查 follower 数都要 COUNT(*)(慢)
SELECT COUNT(*) FROM followers WHERE user_id = 123;
-- ✅ 反规范化:在 users 表增加计数列
ALTER TABLE users ADD COLUMN follower_count INTEGER NOT NULL DEFAULT 0;
-- 用触发器保持同步
CREATE OR REPLACE FUNCTION update_follower_count()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE users SET follower_count = follower_count + 1 WHERE id = NEW.user_id;
ELSIF TG_OP = 'DELETE' THEN
UPDATE users SET follower_count = follower_count - 1 WHERE id = OLD.user_id;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER maintain_follower_count
AFTER INSERT OR DELETE ON followers
FOR EACH ROW EXECUTE FUNCTION update_follower_count();

场景二:预聚合查询

-- ❌ 每次加载用户主页都聚合:
SELECT SUM(amount) FROM orders WHERE user_id = 123 AND status = 'paid';
-- ✅ 物化视图(PostgreSQL 原生支持)
CREATE MATERIALIZED VIEW user_order_stats AS
SELECT
user_id,
COUNT(*) AS total_orders,
SUM(total_amount) AS total_spent,
MAX(created_at) AS last_order_at
FROM orders
WHERE status = 'paid'
GROUP BY user_id;
CREATE INDEX ON user_order_stats (user_id);
-- 定期刷新(或在事务后刷新)
REFRESH MATERIALIZED VIEW CONCURRENTLY user_order_stats;

场景三:避免深层 JOIN

-- ❌ 每次查评论都要 JOIN users 获取作者名
SELECT c.content, u.name AS author_name
FROM comments c
JOIN users u ON c.user_id = u.id;
-- ✅ 在 comments 表冗余存储 author_name(接受用户改名后历史评论不同步)
CREATE TABLE comments (
id          BIGSERIAL PRIMARY KEY,
post_id     BIGINT NOT NULL REFERENCES posts(id),
user_id     BIGINT NOT NULL REFERENCES users(id),
author_name TEXT NOT NULL,   -- ← 冗余,但避免了 JOIN
content     TEXT NOT NULL,
created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

反规范化决策框架

graph TD A["某个查询很慢"] --> B{"是否频繁执行?"} B --> |否| C["先优化索引\n(第 03 章)"] B --> |是| D{"是聚合/统计?"} D --> |是| E["考虑物化视图\n或冗余计数列"] D --> |否| F{"是深层 JOIN?"} F --> |是| G["考虑冗余列\n(接受更新复杂性)"] F --> |否| H["分析 EXPLAIN ANALYZE\n(第 03 章)"] E --> I["评估:更新频率 vs 读取频率\n更新多 → 物化视图定期刷新\n写少读多 → 触发器实时维护"] style C fill:#e8f5e9,stroke:#2e7d32 style I fill:#fff3e0,stroke:#ef6c00

规范化 vs 反规范化 原则总结

场景 推荐策略
数据完整性优先(金融、用户信息) 3NF 规范化
历史时间点数据(订单价格/地址) 快照模式(冗余 + JSONB)
高频统计(帖子点赞数、关注数) 冗余计数列 + 触发器
复杂聚合报表 物化视图
避免高频 JOIN 的读密集查询 谨慎冗余(明确接受更新复杂性)

本节记录清单


下一章索引策略与查询优化——Schema 设计好了,现在学如何用索引让查询飞起来——以及如何用 EXPLAIN ANALYZE 找到真正的性能瓶颈。