SQL Server 2019 存储过程与自定义函数 --- 语法知识点及使用方法详解
一、存储过程概述
存储过程(Stored Procedure) 是一组预编译的T-SQL语句集合,存储在数据库中,可通过名称调用执行。其主要优势包括:
- 性能提升:预编译,减少语法分析和编译开销。
- 代码复用:一次编写,多次调用。
- 安全控制:可授予用户执行权限而不暴露底层表结构。
- 模块化开发:便于维护和版本管理。
- 减少网络流量:客户端只需发送存储过程名和参数。
二、存储过程分类
| 类型 | 说明 | 前缀 | 示例 |
|---|---|---|---|
| 系统存储过程 | SQL Server内置,用于管理数据库对象 | sp_ |
sp_help, sp_rename |
| 自定义存储过程 | 用户创建,实现业务逻辑 | 无固定前缀(建议避免 sp_) |
usp_GetEmployee |
| 扩展存储过程 | 调用外部DLL(已弃用,不推荐) | xp_ |
xp_cmdshell(需谨慎启用) |
⚠️ 重要提示 :自定义存储过程不要使用
sp_前缀 ,因为SQL Server会优先在master数据库中查找,导致性能下降和潜在命名冲突。
三、创建存储过程
语法结构:
sql
CREATE [OR ALTER] PROC[EDURE] [schema_name.]procedure_name
[ { @parameter [AS] data_type } [ = default ] [ OUT | OUTPUT ] ] [ ,...n ]
[ WITH
{ RECOMPILE | ENCRYPTION | EXECUTE AS Clause }
]
AS
{ sql_statement [;] [...n] }
CREATE OR ALTER(SQL Server 2016 SP1+):若不存在则创建,存在则修改,避免先删后建。WITH RECOMPILE:每次执行都重新编译(适用于参数值变化大导致执行计划失效)。WITH ENCRYPTION:加密存储过程定义(防止被查看,但不防破解,且影响调试)。EXECUTE AS:指定执行上下文(如EXECUTE AS OWNER)。
案例1:创建最简单的存储过程(无参数)
sql
-- 创建存储过程:获取所有员工信息
CREATE PROCEDURE dbo.usp_GetAllEmployees
AS
BEGIN
SET NOCOUNT ON; -- 防止返回影响行数,减少网络流量
SELECT
EmployeeID,
FirstName,
LastName,
Salary,
Performance
FROM dbo.Employees
ORDER BY EmployeeID;
END
GO
-- 调用存储过程(三种方式)
EXEC dbo.usp_GetAllEmployees; -- 推荐方式
EXECUTE dbo.usp_GetAllEmployees; -- 完整写法
dbo.usp_GetAllEmployees; -- 省略EXEC(仅在批处理第一句可用)
GO
注释:
SET NOCOUNT ON是最佳实践,避免返回"影响了X行"的消息。- 使用架构名
dbo.是良好习惯,避免歧义。- 命名规范:建议使用
usp_(User Stored Procedure)前缀。
四、调用存储过程
基本语法:
sql
EXEC[UTE] [schema_name.]procedure_name
[ @parameter = ] { value | @variable [ OUTPUT ] | [ DEFAULT ] } [ ,...n ]
五、创建带输入参数的存储过程
案例2:带输入参数 --- 根据部门ID查询员工
sql
-- 假设Employees表有DeptID字段(如之前章节)
IF COL_LENGTH('dbo.Employees', 'DeptID') IS NULL
BEGIN
ALTER TABLE dbo.Employees ADD DeptID INT;
UPDATE dbo.Employees SET DeptID = 1 WHERE EmployeeID IN (1,3);
UPDATE dbo.Employees SET DeptID = 2 WHERE EmployeeID = 2;
END
GO
-- 创建部门表
IF OBJECT_ID('dbo.Departments') IS NULL
BEGIN
CREATE TABLE dbo.Departments (
DeptID INT PRIMARY KEY,
DeptName NVARCHAR(50)
);
INSERT INTO dbo.Departments VALUES (1, 'IT部'), (2, '人事部');
END
GO
-- 创建带输入参数的存储过程
CREATE PROCEDURE dbo.usp_GetEmployeesByDept
@DeptID INT -- 输入参数
AS
BEGIN
SET NOCOUNT ON;
-- 检查参数有效性
IF @DeptID IS NULL
BEGIN
RAISERROR('部门ID不能为空!', 16, 1);
RETURN;
END
-- 查询指定部门的员工
SELECT
e.EmployeeID,
e.FirstName + ' ' + e.LastName AS FullName,
e.Salary,
d.DeptName
FROM dbo.Employees e
INNER JOIN dbo.Departments d ON e.DeptID = d.DeptID
WHERE e.DeptID = @DeptID
ORDER BY e.EmployeeID;
-- 如果没有找到数据,返回提示
IF @@ROWCOUNT = 0
PRINT '未找到部门ID为 ' + CAST(@DeptID AS NVARCHAR) + ' 的员工。';
END
GO
-- 调用示例
EXEC dbo.usp_GetEmployeesByDept @DeptID = 1;
EXEC dbo.usp_GetEmployeesByDept @DeptID = 999; -- 测试不存在部门
GO
注释:
- 使用
RAISERROR抛出自定义错误。@@ROWCOUNT获取上一条语句影响的行数。- 参数名前加
@,调用时可使用@parameter = value形式(推荐)或按位置传参。
六、创建带输出参数的存储过程
案例3:带输出参数 --- 获取部门员工数和平均薪资
sql
CREATE PROCEDURE dbo.usp_GetDeptStats
@DeptID INT, -- 输入参数
@EmployeeCount INT OUTPUT, -- 输出参数:员工数量
@AverageSalary DECIMAL(10,2) OUTPUT -- 输出参数:平均薪资
AS
BEGIN
SET NOCOUNT ON;
-- 初始化输出参数(良好习惯)
SET @EmployeeCount = 0;
SET @AverageSalary = 0.00;
-- 计算统计信息
SELECT
@EmployeeCount = COUNT(*),
@AverageSalary = AVG(Salary)
FROM dbo.Employees
WHERE DeptID = @DeptID;
-- 如果没有员工,输出参数保持0
IF @EmployeeCount = 0
PRINT '警告:部门ID ' + CAST(@DeptID AS NVARCHAR) + ' 无员工记录。';
END
GO
-- 调用带输出参数的存储过程
DECLARE @Count INT, @AvgSal DECIMAL(10,2);
EXEC dbo.usp_GetDeptStats
@DeptID = 1,
@EmployeeCount = @Count OUTPUT, -- 必须加OUTPUT
@AverageSalary = @AvgSal OUTPUT;
-- 显示结果
PRINT '部门1员工数: ' + CAST(@Count AS NVARCHAR);
PRINT '部门1平均薪资: ' + CAST(@AvgSal AS NVARCHAR);
GO
注释:
- 输出参数必须在调用时使用
OUTPUT关键字。- 在存储过程中,直接给输出参数赋值即可。
- 输出参数可用于返回多个标量值,避免使用
SELECT返回结果集。
七、管理存储过程
1. 修改存储过程
方法一:使用 ALTER PROCEDURE
sql
-- 修改存储过程:添加薪资范围筛选
ALTER PROCEDURE dbo.usp_GetEmployeesByDept
@DeptID INT,
@MinSalary DECIMAL(10,2) = 0.00 -- 新增可选参数,默认0
AS
BEGIN
SET NOCOUNT ON;
IF @DeptID IS NULL
BEGIN
RAISERROR('部门ID不能为空!', 16, 1);
RETURN;
END
SELECT
e.EmployeeID,
e.FirstName + ' ' + e.LastName AS FullName,
e.Salary,
d.DeptName
FROM dbo.Employees e
INNER JOIN dbo.Departments d ON e.DeptID = d.DeptID
WHERE e.DeptID = @DeptID
AND e.Salary >= @MinSalary -- 新增条件
ORDER BY e.EmployeeID;
IF @@ROWCOUNT = 0
PRINT '未找到符合条件的员工。';
END
GO
-- 调用(使用默认参数)
EXEC dbo.usp_GetEmployeesByDept @DeptID = 1;
-- 调用(指定最小薪资)
EXEC dbo.usp_GetEmployeesByDept @DeptID = 1, @MinSalary = 8000;
GO
方法二:使用 CREATE OR ALTER(推荐,SQL Server 2016 SP1+)
sql
-- 如果不存在则创建,存在则修改
CREATE OR ALTER PROCEDURE dbo.usp_GetEmployeesByDept
@DeptID INT,
@MinSalary DECIMAL(10,2) = 0.00
AS
BEGIN
SET NOCOUNT ON;
-- ... 同上逻辑
END
GO
注释:
ALTER PROCEDURE要求存储过程必须已存在。CREATE OR ALTER更灵活,是部署脚本的最佳选择。
2. 查看存储过程信息
方法一:使用系统存储过程 sp_help
sql
-- 查看存储过程基本信息
EXEC sp_help 'dbo.usp_GetAllEmployees';
GO
方法二:使用 sp_helptext 查看定义
sql
-- 查看存储过程的完整T-SQL定义
EXEC sp_helptext 'dbo.usp_GetAllEmployees';
GO
方法三:查询系统视图 sys.sql_modules
sql
-- 查询存储过程定义(支持WHERE条件)
SELECT
OBJECT_NAME(object_id) AS ProcedureName,
definition
FROM sys.sql_modules
WHERE OBJECT_NAME(object_id) = 'usp_GetAllEmployees';
GO
方法四:查看参数信息 sys.parameters
sql
-- 查看存储过程参数详情
SELECT
p.name AS ParameterName,
t.name AS DataType,
p.max_length,
p.precision,
p.scale,
p.is_output AS IsOutput,
p.default_value
FROM sys.parameters p
INNER JOIN sys.types t ON p.system_type_id = t.system_type_id
WHERE p.object_id = OBJECT_ID('dbo.usp_GetDeptStats')
ORDER BY p.parameter_id;
GO
3. 重命名存储过程
sql
-- 使用系统存储过程 sp_rename 重命名
EXEC sp_rename
@objname = 'dbo.usp_GetAllEmployees',
@newname = 'usp_GetEmployeeList',
@objtype = 'OBJECT'; -- OBJECT表示对象类型
-- 验证重命名
EXEC sp_help 'dbo.usp_GetEmployeeList';
GO
注释:
sp_rename会修改系统目录,但不会自动更新依赖对象(如其他存储过程中的调用),需手动检查。- 建议在开发阶段重命名,生产环境谨慎操作。
4. 删除存储过程
sql
-- 删除单个存储过程
DROP PROCEDURE IF EXISTS dbo.usp_GetEmployeeList; -- SQL Server 2016+
-- 删除多个存储过程
DROP PROCEDURE IF EXISTS
dbo.usp_GetEmployeesByDept,
dbo.usp_GetDeptStats;
GO
-- 传统写法(兼容旧版本)
IF OBJECT_ID('dbo.usp_GetEmployeeList', 'P') IS NOT NULL
DROP PROCEDURE dbo.usp_GetEmployeeList;
GO
注释:
DROP PROCEDURE IF EXISTS是SQL Server 2016新增语法,避免"对象不存在"错误。'P'表示Procedure类型,OBJECT_ID的第二个参数用于指定对象类型。
八、扩展存储过程(⚠️ 已弃用,仅作了解)
扩展存储过程允许调用外部DLL(通常用C++编写),以执行SQL Server本身不支持的操作(如操作系统命令)。由于安全风险高,SQL Server 2017+ 已移除对扩展存储过程的支持。
示例(仅在旧版本中可用):
sql
-- 启用 xp_cmdshell(高风险!)
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'xp_cmdshell', 1;
RECONFIGURE;
-- 执行操作系统命令(例如列出目录)
EXEC xp_cmdshell 'dir C:\';
-- 禁用 xp_cmdshell
EXEC sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
EXEC sp_configure 'show advanced options', 0;
RECONFIGURE;
强烈建议:使用SQL Server Agent作业、PowerShell脚本或CLR集成(也已不推荐)替代扩展存储过程。
九、自定义函数
函数(Function)是返回值的例程,可在SQL语句中调用(如 SELECT, WHERE, ORDER BY)。与存储过程主要区别:
| 特性 | 存储过程 | 函数 |
|---|---|---|
| 返回值 | 通过OUTPUT参数或结果集 | 必须返回值(标量或表) |
| 调用方式 | EXEC |
可在表达式中使用 |
| 修改数据 | 允许(INSERT/UPDATE/DELETE) | 不允许(SQL Server限制) |
| 事务控制 | 支持 | 不支持 |
| 错误处理 | 支持TRY...CATCH | 支持,但有限制 |
1. 创建标量函数(Scalar Function)
返回单个值(如INT, NVARCHAR, DECIMAL等)。
案例4:创建标量函数 --- 计算税后薪资
sql
-- 创建标量函数:根据薪资计算税后收入(简化税率)
CREATE FUNCTION dbo.fn_CalculateNetSalary
(
@GrossSalary DECIMAL(10,2) -- 税前薪资
)
RETURNS DECIMAL(10,2) -- 返回类型
AS
BEGIN
DECLARE @TaxRate DECIMAL(3,2) = 0.15; -- 假设税率15%
DECLARE @NetSalary DECIMAL(10,2);
IF @GrossSalary <= 0
SET @NetSalary = 0.00;
ELSE
SET @NetSalary = @GrossSalary * (1 - @TaxRate);
RETURN @NetSalary; -- 必须使用RETURN返回
END
GO
-- 调用标量函数
SELECT
EmployeeID,
FirstName + ' ' + LastName AS FullName,
Salary AS GrossSalary,
dbo.fn_CalculateNetSalary(Salary) AS NetSalary -- 在SELECT中使用
FROM dbo.Employees;
-- 在WHERE子句中使用
SELECT * FROM dbo.Employees
WHERE dbo.fn_CalculateNetSalary(Salary) > 7000;
GO
注释:
- 标量函数必须指定
RETURNS类型。- 函数体用
BEGIN...END包裹。- 使用
RETURN返回计算结果。- 性能注意:标量函数在结果集每行都会调用,可能导致性能瓶颈(尤其在大数据量时)。
2. 创建表值函数(Table-Valued Function)
返回一个表(TABLE),可在FROM子句中使用,如同视图。
类型:
- 内联表值函数(Inline Table-Valued Function):函数体是单个SELECT语句,性能好。
- 多语句表值函数(Multi-Statement Table-Valued Function):函数体包含多个语句,需定义表结构,性能较差。
案例5:内联表值函数 --- 获取指定部门员工列表
sql
-- 创建内联表值函数
CREATE FUNCTION dbo.fn_GetEmployeesByDepartment
(
@DeptID INT
)
RETURNS TABLE -- 返回表类型,无需定义结构
AS
RETURN ( -- 直接返回SELECT语句
SELECT
e.EmployeeID,
e.FirstName + ' ' + e.LastName AS FullName,
e.Salary,
d.DeptName
FROM dbo.Employees e
INNER JOIN dbo.Departments d ON e.DeptID = d.DeptID
WHERE e.DeptID = @DeptID
);
GO
-- 调用:如同使用表或视图
SELECT * FROM dbo.fn_GetEmployeesByDepartment(1);
-- 可与其他表JOIN
SELECT
f.FullName,
f.Salary,
f.DeptName,
dbo.fn_CalculateNetSalary(f.Salary) AS NetSalary
FROM dbo.fn_GetEmployeesByDepartment(1) f
WHERE f.Salary > 7000;
GO
注释:
RETURNS TABLE后直接跟RETURN (SELECT ...)。- 无
BEGIN...END。- 性能接近视图,推荐使用。
案例6:多语句表值函数 --- 返回薪资统计信息
sql
-- 创建多语句表值函数
CREATE FUNCTION dbo.fn_GetSalaryStats()
RETURNS @Stats TABLE -- 定义返回表结构
(
DeptID INT,
DeptName NVARCHAR(50),
EmployeeCount INT,
AvgSalary DECIMAL(10,2),
MaxSalary DECIMAL(10,2),
MinSalary DECIMAL(10,2)
)
AS
BEGIN
-- 插入统计数据
INSERT INTO @Stats (DeptID, DeptName, EmployeeCount, AvgSalary, MaxSalary, MinSalary)
SELECT
d.DeptID,
d.DeptName,
COUNT(e.EmployeeID) AS EmployeeCount,
AVG(e.Salary) AS AvgSalary,
MAX(e.Salary) AS MaxSalary,
MIN(e.Salary) AS MinSalary
FROM dbo.Departments d
LEFT JOIN dbo.Employees e ON d.DeptID = e.DeptID
GROUP BY d.DeptID, d.DeptName;
RETURN; -- 返回填充好的表变量
END
GO
-- 调用函数
SELECT * FROM dbo.fn_GetSalaryStats();
-- 与员工表JOIN获取详细信息
SELECT
s.DeptName,
s.EmployeeCount,
s.AvgSalary,
e.FirstName + ' ' + e.LastName AS HighEarner
FROM dbo.fn_GetSalaryStats() s
INNER JOIN dbo.Employees e ON s.DeptID = e.DeptID
WHERE e.Salary = s.MaxSalary; -- 找出每个部门薪资最高者
GO
注释:
- 必须定义返回表结构
@TableName TABLE (...)。- 函数体以
BEGIN...END包裹。- 使用
INSERT INTO @TableVar填充数据。- 以
RETURN结束(无表达式)。- 性能通常低于内联函数,尽量避免复杂逻辑。
3. 删除函数
sql
-- 删除标量函数
DROP FUNCTION IF EXISTS dbo.fn_CalculateNetSalary;
-- 删除表值函数
DROP FUNCTION IF EXISTS dbo.fn_GetEmployeesByDepartment;
DROP FUNCTION IF EXISTS dbo.fn_GetSalaryStats;
GO
-- 传统写法
IF OBJECT_ID('dbo.fn_CalculateNetSalary', 'FN') IS NOT NULL -- 'FN' for scalar function
DROP FUNCTION dbo.fn_CalculateNetSalary;
IF OBJECT_ID('dbo.fn_GetEmployeesByDepartment', 'IF') IS NOT NULL -- 'IF' for inline TVF
DROP FUNCTION dbo.fn_GetEmployeesByDepartment;
IF OBJECT_ID('dbo.fn_GetSalaryStats', 'TF') IS NOT NULL -- 'TF' for multi-statement TVF
DROP FUNCTION dbo.fn_GetSalaryStats;
GO
注释:
- 标量函数类型代码:
FN- 内联表值函数:
IF- 多语句表值函数:
TF
十、综合性案例
综合案例1:员工管理系统 --- 存储过程组合
sql
-- 场景:实现员工薪资调整、绩效评估、报告生成一体化
-- 1. 创建存储过程:调整员工薪资(带事务和错误处理)
CREATE OR ALTER PROCEDURE dbo.usp_AdjustEmployeeSalary
@EmployeeID INT,
@AdjustmentAmount DECIMAL(10,2), -- 调整金额(正数加薪,负数降薪)
@Reason NVARCHAR(200) = '年度调整'
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- 检查员工是否存在
IF NOT EXISTS (SELECT 1 FROM dbo.Employees WHERE EmployeeID = @EmployeeID)
BEGIN
RAISERROR('员工ID %d 不存在!', 16, 1, @EmployeeID);
RETURN;
END
-- 获取当前薪资
DECLARE @CurrentSalary DECIMAL(10,2);
SELECT @CurrentSalary = Salary FROM dbo.Employees WHERE EmployeeID = @EmployeeID;
-- 计算新薪资
DECLARE @NewSalary DECIMAL(10,2) = @CurrentSalary + @AdjustmentAmount;
-- 检查薪资是否为负数
IF @NewSalary < 0
BEGIN
RAISERROR('调整后薪资不能为负数!', 16, 1);
RETURN;
END
-- 更新薪资
UPDATE dbo.Employees
SET Salary = @NewSalary
WHERE EmployeeID = @EmployeeID;
-- 记录薪资历史(假设SalaryHistory表存在)
IF OBJECT_ID('dbo.SalaryHistory') IS NOT NULL
BEGIN
INSERT INTO dbo.SalaryHistory (EmployeeID, OldSalary, NewSalary, Reason)
VALUES (@EmployeeID, @CurrentSalary, @NewSalary, @Reason);
END
COMMIT TRANSACTION;
PRINT '员工ID ' + CAST(@EmployeeID AS NVARCHAR) + ' 薪资调整成功。';
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- 抛出错误信息
DECLARE @ErrMsg NVARCHAR(4000) = ERROR_MESSAGE();
DECLARE @ErrSeverity INT = ERROR_SEVERITY();
RAISERROR(@ErrMsg, @ErrSeverity, 1);
END CATCH
END
GO
-- 2. 创建存储过程:生成部门薪资报告(使用表值函数)
CREATE OR ALTER PROCEDURE dbo.usp_GenerateDeptSalaryReport
AS
BEGIN
SET NOCOUNT ON;
-- 使用之前创建的表值函数
SELECT
DeptName AS 部门,
EmployeeCount AS 员工数,
AvgSalary AS 平均薪资,
MaxSalary AS 最高薪资,
MinSalary AS 最低薪资
FROM dbo.fn_GetSalaryStats()
ORDER BY AvgSalary DESC;
END
GO
-- 3. 创建存储过程:批量调整部门薪资(使用游标)
CREATE OR ALTER PROCEDURE dbo.usp_AdjustDeptSalary
@DeptID INT,
@AdjustmentRate DECIMAL(3,2) -- 调整比率,如0.1表示10%
AS
BEGIN
SET NOCOUNT ON;
DECLARE @EmpID INT, @CurrentSalary DECIMAL(10,2);
DECLARE @Reason NVARCHAR(200) = '部门整体调整 ' + CAST(@AdjustmentRate*100 AS NVARCHAR) + '%';
-- 声明游标
DECLARE dept_cursor CURSOR FAST_FORWARD FOR
SELECT EmployeeID, Salary
FROM dbo.Employees
WHERE DeptID = @DeptID;
OPEN dept_cursor;
FETCH NEXT FROM dept_cursor INTO @EmpID, @CurrentSalary;
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @AdjustmentAmount DECIMAL(10,2) = @CurrentSalary * @AdjustmentRate;
-- 调用薪资调整存储过程
EXEC dbo.usp_AdjustEmployeeSalary
@EmployeeID = @EmpID,
@AdjustmentAmount = @AdjustmentAmount,
@Reason = @Reason;
FETCH NEXT FROM dept_cursor INTO @EmpID, @CurrentSalary;
END
CLOSE dept_cursor;
DEALLOCATE dept_cursor;
PRINT '部门ID ' + CAST(@DeptID AS NVARCHAR) + ' 所有员工薪资调整完成。';
END
GO
-- 测试综合功能
-- (1) 调整个人薪资
EXEC dbo.usp_AdjustEmployeeSalary @EmployeeID = 1, @AdjustmentAmount = 500, @Reason = '表现优秀';
-- (2) 调整部门薪资
EXEC dbo.usp_AdjustDeptSalary @DeptID = 1, @AdjustmentRate = 0.05; -- IT部加薪5%
-- (3) 生成报告
EXEC dbo.usp_GenerateDeptSalaryReport;
-- 查看结果
SELECT * FROM dbo.Employees;
SELECT * FROM dbo.SalaryHistory;
GO
注释:
- 展示存储过程嵌套调用(
usp_AdjustDeptSalary调用usp_AdjustEmployeeSalary)。- 结合事务和错误处理确保数据一致性。
- 利用表值函数简化报告生成。
- 模拟真实业务场景:个人调整、部门批量调整、报告输出。
综合案例2:订单处理系统 --- 标量函数与存储过程结合
sql
-- 创建订单表
CREATE TABLE dbo.Orders (
OrderID INT IDENTITY(1,1) PRIMARY KEY,
CustomerID INT,
OrderDate DATE DEFAULT GETDATE(),
TotalAmount DECIMAL(12,2),
DiscountRate DECIMAL(3,2) DEFAULT 0.00, -- 折扣率
FinalAmount AS TotalAmount * (1 - DiscountRate) PERSISTED -- 计算列
);
GO
-- 插入测试数据
INSERT INTO dbo.Orders (CustomerID, TotalAmount, DiscountRate) VALUES
(1, 1000.00, 0.10),
(2, 2500.00, 0.15),
(1, 800.00, 0.05);
GO
-- 1. 创建标量函数:计算订单折扣金额
CREATE FUNCTION dbo.fn_CalculateDiscountAmount
(
@TotalAmount DECIMAL(12,2),
@DiscountRate DECIMAL(3,2)
)
RETURNS DECIMAL(12,2)
AS
BEGIN
RETURN @TotalAmount * @DiscountRate;
END
GO
-- 2. 创建标量函数:根据客户ID计算累计消费
CREATE FUNCTION dbo.fn_GetCustomerTotalSpent
(
@CustomerID INT
)
RETURNS DECIMAL(12,2)
AS
BEGIN
DECLARE @Total DECIMAL(12,2);
SELECT @Total = SUM(FinalAmount)
FROM dbo.Orders
WHERE CustomerID = @CustomerID;
RETURN ISNULL(@Total, 0.00);
END
GO
-- 3. 创建存储过程:创建新订单(带客户等级判断)
CREATE OR ALTER PROCEDURE dbo.usp_CreateOrder
@CustomerID INT,
@TotalAmount DECIMAL(12,2),
@OrderID INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION;
-- 计算客户等级(简化:累计消费>5000为VIP,折扣5%)
DECLARE @CustomerTotal DECIMAL(12,2) = dbo.fn_GetCustomerTotalSpent(@CustomerID);
DECLARE @DiscountRate DECIMAL(3,2) = 0.00;
IF @CustomerTotal > 5000
SET @DiscountRate = 0.05;
-- 插入订单
INSERT INTO dbo.Orders (CustomerID, TotalAmount, DiscountRate)
VALUES (@CustomerID, @TotalAmount, @DiscountRate);
SET @OrderID = SCOPE_IDENTITY(); -- 获取新订单ID
COMMIT TRANSACTION;
PRINT '订单创建成功,订单ID: ' + CAST(@OrderID AS NVARCHAR);
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
THROW; -- 重新抛出错误
END CATCH
END
GO
-- 4. 创建存储过程:生成客户订单摘要
CREATE OR ALTER PROCEDURE dbo.usp_GetCustomerOrderSummary
@CustomerID INT
AS
BEGIN
SET NOCOUNT ON;
SELECT
OrderID,
OrderDate,
TotalAmount AS 原价,
DiscountRate AS 折扣率,
dbo.fn_CalculateDiscountAmount(TotalAmount, DiscountRate) AS 折扣金额,
FinalAmount AS 最终金额
FROM dbo.Orders
WHERE CustomerID = @CustomerID
ORDER BY OrderDate DESC;
-- 显示汇总信息
SELECT
COUNT(*) AS 订单总数,
SUM(TotalAmount) AS 累计原价,
SUM(dbo.fn_CalculateDiscountAmount(TotalAmount, DiscountRate)) AS 累计折扣,
SUM(FinalAmount) AS 累计支付
FROM dbo.Orders
WHERE CustomerID = @CustomerID;
END
GO
-- 测试订单系统
DECLARE @NewOrderID INT;
-- 创建新订单(客户1累计消费<5000,无折扣)
EXEC dbo.usp_CreateOrder @CustomerID = 1, @TotalAmount = 1200.00, @OrderID = @NewOrderID OUTPUT;
-- 创建新订单(客户2累计消费>5000,享受5%折扣)
EXEC dbo.usp_CreateOrder @CustomerID = 2, @TotalAmount = 3000.00, @OrderID = @NewOrderID OUTPUT;
-- 查看客户订单摘要
EXEC dbo.usp_GetCustomerOrderSummary @CustomerID = 1;
EXEC dbo.usp_GetCustomerOrderSummary @CustomerID = 2;
-- 验证计算列和函数
SELECT
OrderID,
TotalAmount,
DiscountRate,
dbo.fn_CalculateDiscountAmount(TotalAmount, DiscountRate) AS DiscountAmount,
FinalAmount -- 计算列
FROM dbo.Orders;
GO
注释:
- 展示标量函数在存储过程和SELECT语句中的灵活应用。
- 使用计算列
FinalAmount简化业务逻辑。- 存储过程结合事务确保订单创建的原子性。
- 输出参数
@OrderID返回新创建的订单ID。
十一、最佳实践与注意事项
存储过程:
- 命名规范 :使用
usp_前缀,避免sp_。 - SET NOCOUNT ON:减少网络流量。
- 错误处理 :使用
TRY...CATCH和事务。 - 参数验证:检查输入参数合法性。
- 避免游标:优先使用集合操作。
- 权限最小化 :仅授予
EXECUTE权限。
函数:
- 优先内联表值函数:性能优于多语句函数。
- 慎用标量函数:大数据量时可能导致性能问题(考虑内联或计算列)。
- 无副作用:函数内不能修改数据(INSERT/UPDATE/DELETE)。
- 确定性:尽量创建确定性函数(相同输入永远相同输出),便于优化。
通用:
- 版本控制 :使用
CREATE OR ALTER简化部署。 - 文档注释:在代码中添加注释说明用途、参数、返回值。
- 性能监控 :使用
SET STATISTICS IO/TIME ON分析执行效率。 - 定期维护 :更新统计信息,重建索引,避免参数嗅探问题(可使用
OPTION (RECOMPILE))。
✅ 本章全面覆盖 SQL Server 存储过程与函数的核心语法、管理操作及实战案例,可直接应用于实际开发。牢记:"合理使用存储过程和函数,能极大提升数据库应用的性能与可维护性!"