MySQL 系列:第10篇 存储过程与自定义函数

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


前面几篇我们都是在客户端写 SQL,然后发给 MySQL 执行。但有些复杂的业务操作涉及多条 SQL,如果能打包成一个"函数"放在数据库里,直接调用就好了。这就是存储过程自定义函数的价值。今天用 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 inventory (
    id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(100) NOT NULL UNIQUE,
    stock INT NOT NULL DEFAULT 0,
    locked_stock INT NOT NULL DEFAULT 0
) ENGINE=InnoDB
""")

# 操作日志表
cursor.execute("""
CREATE TABLE IF NOT EXISTS inventory_log (
    id INT AUTO_INCREMENT PRIMARY KEY,
    product_name VARCHAR(100),
    operation VARCHAR(20),
    quantity INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB
""")

cursor.execute("TRUNCATE inventory")
cursor.execute("TRUNCATE inventory_log")

# 插入初始库存
products = [
    ("机械键盘", 100, 0),
    ("蓝牙耳机", 50, 0),
    ("显示器", 30, 0),
]
cursor.executemany("INSERT INTO inventory (product_name, stock, locked_stock) VALUES (%s,%s,%s)", products)
conn.commit()
print("✅ 初始库存已就绪")

2. 存储过程(Stored Procedure)

存储过程是一组预先编译并存储在数据库中的 SQL 语句,可以有输入参数和输出参数,支持流程控制,相当于"数据库里的编程函数"。

2.1 创建第一个存储过程:扣减库存

bash 复制代码
DELIMITER $$

CREATE PROCEDURE deduct_stock(
    IN p_product_name VARCHAR(100),
    IN p_quantity INT,
    OUT p_result INT
)
BEGIN
    DECLARE current_stock INT;
    
    -- 查询当前库存
    SELECT stock INTO current_stock 
    FROM inventory 
    WHERE product_name = p_product_name;
    
    -- 判断库存是否充足
    IF current_stock IS NULL THEN
        SET p_result = -1;  -- 商品不存在
    ELSEIF current_stock < p_quantity THEN
        SET p_result = -2;  -- 库存不足
    ELSE
        -- 扣减库存
        UPDATE inventory SET stock = stock - p_quantity 
        WHERE product_name = p_product_name;
        
        -- 记录日志
        INSERT INTO inventory_log (product_name, operation, quantity)
        VALUES (p_product_name, 'deduct', p_quantity);
        
        SET p_result = 0;   -- 成功
    END IF;
END$$

DELIMITER ;

语法要点

  • IN 输入参数,OUT 输出参数,INOUT 兼具二者。

  • BEGIN ... END 包裹过程体。

  • DECLARE 声明局部变量。

  • SELECT ... INTO 将查询结果赋值给变量。

  • DELIMITER $$ 临时修改语句分隔符,因为过程体内用到了分号。

2.2 用 Python 调用存储过程

bash 复制代码
# 创建存储过程(通过 Python 执行)
create_sp_sql = """
CREATE PROCEDURE IF NOT EXISTS deduct_stock(
    IN p_product_name VARCHAR(100),
    IN p_quantity INT,
    OUT p_result INT
)
BEGIN
    DECLARE current_stock INT;
    SELECT stock INTO current_stock FROM inventory WHERE product_name = p_product_name;
    IF current_stock IS NULL THEN
        SET p_result = -1;
    ELSEIF current_stock < p_quantity THEN
        SET p_result = -2;
    ELSE
        UPDATE inventory SET stock = stock - p_quantity WHERE product_name = p_product_name;
        INSERT INTO inventory_log (product_name, operation, quantity)
        VALUES (p_product_name, 'deduct', p_quantity);
        SET p_result = 0;
    END IF;
END
"""
cursor.execute(create_sp_sql)
print("✅ 存储过程 deduct_stock 创建成功")

# 调用存储过程
result_args = (0,)  # 占位 OUT 参数
cursor.callproc("deduct_stock", ("机械键盘", 3, result_args[0]))

# 获取 OUT 参数(MySQL 会自动生成会话变量 @_procname_argindex)
cursor.execute("SELECT @_deduct_stock_2")
result = cursor.fetchone()[0]

status_map = {0: "扣减成功", -1: "商品不存在", -2: "库存不足"}
print(f"结果: {status_map.get(result, '未知错误')}")

# 验证库存和日志
cursor.execute("SELECT product_name, stock FROM inventory")
print("\n📊 当前库存:")
for row in cursor.fetchall():
    print(f"  {row[0]}: {row[1]}")

cursor.execute("SELECT * FROM inventory_log")
print("\n📋 操作日志:")
for row in cursor.fetchall():
    print(f"  {row[2]} {row[1]} x{row[3]} @ {row[4]}")

预期输出

bash 复制代码
✅ 存储过程 deduct_stock 创建成功
结果: 扣减成功

📊 当前库存:
  机械键盘: 97
  蓝牙耳机: 50
  显示器: 30

📋 操作日志:
  deduct 机械键盘 x3 @ 2025-07-22 10:30:00

3. 自定义函数(User-Defined Function)

自定义函数和存储过程很像,但有三个本质区别:

  1. 必须返回一个值(标量)

  2. 不能执行 DML(不允许 INSERT/UPDATE/DELETE)

  3. 可以在 SQL 中直接调用,就像内置函数一样

3.1 创建自定义函数:计算折扣价

bash 复制代码
create_func_sql = """
CREATE FUNCTION IF NOT EXISTS discount_price(
    original_price DECIMAL(10,2),
    discount_rate DECIMAL(3,2)
)
RETURNS DECIMAL(10,2)
DETERMINISTIC
BEGIN
    RETURN original_price * (1 - discount_rate);
END
"""
cursor.execute(create_func_sql)
print("✅ 函数 discount_price 创建成功")

# 在 SQL 中直接使用
cursor.execute("SELECT discount_price(100, 0.15)")
print(f"100 元打 85 折: ¥{cursor.fetchone()[0]}")

cursor.execute("SELECT discount_price(399, 0.2)")
print(f"399 元打 8 折: ¥{cursor.fetchone()[0]}")

预期输出

bash 复制代码
✅ 函数 discount_price 创建成功
100 元打 85 折: ¥85.00
399 元打 8 折: ¥319.20

DETERMINISTIC 表示相同输入永远返回相同输出,有助于优化器进行常量折叠。

3.2 存储过程 vs 自定义函数

4. 流程控制:让数据库"聪明"起来

4.1 IF ... ELSEIF ... ELSE

上面扣减库存过程已经用过。

4.2 CASE WHEN(在过程中)

bash 复制代码
create_case_sp = """
CREATE PROCEDURE IF NOT EXISTS check_stock_level(
    IN p_product_name VARCHAR(100),
    OUT p_level VARCHAR(20)
)
BEGIN
    DECLARE s INT;
    SELECT stock INTO s FROM inventory WHERE product_name = p_product_name;
    SET p_level = CASE
        WHEN s >= 50 THEN '充足'
        WHEN s >= 10 THEN '偏低'
        ELSE '告急'
    END;
END
"""
cursor.execute(create_case_sp)

cursor.callproc("check_stock_level", ("显示器", ""))
cursor.execute("SELECT @_check_stock_level_2")
print(f"显示器库存等级: {cursor.fetchone()[0]}")

预期输出

4.3 循环:WHILE 与游标(CURSOR)

下面写一个批量锁库存的过程:

bash 复制代码
cursor.execute("""
CREATE PROCEDURE IF NOT EXISTS batch_lock_stock()
BEGIN
    DECLARE done INT DEFAULT 0;
    DECLARE p_name VARCHAR(100);
    DECLARE cur CURSOR FOR SELECT product_name FROM inventory WHERE stock < 20;
    DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
    
    OPEN cur;
    read_loop: LOOP
        FETCH cur INTO p_name;
        IF done THEN
            LEAVE read_loop;
        END IF;
        UPDATE inventory SET locked_stock = stock WHERE product_name = p_name;
    END LOOP;
    CLOSE cur;
END
""")
print("✅ 批量锁库存过程创建成功")

cursor.callproc("batch_lock_stock")
cursor.execute("SELECT product_name, stock, locked_stock FROM inventory")
print("\n📊 锁库后状态:")
for row in cursor.fetchall():
    print(f"  {row[0]}: 库存={row[1]}, 锁定={row[2]}")

当前没有库存 < 20 的商品,所以 locked_stock 不会有变化。你可以插入一条 ('数据线', 5, 0) 后再跑一次,观察效果。

5. Python 封装:把存储过程包装成服务

在生产代码中,我们通常会把存储过程调用封装成类方法:

bash 复制代码
class InventoryManager:
    def __init__(self, conn):
        self.conn = conn
        self.cursor = conn.cursor()
    
    def deduct(self, product_name, quantity):
        """扣减库存,返回 (success: bool, message: str)"""
        try:
            result_args = (0,)
            self.cursor.callproc("deduct_stock", (product_name, quantity, result_args[0]))
            self.cursor.execute("SELECT @_deduct_stock_2")
            result = self.cursor.fetchone()[0]
            
            if result == 0:
                self.conn.commit()
                return True, "扣减成功"
            elif result == -1:
                return False, "商品不存在"
            elif result == -2:
                return False, "库存不足"
        except Exception as e:
            self.conn.rollback()
            return False, str(e)
    
    def check_level(self, product_name):
        """查询库存等级"""
        self.cursor.callproc("check_stock_level", (product_name, ""))
        self.cursor.execute("SELECT @_check_stock_level_2")
        return self.cursor.fetchone()[0]

# 使用
mgr = InventoryManager(conn)
success, msg = mgr.deduct("蓝牙耳机", 5)
print(f"扣减蓝牙耳机×5: {msg}")

level = mgr.check_level("蓝牙耳机")
print(f"蓝牙耳机库存等级: {level}")

6. 现代架构下的定位:为什么"少用"存储过程

尽管存储过程有网络开销小、预编译快、安全隔离等优点,但越来越多的团队将业务逻辑从数据库迁出,原因如下:

  • 调试困难:缺乏断点、堆栈,错误信息不友好。

  • 伸缩性差:数据库是最难水平扩展的一层,逻辑越重越容易成为瓶颈。

  • 维护成本高:存储过程语言(SQL/PSM)远不如 Python/Java 灵活,复杂逻辑实现起来很痛苦。

  • 耦合紧密:更换数据库(如迁移到 PostgreSQL)需要全部重写。

  • CI/CD 不友好:存储过程的版本管理和回滚不如应用代码方便。

最佳实践

  • 简单的数据校验、状态转换可以用存储过程(减少网络往返)。

  • 复杂业务逻辑、计算密集型操作放在应用层(Python/Java)。

  • 自定义函数 仅用于简单的格式化、转换,方便在 SQL 中复用。

7. 动手试试:完善库存系统

基于上面创建的 inventory 表,完成以下挑战:

  1. 写一个存储过程 restock:增加库存并记录日志,返回新库存量作为 OUT 参数。

  2. 写一个自定义函数 total_value :计算指定商品的总价值(需要先给表加 price 列,可临时设置一个固定值)。

  3. 用 Python 调用 restock,给"机械键盘"补货 20 件,并打印操作后的库存量。

  4. 比较:用 Python 直接写三条 SQL(查库存 → 更新 → 记日志)和调用一次存储过程,哪种更适合你们的项目?写下你的理由。

8. 总结

今天我们掌握了:

  • 存储过程 :封装多步 DML + 流程控制,通过 CALL 调用,适合原子业务操作。

  • 自定义函数:返回标量值,无副作用,可在 SQL 中直接使用。

  • 流程控制IF/ELSECASE WHENWHILE + 游标。

  • Python 调用cursor.callproc() + 会话变量获取 OUT 参数。

  • 架构思考:存储过程是一把双刃剑,现代微服务架构更倾向于"轻数据库、重服务"。

理解存储过程的原理,但把真正的业务核心放在应用层,是我给你的建议。下一篇我们将学习触发器与事件调度器,看 MySQL 如何自动化响应数据变更与定时任务。

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

相关推荐
XovH1 小时前
MySQL 系列:第8篇 子查询与集合操作
后端
XovH1 小时前
MySQL 系列:第9篇 视图——定制化数据窗口
后端
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 正在改写协同系统 😍😍😍
前端·后端·面试