"能用应用层解决的不用存储过程",十年数据库经验总结

十五年数据库相关经验,做过 DBA、架构师、技术顾问。不求"颠覆",只求"靠谱"。


数据库里最危险的东西不是慢 SQL,是藏在存储过程和触发器里的业务逻辑。

慢 SQL 出问题的时候你能看到------执行计划不对、CPU 飙高、日志里有记录。但存储过程和触发器出问题的时候,业务逻辑静默错误:该扣的钱没扣、该生成的记录没生成、该校验的条件没校验。查了半天应用代码没问题,最后发现是数据库里的触发器漏了一条规则。

存储过程和触发器本身不是坏东西。用对了,它们是数据库的利器。用错了,它们是定时炸弹。

今天把存储过程和触发器的使用边界讲清楚。什么时候该用、什么时候绝对不该用、踩了什么坑、怎么治理历史遗留的烂代码。


01 存储过程:把复杂逻辑放在数据库里

存储过程是什么:一段预编译的 SQL 代码块,存在数据库里,可以被应用调用。

适合用存储过程的场景

场景一:批量数据处理。

比如月底出账单,涉及几十万用户的计费数据汇总。用应用层逐条查、逐条算、逐条插,效率极低。用存储过程,数据库内部完成计算和写入,减少了应用和数据库之间的网络往返。

sql 复制代码
-- 示例:月度账单汇总
CREATE OR REPLACE PROCEDURE monthly_billing(target_month VARCHAR)
AS $$
DECLARE
    r RECORD;
BEGIN
    -- 汇总用量
    FOR r IN SELECT user_id, SUM(amount) AS total
             FROM usage_records
             WHERE billing_month = target_month
             GROUP BY user_id
    LOOP
        -- 计算账单
        INSERT INTO bills (user_id, month, amount, status)
        VALUES (r.user_id, target_month, r.total * 0.8, 'pending');
    END LOOP;
    
    COMMIT;
END;
$$ LANGUAGE plpgsql;

场景二:强一致性要求的复合操作。

比如转账:A 账户扣钱、B 账户加钱、记录流水。这三个操作必须在一个事务里完成,要么全成功,要么全失败。放在存储过程里,事务边界清晰,比应用层拼 SQL 更可靠。

sql 复制代码
CREATE OR REPLACE PROCEDURE transfer(
    from_account INT,
    to_account INT,
    amount DECIMAL
)
AS $$
BEGIN
    -- A 扣钱
    UPDATE accounts SET balance = balance - amount
    WHERE id = from_account AND balance >= amount;
    
    IF NOT FOUND THEN
        RAISE EXCEPTION '余额不足';
    END IF;
    
    -- B 加钱
    UPDATE accounts SET balance = balance + amount
    WHERE id = to_account;
    
    -- 记录流水
    INSERT INTO transactions (from_id, to_id, amount, created_at)
    VALUES (from_account, to_account, amount, NOW());
END;
$$ LANGUAGE plpgsql;

场景三:数据迁移/清洗脚本。

数据迁移的时候,临时写一段存储过程做数据转换和校验,跑完就删。这种场景不需要改应用代码,直接在数据库里操作,效率最高。

存储过程的实际生态

PL/pgSQL(PostgreSQL/KES)和 PL/SQL(Oracle)的存储过程语法类似,但和 MySQL 的存储过程语法差异不小。如果团队从 Oracle 迁移过来,金仓 KES 的 Oracle 兼容模式能覆盖大部分 PL/SQL 语法,存储过程不需要全部重写,改几个关键字就能跑。这在 Oracle 迁移项目里是个实打实的优势------存量代码的改造量直接决定了迁移工期。


02 触发器:隐藏的业务逻辑炸弹

触发器是什么:当某个表发生 INSERT/UPDATE/DELETE 时,自动执行一段代码。

适合用触发器的场景

场景一:审计日志自动记录。

每次修改数据时,自动把旧值、新值、修改人、修改时间记录到审计表里。这种逻辑写在触发器里,应用层完全不用管,保证不会漏记。

sql 复制代码
-- 审计触发器函数
CREATE OR REPLACE FUNCTION audit_trigger_func()
RETURNS TRIGGER AS $$
BEGIN
    INSERT INTO audit_log (table_name, old_data, new_data, changed_by, changed_at)
    VALUES (TG_TABLE_NAME, row_to_json(OLD), row_to_json(NEW), current_user, NOW());
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- 绑定到表
CREATE TRIGGER employees_audit_trigger
    AFTER UPDATE ON employees
    FOR EACH ROW
    EXECUTE FUNCTION audit_trigger_func();

场景二:数据冗余自动维护。

比如订单表有 order_count 字段,每次新增订单时自动 +1。这种冗余字段的维护放在触发器里,不会忘记更新。

场景三:数据校验。

