资讯动态

SQL Server 存储过程实战:从基础 CRUD 到事务、分页与批量处理

发布时间:2026/10/27 14:18:49 来源:尧图企业网站定制
简介面向SQL Server初学者与数据库开发人员的一份存储过程入门实例包聚焦预编译SQL语句集合的创建、调用与参数传递帮助读者理解如何用存储过程封装业务逻辑、提升执行效率并实现权限控制。压缩包共3个文件均为sql脚本整体约4KB内容涵盖供应链管理场景下的报表生成与订单处理类过程以及用于生成下一批次号的用户自定义函数可对照学习输入参数、结果集返回与函数复用的写法。目前已有2023人学习下载适合作为课堂练习或项目起步阶段的参考素材。读者可从中获取可直接运行的示例脚本观察存储过程与函数在命名、参数定义和调用方式上的差异并借助注释与结构梳理编写思路为后续学习事务控制、异常处理等进阶内容打下基础。1. 从一次凌晨三点的排查说起这套 SQL Server 存储过程例子到底解决什么问题去年冬天某公司业务库在凌晨三点突然出现批量订单状态不同步值班同事第一反应是去翻应用层日志翻了两个小时没定位到问题最后发现是三个业务模块各自写了一套几乎一样的更新逻辑其中一处漏了事务边界。这类事故的根因往往不在业务复杂度而在于数据访问逻辑散落在各处、没有统一入口。这份「SQLSERVER存储过程例子」资源正是围绕存储过程这个核心把增删改查、事务控制、参数校验、分页查询、批量处理这些高频场景整理成可直接运行的示例集合适合刚接触 SQL Server 数据层开发的新手照着跑通也适合需要把散落 SQL 收敛成存储过程的老手做参考模板。存储过程不是新东西但把它写对、写稳、写得可维护仍然是一线开发绕不开的基本功。2. 项目定位与技术选型为什么是存储过程而不是拼 SQL2.1 这份资源覆盖的功能边界从标题和关键词能确定这份资源的主体是 SQL Server 平台上的存储过程示例集合不是某个完整应用系统也不是 ORM 框架。它的价值在于把存储过程从「语法演示」拉到「工程可用」的层面。按常见组织方式这类示例集合一般会覆盖以下几类场景场景类别典型内容对应工程价值基础 CRUD单表增删改查、按主键查询、条件更新建立存储过程基本写法认知事务控制多表写入、TRY...CATCH、显式事务保证数据一致性参数与校验输入参数、输出参数、默认值、空值处理减少脏数据进入分页查询OFFSET/FETCH、ROW_NUMBER 分页支撑列表页性能批量处理表值参数、循环更新、批量插入应对数据同步场景需要说明的是输入里没有给出具体文件清单以上是按「SQL Server 存储过程示例集合」这个定位下合格开发最可能采用的组织方式归纳的。实际拿到资源后建议先按这个维度给脚本分类再决定从哪个例子开始跑。2.2 技术栈与运行环境存储过程示例的运行环境很纯粹SQL Server 数据库实例加一个能执行 T-SQL 的客户端。常见组合是 SQL Server 2016 及以上版本配 SQL Server Management StudioSSMS或者用 Azure Data Studio、VS Code 的 SQL 插件替代。版本选择上有个实际考量如果示例里用到STRING_AGG、OFFSET FETCH、表值参数这些特性2016 之前的老版本会直接报错所以建议至少准备 2016 及以上实例。本地跑的话Developer 版实例足够示例脚本不涉及企业版特性。技术选型上这份资源刻意不绑定应用层语言存储过程本身就是数据库侧的能力Java、C#、Python 调用方式不同但过程体一致。这种「数据层独立」的定位好处是示例可以脱离具体后端框架单独验证坏处是读者需要自己补上调用侧代码。我一般会建议先用 SSMS 把过程体跑通再接到应用里避免应用层报错和 SQL 报错混在一起排查。2.3 目录结构怎么读这类示例集合的目录通常按功能分脚本文件常见形态如下sqlserver-procedures/ ├── 01_basic/ # 基础增删改查 │ ├── create_table.sql # 建表脚本 │ ├── sp_insert.sql # 插入过程 │ └── sp_query.sql # 查询过程 ├── 02_transaction/ # 事务与异常处理 │ └── sp_order_sync.sql ├── 03_paging/ # 分页查询 │ └── sp_page_list.sql ├── 04_batch/ # 批量处理 │ └── sp_batch_update.sql └── README.md读这类目录有个顺序建议先跑create_table.sql建好测试表再按编号顺序执行过程脚本最后用EXEC调用验证。不要跳过建表直接跑过程否则会卡在对象不存在上这是新手最常见的第一个坎。3. 核心模块拆解一个存储过程从定义到调用的完整链路3.1 基础 CRUD 过程参数定义与返回值约定先看最基础的插入过程这是理解存储过程参数机制的入口。下面这段是这类示例里最常见的写法-- 创建订单表测试用 CREATE TABLE Orders ( OrderId INT IDENTITY(1,1) PRIMARY KEY, OrderNo VARCHAR(32) NOT NULL, Amount DECIMAL(18,2) NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 0, CreatedAt DATETIME NOT NULL DEFAULT GETDATE() ); GO -- 插入订单存储过程 CREATE PROCEDURE sp_Order_Insert OrderNo VARCHAR(32), Amount DECIMAL(18,2), NewId INT OUTPUT -- 输出参数回传自增主键 AS BEGIN SET NOCOUNT ON; -- 关闭行数计数减少网络往返 -- 参数校验单号为空的直接拒绝 IF OrderNo IS NULL OR LTRIM(RTRIM(OrderNo)) BEGIN RAISERROR(OrderNo 不能为空, 16, 1); RETURN -1; END INSERT INTO Orders (OrderNo, Amount) VALUES (OrderNo, Amount); SET NewId SCOPE_IDENTITY(); -- 取当前作用域自增ID END; GO这段代码有几个关键点值得逐一说清。OrderNo、Amount是输入参数NewId带OUTPUT关键字调用方可以拿到插入后的自增主键这在「插入后立刻要用新 ID 做后续操作」的场景里很实用。SET NOCOUNT ON是性能习惯避免每条语句返回「受影响行数」的额外结果集应用层解析时更干净。RAISERROR配合RETURN -1是错误约定调用方既能捕获异常也能读返回码。SCOPE_IDENTITY()而不是IDENTITY是因为后者会被触发器里的插入干扰这是踩过坑才会记住的细节。调用方式如下DECLARE id INT; EXEC sp_Order_Insert OrderNo NO20240101, Amount 99.50, NewId id OUTPUT; SELECT id AS NewOrderId;参数用命名方式传可读性好也不怕顺序写错。输出参数调用时必须带OUTPUT漏了会拿到 NULL这是新手高频翻车点。3.2 事务与异常处理TRY...CATCH 加显式事务单表操作简单多表写入就必须上事务。这类示例里典型的是订单主表加明细表同步写入CREATE PROCEDURE sp_Order_CreateWithDetail OrderNo VARCHAR(32), Amount DECIMAL(18,2), ProductId INT, Qty INT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; -- 遇错自动回滚兜底 BEGIN TRY BEGIN TRANSACTION; DECLARE orderId INT; INSERT INTO Orders (OrderNo, Amount) VALUES (OrderNo, Amount); SET orderId SCOPE_IDENTITY(); INSERT INTO OrderDetails (OrderId, ProductId, Qty) VALUES (orderId, ProductId, Qty); COMMIT TRANSACTION; END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK TRANSACTION; -- 把原始错误信息抛回调用方 DECLARE msg NVARCHAR(4000) ERROR_MESSAGE(); RAISERROR(msg, 16, 1); END CATCH END; GOSET XACT_ABORT ON是这段的保险丝某些错误比如超时、部分严重级别错误不会自动触发 CATCH 里的回滚开启后事务会被强制标记为不可提交避免半截数据落库。TRANCOUNT 0判断再回滚是防止在事务已自动回滚的情况下重复 ROLLBACK 报错。CATCH 里重新RAISERROR而不是吞掉异常是为了让应用层能拿到真实错误信息不然排查时就是黑匣子。这里有个血泪经验CATCH 块里如果只写ROLLBACK不抛错应用层会以为成功数据却没写进去这种静默失败最难查。3.3 分页查询过程OFFSET/FETCH 与排序参数列表页分页是存储过程的高频用途。SQL Server 2012 之后推荐OFFSET/FETCH比ROW_NUMBER()子查询更直观CREATE PROCEDURE sp_Order_PageList PageIndex INT 1, PageSize INT 20, Status TINYINT NULL, Total INT OUTPUT AS BEGIN SET NOCOUNT ON; -- 防御页码和页大小兜底 IF PageIndex 1 SET PageIndex 1; IF PageSize 1 SET PageSize 20; IF PageSize 200 SET PageSize 200; -- 防止一次拉太多 SELECT Total COUNT(1) FROM Orders WHERE (Status IS NULL OR Status Status); SELECT OrderId, OrderNo, Amount, Status, CreatedAt FROM Orders WHERE (Status IS NULL OR Status Status) ORDER BY OrderId DESC OFFSET (PageIndex - 1) * PageSize ROWS FETCH NEXT PageSize ROWS ONLY; END; GOStatus IS NULL OR Status Status是可选条件惯用写法传 NULL 就查全部传值就过滤省得为每种组合写一个过程。Total输出参数回传总数前端分页组件要用。PageSize上限 200 是防御性设计避免调用方传个 100000 把库拖垮。注意OFFSET/FETCH必须配合ORDER BY没有排序子句会直接语法报错这点和 MySQL 的 LIMIT 不一样从 MySQL 转过来的同学容易在这里卡住。3.4 批量处理表值参数替代循环批量更新如果写成游标逐行处理数据量一大就慢得离谱。这类示例里更工程化的做法是表值参数Table-Valued Parameter-- 定义表类型 CREATE TYPE OrderStatusUpdate AS TABLE ( OrderId INT PRIMARY KEY, Status TINYINT NOT NULL ); GO CREATE PROCEDURE sp_Order_BatchUpdateStatus Updates OrderStatusUpdate READONLY AS BEGIN SET NOCOUNT ON; UPDATE o SET o.Status u.Status FROM Orders o INNER JOIN Updates u ON o.OrderId u.OrderId; END; GO表值参数用READONLY修饰过程内不能修改它这是硬性要求。调用时在应用层构造 DataTable 或对应结构传入一次网络往返完成批量更新比循环调用快一个数量级。这里有个注意点表值参数在部分驱动里需要额外配置才能传比如某些版本的 JDBC 驱动要显式启用接应用层时先确认驱动支持不然会卡在「参数类型不识别」上。4. 跑起来从建库到验证的完整步骤4.1 环境准备与脚本执行顺序先把实例准备好本地用 SSMS 连上后新建一个测试库避免在业务库上直接跑示例脚本。执行顺序建议固定为建库 → 建表 → 建类型 → 建过程 → 调用验证。建类型要在建过程之前因为过程引用了表值参数类型顺序错了会报「类型不存在」。每执行完一个脚本用SELECT * FROM sys.procedures确认过程已创建比凭感觉可靠。4.2 用 EXEC 逐个验证过程建好后不要急着接应用先在 SSMS 里用EXEC跑通。基础插入、事务写入、分页查询各调一次重点看输出参数是否拿到值、异常分支是否按预期抛错。验证事务过程时可以故意传一个违反约束的参数观察是否整体回滚这是确认事务边界写对的最直接办法。4.3 接应用层时的调用要点应用层调用存储过程核心是参数绑定和结果集读取。以常见后端为例命令类型要设为存储过程而不是文本 SQL输出参数要单独注册方向。批量场景优先用表值参数退而求其次用批量插入语句尽量避免在应用层循环调用单条过程。调用侧代码不在资源范围内但这一步是「跑通」到「用起来」的必经环节建议单独写个小测试类验证参数传递再合入业务代码。5. 避坑与排查存储过程落地时最容易翻车的几处5.1 现象过程改了但调用还是旧逻辑原因通常是执行计划缓存或脚本没重新编译。解决方式是执行sp_recompile 过程名或在CREATE PROCEDURE时加WITH RECOMPILE仅调试期用并确认改的是目标库而不是同名测试库。5.2 现象输出参数拿到 NULL原因多为调用时漏写OUTPUT关键字或过程内没给输出参数赋值。解决方式是检查EXEC语句的参数声明并在过程末尾确认SET 输出参数 ...一定被执行到尤其是提前RETURN的分支。5.3 现象事务过程报「当前事务无法提交」原因一般是 CATCH 里回滚后没有正确退出或嵌套事务计数没处理好。解决方式是统一用TRANCOUNT 0判断再回滚避免在已回滚的事务上继续操作同时检查是否有触发器内部开了事务。5.4 现象分页查询结果顺序不稳定原因是没有唯一排序键ORDER BY用了可能重复的列。解决方式是把主键加入排序比如ORDER BY CreatedAt DESC, OrderId DESC保证分页边界稳定否则翻页时会出现重复或漏数据。5.5 现象批量更新偶发超时原因多为表值参数数据量过大或缺少索引。解决方式是控制单批数据量常见做法是每批 1000 到 5000 行并确认关联列有索引必要时拆分批次提交。6. 二次开发把示例改造成自己项目的存储过程规范拿到这套示例后直接照抄能跑但要落到自己项目里得做几处改造。第一处是命名规范统一示例里可能混用sp_前缀实际项目里sp_在 SQL Server 中有特殊含义系统会先在 master 库找建议改成usp_或业务前缀避免解析开销和命名冲突。第二处是错误码体系示例用RAISERROR抛文本项目里更稳的做法是定义错误码表过程内抛码、应用层映射文案排查时按码检索比搜文本快得多。第三处是参数校验下沉。示例里的校验比较轻实际项目建议把必填、长度、枚举范围校验统一放在过程入口配合THROW替代RAISERRORSQL Server 2012 之后THROW语法更简洁且能保留原始错误号。第四处是版本管理存储过程脚本要进代码仓库每次变更走脚本文件而不是直接在 SSMS 里改否则线上和仓库对不上回滚时没有后悔药。验证改造是否到位我一般用三步先用边界参数跑一遍空值、超长、非法枚举确认全部按预期拒绝再故意制造事务中途失败确认整体回滚且错误信息完整最后用批量数据压一次分页和批量更新看执行计划和耗时。这三步走完基本能确认过程在真实流量下不会出大问题。有个具体技巧值得单独说给每个过程加一段注释头写清用途、参数、返回、修改记录格式固定。看起来是小事但半年后回来改的人很可能就是自己会感谢这个习惯。下面是个模板/* 过程名 : usp_Order_CreateWithDetail 用途 : 订单主表明细表事务写入 参数 : OrderNo 单号, Amount 金额, ProductId 商品, Qty 数量 返回 : 无失败抛异常 修改 : 2024-01-01 初版 */从那以后我每次新增或改动存储过程都强制走一遍「脚本进仓库 注释头 边界参数自测」这三件事吃过一次线上静默失败的亏之后再也不敢省。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