Oracle SQL:将WITH ... SELECT ...重构为函数的最干净方法是什么

时间:2018-07-23 03:36:43

标签: sql oracle plsql sql-function

因此,我一直在尝试寻找最佳方法,以

的形式在计划脚本中重写大量SQL。
WITH
A AS (...<SUB_QA>...),
B AS (...<SUB_QB>...),
C AS (...<SUB_QC>...),
...

SELECT ... FROM
A 
LEFT JOIN B 
LEFT JOIN C
LEFT JOIN ...
ON ....

进入功能。这主要是为了便于在多个位置重用该大块表示的相同逻辑。

  • 约束1:只能使用RECORD而不是创建自定义的TYPE;

  • 约束2:必须保留那些子查询的内容(例如 ,等等)保留在WITH子句下,因为每个 相当复杂。

到目前为止,我仅提出了以下内容作为简化示例。

  • 它涉及将WITH子句放在光标循环中
  • 但是它是否在每个循环中运行WITH子句,这在性能方面会引起很大的困扰?以函数形式编写时,以及当我使用SqlDeveloper的“说明计划”函数运行时,它根本不会透露很多有用的信息。
  • 是否有更好/更清洁/更高效的方式来做到这一点?

SQL创建数据:

--------PERSON table------------
DROP TABLE Test_Persons;
CREATE TABLE Test_Persons (
    PersonID int,
    LastName varchar2(255),
    FirstName varchar2(255)
);

INSERT INTO Test_Persons
    (PersonID,LastName,FirstName)
    values(1,'LN_1','FN_1');

INSERT INTO Test_Persons
    (PersonID,LastName,FirstName)
    values(2,'LN_2','FN_2');

INSERT INTO Test_Persons
    (PersonID,LastName,FirstName)
    values(3,'LN_21','FN_2');

--------Salary table------------
DROP TABLE TEST_SALARY_A;
CREATE TABLE TEST_SALARY_A ( -- no 'OR REPLACE' for ORACLE
    SalaryID int,
    PersonID int,
    Amount int,
    Tax int,
    Bank varchar2(20)
);

INSERT INTO TEST_SALARY_A
    (SalaryID, PersonID, Amount, Tax, Bank)
    VALUES
    (1, 1, 1000, 300, 'BOA1');

INSERT INTO TEST_SALARY_A
    (SalaryID, PersonID, Amount, Tax, Bank)
    VALUES
    (2, 2, 2000, 600, 'JP1');

INSERT INTO TEST_SALARY_A
    (SalaryID, PersonID, Amount, Tax, Bank)
    VALUES
    (3, 3, 3000, 900, 'TD1');

--------Address table------------
DROP TABLE TEST_ADDRESS_A;
CREATE TABLE TEST_ADDRESS_A ( 
    AddressID int,
    PersonID int,
    Address varchar2(255)
);

INSERT INTO TEST_ADDRESS_A
    (AddressID, PersonID, Address)
    VALUES
    (1, 1, 'address1');

INSERT INTO TEST_ADDRESS_A
    (AddressID, PersonID, Address)
    VALUES
    (2, 2, 'address2');

INSERT INTO TEST_ADDRESS_A
    (AddressID, PersonID, Address)
    VALUES
    (3, 3, 'address3');

commit;

块中的原始SQL:

------------------Original--------------------
WITH 
TEST_JOINED_1 AS (
    SELECT
        tps.PERSONID,
        tps.LASTNAME,
        tsd.ADDRESS
    FROM TEST_PERSONS tps
    LEFT JOIN TEST_ADDRESS_A tsd ON tps.personid = tsd.personid WHERE tps.LASTNAME = 'LN_1'
),
TEST_JOINED_2 AS (
    SELECT
        tps.PERSONID,
        tsl.BANK,
        tsl.TAX
    FROM TEST_PERSONS tps
    LEFT JOIN TEST_SALARY_A tsl ON tps.personid = tsl.personid WHERE tps.LASTNAME = 'LN_1'
)

SELECT tj1.PERSONID as tj1_ID, tj1.LASTNAME, tj1.ADDRESS, tj2.PERSONID as tj2_ID, tj2.BANK, tj2.TAX
  FROM TEST_JOINED_1 tj1
  LEFT JOIN TEST_JOINED_2 tj2 ON tj1.PERSONID = tj2.PERSONID
WHERE tj1.LASTNAME = 'LN_1';

用功能重写:

------------------Rewritten in functions with ------------------
------------------Contraint 1: can only use RECORD instead of creating customized TYPE;------------
------------------Contraint 2: have to keep the content of the two subqueries under WITH clause exactly as it is --------------
CREATE OR REPLACE PACKAGE MY_JOIN_TEST_SP_PACKAGE_3 AS

    TYPE join_record_type IS RECORD(
      PersonID1 int,
      LastName varchar2(255),
      Address varchar2(255),
      PersonID2 int,
      Bank varchar2(20),
      Tax   int
    );

    TYPE join_record_table_type IS TABLE OF join_record_type;

    FUNCTION get_joined_data(last_name VARCHAR2)
        RETURN join_record_table_type
        PIPELINED;
