SQL Server 2019入门学习教程,从入门到精通,SQL Server 2019 存储过程与自定义函数 — 语法知识点及使用方法详解(15)

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。

十一、最佳实践与注意事项

存储过程:

  1. 命名规范 :使用 usp_ 前缀,避免 sp_
  2. SET NOCOUNT ON:减少网络流量。
  3. 错误处理 :使用 TRY...CATCH 和事务。
  4. 参数验证:检查输入参数合法性。
  5. 避免游标:优先使用集合操作。
  6. 权限最小化 :仅授予 EXECUTE 权限。

函数:

  1. 优先内联表值函数:性能优于多语句函数。
  2. 慎用标量函数:大数据量时可能导致性能问题(考虑内联或计算列)。
  3. 无副作用:函数内不能修改数据(INSERT/UPDATE/DELETE)。
  4. 确定性:尽量创建确定性函数(相同输入永远相同输出),便于优化。

通用:

  • 版本控制 :使用 CREATE OR ALTER 简化部署。
  • 文档注释:在代码中添加注释说明用途、参数、返回值。
  • 性能监控 :使用 SET STATISTICS IO/TIME ON 分析执行效率。
  • 定期维护 :更新统计信息,重建索引,避免参数嗅探问题(可使用 OPTION (RECOMPILE))。

✅ 本章全面覆盖 SQL Server 存储过程与函数的核心语法、管理操作及实战案例,可直接应用于实际开发。牢记:"合理使用存储过程和函数,能极大提升数据库应用的性能与可维护性!"

相关推荐
祢真伟大2 小时前
DM8单库使用DMDRS数据同步到dpc-步骤三
数据库
强子感冒了2 小时前
Javascript学习笔记:BOM和DOM
javascript·笔记·学习
dinga198510262 小时前
MySQL 批量删除海量数据的几种方法
数据库·mysql
Aric_Jones2 小时前
博客RBAC权限模型与安全认证全解析
数据库·安全·oracle
2501_901147832 小时前
学习笔记|LeetCode 739 每日温度:从暴力枚举到单调栈线性最优解
笔记·学习·leetcode
爱编程的Zion2 小时前
小白AI学习笔记---第一章,如何正确使用
人工智能·笔记·学习
Gary Studio2 小时前
rtos入门问题
学习
我命由我123452 小时前
Photoshop - Photoshop 工具栏(64)计数工具
学习·职场和发展·求职招聘·职场发展·课程设计·学习方法·photoshop
科技林总3 小时前
【系统分析师】9.2 数据安全与保密
学习