乐途乐途
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
    • HTTP协议
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • MyBatis
    • Spring
    • Spring MVC
    • Maven 入门
    • Maven 进阶
    • Java 设计模式
  • 缓存

    • Redis
联系
阿里云
  • 学习路径
  • 第1章 数据库基础与安装

    • MySQL 简介
    • MySQL 5.6 到 5.7 到 8.0 关键差异速查
    • 安装 MySQL 5.7
    • 连接与断开服务器
    • 创建数据库
    • 创建数据表
    • 数据库与数据表
    • 加载数据
    • 获取数据库信息
    • 批处理模式
    • SHOW 语句汇总
    • FLUSH 与 RESET 语句
    • my.cnf 核心参数
    • 字符集与排序规则
  • 第2章 SQL基础查询

    • SELECT
    • WHERE
    • ORDER BY
    • LIMIT
    • COUNT
    • 聚合函数
    • 比较运算符
    • 逻辑运算符
    • 算术运算符
    • 模式匹配
    • NULL 值处理
    • UPDATE
    • DELETE
    • REPLACE
    • SELECT INTO
  • 第3章 数据类型与运算符

    • 数值类型
    • 字符串类型
    • 日期时间类型
    • BIT 类型
    • ENUM 类型
    • SET 类型
    • JSON 类型
    • 类型转换
  • 第4章 函数与表达式

    • 字符串函数
    • 数值函数
    • 日期函数
    • 全文检索函数
  • 第5章 高级查询与子查询

    • JOIN
    • 子查询
    • UNION
    • GROUP BY
    • HAVING
    • DISTINCT
  • 第6章 表与索引

    • 数据定义语言
    • 修改表结构
    • 视图
    • 修改视图与检查选项
    • 外键
    • 索引
    • 唯一索引
    • 复合索引
    • 存储引擎对比
    • 分区表
    • 第一范式与第二范式
    • 第三范式与 BC 范式
    • 反范式设计
  • 第7章 存储过程与函数

    • 存储过程
    • 存储函数
    • 变量
    • 流程控制
    • 游标
    • 预处理语句
  • 第8章 事务与锁

    • 事务
    • 事务隔离级别
    • 锁机制
    • MVCC
    • 死锁专题分析
    • LOCK TABLES
    • XA 事务
  • 第9章 用户管理与安全

    • 用户管理
    • 权限管理
    • 角色
    • SQL 注入防范
  • 第10章 性能优化入门

    • 执行计划
    • 索引优化
    • 查询优化
    • 查询优化器提示
    • 慢查询日志
    • InnoDB 深入机制
    • InnoDB 专项优化
    • Performance Schema
    • sys Schema
  • 第11章 复制与高可用

    • 主从复制原理
    • 半同步复制配置
    • binlog 开启与 point-in-time 恢复
    • mysqldump 全库备份
    • mysqldump 单表与条件备份
    • mysqldump 恢复与导入
    • xtrabackup 全量热备
    • xtrabackup 准备与恢复
    • xtrabackup 增量与流式备份
  • 第12章 触发器与事件

    • 触发器
    • 事件调度器
  • 参考

    • MySQL 5.7 专业术语大全
    • MySQL 5.7 关键字与保留字大全

反范式设计

导学

范式化让表结构整洁、无冗余,但代价是查询时需要大量 JOIN。当数据量达到千万级,多表 JOIN 的性能可能无法接受。反范式设计(Denormalization)就是有意引入可控冗余,用空间换时间,让关键查询只需访问一张表。

定义

反范式设计:在已经满足范式要求的基础上,有意引入冗余数据、合并表或创建汇总字段,以减少查询时的 JOIN 次数、提升读取性能的设计策略。

场景一:合并表减少 JOIN

当前数据状态:范式化设计的员工和部门信息。

CREATE TABLE departments (
    dept_id INT PRIMARY KEY,
    dept_name VARCHAR(20)
);

CREATE TABLE employees (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    dept_id INT,
    FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);

