视图一行数据都不存,我为什么还敢把核心报表全压在它上面?
有段三表连接加聚合的统计 SQL,被我抄进了十几个报表和定时任务;底表字段一改名,全局搜索改了一下午,还是漏挂了两个生产报表。
这篇把视图(View)的创建、修改、删除、可更新判定一次讲透,看完你能把重复 SQL 收敛到一处,也能避开我踩过的四个坑。

一、场景:同一段 SQL 抄了十几遍,改一处漏一片
报表要展示员工、部门、城市,还要带部门平均工资,原始写法是一段三表连接加窗口函数,每个用到它的服务都得抄一遍。
sql
SELECT e.employee_id,
e.last_name,
d.department_name,
l.city,
AVG(e.salary) OVER (PARTITION BY e.department_id) AS dept_avg_salary
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN locations l ON d.location_id = l.location_id;
这段 SQL 出现在报表服务、定时任务、导出接口里。连接逻辑一改,十几处都得同步;更麻烦的是,每个开发抄的版本还略有出入,统计口径慢慢就对不上了。
问题就变成:同一段查询逻辑,能不能只写一份、处处复用?
二、踩坑:视图上线后,四个坑轮流找上门
我把它封成视图,确实省事,可新坑在一周内就凑齐了。
| 坑 | 现象 | 触发时机 |
|---|---|---|
| 底表改列,视图失效 | 查询视图直接报列不存在,Oracle 中视图变 INVALID | 基础表 DROP / RENAME 列,视图仍引用旧列 |
| 视图写不进去 | 报目标表不可更新、非键保留表 | 视图含多表连接或聚合,却执行 UPDATE / INSERT |
| 改完视图权限丢了 | 业务账号突然查不了视图 | 用 DROP 再 CREATE 的方式改视图,授权未重建 |
| 视图套视图,性能崩 | 简单查询嵌套四五层,执行计划巨大 | 上层视图再建视图,优化器无法合并 |
这四个坑我都在生产环境背过。底表改列是结构问题,视图写不进去是定义问题,授权丢失是运维习惯问题,嵌套视图是性能问题;它们的根因都指向同一件事------视图到底是个什么对象?
三、底层原理:数据库里根本没有这张"表"
视图(View)是存储在数据字典(Data Dictionary)里的一条 SELECT 语句,对外呈现为虚拟表(Virtual Table)。普通视图不保存任何数据行,只保存定义本身。
查询视图时,数据库执行视图合并(View Merging):把视图定义中的 SELECT 与外层查询拼接改写,再基于基础表(Base Table)生成执行计划。整个过程对调用方透明,你以为在查一张表,引擎查的还是底表,三者的行为差别由此而来。
| 对比项 | 基础表 | 视图 | 子查询 |
|---|---|---|---|
| 是否存数据 | 存,占物理空间 | 不存,只存定义 | 不存 |
| 生命周期 | 持久 | 持久,可反复引用 | 只在当前语句内 |
| 能否独立授权 | 能 | 能 | 不能 |
| 结构变更影响 | 涉及数据迁移 | 依赖底表,可能失效 | 改完即走 |
注意:查视图,就是临时执行一段被保存的 SELECT;物理空间里没有这张表,只有这句话。
四、解决方案:建、改、删、授权一次做对
4.1 创建视图
标准语法如下,方括号为可选项。
sql
CREATE VIEW view_name [(column_name [, ...])]
AS
SELECT_statement
[WITH CHECK OPTION];
把开头那段查询收敛成视图,连接逻辑从此只有一份。
sql
CREATE VIEW emp_location_info AS
SELECT e.employee_id,
e.last_name,
d.department_name,
l.city
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN locations l ON d.location_id = l.location_id;
之后所有报表都查同一处,还可以直接追加条件。
sql
SELECT * FROM emp_location_info WHERE city = 'Seattle';
需要固定列名、屏蔽底层列名变化时,显式写出列清单。
sql
CREATE VIEW emp_location_info_v2 (emp_id, emp_name, dept_name, city) AS
SELECT e.employee_id, e.last_name, d.department_name, l.city
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN locations l ON d.location_id = l.location_id;
聚合统计也能封装,部门薪资汇总只需维护一次。
sql
CREATE VIEW dept_salary_summary AS
SELECT department_id,
COUNT(*) AS emp_count,
AVG(salary) AS avg_salary,
MAX(salary) AS max_salary
FROM employees
GROUP BY department_id;
4.2 修改视图:只替换定义,别裸 DROP
改视图有三种路径,权限结果完全不同。
| 方式 | 适用数据库 | 授权是否保留 |
|---|---|---|
| CREATE OR REPLACE VIEW | MySQL、Oracle | 保留 |
| ALTER VIEW | SQL Server、MySQL | 保留 |
| DROP + CREATE | 通用 | 丢失,需重新授权 |
MySQL、Oracle 用替换写法。
sql
CREATE OR REPLACE VIEW emp_location_info AS
SELECT e.employee_id,
e.last_name,
d.department_name,
l.city,
l.state_province
FROM employees e
JOIN departments d ON e.department_id = d.department_id
JOIN locations l ON d.location_id = l.location_id;
SQL Server 用 ALTER 写法。
sql
ALTER VIEW emp_location_info AS
SELECT ... ;
替换只改定义,视图对象本身没被删除,GRANT 出去的权限还在;DROP 再 CREATE 属于新建对象,旧授权不会跟着回来。
4.3 删除视图
sql
DROP VIEW view_name;
视图可能不存在、不想让脚本报错时,用 IF EXISTS。
sql
DROP VIEW IF EXISTS view_name;
删视图只删定义,基础表的数据一行不动;但其他视图、存储过程若引用了它,会变成失效状态,需要重新编译或修复。
4.4 用视图做行列权限隔离
employees 表含工资等敏感列。给外部账号只开放编号、姓名、部门三列,先建视图。
sql
CREATE VIEW emp_public_info AS
SELECT employee_id, last_name, department_id
FROM employees;
只授权视图,不授权基础表。
sql
GRANT SELECT ON emp_public_info TO user_a;
限制行同理,只允许看到 10 号部门的数据。
sql
CREATE VIEW emp_dept10 AS
SELECT employee_id, last_name, salary, department_id
FROM employees
WHERE department_id = 10;
4.5 可更新视图与 WITH CHECK OPTION
视图支持 INSERT、UPDATE、DELETE,要同时满足下列条件。
| 条件 | 要求 |
|---|---|
| 数据来源 | 仅基于单个基础表 |
| 禁止结构 | 无 GROUP BY、HAVING、聚合函数、DISTINCT、UNION |
| 列要求 | 无计算列,包含主键或唯一键用于定位行 |
带 WHERE 条件的可更新视图,加上检查选项(WITH CHECK OPTION),可以防止写入后数据"逃出"视图范围。
sql
CREATE VIEW v_dept10_check AS
SELECT employee_id, last_name, salary, department_id
FROM employees
WHERE department_id = 10
WITH CHECK OPTION;

