DM
存储过程介绍 存储过程是一种在数据库中存储复杂程序,以便外部程序调用的一种数据库对象。
存储过程是为了完成特定功能的SQL
语句集,经编译创建并保存在数据库中,用户通过指定存储过程的名字并给定参数来调用执行。
处理模式 | 执行流程 | 核心问题 | 适用场景 |
---|---|---|---|
应用层 SQL |
应用层拼接 SQL → 网络传输 → 数据库解析 SQL → 执行 → 返回结果 |
1. 网络 IO 开销大(尤其批量处理);2. SQL 重复解析,性能损耗;3. 业务逻辑分散在应用层,维护成本高 |
简单查询、单条数据操作 |
DM 存储过程 |
应用层调用存储过程 → 数据库直接执行预编译 SQL → 返回结果 |
1. 需提前定义,灵活性略低;2. 调试难度高于普通 SQL |
批量数据处理、复杂业务逻辑(如报表生成、数据同步)、权限管控严格的场景 |
优点:
SELECT
指令来运行,因为它是子程序,与查看表,数据表或用户定义函数不同。缺点:
**存储过程最大的优点就是可以通过“数据处理本地化”来提升性能:**假设需批量更新 10 万条数据:若用应用层循环执行 UPDATE,需 10 万次网络传输 + 10 万次 SQL 解析;而用 DM 存储过程,仅需 1 次调用(网络传输)+1 次预编译(存储过程创建时已完成),执行效率可提升 10 倍以上。这也是金融、电商等 “大数据量处理场景” 优先选择存储过程的核心原因。
数据库: DM8(安装过程省略)
操作系统: Kylin10 SP2
创建测试表
1.创建用户表(USER_INFO
)
CREATE TABLE USER_INFO (
USER_ID INT PRIMARY KEY, -- 用户ID(主键)
USER_NAME VARCHAR(50) NOT NULL, -- 用户名
PHONE VARCHAR(20) UNIQUE, -- 手机号(唯一)
REGISTER_TIME DATETIME NOT NULL, -- 注册时间
STATUS CHAR(1) DEFAULT '1' -- 状态:1-正常,0-禁用
);
2.创建订单表(ORDER_INFO
)
CREATE TABLE ORDER_INFO (
ORDER_ID INT PRIMARY KEY, -- 订单ID(主键)
USER_ID INT NOT NULL, -- 关联用户ID
ORDER_AMOUNT DECIMAL(10,2) NOT NULL, -- 订单金额
ORDER_TIME DATETIME NOT NULL, -- 下单时间
PAY_STATUS CHAR(1) DEFAULT '0', -- 支付状态:0-未支付,1-已支付,2-退款
FOREIGN KEY (USER_ID) REFERENCES USER_INFO(USER_ID) -- 外键关联用户表
);
3.插入测试数据
-- 插入3个用户
INSERT INTO USER_INFO (USER_ID, USER_NAME, PHONE, REGISTER_TIME, STATUS)
VALUES
(1, '张三', '13800138000', '2024-01-01 10:00:00', '1'),
(2, '李四', '13900139000', '2024-02-15 14:30:00', '1'),
(3, '王五', '13700137000', '2024-03-20 09:15:00', '0');
-- 插入5个订单
INSERT INTO ORDER_INFO (ORDER_ID, USER_ID, ORDER_AMOUNT, ORDER_TIME, PAY_STATUS)
VALUES
(1001, 1, 99.90, '2024-01-02 08:30:00', '1'),
(1002, 1, 199.50, '2024-01-10 15:20:00', '1'),
(1003, 2, 2999.00, '2024-02-16 11:00:00', '0'),
(1004, 2, 499.00, '2024-03-01 16:40:00', '1'),
(1005, 3, 89.00, '2024-03-21 10:10:00', '0');
-- 提交事务
COMMIT;
验证数据:
SELECT * FROM USER_INFO;
SELECT * FROM ORDER_INFO;
学习DM存储过程首先先从最简单的存储过程开始。我们创建一个存储过程,实现调用时输出"Hello DM!"
1.创建存储过程SQL
-- 创建存储过程PROC_HELLO_DM
CREATE OR REPLACE PROCEDURE PROC_HELLO_DM
AS
-- 声明部分:变量、游标等(此案例无变量,故为空)
BEGIN
-- 执行部分:业务逻辑
DBMS_OUTPUT.PUT_LINE('Hello DM!'); -- 输出内容
END;
2.关键语法解析
CREATE OR REPLACE
:若存储过程已存在,则覆盖(避免 “存储过程已存在” 报错);PROCEDURE PROC_HELLO_DM
:PROCEDURE
是关键字,PROC_HELLO_DM
是存储过程名(命名规范:前缀 PROC
_+ 业务含义);AS
:分隔 “存储过程名” 与 “声明部分”(也可使用IS
,DM 中AS
与IS
等价);BEGIN...END
:执行部分,包含具体的业务逻辑;DBMS_OUTPUT.PUT_LINE
:DM 提供的输出函数,类似 Java 的System.out.println
,需开启输出才能看到结果。3.调用存储过程
在DM中,我们通过call
语句调用存储过程
-- 调用存储过程
CALL PROC_HELLO_DM();
变量是存储过程中存储临时数据的 “容器”,DM 支持多种变量类型(如INT
、VARCHAR
、DATETIME
、DECIMAL
),且支持 % TYPE
% ROWTYPE
”等动态类型,提升代码灵活性。
创建存储过程,查询用户ID=1的用户名和注册时间,输出
1.创建存储过程SQL
CREATE OR REPLACE PROCEDURE PROC_QUERY_USER
AS
-- 声明变量:变量名 类型 [默认值]
v_user_id INT := 1; -- 用户ID,默认值1
v_user_name VARCHAR(50); -- 用户名
v_register_time DATETIME; -- 注册时间
BEGIN
-- 1. 从USER_INFO表查询数据,赋值给变量
SELECT USER_NAME, REGISTER_TIME
INTO v_user_name, v_register_time -- 注意:INTO后变量顺序需与SELECT字段顺序一致
FROM USER_INFO
WHERE USER_ID = v_user_id;
-- 2. 输出结果
DBMS_OUTPUT.PUT_LINE('用户ID:' || v_user_id); -- ||是字符串拼接符
DBMS_OUTPUT.PUT_LINE('用户名:' || v_user_name);
DBMS_OUTPUT.PUT_LINE('注册时间:' || TO_CHAR(v_register_time, 'YYYY-MM-DD HH24:MI:SS')); -- 格式化日期
END;
2.关键语法解析
变量声明格式:变量名 类型 [:= 默认值]
,如v_user_id INT := 1
;
INTO
子句:用于将SELECT
查询结果赋值给变量,要求查询结果只能有1行
字符串拼接:DM用||拼接字符串,如:'用户ID:' || v_user_id
日期格式化:TO_CHAR(v_register_time, 'YYYY-MM-DD HH24:MI:SS')
将DATETIME
类型转换为字符串,避免默认格式。
3.调用存储结果
CALL PROC_QUERY_USER();
如果当表结构发生变化时,若变量声明时用固定类型,则原本的存储过程会报错,需要手动修改存储过程;而使用%TYPE
(引用列类型)或%ROWTYPE
引用表行类型,可以自动适配表结构变化,减少维护成本。
1.%TYPE:引用列类型
使用%TYPE
声明变量,查询用户 ID=2 的手机号。
存储过程SQL
CREATE OR REPLACE PROCEDURE PROC_QUERY_USER_PHONE
AS
-- v_phone的类型与USER_INFO表的PHONE列一致
v_phone USER_INFO.PHONE%TYPE;
v_user_id USER_INFO.USER_ID%TYPE := 2; -- v_user_id的类型与USER_ID列一致
BEGIN
SELECT PHONE
INTO v_phone
FROM USER_INFO
WHERE USER_ID = v_user_id;
DBMS_OUTPUT.PUT_LINE('用户ID=' || v_user_id || '的手机号:' || v_phone);
END;
调用结果:
call PROC_QUERY_USER_PHONE();
2.%ROWTYPE:引用表行类型
用%ROWTYPE
声明变量,存储用户 ID=3 的整行数据,并输出用户名和状态
存储过程SQL:
CREATE OR REPLACE PROCEDURE PROC_QUERY_USER_ROW
AS
-- v_user_row的类型与USER_INFO表的“一行数据”一致,包含所有列
v_user_row USER_INFO%ROWTYPE;
BEGIN
SELECT *
INTO v_user_row
FROM USER_INFO
WHERE USER_ID = 3;
DBMS_OUTPUT.PUT_LINE('用户名:' || v_user_row.USER_NAME);
DBMS_OUTPUT.PUT_LINE('状态:' || CASE v_user_row.STATUS WHEN '1' THEN '正常' WHEN '0' THEN '禁用' END);
END;
调用结果:
CALL PROC_QUERY_USER_ROW();
使用%ROWTYPE
比单独声明多个变量更简洁。例如,若需访问用户的USER_NAME
、PHONE
、REGISTER_TIME
三个列,用%ROWTYPE
只需 1 个变量,而单独声明需 3 个变量。
存储过程的核心用法就是实现复杂业务逻辑,流程控制(if else,LOOP循环等)是实现逻辑分支的关键。
IF
条件判断创建存储过程,输入用户ID,判断用户状态
1.创建存储过程SQL
CREATE OR REPLACE PROCEDURE PROC_JUDGE_USER_STATUS(
-- 输入参数:IN表示“仅传入值,不返回”
p_user_id IN INT
)
AS
v_user_name USER_INFO.USER_NAME%TYPE;
v_status USER_INFO.STATUS%TYPE;
BEGIN
-- 查询用户状态和用户名
SELECT USER_NAME, STATUS
INTO v_user_name, v_status
FROM USER_INFO
WHERE USER_ID = p_user_id;
-- IF条件判断
IF v_status = '1' THEN
DBMS_OUTPUT.PUT_LINE('用户' || v_user_name || '(ID=' || p_user_id || ')正常');
ELSIF v_status = '0' THEN -- 注意:DM中是ELSIF,不是ELSE IF
DBMS_OUTPUT.PUT_LINE('用户' || v_user_name || '(ID=' || p_user_id || ')已禁用');
ELSE
DBMS_OUTPUT.PUT_LINE('用户' || v_user_name || '(ID=' || p_user_id || ')状态异常');
END IF;
END;
2.关键语法解析
IN p_user_id INT
表示p_user_id
是输入参数,调用时必须传入值;IF...ELSIF...ELSE
:多分支判断,注意ELSIF
的拼写(无空格,非ELSE IF
);END IF
:必须闭合IF
块,否则会报语法错误。3.调用存储过程
-- 调用1:用户ID=1(正常)
CALL PROC_JUDGE_USER_STATUS(1);
-- 调用2:用户ID=3(禁用)
CALL PROC_JUDGE_USER_STATUS(3);
DMSQL 程序支持五种类型的循环语句:LOOP 语句、WHILE 语句、FOR 语句、REPEAT 语句和 FORALL 语句。其中前四种为基本类型的循环语句:LOOP 语句循环重复执行一系列语句,直到 EXIT 语句终止循环为止;WHILE 语句循环检测一个条件表达式,当表达式的值为 TRUE 时就执行循环体的语句序列;FOR 语句对一系列的语句重复执行指定次数的循环;REPEAT 语句重复执行一系列语句直至达到条件表达式的限制要求。FORALL 语句对一条 DML 语句执行多次,当 DML 语句中使用数组或嵌套表时可进行优化处理,能大幅提升性能。
在这里重点讲解最常用的LOOP
和FOR
创建存储过程,批量插入5条用户测试数据,使用LOOP实现
存储过程SQL
CREATE OR REPLACE PROCEDURE PROC_INSERT_USER_LOOP
AS
v_user_id INT := 10; -- 起始用户ID
v_count INT := 0; -- 计数器,控制插入次数
BEGIN
-- 无限循环
LOOP
-- 插入数据
INSERT INTO USER_INFO (USER_ID, USER_NAME, PHONE, REGISTER_TIME, STATUS)
VALUES (v_user_id, '测试用户' || v_user_id, '13800' || v_user_id || '0000', SYSDATE, '1');
-- 计数器+1
v_count := v_count + 1;
-- 退出条件:插入5次后退出
EXIT WHEN v_count >= 5;
-- 用户ID+1
v_user_id := v_user_id + 1;
END LOOP;
-- 提交事务(批量插入需手动提交)
COMMIT;
DBMS_OUTPUT.PUT_LINE('成功插入' || v_count || '条用户数据');
END;
调用与验证
CALL PROC_INSERT_USER_LOOP();
-- 验证插入结果
SELECT USER_ID, USER_NAME, PHONE FROM USER_INFO WHERE USER_ID >= 10;
FOR
循环:固定次数循环创建存储过程,用FOR
循环查询用户 ID 从 1 到 5 的用户名,输出结果。
存储过程SQL
CREATE OR REPLACE PROCEDURE PROC_QUERY_USER_FOR
AS
v_user_name USER_INFO.USER_NAME%TYPE;
BEGIN
-- FOR循环:从1到5(包含5),每次i+1
FOR i IN 1..5 LOOP
-- 捕获“用户ID不存在”的错误(避免循环中断)
BEGIN
SELECT USER_NAME
INTO v_user_name
FROM USER_INFO
WHERE USER_ID = i;
DBMS_OUTPUT.PUT_LINE('用户ID=' || i || ':' || v_user_name);
EXCEPTION
WHEN NO_DATA_FOUND THEN -- 未找到数据时触发
DBMS_OUTPUT.PUT_LINE('用户ID=' || i || ':不存在');
END;
END LOOP;
END;
调用与验证
CALL PROC_QUERY_USER_FOR();
为什么在FOR
循环中加EXCEPTION
?
若用户 ID=4 不存在,SELECT...INTO
会报NO_DATA_FOUND
错误,若不捕获,整个存储过程会中断,后续的用户 ID=5 查询也无法执行。在循环内部加局部EXCEPTION
,可确保即使某一次循环出错,后续循环仍能正常执行。
当存储过程需要处理SELECT
返回的多行数据时(如批量更新订单状态、批量计算用户积分),变量无法满足需求,此时需用游标(CURSOR) 。游标本质是 “指向多行结果集的指针”,可逐行读取数据并处理。
静态游标是只读游标,它总是按照打开游标时的原样显示结果集,在编译时就能确定静态游标使用的查询。
静态游标又分为两种:隐式游标和显式游标。
隐式游标: 隐式游标使用 FOR ... IN
结构,由系统自动管理游标的打开、读取和关闭,代码简洁,推荐在 DM8 中使用。
显示游标: 显式游标需要手动管理 OPEN
、FETCH
、CLOSE
,在 DM8 中也支持。更适合需要精细控制游标行为的场景;
创建存储过程,查询所有"已支付"(PAY_STATUS=1)的订单,输出订单ID、用户ID、订单金额,并计算总金额。
1.存储过程SQL
隐式游标
CREATE OR REPLACE PROCEDURE PROC_QUERY_PAID_ORDERS
AS
v_total_amount DECIMAL(12, 2) := 0; -- 总金额
BEGIN
-- 隐式游标遍历订单数据
FOR rec IN (
SELECT ORDER_ID, USER_ID, ORDER_AMOUNT
FROM ORDER_INFO
WHERE PAY_STATUS = '1'
) LOOP
-- 输出订单信息
DBMS_OUTPUT.PUT_LINE('订单ID:' || rec.ORDER_ID || ',用户ID:' || rec.USER_ID || ',金额:' || rec.ORDER_AMOUNT);
-- 累加总金额
v_total_amount := v_total_amount + rec.ORDER_AMOUNT;
END LOOP;
-- 输出总金额
DBMS_OUTPUT.PUT_LINE('------------------------');
DBMS_OUTPUT.PUT_LINE('已支付订单总金额:' || v_total_amount);
END;
显示游标
CREATE OR REPLACE PROCEDURE PROC_QUERY_PAID_ORDERS
AS
-- 声明显式游标
CURSOR cur_paid_orders IS
SELECT ORDER_ID, USER_ID, ORDER_AMOUNT
FROM ORDER_INFO
WHERE PAY_STATUS = '1';
-- 声明变量用于接收游标数据
v_order_id cur_paid_orders.ORDER_ID%TYPE;
v_user_id cur_paid_orders.USER_ID%TYPE;
v_order_amount cur_paid_orders.ORDER_AMOUNT%TYPE;
v_total_amount DECIMAL(12, 2) := 0; -- 总金额
BEGIN
-- 打开游标
OPEN cur_paid_orders;
-- 循环读取数据
LOOP
FETCH cur_paid_orders INTO v_order_id, v_user_id, v_order_amount;
EXIT WHEN cur_paid_orders%NOTFOUND;
-- 输出订单信息
DBMS_OUTPUT.PUT_LINE('订单ID:' || v_order_id || ',用户ID:' || v_user_id || ',金额:' || v_order_amount);
-- 累加总金额
v_total_amount := v_total_amount + v_order_amount;
END LOOP;
-- 关闭游标
CLOSE cur_paid_orders;
-- 输出总金额
DBMS_OUTPUT.PUT_LINE('------------------------');
DBMS_OUTPUT.PUT_LINE('已支付订单总金额:' || v_total_amount);
END;
2.关键语法解析
显示游标
CURSOR 游标名 IS SELECT语句
,定义要处理的多行结果集OPEN
(打开游标,执行 SELECT)→ FETCH
(逐行读取数据)→ CLOSE
(关闭游标);%NOTFOUND
(游标无数据时为 TRUE),用于判断是否退出循环;3.调用结果
CALL PROC_QUERY_PAID_ORDERS();
普通游标的查询条件是固定的(如PAY_STATUS='1'
),若需根据传入参数动态调整查询条件(如查询指定用户的订单),需用动态游标。
与静态游标不同,动态游标在声明部分只是先声明一个游标类型的变量,并不指定其关联的查询语句,在执行部分打开游标时才指定查询语句。动态游标的使用主要在定义和打开时与显式游标不同,下面进行详细介绍。
1.存储过程SQL
CREATE OR REPLACE PROCEDURE PROC_QUERY_ORDERS_DYNAMIC(
p_user_id IN INT, -- 输入:用户ID
p_pay_status IN CHAR(1) -- 输入:支付状态
)
AS
-- 1. 声明动态游标类型
TYPE ref_cursor_type IS REF CURSOR RETURN DLJTEST.ORDER_INFO%ROWTYPE;
-- 2. 声明动态游标变量
cur_dynamic_orders ref_cursor_type;
-- 3. 声明变量存储游标数据
v_order_row DLJTEST.ORDER_INFO%ROWTYPE;
BEGIN
-- 4. 打开动态游标:根据输入参数调整查询条件
OPEN cur_dynamic_orders FOR
SELECT *
FROM DLJTEST.ORDER_INFO
WHERE USER_ID = p_user_id
AND PAY_STATUS = p_pay_status;
-- 5. 逐行读取并输出
DBMS_OUTPUT.PUT_LINE('用户ID=' || p_user_id || ',支付状态=' || p_pay_status || '的订单:');
LOOP
FETCH cur_dynamic_orders INTO v_order_row;
EXIT WHEN cur_dynamic_orders%NOTFOUND;
DBMS_OUTPUT.PUT_LINE('订单ID:' || v_order_row.ORDER_ID || ',金额:' || v_order_row.ORDER_AMOUNT || ',时间:' || TO_CHAR(v_order_row.ORDER_TIME, 'YYYY-MM-DD'));
END LOOP;
-- 6. 关闭游标
CLOSE cur_dynamic_orders;
END;
2.调用与结果
CALL PROC_QUERY_ORDERS_DYNAMIC(1, '1');
存储过程执行过程中可能出现错误(如数据不存在、主键冲突等),若不处理,会直接中断执行并抛出错误信息;而异常处理可以捕获错误,执行自定义逻辑(如回滚事务、记录日志),让存储过程更加健壮。
DM 预定义了多种常见异常(如NO_DATA_FOUND
、DUP_VAL_ON_INDEX
),以下是企业级开发中最常用的异常处理案例。下面创建一个存储过程,,输入用户 ID 和新手机号,更新用户手机号;若用户不存在或手机号已存在,输出错误信息。
1.存储过程SQL
CREATE OR REPLACE PROCEDURE PROC_UPDATE_USER_PHONE(
IN p_user_id INT, -- 输入:用户ID
IN p_new_phone VARCHAR(20) -- 输入:新手机号
)
AS
BEGIN
-- 更新手机号
UPDATE USER_INFO
SET PHONE = p_new_phone
WHERE USER_ID = p_user_id;
-- 判断是否更新成功(影响行数为0表示用户不存在)
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20001, '用户ID=' || p_user_id || '不存在'); -- 手动抛出异常
END IF;
-- 提交事务
COMMIT;
DBMS_OUTPUT.PUT_LINE('用户ID=' || p_user_id || '的手机号已更新为:' || p_new_phone);
-- 异常处理块
EXCEPTION
-- 1. 捕获“未找到数据”异常(虽已用SQL%ROWCOUNT判断,仍保留以防遗漏)
WHEN NO_DATA_FOUND THEN
ROLLBACK; -- 回滚事务
DBMS_OUTPUT.PUT_LINE('错误:用户ID=' || p_user_id || '不存在');
-- 2. 捕获“主键/唯一键冲突”异常(手机号是唯一键,重复会触发)
WHEN DUP_VAL_ON_INDEX THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('错误:手机号' || p_new_phone || '已被占用');
-- 3. 捕获“手动抛出的异常”(RAISE_APPLICATION_ERROR抛出的异常)
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('未知错误:' || SQLERRM); -- SQLERRM:获取错误信息
DBMS_OUTPUT.PUT_LINE('错误代码:' || SQLCODE); -- SQLCODE:获取错误代码
END;
2.关键语法解析
SQL%ROWCOUNT
:返回上一条SQL语句影响的行数,用于判断更新/插入是否成功;RAISE_APPLICATION_ERROR
:手动抛出异常,第一个参数是错误代码,第二个参数是错误信息;NO_DATA_FOUND
:SELECT...INTO
未找到数据时触发;DUP_VAL_ON_INDEX
:插入 / 更新数据违反唯一键约束时触发;OTHERS
:捕获所有未被明确处理的异常,是 “兜底” 处理;ROLLBACK
:异常发生时回滚事务,避免数据不一致。3.调用测试
**正常更新:**用户 ID=1,新手机号 = 13800138001(未被占用)
CALL PROC_UPDATE_USER_PHONE(1, '13800138001');
**用户不存在:**用户 ID=99,新手机号 = 13800138002
CALL PROC_UPDATE_USER_PHONE(99, '13800138002');
**手机号重复:**用户 ID=2,新手机号 = 13800138001(已被用户 1 占用)
DM 存储过程的核心价值,正体现在对关键场景的精准适配:批量数据处理时,凭 “本地化执行” 减少网络 IO 与 SQL 解析开销,用 FORALL 等优化手段让效率倍增;复杂业务逻辑场景下,可封装如订单计算、报表生成等逻辑,避免业务分散于应用层,降低维护成本;权限管控严格的领域,能通过细粒度执行权限(而非表级权限)保障数据安全,还兼容 Oracle 语法降低迁移门槛。加之异常处理机制保障数据一致,使其成为国产数据库场景下高效、安全处理数据的实用工具。
文章
阅读量
获赞