INSERT INTO departments VALUES (10, '技术部'), (20, '产品部');
INSERT INTO employees VALUES (1, '大翔', 10), (2, '白歌', 20);

查询员工及其部门名称(需要 JOIN):

SELECT e.emp_name, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.dept_id;
emp_namedept_name
大翔技术部
白歌产品部

问题:如果这是一个每秒查询 10 万次的接口,每次 JOIN 都有开销。部门名称很少变动,是否可以冗余存储?

反范式设计:在员工表中冗余部门名称

操作语句:

-- 反范式化后的员工表
CREATE TABLE employees_denorm (
    emp_id INT PRIMARY KEY,
    emp_name VARCHAR(20),
    dept_id INT,
    dept_name VARCHAR(20)   -- 冗余:直接从 departments 复制
);

INSERT INTO employees_denorm VALUES
(1, '大翔', 10, '技术部'),
(2, '白歌', 20, '产品部');

-- 查询不再需要 JOIN
SELECT emp_name, dept_name FROM employees_denorm;

操作后数据状态:

emp_namedept_name
大翔技术部
白歌产品部

结果解读:

  • 查询从 JOIN 变为单表查询,性能提升明显
  • 代价:部门改名时需要更新 employees_denorm 表中所有相关行
  • 适用场景:读多写少、部门名称变更极少的业务(如电商订单中冗余商品名称)

场景二:添加汇总字段减少聚合计算

当前数据状态:订单和订单明细表。

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    user_id INT,
    order_time DATETIME
);

CREATE TABLE order_items (
    item_id INT PRIMARY KEY,
    order_id INT,
    product_name VARCHAR(50),
    quantity INT,
    unit_price DECIMAL(10,2)
);

INSERT INTO orders VALUES (1001, 1, '2024-01-15 10:00:00');
INSERT INTO order_items VALUES
(1, 1001, 'MySQL教程', 2, 99.00),
(2, 1001, 'Linux命令手册', 1, 59.00);

查询订单总金额(每次都要聚合计算):

SELECT o.order_id, SUM(i.quantity * i.unit_price) AS total_amount
FROM orders o
JOIN order_items i ON o.order_id = i.order_id
WHERE o.order_id = 1001
GROUP BY o.order_id;
order_idtotal_amount
1001257.00

反范式设计:在订单表中冗余总金额

操作语句:

-- 反范式化后的订单表
CREATE TABLE orders_denorm (
    order_id INT PRIMARY KEY,
    user_id INT,
    order_time DATETIME,
    total_amount DECIMAL(10,2) DEFAULT 0,  -- 冗余汇总字段
    item_count INT DEFAULT 0               -- 冗余商品件数
);

-- 插入订单时同时计算总金额(或在应用层计算后写入)
INSERT INTO orders_denorm (order_id, user_id, order_time, total_amount, item_count)
VALUES (1001, 1, '2024-01-15 10:00:00', 257.00, 3);

-- 查询订单列表时直接读取,无需 JOIN 和聚合
SELECT order_id, total_amount, item_count FROM orders_denorm WHERE order_id = 1001;

操作后数据状态:

order_idtotal_amountitem_count
1001257.003

结果解读:

  • 订单列表页(高频查询)直接读取 total_amount,无需 JOIN order_items
  • 代价:新增/修改明细时,必须同步更新 orders_denorm.total_amount
  • 维护方式:1. 应用层双写;2. 触发器自动更新;3. 定时任务补偿
  • 适用场景:订单列表、报表首页等高频聚合查询场景

场景三:宽表设计(预 JOIN)

当前数据状态:电商场景中,商品信息分散在多张表中。

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(50),
    category_id INT,
    brand_id INT
);

CREATE TABLE categories (category_id INT PRIMARY KEY, category_name VARCHAR(20));
CREATE TABLE brands (brand_id INT PRIMARY KEY, brand_name VARCHAR(20));

商品搜索页需要展示:商品名、分类名、品牌名——每次都要 3 表 JOIN。

