【问题标题】:Is there a way to dynamically set SELECT Query for a FOR LOOP有没有办法为 FOR LOOP 动态设置 SELECT 查询
【发布时间】:2021-11-13 21:58:33
【问题描述】:

我正在尝试的是根据特定条件迭代 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;

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

【问题讨论】:

    标签: sql oracle plsql


    【解决方案1】:

    您可以在查询中检查参数是否为 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;
    /
    

    【讨论】:

    • 这没有帮助。我不能要求空值。要指定,在我的第一个选择中,我想遍历函数返回的一些行。例如,该函数返回以字母 B 开头的所有员工。在我的第二次选择中,我没有调用该函数,而只是选择了所有员工的姓名,但这只是该函数真正作用的一个非常基本的示例。
    • 好的,这与您的原始问题不同,您的原始问题具有相同的语句和不同的 where 子句 - 这就是我的回答。我针对以 2 种非常不同的方式填充数据的情况更新了答案。
    【解决方案2】:

    您可以在PL/SQL中使用cursorscursor 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;
    

    【讨论】:

    • @MarmiteBomber 重写以完全避免使用字符串。
    【解决方案3】:

    你有两个目标

    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;
    

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

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

    上述方法的问题在于无法扩展。如果您有四个可选条件,则需要 16 倍 IF 和高度冗余的代码。

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

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

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

    这将使绑定变量目标无效!

    为了与示例保持一致,您要使用可选参数 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)
    

    它包含两个绑定变量,优化器(带有 *shortcut 评估)会将其简化为所需的

    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
    

    与此想法相似的主题:hereherehere

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2017-12-15
      • 1970-01-01
      • 1970-01-01
      • 2022-10-24
      • 2017-05-12
      • 1970-01-01
      • 1970-01-01
      • 2013-10-19
      相关资源
      最近更新 更多