1. 为什么我们需要存储过程?
十年前我刚接触数据库开发时,总喜欢把所有SQL语句都写在应用程序代码里。直到有次遇到一个需要修改上百个应用程序文件的紧急需求,我才真正理解存储过程的价值。存储过程就像是数据库里的"预制菜",把常用的数据操作逻辑封装起来,不仅提高了执行效率,更重要的是实现了业务逻辑的集中管理。
在SQL Server环境中,存储过程(Stored Procedure)是预编译的T-SQL语句集合,它被命名并存储在数据库中。与直接在应用程序中拼接SQL语句相比,存储过程有几个显著优势:
- 性能更优:存储过程在首次执行时就被编译和优化,后续调用直接使用执行计划
- 安全性更高:可以精细控制对存储过程的执行权限,而不需要直接开放表权限
- 维护更方便:业务逻辑变更只需修改存储过程,无需重新部署应用程序
- 减少网络流量:只需传递存储过程名和参数,而不是完整的SQL语句
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,
Department
FROM Employees
WHERE HireDate >= DATEADD(DAY, -@Days, GETDATE())
-- 对表变量进行进一步处理
SELECT * FROM @RecentHires ORDER BY HireDate DESC
END
4. 存储过程性能优化
4.1 参数嗅探问题与解决方案
参数嗅探(Parameter Sniffing)是SQL Server在首次执行存储过程时,根据提供的参数值生成执行计划的行为。虽然这通常能提高性能,但有时会导致后续执行使用不合适的计划。
解决方案1:使用局部变量
sql复制CREATE PROCEDURE GetOrdersByDateRange
@StartDate DATETIME,
@EndDate DATETIME
AS
BEGIN
-- 将参数复制到局部变量
DECLARE @LocalStartDate DATETIME = @StartDate
DECLARE @LocalEndDate DATETIME = @EndDate
SELECT * FROM Orders
WHERE OrderDate BETWEEN @LocalStartDate AND @LocalEndDate
END
解决方案2:使用OPTIMIZE FOR提示
sql复制CREATE PROCEDURE GetOrdersByDateRange
@StartDate DATETIME,
@EndDate DATETIME
AS
BEGIN
SELECT * FROM Orders
WHERE OrderDate BETWEEN @StartDate AND @EndDate
OPTION (OPTIMIZE FOR (@StartDate = '2023-01-01', @EndDate = '2023-01-31'))
END
解决方案3:使用RECOMPILE选项
sql复制CREATE PROCEDURE GetOrdersByDateRange
@StartDate DATETIME,
@EndDate DATETIME
WITH RECOMPILE
AS
BEGIN
SELECT * FROM Orders
WHERE OrderDate BETWEEN @StartDate AND @EndDate
END
4.2 索引优化建议
存储过程的性能很大程度上依赖于表上的索引。在设计存储过程时,应考虑:
- WHERE子句中的列应该被索引
- JOIN条件中的列应该被索引
- ORDER BY子句中的列可以考虑包含在索引中
例如,对于这个存储过程:
sql复制CREATE PROCEDURE GetCustomerOrders
@CustomerID INT,
@StartDate DATETIME,
@EndDate DATETIME
AS
BEGIN
SELECT o.OrderID, o.OrderDate, o.TotalAmount, p.ProductName, od.Quantity
FROM Orders o
JOIN OrderDetails od ON o.OrderID = od.OrderID
JOIN Products p ON od.ProductID = p.ProductID
WHERE o.CustomerID = @CustomerID
AND o.OrderDate BETWEEN @StartDate AND @EndDate
ORDER BY o.OrderDate DESC
END
建议创建以下索引:
sql复制CREATE INDEX IX_Orders_CustomerID_OrderDate ON Orders(CustomerID, OrderDate DESC)
CREATE INDEX IX_OrderDetails_OrderID ON OrderDetails(OrderID)
CREATE INDEX IX_Products_ProductID ON Products(ProductID)
4.3 执行计划分析与优化
使用SET STATISTICS IO ON和SET STATISTICS TIME ON来分析存储过程的性能:
sql复制CREATE PROCEDURE AnalyzeOrderProcessing
AS
BEGIN
SET STATISTICS IO ON
SET STATISTICS TIME ON
-- 存储过程的核心逻辑
-- ...
SET STATISTICS IO OFF
SET STATISTICS TIME OFF
END
执行后,消息选项卡会显示详细的I/O和时间统计信息,帮助我们识别性能瓶颈。
5. 团队协作与版本控制
5.1 存储过程命名规范
良好的命名规范对团队协作至关重要。以下是我们团队采用的规范:
| 前缀 | 用途 | 示例 |
|---|---|---|
| usp_ | 用户存储过程 | usp_GetCustomerOrders |
| sp_ | 系统存储过程(避免使用) | |
| xp_ | 扩展存储过程(避免使用) |
其他命名建议:
- 使用动词+名词结构,如UpdateProductInventory
- 避免使用空格和特殊字符
- 保持名称简洁但具有描述性
5.2 源代码控制集成
虽然SQL Server Management Studio(SSMS)没有内置的版本控制支持,但我们可以:
- 将存储过程脚本保存为.sql文件
- 使用Git等版本控制系统管理这些文件
- 为每个变更创建有意义的提交消息
我习惯为每个存储过程创建单独的文件,命名如:
code复制StoredProcedures/
├── usp_GetCustomerOrders.sql
├── usp_UpdateInventory.sql
└── usp_GenerateMonthlyReport.sql
5.3 变更管理与文档
每个存储过程应该包含头部注释,说明其目的、参数、修改历史等:
sql复制CREATE PROCEDURE usp_CalculateEmployeeBonus
@EmployeeID INT,
@Year INT
/*
目的: 计算指定员工在指定年度的奖金
创建人: 张三
创建日期: 2023-01-15
修改历史:
2023-03-10 李四 增加对兼职员工的支持
2023-06-05 王五 修正奖金计算逻辑
参数:
@EmployeeID - 员工ID
@Year - 年度
返回: 包含奖金金额的结果集
*/
AS
BEGIN
-- 过程体
END
6. 实际案例:订单处理系统
让我们通过一个完整的订单处理案例来综合运用所学知识。
6.1 订单处理流程设计
典型的订单处理流程包括:
- 创建订单头
- 添加订单明细
- 计算总金额
- 更新库存
- 记录交易日志
我们将这些步骤封装在一个事务性存储过程中:
sql复制CREATE PROCEDURE usp_ProcessCustomerOrder
@CustomerID INT,
@OrderItems OrderItemType READONLY, -- 表值参数
@OrderID INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- 步骤1: 创建订单头
INSERT INTO Orders (CustomerID, OrderDate, Status)
VALUES (@CustomerID, GETDATE(), 'Processing')
SET @OrderID = SCOPE_IDENTITY()
-- 步骤2: 添加订单明细
INSERT INTO OrderDetails (OrderID, ProductID, Quantity, UnitPrice)
SELECT
@OrderID,
oi.ProductID,
oi.Quantity,
p.UnitPrice
FROM @OrderItems oi
JOIN Products p ON oi.ProductID = p.ProductID
-- 步骤3: 计算并更新订单总金额
UPDATE Orders
SET TotalAmount = (
SELECT SUM(Quantity * UnitPrice)
FROM OrderDetails
WHERE OrderID = @OrderID
)
WHERE OrderID = @OrderID
-- 步骤4: 更新库存
UPDATE p
SET p.UnitsInStock = p.UnitsInStock - oi.Quantity
FROM Products p
JOIN @OrderItems oi ON p.ProductID = oi.ProductID
-- 步骤5: 记录交易日志
INSERT INTO TransactionLog (OrderID, Action, ActionDate)
VALUES (@OrderID, 'Order Processed', GETDATE())
-- 更新订单状态
UPDATE Orders
SET Status = 'Completed'
WHERE OrderID = @OrderID
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- 记录错误
INSERT INTO ErrorLog (ErrorTime, ErrorNumber, ErrorSeverity,
ErrorState, ErrorProcedure, ErrorLine, ErrorMessage)
VALUES (GETDATE(), ERROR_NUMBER(), ERROR_SEVERITY(),
ERROR_STATE(), ERROR_PROCEDURE(), ERROR_LINE(), ERROR_MESSAGE())
-- 重新抛出错误
THROW;
END CATCH
END
6.2 表值参数的使用
上面的存储过程使用了表值参数(OrderItemType),这是SQL Server 2008引入的功能,允许我们将表格数据作为参数传递给存储过程。
首先需要创建表类型:
sql复制CREATE TYPE OrderItemType AS TABLE (
ProductID INT,
Quantity INT
)
然后在应用程序中可以这样调用:
csharp复制// C#示例代码
DataTable orderItems = new DataTable();
orderItems.Columns.Add("ProductID", typeof(int));
orderItems.Columns.Add("Quantity", typeof(int));
// 添加订单项
orderItems.Rows.Add(101, 2);
orderItems.Rows.Add(205, 1);
using (SqlConnection conn = new SqlConnection(connectionString))
{
SqlCommand cmd = new SqlCommand("usp_ProcessCustomerOrder", conn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("@CustomerID", 12345);
cmd.Parameters.Add("@OrderItems", SqlDbType.Structured).Value = orderItems;
cmd.Parameters.Add("@OrderID", SqlDbType.Int).Direction = ParameterDirection.Output;
conn.Open();
cmd.ExecuteNonQuery();
int orderId = (int)cmd.Parameters["@OrderID"].Value;
}
6.3 性能优化实践
对于高频调用的存储过程,我们可以采用以下优化策略:
- 使用SET NOCOUNT ON减少网络流量
- 避免在循环中执行SQL操作
- 合理使用临时表和表变量
- 考虑使用WITH RECOMPILE选项处理参数变化大的情况
- 定期更新统计信息
例如,优化后的库存更新逻辑:
sql复制-- 优化前的循环更新
DECLARE @ProductID INT, @Qty INT
DECLARE item_cursor CURSOR FOR
SELECT ProductID, Quantity FROM @OrderItems
OPEN item_cursor
FETCH NEXT FROM item_cursor INTO @ProductID, @Qty
WHILE @@FETCH_STATUS = 0
BEGIN
UPDATE Products
SET UnitsInStock = UnitsInStock - @Qty
WHERE ProductID = @ProductID
FETCH NEXT FROM item_cursor INTO @ProductID, @Qty
END
CLOSE item_cursor
DEALLOCATE item_cursor
-- 优化后的批量更新
UPDATE p
SET p.UnitsInStock = p.UnitsInStock - oi.Quantity
FROM Products p
JOIN @OrderItems oi ON p.ProductID = oi.ProductID
7. 常见问题与解决方案
7.1 存储过程执行缓慢
可能原因及解决方案:
| 问题原因 | 解决方案 |
|---|---|
| 缺少合适的索引 | 分析执行计划,添加必要的索引 |
| 参数嗅探问题 | 使用局部变量或OPTIMIZE FOR提示 |
| 统计信息过时 | 更新统计信息:UPDATE STATISTICS 表名 |
| 锁竞争 | 优化事务隔离级别,减少锁持有时间 |
7.2 权限问题
存储过程执行权限的最佳实践:
- 避免直接授予用户表权限
- 创建数据库角色并分配存储过程执行权限
- 使用EXECUTE AS控制执行上下文
sql复制-- 创建角色并授权
CREATE ROLE OrderProcessor
GRANT EXECUTE ON usp_ProcessCustomerOrder TO OrderProcessor
-- 使用EXECUTE AS
CREATE PROCEDURE usp_GetSensitiveData
WITH EXECUTE AS 'dbo'
AS
BEGIN
-- 过程体
END
7.3 调试技巧
SSMS中的调试方法:
- 设置断点:在存储过程代码中点击左侧灰色区域
- 开始调试:点击调试按钮或按F5
- 查看变量值:在"局部变量"窗口中
- 单步执行:使用F10(跳过)和F11(进入)
对于生产环境,可以使用PRINT语句输出调试信息:
sql复制CREATE PROCEDURE usp_DebugExample
@InputParam INT
AS
BEGIN
PRINT '开始执行usp_DebugExample'
PRINT '输入参数值: ' + CAST(@InputParam AS VARCHAR)
-- 业务逻辑
PRINT '执行完成'
END
8. 现代开发中的存储过程实践
8.1 与ORM框架协作
虽然Entity Framework等ORM框架流行,但存储过程仍有其价值:
- 复杂业务逻辑更适合在数据库中实现
- 批量操作在存储过程中效率更高
- 可以结合使用ORM和存储过程
Entity Framework调用存储过程示例:
csharp复制using (var context = new MyDbContext())
{
var customerIdParam = new SqlParameter("@CustomerID", 12345);
var startDateParam = new SqlParameter("@StartDate", DateTime.Today.AddMonths(-1));
var endDateParam = new SqlParameter("@EndDate", DateTime.Today);
var orders = context.Database.SqlQuery<OrderDTO>(
"EXEC usp_GetCustomerOrders @CustomerID, @StartDate, @EndDate",
customerIdParam, startDateParam, endDateParam)
.ToList();
}
8.2 微服务架构中的存储过程
在微服务架构中,存储过程可以作为数据库服务的接口:
- 每个微服务拥有自己的数据库
- 通过存储过程暴露数据访问接口
- 其他服务通过调用这些存储过程访问数据
这种模式的好处:
- 保持数据访问逻辑集中
- 减少网络往返
- 更容易实施数据变更
8.3 自动化测试策略
存储过程也应该有自动化测试:
- 使用tSQLt等SQL Server单元测试框架
- 为每个存储过程创建测试用例
- 在CI/CD管道中集成测试
示例测试用例:
sql复制EXEC tSQLt.NewTestClass 'OrderProcessingTests'
GO
CREATE PROCEDURE OrderProcessingTests.[test usp_ProcessCustomerOrder creates order]
AS
BEGIN
-- 准备测试数据
DECLARE @CustomerID INT = 1
DECLARE @OrderItems OrderItemType
INSERT INTO @OrderItems (ProductID, Quantity) VALUES (101, 2), (102, 1)
DECLARE @OrderID INT
-- 执行存储过程
EXEC usp_ProcessCustomerOrder @CustomerID, @OrderItems, @OrderID OUTPUT
-- 验证结果
DECLARE @OrderCount INT
SELECT @OrderCount = COUNT(*) FROM Orders WHERE OrderID = @OrderID
EXEC tSQLt.AssertEquals 1, @OrderCount, '订单未正确创建'
END
9. 维护与监控
9.1 依赖关系分析
了解存储过程之间的调用关系很重要:
sql复制-- 查找调用特定存储过程的对象
SELECT referencing_schema_name, referencing_entity_name
FROM sys.dm_sql_referencing_entities('dbo.usp_ProcessCustomerOrder', 'OBJECT')
-- 查找存储过程依赖的对象
SELECT referenced_schema_name, referenced_entity_name
FROM sys.dm_sql_referenced_entities('dbo.usp_ProcessCustomerOrder', 'OBJECT')
9.2 性能监控
使用系统视图监控存储过程性能:
sql复制-- 最耗时的存储过程
SELECT TOP 10
OBJECT_NAME(object_id) AS ProcedureName,
total_elapsed_time/execution_count AS avg_elapsed_time,
execution_count,
last_execution_time
FROM sys.dm_exec_procedure_stats
ORDER BY avg_elapsed_time DESC
-- 查找特定存储过程的执行统计
SELECT
qs.execution_count,
qs.total_elapsed_time/1000 AS total_elapsed_time_ms,
qs.last_elapsed_time/1000 AS last_elapsed_time_ms,
qs.min_elapsed_time/1000 AS min_elapsed_time_ms,
qs.max_elapsed_time/1000 AS max_elapsed_time_ms
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st
WHERE st.text LIKE '%usp_ProcessCustomerOrder%'
9.3 版本迁移策略
修改生产环境中的存储过程时,应采用安全策略:
- 使用ALTER PROCEDURE而不是DROP/CREATE,保持权限不变
- 重大变更应先部署到测试环境
- 考虑使用模式版本控制工具如Flyway或Redgate SQL Change Automation
- 为回滚准备旧版本脚本
sql复制-- 安全的修改方式
ALTER PROCEDURE usp_ProcessCustomerOrder
@CustomerID INT,
@OrderItems OrderItemType READONLY,
@OrderID INT OUTPUT
AS
BEGIN
-- 新的实现逻辑
END
10. 从存储过程到函数
虽然本文聚焦存储过程,但SQL Server还提供了函数(Function)。了解它们的区别很重要:
| 特性 | 存储过程 | 函数 |
|---|---|---|
| 返回值 | 可以没有或返回多个结果集 | 必须返回单个值或表 |
| 参数 | 支持输入、输出参数 | 只支持输入参数 |
| 使用场景 | 执行操作、业务逻辑 | 计算并返回结果 |
| 调用方式 | EXEC或EXECUTE | 在SELECT等语句中直接调用 |
| 事务 | 可以包含事务 | 不能包含事务 |
| 错误处理 | 可以使用TRY-CATCH | 不能使用TRY-CATCH |
选择指南:
- 需要执行操作或修改数据 → 使用存储过程
- 需要计算并返回结果 → 使用函数
- 需要在查询中重用逻辑 → 使用表值函数
11. 存储过程设计模式
11.1 CRUD模式
为每个表创建一组标准的CRUD存储过程:
sql复制-- 创建
CREATE PROCEDURE usp_CreateProduct
@ProductName NVARCHAR(100),
@UnitPrice DECIMAL(10,2),
@UnitsInStock INT
AS
BEGIN
INSERT INTO Products (ProductName, UnitPrice, UnitsInStock)
VALUES (@ProductName, @UnitPrice, @UnitsInStock)
RETURN SCOPE_IDENTITY()
END
-- 读取
CREATE PROCEDURE usp_GetProduct
@ProductID INT
AS
BEGIN
SELECT * FROM Products WHERE ProductID = @ProductID
END
-- 更新
CREATE PROCEDURE usp_UpdateProduct
@ProductID INT,
@ProductName NVARCHAR(100),
@UnitPrice DECIMAL(10,2),
@UnitsInStock INT
AS
BEGIN
UPDATE Products
SET ProductName = @ProductName,
UnitPrice = @UnitPrice,
UnitsInStock = @UnitsInStock
WHERE ProductID = @ProductID
END
-- 删除
CREATE PROCEDURE usp_DeleteProduct
@ProductID INT
AS
BEGIN
DELETE FROM Products WHERE ProductID = @ProductID
END
11.2 工厂模式
使用存储过程作为对象工厂,根据参数返回不同的结果集:
sql复制CREATE PROCEDURE usp_GetReport
@ReportType VARCHAR(20),
@StartDate DATETIME = NULL,
@EndDate DATETIME = NULL
AS
BEGIN
IF @ReportType = 'Sales'
SELECT * FROM SalesReportView
WHERE (@StartDate IS NULL OR OrderDate >= @StartDate)
AND (@EndDate IS NULL OR OrderDate <= @EndDate)
ELSE IF @ReportType = 'Inventory'
SELECT * FROM InventoryReportView
ELSE IF @ReportType = 'Customer'
SELECT * FROM CustomerReportView
ELSE
RAISERROR('无效的报表类型: %s', 16, 1, @ReportType)
END
11.3 策略模式
将可变算法封装在不同的存储过程中,由主存储过程根据条件调用:
sql复制CREATE PROCEDURE usp_CalculateShipping
@OrderID INT,
@ShippingMethod VARCHAR(20)
AS
BEGIN
DECLARE @ShippingCost DECIMAL(10,2)
IF @ShippingMethod = 'Standard'
EXEC usp_CalculateStandardShipping @OrderID, @ShippingCost OUTPUT
ELSE IF @ShippingMethod = 'Express'
EXEC usp_CalculateExpressShipping @OrderID, @ShippingCost OUTPUT
ELSE IF @ShippingMethod = 'International'
EXEC usp_CalculateInternationalShipping @OrderID, @ShippingCost OUTPUT
UPDATE Orders
SET ShippingCost = @ShippingCost
WHERE OrderID = @OrderID
END
12. 存储过程与安全
12.1 SQL注入防护
存储过程本身不能完全防止SQL注入,特别是在使用动态SQL时。防护措施:
- 使用参数化查询而非字符串拼接
- 对输入参数进行验证
- 使用QUOTENAME()函数处理标识符
- 限制应用程序的数据库权限
sql复制-- 不安全的动态SQL
DECLARE @SQL NVARCHAR(MAX) = N'SELECT * FROM ' + @TableName + ' WHERE ID = ' + @ID
-- 安全的参数化动态SQL
DECLARE @SQL NVARCHAR(MAX) = N'SELECT * FROM ' + QUOTENAME(@TableName) + ' WHERE ID = @IDParam'
DECLARE @ParmDefinition NVARCHAR(500) = N'@IDParam INT'
EXEC sp_executesql @SQL, @ParmDefinition, @IDParam = @ID
12.2 数据加密
对于敏感数据,可以在存储过程中实现加密:
sql复制CREATE PROCEDURE usp_StoreSensitiveData
@PlainText NVARCHAR(MAX),
@Key VARCHAR(32)
AS
BEGIN
-- 加密数据
DECLARE @Encrypted VARBINARY(MAX)
SET @Encrypted = ENCRYPTBYKEY(KEY_GUID('MySymmetricKey'), @PlainText)
-- 存储加密数据
INSERT INTO SensitiveData (EncryptedContent)
VALUES (@Encrypted)
END
CREATE PROCEDURE usp_RetrieveSensitiveData
@DataID INT,
@Key VARCHAR(32)
AS
BEGIN
-- 解密数据
SELECT
DataID,
CONVERT(NVARCHAR(MAX), DECRYPTBYKEY(EncryptedContent)) AS DecryptedContent
FROM SensitiveData
WHERE DataID = @DataID
END
12.3 行级安全
SQL Server 2016引入了行级安全性,可以在存储过程中实现:
sql复制-- 创建安全策略
CREATE SCHEMA Security
GO
CREATE FUNCTION Security.fn_securitypredicate(@CustomerID INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS fn_securitypredicate_result
WHERE @CustomerID = CAST(SESSION_CONTEXT(N'CustomerID') AS INT)
OR IS_ROLEMEMBER('db_owner') = 1
GO
CREATE SECURITY POLICY Security.CustomerFilter
ADD FILTER PREDICATE Security.fn_securitypredicate(CustomerID)
ON dbo.Orders
GO
-- 在存储过程中设置上下文
CREATE PROCEDURE usp_GetCustomerOrders
@CustomerID INT
AS
BEGIN
EXEC sp_set_session_context 'CustomerID', @CustomerID
SELECT * FROM Orders
END
13. 高级主题:CLR存储过程
对于极复杂的逻辑,可以考虑使用CLR存储过程:
- 在.NET中编写存储过程逻辑
- 编译为程序集
- 在SQL Server中注册程序集
- 创建CLR存储过程
C#示例:
csharp复制using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class AdvancedMath
{
[Microsoft.SqlServer.Server.SqlProcedure]
public static void CalculateStatistics(SqlString data)
{
// 复杂的统计计算逻辑
// ...
// 发送结果
SqlContext.Pipe.Send("计算完成");
}
}
SQL Server中部署:
sql复制CREATE ASSEMBLY AdvancedMath FROM 'C:\path\to\AdvancedMath.dll'
WITH PERMISSION_SET = SAFE
CREATE PROCEDURE usp_CalculateStatistics
@data NVARCHAR(MAX)
AS EXTERNAL NAME AdvancedMath.AdvancedMath.CalculateStatistics
注意:CLR存储过程应谨慎使用,仅当T-SQL无法有效实现功能时才考虑。
14. 存储过程与JSON
现代SQL Server支持JSON处理,可以在存储过程中使用:
sql复制CREATE PROCEDURE usp_SaveJsonOrder
@OrderJson NVARCHAR(MAX)
AS
BEGIN
-- 从JSON提取数据
DECLARE @CustomerID INT = JSON_VALUE(@OrderJson, '$.CustomerID')
DECLARE @OrderDate DATETIME = JSON_VALUE(@OrderJson, '$.OrderDate')
-- 插入订单头
INSERT INTO Orders (CustomerID, OrderDate)
VALUES (@CustomerID, @OrderDate)
DECLARE @OrderID INT = SCOPE_IDENTITY()
-- 插入订单明细
INSERT INTO OrderDetails (OrderID, ProductID, Quantity)
SELECT
@OrderID,
ProductID,
Quantity
FROM OPENJSON(@OrderJson, '$.Items')
WITH (
ProductID INT '$.ProductID',
Quantity INT '$.Quantity'
)
-- 返回JSON响应
SELECT @OrderID AS OrderID, 'Success' AS Status
FOR JSON PATH
END
15. 存储过程与GraphQL
虽然GraphQL通常与REST API关联,但也可以与存储过程集成:
sql复制CREATE PROCEDURE usp_GraphQLQuery
@Query NVARCHAR(MAX)
AS
BEGIN
-- 解析GraphQL查询
DECLARE @OperationType NVARCHAR(20)
DECLARE @EntityType NVARCHAR(50)
-- 简单解析逻辑(实际实现会更复杂)
IF @Query LIKE '%query {%products%'
BEGIN
SELECT * FROM Products FOR JSON PATH
END
ELSE IF @Query LIKE '%query {%orders%'
BEGIN
SELECT
o.OrderID,
o.OrderDate,
o.TotalAmount,
(
SELECT
od.ProductID,
od.Quantity,
p.ProductName
FROM OrderDetails od
JOIN Products p ON od.ProductID = p.ProductID
WHERE od.OrderID = o.OrderID
FOR JSON PATH
) AS Items
FROM Orders o
FOR JSON PATH
END
END
16. 存储过程与AI集成
SQL Server的机器学习服务允许在存储过程中调用Python或R脚本:
sql复制CREATE PROCEDURE usp_PredictSales
@ProductID INT,
@HistoricalMonths INT = 12
AS
BEGIN
DECLARE @PythonScript NVARCHAR(MAX) = N'
import pandas as pd
from sklearn.linear_model import LinearRegression
# 获取输入数据
historical_data = InputDataSet
# 准备模型(简化示例)
X = historical_data[["Month"]]
y = historical_data["Sales"]
model = LinearRegression().fit(X, y)
# 预测下个月
next_month = max(historical_data["Month"]) + 1
prediction = model.predict([[next_month]])
OutputDataSet = pd.DataFrame({"Month": [next_month], "PredictedSales": [prediction[0]]})
'
-- 获取历史数据
DECLARE @HistoricalData NVARCHAR(MAX)
SET @HistoricalData = (
SELECT
DATEDIFF(MONTH, '2000-01-01', SaleDate) AS Month,
SUM(Quantity) AS Sales
FROM Sales
WHERE ProductID = @ProductID
AND SaleDate >= DATEADD(MONTH, -@HistoricalMonths, GETDATE())
GROUP BY DATEDIFF(MONTH, '2000-01-01', SaleDate)
FOR JSON PATH
)
-- 执行Python脚本
EXEC sp_execute_external_script
@language = N'Python',
@script = @PythonScript,
@input_data_1 = @HistoricalData,
@input_data_1_name = N'InputDataSet'
WITH RESULT SETS ((Month INT, PredictedSales DECIMAL(10,2)))
END
17. 存储过程重构技巧
随着系统演进,存储过程也需要重构。常见重构手法:
- 提取方法:将重复代码提取为新的存储过程
- 内联方法:将过于简单的存储过程内联到调用处
- 拆分过程:将过大的存储过程拆分为多个小过程
- 参数化:将硬编码值改为参数
- 引入表变量:替换临时表减少开销
重构示例:
sql复制--