有没有办法动态设置 SELECT 查询 FOR 循环

Is there a way to dynamically set SELECT Query for a FOR LOOP

我正在尝试的是根据特定条件迭代 SELECT。

CREATE OR REPLACE PROCEDURE procedure
IS
selectForLoop varchar2(256);
v_birthdate date;
BEGIN
select X into v_birthdate from Y where C = Z;
IF true THEN
   selectForLoop := 'Select name from employees';
ELSE
   selectForLoop := 'Select name from employees where birthdate = v_birthdate';
END IF;

FOR val in (selectForLoop)
LOOP
   BusinessLogic
END LOOP;

我能做的是:

CREATE OR REPLACE PROCEDURE procedure
IS
v_birthdate date;
BEGIN 
select X into v_birthdate from Y where C = Z;
If true THEN
   FOR i IN (Select name from employees)
   LOOP
      BusinessLogic
   END LOOP;
ELSE
   FOR i IN (Select name from employees where birthdate = v_birthdate
   LOOP
      BusinessLogic
   END LOOP;
END IF;

这将是一个解决方案,但业务逻辑在这两种方式上都是相同的,只是我想要迭代的数据不同。我真的很想阻止那种肮脏的复制粘贴尝试。 有什么建议吗?

您可以检查查询中的参数是否为 NULL。示例(基于示例数据emp/dept)


CREATE OR REPLACE PROCEDURE list_employees (hired_after_i DATE DEFAUlT NULL)
AS
BEGIN
  FOR r IN 
    (SELECT * FROM emp 
      WHERE hiredate > hired_after_i OR 
            hired_after_i IS NULL)
  LOOP
    dbms_output.put_line(r.ename||' hired on '||r.hiredate);
  END LOOP;
END;
/
Procedure LIST_EMPLOYEES compiled

set serveroutput on size 999999
BEGIN
  list_employees;
END;
/

SMITH hired on 17-DEC-1980
ALLEN hired on 20-FEB-1981
WARD hired on 22-FEB-1981
JONES hired on 02-APR-1981
MARTIN hired on 28-SEP-1981
BLAKE hired on 01-MAY-1981
CLARK hired on 09-JUN-1981
SCOTT hired on 19-APR-1987
KING hired on 17-NOV-1981
TURNER hired on 08-SEP-1981
ADAMS hired on 23-MAY-1987
JAMES hired on 03-DEC-1981
FORD hired on 03-DEC-1981
MILLER hired on 23-JAN-1982

PL/SQL procedure successfully completed.

BEGIN
  list_employees(hired_after_i => DATE'1982-01-01');
END;
/
SCOTT hired on 19-APR-1987
ADAMS hired on 23-MAY-1987
MILLER hired on 23-JAN-1982

PL/SQL procedure successfully completed.

如果您想查看一个集合,但它的填充方式不同,那么这也是一种可能。对于此示例,我根据参数的值以 2 种不同的方式填充 pl/sql 集合,然后循环遍历集合一次。

create or replace PROCEDURE list_employees2 (some_input_variable VARCHAR2)
AS
  TYPE emp_t  IS TABLE OF emp.ename%TYPE INDEX BY BINARY_INTEGER;
  t_emp emp_t;
BEGIN
  IF some_input_variable = 'A' THEN
    SELECT ename 
      BULK COLLECT INTO t_emp 
      FROM emp WHERE hiredate > DATE'1982-01-01';
  ELSIF some_input_variable = 'B' THEN
    t_emp(1) := 'KOEN';
    t_emp(2) := 'MIKE';
  END IF;
  FOR i IN 1 .. t_emp.COUNT LOOP
    dbms_output.put_line(t_emp(i));
  END LOOP;
END;
/

您可以在 PL/SQL 中使用 cursors and cursor variables 在查询之间动态切换。

CREATE OR REPLACE PROCEDURE procedure
IS
  selectForLoop varchar2(256);
  v_name employees%name%TYPE;
  v_birthdate date;
  v_cursor SYS_REFCURSOR;
BEGIN
  IF condition THEN
    OPEN v_cursor FOR SELECT name FROM employees;
  ELSE
    OPEN v_cursor FOR SELECT name FROM employees WHERE birthdate = 
 v_birthdate;
  END IF;

  LOOP
    FETCH v_cursor INTO v_name;
    EXIT WHEN v_cursor%NOTFOUND;
    BusinessLogic;
  END LOOP;
  CLOSE v_cursor;
END;

您还可以将游标变量作为参数传递给您的业务逻辑,并将循环移动到业务逻辑过程中:

OPEN v_cursor FOR selectForLoop;
BusinessLogic(v_cursor);
CLOSE v_cursor;


CREATE OR REPLACE
PROCEDURE BusinessLogic(p_cursor SYS_REFCURSOR IN) IS
  v_name employees%name%TYPE;
BEGIN
  LOOP
    FETCH p_cursor INTO v_name;
    EXIT WHEN p_cursor%NOTFOUND;
    -- Do business logic
  END LOOP;
END;

你有两个objectives

  1. 保留代码DRY

  2. 使用绑定变量

如果您只有一个非常简单的条件(如您的示例,使用过滤器或不使用过滤器),您可以使用 IF 语句打开 不同 游标两种情况。

IF salary_from is null THEN
  OPEN v_cursor FOR SELECT LAST_NAME, SALARY FROM hr.employees;
ELSE
  OPEN v_cursor FOR SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= salary_from;
END IF;

请注意,您应该小心使用谓词SALARY >= salary_from OR salary_from is NULL[=26] OR 解决方案 =]