注意:改视图只换定义,DROP 再 CREATE 会把授权一起带走;改视图的默认动作是替换,不是重建。
五、实操验证:判定、拦截、依赖三个动作
5.1 先判定视图能不能写
拿到一个陌生视图,按下面的顺序过一遍定义。
latex
┌────────────────────────────────────┐
│ 检查视图定义 │
└──────────────┬─────────────────────┘
▼
┌──────────────────────────────────────┐
│ 是否命中以下任一项? │
│ · 多表 JOIN / 子查询 │
│ · GROUP BY / 聚合函数 / HAVING │
│ · DISTINCT / UNION │
│ · 计算列 / 伪列 │
└──────────────┬───────────────────────┘
│
┌───────┴────────┐
▼ 命中任一项 ▼ 全部没有,且含主键/唯一键
┌───────────────┐ ┌──────────────┐
│ 只读,不可更新 │ │ 可更新视图 │
└───────────────┘ └──────────────┘
5.2 验证 CHECK OPTION 的拦截
视图内部调整工资,部门没变,正常执行。
sql
UPDATE v_dept10_check
SET salary = 12000
WHERE employee_id = 200;
latex
Query OK, 1 row affected
更新后仍满足 department_id = 10,语句放行。
尝试把部门改成 20。
sql
UPDATE v_dept10_check
SET department_id = 20
WHERE employee_id = 200;
latex
ERROR 1369 (HY000): CHECK OPTION failed 'hr.v_dept10_check'
更新后该行不再属于视图,被 WITH CHECK OPTION 拦截,数据未变更。
5.3 动底表前先查依赖
基础表改列前,先确认哪些视图引用了它,各库查询入口如下。
| 数据库 | 依赖/定义查询对象 |
|---|---|
| Oracle | USER_DEPENDENCIES、ALL_DEPENDENCIES |
| SQL Server | sys.sql_expression_dependencies |
| MySQL | information_schema.VIEWS |
| PostgreSQL | pg_depend、information_schema.view_table_usage |
MySQL 下直接查视图定义。
sql
SELECT table_name, view_definition
FROM information_schema.views
WHERE table_schema = 'hr';
变更窗口里先跑一遍依赖查询,再决定改列还是先替换视图,能省掉一次视图失效告警。
注意:视图能不能写,定义说了算;写不进去先查 SELECT 结构,别硬怼。
六、总结延伸:什么时候该用,什么时候别碰
| 场景 | 建议 |
|---|---|
| 多表连接、聚合统计反复出现 | 用视图封装,统一口径 |
| 敏感列、敏感行需要隔离 | 用视图 + 只授视图权限 |
| 表结构变更、旧系统兼容 | 用视图维持原接口 |
| 高频复杂查询、性能敏感 | 普通视图无效,考虑物化视图 |
| 视图上再套多层视图 | 别这么干,合并难度和性能都会失控 |
普通视图不存数据,每次查询动态展开;物化视图(Materialized View)会把结果集真实存下来,用刷新换性能,是数据仓库里的主力。

注意:视图是权限边界,不是安全边界;是逻辑封装,不是性能优化。
参考链接
- MySQL 8.4 Reference Manual --- CREATE VIEW
- Oracle Database 19c --- CREATE VIEW
- SQL Server --- CREATE VIEW
- PostgreSQL --- CREATE VIEW
术语速查表
| 术语 | 英文 | 解释 |
|---|---|---|
| 视图 | View | 存储在数据字典中的 SELECT 定义,虚拟表 |
| 基础表 | Base Table | 真实存放数据的表,视图的数据来源 |
| 数据字典 | Data Dictionary | 存储数据库对象定义的系统表集合 |
| 视图合并 | View Merging | 查询视图时将视图定义与外层 SQL 拼接改写 |
| 检查选项 | WITH CHECK OPTION | 限制通过视图写入后数据必须仍满足视图条件 |
| 物化视图 | Materialized View | 实际存储查询结果的视图,以刷新换性能 |
| 行级安全 | Row Level Security | 按行控制数据可见性的数据库安全策略 |