MySQL 系列:第9篇 视图——定制化数据窗口

IT策士 10余年一线大厂经验,专注 IT 思维、架构、职场进阶。我会在各个平台持续发布最新文章,助你少走弯路。


经过前几篇的学习,你已经能写出复杂的 JOIN 和子查询了。但复杂的 SQL 往往又长又难读,每次都要重复编写,而且直接暴露表结构给应用程序会带来安全隐患。有没有一种方式,能把复杂查询"封装"成一个简单的"虚拟表"?这就是视图(View)。今天我们用 Python 实战,掌握视图的创建、使用、修改和最佳实践。

1. 准备数据:多表电商场景

沿用之前的三表结构,增加一些数据以便演示。

bash 复制代码
import mysql.connector

conn = mysql.connector.connect(
    host="127.0.0.1", port=3306,
    user="root", password="MyNewPass123!",
    database="shop"
)
cursor = conn.cursor()

# 确保表存在(如已存在则保留)
cursor.execute("""
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    city VARCHAR(20),
    vip_level TINYINT DEFAULT 0
) ENGINE=InnoDB
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(200) NOT NULL,
    price DECIMAL(10,2) NOT NULL,
    category VARCHAR(30)
) ENGINE=InnoDB
""")
cursor.execute("""
CREATE TABLE IF NOT EXISTS orders (
    id INT AUTO_INCREMENT PRIMARY KEY,
    user_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT NOT NULL,
    order_date DATE NOT NULL
) ENGINE=InnoDB
""")

# 清空并灌入测试数据
cursor.execute("TRUNCATE users")
cursor.execute("TRUNCATE products")
cursor.execute("TRUNCATE orders")

users = [
    (1, "张三", "北京", 2),
    (2, "李四", "上海", 1),
    (3, "王五", "深圳", 0),
    (4, "赵六", "杭州", 1),
]
cursor.executemany("INSERT INTO users (id,name,city,vip_level) VALUES (%s,%s,%s,%s)", users)

products = [
    (1, "机械键盘", 399.00, "电脑外设"),
    (2, "蓝牙耳机", 259.00, "音频设备"),
    (3, "显示器", 1899.00, "电脑外设"),
    (4, "鼠标", 99.00, "电脑外设"),
]
cursor.executemany("INSERT INTO products (id,title,price,category) VALUES (%s,%s,%s,%s)", products)

orders = [
    (1, 1, 1, 2, "2025-07-01"),
    (2, 2, 2, 1, "2025-07-05"),
    (3, 3, 1, 1, "2025-07-10"),
    (4, 1, 3, 1, "2025-07-12"),
    (5, 2, 1, 1, "2025-07-15"),
]
cursor.executemany("INSERT INTO orders (id,user_id,product_id,quantity,order_date) VALUES (%s,%s,%s,%s,%s)", orders)
conn.commit()
print("✅ 测试数据准备完毕")

2. 什么是视图?

视图可以理解为一个保存下来的查询 ,它有名字,可以像表一样被 SELECT 引用。但视图本身不存储数据,只是封装了 SQL 逻辑,每次查询视图时,MySQL 会动态执行其定义的查询。

核心好处

  • 简化复杂查询 :把多表 JOIN、聚合计算封装成视图,后续只需 SELECT * FROM view_name

  • 安全隔离:只暴露需要的列给应用,隐藏底层表结构和敏感字段。

  • 逻辑抽象:表结构变更时,只需修改视图定义,应用代码无需改动。

3. 创建视图

3.1 基础语法

bash 复制代码
CREATE [OR REPLACE] VIEW 视图名 [(列别名列表)] AS
SELECT 语句
[WITH CHECK OPTION];

OR REPLACE:如果视图已存在则替换,常用于更新视图逻辑。

3.2 实战:创建订单概览视图

我们要频繁查询"订单号、客户姓名、商品名称、金额、日期",每次都写三表 JOIN 太麻烦。直接创建视图:

bash 复制代码
create_view_sql = """
CREATE OR REPLACE VIEW order_summary AS
SELECT 
    o.id AS order_id,
    u.name AS customer,
    p.title AS product,
    p.price * o.quantity AS total_amount,
    o.order_date,
    o.quantity
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN products p ON o.product_id = p.id
"""
cursor.execute(create_view_sql)
print("✅ 视图 order_summary 创建成功")

# 像查表一样查询视图
cursor.execute("SELECT * FROM order_summary ORDER BY total_amount DESC")
print("\n📊 订单概览(通过视图):")
for row in cursor.fetchall():
    print(f"  订单#{row[0]} {row[1]} 购买 {row[2]} 金额¥{row[3]} 日期{row[4]}")

预期输出

bash 复制代码
✅ 视图 order_summary 创建成功

📊 订单概览(通过视图):
  订单#4 张三 购买 显示器 金额¥1899.00 日期2025-07-12
  订单#1 张三 购买 机械键盘 金额¥798.00 日期2025-07-01
  订单#3 王五 购买 机械键盘 金额¥399.00 日期2025-07-10
  订单#5 李四 购买 机械键盘 金额¥399.00 日期2025-07-15
  订单#2 李四 购买 蓝牙耳机 金额¥259.00 日期2025-07-05

现在应用程序只需记住简单的视图名,不用再重复编写多表 JOIN,查询语句从 10 行变成 1 行。

4. 视图的高级特性

4.1 视图上的聚合:用户消费统计

bash 复制代码
cursor.execute("""
CREATE OR REPLACE VIEW user_spending AS
SELECT 
    u.id AS user_id,
    u.name,
    u.city,
    COUNT(o.id) AS order_count,
    COALESCE(SUM(p.price * o.quantity), 0) AS total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY u.id, u.name, u.city
""")
print("✅ 视图 user_spending 创建成功")

cursor.execute("SELECT * FROM user_spending ORDER BY total_spent DESC")
print("\n📊 用户消费统计:")
for row in cursor.fetchall():
    print(f"  {row[1]} ({row[2]}) 订单数:{row[3]} 总消费:¥{row[4]}")

预期输出

bash 复制代码
✅ 视图 user_spending 创建成功

📊 用户消费统计:
  张三 (北京) 订单数:2 总消费:¥2697.00
  李四 (上海) 订单数:2 总消费:¥658.00
  王五 (深圳) 订单数:1 总消费:¥399.00
  赵六 (杭州) 订单数:0 总消费:¥0.00

4.2 为视图列起别名

创建视图时可以覆盖默认列名:

bash 复制代码
cursor.execute("""
CREATE OR REPLACE VIEW user_spending_v2 (用户ID, 姓名, 城市, 订单数, 消费总额) AS
SELECT u.id, u.name, u.city, COUNT(o.id), COALESCE(SUM(p.price * o.quantity), 0)
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
LEFT JOIN products p ON o.product_id = p.id
GROUP BY u.id, u.name, u.city
""")
cursor.execute("SELECT * FROM user_spending_v2 WHERE 消费总额 > 500")
print("\n💰 消费总额 > 500 的用户:")
for row in cursor.fetchall():
    print(f"  {row[1]} {row[3]}单 ¥{row[4]}")

4.3 WITH CHECK OPTION:约束数据修改

对于可更新视图,WITH CHECK OPTION 可以确保通过视图插入或修改的数据不会"消失"在视图之外。

bash 复制代码
# 创建仅包含 VIP 用户的视图
cursor.execute("""
CREATE OR REPLACE VIEW vip_users AS
SELECT id, name, city, vip_level
FROM users
WHERE vip_level >= 1
WITH CHECK OPTION
""")

