存储过程这个东西在数据库圈子里争议一直很大。有人觉得它把业务逻辑塞进数据库后期维护起来想骂人也有人把它当成压箱底的法宝凡是需要多步操作、批量处理和事务控制的场景都用它把活干得干净利落。我在平时的项目里MySQL、SQL Server、Oracle 都接触过说实话存储过程不是万能的但只要用对了地方它确实是能显著提升开发效率和系统稳定性的利器。这篇内容适合后端开发、DBA、数据分析师以及正在接手老系统的人。你会看到三类主流数据库里存储过程的写法差异也会看到我从实际项目中梳理出来的建模思路、完整案例、优化手段和踩坑记录。看完之后至少你自己能判断这个业务逻辑到底该不该写进存储过程如果该写又该怎么写得稳、跑得快、还能让人看得懂。1. 存储过程到底解决什么问题1.1 一次下单背后的数据操作先从一个最简单的业务场景说起用户下单。表面上看只是一次点击后台要干的事情远不止插入一条记录。要扣库存、校验订单金额、生成流水、更新用户余额、甚至还要调外部接口记录日志。如果这些操作全放在应用层代码里每次请求都要来回闯数据库好几次反而把简单事情搞复杂。存储过程解决的核心问题就是把“一系列SQL操作”包装成一个可复用的单元。一次调用数据库内部完成所有操作并返回结果。这样做有三个直接的好处网络开销大幅降低因为不再需要客户端和服务端之间频繁交互业务一致性更容易保证因为整个过程可以在一个事务里完成权限管理也更聚焦可以让应用账号只执行存储过程而不直接操作底层表。在我经手过的项目中有一类场景特别适合存储过程对账。比如财务系统每天凌晨要跟第三方支付渠道对账需要把几万条交易记录和渠道回传的文件做比对找到差异项。这个任务涉及大量循环、条件判断和状态更新如果用应用层代码实现要么需要写非常复杂的查询逻辑要么需要把大量数据拉到内存中逐步判断。而用存储过程在数据库内部完成不仅能把中间结果放在临时表里就地加工还能显著减少数据传输时间整批处理往往几秒钟就跑完。1.2 什么时候该用、什么时候别用存储过程不是银弹这一点必须放在最前面说。我见过团队把存储过程当万能工具结果项目做到一半发现改了表结构连带着十几个存储过程全部要重写改起来非常痛苦。从我的经验来看下面这些情况适合用存储过程强事务、多步骤的批量操作比如订单结算、库存调整、资金划拨。报表统计、数据清洗、批量迁移。需要隐藏敏感表结构只对外暴露固定接口的场景。多个应用共享同一套业务规则避免重复实现导致的逻辑漂移。反过来这些情况尽量别碰高并发互联网前端接口每秒钟几千次调用存储过程在数据库端的高频执行反而加剧连接和CPU压力。逻辑快速迭代、经常调整需求的功能因为应用代码的发布可以灰度存储过程的部署往往是一次性的。微服务架构下各服务应该保持独立的数据域和业务边界把太多公共逻辑下沉到数据库会让服务耦合度失控。说到底选择存储过程或应用代码本质是“计算放在哪里”的取舍。放在数据库里代码离数据更近批量处理更快但扩展性和可维护性会受影响放在应用层灵活性更高但网络和内存开销更难控制。我自己的判断标准是如果这个操作在一个事务里需要连续读写多张表而且未来半年大概率不会频繁变更那就值得写成存储过程如果只是简单的单表增删改查没必要绕这一圈。1.3 三大主流数据库中的存储过程差异很多开发者在MySQL里写了几年SQL突然接手一个使用SQL Server或Oracle的老系统看到存储过程写法完全不一样一时间无从下手。其实核心思想是相通的定义过程、声明变量、写流程控制、处理异常。差异主要在语法外壳和内置功能上。我用一个表格把三者的关键差异列出来对比项MySQLSQL ServerOracle创建语法CREATE PROCEDURECREATE PROCEDURE / CREATE FUNCTIONCREATE OR REPLACE PROCEDURE存储过程语言基于SQL的存储过程语法Transact-SQL (T-SQL)PL/SQL参数模式IN / OUT / INOUT默认输入OUTPUT关键字IN / OUT / IN OUT匿名块早期版本无现在支持BEGIN...END直接用BEGIN...ENDDECLARE...BEGIN...END异常处理DECLARE ... HANDLERTRY...CATCHEXCEPTION WHEN ...事务控制START TRANSACTION / COMMIT / ROLLBACKBEGIN TRANSACTION / COMMIT / ROLLBACK默认自动可显式COMMIT / ROLLBACK调试友好度较一般SSMS调试体验较好PL/SQL Developer等工具较完善从结构设计上看Oracle 的 PL/SQL 更接近一门完整编程语言支持包、函数重载、自治事务这些高级特性SQL Server 的 T-SQL 和 Windows 生态配合得好和 .NET、SSIS 等工具无缝衔接MySQL 的存储过程则轻量很多通用性也不错但在复杂业务场景下能用的高级特性相对少。备用方案是如果你只是希望封装一段简单的多步骤SQL在哪个数据库上实现差别不大但如果有复杂的业务逻辑优先考虑业务规则所在领域的平台能力。2. 语法基础与核心细节2.1 MySQL从DELIMITER到异常处理用MySQL写过存储过程的人几乎都被DELIMITER折腾过。它的原理不复杂MySQL默认用分号作为语句结束符而存储过程内部也全是分号如果不先把结束符改掉mysql客户端在读到第一个分号时就认为命令结束了自然报错。标准写法是这样DELIMITER // CREATE PROCEDURE sp_demo(IN p_id INT, OUT p_result VARCHAR(50)) BEGIN -- 过程体 SELECT name INTO p_result FROM users WHERE id p_id; END // DELIMITER ;这里的DELIMITER //是告诉客户端“接下来用//作为语句结束标志”等整个存储过程创建完成再把它改回分号。如果你用Navicat或DBeaver这类图形工具工具本身已经帮你处理了结束符问题不需要手写DELIMITER但理解这个概念依然很重要因为在命令行手工执行或写部署脚本时这是最常踩的坑。MySQL存储过程的参数模式有三个IN表示入参OUT表示出参INOUT表示既传入又传出。经典的做法是“用OUT返回执行结果”比如返回状态码、返回错误信息。这样就可以在应用层通过读取输出参数来感知存储过程的执行情况而不是在过程中直接SELECT结果。异常处理这一点很多新手容易忽略。MySQL的语法是用DECLARE ... HANDLER来捕获异常DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_code -1; END;这意味着当过程中发生任何SQL异常时自动回滚事务并设置返回值。如果没有这段处理异常发生时容易留下半个事务数据就毁了。我通常在动手写存储过程之前先把这个骨架搭好再往里面填业务逻辑这样可以保证流程上线时异常路径基本是完整的。2.2 SQL ServerOUTPUT参数与TRY...CATCHSQL Server 的存储过程在语法上比MySQL更规整而且和开发工具链的集成度高。创建一个带输出参数的过程通常长这样CREATE PROCEDURE dbo.sp_settle_order order_id BIGINT, amount DECIMAL(12,2) OUTPUT, code INT OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; SELECT amount order_amount FROM dbo.orders WITH (UPDLOCK, ROWLOCK) WHERE order_id order_id; IF ROWCOUNT 0 BEGIN SET code 10001; ROLLBACK; RETURN; END; INSERT INTO dbo.settle_log(order_id, settle_amount, operator) VALUES (order_id, amount, system); UPDATE dbo.orders SET settle_status 1 WHERE order_id order_id; COMMIT; SET code 0; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK; SET code ERROR_NUMBER(); END CATCH; END;这里有几个细节值得注意。SET NOCOUNT ON用来关闭“受影响行数”的提示可以减少网络流量和客户端干扰。WITH (UPDLOCK, ROWLOCK)是在查询时加更新锁和行锁防止并发场景下的金额被重复处理。ROWCOUNT获取上一条语句影响的行数常常配合IF判断使用确保订单存在才继续执行。SQL Server的异常处理用TRY...CATCH比MySQL的HANDLER更直观。CATCH块里可以用ERROR_NUMBER()、ERROR_MESSAGE()等函数取得错误码和错误信息再通过输出参数返回给调用方。实际项目中我会把ERROR_MESSAGE()写入日志表方便事后分析。2.3 Oracle PL/SQL包和自治事务Oracle 的存储过程基于PL/SQL语法和风格与MySQL、SQL Server差别最大。它的基本结构更接近编程语言有独立的声明段、执行段和异常段CREATE OR REPLACE PROCEDURE sp_settle_order( p_order_id IN NUMBER, p_amount OUT NUMBER, p_code OUT NUMBER ) IS v_amount NUMBER : 0; v_status NUMBER; BEGIN SELECT order_status, order_amount INTO v_status, v_amount FROM orders WHERE order_id p_order_id FOR UPDATE; IF v_status 2 THEN p_code : 10001; p_amount : 0; ELSE INSERT INTO settle_log(order_id, settle_amount, operator) VALUES (p_order_id, v_amount, system); UPDATE orders SET settle_status 1 WHERE order_id p_order_id; COMMIT; p_code : 0; p_amount : v_amount; END IF; EXCEPTION WHEN NO_DATA_FOUND THEN p_code : 10002; p_amount : 0; WHEN OTHERS THEN ROLLBACK; p_code : SQLCODE; END;PL/SQL最让我喜欢的一点是SELECT INTO可以直接把查询结果赋给变量配合%TYPE和%ROWTYPE数据类型可以自动适配表结构变化这在其他数据库中要麻烦得多。Oracle还支持把多个相关的存储过程、函数和变量打包成“包”统一管理类似Java里的类。还有一个非常有实战价值的概念叫自治事务。如果一个存储过程主体的事务回滚了但你又希望把错误日志保存下来可以用PRAGMA AUTONOMOUS_TRANSACTION把日志插入做成独立事务这样日志不会随主事务一起回滚。这在排障时非常关键否则你根本看不到失败现场。2.4 游标与动态SQL用对地方才能少踩坑存储过程里常常遇到“按行处理”的需求比如逐个处理某张表里符合条件的记录。行级处理最常用的工具是游标。但我要先泼一盆冷水游标的性能通常很差因为它在数据库内部基本上是逐行读取、逐行处理聚合和批量修改场景下远不如一条UPDATE、一条INSERT SELECT来得高效。我的经验是能用集合操作一条SQL搞定就绝不逐行处理。只有下面这几种情况才考虑用游标需要对每行执行不同的逻辑而且逻辑差异很大无法用单一SQL表达。需要按特定顺序逐步处理数据。需要边处理边记录每行的失败原因实现类似“部分成功、部分失败”的效果。动态SQL也是存储过程里的常客。比如按用户传入的表名、字段名、排序字段生成SQL这时候必须非常小心。我用动态SQL时始终坚持两件事一是表名、字段名用白名单校验只允许指定的字符串参与拼接二是值部分一律用参数绑定。拿MySQL举例PREPARE和EXECUTE支持占位符尽量不要用CONCAT拼接值进去。这样既挡住SQL注入又让数据库更容易复用执行计划性能也更稳定。3. 实操案例订单结算存储过程3.1 业务需求与表结构设计光说理论不够我把一个真实的订单结算场景拆开给你看。假设我们要处理一张订单的退款结算订单主表里有订单状态、订单金额、结算状态明细表记录每件商品结算日志表记录每一次结算动作和操作人。核心需求是这样只有状态为“已确认”的订单才能结算结算动作只能执行一次已经结算过的订单不能重复结算整个操作必须在事务内完成任何一步失败都要完全回滚。这个场景特别适合用存储过程因为多表读写、条件校验、日志记录一次搞定。先建三张测试表CREATE TABLE orders ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id BIGINT NOT NULL, order_status TINYINT NOT NULL DEFAULT 0, order_amount DECIMAL(12,2) NOT NULL DEFAULT 0, settle_status TINYINT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_items ( item_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, product_name VARCHAR(100), quantity INT, price DECIMAL(12,2) ); CREATE TABLE settle_log ( log_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT, settle_amount DECIMAL(12,2), operator VARCHAR(50), log_time DATETIME DEFAULT CURRENT_TIMESTAMP );3.2 MySQL完整实现与逐段说明下面是一段我常用的MySQL存储过程模板负责订单结算。它支持传入订单号返回结算金额和状态码DELIMITER // CREATE PROCEDURE sp_settle_order( IN p_order_id BIGINT, OUT p_amount DECIMAL(12,2), OUT p_code INT ) BEGIN DECLARE v_amount DECIMAL(12,2) DEFAULT 0; DECLARE v_status TINYINT; DECLARE v_settle_status TINYINT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_code -1; END; START TRANSACTION; SELECT order_status, settle_status, order_amount INTO v_status, v_settle_status, v_amount FROM orders WHERE order_id p_order_id FOR UPDATE; IF v_status 2 THEN SET p_code 10001; -- 订单状态不允许结算 SET p_amount 0; ROLLBACK; ELSEIF v_settle_status 1 THEN SET p_code 10002; -- 订单已结算过 SET p_amount 0; ROLLBACK; ELSE INSERT INTO settle_log(order_id, settle_amount, operator) VALUES (p_order_id, v_amount, system); UPDATE orders SET settle_status 1 WHERE order_id p_order_id; COMMIT; SET p_code 0; SET p_amount v_amount; END IF; END // DELIMITER ;这段代码的关键点我逐个说FOR UPDATE是悲观锁在事务内把这一行锁住防止并发结算造成重复写入。三个状态变量在声明段统一初始化避免忘记赋值导致NULL判断失误。EXIT HANDLER捕获任何SQL异常后直接回滚并把p_code置为-1调用方可以依据这个值判断“系统内部错误”。状态码10101、10002是我自定义的错误类型方便业务方直接识别。要注意的是SELECT INTO如果查不到记录在MySQL里不会报错而是产生警告变量保持初值。这会导致后面的IF判断走到“不允许结算”分支。为了更严谨可以增加ROW_COUNT()判断或者在调用前确认订单号一定存在。我在很多项目里把这一层交给调用方校验存储过程只处理核心状态。3.3 调用、验证与日志追踪存储过程写好之后怎么验证它是否按预期工作在MySQL客户端或Navicat里可以这样调用SET p_amount 0; SET p_code 0; CALL sp_settle_order(1001, p_amount, p_code); SELECT p_code, p_amount;第一次调用时如果订单状态正确p_code返回0p_amount返回订单金额settle_log里多了一条记录。你再调第二次p_code应该返回10002并且不会再插入新的日志。这个“重复调用幂等”的验证非常关键因为真实业务中网络超时、应用重试都可能导致同一请求被提交多次。日志追踪这块我一直坚持在存储过程里写审计日志。不一定每次成功操作都要写但至少失败路径必须留痕。很多线上事故排查困难就是因为数据库里只留下一个返回值根本不知道当时数据是什么状态。如果你用的是SQL Server或Oracle完全可以在CATCH或EXCEPTION块里调用一个独立的日志存储过程记录错误号、错误信息和当时参数效果立竿见影。3.4 性能验证让EXPLAIN说话存储过程内部能不能高效运行取决于内部每一条SQL的执行计划是否靠谱。写完存储过程后我习惯把里面的核心查询单独拎出来跑EXPLAIN确认走了合适的索引再进行完整调用。比如上面的结算过程WHERE条件是order_id p_order_id如果orders表的主键确实是order_id那没问题。但如果你实际业务经常按customer_id查订单那么orders表就应该在customer_id上建立辅助索引否则对海量数据按客户查询会全表扫描就算存储过程逻辑没问题性能也会被拖垮。还应该监测锁的持有时间。事务里SELECT FOR UPDATE锁定的行在COMMIT或ROLLBACK之前会一直占用如果过程体里还有大量耗时的计算就会延长锁等待时间。优化方向通常是缩小事务范围、把不必要的查询移出事务、给关键表设计更精确的索引或者把大事务拆成多个小批次。关于这一点我在第四节再展开讲。4. 常见问题与排查技巧4.1 编译报错与执行结果异常存储过程在开发阶段最容易遇到的问题我按频率排个序。如果是MySQL最常见的还是DELIMITER没有配对。你在命令行里执行一个长过程忘记在末尾改回分隔符后续所有SQL都会跟到上一个过程体里去报错位置莫名其妙。解决办法很简单创建语句都用客户端工具执行命令行脚本则反复检查DELIMITER位置。第二种问题出在变量作用域。MySQL和SQL Server都有用户变量和局部变量的区别。像name这种以开头的用户变量会一直存在会话里容易造成状态残留我建议尽量用DECLARE声明的局部变量用完自动释放。第三种是字符集和排序规则导致的问题。存储过程里定义了变量但变量的字符集与表字段不一致在比较时会出现Illegal mix of collations之类的报错。解决方式是在建表时就统一字符集或在变量声明处指定COLLATE。执行结果异常也不少见。我碰到过最典型的是“存储过程执行成功了但数据没变”最后发现是调用方没有重新编译包或者连接的数据库实例不对。遇到“看起来成功实际上没生效”的情况先排查你真正连接的是哪台库再看事务有没有COMMIT。4.2 慢SQL优化先读EXPLAIN关键字段存储过程变慢绝大多数时候不是过程体本身慢而是里面的某一两条SQL性能有问题。慢SQL优化的标准动作是先跑EXPLAIN很多人知道要看结果但不知道重点看什么。我平时只关注这几个字段type从好到差大致是const、eq_ref、ref、range、index、ALL。如果是ALL基本可以断定全表扫描要立刻优化索引。key实际用到的索引。如果为NULL说明没用索引。rows预估扫描行数。这个数字越大单条SQL的成本越高。Extra如果出现Using filesort或Using temporary说明有额外的排序或临时表操作在数据量大时非常影响性能。举个典型案例。某次我排查一个报表存储过程每天凌晨要汇总前一天的数据语句里对created_at加了DATE()函数写成WHERE DATE(created_at) CURDATE() - INTERVAL 1 DAY。结果就是即使created_at有索引因为函数包裹了列索引也发挥不了作用。改成created_at CURDATE() - INTERVAL 1 DAY AND created_at CURDATE()之后执行时间从20秒降到0.2秒。核心原则是别在索引列上做函数运算。对于大表的批量更新还可以考虑分片处理比如按主键ID范围分批执行每批1000条代替一次更新几百万行。这样能减少锁范围和回滚日志压力尤其在生产环境的低峰期跑批任务时是很实用的保命技巧。并行SQL优化也是最近大家讨论较多的话题。Oracle里可以对大查询加PARALLEL提示SQL Server可以配置MAXDOPMySQL 8.0也有并行查询的探索。但并行不是免费的它需要占用多个CPU核心也可能放大锁冲突。我的建议是先在单线程下把执行计划调到最优再根据资源余量谨慎开启并行度绝不能盲目追求“越多越好”。4.3 SQL注入与权限最小化存储过程本身不会自动让系统安全但用对方式能显著降低SQL注入风险。很多注入漏洞的根源是应用层直接拼接用户输入生成SQL。而存储过程如果使用参数传递值部分一律走绑定变量注入路径就被切断了大半。动态SQL是重灾区。段首说了动态SQL里的表名、字段名要做白名单校验动态值要绑定参数千万不要老老实实把整个字符串拼起来执行。网上流传的那些“万能密码绕过”攻击就是专门针对拼接SQL的登录逻辑。我在代码评审时看到CONCAT拼接用户输入的地方几乎都会标红要求改掉。权限最小化同样重要。给应用用的数据库账号尽量只授予它执行存储过程的权限而不授予直接SELECT、UPDATE、DELETE底层表的权限。这样就算应用被攻破攻击者能调用的也只是你预先写好的过程而不是任意操作全库数据。存储过程天然适合做“数据安全接口”前提是你愿意花时间把权限体系设计清楚。4.4 存储过程的版本管理与监控存储过程是存在数据库里的很多团队对它的版本管理做得远不如应用代码严格这导致一个典型问题生产环境跑的过程和代码仓库里的版本对不上出了故障都无法回滚。我现在的做法是把每个存储过程当作正式代码管理。文件统一放在仓库的/sql/procedures目录下文件名就是过程名。建表脚本和存储过程脚本一起入库每次修改都走代码评审。数据库里再加一张version_log表存储过程开头写清版本号和变更说明每次发布更新一条记录。这样即便出了问题也能快速定位到是哪个版本导致。监控也不可或缺。存储过程的执行时间、返回码、调用频率都应该有日志采集。我习惯在过程入口和结束处记录时间戳把耗时超过阈值的调用写入慢日志表。慢SQL优化不是发现一次做一次而是要靠持续的监控数据来驱动。5. 工具链与部署环境5.1 常见客户端工具怎么选不同数据库的存储过程开发体验差别很明显选对工具能省不少时间。MySQL环境我常用Navicat和DBeaver。Navicat在左侧树里展开“函数”节点就能找到存储过程可以直接编辑、调试和调用对于不熟悉命令行的开发者非常友好。DBeaver是开源免费的跨平台还支持ER图查看适合团队预算有限时使用。SQL Server环境首选SSMSSQL Server Management Studio。它对T-SQL的调试支持很完善断点、变量监视、调用堆栈都有排查存储过程问题比Navicat更好用。新版SSMS还能连接Azure数据库迁移和运维也方便。Oracle环境就比较多样了。PL/SQL Developer是老牌工具Oracle SQL Developer是免费的官方工具两者都支持包管理、源码版本查看、调试。我会根据团队习惯选但无论选哪个都要重视“存储过程源码版本管理”这件事。5.2 安装迁移中的几个坑就像热词里提到的SQL Server安装经常卡在一些莫名其妙的地方。比如“无法启动Windows Management InstrumentationWMI服务”很可能是因为WMI相关的依赖服务被禁用或者WMI仓库损坏。处理思路通常是先用管理员身份打开命令提示符执行sc config winmgmt start auto再通过服务控制台启动Windows Management Instrumentation服务如果还不奏效可能需要修复WMI存储库。还有“无法卸载SQL Server 2008 R2安装程序支持文件”的问题常见原因是安装程序残留的注册表项或文件被占用。可以先重启系统再用Windows Installer CleanUp工具清理或者手工删除相关注册表项。这类问题虽然不影响运行但妨碍后续重新安装属于投产前要解决的隐患。安装SQL Server 2019/2022时也有类似“命名管道提供程序无法打开”的报错多半是SQL Server服务没有启动或者网络协议配置里Named Pipes被禁用检查服务状态和客户端协议即可。这些不是存储过程本身的内容但如果你在本地搭建实验环境时被安装环节卡住后面的一切都无从谈起。我每次换电脑配环境都会提前把版本、补丁、实例名、端口确认好能少走很多弯路。5.3 从SQL到ER图写存储过程之前能不能把数据表之间的关系梳理清楚直接决定了过程内部JOIN、更新、删除的难度。很多工具支持从数据库反向生成ER图比如MySQL Workbench、DBeaver、Navicat、SQL Server的SSMS图表功能。我通常的习惯是新建项目先让表结构脚本生成ER图重点检查主外键关系、索引是否遗漏、字段命名是否统一。ER图看清楚之后再动手写存储过程基本不会出现“被表结构坑到”的情况。以前我写过一段复杂报表过程自认为逻辑天衣无缝结果一旦连表关联到6张以上JOIN顺序稍有不对结果就错得离谱。后来养成了先画ER图的习惯再复杂的关系也能一眼看出切入点。最后再分享一点个人体会做了这么多年数据库相关工作我越来越觉得存储过程最大的价值不是它能让SQL飞起来而是它是一种“把复杂度关进笼子”的工程手段。一条复杂业务链路如果用应用代码写十个人可能有十种风格如果把它沉淀成一个命名清晰的存储过程配合日志和权限设计整个系统的可维护性反而会大大提升。我踩过的最痛一个坑是曾经在一批订单数据的处理逻辑里忘了加锁结果并发调度重复执行产生了大量脏数据光修复就花了一个通宵。而后来所有类似场景我都坚持“事务加上行锁、过程里写审计日志、调用前做幂等校验”这三板斧几乎能在绝大多数业务里稳定兜底。如果你现在正在学存储过程我建议不要急着追求花哨的语法先把你业务里最复杂的一个批量操作改写成存储过程试试。过程中你自然会遇到事务、异常、锁、性能这些躲不开的问题把这些都趟一遍你对数据库的理解会提升一个台阶。