📖 本篇简介:前面我们都是一条一条地写 SQL,本篇进入"封装"的世界——把常用查询封装成视图、把一段业务逻辑封装成存储过程,重复调用、统一口径。本篇讲视图的创建修改、存储过程的参数与流程控制、以及游标的基本用法。学完后你能把复杂查询模块化,避免到处复制粘贴 SQL。本示例基于 MySQL 8.0 语法。
目录
- 1 视图虚拟的表
- 2 视图的创建修改与删除
- 3 存储过程入门
- 4 存储过程的参数INOUT与OUT与INOUT
- 5 存储过程中的流程控制
- 6 游标CURSOR
- 7 视图与存储过程速查表
- 8 常见坑与注意事项
- 9 练习题
1 视图虚拟的表
视图(View)是一张"虚拟的表":它本身不存数据,数据仍来自基表,视图只是把一条 SELECT 查询存了起来。你像查表一样查视图,MySQL 在背后执行那条查询。
视图的典型用途:
1. 简化复杂查询:多表 JOIN + 子查询写一次存成视图,以后直接 SELECT * FROM 视图。
2. 权限控制:只开放视图给某些用户,隐藏敏感列(如把手机号脱敏后再开放)。
3. 统一口径:把"活跃用户""销售额"等统计逻辑固定在视图里,所有报表引用同一个定义。
| 对比 | 普通表 | 视图 |
|---|---|---|
| 存数据 | 是 | 否,查询时实时计算 |
| 占用空间 | 数据 + 索引 | 只存定义 |
| 修改基表 | 直接影响视图结果 | 视图自动反映最新数据 |
| 适合 | 业务数据 | 简化查询、权限隔离 |
2 视图的创建修改与删除
2.1 创建视图
-- 创建一个"用户订单汇总"视图(MySQL 8.0)
CREATE VIEW v_user_order AS
SELECT
u.user_id,
u.name,
COUNT(o.order_id) AS order_cnt,
SUM(o.amount) AS total_amt
FROM user_info u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name;
建好后,像查表一样用它:
-- 查询视图(MySQL 8.0)
SELECT * FROM v_user_order WHERE total_amt > 1000;
2.2 修改与删除视图
-- 修改视图定义(MySQL 8.0)
CREATE OR REPLACE VIEW v_user_order AS
SELECT u.user_id, u.name, COUNT(o.order_id) AS cnt
FROM user_info u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name;
-- 删除视图(MySQL 8.0)
DROP VIEW IF EXISTS v_user_order;
CREATE OR REPLACE 的好处是:视图不存在就创建、存在就替换,省去先 DROP 再 CREATE 的两步。
2.3 视图的可更新性
普通视图在一定条件下可以 INSERT/UPDATE/DELETE,MySQL 会把操作转发回基表。但包含以下元素的视图不可更新:
- 聚合函数(
SUM/COUNT)、GROUP BY、DISTINCT; - 多表 JOIN 且修改涉及多表;
- 子查询在 SELECT 列表里。
记住:视图主要是用来"查"的,别指望拿复杂聚合视图去做写操作。
2.4 视图实战:脱敏后开放
客服系统只想让客服看到脱敏后的手机号,不想暴露完整号码。建一个视图专门给他们用:
-- 脱敏视图(MySQL 8.0)
CREATE VIEW v_user_mask AS
SELECT
user_id,
name,
CONCAT(SUBSTRING(phone,1,3), '****', SUBSTRING(phone,8)) AS phone,
city
FROM user_info;
客服账号只授予这个视图的 SELECT 权限,碰不到原始表,敏感字段自然泄露不出去。这就是视图做权限控制的典型场景。
3 存储过程入门
存储过程(Stored Procedure)是预先编译好、存在数据库里的一段 SQL 程序。它可以接收参数、有变量、有分支循环,像写函数一样把业务逻辑封装进去。
3.1 第一个存储过程
-- 最简单的存储过程(MySQL 8.0)
DELIMITER $$ -- 临时把语句结束符改成 $$
CREATE PROCEDURE sp_hello()
BEGIN
SELECT 'Hello, MySQL' AS msg; -- 过程体里的 SQL
END$$
DELIMITER ; -- 改回分号
几个要点:
1. DELIMITER $$:因为过程体里有分号,要临时改结束符,否则 MySQL 遇到第一个 ; 就以为语句结束了。
2. BEGIN ... END 包住过程体。
3. 调用:CALL sp_hello();
-- 调用存储过程(MySQL 8.0)
CALL sp_hello();
3.2 查看与删除存储过程
-- 查看已创建的存储过程(MySQL 8.0)
SHOW PROCEDURE STATUS WHERE Db = '你的库名';
-- 删除存储过程(MySQL 8.0)
DROP PROCEDURE IF EXISTS sp_hello;
4 存储过程的参数INOUT与OUT与INOUT
存储过程有三种参数模式:
| 模式 | 含义 | 能否返回给调用者 |
|---|---|---|
| IN(默认) | 输入参数,调用时传入 | 否 |
| OUT | 输出参数,过程里赋值后带回 | 是 |
| INOUT | 既传入又带回 | 是 |
4.1 IN:传入参数
-- 按用户 ID 查订单数(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_order_cnt(IN p_uid BIGINT)
BEGIN
SELECT COUNT(*) AS order_cnt
FROM orders
WHERE user_id = p_uid;
END$$
DELIMITER ;
-- 调用
CALL sp_order_cnt(1001);
4.2 OUT:带回结果
-- 把订单数写到 OUT 参数(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_get_cnt(IN p_uid BIGINT, OUT p_cnt INT)
BEGIN
SELECT COUNT(*) INTO p_cnt
FROM orders
WHERE user_id = p_uid;
END$$
DELIMITER ;
-- 调用:用用户变量 @ 接收 OUT 值
CALL sp_get_cnt(1001, @cnt);
SELECT @cnt;
@cnt 是会话用户变量,CALL 后过程把结果写进去,再 SELECT @cnt 取出来。
4.3 INOUT:既传入又带回
INOUT 把外部变量传进去,过程里改完再带回来,适合"就地更新":
-- 给传入的数加 10(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_add10(INOUT v INT)
BEGIN
SET v = v + 10;
END$$
DELIMITER ;
SET @x = 5;
CALL sp_add10(@x);
SELECT @x; -- 15
三种参数怎么选:只想传入用 IN;想把结果带出来用 OUT;既要用旧值又要更新后带回来,用 INOUT。
5 存储过程中的流程控制
过程体里可以用变量、IF、CASE、WHILE、LOOP、REPEAT,形成真正的程序逻辑。
5.1 变量与 IF 判断
-- 根据金额打标(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_level(IN p_amt DECIMAL(10,2), OUT p_level VARCHAR(10))
BEGIN
IF p_amt >= 10000 THEN
SET p_level = 'VIP';
ELSEIF p_amt >= 1000 THEN
SET p_level = '会员';
ELSE
SET p_level = '普通';
END IF;
END$$
DELIMITER ;
CALL sp_level(5000, @lvl);
SELECT @lvl; -- 会员
这里 SET 给变量赋值,IF ... ELSEIF ... ELSE ... END IF 做多分支判断。
5.2 循环:WHILE
-- 循环累加 1 到 100(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_sum(OUT p_total INT)
BEGIN
DECLARE i INT DEFAULT 1; -- 局部变量,必须声明在 BEGIN 开头
DECLARE s INT DEFAULT 0;
WHILE i <= 100 DO
SET s = s + i;
SET i = i + 1;
END WHILE;
SET p_total = s;
END$$
DELIMITER ;
CALL sp_sum(@t);
SELECT @t; -- 5050
注意 DECLARE 声明的局部变量必须放在 BEGIN ... END 的最开头,这是 MySQL 的语法要求。
5.3 CASE 与错误处理
过程里也能用 CASE 做多分支,和查询里的 CASE WHEN 类似。另外,DECLARE HANDLER 可以捕获错误,让过程遇到异常时不至于直接崩溃:
-- 捕获错误并打标记(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_safe_insert(IN p_uid BIGINT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION -- 遇到任何 SQL 错误就走这里
BEGIN
SELECT '插入失败,已回滚' AS err_msg;
END;
INSERT INTO user_info(user_id, name) VALUES(p_uid, CONCAT('u', p_uid));
SELECT '插入成功' AS ok;
END$$
DELIMITER ;
EXIT HANDLER 表示捕获后退出当前 BEGIN...END 块。真实项目里这比让程序裸奔强得多——出错时至少有个明确提示。
6 游标CURSOR
游标(Cursor)用于"逐行处理"查询结果集。普通 SQL 是一次性处理整个结果集,游标则像指针一样一行一行往下走,适合需要对每行做不同处理的场景。
-- 用游标逐行给用户分级(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_cursor_demo()
BEGIN
DECLARE done INT DEFAULT 0; -- 循环结束标志
DECLARE v_uid BIGINT; -- 接收每行字段
DECLARE v_amt DECIMAL(10,2);
-- 声明游标:绑定要遍历的 SELECT
DECLARE cur CURSOR FOR SELECT user_id, total_amt FROM user_stat;
-- 当游标读完没有更多行时,把 done 置 1
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN cur; -- 打开游标
read_loop: LOOP
FETCH cur INTO v_uid, v_amt; -- 取下一行
IF done = 1 THEN
LEAVE read_loop; -- 退出循环
END IF;
-- 这里对 v_uid / v_amt 做逐行处理
INSERT INTO user_log(user_id, info) VALUES(v_uid, CONCAT('amt=', v_amt));
END LOOP;
CLOSE cur; -- 关闭游标
END$$
DELIMITER ;
游标是"逐行"处理,数据量大时性能差,能集合化处理就不要用游标。它适合迁移数据、批量修复这类脚本型任务。
6.1 能集合操作就别用游标
举个对比例子:要给所有 VIP 用户的日志表补一条记录,用游标要逐行 INSERT;用一条集合 SQL 更简单更快:
-- 集合操作代替游标(MySQL 8.0)
INSERT INTO user_log(user_id, info)
SELECT user_id, CONCAT('amt=', total_amt)
FROM user_stat
WHERE total_amt >= 10000;
| 方式 | 优点 | 缺点 |
|---|---|---|
| 集合 INSERT...SELECT | 快、代码短 | 逻辑固定,无法逐行分支 |
| 游标 | 可逐行判断、灵活 | 慢、代码长 |
绝大多数"批量处理"都能用集合 SQL 完成,游标留给真正需要逐行差异化处理的场景。
7 视图与存储过程速查表
| 操作 | 语句 |
|---|---|
| 建视图 | CREATE VIEW v_xx AS SELECT ...; |
| 改视图 | CREATE OR REPLACE VIEW v_xx AS ...; |
| 删视图 | DROP VIEW IF EXISTS v_xx; |
| 建存储过程 | CREATE PROCEDURE sp_xx(参数) BEGIN ... END; |
| 调用过程 | CALL sp_xx(参数); |
| OUT 接收 | CALL sp_xx(参数, @out); SELECT @out; |
| 删过程 | DROP PROCEDURE IF EXISTS sp_xx; |
| 临时改结束符 | DELIMITER $$ ... DELIMITER ; |
| 局部变量 | DECLARE v INT DEFAULT 0;(放 BEGIN 开头) |
| 游标三步 | OPEN cur; FETCH cur INTO ...; CLOSE cur; |
8 常见坑与注意事项
1. 忘了改 DELIMITER:写存储过程时过程体里有分号,不先 DELIMITER $$ 会直接语法报错。
2. DECLARE 放错位置:局部变量必须在 BEGIN 块最开头,放后面会报错。
3. 视图里用 SELECT *:基表改了列,视图定义不会自动更新,建议视图里显式列出列名。
4. 存储过程滥用:业务逻辑塞进数据库,难调试、难版本管理、跨数据库迁移困难。简单逻辑优先放应用层。
5. 游标性能:逐行处理慢,能用 INSERT INTO ... SELECT 集合操作就别用游标。
6. 视图嵌套过深:视图套视图套视图,排查问题时层层嵌套很难懂。视图一般套一两层就够。
7. 权限安全:存储过程和视图都可能被用来隐藏敏感数据,但也要注意 DEFINER 权限问题,迁移到新库时容易因定义者账号不存在而失效。
9 练习题
练习 1:创建视图
把"每个用户的订单总数和总金额"做成一个视图 v_user_summary。
<details>
<summary>参考答案</summary>
-- 用户汇总视图(MySQL 8.0)
CREATE VIEW v_user_summary AS
SELECT user_id,
COUNT(*) AS order_cnt,
SUM(amount) AS total_amt
FROM orders
GROUP BY user_id;
</details>
练习 2:写一个带 IN 参数的过程
写一个过程 sp_dept_avg(dept_id),查出该部门的平均工资。
<details>
<summary>参考答案</summary>
-- 部门平均工资过程(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_dept_avg(IN p_did INT)
BEGIN
SELECT dept_id, AVG(salary) AS avg_sal
FROM employee
WHERE dept_id = p_did
GROUP BY dept_id;
END$$
DELIMITER ;
</details>
练习 3:OUT 参数
写一个过程,把某个用户的总订单金额通过 OUT 参数带出来。
<details>
<summary>参考答案</summary>
-- 带出总金额(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_user_amt(IN p_uid BIGINT, OUT p_amt DECIMAL(12,2))
BEGIN
SELECT IFNULL(SUM(amount),0) INTO p_amt
FROM orders WHERE user_id = p_uid;
END$$
DELIMITER ;
CALL sp_user_amt(1001, @amt);
SELECT @amt;
</details>
练习 4:WHILE 循环
写一个过程,用 WHILE 计算 1~N 的平方和,结果通过 OUT 返回。
<details>
<summary>参考答案</summary>
-- 平方和(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_sq_sum(IN p_n INT, OUT p_total BIGINT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE s BIGINT DEFAULT 0;
WHILE i <= p_n DO
SET s = s + i*i;
SET i = i + 1;
END WHILE;
SET p_total = s;
END$$
DELIMITER ;
</details>
练习 5:INOUT 参数
写一个过程 sp_double(INOUT v INT),把传入的值翻倍后存回同一个参数。
<details>
<summary>参考答案</summary>
-- INOUT 翻倍(MySQL 8.0)
DELIMITER $$
CREATE PROCEDURE sp_double(INOUT v INT)
BEGIN
SET v = v * 2;
END$$
DELIMITER ;
SET @x = 21;
CALL sp_double(@x);
SELECT @x; -- 42
</details>
本章小结
视图和存储过程都是"封装":视图封装查询、存储过程封装逻辑。视图适合简化报表和权限隔离,存储过程适合把重复性、脚本型的数据库操作固化下来。但要警惕过度封装——业务逻辑能放应用层就别塞进数据库,否则调试、迁移、维护都会变难。下一篇讲触发器与事件调度,它们是"自动执行"的封装。
下一篇预告:第 13 章 触发器与事件调度
文章回复
0 条公开回复