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)
自定义函数和存储过程很像,但有三个本质区别:
-
必须返回一个值(标量)
-
不能执行 DML(不允许 INSERT/UPDATE/DELETE)
-
可以在 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 表,完成以下挑战:
-
写一个存储过程
restock:增加库存并记录日志,返回新库存量作为 OUT 参数。 -
写一个自定义函数
total_value:计算指定商品的总价值(需要先给表加price列,可临时设置一个固定值)。 -
用 Python 调用
restock,给"机械键盘"补货 20 件,并打印操作后的库存量。 -
比较:用 Python 直接写三条 SQL(查库存 → 更新 → 记日志)和调用一次存储过程,哪种更适合你们的项目?写下你的理由。
8. 总结
今天我们掌握了:
-
存储过程 :封装多步 DML + 流程控制,通过
CALL调用,适合原子业务操作。 -
自定义函数:返回标量值,无副作用,可在 SQL 中直接使用。
-
流程控制 :
IF/ELSE、CASE WHEN、WHILE+ 游标。 -
Python 调用 :
cursor.callproc()+ 会话变量获取 OUT 参数。 -
架构思考:存储过程是一把双刃剑,现代微服务架构更倾向于"轻数据库、重服务"。
理解存储过程的原理,但把真正的业务核心放在应用层,是我给你的建议。下一篇我们将学习触发器与事件调度器,看 MySQL 如何自动化响应数据变更与定时任务。
想了解更多还可以去各个平台搜索「IT策士」,一起升级 IT 思维 !