Databend SQL 存储过程使用指南
一、什么是存储过程?
存储过程(Stored Procedure)是一组预编译的 SQL 语句集合,它们被保存在数据库中,可以像函数一样被重复调用。想象一下,如果你经常需要执行一系列复杂的数据处理操作,与其每次都手动输入这些 SQL 语句,不如将它们封装成一个存储过程,需要时直接调用即可。
存储过程的优势
- 代码复用:一次编写,多次调用,避免重复代码
- 性能优化:预编译的 SQL 语句执行效率更高
- 业务逻辑封装:将复杂的业务逻辑封装在数据库层
- 维护便利:统一管理和修改业务逻辑
- 安全性:通过权限控制,限制用户对底层数据的直接访问
二、第一个存储过程:Hello World
让我们从最简单的例子开始。假设我们需要一个简单的加法存储过程:
CREATE PROCEDURE my_add(a Int32, b Int32)
RETURNS Int32
LANGUAGE SQL
AS $$
BEGINRETURN a + b;
END;
$$;
语法解析
让我们逐行理解这个存储过程:
CREATE PROCEDURE my_add:创建一个名为my_add的存储过程(a Int32, b Int32):定义输入参数a和b,类型为 Int32RETURNS Int32:指定返回值类型为 Int32LANGUAGE SQL:指定使用 SQL 语言编写(目前 Databend 仅支持 SQL)AS $$ ... $$:使用美元符号包裹存储过程的主体代码BEGIN ... END:存储过程主体的开始和结束标记RETURN a + b:执行计算并返回结果
调用存储过程
创建后,我们可以这样调用它, 注意参数类型需要显式指定:
call PROCEDURE my_add(3::Int,4::Int);
----
7
SqlScript
存储过程中的语法我们称之为 SqlScript, 我们也可以直接使用 execute immediate 来执行 SqlScript 语句。
- 执行单个 SQL
execute immediate 'CREATE TABLE test (id Int32)';
- 执行多个 SQL, 用 begin 和 end 包裹
execute immediate $$
BEGINselect 33;let s RESULTSET := select number from numbers(100);RETURN TABLE(s);
END;
$$;
三、进阶:使用变量和流程控制
现在让我们学习如何在 SqlScript 中使用变量、条件判断和循环。
3.1 变量声明和使用
Scalar 变量
在 Databend 中,使用 LET 关键字声明变量:
语法有:
LET <variable_name> := <value>-- 声明并初始化变量 xLET <variable_name> [<type>] := <value>-- 声明并初始化变量 xLET <variable_name> [<type>] DEFAULT <value>-- 声明并初始化变量 xLET <variable_name> [<type>]-- 声明变量 x, 后续初始化
execute immediate $$
BEGINLET sum := 0; -- 声明并初始化变量 sumFOR i IN 1 TO 10 DOIF i % 2 = 0 THENsum := sum + i; -- 累加偶数END IF;END FOR;RETURN sum;
END;
$$;
在这个例子中:
LET sum := 0:声明一个名为sum的变量并初始化为 0:=:赋值操作符RETURNS UInt8 NOT NULL:指定返回值不能为 NULL
ResultSet 变量
ResultSet 变量用于存储查询结果集,语法有:
示例语法:
execute immediate $$
BEGINLET x RESULTSET := select number from numbers(10);RETURN TABLE(x);
END;
$$;
上面是返回结果集,所以使用 RETURN TABLE(x) 语句
Cursor 变量
Cursor 变量用于遍历结果集,语法有:
LET <cursor_variable> CURSOR for <query>LET <cursor_variable> CURSOR for <result_set_variable>OPEN <cursor_variable>FETCH <cursor_variable> INTO <variable>CLOSE <cursor_variable>for <variable> in <cursor_variable> do ... end for
示例语法:
execute immediate $$
BEGINLET v Int;LET c CURSOR for select max(number) from numbers(10);OPEN c;FETCH c INTO v;CLOSE c;let d RESULTSET := select number from numbers(10);let e CURSOR for d;for v2 in e dov := v + v2.number;end for;return v;
END;
$$;
3.2 条件判断:IF-THEN-ELSEIF-ELSE
IF 语句允许我们根据条件执行不同的代码分支:
execute immediate $$
BEGINLET score := 57 + 10 + 10 + 10;LET grade := '';IF score >= 90 THENgrade := '优秀';ELSEIF score >= 80 THENgrade := '良好';ELSEIF score >= 70 THENgrade := '中等';ELSEIF score >= 60 THENgrade := '及格';ELSEgrade := '不及格';END IF;RETURN grade;
END;
$$;
3.3 循环:FOR 循环
FOR 循环有两种常见形式:
形式一:范围循环
FOR i IN start_value TO end_value DO-- 循环体
END FOR;
示例:
execute immediate $$
BEGINLET sum := 0;FOR i IN 1 TO 10 DOsum := sum + i;END FOR;RETURN sum;
END;
$$;
形式二:结果集循环
示例:
execute immediate $$
BEGIN-- 声明一个结果集变量LET x RESULTSET := SELECT number n FROM numbers(10);LET sum := 0;-- 遍历结果集FOR r IN x DO-- 使用 r.n 访问列值sum := sum + r.n;END FOR;RETURN sum;
END;
$$;
四、高级应用:嵌套循环与复杂逻辑
让我们看一个更复杂的例子,展示嵌套循环和多层逻辑:
execute immediate $$
BEGIN-- 声明结果集变量:从 0 到 9 的数字LET x RESULTSET := SELECT number n FROM numbers(10);LET sum := 0;-- 外层循环:遍历结果集FOR x IN x DO-- 内层循环:从 0 到当前数字FOR batch IN 0 TO x.n DOIF batch % 2 = 0 THENsum := sum + batch; -- 偶数加ELSEsum := sum - batch; -- 奇数减END IF;END FOR;END FOR;RETURN sum;
END;
$$;
逻辑分析
让我们分析一下这个过程的执行流程:
- 外层循环:遍历 0-9 这 10 个数字
- 内层循环:对于每个数字 n,从 0 循环到 n
- 条件判断:如果是偶数则加,奇数则减
例如当 x.n = 3 时:
-
batch = 0(偶):sum += 0
-
batch = 1(奇):sum -= 1
-
batch = 2(偶):sum += 2
-
batch = 3(奇):sum -= 3
动态拼接语句,嵌套执行
execute immediate $$
BEGINLET tbl_name := 'abcd1' ;LET drop_sql := 'DROP TABLE default."' || tbl_name || '"' ;EXECUTE IMMEDIATE :drop_sql ;
END ;
$$ ;
五、返回表格数据
除了返回单个值,存储过程还可以返回整张表:
execute immediate $$
BEGINRETURN TABLE(SELECTnumber % 3 d,SUM(number) AS total_amountFROM numbers(10)GROUP BY d) ;
END ;
$$ ;
六、存储过程管理
| 操作 | SQL | 说明 |
|---|---|---|
| 查看所有存储过程 | SHOW PROCEDURES; | |
| 查看存储过程详情 | DESC PROCEDURE sum_even_numbers(UInt8, UInt8);或 DESCRIBE PROCEDURE sum_even_numbers(UInt8, UInt8); | 注意: • 无参数的存储过程使用空括号: DESC PROCEDURE proc_name()• 有参数的必须指定确切的参数类型 |
| 删除存储过程 | DROP PROCEDURE my_add(int, int); | |
| 替换存储过程 | CREATE OR REPLACE PROCEDURE my_add(a Int32, b Int32)RETURNS Int32LANGUAGE SQLAS $$BEGINRETURN a + b + 3;END;$$; |
七、 最佳实践
7.1 命名规范
- 使用有意义的名称,清晰表达功能
- 使用下划线分隔单词(snake_case)
- 添加前缀区分不同类型的过程(如
calc_,get_,update_)
-- 好的命名
CREATE PROCEDURE calc_monthly_revenue(...)
CREATE PROCEDURE get_active_users(...)
CREATE PROCEDURE update_user_status(...)-- 不好的命名
CREATE PROCEDURE proc1(...)
CREATE PROCEDURE x(...)
7.2 注释说明
始终为存储过程添加清晰的注释:
CREATE PROCEDURE process_orders(order_date DATE)
RETURNS INT
LANGUAGE SQL
COMMENT = '处理指定日期的订单,返回处理数量'
AS $$ ... $$ ;
7.3 性能考虑
- 避免过度循环:对于大数据集,尽量使用集合操作而非逐行循环
- 合理使用索引:在存储过程中查询的表应有适当的索引
- 批量操作:尽可能使用批量插入/更新而非逐条处理
- 结果集大小:返回表格时,使用 LIMIT 限制结果集大小
八、实战案例:数据清洗流程
让我们用二个实际案例来综合运用所学知识:
9.1 清理和归档不活跃用户数据
CREATE OR REPLACE PROCEDURE cleanup_user_data(days_threshold INT)
RETURNS TABLE(action VARCHAR,user_count INT,processed_at TIMESTAMP
)
LANGUAGE SQL
COMMENT = '清理和归档不活跃用户数据'
AS $$
BEGINLET cutoff_date := DATE_SUB(DAY, days_threshold,today()) ;LET inactive_users := 0 ;LET deleted_users := 0 ;-- 统计不活跃用户LET inactive_resultset RESULTSET :=SELECT COUNT(*) AS cntFROM usersWHERE last_login_date < cutoff_dateAND status = 'active' ;FOR r IN inactive_resultset DOinactive_users := r.cnt ;END FOR ;-- 标记不活跃用户UPDATE usersSET status = 'inactive'WHERE last_login_date < cutoff_dateAND status = 'active' ;-- 删除长期不活跃用户DELETE FROM usersWHERE last_login_date < DATE_SUB(cutoff_date, INTERVAL days_threshold DAY)AND status = 'inactive' ;-- 返回处理结果RETURN TABLE(SELECT'Marked Inactive' AS action,inactive_users AS user_count,CURRENT_TIMESTAMP() AS processed_atUNION ALLSELECT'Deleted' AS action,deleted_users AS user_count,CURRENT_TIMESTAMP() AS processed_at) ;
END ;
$$ ;
调用方式:
-- 清理 90 天未登录的用户
CALL PROCEDURE cleanup_user_data(90::Int) ;
9.2 扫描表并合并数据到target表
CREATE OR REPLACE PROCEDURE PROC_MERGE_GPS()
RETURNS STRING
LANGUAGE SQL
AS
$$
BEGINcreate or replace table default.gps as select number from numbers(100) ;create or replace table default.abcd1 as select number from numbers(100) ;create or replace table default.abcd2 as select number from numbers(100) ;create or replace table default.abcd3 as select number from numbers(100) ;-- Step 1: 查询符合条件的表名(使用 INFORMATION_SCHEMA)LET records RESULTSET := (select name from system.tables where database = 'default' and name like '%abcd%') ;LET table_count := 0 ;LET record_count := 0 ;LET table_names := [] ;LET union_parts := [] ;for table_record in records DOLET name := table_record.name ;table_count := table_count + 1 ;table_names := ARRAY_APPEND(table_names, name) ;union_parts := ARRAY_APPEND(union_parts, 'SELECT * FROM default.' || name) ;END FOR ;-- 如果没有匹配的表,直接返回IF (table_count = 0) THENRETURN 'No data to process' ;END IF ;-- Step 3: 创建临时视图LET view_sql := 'CREATE OR REPLACE VIEW default.TEMPORARY_GPS_TABLES AS ' || ARRAY_TO_STRING(union_parts, ' UNION ALL ') ;EXECUTE IMMEDIATE :view_sql ;-- Step 2: 查询表中的记录数LET record_count_sql := 'SELECT COUNT(*) c FROM default.TEMPORARY_GPS_TABLES' ;LET r RESULTSET := EXECUTE IMMEDIATE :record_count_sql ;for record in r DOrecord_count := record.c ;END FOR ;-- Step 4: 设置会话参数, exampleEXECUTE IMMEDIATE 'set max_block_size = 65536' ;-- Step 5: 执行 示例SQLLET merge_sql := 'insert into default.gps select * from default.TEMPORARY_GPS_TABLES;' ;EXECUTE IMMEDIATE :merge_sql ;-- Step 6: 清理:删除视图EXECUTE IMMEDIATE 'DROP VIEW IF EXISTS default.TEMPORARY_GPS_TABLES' ;-- Step 7: 删除所有 %abcd% 表FOR i IN 1 TO ARRAY_SIZE(table_names) DOLET tbl_name := table_names[i]::STRING ;LET drop_sql := 'DROP TABLE default."' || tbl_name || '"' ;EXECUTE IMMEDIATE :drop_sql ;END FOR ;RETURN 'Merge completed successfully. Processed ' || table_count || ' tables. Total records: ' || record_count ;
END ;
$$ ;
调用结果:
call PROCEDURE PROC_MERGE_GPS() ;
---
Merge completed successfully. Processed 3 tables. Total records: 300
九、总结
Databend 的 SQL 存储过程为数据处理提供了强大而灵活的工具。通过本文,我们学习了:
- 基础语法:如何创建和调用存储过程
- 变量和赋值:使用 LET 声明和管理变量
- 流程控制:IF 条件判断和 FOR 循环
- 高级特性:嵌套循环、结果集遍历、返回表格
- 管理操作:查看、描述、删除存储过程
- 最佳实践:命名规范、注释、错误处理、性能优化
关键要点回顾
- ✅ 使用
CREATE PROCEDURE创建存储过程 - ✅ 使用
CALL PROCEDURE调用存储过程 - ✅ 使用
EXECUTE IMMEDIATE执行动态 SQL - ✅ 使用
LET声明变量,:=赋值 - ✅ 支持
IF-THEN-ELSE条件判断 - ✅ 支持
FOR...IN...DO循环 - ✅ 可以返回单个值或整张表
- ✅ 使用
CREATE OR REPLACE更新存储过程 - ✅ 使用
RESULTSET类型处理查询结果
下一步
现在你已经掌握了 Databend 存储过程的核心知识,可以开始:
- 在自己的项目中创建简单的存储过程
- 逐步引入更复杂的逻辑和流程控制
- 将常用的数据处理任务封装为存储过程
- 探索更多高级特性和优化技巧
Happy coding with Databend! 🚀
关于 Databend
Databend 是一款开源、弹性、低成本,基于对象存储也可以做实时分析的新式湖仓。期待您的关注,一起探索云原生数仓解决方案,打造新一代开源 Data Cloud。
👨💻 Databend Cloud:databend.cn
📖 Databend 文档:docs.databend.cn
💻 Wechat:Databend
✨ GitHub:github.com/databendlab…