END;
/

CREATE OR REPLACE PACKAGE BODY MY_JOIN_TEST_SP_PACKAGE_3 AS

    FUNCTION get_joined_data(last_name VARCHAR2)
        RETURN join_record_table_type
        PIPELINED 
        AS

        join_record join_record_type;

        BEGIN
            FOR x IN (
                -------------------start - WITH clause -- does this run for every RECORD x in the loop??? -----------------------
                WITH 
                TEST_JOINED_1 AS (
                    SELECT
                        tps.PERSONID,
                        tps.LASTNAME,
                        tsd.ADDRESS
                    FROM TEST_PERSONS tps
                    LEFT JOIN TEST_ADDRESS_A tsd ON tps.personid = tsd.personid WHERE tps.LASTNAME = last_name

                ),
                TEST_JOINED_2 AS (
                    SELECT
                        tps.PERSONID,
                        tsl.BANK,
                        tsl.TAX
                    FROM TEST_PERSONS tps
                    LEFT JOIN TEST_SALARY_A tsl ON tps.personid = tsl.personid WHERE tps.LASTNAME = last_name
                )
                -------------------end - WITH clause -------------------

                -------------------start - main select-----------------------
                SELECT tj1.PERSONID as tj1_ID, tj1.LASTNAME, tj1.ADDRESS, tj2.PERSONID as tj2_ID, tj2.BANK, tj2.TAX
                  FROM TEST_JOINED_1 tj1
                  LEFT JOIN TEST_JOINED_2 tj2 ON tj1.PERSONID = tj2.PERSONID
                WHERE tj1.LASTNAME = last_name
                -------------------end - main select--------------------------             
             )
          LOOP
            SELECT x.tj1_ID, x.LASTNAME, x.ADDRESS, x.tj2_ID, x.BANK, x.TAX
                INTO join_record
                FROM DUAL;
            PIPE ROW (join_record);
          END LOOP;
        END;
END; -- END of CREATE
/

select * from table(MY_JOIN_TEST_SP_PACKAGE_3.get_joined_data('LN_1'));

已编辑:修改示例代码,使其在WITH子句中具有变量

----------------------Create GLOBAL TEMPORARY TABLE -------------------------
DROP TABLE my_global_temp_table;
CREATE GLOBAL TEMPORARY TABLE my_global_temp_table (
      PersonID int,
      LastName varchar2(255),
      Address varchar2(255),
      Bank varchar2(20)
)
ON COMMIT DELETE ROWS;

----------------------Create PACKAGE AND FUNCTION -------------------------
CREATE OR REPLACE PACKAGE MY_JOIN_TEST_SP_PACKAGE_3 AS

    TYPE join_record_type IS RECORD(
      PersonID int,
      LastName varchar2(255),
      Address varchar2(255),
      Bank varchar2(20)
    );

    TYPE join_record_table_type IS TABLE OF join_record_type;

    FUNCTION get_joined_data(last_name VARCHAR2)
        RETURN join_record_table_type
        PIPELINED;
END;
/

CREATE OR REPLACE PACKAGE BODY MY_JOIN_TEST_SP_PACKAGE_3 AS


    FUNCTION get_joined_data(last_name VARCHAR2)
        RETURN join_record_table_type
        PIPELINED 
        AS

        join_record join_record_type;

        BEGIN
            --------------------use GLOBAL TEMPORARY TABLE-------------------------
            INSERT INTO my_global_temp_table
                    -------------------start - WITH ... SELECT ... clause -- does this run for every RECORD x in the loop??? -----------------------
                    WITH 
                    TEST_JOINED_1 AS (
                        SELECT
                            tps.PERSONID,
                            tps.LASTNAME,
                            tsd.ADDRESS
                        FROM TEST_PERSONS tps
                        LEFT JOIN TEST_ADDRESS_A tsd ON tps.personid = tsd.personid
                        WHERE tps.LASTNAME = last_name
                    )
                    SELECT tj1.PERSONID, tj1.LASTNAME, tj1.ADDRESS, ts.BANK
                      FROM TEST_JOINED_1 tj1
                      LEFT JOIN TEST_SALARY_A ts ON tj1.PERSONID = ts.PERSONID
                    WHERE tj1.LASTNAME = last_name;
                    -------------------end - WITH ... SELECT ... clause --         

            FOR x IN (
                SELECT * FROM my_global_temp_table
            )
            LOOP
                SELECT x.PERSONID, x.LASTNAME, x.ADDRESS, x.BANK
                    INTO join_record
                    FROM DUAL;
                PIPE ROW (join_record);
            END LOOP;
        END;
