数据库对象(视图、存储过程、函数)
一、视图(VIEW)
1.1 什么是视图
视图是一个虚拟表,不存储数据,本质是对查询的封装。每次查询视图时都会执行其对应的 SELECT 语句。
1.2 创建和使用视图
sql
-- 创建视图
CREATE VIEW v_employee_dept AS
SELECT e.id, e.name, e.salary, d.dept_name
FROM employee e
JOIN department d ON e.dept_id = d.id;
-- 使用视图(和普通表一样查询)
SELECT * FROM v_employee_dept WHERE salary > 10000;
-- 查看视图定义
SHOW CREATE VIEW v_employee_dept;
-- 删除视图
DROP VIEW v_employee_dept;
1.3 视图进阶用法
sql
-- 1. 带聚合的视图
CREATE VIEW v_dept_salary_summary AS
SELECT d.dept_name,
COUNT(e.id) AS emp_count,
ROUND(AVG(e.salary), 2) AS avg_salary,
MAX(e.salary) AS max_salary
FROM department d
LEFT JOIN employee e ON d.id = e.dept_id
GROUP BY d.id, d.dept_name;
-- 2. 视图上再建视图(不推荐多层嵌套)
CREATE VIEW v_high_salary_dept AS
SELECT * FROM v_dept_salary_summary
WHERE avg_salary > 12000;
-- 3. 可更新视图(满足条件的视图可以更新底层数据)
CREATE VIEW v_employee_basic AS
SELECT id, name, dept_id, salary
FROM employee
WHERE dept_id IS NOT NULL;
-- 可通过视图更新数据
UPDATE v_employee_basic SET salary = 16000 WHERE id = 1;
-- 实际更新的是 employee 表
-- 带 CHECK OPTION 的视图(防止更新后脱离视图范围)
CREATE VIEW v_tech_employee AS
SELECT id, name, dept_id, salary
FROM employee
WHERE dept_id = 1
WITH CHECK OPTION;
-- 下面这条会报错,因为更新后 dept_id=2 不在视图范围内
UPDATE v_tech_employee SET dept_id = 2 WHERE id = 1;
1.4 视图的优缺点
| 优点 | 说明 |
|---|---|
| 简化查询 | 封装复杂 SQL,调用方无需关心细节 |
| 逻辑安全性 | 只暴露需要的字段,隐藏敏感数据 |
| 逻辑独立性 | 底层表结构变化时,可通过修改视图屏蔽影响 |
| 缺点 | 说明 |
|---|---|
| 性能问题 | 复杂视图每次查询都执行底层 SQL |
| 更新限制 | 多表连接的视图不一定可更新 |
| 调试困难 | 视图嵌套多层时排查问题困难 |
实战案例:用户权限视图
sql
-- 场景:对外部系统只暴露用户的基本信息,隐藏敏感字段
CREATE VIEW v_user_public AS
SELECT id, username, nickname, avatar_url
FROM user
WHERE is_deleted = 0;
-- 内部系统视图(增加更多字段但隐藏密码)
CREATE VIEW v_user_internal AS
SELECT id, username, nickname, email, phone, status, create_time
FROM user
WHERE is_deleted = 0;
二、存储过程(PROCEDURE)
2.1 创建和执行存储过程
sql
-- 修改分隔符(因为过程体内有 ;)
DELIMITER $$
-- 创建无参存储过程
CREATE PROCEDURE sp_get_all_employees()
BEGIN
SELECT * FROM employee;
END$$
-- 改回分隔符
DELIMITER ;
-- 调用存储过程
CALL sp_get_all_employees();
-- 查看存储过程定义
SHOW CREATE PROCEDURE sp_get_all_employees;
-- 删除存储过程
DROP PROCEDURE IF EXISTS sp_get_all_employees;
2.2 带参数的存储过程
sql
DELIMITER $$
-- IN 参数(输入)
CREATE PROCEDURE sp_get_employee_by_dept(
IN dept_id_param INT
)
BEGIN
SELECT e.name, e.salary, d.dept_name
FROM employee e
JOIN department d ON e.dept_id = d.id
WHERE e.dept_id = dept_id_param;
END$$
-- OUT 参数(输出)
CREATE PROCEDURE sp_get_dept_stats(
IN dept_id_param INT,
OUT emp_count INT,
OUT avg_salary DECIMAL(10,2)
)
BEGIN
SELECT COUNT(*), AVG(salary)
INTO emp_count, avg_salary
FROM employee
WHERE dept_id = dept_id_param;
END$$
-- INOUT 参数
CREATE PROCEDURE sp_increase_salary(
INOUT salary_param DECIMAL(10,2),
IN percentage DECIMAL(5,2)
)
BEGIN
SET salary_param = salary_param * (1 + percentage / 100);
END$$
DELIMITER ;
-- 调用 OUT 参数的过程
CALL sp_get_dept_stats(1, @count, @avg);
SELECT @count AS emp_count, @avg AS avg_salary;
-- 调用 INOUT 参数的过程
SET @sal = 10000;
CALL sp_increase_salary(@sal, 10);
SELECT @sal; -- 11000
2.3 实战:批量插入测试数据
sql
DELIMITER $$
CREATE PROCEDURE sp_batch_insert_employee(
IN total_count INT
)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE dept_id_val INT;
DECLARE salary_val DECIMAL(10,2);
-- 开启事务
START TRANSACTION;
WHILE i <= total_count DO
-- 随机分配部门(1~3)
SET dept_id_val = FLOOR(1 + RAND() * 3);
SET salary_val = ROUND(5000 + RAND() * 20000, 2);
INSERT INTO employee(name, dept_id, salary)
VALUES (CONCAT('测试用户', i), dept_id_val, salary_val);
SET i = i + 1;
-- 每1000条提交一次
IF i % 1000 = 0 THEN
COMMIT;
START TRANSACTION;
END IF;
END WHILE;
COMMIT;
END$$
DELIMITER ;
-- 插入 10000 条测试数据
CALL sp_batch_insert_employee(10000);
三、自定义函数(FUNCTION)
3.1 创建和使用函数
sql
DELIMITER $$
-- 标量函数:返回单个值
CREATE FUNCTION fn_get_dept_name(dept_id INT)
RETURNS VARCHAR(50)
DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE dept_name VARCHAR(50);
SELECT d.dept_name INTO dept_name
FROM department d
WHERE d.id = dept_id;
RETURN dept_name;
END$$
DELIMITER ;
-- 使用函数
SELECT id, name, fn_get_dept_name(dept_id) AS dept_name FROM employee;
3.2 存储过程与函数的区别
| 特性 | 存储过程 | 函数 |
|---|---|---|
| 返回值 | 多个 OUT 参数 | 单一返回值必须 |
| 调用方式 | CALL | SELECT 或表达式中调用 |
| 事务控制 | 支持 COMMIT/ROLLBACK | 不支持 |
| SELECT 中使用 | 不可以 | 可以 |
| 适用场景 | 批量操作、复杂业务逻辑 | 计算、封装查询 |
3.3 实战:常用自定义函数
sql
DELIMITER $$
-- 1. 计算年龄
CREATE FUNCTION fn_calc_age(birth_date DATE)
RETURNS INT
DETERMINISTIC
NO SQL
BEGIN
RETURN TIMESTAMPDIFF(YEAR, birth_date, CURDATE());
END$$
-- 2. 脱敏手机号(138****1234)
CREATE FUNCTION fn_mask_phone(phone VARCHAR(20))
RETURNS VARCHAR(20)
DETERMINISTIC
NO SQL
BEGIN
RETURN CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4));
END$$
-- 3. 计算订单折扣金额
CREATE FUNCTION fn_calc_discount(
total_amount DECIMAL(12,2),
discount_rate DECIMAL(5,2)
)
RETURNS DECIMAL(12,2)
DETERMINISTIC
NO SQL
BEGIN
DECLARE result DECIMAL(12,2);
SET result = ROUND(total_amount * discount_rate, 2);
RETURN result;
END$$
DELIMITER ;
-- 使用示例
SELECT
fn_mask_phone('13812345678') AS masked_phone; -- 138****5678
SELECT
fn_calc_discount(1000, 0.85) AS discounted_price; -- 850.00
四、实战综合案例
案例:订单报表存储过程
sql
DELIMITER $$
CREATE PROCEDURE sp_order_report(
IN start_date DATE,
IN end_date DATE,
IN min_amount DECIMAL(12,2),
OUT total_orders INT,
OUT total_revenue DECIMAL(12,2)
)
BEGIN
-- 临时表存储中间结果
CREATE TEMPORARY TABLE tmp_report AS
SELECT
DATE(o.order_date) AS order_day,
o.status,
COUNT(*) AS order_count,
SUM(o.amount) AS daily_revenue
FROM orders o
WHERE DATE(o.order_date) BETWEEN start_date AND end_date
AND o.amount >= min_amount
GROUP BY DATE(o.order_date), o.status
ORDER BY order_day;
-- 统计总数
SELECT COUNT(*), SUM(daily_revenue)
INTO total_orders, total_revenue
FROM tmp_report;
-- 返回明细结果集
SELECT * FROM tmp_report;
-- 清理临时表
DROP TEMPORARY TABLE tmp_report;
END$$
DELIMITER ;
-- 调用报表
CALL sp_order_report('2024-01-01', '2024-03-31', 100, @total, @revenue);
SELECT @total AS total_orders, @revenue AS total_revenue;
总结
| 对象 | 核心用途 | 使用建议 |
|---|---|---|
| 视图 | 封装查询、权限控制 | 避免多层嵌套、复杂聚合慎用 |
| 存储过程 | 业务逻辑封装、批量操作 | 复杂逻辑用编程语言替代、注意事务控制 |
| 函数 | 计算、查询封装 | 保持无状态、避免在 WHERE 中使用(导致索引失效) |