跳到主要内容

创建日期:2026-09-08 | 最近更新:2026-09-08 本机 MySQL 8.4.0 实测;下方输出均为真实结果。

MySQL 复习 6:视图、存储过程、函数与触发器

这四样是把逻辑「下沉到数据库」的手段:视图是「存起来的查询」,存储过程/函数是「数据库里写逻辑」,触发器是「表变化时自动执行」。能用,但都要克制——本系列给你「什么时候用、什么时候别用」的结论。

1. 视图(VIEW):一张「存起来的查询」

视图不是真表,是一条查询的命名,用起来像表:

CREATE VIEW v_city_orders AS
SELECT c.city,
COUNT(*) AS orders,
ROUND(SUM(o.amount),2) AS total
FROM orders o
JOIN customers c ON c.id = o.customer_id
GROUP BY c.city;

-- 之后当成表查:
SELECT * FROM v_city_orders ORDER BY total DESC LIMIT 3;

真实输出:

+--------+--------+----------+
| city | orders | total |
+--------+--------+----------+
| 广州 | 834 | 54440.00 |
| 深圳 | 834 | 54212.58 |
| 成都 | 833 | 54125.00 |
+--------+--------+----------+

用途与坑

  • ✅ 把复杂 JOIN/口径收口成一张「虚拟表」,业务代码只写 FROM v_city_orders;口径改一处即可;
  • ✅ 给不同角色开「只见部分列」的视图,做粗粒度权限;
  • ⚠️ 视图只是查询,每次查它都会重新执行底层 SQL(MySQL 无物化视图自动刷新);别以为视图 = 缓存。复杂视图下层的性能问题原样还在(用篇 3 的 EXPLAIN 看视图背后查询即可)。

2. 存储过程(STORED PROCEDURE):数据库里的「函数流程」

CREATE PROCEDURE sp_city_order_total(IN city_name VARCHAR(30))
SELECT c.city,
COUNT(*) AS orders,
ROUND(SUM(o.amount),2) AS total
FROM orders o
JOIN customers c ON c.id = o.customer_id
WHERE c.city = city_name
GROUP BY c.city;

CALL sp_city_order_total('上海');

真实输出:

+--------+--------+----------+
| city | orders | total |
+--------+--------+----------+
| 上海 | 833 | 54067.42 |
+--------+--------+----------+

使用建议

  • ✅ 适合:多个应用/脚本共用同一段固定 SQL 逻辑、或数据库侧一次性批处理(定时任务);
  • ⚠️ 别把业务编排写进过程(分支、循环、调其它系统)——那会变成「DB 里藏了一套后端」,难测试、难版本化、难水平扩展。现代后端偏好:逻辑放应用层,SQL 保持简单

3. 函数(FUNCTION):返回一个值的「小工具」

CREATE FUNCTION fn_add_tax(amount DECIMAL(10,2))
RETURNS DECIMAL(10,2)
DETERMINISTIC -- 声明:同样输入永远同样输出(优化器可用)
RETURN ROUND(amount * 1.13, 2);

SELECT fn_add_tax(100) AS taxed;

真实输出:

+--------+
| taxed |
+--------+
| 113.00 |
+--------+
  • 函数能在 SQL 里直接用(SELECT/WHERE 里调),适合纯计算(税率、格式化、业务常量换算);
  • ⚠️ 别在函数里做复杂查询(每行调用一次性能爆炸);
  • ⚠️ 对列套函数会让索引失效(篇 3 的 DATE() 同理)。

4. 触发器(TRIGGER):表变化时自动执行

触发器在 INSERT/UPDATE/DELETE 前后自动跑一段逻辑。典型:审计日志

-- 1. 建一张审计日志表
CREATE TABLE order_status_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT, old_status VARCHAR(10), new_status VARCHAR(10),
changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 2. 订单状态变更时自动记一笔
CREATE TRIGGER trg_order_status
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
IF OLD.status <> NEW.status THEN
INSERT INTO order_status_log(order_id, old_status, new_status)
VALUES (OLD.id, OLD.status, NEW.status);
END IF;
END;

-- 3. 触发一次(注意:语法里 CREATE TRIGGER 有多条语句,要用 DELIMITER 包,见下)
UPDATE orders SET status='shipped' WHERE id=1 AND status='paid';

真实结果(触发器自动写入审计表):

+----+----------+------------+------------+---------------------+
| id | order_id | old_status | new_status | changed_at |
+----+----------+------------+------------+---------------------+
| 1 | 1 | paid | shipped | 2026-09-08 15:39:03 |
+----+----------+------------+------------+---------------------+

客户端里写多语句过程/触发器要先把结束符改成别的(否则 ; 提前截断):

DELIMITER //
CREATE TRIGGER ... BEGIN ... END //
DELIMITER ;

触发器使用建议

  • ✅ 适合:强制审计(状态/金额变更留痕)、更新时间戳、防误删——「无论谁通过哪条路改,都跑」是它不可替代的价值;
  • ⚠️ 触发器在你背后偷偷执行,出问题极难排查;别在里面做慢查询/复杂业务;触发逻辑要幂等;
  • 现代实践倾向:审计靠应用层 + 时间戳列搞定,触发器留给「数据库兜底级」需求。

5. 一句话总结(选型心法)

手段适合别滥用
视图收口口径、简化查询以为能缓存/提速
存储过程DB 侧固定批处理写复杂业务编排
函数纯计算小工具做查询 / 慢逻辑
触发器强制审计 / 兜底约束隐藏复杂逻辑

现代后端共识:把「逻辑」尽量放应用层,数据库专注「数据 + 约束 + 简单高效查询」。视图和触发器作为「收口/兜底」用,物尽其用而不喧宾夺主。

动手(用 shop)

  1. 建一个「每个城市订单额」的视图并查它;
  2. 写一个函数:输入 amount,返回 >=100 记为大单否则小单,SELECT 里调用;
  3. 给 orders 加触发器记录「金额变更」(OLD.amount vs NEW.amount),改一笔看审计表。

自测

  1. 视图本质是什么?查视图会重新执行底层 SQL 吗?
  2. 为什么别把业务编排写进存储过程?
  3. 对列用函数为什么导致索引失效?
  4. 触发器适合做什么、风险是什么?
  5. DELIMITER 是干嘛用的?

下一篇:性能调优与生产要点——从「能跑」到「扛得住」。