END; -- END of CREATE
/

--------------------Call the FUNCTION-------------------------
select * from table(MY_JOIN_TEST_SP_PACKAGE_3.get_joined_data('LN_1'));

已编辑:根据@Littefoot建议,尝试使用CREATE GLOBAL TEMP表,但给出“ 17/21 PL / SQL:ORA-00936:缺少表达式”。我不确定为什么吗?

已编辑:纠正了Insert语法,但会出现错误“ ORA-14551:无法在查询内执行DML操作”,我相信这是因为我从SELECT <中调用了包含该Insert的函数。 / strong>

2 个答案:

答案 0 :(得分:2)

如果您不使用变量和其他pl sql构造,建议您以表或实例化视图的形式破坏子句。这样,您无需冒着在pl sql块中重写查询逻辑并丢失某些内容的风险。 我建议使用物化视图而不是表来使用物化视图,因为它的优点是您下次加载数据时无需删除表,并且可以对物化视图使用nologging。 这将非常快并且具有最小的风险。

谢谢 巴努·亚达夫(Bhanu Yadav)

答案 1 :(得分:1)

由于WITH分解子句中没有 dynamic (即您不使用变量-至少,我没有发现变量),因此建议您创建视图(基于该WITH),并在需要时使用它。

如果实际查询确实很复杂并且需要花费一些时间来执行,则可以创建一个全局临时表(GTT),最有可能选择在会话(ON COMMIT PRESERVE ROWS)中正确保存其数据索引并将其存储在其中的视图(或WITH)内容。然后,您将在代码中使用GTT

尽管,Oracle会将查询返回的日期保留在内存中,所以您甚至可能必须真正“执行”一次,但是内存不是无限的,因此...对其进行测试,比较获得的结果,然后选择一个那似乎是最好的。

对我来说,GTT的想法听起来很有希望,但是如果没有实际信息,就很难决定。

[编辑,关于GTT]

从您的观点来看,Oracle的“全局临时表”实际上是“本地”(请注意,如果您使用的是18c(虽然我认为您不在),则可以创建 private 临时表)。使用create global temporary table ...创建一次。您插入其中的数据仅对您可见,其他人则看不见。它仅限于您自己的交易(如果使用ON COMMIT DELETE ROWS创建)或会话(ON COMMIT PRESERVE ROWS)。选择最适合您的那个。

是什么意思?这意味着您只需创建一次GTT,即可提供列列表及其数据类型。每个使用您的过程的用户都将在其中插入自己的数据集(如您所说,您将使用带有LAST NAME参数的查询)并在整个事务(或会话)中使用它。许多用户可以同时执行此操作,但是-正如我已经说过-每个人只会看到自己的数据。

这是伪代码:

-- create table once. Do NOT create it, drop it, create again tomorrow, drop ... 
-- Create it once, use it many times.
create global temporary table gtt_my_data
  (id        number,
   c_name    varchar2(20), ...
  )
on commit preserve rows;

create index i1_gmd_id on gtt_my_data (id);

-- your procedure
procedure p_myproc (par_last_name in varchar2) is
begin
  insert into gtt_my_data (id, c_name, ...)
    select id, c_name, ...
    from some_table join some_other_table ...
    where some_table.last_name = par_last_name;

  -- now, do whatever you do. When you need to fetch data from the GTT, do so
  select ... into ...
    from table_x join gtt_my_data on ...

  update ... set some_column = (select another_column
                                from table_y join gtt_my_data on ...
                               )

end;

完成后:如果您结束会话,则会从GTT中删除数据。如果需要,可以手动进行操作(删除或截断其内容)。

[编辑#2:插入GTT]

插入错误;您没有插入values,而是类似以下内容:

INSERT INTO my_global_temp_table
  WITH test_joined_1 AS 
    (SELECT tps.personid,
            tps.lastname,
            tsd.address
       FROM test_persons tps
       LEFT JOIN test_address_a tsd ON tps.personid = tsd.personid
       WHERE tps.lastname = last_name
    )
  SELECT tj1.personid,
         tj1.lastname,
         tj1.address,
         ts.bank
  FROM test_joined_1 tj1
  LEFT JOIN test_salary_a ts ON tj1.personid = ts.personid
  WHERE tj1.lastname = last_name;

简化,基于Scott的模式:

SQL> create table test (empno number, deptno number);

Table created.

SQL> insert into test (empno, deptno)
  2  with temp as
  3    (select empno, deptno from emp)
  4  select t.empno, t.deptno
  5  from temp t join dept d on d.deptno = t.deptno
  6  where d.deptno = 10;

3 rows created.

SQL>