比如工资字段不能低于最低工资标准,不能高于上限。在触发器里做校验,任何写入路径(应用、手工 SQL、批量导入)都不会绕过。

触发器的隐患

触发器最大的问题是不可见性。

一个开发者来查"为什么这个用户的订单状态不对",翻了应用代码没找到问题,翻了配置没找到问题,最后才发现是表上有个触发器在偷偷改数据。而这个触发器是三年前某个离职的人写的,没人知道它的存在。

所以触发器的第一条铁律:能不用就不用,用了必须文档化。


03 定时任务:数据库里的"cron"

数据库也可以跑定时任务(PostgreSQL 叫 pg_cron / pgAgent,MySQL 叫 Event Scheduler,Oracle 叫 DBMS_SCHEDULER)。

适合用数据库定时任务的场景

场景一:定期清理过期数据。

sql 复制代码
-- 每天凌晨 2 点清理 90 天前的日志
SELECT cron.schedule('cleanup_old_logs',
    '0 2 * * *',
    $$DELETE FROM access_logs WHERE created_at < NOW() - INTERVAL '90 days'$$
);

场景二:定期汇总统计。

比如每天凌晨把当天的明细数据汇总到统计表里,第二天查报表的时候直接读汇总表,不用每次实时算。

场景三:定期刷新物化视图。

物化视图存的是预计算结果,需要定期刷新保持数据新鲜。

不该用数据库定时任务的场景

  • 需要复杂调度逻辑的任务。 比如有依赖关系的多个任务、失败重试策略、并发控制------这些是 Airflow、Celery 等应用层调度工具的强项,不是数据库该干的。
  • 需要和外部系统交互的任务。 比如调 API、发消息、写文件------数据库不应该干这些事。
  • 业务逻辑相关的定时任务。 比如"每天给过期用户发提醒邮件"------这是应用层的事,不应该让数据库去发邮件。

04 反模式:什么时候绝对不该用

反模式一:业务逻辑全部写存储过程

有些团队的架构是"应用层只做薄薄一层,所有业务逻辑都在存储过程里"。这种架构的问题:

  • 版本管理困难。 存储过程存在数据库里,不像应用代码有 Git 版本控制。谁改的、改了什么、什么时候改的,很难追溯。
  • 测试困难。 存储过程的单元测试比应用代码难得多,CI/CD 流水线不好集成。
  • 调试困难。 出了 bug,要查应用代码 → 存储过程 → 触发器 → 另一个触发器,链路极长。
  • 人才瓶颈。 能写好应用代码的人多,能写好存储过程的人少。团队里如果只有一个人懂存储过程,他就是单点故障。

正确做法:存储过程只保留数据处理、事务管理、批量操作这类"数据库擅长的事情"。业务规则、流程控制、状态机这些放在应用层。

反模式二:触发器里写复杂业务逻辑

触发器适合做简单的数据操作(记日志、维护冗余字段、基础校验)。但有些团队把完整的业务流程写在触发器里:

sql 复制代码
-- 反模式:触发器里做完整业务流程
CREATE TRIGGER order_after_insert
    AFTER INSERT ON orders
    FOR EACH ROW
BEGIN
    -- 扣库存
    UPDATE inventory SET stock = stock - NEW.quantity WHERE product_id = NEW.product_id;
    
    -- 计算积分
    UPDATE users SET points = points + NEW.quantity * 10 WHERE id = NEW.user_id;
    
    -- 发短信通知
    -- (别真在触发器里调外部 API,但有人这么想过)
    
    -- 生成财务报表记录
    INSERT INTO finance_records ...
    
    -- 更新用户等级
    IF ... THEN UPDATE users SET level = ...
END;

这种触发器是定时炸弹。任何一环出问题,整个订单写入就失败。而且触发器里做的事对应用层是透明的,应用开发者根本不知道一条 INSERT 背后触了多少事。

正确做法:触发器只做一件事------记录审计日志。其他的业务流程放在应用层,或者用存储过程显式调用,而不是隐式触发。

反模式三:触发器嵌套触发器

表 A 的触发器更新表 B,表 B 的触发器更新表 C,表 C 的触发器又更新表 A......

这种嵌套触发器一旦出问题,排查难度是指数级的。而且不同数据库对触发器嵌套层数的限制不同,换数据库可能直接报错。


对比:该用 vs 不该用

场景 建议 原因
批量数据汇总计算 存储过程 ✅ 数据库内部计算,减少网络往返
转账等强一致复合操作 存储过程 ✅ 事务边界清晰
审计日志自动记录 触发器 ✅ 任何写入路径都不会漏
基础数据校验 触发器 ✅ 所有写入路径都生效
定时清理过期数据 定时任务 ✅ 简单、可靠、不依赖外部系统
完整业务流程 存储过程 ❌ 版本管理、测试、调试都困难
调外部 API 触发器 ❌ 数据库不该干这种事
业务规则判断 存储过程 ❌ 放应用层更灵活、更好测
复杂调度任务 定时任务 ❌ 用专业调度工具
触发器里触发触发器 触发器 ❌ 排查难度指数级增长