# 尝试通过视图插入一个非 VIP 用户(vip_level=0)
try:
    cursor.execute("INSERT INTO vip_users (name, city, vip_level) VALUES ('孙七', '北京', 0)")
    conn.commit()
except mysql.connector.IntegrityError as e:
    print(f"❌ 插入被拒绝(不符合视图条件): {e}")

预期输出

bash 复制代码
❌ 插入被拒绝(不符合视图条件): 1369 (HY000): CHECK OPTION failed 'shop.vip_users'

WITH CHECK OPTION 确保通过视图插入的数据一定满足视图的 WHERE 条件,防止"插进去却查不出来"的怪异情况。

5. 可更新视图的限制

并非所有视图都能执行 INSERT/UPDATE/DELETE。以下情况会导致视图不可更新

  • 定义中包含 GROUP BYHAVINGDISTINCT、聚合函数

  • 使用了 UNION / UNION ALL

  • FROM 子句包含不可更新的子查询

  • 引用了多个表(除非用 INNER JOIN 且满足特定条件)

判断方法 :查询 information_schema.VIEWSIS_UPDATABLE 字段。

bash 复制代码
cursor.execute("""
SELECT TABLE_NAME, IS_UPDATABLE 
FROM information_schema.VIEWS 
WHERE TABLE_SCHEMA = 'shop'
""")
print("\n🔍 视图可更新性:")
for row in cursor.fetchall():
    print(f"  {row[0]}: {'✅ 可更新' if row[1] == 'YES' else '❌ 不可更新'}")

预期输出

bash 复制代码
🔍 视图可更新性:
  order_summary: ❌ 不可更新
  user_spending: ❌ 不可更新
  user_spending_v2: ❌ 不可更新
  vip_users: ✅ 可更新

6. 修改与删除视图

bash 复制代码
# 修改视图(OR REPLACE)
cursor.execute("""
CREATE OR REPLACE VIEW vip_users AS
SELECT id, name, city, vip_level
FROM users
WHERE vip_level >= 2
""")

# 删除视图
cursor.execute("DROP VIEW IF EXISTS user_spending_v2")
print("✅ 视图 user_spending_v2 已删除")

# 查看所有视图
cursor.execute("SHOW FULL TABLES WHERE Table_type = 'VIEW'")
print("\n📋 当前数据库中的视图:")
for row in cursor.fetchall():
    print(f"  {row[0]}")

7. 视图的性能考量与陷阱

常见误区 :视图只是 SQL 的快捷方式,不会提升性能。每次查询视图,MySQL 都会执行底层定义的查询。

陷阱示例:两个视图嵌套使用

bash 复制代码
# 视图1:高消费用户
cursor.execute("""
CREATE OR REPLACE VIEW high_spenders AS
SELECT name, total_spent FROM user_spending WHERE total_spent > 500
""")

# 视图2:高消费用户的订单(嵌套视图)
cursor.execute("""
CREATE OR REPLACE VIEW high_spender_orders AS
SELECT os.*
FROM order_summary os
INNER JOIN high_spenders hs ON os.customer = hs.name
""")

cursor.execute("SELECT * FROM high_spender_orders")

看似方便,但实际执行时 high_spender_orders 会展开成 order_summary + user_spending 的嵌套查询,性能可能非常差。嵌套视图是性能杀手,应该避免。

最佳实践

  • 视图不要嵌套超过两层

  • 复杂查询用 EXPLAIN 分析视图背后的执行计划

  • 对于频繁查询且数据变化不频繁的场景,可考虑物化视图(MySQL 不原生支持,可用表 + 定时任务模拟)

8. Python 封装:透明使用视图

我们可以像操作普通表一样操作视图,从而让代码更清晰:

bash 复制代码
class ReportService:
    def __init__(self, conn):
        self.conn = conn
        self.cursor = conn.cursor()
    
    def get_user_spending(self, min_amount=0):
        """获取消费统计(通过视图)"""
        sql = "SELECT * FROM user_spending WHERE total_spent >= %s ORDER BY total_spent DESC"
        self.cursor.execute(sql, (min_amount,))
        return self.cursor.fetchall()
    
    def get_order_summary(self, customer_name=None):
        """获取订单概览"""
        if customer_name:
            self.cursor.execute("SELECT * FROM order_summary WHERE customer = %s", (customer_name,))
        else:
            self.cursor.execute("SELECT * FROM order_summary ORDER BY order_date")
        return self.cursor.fetchall()

# 使用
report = ReportService(conn)
print("\n📊 张三的订单:")
for row in report.get_order_summary("张三"):
    print(f"  {row[2]} ¥{row[3]}")

print("\n📊 消费超过 500 的用户:")
for row in report.get_user_spending(500):
    print(f"  {row[1]} ¥{row[4]}")

9. 动手试试:定制你的视图

基于现有三表,完成以下练习:

  1. 创建一个视图 product_sales,显示每件商品的名称、类别、销售总次数、销售总金额,并过滤掉从未出售的商品。

  2. 通过 product_sales 查询最畅销的商品(销售总次数最多)。

  3. 尝试通过视图 product_sales 插入一条记录,观察结果并解释为什么。

  4. 创建一个只读视图 beijing_users ,只包含北京用户,并附带 WITH CHECK OPTION。测试向其插入"上海"用户是否会被拒绝。

提示

  • 第 1 题用 GROUP BY + HAVING COUNT(o.id) > 0

  • 第 3 题中,product_sales 包含聚合函数,不可更新,会报错。

  • 第 4 题用 WHERE city = '北京'WITH CHECK OPTION

参考代码:

bash 复制代码
# 1. 创建 product_sales 视图
cursor.execute("""
CREATE OR REPLACE VIEW product_sales AS
SELECT p.title, p.category, COUNT(o.id) AS sales_count, SUM(p.price * o.quantity) AS total_revenue
FROM products p
INNER JOIN orders o ON p.id = o.product_id
GROUP BY p.id, p.title, p.category
""")

# 2. 查询最畅销
cursor.execute("SELECT * FROM product_sales ORDER BY sales_count DESC LIMIT 1")
print("最畅销商品:", cursor.fetchone())

10. 总结

今天我们学会了视图的核心能力:

  • 封装复杂查询:把多表 JOIN、聚合包装成简洁的虚拟表。

  • 安全隔离:限制应用可见的列和行,配合权限实现数据保护。

  • 可更新视图 :有限制条件,配合 WITH CHECK OPTION 做约束校验。

  • 注意事项:视图不提升性能,嵌套视图是大坑,聚合视图不可更新。

视图像一扇窗户------让你在不改变房屋结构的前提下,欣赏到不同角度的风景。下一篇文章我们将学习存储过程与自定义函数,看数据库如何"会自己动"。

想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !

相关推荐
XovH1 小时前
MySQL 系列:第10篇 存储过程与自定义函数
后端
XovH1 小时前
MySQL 系列:第8篇 子查询与集合操作
后端
vortex51 小时前
新手前后端开发学习指南:从Flask框架到全栈实践
后端·python·flask
leeyi2 小时前
Retriever 组件:让 Agent 学会「翻资料」的统一接口
人工智能·后端·agent
一个做软件开发的牛马2 小时前
MyBatis 从零实战:完整搭建可运行 Demo,注解与 XML 双模式开发详解
java·后端
砍材农夫2 小时前
python环境|conda安装和使用(1)
开发语言·后端·python·conda
用户298698530142 小时前
Java 实践:查找与提取 Word 文档超链接
java·后端
Rust研习社2 小时前
Rust 错误处理的黄金搭档:一个定义错误,一个传播错误
后端·rust·编程语言
Moment2 小时前
从多人编辑到 Agent 写文档,Hocuspocus v4 正在改写协同系统 😍😍😍
前端·后端·面试