尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

数据库存储过程与触发器实战:使用边界、反模式识别与历史代码治理

数据库存储过程与触发器实战:使用边界、反模式识别与历史代码治理 十五年数据库相关经验做过 DBA、架构师、技术顾问。不求颠覆只求靠谱。数据库里最危险的东西不是慢 SQL是藏在存储过程和触发器里的业务逻辑。慢 SQL 出问题的时候你能看到——执行计划不对、CPU 飙高、日志里有记录。但存储过程和触发器出问题的时候业务逻辑静默错误该扣的钱没扣、该生成的记录没生成、该校验的条件没校验。查了半天应用代码没问题最后发现是数据库里的触发器漏了一条规则。存储过程和触发器本身不是坏东西。用对了它们是数据库的利器。用错了它们是定时炸弹。今天把存储过程和触发器的使用边界讲清楚。什么时候该用、什么时候绝对不该用、踩了什么坑、怎么治理历史遗留的烂代码。01 存储过程把复杂逻辑放在数据库里存储过程是什么一段预编译的 SQL 代码块存在数据库里可以被应用调用。适合用存储过程的场景场景一批量数据处理。比如月底出账单涉及几十万用户的计费数据汇总。用应用层逐条查、逐条算、逐条插效率极低。用存储过程数据库内部完成计算和写入减少了应用和数据库之间的网络往返。-- 示例月度账单汇总CREATEORREPLACEPROCEDUREmonthly_billing(target_monthVARCHAR)AS$$DECLAREr RECORD;BEGIN-- 汇总用量FORrINSELECTuser_id,SUM(amount)AStotalFROMusage_recordsWHEREbilling_monthtarget_monthGROUPBYuser_idLOOP-- 计算账单INSERTINTObills(user_id,month,amount,status)VALUES(r.user_id,target_month,r.total*0.8,pending);ENDLOOP;COMMIT;END;$$LANGUAGEplpgsql;场景二强一致性要求的复合操作。比如转账A 账户扣钱、B 账户加钱、记录流水。这三个操作必须在一个事务里完成要么全成功要么全失败。放在存储过程里事务边界清晰比应用层拼 SQL 更可靠。CREATEORREPLACEPROCEDUREtransfer(from_accountINT,to_accountINT,amountDECIMAL)AS$$BEGIN-- A 扣钱UPDATEaccountsSETbalancebalance-amountWHEREidfrom_accountANDbalanceamount;IFNOTFOUNDTHENRAISE EXCEPTION余额不足;ENDIF;-- B 加钱UPDATEaccountsSETbalancebalanceamountWHEREidto_account;-- 记录流水INSERTINTOtransactions(from_id,to_id,amount,created_at)VALUES(from_account,to_account,amount,NOW());END;$$LANGUAGEplpgsql;场景三数据迁移/清洗脚本。数据迁移的时候临时写一段存储过程做数据转换和校验跑完就删。这种场景不需要改应用代码直接在数据库里操作效率最高。存储过程的实际生态PL/pgSQLPostgreSQL/KES和 PL/SQLOracle的存储过程语法类似但和 MySQL 的存储过程语法差异不小。如果团队从 Oracle 迁移过来金仓 KES 的 Oracle 兼容模式能覆盖大部分 PL/SQL 语法存储过程不需要全部重写改几个关键字就能跑。这在 Oracle 迁移项目里是个实打实的优势——存量代码的改造量直接决定了迁移工期。02 触发器隐藏的业务逻辑炸弹触发器是什么当某个表发生 INSERT/UPDATE/DELETE 时自动执行一段代码。适合用触发器的场景场景一审计日志自动记录。每次修改数据时自动把旧值、新值、修改人、修改时间记录到审计表里。这种逻辑写在触发器里应用层完全不用管保证不会漏记。-- 审计触发器函数CREATEORREPLACEFUNCTIONaudit_trigger_func()RETURNSTRIGGERAS$$BEGININSERTINTOaudit_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());RETURNNEW;END;$$LANGUAGEplpgsql;-- 绑定到表CREATETRIGGERemployees_audit_triggerAFTERUPDATEONemployeesFOR EACH ROWEXECUTEFUNCTIONaudit_trigger_func();场景二数据冗余自动维护。比如订单表有order_count字段每次新增订单时自动 1。这种冗余字段的维护放在触发器里不会忘记更新。场景三数据校验。比如工资字段不能低于最低工资标准不能高于上限。在触发器里做校验任何写入路径应用、手工 SQL、批量导入都不会绕过。触发器的隐患触发器最大的问题是不可见性。一个开发者来查为什么这个用户的订单状态不对翻了应用代码没找到问题翻了配置没找到问题最后才发现是表上有个触发器在偷偷改数据。而这个触发器是三年前某个离职的人写的没人知道它的存在。所以触发器的第一条铁律能不用就不用用了必须文档化。03 定时任务数据库里的cron数据库也可以跑定时任务PostgreSQL 叫 pg_cron / pgAgentMySQL 叫 Event SchedulerOracle 叫 DBMS_SCHEDULER。适合用数据库定时任务的场景场景一定期清理过期数据。-- 每天凌晨 2 点清理 90 天前的日志SELECTcron.schedule(cleanup_old_logs,0 2 * * *,$$DELETEFROMaccess_logsWHEREcreated_atNOW()-INTERVAL90 days$$);场景二定期汇总统计。比如每天凌晨把当天的明细数据汇总到统计表里第二天查报表的时候直接读汇总表不用每次实时算。场景三定期刷新物化视图。物化视图存的是预计算结果需要定期刷新保持数据新鲜。不该用数据库定时任务的场景需要复杂调度逻辑的任务。比如有依赖关系的多个任务、失败重试策略、并发控制——这些是 Airflow、Celery 等应用层调度工具的强项不是数据库该干的。需要和外部系统交互的任务。比如调 API、发消息、写文件——数据库不应该干这些事。业务逻辑相关的定时任务。比如每天给过期用户发提醒邮件——这是应用层的事不应该让数据库去发邮件。04 反模式什么时候绝对不该用反模式一业务逻辑全部写存储过程有些团队的架构是应用层只做薄薄一层所有业务逻辑都在存储过程里。这种架构的问题版本管理困难。存储过程存在数据库里不像应用代码有 Git 版本控制。谁改的、改了什么、什么时候改的很难追溯。测试困难。存储过程的单元测试比应用代码难得多CI/CD 流水线不好集成。调试困难。出了 bug要查应用代码 → 存储过程 → 触发器 → 另一个触发器链路极长。人才瓶颈。能写好应用代码的人多能写好存储过程的人少。团队里如果只有一个人懂存储过程他就是单点故障。正确做法存储过程只保留数据处理、事务管理、批量操作这类数据库擅长的事情。业务规则、流程控制、状态机这些放在应用层。反模式二触发器里写复杂业务逻辑触发器适合做简单的数据操作记日志、维护冗余字段、基础校验。但有些团队把完整的业务流程写在触发器里-- 反模式触发器里做完整业务流程CREATETRIGGERorder_after_insertAFTERINSERTONordersFOR EACH ROWBEGIN-- 扣库存UPDATEinventorySETstockstock-NEW.quantityWHEREproduct_idNEW.product_id;-- 计算积分UPDATEusersSETpointspointsNEW.quantity*10WHEREidNEW.user_id;-- 发短信通知-- 别真在触发器里调外部 API但有人这么想过-- 生成财务报表记录INSERTINTOfinance_records...-- 更新用户等级IF...THENUPDATEusersSETlevel...END;这种触发器是定时炸弹。任何一环出问题整个订单写入就失败。而且触发器里做的事对应用层是透明的应用开发者根本不知道一条 INSERT 背后触了多少事。正确做法触发器只做一件事——记录审计日志。其他的业务流程放在应用层或者用存储过程显式调用而不是隐式触发。反模式三触发器嵌套触发器表 A 的触发器更新表 B表 B 的触发器更新表 C表 C 的触发器又更新表 A……这种嵌套触发器一旦出问题排查难度是指数级的。而且不同数据库对触发器嵌套层数的限制不同换数据库可能直接报错。对比该用 vs 不该用场景建议原因批量数据汇总计算存储过程 ✅数据库内部计算减少网络往返转账等强一致复合操作存储过程 ✅事务边界清晰审计日志自动记录触发器 ✅任何写入路径都不会漏基础数据校验触发器 ✅所有写入路径都生效定时清理过期数据定时任务 ✅简单、可靠、不依赖外部系统完整业务流程存储过程 ❌版本管理、测试、调试都困难调外部 API触发器 ❌数据库不该干这种事业务规则判断存储过程 ❌放应用层更灵活、更好测复杂调度任务定时任务 ❌用专业调度工具触发器里触发触发器触发器 ❌排查难度指数级增长决策框架存储过程和触发器的使用边界维度用存储过程用触发器用应用层批量数据处理✅❌效率低事务一致性✅⚠️ 可以但不够灵活✅ 需要小心处理审计日志❌ 可以但没必要✅容易漏数据校验⚠️ 不如约束✅✅ 但只覆盖应用路径业务流程❌❌✅外部交互❌❌✅复杂调度❌❌✅深度分析为什么存储过程和触发器容易被滥用根因是便利性诱惑。写存储过程比写应用代码快不需要建项目、配环境、写接口。打开数据库客户端几行 SQL 就搞定逻辑。触发器更方便不用改应用代码数据库里加一个触发器业务逻辑就生效了。短期看确实方便。但长期的代价是技术债积累。存储过程和触发器不像应用代码那样有完善的版本管理、代码审查、自动化测试。它们悄无声息地积累在数据库里几年下来数据库成了一个黑盒——外面的人不知道里面藏了多少逻辑。迁移成本爆炸。当有一天需要换数据库的时候这些存储过程和触发器就是最大的障碍。语法不兼容、功能不支持、逻辑需要重写。如果当初把业务逻辑放在应用层换数据库只需要改连接字符串。团队知识孤岛。能写好存储过程的人越来越少。很多团队里存储过程的原始作者已经离职后来的人看不懂、不敢改、不敢删。这些代码就永远留在那里。所以存储过程和触发器的核心原则是用得越少用得越好。能用应用层解决的不用存储过程能用约束解决的不用触发器。把它们留给真正需要它们的场景——批量处理、事务一致性、审计日志。历史代码治理建议如果你的数据库里已经有一堆存储过程和触发器怎么办先盘点。列出所有存储过程和触发器标记功能、调用方、重要性。不知道有什么就没法管。分类处理。审计类触发器保留业务流程类标记为待迁移批量处理类保留但补充文档。逐步迁移。不要一次性全删。按业务模块逐个迁移到应用层每迁一个验证一个。建立规范。新代码禁止新增存储过程和触发器除审计类触发器外旧的逐步清理。总结存储过程和触发器不是洪水猛兽是双刃剑。存储过程适合批量处理、事务一致性、数据迁移这些场景。触发器适合审计日志、基础校验、冗余字段维护这些场景。但它们不适合完整业务流程、外部交互、复杂调度。用得越少用得越好。能用应用层解决的不用存储过程能用约束解决的不用触发器。把存储过程和触发器留给数据库真正擅长的事——数据处理和事务管理。不要在数据库里藏业务逻辑。数据库是存数据的地方不是跑业务的地方。后续我会继续分享数据库字符集陷阱、分区表实战这些话题跟着我一篇篇学数据库这块就没问题了。有问题评论区见。十五年数据库领域老炮。关注我一起把数据库这件事搞明白。
返回列表