为什么不建议使用存储过程?
结论:存储过程把业务逻辑沉到数据库里,短期省了网络往返,长期换来的是版本管理缺失、调试困难、迁移锁定、扩展性差、监控盲区。互联网团队普遍把逻辑放回应用层,只把「大批量数据处理」这类确实适合数据库做的事留在存储过程里。
一、六条不推荐的理由
| 维度 | 存储过程的问题 | 应用层代码的优势 |
|---|---|---|
| 版本管理 | 逻辑存在库里,不在 Git,变更无 diff、无 code review | 正常分支、评审、回滚 |
| 调试 | 只能靠 SELECT 打点,无断点、无堆栈 | IDE 断点、日志、链路追踪 |
| 测试 | 单测困难,依赖真实库与数据 | 单元测试、Mock、CI |
| 迁移 | 各家方言不兼容(MySQL/PostgreSQL/Oracle) | 换库只改驱动与少量 SQL |
| 扩展 | 只能纵向扩 DB,CPU 密集逻辑挤占数据库资源 | 应用无状态,可水平扩容 |
| 可观测 | 慢在过程内部,APM 只能看到一条 CALL | 指标、Trace、日志完整 |
二、逻辑位置的对比
逻辑放存储过程
App ──CALL proc(?)──▶ MySQL ──▶ 计算/循环/事务 ──▶ 返回结果
▲
└── 扩容只能加 CPU/内存;逻辑不可复用;
调试要看 error log 与 SELECT 打点
逻辑放应用层
App ──简单 SQL──▶ MySQL
│
└── 计算/编排/事务边界/重试/幂等/日志/Trace 都在应用内
可水平扩容、可单测、可灰度
数据库是「有状态且最难扩容」的一层,把计算放进去等于把最贵的资源用在最不该用的地方。
三、代码对比
存储过程写法:
DELIMITER $$
CREATE PROCEDURE sp_settle(IN p_uid BIGINT, OUT p_total DECIMAL(12,2))
BEGIN
DECLARE v_sum DECIMAL(12,2) DEFAULT 0;
DECLARE v_status INT DEFAULT 0;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
SELECT COALESCE(SUM(amount), 0) INTO v_sum
FROM t_bill WHERE uid = p_uid AND settled = 0;
UPDATE t_bill SET settled = 1 WHERE uid = p_uid AND settled = 0;
INSERT INTO t_settle_log (uid, total, created_at)
VALUES (p_uid, v_sum, NOW());
COMMIT;
SET p_total = v_sum;
END$$
DELIMITER ;
应用层等价实现(逻辑可测、可追踪、可重试):
@Transactional(rollbackFor = Exception.class)
public BigDecimal settle(long uid) {
BigDecimal total = billMapper.sumUnsettled(uid); // SELECT ... FOR UPDATE 视场景
int updated = billMapper.markSettled(uid);
settleLogMapper.insert(uid, total);
log.info("settle uid={} total={} rows={}", uid, total, updated);
return total;
}
对应 SQL 依旧简单,事务边界、重试、幂等、日志都在应用侧,出问题能靠 Trace 定位到具体一次调用。
四、什么时候仍可以用
- 纯批处理:一次性数据订正、大表分页迁移、ETL 中间步骤,用存储过程避免几十万次网络往返。
- DBA 运维脚本:巡检、统计、临时修复,跑完即弃,不进业务链路。
- 极端低延迟的嵌入式场景:逻辑与数据强绑定、且数据库不打算换。
判断标准很简单:这段逻辑是否需要长期演进、是否需要多人协作、是否可能换库。三个答案里有一个「是」,就别放进存储过程。
五、如果已经有一堆存储过程
不要一次性重写。按「调用频次 × 变更频次」排序,先把高频变更的过程迁到应用层,保留低频只读过程;迁移期间用双跑对比结果,确认一致后再下线。
常见追问
- 存储过程真的更快吗? 只在「大量小语句往返」场景明显;单条复杂 SQL 放哪都一样,而数据库 CPU 通常更贵。
- 存储过程能防 SQL 注入吗? 参数化调用本身不拼接 SQL,但过程内部用
CONCAT拼动态 SQL 同样有注入风险。 - 存储过程的权限怎么控? 用
DEFINER可以提权执行,这既是便利也是安全隐患,审计时容易被忽略。 - MySQL 的存储过程有什么额外限制? 不支持包、调试能力弱、
SHOW PROCEDURE STATUS信息有限,且 DDL 变更需要DROP重建。 - 分库分表后存储过程还能用吗? 基本不能,跨分片逻辑无法在单个库内完成,最终还是要回到应用层聚合。
- 团队已有大量存储过程,怎么治理? 建规范:新逻辑禁止新增,存量按调用频次逐步迁移并加回归对比。
小结
不推荐存储过程的本质,是「不要把最难扩容、最难测试、最难追踪的一层变成业务逻辑的容器」。把编排、事务、重试、幂等留在应用层,数据库只负责它最擅长的事:存储与索引。