为什么不建议使用存储过程?

#中间件#面试八股

为什么不建议使用存储过程?

结论:存储过程把业务逻辑沉到数据库里,短期省了网络往返,长期换来的是版本管理缺失、调试困难、迁移锁定、扩展性差、监控盲区。互联网团队普遍把逻辑放回应用层,只把「大批量数据处理」这类确实适合数据库做的事留在存储过程里。

一、六条不推荐的理由

维度存储过程的问题应用层代码的优势
版本管理逻辑存在库里,不在 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 重建。
  • 分库分表后存储过程还能用吗? 基本不能,跨分片逻辑无法在单个库内完成,最终还是要回到应用层聚合。
  • 团队已有大量存储过程,怎么治理? 建规范:新逻辑禁止新增,存量按调用频次逐步迁移并加回归对比。

小结

不推荐存储过程的本质,是「不要把最难扩容、最难测试、最难追踪的一层变成业务逻辑的容器」。把编排、事务、重试、幂等留在应用层,数据库只负责它最擅长的事:存储与索引。

笔记加载中…