反范式设计:商品搜索宽表

操作语句:

-- 搜索宽表:预先将多表信息合并
CREATE TABLE product_search (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(50),
    category_id INT,
    category_name VARCHAR(20),    -- 冗余
    brand_id INT,
    brand_name VARCHAR(20),     -- 冗余
    price DECIMAL(10,2),
    INDEX idx_category (category_name),
    INDEX idx_brand (brand_name)
);

INSERT INTO product_search VALUES
(1, 'MySQL 5.7 权威指南', 10, '计算机', 100, '图灵出版社', 99.00),
(2, 'Linux 运维实战', 10, '计算机', 101, '人民邮电', 89.00);

-- 搜索只需单表查询
SELECT * FROM product_search WHERE category_name = '计算机';

操作后数据状态:

product_idproduct_namecategory_namebrand_nameprice
1MySQL 5.7 权威指南计算机图灵出版社99.00
2Linux 运维实战计算机人民邮电89.00

结果解读:

  • 搜索页从 3 表 JOIN 变为单表查询,且可在冗余列上建索引
  • 代价:分类名或品牌名变更时,需要更新宽表中所有相关商品
  • 维护方式:通常用 ETL 工具或消息队列异步同步,接受秒级延迟
  • 适用场景:搜索引擎、BI 报表、数据仓库等读极多、写可控的场景

反范式化的代价与维护策略

维护方式原理优点缺点
应用层双写写主表时同时写冗余表实时一致代码复杂,容易遗漏
触发器主表变更自动触发更新对应用透明增加写延迟,调试困难
定时任务定时扫描差异并补偿实现简单有延迟,非实时
消息队列主表变更发消息,消费者更新冗余解耦、可扩展架构复杂,有延迟

常见误区

误区正解
"反范式化就是不要范式"不是。反范式化是先范式化,再有选择地引入冗余。直接不做范式化是"混乱",不是反范式化。
"反范式化一定能提升性能"不一定。如果冗余数据更新频繁,维护开销可能抵消查询收益。
"所有读多写少的场景都应该反范式化"不是。只有瓶颈确实在 JOIN 或聚合时才考虑。小数据量下范式化查询已经足够快。
"反范式化后不需要维护冗余数据"必须维护。否则数据不一致会导致严重业务 Bug。

面试考点

Q:什么时候应该反范式化?

  1. 读多写少,且 JOIN 或聚合成为性能瓶颈;2. 冗余数据变更频率极低(如商品名称、部门名称);3. 有可靠的机制维护冗余数据一致性(双写、触发器、消息队列)。

Q:反范式化和范式化怎么选?

先按 3NF 设计,确保无更新异常。然后针对高频查询路径评估:如果 JOIN 成为瓶颈,且冗余数据稳定,再引入可控冗余。核心交易数据(订单金额、库存)谨慎反范式化,必须保证强一致。

Q:反范式化后如何保证数据一致?

常用方式:应用层双写(实时)、数据库触发器(实时但对应用透明)、定时任务补偿(可接受延迟)、消息队列异步同步(解耦)。选择取决于业务对延迟的容忍度。

小结

  • 反范式设计是有意引入可控冗余,用空间换时间、用写入成本换读取性能
  • 常见手段:合并表减少 JOIN、添加汇总字段减少聚合、构建宽表预 JOIN
  • 反范式化的前提是先做好范式化,否则是混乱而非设计
  • 必须配套数据一致性维护机制,否则冗余会变成数据质量的灾难
  • 核心交易数据谨慎反范式化,报表/搜索/分析场景更适合

清理测试数据:

DROP TABLE IF EXISTS employees_denorm;
DROP TABLE IF EXISTS orders_denorm;
DROP TABLE IF EXISTS product_search;
DROP TABLE IF EXISTS employees;
DROP TABLE IF EXISTS departments;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS categories;
DROP TABLE IF EXISTS brands;
上一页
第三范式与 BC 范式