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 BY、HAVING、DISTINCT、聚合函数 -
使用了
UNION/UNION ALL -
FROM 子句包含不可更新的子查询
-
引用了多个表(除非用 INNER JOIN 且满足特定条件)
判断方法 :查询 information_schema.VIEWS 的 IS_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. 动手试试:定制你的视图
基于现有三表,完成以下练习:
-
创建一个视图
product_sales,显示每件商品的名称、类别、销售总次数、销售总金额,并过滤掉从未出售的商品。 -
通过
product_sales查询最畅销的商品(销售总次数最多)。 -
尝试通过视图
product_sales插入一条记录,观察结果并解释为什么。 -
创建一个只读视图
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 思维 !