
[英]Oracle SQL and PL/SQL : how to minimize execution time of retrieving members of the object (returned by user's function)
[英]How to reuse a query block in PL/SQL to minimize code boilerplate and object spaghetti
各位 PL/SQL 专家,这也许是一个无法回答的问题,但也许有人有一个绝妙的解决方案。
今天我发现自己复制粘贴了一个非常冗长的 SQL 语句,以便我可以在一种情况下有条件地加入另一个表,而在另一种情况下不这样做。 显然,这让我这个程序员感到厌烦,他现在有两个 SQL 的副本需要维护:
IF <condition>
THEN
FOR rec_data IN (SELECT <complex SQL, pages long>)
LOOP
PIPE ROW(....);
END LOOP;
ELSE
FOR rec_data IN (SELECT *
FROM (SELECT <same complex SQL, pages long> ) x,
<another table> y)
LOOP
PIPE ROW(....);
END LOOP;
END IF;
如何避免拥有相同 SQL 的两个副本?
选项:
使用动态 SQL 并有条件地将主 SQL 与另一个执行附加连接的查询块一起包装。 缺点:动态 SQL 更难阅读和使用,因为所有 escaping 单引号,维护绑定变量列表等。而且,它是字符串操作,感觉就像糟糕的编码。 而且,我必须定义一个记录类型,其中包含要从中获取的所有列。 更多的工作,更多的冗余编码(我需要为几十个函数做这个,而不仅仅是一个,所以这很重要)。
创建另一个运行核心 SQL 并返回行的流水线 function,然后在我的顶部 function 中以两种不同的方式查询。 缺点:我的代码现在被拆分成一个完全不同的 object,现在我不仅要为行创建类型,而且它们必须是 SQL 类型。 很多定义只是为了一个函数的本地使用。
创建一个全局临时表并加载它的核心 SQL. Select 直接从它或 select 并有条件地加入。 缺点:现在我在我的代码 object 之外有一个用硬编码列定义定义的表,仅供该代码 object 使用。请记住,我有许多这样的函数要编写,我不想要 object 意大利面条。
为核心SQL创建视图,条件查询。 缺点:单独维护 object,加上无法将变量深入视图。
无条件地只使用较长的版本(带有条件连接的版本),但在其中使用花哨的 CASE/DECODE 来有效地禁用连接(例如DECODE(<condition>,x.join_key,NULL) = y.join_key
)。 缺点:这是相当 hacky,如果您有条件地加入的附加“表”是昂贵的 PL/SQL 管道 function,那么获得性能优势可能并不那么容易。我试图避免调用 function,如果不需要 output。
所希望的是避免必须复制粘贴,避免字符串操作,并避免必须定义列定义只是为了获取只在本地需要的东西。 就像我们需要做这样的事情(从我的 PL/SQL cursor var 中读取 SQL...这是伪代码,我知道你不能按照写的那样做!)
DECLARE
CURSOR cur_data IS
SELECT <complex SQL>;
BEGIN
IF <condition>
THEN
FOR rec_data IN (SELECT *
FROM cur_data)
LOOP
PIPE ROW(....);
END LOOP;
ELSE
FOR rec_data IN (SELECT *
FROM cur_data,
other_table)
LOOP
PIPE ROW(....);
END LOOP;
END IF;
任何疯狂的想法?
Dynamic SQL 是处理样板代码的好方法。 在大多数编程语言中,动态代码都是有问题的,因为很难对编程语言和环境进行推理,而且字符串操作很丑陋。 Oracle 有一些功能可以缓解这些问题。
Oracle 提供了数据字典和 PL/Scope 等工具,可以更轻松地推断我们的数据库环境和代码。 SQL 对象很容易通过使用ALL_TAB_COLUMNS
等视图的简单 SQL 语句来理解。
Oracle 具有可以显着清理字符串操作代码的功能。 我们可以通过组合多行字符串、替代引用机制和简单的模板系统来构建更清晰的代码,而不是无休止的连接和使用大量的引号。
多行字符串意味着简单地使用本机行尾而不是连接CHR(10)||CHR(13)
。 (我很困惑为什么 2023 年的一些语言不支持这样一个简单的概念。)替代的引用语法允许我们指定我们自己的分隔符,比如q'....!'
, q'[...]'
和q'<...>'
- 不再有双引号。 模板不需要花哨的引擎,只需要简单的变量语法和REPLACE
函数。
declare
-- Create a SQL template with well-formatted code.
-- The variables will be replaced later.
-- In trivial examples, templating may need more lines of code than concatenation,
-- but for REAL code, defining the template in one place up front is a life-saver.
v_sql clob :=
q'[
insert into some_table
select 'a' b, 'c' d, '#VALUE1#'
from #TABLE1#
#WHERE1#
]'
begin
-- Set variables.
-- (In real code, you may need to worry about SQL injection and the performance of
-- using literals instead of bind variables.)
v_value1 := 'A';
v_table1 := 'dual';
v_where := 'where 1=1'
...
-- Replace the variables here.
v_sql := replace(replace(replace(v_sql
, '#VALUE1#', v_value1)
, '#TABLE1#', v_table1)
, '#WHERE1#', v_where);
-- Printing the SQL is useful for debugging.
dbms_output.put_line(v_sql);
-- Run the SQL.
-- (This will get more complicated for bind variables and retrieving results.)
execute immediate v_sql;
end;
/
缺点最少的解决方案似乎使用普通 SQL 和有条件关闭的连接。 在我的例子中,它是一个昂贵的流水线 function,我并不总是需要它的 output。 所以:
SELECT *
FROM (<very long main query>)
LEFT OUTER JOIN (SELECT * FROM expensivefunction(in_param => 12345)) s ON 'N' = var_bypass_function
在 PLSQL 中将 var_bypass_function 设置为 Y 或 N,然后执行 cursor。我已经通过 dbms_output 跟踪验证,当连接条件始终为 false(Y=N、1=2 等)时,它会修剪整个块并绕过执行一共function。 所以没有必要把我的 SQL 放在两个不同的地方。
由于有很多条件和 SQL 的变化取决于这些条件,也许你可以创建一个小的 package 来完成它。 您可以在那里定义您的长查询(仅一次)并将其与一些嵌入变量组合以处理变化的部分。 这是您应该根据您的环境和需要进行调整的基本结构。
这是 package:
CREATE OR REPLACE
PACKAGE REF_CURSORS AS
--
Procedure Init;
--
Function Get_Cursor(p_add_select IN VARCHAR2 := 'Select base.* ', p_add_from IN VARCHAR2 := '', p_add_where IN VARCHAR2 := '') RETURN SYS_REFCURSOR;
--
Procedure Do_It;
--
END REF_CURSORS;
...和 package 正文:
CREATE OR REPLACE
PACKAGE BODY REF_CURSORS AS
-- variables to construct different cursors
m_sql VarChar2(4000) := '';
m_select VarChar2(2000) := '';
m_from VarChar2(255) := '';
m_where VarChar2(255) := '';
--
-- here you can declare all the variables that you need - just once - use them later to fetch into
c_ID Number(6) := 0;
c_NAME VarChar2(32) := '';
c_BORN DATE;
--
-- ---------------------------------------------------------------------------------------------------
PROCEDURE Init AS
BEGIN
-- here you can define the part that doesn't change of your pages long SQL and embed some variables for parts that does change
m_sql := m_select || ' FROM (SELECT 1 "ID", ''John'' "NAME", To_Date(''1987-NOV-27'', ''yyyy-MON-dd'') "BORN" From dual Union All
SELECT 2 "ID", ''Mary'' "NAME", To_Date(''1989-MAY-29'', ''yyyy-MON-dd'') "BORN" From dual Union All
SELECT 3 "ID", ''Mike'' "NAME", To_Date(''1991-JAN-20'', ''yyyy-MON-dd'') "BORN" From dual
) base' || ' ' || m_from || ' ' || m_where;
--
END Init;
-- -----------------------------------------------------------------------------------------------------------
FUNCTION Get_Cursor (p_add_select IN VARCHAR2 := 'Select base.* ', p_add_from IN VARCHAR2 := '', p_add_where IN VARCHAR2 := '') RETURN SYS_REFCURSOR AS
BEGIN
Declare
m_cursor SYS_REFCURSOR;
Begin
-- building cursors
m_select := p_add_select;
m_from := p_add_from;
m_where := p_add_where;
Init;
-- return built cursor
OPEN m_cursor FOR m_sql;
RETURN m_cursor;
End;
END Get_Cursor;
-- ------------------------------------------------------------------------------------------------------------
PROCEDURE Do_It AS
BEGIN
Declare
l_cursor SYS_REFCURSOR;
Begin
For i In 0..3 Loop
If i = 0 Then
l_cursor := Get_Cursor();
Else
l_cursor := Get_Cursor(p_add_where => ' WHERE ID = ' || i);
End If;
DBMS_OUTPUT.PUT_LINE(m_sql || Chr(10)); -- test print out
LOOP
FETCH l_cursor Into c_ID, c_NAME, c_BORN;
EXIT WHEN l_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(c_ID || ' | ' || c_NAME || ' | ' || c_BORN); -- test print out
END LOOP;
CLOSE l_cursor;
End Loop;
--
l_cursor := Get_Cursor( p_add_select => 'Select added.* ',
p_add_from => 'Left Join (Select 4 "ID", ''Bob'' "NAME", To_Date(''06.07.2000'', ''dd.mm.yyyy'') "BORN" From Dual) added ON(added.ID > base.ID)',
p_add_where => ' WHERE added.ID = 4');
FETCH l_cursor Into c_ID, c_NAME, c_BORN;
DBMS_OUTPUT.PUT_LINE('-- ************************************ --'); -- test print out
DBMS_OUTPUT.PUT_LINE(m_sql); -- test print out
DBMS_OUTPUT.PUT_LINE(c_ID || ' | ' || c_NAME || ' | ' || c_BORN); -- test print out
End;
END Do_It;
END REF_CURSORS;
测试功能在 Do_It Procedure 中编码,其中将一些循环和条件放在一起,最后嵌入了一些额外的数据,并设置了全新的 select 语句。
调用和结果打印输出:
SET SERVEROUTPUT ON
BEGIN
REF_CURSORS.Do_It;
END;
/
anonymous block completed
Select base.* FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual
) base
1 | John | 27-NOV-87
2 | Mary | 29-MAY-89
3 | Mike | 20-JAN-91
Select base.* FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual
) base WHERE ID = 1
1 | John | 27-NOV-87
Select base.* FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual
) base WHERE ID = 2
2 | Mary | 29-MAY-89
Select base.* FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual
) base WHERE ID = 3
3 | Mike | 20-JAN-91
-- ************************************ --
Select added.* FROM (SELECT 1 "ID", 'John' "NAME", To_Date('1987-NOV-27', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 2 "ID", 'Mary' "NAME", To_Date('1989-MAY-29', 'yyyy-MON-dd') "BORN" From dual Union All
SELECT 3 "ID", 'Mike' "NAME", To_Date('1991-JAN-20', 'yyyy-MON-dd') "BORN" From dual
) base Left Join (Select 4 "ID", 'Bob' "NAME", To_Date('06.07.2000', 'dd.mm.yyyy') "BORN" From Dual) added ON(added.ID > base.ID) WHERE added.ID = 4
4 | Bob | 06-JUL-00
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.