视图、存储过程与函数
前言:为什么需要这三样东西?
想象一下,你是一家大型超市的经理:
- 表(Table) 就像你的货架——上面摆着真实的商品(数据)。
- 但顾客不需要知道所有货架怎么摆放,他们只想“看热销商品”或“查某类折扣品”;
- 收银员也不用每次都手动算总价,而是调用一个“计算账单”的流程;
- 财务人员更不会直接翻货架,而是用一个“计算税费”的公式。
在数据库中:
- 视图 就是给顾客看的“精选商品清单”;
- 存储过程 就是收银员执行的“结账流程”;
- 函数 就是财务用的“税率计算器”。
它们都不是真实的数据容器,而是对数据操作的封装与抽象,目的是让系统更安全、更高效、更易维护。
一、视图(View):虚拟的“数据窗口”
1.1 标准定义(ISO/IEC 9075)
视图(View) 是一个命名的、持久化的查询表达式,其结果表现为一张虚拟表(Virtual Table)。视图本身不存储数据(除非是物化视图),其内容在每次被引用时动态计算自一个或多个基表(Base Tables)。
形式化语法(简化自 SQL/Foundation):
CREATE VIEW view_name [ ( column_list ) ]
AS query_expression
[ WITH [ CASCADED | LOCAL ] CHECK OPTION ];
1.2 通俗解释
你可以把视图理解为:
“一张永远最新的动态报表”。
它不像普通表那样存数据,而是记住了一个查询语句。每次你“打开这张报表”,数据库就立刻执行这个查询,把最新结果呈现给你。
✅ 举个例子:
你有一张员工表 employees 和部门表 departments。你想经常查看“销售部的在职员工”。
与其每次都写:
SELECT e.name, e.salary, d.dept_name
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE d.dept_name = 'Sales' AND e.status = 'active';
不如创建一个视图:
CREATE VIEW sales_team AS
SELECT e.name, e.salary
FROM employees e
JOIN departments d ON e.dept_id = d.id
WHERE d.dept_name = 'Sales' AND e.status = 'active';
之后只需:
SELECT * FROM sales_team;
——就像打开一个固定的“销售团队名单”,但里面的数据永远是最新的!
1.3 关键特性详解
| 特性 | 技术说明 | 通俗类比 |
|---|---|---|
| 虚拟性 | 不占用额外存储空间(除临时结果外) | 报表不是复印的,而是实时打印的 |
| 只读倾向 | 默认用于查询;更新需满足严格条件 | 你能看名单,但不能直接在名单上改名字(除非系统允许) |
| 封装复杂性 | 隐藏多表 JOIN、子查询等逻辑 | 顾客不用知道仓库在哪,只看前台展示 |
| 安全控制 | 可授权用户访问视图而非基表 | 实习生只能看“公开客户列表”,看不到完整客户表 |
| 逻辑独立性 | 基表结构变化时,若视图仍有效,应用无需改 | 超市换货架布局,但“热销区”指示牌不变 |
1.4 可更新视图(Updatable View)
SQL 标准规定,只有当视图满足以下条件时,才允许通过它修改底层数据:
- 查询仅基于单个基表;
- 不包含
DISTINCT、GROUP BY、HAVING、聚合函数(如SUM()); - SELECT 列表中不含表达式(如
salary * 1.1)或常量; - 包含基表的主键或唯一非空约束列。
✅ 可更新示例:
CREATE VIEW active_employees AS SELECT id, name, salary FROM employees WHERE status = 'active'; -- 可执行:UPDATE active_employees SET salary = 8000 WHERE id = 101;
❌ 不可更新示例:
CREATE VIEW dept_avg_salary AS SELECT dept_id, AVG(salary) AS avg_sal FROM employees GROUP BY dept_id; -- 无法更新:平均工资不是某个具体员工的属性!
1.5 物化视图(Materialized View)——高级扩展
⚠️ 注意:物化视图不属于 SQL 标准核心,是各厂商扩展功能。
- 普通视图:每次查都重新算(实时但慢);
- 物化视图:把结果物理存下来,定期刷新(快但非实时)。
| 数据库 | 是否支持物化视图 |
|---|---|
| Oracle | ✅(CREATE MATERIALIZED VIEW) |
| PostgreSQL | ✅(需手动 REFRESH) |
| SQL Server | ✅(称为“索引视图”) |
| MySQL | ❌(需用定时任务+临时表模拟) |
💡 通俗理解:
普通视图 = 每次现场做菜;
物化视图 = 提前做好菜放冰箱,吃的时候热一下。
1.6 使用场景总结
✅ 强烈推荐使用视图的场景:
- 简化复杂查询(尤其涉及多表关联);
- 实现行级/列级数据安全(最小权限原则);
- 提供向后兼容接口(表结构变更时保护老应用);
- 构建业务语义层(如
high_value_customers,pending_orders)。
❌ 应避免使用视图的场景:
- 需要传入运行时参数(标准 SQL 视图不支持参数);
- 追求极致性能且数据量极大(考虑物化视图或应用缓存);
- 深度嵌套(如视图 A → 视图 B → 视图 C),导致优化器失效。
二、存储过程(Stored Procedure):数据库中的“自动化脚本”
2.1 标准定义(ISO/IEC 9075-4: SQL/PSM)
存储过程(Stored Procedure) 是一个命名的、持久化的程序模块,由一组 SQL 语句和过程化控制结构(如变量声明、条件判断、循环、异常处理)组成,可接受输入(IN)、输出(OUT)或双向(INOUT)参数,并通过
CALL语句调用执行。其主要用途是封装复杂的业务逻辑和数据操作流程。
形式化语法(简化):
CREATE PROCEDURE proc_name ( [ parameter_declaration [, ...] ] )[ characteristic ... ]routine_body
2.2 通俗解释
你可以把存储过程理解为:
“数据库里的自动化机器人”。
你告诉它:“当我调用你时,请按顺序做这几件事:先检查库存,再扣减数量,然后记录日志,最后发通知。”
它就会忠实地执行这一整套流程,并且可以在过程中“记住中间结果”(变量)、“根据情况做不同事”(IF/ELSE)、“重复做事”(LOOP)。
✅ 举个例子:银行转账
你不想让应用层分别执行“扣款”和“入账”两个操作(万一中间断了怎么办?),于是写一个存储过程:
CREATE PROCEDURE Transfer(IN from_account INT,IN to_account INT,IN amount DECIMAL(12,2),OUT result_message VARCHAR(100)
)
BEGINDECLARE EXIT HANDLER FOR SQLEXCEPTIONBEGINROLLBACK;SET result_message = '转账失败:系统异常';END;START TRANSACTION;UPDATE accounts SET balance = balance - amount WHERE id = from_account;UPDATE accounts SET balance = balance + amount WHERE id = to_account;INSERT INTO transaction_log VALUES (..., 'SUCCESS');COMMIT;SET result_message = '转账成功';
END;
应用只需调用:
CALL Transfer(1001, 1002, 500.00, @msg);
SELECT @msg;
——整个流程原子执行,要么全成功,要么全回滚。
2.3 核心特性详解
| 特性 | 技术说明 | 通俗类比 |
|---|---|---|
| 过程化逻辑 | 支持变量、IF、WHILE、异常处理等 | 机器人能“思考”和“决策” |
| DML/DDL 支持 | 可执行 INSERT/UPDATE/DELETE/CREATE 等 | 能实际“动手操作货架” |
| 事务控制 | 可包含 COMMIT/ROLLBACK(行为因 DBMS 而异) | 能保证“一套动作要么全做完,要么全不算” |
| 参数模式 | 支持 IN(输入)、OUT(输出)、INOUT(双向) | 能接收指令,也能返回结果 |
| 无直接返回值 | 不能像函数那样“返回一个值”,但可通过 OUT 参数传回 | 机器人不会“吐出一个数字”,但会“写一张纸条给你” |
| 调用方式 | 必须通过 CALL 或 EXECUTE 单独调用 | 你需要明确“启动机器人”,不能把它塞进一句话里 |
2.4 事务语义差异(重要!)
不同数据库对存储过程中事务的处理不同,这是工程实践中极易踩坑的点:
| 数据库 | 默认事务行为 | 注意事项 |
|---|---|---|
| Oracle | 过程内 COMMIT 会提交整个事务 | 若需局部提交,需使用“自治事务(Autonomous Transaction)” |
| SQL Server | 支持嵌套事务,COMMIT 仅减少计数器 | 需配合 @@TRANCOUNT 使用 |
| PostgreSQL | v11+ 支持 PROCEDURE 中的 COMMIT/ROLLBACK | 旧版本只能用函数(不能控制事务) |
| MySQL | 默认自动提交,需显式 START TRANSACTION | 过程内可控制事务,但需谨慎 |
✅ 最佳实践:事务边界尽量在应用层控制,存储过程内避免
COMMIT,除非有强一致性要求。
2.5 使用场景总结
✅ 典型适用场景:
- 执行多步骤、强一致性的业务流程(如订单创建、支付、库存扣减);
- 批量数据处理(ETL、数据归档、清洗);
- 封装敏感操作(如删除用户数据),仅暴露过程接口;
- 减少网络往返(一次调用执行多条 SQL,提升性能);
- 实现数据库端的定时任务(配合调度器)。
❌ 应谨慎使用的场景:
- 简单 CRUD 操作(过度封装反而增加复杂度);
- 核心业务逻辑(难以测试、调试、版本控制);
- 跨数据库迁移项目(存储过程语法差异极大)。
三、函数(Function):SQL 表达式中的“计算器”
3.1 标准定义(ISO/IEC 9075-4: SQL/PSM)
函数(Function) 是一个必须返回单一值(标量值或表)的命名程序单元,可在 SQL 表达式中直接调用(如
SELECT、WHERE、ORDER BY子句)。函数的设计初衷是封装无副作用的计算逻辑,强调确定性与可复用性。
SQL 标准将函数分为两类:
- 标量函数(Scalar Function):返回单个值(如
UPPER('abc') → 'ABC'); - 表值函数(Table-Valued Function, TVF):返回结果集(如 Oracle 的 pipelined function)。
形式化语法(标量函数):
CREATE FUNCTION func_name ( param type [, ...] )
RETURNS return_type
[ DETERMINISTIC | NOT DETERMINISTIC ]
[ READS SQL DATA | MODIFIES SQL DATA | NO SQL ]
routine_body
3.2 通俗解释
你可以把函数理解为:
“一个插在 SQL 语句里的小计算器”。
它不能自己“做事”,但能在你写查询的时候,“当场帮你算一个值”。
✅ 举个例子:计算个人所得税
你不想在应用里写税率逻辑,也不想每次手动算,于是创建一个函数:
CREATE FUNCTION calc_tax(income NUMERIC)
RETURNS NUMERIC
DETERMINISTIC
READS SQL DATA
BEGINIF income <= 5000 THEN RETURN 0;ELSEIF income <= 10000 THEN RETURN (income - 5000) * 0.1;ELSE RETURN 500 + (income - 10000) * 0.2;END IF;
END;
然后在查询中直接使用:
SELECT name, salary, calc_tax(salary) AS tax FROM employees;
——就像在 Excel 里写 =TAX(A2) 一样自然!
3.3 副作用与 DML 限制(关键!)
SQL 标准不禁止函数包含 DML(如 INSERT),但强烈反对,因为:
- 函数被设计用于表达式上下文(如
SELECT),而SELECT应是只读操作; - 如果函数偷偷改了数据,会导致查询结果不可预测,破坏 ACID 语义。
各数据库的实际策略:
| 数据库 | 允许函数含 DML? | 能否在 SELECT 中调用含 DML 的函数? |
|---|---|---|
| Oracle | ✅ 允许 | ❌ 禁止(报错 ORA-14551) |
| SQL Server | ✅ 允许 | ⚠️ 在某些上下文(如计算列)中禁止 |
| PostgreSQL | ✅ 允许 | ✅ 允许(但需声明 VOLATILE) |
| MySQL | ❌ 严格禁止 | — |
✅ 黄金法则:函数应是“纯函数”——相同输入永远返回相同输出,且不修改任何外部状态。
3.4 表值函数(TVF):带参数的“动态视图”
由于标准视图不支持参数,表值函数(TVF) 成为实现“参数化查询”的标准方案。
✅ 示例(SQL Server):
CREATE FUNCTION GetEmployeesByDept(@dept_id INT)
RETURNS TABLE
AS
RETURN (SELECT id, name, salaryFROM employeesWHERE dept_id = @dept_id
);
使用:
SELECT * FROM GetEmployeesByDept(10);
💡 通俗理解:
视图 = 固定菜单;
表值函数 = “告诉我你想吃什么部门,我给你列出来”。
| 数据库 | 是否支持 TVF |
|---|---|
| Oracle | ✅(Pipelined Functions) |
| SQL Server | ✅(内联 TVF 性能极佳) |
| PostgreSQL | ✅(RETURNS TABLE (...)) |
| MySQL | ❌(仅支持标量函数) |
3.5 使用场景总结
✅ 强烈推荐使用函数的场景:
- 字段格式化(如
format_phone(phone)); - 数值计算(税率、折扣、年龄、评分);
- 数据验证(如
is_valid_email(email)); - 构建生成列(Generated Columns);
- 在
WHERE中复用复杂条件(如WHERE is_weekend(order_date))。
❌ 应避免的场景:
- 在函数中执行 DML 或事务操作;
- 实现复杂业务流程(应交给存储过程或应用层);
- 依赖会话变量或非确定性操作(除非明确声明
NOT DETERMINISTIC)。
四、三者终极对比:一张表看懂本质区别
| 维度 | 视图(View) | 存储过程(Stored Procedure) | 函数(Function) |
|---|---|---|---|
| 本质 | 虚拟表(结果集) | 自动化脚本(执行单元) | 计算器(表达式组件) |
| 返回什么 | 多行多列的表 | 无直接返回值(可通过 OUT 参数返回多个值) | 一个值(标量)或一张表(TVF) |
| 怎么调用 | SELECT * FROM view_name | CALL proc_name(...) | SELECT func(col) FROM t |
| 能改数据吗 | ❌(定义中仅 SELECT) | ✅ | ⚠️ 技术可能,但应禁止 |
| 能控制事务吗 | ❌ | ✅ | ❌ |
| 支持参数吗 | ❌(标准 SQL 不支持) | ✅(IN/OUT/INOUT) | ✅(通常仅 IN) |
| 能在 SQL 表达式中用吗 | ✅(作为表源) | ❌ | ✅(作为值) |
| 主要用途 | 简化查询、安全控制 | 执行流程、批处理、事务 | 封装计算、字段转换 |
| 副作用 | 无 | 允许 | 应无(纯函数) |
| 可移植性 | 高(标准支持好) | 低(语法差异大) | 中(TVF 支持不一) |
五、如何选择?—— 决策树
你想做什么?
│
├─ 想把一个复杂查询当作一张表来用? ──→ 视图(View)
│
├─ 需要传参数的动态查询结果?
│ ├─ 数据库支持 TVF? ──→ 表值函数(TVF)
│ └─ 不支持? ──→ 存储过程 + 临时表 / 应用层拼接
│
├─ 想在 SELECT/WHERE 中计算一个值? ──→ 标量函数(Function)
│
├─ 要执行一系列 DML 并保证原子性? ──→ 存储过程(Procedure)
│
├─ 要实现行/列级安全? ──→ 视图 + 权限控制
│
└─ 要高性能缓存结果? ├─ 支持物化视图? ──→ 物化视图└─ 否? ──→ 应用层缓存
六、最佳实践与反模式
✅ 推荐实践
- 优先使用视图:它是标准、安全、可移植的数据抽象方式;
- 函数保持纯净:无 DML、无会话依赖、声明
DETERMINISTIC(若适用); - 过程用于“动作”:只封装必须在数据库端完成的事务性操作;
- 避免逻辑下沉:核心业务规则放在应用层,便于测试与迭代;
- 文档化与版本控制:将数据库对象纳入 Git + CI/CD 流程。
❌ 经典反模式
| 反模式 | 风险 |
|---|---|
| 在函数中写 INSERT/UPDATE | 导致查询产生副作用,破坏数据一致性 |
| 用存储过程实现整个业务逻辑 | 代码难以测试、调试、重构,形成“数据库黑盒” |
| 深度嵌套视图(>3层) | 优化器无法有效推导执行计划,性能急剧下降 |
| 依赖 MySQL 特有语法写过程 | 未来迁移到 PostgreSQL/Oracle 时成本极高 |
| 视图中使用 SELECT * | 基表新增列后可能导致应用崩溃 |