决策框架:存储过程和触发器的使用边界

维度 用存储过程 用触发器 用应用层
批量数据处理 ✅ ❌ 效率低
事务一致性 ✅ ⚠️ 可以但不够灵活 ✅ 需要小心处理
审计日志 ❌ 可以但没必要 ✅ 容易漏
数据校验 ⚠️ 不如约束 ✅ ✅ 但只覆盖应用路径
业务流程 ❌ ❌ ✅
外部交互 ❌ ❌ ✅
复杂调度 ❌ ❌ ✅

深度分析:为什么存储过程和触发器容易被滥用

根因是便利性诱惑。

写存储过程比写应用代码快:不需要建项目、配环境、写接口。打开数据库客户端,几行 SQL 就搞定逻辑。触发器更方便:不用改应用代码,数据库里加一个触发器,业务逻辑就生效了。

短期看确实方便。但长期的代价是:

技术债积累。 存储过程和触发器不像应用代码那样有完善的版本管理、代码审查、自动化测试。它们悄无声息地积累在数据库里,几年下来,数据库成了一个"黑盒"------外面的人不知道里面藏了多少逻辑。

迁移成本爆炸。 当有一天需要换数据库的时候,这些存储过程和触发器就是最大的障碍。语法不兼容、功能不支持、逻辑需要重写。如果当初把业务逻辑放在应用层,换数据库只需要改连接字符串。

团队知识孤岛。 能写好存储过程的人越来越少。很多团队里,存储过程的原始作者已经离职,后来的人看不懂、不敢改、不敢删。这些代码就永远留在那里。

所以存储过程和触发器的核心原则是:用得越少,用得越好。 能用应用层解决的不用存储过程,能用约束解决的不用触发器。把它们留给真正需要它们的场景------批量处理、事务一致性、审计日志。


历史代码治理建议

如果你的数据库里已经有一堆存储过程和触发器,怎么办?

  1. 先盘点。 列出所有存储过程和触发器,标记功能、调用方、重要性。不知道有什么,就没法管。
  2. 分类处理。 审计类触发器保留,业务流程类标记为待迁移,批量处理类保留但补充文档。
  3. 逐步迁移。 不要一次性全删。按业务模块逐个迁移到应用层,每迁一个验证一个。
  4. 建立规范。 新代码禁止新增存储过程和触发器(除审计类触发器外),旧的逐步清理。

总结

存储过程和触发器不是洪水猛兽,是双刃剑。

存储过程适合批量处理、事务一致性、数据迁移这些场景。触发器适合审计日志、基础校验、冗余字段维护这些场景。但它们不适合完整业务流程、外部交互、复杂调度。

用得越少,用得越好。能用应用层解决的不用存储过程,能用约束解决的不用触发器。把存储过程和触发器留给数据库真正擅长的事------数据处理和事务管理。

不要在数据库里藏业务逻辑。数据库是存数据的地方,不是跑业务的地方。

后续我会继续分享数据库字符集陷阱、分区表实战这些话题,跟着我一篇篇学,数据库这块就没问题了。

有问题评论区见。


十五年数据库领域老炮。关注我,一起把数据库这件事搞明白。

相关推荐
AC赳赳老秦1 小时前
公开音频转写信息提取:OpenClaw 处理发布会与听证会文本并提取核心决策信息
大数据·开发语言·汇编·数据库·人工智能·deepseek·openclaw
弈栈录1 小时前
Java AI 应用的异步化与高并发设计
java·后端·架构
老纪的技术唠嗑局1 小时前
异步索引特性解析:OceanBase 如何提升持续写入场景下的索引检索性能
数据库
老纪的技术唠嗑局1 小时前
Tibo 谈 Codex:harness 总比模型快一步
数据库·人工智能
硅基手札2 小时前
【risc-v专栏 06b】内存架构深挖:PMA / ePMP / Cache / CMO / MMU 细节 / RVWMO 形式化
架构·risc-v
猫咪宝妖2 小时前
信息安全工程师 第四级 结构化保护级
网络·数据库·安全
Andreapiki2 小时前
风险审计校招技术栈拆解:SQL、Python、Power BI在2026届JD中的真实权重
数据库·python·sql
CAD开发小慧2 小时前
MFC工业软件换上4K屏后界面模糊?BCGControlBar Pro如何适配高DPI与多屏
架构
nvd112 小时前
从存算一体到现代湖仓:对标传统关系型数据库透视 Bucket + Iceberg + Trino 的物理本质
数据库·bigdata