Node系列 · 数据库:视图
视图(View)是"虚拟表"——查询时按定义动态生成结果,不存储实际数据。视图能简化复杂 JOIN、控制数据访问权限、抽象业务规则——但性能与可维护性有 trade-off,需要明确何时该用、何时该拆。
一、什么是视图
视图是包装好的 SELECT 语句,对外表现像一张表:
sql
-- 创建一个视图:学生 + 班级 + 综合成绩
CREATE VIEW `v_student_score` AS
SELECT
s.id AS `student_id`,
s.name AS `student_name`,
s.class_id AS `class_id`,
c.name AS `class_name`,
sc.subject,
sc.score
FROM `student` s
JOIN `class` c ON c.id = s.class_id
JOIN `score` sc ON sc.student_id = s.id;之后可以像查表一样查视图:
sql
SELECT * FROM `v_student_score` WHERE `class_name` = '高三一班';INFO
视图不存储数据。每次查视图,MySQL 都会执行它背后的 SELECT 语句。数据始终在原表里。
二、视图的核心作用
2.1 简化复杂查询
把多表 JOIN、聚合、过滤封装成视图,业务代码直接 SELECT * FROM v_xxx:
sql
-- 没有视图:每次都要写完整 JOIN
SELECT s.name, SUM(sc.score) AS total
FROM student s
JOIN score sc ON sc.student_id = s.id
WHERE s.class_id = 3
GROUP BY s.id;
-- 有视图:业务代码只关心简单查询
SELECT `student_name`, SUM(`score`) AS total
FROM `v_student_score`
WHERE `class_id` = 3
GROUP BY `student_id`;2.2 数据权限隔离
让不同角色看到不同字段:
sql
-- 给运营看的视图:脱敏手机号
CREATE VIEW `v_student_for_ops` AS
SELECT
s.id,
s.name,
s.class_id,
CONCAT(LEFT(s.`phone`, 3), '****', RIGHT(s.`phone`, 4)) AS `phone_masked`
FROM `student` s;
-- 运营账号只有 v_student_for_ops 的权限,看不到真实手机号2.3 抽象业务规则
"近 30 天活跃用户" 这种业务口径经常变。封装成视图,调用方不感知:
sql
CREATE VIEW `v_active_users_30d` AS
SELECT DISTINCT `user_id`
FROM `login_log`
WHERE `created_at` >= DATE_SUB(NOW(), INTERVAL 30 DAY);三、创建与管理视图
3.1 创建
sql
CREATE VIEW `v_xxx` AS
SELECT ...;3.2 替换已有视图
sql
CREATE OR REPLACE VIEW `v_xxx` AS
SELECT ...;3.3 删除
sql
DROP VIEW IF EXISTS `v_xxx`;3.4 查看视图定义
sql
SHOW CREATE VIEW `v_xxx`;四、视图的限制
4.1 视图不一定能更新
视图背后是 SELECT,有些视图是只读的,不能 INSERT / UPDATE / DELETE:
| 视图定义 | 是否可更新 |
|---|---|
| 单表、无聚合、无 DISTINCT、无 GROUP BY | ✅ 可更新 |
| 多表 JOIN | ❌ 不可更新 |
含聚合函数(COUNT / SUM) | ❌ 不可更新 |
含 DISTINCT / GROUP BY / HAVING | ❌ 不可更新 |
含 UNION | ❌ 不可更新 |
子查询引用 FROM 的表 | ❌ 不可更新 |
WARNING
MySQL 允许"看似可更新"的视图背后静默失败——比如 INSERT INTO v_xxx 没报错但实际没生效。生产环境对视图的写操作要谨慎,最好绕过视图直接改原表。
4.2 视图性能不一定好
每次查视图都执行一次底层 SELECT。如果视图背后 JOIN 了 5 张表又没建索引,比直接写 JOIN 更慢。
sql
EXPLAIN SELECT * FROM `v_student_score` WHERE `class_name` = '高三一班';
-- type=ALL + rows=100 万 → 全表扫描,性能差优化方法:
- 给底层表的关键字段建索引
- 视图里加
WHERE条件过滤 - 用 物化视图(MySQL 不支持,但可以定时 INSERT INTO 一张真实表)
4.3 视图嵌套有上限
MySQL 视图嵌套层数有上限(默认不高),复杂业务不要套太多层。
五、视图 vs 临时表 vs 子查询
| 维度 | 视图 | 临时表 | 子查询 |
|---|---|---|---|
| 存储 | 不存储(虚拟) | 存储在内存 / 磁盘 | 不存储 |
| 复用 | 多查询可复用 | 会话内复用 | 单查询内 |
| 索引 | 不能加索引 | 能加索引 | 不能 |
| 适用 | 稳定抽象层 | 复杂中间计算 | 一次性查询 |
sql
-- 视图:稳定抽象,全局可复用
CREATE VIEW v_top_students AS
SELECT student_id, AVG(score) AS avg_score
FROM score
GROUP BY student_id
HAVING avg_score > 90;
-- 临时表:会话内的中间结果,可建索引
CREATE TEMPORARY TABLE tmp_score AS
SELECT student_id, AVG(score) AS avg_score
FROM score
GROUP BY student_id;
ALTER TABLE tmp_score ADD INDEX idx_avg (avg_score);
-- 子查询:一次性
SELECT * FROM (
SELECT student_id, AVG(score) AS avg_score
FROM score
GROUP BY student_id
) t WHERE avg_score > 90;六、Node 端使用视图
视图对应用层是透明的——当表用:
javascript
const mysql = require('mysql2/promise');
// 视图对 Node 来说就是一张普通表
const [rows] = await pool.execute(
'SELECT * FROM v_student_score WHERE class_id = ? ORDER BY score DESC LIMIT 10',
[3]
);七、何时用视图
✅ 适合用:
- 复杂 JOIN 的复用(不同业务都要查同一份数据)
- 权限隔离(不同角色看不同字段)
- 业务口径封装("活跃用户"等)
❌ 不适合:
- 性能关键的查询(视图不能加索引,复杂视图性能差)
- 频繁变更的业务逻辑(改视图定义比改应用代码麻烦)
- 多层嵌套(层数多难调试)
八、小结
- 视图 = 包装好的 SELECT,对外是虚拟表;不存储数据
- 三大作用:简化查询 / 权限隔离 / 抽象业务规则
- 多表 JOIN、聚合、DISTINCT 视图不能更新
- 视图性能 = 底层 SELECT 性能;复杂视图要靠索引优化
- 与临时表 / 子查询的取舍:视图是稳定抽象层,临时表是会话中间计算,子查询是一次性
