MySQL知识梳理(6)

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 语句定义 数据结构
增删改 ⚠️ 受限 ✅ 完全支持
可建索引 ❌ 不能建 ✅ 可以建
执行速度 每次查询时执行 直接访问

面试高频题:视图的优点有哪些?

  1. 简化复杂查询:封装 JOIN、聚合等复杂逻辑
  2. 数据安全:隐藏敏感列,只暴露必要字段
  3. 逻辑独立性:修改表结构不影响应用层
  4. 复用性:一处定义多处使用

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;

可更新的必要条件:

  1. 不包含 GROUP BYDISTINCTHAVING
  2. 不包含聚合函数(SUMCOUNTAVG 等)
  3. 不包含 UNIONUNION ALL
  4. FROM 子句不包含子查询
  5. 不包含多表连接(单表视图通常可更新)

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
        ├── 控制表宽度
        ├── 合理外键
        ├── 预估数据量
        └── 统一命名规范

写在最后

  1. 动手实践:每个知识点都亲手敲一遍 SQL,比看十遍文档记得牢

  2. 理解原理:面试官更关注"为什么",比如"为什么要用视图"、"什么时候反范式"

  3. 对比记忆:视图 vs 表、存储过程 vs 触发器、范式 vs 反范式------对比学习效果翻倍

  4. 关注版本:MySQL 8.0 引入了角色管理、窗口函数等新特性,部分面试会考察

  5. 结合业务:学习这些知识时多思考实际业务场景,比如"电商系统如何设计库存表"、"用户表如何避免查询慢"