1. 为什么我们需要存储过程?
十年前我刚接触数据库开发时,总喜欢把所有SQL语句都写在应用程序代码里。直到有次遇到一个需要修改上百个应用程序文件的紧急需求,我才真正理解存储过程的价值。存储过程就像是数据库里的"预制菜",把常用的数据操作逻辑封装起来,不仅提高了执行效率,更重要的是实现了业务逻辑的集中管理。
在SQL Server环境中,存储过程(Stored Procedure)是预编译的T-SQL语句集合,它被命名并存储在数据库中。与直接在应用程序中拼接SQL语句相比,存储过程有几个显著优势:
- 性能更优:存储过程在首次执行时就被编译和优化,后续调用直接使用执行计划
- 安全性更高:可以精细控制对存储过程的执行权限,而不需要直接开放表权限
- 维护更方便:业务逻辑变更只需修改存储过程,无需重新部署应用程序
- 减少网络流量:只需传递存储过程名和参数,而不是完整的SQL语句
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 存储过程基础入门
2.1 创建第一个存储过程
让我们从最简单的例子开始。假设我们有一个员工表Employees,需要创建一个存储过程来查询特定部门的员工:
sql复制CREATE PROCEDURE GetEmployeesByDepartment
@DepartmentName NVARCHAR(50)
AS
BEGIN
SELECT EmployeeID, FirstName, LastName, Email
FROM Employees
WHERE Department = @DepartmentName
ORDER BY LastName, FirstName;
END
这个例子展示了存储过程的基本结构:
CREATE PROCEDURE是创建语句GetEmployeesByDepartment是过程名@DepartmentName是输入参数AS BEGIN...END包裹了过程体
提示:存储过程命名建议采用"动词+名词"的格式,如GetXxx、UpdateXxx、DeleteXxx等,这样更容易理解其功能。
2.2 执行存储过程
执行上面创建的存储过程非常简单:
sql复制EXEC GetEmployeesByDepartment @DepartmentName = '销售部'
或者简写为:
sql复制EXEC GetEmployeesByDepartment '销售部'
2.3 带输出参数的存储过程
存储过程不仅可以接受输入,还可以返回输出参数。例如,我们创建一个计算部门平均工资的存储过程:
sql复制CREATE PROCEDURE CalculateAvgSalary
@DepartmentName NVARCHAR(50),
@AvgSalary DECIMAL(10,2) OUTPUT
AS
BEGIN
SELECT @AvgSalary = AVG(Salary)
FROM Employees
WHERE Department = @DepartmentName;
END
调用这个存储过程时需要注意输出参数的处理:
sql复制DECLARE @Avg DECIMAL(10,2)
EXEC CalculateAvgSalary '销售部', @Avg OUTPUT
SELECT @Avg AS '销售部平均工资'
3. 存储过程进阶技巧
3.1 错误处理与事务管理
健壮的存储过程必须包含错误处理机制。SQL Server提供了TRY...CATCH结构:
sql复制CREATE PROCEDURE UpdateEmployeeSalary
@EmployeeID INT,
@NewSalary DECIMAL(10,2)
AS
BEGIN
BEGIN TRY
BEGIN TRANSACTION
UPDATE Employees
SET Salary = @NewSalary
WHERE EmployeeID = @EmployeeID;
-- 可以添加其他操作,如记录日志等
COMMIT TRANSACTION
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- 返回错误信息
DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE()
RAISERROR(@ErrorMessage, 16, 1)
END CATCH
END
这个例子展示了:
- 使用BEGIN TRANSACTION开始事务
- 在TRY块中执行核心操作
- 在CATCH块中进行错误处理和事务回滚
- 使用RAISERROR返回错误信息
3.2 动态SQL的使用
有时我们需要根据参数动态构建SQL语句:
sql复制CREATE PROCEDURE SearchEmployees
@FirstName NVARCHAR(50) = NULL,
@LastName NVARCHAR(50) = NULL,
@Department NVARCHAR(50) = NULL
AS
BEGIN
DECLARE @SQL NVARCHAR(MAX)
DECLARE @WhereClause NVARCHAR(MAX) = ''
-- 动态构建WHERE条件
IF @FirstName IS NOT NULL
SET @WhereClause = @WhereClause + ' AND FirstName LIKE ''' + @FirstName + '%'''
IF @LastName IS NOT NULL
SET @WhereClause = @WhereClause + ' AND LastName LIKE ''' + @LastName + '%'''
IF @Department IS NOT NULL
SET @WhereClause = @WhereClause + ' AND Department = ''' + @Department + ''''
-- 移除开头的" AND "
IF LEN(@WhereClause) > 0
SET @WhereClause = ' WHERE ' + SUBSTRING(@WhereClause, 5, LEN(@WhereClause))
-- 构建完整SQL
SET @SQL = 'SELECT EmployeeID, FirstName, LastName, Department FROM Employees' + @WhereClause
-- 执行动态SQL
EXEC sp_executesql @SQL
END
重要安全提示:使用动态SQL时要特别注意SQL注入风险。上面的例子中参数值被直接拼接到SQL中,实际项目中应该使用参数化查询:
sql复制-- 更安全的动态SQL示例
DECLARE @ParmDefinition NVARCHAR(500) = N'@FirstNameParam NVARCHAR(50)'
SET @SQL = N'SELECT * FROM Employees WHERE FirstName LIKE @FirstNameParam + ''%'''
EXEC sp_executesql @SQL, @ParmDefinition, @FirstNameParam = @FirstName
3.3 临时表与表变量
在复杂的数据处理中,临时表和表变量非常有用:
sql复制CREATE PROCEDURE GenerateDepartmentReport
AS
BEGIN
-- 创建临时表存储中间结果
CREATE TABLE #DeptStats (
Department NVARCHAR(50),
EmployeeCount INT,
AvgSalary DECIMAL(10,2),
MaxSalary DECIMAL(10,2)
)
-- 填充临时表数据
INSERT INTO #DeptStats
SELECT
Department,
COUNT(*) AS EmployeeCount,
AVG(Salary) AS AvgSalary,
MAX(Salary) AS MaxSalary
FROM Employees
GROUP BY Department
-- 使用临时表生成最终报表
SELECT
d.Department,
d.EmployeeCount,
d.AvgSalary,
d.MaxSalary,
e.FirstName + ' ' + e.LastName AS HighestPaidEmployee
FROM #DeptStats d
JOIN Employees e ON d.Department = e.Department AND d.MaxSalary = e.Salary
-- 临时表会在存储过程结束时自动删除
END
表变量是另一种选择,适用于较小的数据集:
sql复制CREATE PROCEDURE GetRecentHires
@Days INT = 30
AS
BEGIN
DECLARE @RecentHires TABLE (
EmployeeID INT,
FullName NVARCHAR(100),
HireDate DATE,
Department NVARCHAR(50)
)
INSERT INTO @RecentHires
SELECT
EmployeeID,
FirstName + ' ' + LastName,
HireDate,
