MySQL知识梳理(6)------数据库对象与设计
作者:没有四次元口袋的蓝胖
日期:2026-06-24
标签:MySQL, 存储过程, 视图, 权限, 设计范式
一、存储过程
1.1 什么是存储过程?
存储过程是预编译的 SQL 代码块,存储在数据库中,可接受参数并返回值。你可以把它理解为数据库端的"函数"。
sql
-- 基本语法
DELIMITER //
CREATE PROCEDURE proc_name([IN|OUT|INOUT] param_name param_type)
BEGIN
-- SQL 语句
END //
DELIMITER ;
1.2 优缺点对比
| 优点 | 缺点 |
|---|---|
| ⚡ 减少网络传输(一次调用执行多条SQL) | ❌ 调试困难,没有可视化调试器 |
| ✅ 提高执行效率(预编译缓存) | ❌ 移植性差,MySQL/Oracle语法有差异 |
| ✅ 代码复用,一处定义多处调用 | ❌ 版本管理困难 |
| ✅ 提高安全性(参数化防止SQL注入) | ❌ 业务逻辑分散在DB层 |
1.3 参数类型详解
sql
DELIMITER //
CREATE PROCEDURE manage_user(
IN user_id INT, -- 输入参数,只读
OUT total_count INT, -- 输出参数
INOUT user_status VARCHAR(20) -- 输入输出参数
)
BEGIN
-- IN: 外部传入值,过程中可读取
-- OUT: 初始值为NULL,可在过程内赋值供外部使用
-- INOUT: 既可传入值,过程内也可修改并返回
SELECT COUNT(*) INTO total_count FROM users WHERE status = user_status;
SET user_status = CONCAT('processed_', user_status);
END //
DELIMITER ;
-- 调用示例
CALL manage_user(1, @count, @status);
SELECT @count, @status;
⚡ 面试坑点:INOUT 参数容易被忽略,很多人以为参数只能是输入或输出,实际上 MySQL 支持既输入又输出的参数。
1.4 创建、调用与删除
sql
-- 创建存储过程
CREATE PROCEDURE get_user_info(IN uid INT)
BEGIN
SELECT * FROM users WHERE id = uid;
END;
-- 调用存储过程
CALL get_user_info(10);
-- 删除存储过程
DROP PROCEDURE IF EXISTS get_user_info;
-- 查看存储过程列表
SHOW PROCEDURE STATUS LIKE 'get_%';
⚡ 高频面试题:存储过程与预编译SQL有什么区别?
存储过程是一组预编译SQL的集合,保存在数据库端;而普通预编译SQL是单条语句的预编译。存储过程减少了网络往返次数,但业务逻辑耦合在数据库层不利于维护。
二、触发器
2.1 什么是触发器?
触发器是当特定事件发生时自动执行的 SQL 集合。事件可以是 INSERT、UPDATE、DELETE,操作时机可以是 BEFORE 或 AFTER。
sql
CREATE TRIGGER trigger_name
{BEFORE|AFTER} {INSERT|UPDATE|DELETE}
ON table_name FOR EACH ROW
BEGIN
-- 触发器逻辑
END;
2.2 六种触发器类型
| 类型 | 触发时机 | 典型场景 |
|---|---|---|
| BEFORE INSERT | 插入前 | 数据校验、修改待插入值 |
| AFTER INSERT | 插入后 | 关联数据同步、日志记录 |
| BEFORE UPDATE | 更新前 | 变更校验 |
| AFTER UPDATE | 更新后 | 审计追踪 |
| BEFORE DELETE | 删除前 | 关联数据检查 |
| AFTER DELETE | 删除后 | 级联清理、数据归档 |
2.3 实际应用场景
场景1:自动填充创建/更新时间
sql
CREATE TRIGGER trg_orders_insert
BEFORE INSERT ON orders
FOR EACH ROW
BEGIN
SET NEW.create_time = NOW();
SET NEW.status = IFNULL(NEW.status, 'pending');
END;
场景2:订单创建时自动扣减库存
sql
CREATE TRIGGER trg_order_create
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
UPDATE products
SET stock = stock - NEW.quantity
WHERE id = NEW.product_id;
END;
场景3:数据归档(删除用户时保留历史记录)
sql
CREATE TRIGGER trg_user_delete
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
INSERT INTO users_archive (id, name, email, deleted_at)
VALUES (OLD.id, OLD.name, OLD.email, NOW());
END;
⚡ 面试坑点:OLD 和 NEW 关键字是触发器特有的引用方式:
- INSERT 触发器只能使用 NEW
- DELETE 触发器只能使用 OLD
- UPDATE 触发器两者都可使用
三、视图
3.1 什么是视图?
视图是基于 SQL 查询结果的虚拟表,本身不存储数据,查询时动态生成结果。
sql
-- 创建视图
CREATE VIEW v_user_orders AS
SELECT u.id, u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
-- 使用视图
SELECT * FROM v_user_orders WHERE order_count > 5;
3.2 视图 vs 表 对比
| 特性 | 视图 | 表 |
|---|---|---|
| 存储数据 | ❌ 不存储,动态生成 | ✅ 物理存储 |
| 本质 | SELECT 语句定义 | 数据结构 |
| 增删改 | ⚠️ 受限 | ✅ 完全支持 |
| 可建索引 | ❌ 不能建 | ✅ 可以建 |
| 执行速度 | 每次查询时执行 | 直接访问 |
⚡ 面试高频题:视图的优点有哪些?
- 简化复杂查询:封装 JOIN、聚合等复杂逻辑
- 数据安全:隐藏敏感列,只暴露必要字段
- 逻辑独立性:修改表结构不影响应用层
- 复用性:一处定义多处使用
3.3 创建、使用与删除
sql
-- 创建视图(带加密)
CREATE ALGORITHM = MERGE VIEW v_emp_details AS
SELECT e.name, d.dept_name, e.salary
FROM employees e
JOIN departments d ON e.dept_id = d.id;
-- 创建只读视图
CREATE VIEW v_emp_readonly AS
SELECT * FROM employees
WITH CHECK OPTION;
-- 使用视图
SELECT * FROM v_emp_details WHERE dept_name = '技术部';
-- 删除视图
DROP VIEW IF EXISTS v_emp_details;
-- 查看视图结构
DESCRIBE v_emp_details;
SHOW CREATE VIEW v_emp_details;
四、视图的增删改
4.1 可更新视图的条件
视图不是"镜中花",满足以下条件时可以更新数据:
sql
-- ✅ 可更新的简单视图
CREATE VIEW v_adult_users AS
SELECT id, name, age FROM users WHERE age >= 18;
-- 可以执行 INSERT/UPDATE/DELETE
UPDATE v_adult_users SET age = 20 WHERE id = 1;
可更新的必要条件:
- 不包含
GROUP BY、DISTINCT、HAVING - 不包含聚合函数(
SUM、COUNT、AVG等) - 不包含
UNION、UNION ALL - FROM 子句不包含子查询
- 不包含多表连接(单表视图通常可更新)
4.2 WITH CHECK OPTION
这个选项确保通过视图插入或更新的数据对视图可见,防止"窗外数据"的出现。
sql
-- 只允许插入/更新 age >= 18 的数据
CREATE VIEW v_adult_users AS
SELECT id, name, age FROM users WHERE age >= 18
WITH CHECK OPTION;
-- ✅ 成功(age = 25,满足 age >= 18)
INSERT INTO v_adult_users VALUES (1, '张三', 25);
-- ❌ 失败!违反 CHECK OPTION(age = 16,不满足 age >= 18)
INSERT INTO v_adult_users VALUES (2, '李四', 16);
⚡ 面试高频题:WITH CHECK OPTION 的作用是什么?
防止通过视图插入或更新不符合视图 WHERE 条件的数据。如果没有这个选项,你可以通过视图插入"窗外数据"------即对视图不可见的数据。
4.3 不可更新视图示例
sql
-- ❌ 不可更新:包含聚合函数
CREATE VIEW v_user_count AS
SELECT department, COUNT(*) AS cnt FROM users GROUP BY department;
-- 无法通过此视图增删改数据
-- ❌ 不可更新:包含多表连接
CREATE VIEW v_emp_dept AS
SELECT e.name, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id;
-- MySQL 中多表视图通常不可更新
五、用户创建与授权
5.1 基础操作
sql
-- 创建用户
CREATE USER 'username'@'host' IDENTIFIED BY 'password';
-- host: localhost(本地)、%(任意主机)、具体IP
-- 授权
GRANT privilege_type ON database.table TO 'username'@'host';
-- 刷新权限(重要!)
FLUSH PRIVILEGES;
5.2 权限类型一览
| 权限类型 | 说明 |
|---|---|
| ALL PRIVILEGES | 所有权限 |
| SELECT, INSERT, UPDATE, DELETE | 基础DML权限 |
| CREATE, DROP, ALTER | DDL权限 |
| REFERENCES | 外键权限 |
| INDEX | 索引管理权限 |
| EXECUTE | 执行存储过程/函数 |
5.3 三种用户创建示例
sql
-- 示例1:应用开发账号(只能读写指定数据库)
CREATE USER 'app_user'@'%' IDENTIFIED BY 'App@2024';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_user'@'%';
-- 示例2:数据分析账号(只读)
CREATE USER 'analyst'@'localhost' IDENTIFIED BY 'Ana@2024';
GRANT SELECT ON myapp.* TO 'analyst'@'localhost';
-- 示例3:管理员账号(所有权限)
CREATE USER 'dba'@'localhost' IDENTIFIED BY 'DBA@2024';
GRANT ALL PRIVILEGES ON *.* TO 'dba'@'localhost' WITH GRANT OPTION;
⚡ 面试坑点:
- 创建用户后必须
FLUSH PRIVILEGES才能生效(除非用 GRANT 自动刷新) WITH GRANT OPTION允许该用户授权给其他人,生产环境慎用
六、权限管理
6.1 权限查看与撤销
sql
-- 查看用户所有权限
SHOW GRANTS FOR 'app_user'@'%';
-- 撤销指定权限
REVOKE INSERT, UPDATE ON myapp.* FROM 'app_user'@'%';
-- 撤销所有权限(保留用户)
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'app_user'@'%';
-- 删除用户
DROP USER 'app_user'@'%';
-- 修改密码
ALTER USER 'app_user'@'%' IDENTIFIED BY 'NewPass@2024';
6.2 四级权限范围
| 范围 | 语法 | 说明 |
|---|---|---|
| 全局级别 | *.* |
所有数据库和表 |
| 数据库级别 | db_name.* |
某个数据库的所有对象 |
| 表级别 | db_name.table_name |
某张表 |
| 列级别 | 授予时指定列名 | 某表的特定列(很少用) |
sql
-- 全局:所有数据库的所有表
GRANT SELECT ON *.* TO 'reader'@'%';
-- 数据库:myapp库的所有表
GRANT ALL ON myapp.* TO 'developer'@'localhost';
-- 表:myapp库的users表
GRANT SELECT, UPDATE ON myapp.users TO 'support'@'%';
-- 列(很少用)
GRANT SELECT, UPDATE(id, name) ON myapp.users TO 'limited_user'@'%';
⚡ 面试高频题:如何最小权限原则?
按需授权,只给必要的最小权限集。应用账号通常只需要 SELECT/INSERT/UPDATE/DELETE,业务 DBA 需要 DDL 权限,管理员才需要 ALL。
七、角色管理(MySQL 8.0+)
7.1 角色基础操作
MySQL 8.0 引入了**角色(ROLE)**概念,类似于"权限组",简化权限管理。
sql
-- 创建角色
CREATE ROLE 'app_developer', 'app_read', 'app_write';
-- 为角色授权
GRANT SELECT ON myapp.* TO 'app_read';
GRANT SELECT, INSERT, UPDATE, DELETE ON myapp.* TO 'app_write';
GRANT ALL ON myapp.* TO 'app_developer';
-- 为用户分配角色
GRANT 'app_read' TO 'dev1'@'localhost';
GRANT 'app_developer' TO 'dba'@'localhost';
7.2 激活与默认角色
sql
-- 设置默认角色(用户登录时自动激活)
SET DEFAULT ROLE 'app_read' FOR 'dev1'@'localhost';
-- 手动激活角色
SET ROLE 'app_read';
SET ROLE 'app_write';
-- 查看当前会话激活的角色
SELECT CURRENT_ROLE();
⚡ 面试坑点 :MySQL 的角色是"激活后才生效"的,不像 Oracle 自动激活。需要用 SET DEFAULT ROLE 或每次登录后 SET ROLE 才能使用角色权限。
7.3 撤销与删除
sql
-- 从用户撤销角色
REVOKE 'app_read' FROM 'dev1'@'localhost';
-- 删除角色
DROP ROLE 'app_read';
-- 角色权限也会被同步撤销
八、三大范式
8.1 第一范式(1NF)------ 原子性
定义:每个列都是不可分割的原子值。
反例(违反1NF):
| id | name | phone |
|---|---|---|
| 1 | 张三 | 13800138000, 010-12345678 |
正例(符合1NF):
| id | name | phone_main | phone_office |
|---|---|---|---|
| 1 | 张三 | 13800138000 | 01012345678 |
⚡ 面试题:为什么需要原子性?
原子性保证数据的最小不可分原则。非原子列难以精确查询、统计和关联。比如查询"北京的用户",如果地址是"北京市朝阳区XX"就无法直接筛选。
8.2 第二范式(2NF)------ 消除部分依赖
定义:在满足1NF的基础上,非主键列必须完全依赖于主键,不能只依赖主键的一部分。
反例(违反2NF):
| order_id | product_id | order_time | product_name |
|---|---|---|---|
| 1 | 100 | 2024-01-01 | 手机 |
主键是 (order_id, product_id),但 product_name 只依赖 product_id,不依赖 order_id------这是部分依赖。
正例(符合2NF):
订单表(订单ID, 订单时间, 客户ID)
商品表(商品ID, 商品名称, 价格)
订单明细表(订单ID, 商品ID, 数量)
⚡ 面试高频题:复合主键的场景下才可能违反2NF,单一主键的表天然满足2NF。
8.3 第三范式(3NF)------ 消除传递依赖
定义:在满足2NF的基础上,非主键列之间不能存在传递依赖。
反例(违反3NF):
| student_id | name | department | dept_phone |
|---|---|---|---|
| 1 | 张三 | 计算机系 | 010-12345678 |
student_id → department → dept_phone 存在传递依赖:dept_phone 依赖于 department,而不是直接依赖于 student_id。
正例(符合3NF):
学生表(学生ID, 姓名, 系别ID)
系别表(系别ID, 系别名称, 联系电话)
⚡ 面试题:三大范式解决了什么问题?
1NF消除重复数据;2NF消除部分依赖导致的冗余;3NF消除传递依赖。三者共同目标:减少数据冗余、避免更新异常。
九、反范式设计
9.1 什么时候需要反范式?
反范式是为了性能主动引入冗余,以空间换时间。
sql
-- 反范式示例:在订单表中冗余客户名称
CREATE TABLE orders (
id INT PRIMARY KEY,
customer_id INT,
customer_name VARCHAR(50), -- 冗余字段,避免JOIN
order_time DATETIME,
total_amount DECIMAL(10,2)
);
9.2 适用场景
| 场景 | 说明 |
|---|---|
| 读多写少 | 频繁查询的字段可冗余,避免 JOIN |
| 高并发优化 | 减少 JOIN 开销,降低锁竞争 |
| 报表/统计 | 预计算汇总数据,定时刷新 |
| 缓存层 | 将热点数据冗余到业务表 |
9.3 注意事项
⚡ 核心原则:反范式必须配套一致性保证机制!
sql
-- 方案1:应用层保证一致性
UPDATE customers SET name = '新名称' WHERE id = ?;
UPDATE orders SET customer_name = '新名称' WHERE customer_id = ?; -- 同步更新冗余
-- 方案2:触发器保证一致性
CREATE TRIGGER trg_sync_name
AFTER UPDATE ON customers
FOR EACH ROW
BEGIN
UPDATE orders SET customer_name = NEW.name WHERE customer_id = NEW.id;
END;
-- 方案3:定时任务同步
-- 每日凌晨执行数据一致性检查和同步
⚡ 面试高频题:范式 vs 反范式如何选择?
原则是先范式,再根据性能需求适度反范式。一般业务系统满足3NF即可,在核心查询场景(如报表、列表页)适当冗余。
十、表设计原则
10.1 六条最佳实践
1. 使用逻辑自增主键
sql
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY
优点:插入快、占用小、查询快。UUID虽全局唯一但插入性能差。
2. 字段设为 NOT NULL + DEFAULT
sql
status TINYINT NOT NULL DEFAULT 1,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
避免 NULL 的不确定性,减少程序判断。
3. 控制单表宽度
建议单表字段 ≤ 20-30 个
字段过多考虑垂直拆分,大字段(TEXT/BLOB)单独建表。
4. 合理使用外键
sql
-- 强一致性场景用物理外键
FOREIGN KEY (class_id) REFERENCES class(id)
-- 高并发互联网场景用逻辑外键,应用层控制
5. 预估数据量,提前规划
sql
-- 大表考虑分区
PARTITION BY RANGE (create_time) (
PARTITION p2024 VALUES LESS THAN ('2025-01-01'),
PARTITION p2025 VALUES LESS THAN ('2026-01-01'),
PARTITION pmax VALUES LESS THAN MAXVALUE
);
6. 统一命名规范
表名单数:user / order / product
字段名下划线:user_name / order_id / create_time
布尔字段前缀 is_/has_/can_:is_deleted / is_active
10.2 命名规范速查
| 类型 | 规范 | 示例 |
|---|---|---|
| 表名 | 小写下划线,单数 | user, order_detail |
| 主键 | id 或 表名_id | id, user_id |
| 外键 | 关联表_id | class_id, product_id |
| 时间字段 | _time / _at | create_time, updated_at |
| 状态字段 | is_/status | is_deleted, status |
| 索引前缀 | idx_ / uk_ / fk_ | idx_name, uk_email |
📋 思维导图速览
MySQL核心知识------数据库对象与设计
│
├── 📦 数据库对象
│ │
│ ├── 存储过程
│ │ ├── 预编译SQL代码块
│ │ ├── 参数类型:IN / OUT / INOUT
│ │ └── 适用:批量操作、复杂业务逻辑
│ │
│ ├── 触发器
│ │ ├── 6种类型:BEFORE/AFTER × INSERT/UPDATE/DELETE
│ │ ├── OLD/NEW引用
│ │ └── 场景:审计日志、级联更新、数据归档
│ │
│ └── 视图
│ ├── 虚拟表,不存储数据
│ ├── 可更新条件:简单查询、无聚合
│ ├── WITH CHECK OPTION 防窗外数据
│ └── 作用:简化查询、保护数据
│
├── 🔐 权限管理
│ │
│ ├── 用户与授权
│ │ ├── CREATE USER + GRANT + FLUSH
│ │ ├── 4级权限:全局/数据库/表/列
│ │ └── 最小权限原则
│ │
│ └── 角色(MySQL 8.0+)
│ ├── CREATE ROLE + GRANT TO
│ ├── SET DEFAULT ROLE
│ └── 需手动激活
│
└── 🎯 设计范式
│
├── 三大范式
│ ├── 1NF:列原子性,不可分割
│ ├── 2NF:消除部分依赖(复合主键场景)
│ └── 3NF:消除传递依赖
│
├── 反范式
│ ├── 空间换时间
│ ├── 读多写少场景
│ └── 必须配套一致性机制
│
└── 表设计原则
├── 逻辑自增主键
├── NOT NULL + DEFAULT
├── 控制表宽度
├── 合理外键
├── 预估数据量
└── 统一命名规范
写在最后
-
动手实践:每个知识点都亲手敲一遍 SQL,比看十遍文档记得牢
-
理解原理:面试官更关注"为什么",比如"为什么要用视图"、"什么时候反范式"
-
对比记忆:视图 vs 表、存储过程 vs 触发器、范式 vs 反范式------对比学习效果翻倍
-
关注版本:MySQL 8.0 引入了角色管理、窗口函数等新特性,部分面试会考察
-
结合业务:学习这些知识时多思考实际业务场景,比如"电商系统如何设计库存表"、"用户表如何避免查询慢"