为什么?您将 一个查询 用于两种截然不同的场景。游标可以 returns 所有数据或非常有限的数据,这可能需要 不同的 访问方法(例如索引访问与完整 table 扫描)。因此,在一种情况下,您 可能 和次优计划。

上述方法的问题在于它无法缩放。如果你有四个可选标准,你将需要 16 倍 IF 和一个高度冗余的代码。

保持上述 objective 有效的解决方案是什么?

使用动态SQL,但不要连接条件值,例如

 SELECT LAST_NAME, SALARY FROM hr.employees WHERE salary >= 1000 and salary <= 10000

这将使绑定变量无效 objective!

为了与带有可选参数 salary_fromsalary_to 的示例保持一致,您要使用

open v_cursor for v_sql using salary_from, salary_to;

但是这要求 both 绑定变量必须在查询文本中定义 - 如果您只有 salary_from 应该做什么?

打开此查询的游标

SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from 

会导致错误

ORA-01006: bind variable does not exist

诀窍是使用 dummy 谓词总是返回 true,但包含一个绑定变量(将被忽略)。

因此,如果您只有 salary_from 作为过滤器,您将创建以下动态 SQL

SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from AND  (1=1 or SALARY <= :salary_to)

它包含绑定变量,优化器(使用*快捷方式求值)会将其简化为所需的

SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from

所以工资过滤器示例的相关代码是

IF salary_from is NOT null THEN
  v_sql := 'SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from';
ELSE
  v_sql := 'SELECT LAST_NAME, SALARY FROM hr.employees WHERE (1 = 1 or SALARY >= :salary_from)';  
END IF;
IF salary_to is NOT null THEN
  v_sql := v_sql ||' AND  SALARY <= :salary_to';
ELSE
  v_sql := v_sql ||' AND  (1=1 or SALARY <= :salary_to)';  
END IF;
open v_cursor for v_sql using salary_from, salary_to;

下面是四种情况SQL生成的概览

-- no filter
SELECT LAST_NAME, SALARY FROM hr.employees WHERE (1 = 1 or SALARY >= :salary_from) AND  (1=1 or SALARY <= :salary_to)
-- salary_from 
SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from AND  (1=1 or SALARY <= :salary_to)
-- salary from, to
SELECT LAST_NAME, SALARY FROM hr.employees WHERE SALARY >= :salary_from AND  SALARY <= :salary_to
-- salary_to
SELECT LAST_NAME, SALARY FROM hr.employees WHERE (1 = 1 or SALARY >= :salary_from) AND  SALARY <= :salary_to

归功于此想法的类似主题:here, here and