开发者

Trying to Understand PLSQL Function

开发者 https://www.devze.com 2023-02-01 11:04 出处:网络
I am new to PLSQL and I have this huge plsql function which am trying to understand and am having hard time understanding the flow and so I would really appreciate if anyone can run me through the big

I am new to PLSQL and I have this huge plsql function which am trying to understand and am having hard time understanding the flow and so I would really appreciate if anyone can run me through the big pieces so that I can understand the flow. Guidance would be highly appreciated.

      FUNCTION analysis(            
        REGION_ID_P                 VARCHAR2,
        COUNTRY_ID_P            VARCHAR2 ,
        SUB_REGION_ID_P         VARCHAR2 ,
        CUSTOMER_TYPE_ID_P          VARCHAR2 ,
        RECEIVED_FROM_DATE_P        VARCHAR2 ,
        RECEIVED_TO_DATE_P          VARCHAR2,
        CUSTOMER_ID_P           VARCHAR2 ,
        PRIORITY_ID_P               VARCHAR2,
        WORK_GROUP_ID_P       VARCHAR2,
        CITY_ID_P VARCHAR2,
        USER_ID_P               VARCHAR2            
  )  RETURN ANALYSIS_REPORT_TAB_TYPE pipelined
  IS
          with_sql LONG;
          e_sql LONG;
          where_sql LONG;
          group_by_sql LONG;
          curent_date Date;
      v_row ANALYSIS_REPORT_ROW_TYPE := ANALYSIS_REPORT_ROW_TYPE(
                  NULL,
                  NULL,
                  NULL,
                  NULL,
                  NULL,
                  NULL,
                  NULL,
                  NULL
      );
       TYPE rectyp IS REF CURSOR;                                                                                                                                                                                                   -- define weak REF CURSOR type
       rrc_rectyp                    rectyp;

       TYPE recordvar IS RECORD(
    MONTHS        VARCHAR2(100),
  ORDERBY_MONTHS VARCHAR2(100),
    REQ_RECEIVED  NUMBER(9,2),
    REQ_STILL_OPEN NUMBER(9,2),
    REQ_AWAIT_ACCEPTANCE NUMBER(9,2),
    REQ_WITH_ATT NUMBER(9,2),
    REQ_CLOSED NUMBER(9,2),
    REQ_CANCELLED NUMBER(9,2)
       );
       res_rec                       recordvar;
  BEGIN

    select sysdate +substr(to_char(systimestamp, 'tzr'),3,1)/24 into curent_date from dual;
                where_sql := ' AND 1=1 ';
        IF COUNTRY_ID_P IS NOT NULL THEN
                  where_sql := where_sql ||' AND x.country_id ='|| COUNTRY_ID_P;
                END IF;
                IF SUB_REGION_ID_P IS NOT NULL THEN
                  where_sql := where_sql ||' AND x.SUB_REGION_ID ='|| SUB_REGION_ID_P;
                END IF;  
                IF CUSTOMER_TYPE_ID_P IS NOT NULL THEN
                  where_sql := where_sql ||' AND x.CUSTOMER_TYPE_ID ='|| CUSTOMER_TYPE_ID_P;
                END IF;
                IF RECEIVED_FROM_DATE_P IS NOT NULL THEN
                        where_sql := where_sql||' AND convert_time(received_date, ''GMT'', ''GMT'') >= convert_time(trunc(to_date('''||RECE开发者_如何转开发IVED_FROM_DATE_P||''',''dd/mm/yyyy HH24:MI:SS'')), ''Europe/Paris'', ''GMT'')';
                END IF;
                IF RECEIVED_TO_DATE_P IS NOT NULL  THEN
                        where_sql := where_sql||' AND convert_time(received_date, ''GMT'', ''GMT'') <= convert_time(trunc(to_date('''||RECEIVED_TO_DATE_P||''',''dd/mm/yyyy HH24:MI:SS'')), ''Europe/Paris'', ''GMT'')';
                END IF;
                IF CUSTOMER_ID_P IS NOT NULL THEN
                  where_sql := where_sql||' AND x.CUSTOMER_ID in(select CUSTOMER_ID from lk_customer where upper(CUSTOMER_NAME) like upper('''||CUSTOMER_ID_P||'%''))';
                END IF;  
                IF PRIORITY_ID_P IS NOT NULL THEN
                   where_sql := where_sql ||' AND x.PRIORITY_ID ='|| PRIORITY_ID_P;
                END IF;    
                IF WORK_GROUP_ID_P IS NOT NULL THEN
                   where_sql := where_sql ||' AND x.WORKGROUP_ID ='|| WORK_GROUP_ID_P;
                END IF;                     
                IF CITY_ID_P IS NOT NULL THEN
                  where_sql := where_sql ||' AND x.CITY_ID = ' || CITY_ID_P;
                END IF;     
    group_by_sql := ' group by to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''mm/YYYY''),to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''yyyy/mm'')';                    

    with_sql := 'with
               b AS (select cep_work_item_no from ap_main where req_accept_date is null and  ecep_ap_utils.f_business_days(received_date,'''||curent_date||''')>30),
           e AS (select cep_work_item_no from ap_main where status_id=1 and req_accept_date is not null and stage_ID != 10 and stage_Id !=4 and ecep_ap_utils.f_business_days(received_date,'''||curent_date||''')>30),
           --f AS  (select cep_work_item_no from ap_main  where received_date is not null),
           m AS (select cep_work_item_no from ap_main  where received_date is not null and status_id=1), 
         n AS (select cep_work_item_no from ap_main  where  status_id=2),
         o AS (select cep_work_item_no from ap_main  where  status_id=3)';

--e_sql := ' SELECT MONTHS, REQ_RECEIVED,REQ_STILL_OPEN, REQ_AWAIT_ACCEPTANCE, REQ_WITH_ATT from (';
--e_sql := with_sql;
    e_sql := with_sql||' select   to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''mm/YYYY'') MONTHS, to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''yyyy/mm'') ORDERBY_MONTHS,
        count(x.cep_work_item_no)  REQ_RECEIVED,
        count(m.cep_work_item_no) REQ_STILL_OPEN,count(b.cep_work_item_no) REQ_AWAIT_ACCEPTANCE,count(e.cep_work_item_no) REQ_WITH_ATT,
  count(n.cep_work_item_no) REQ_CLOSED, count(o.cep_work_item_no) REQ_CANCELLED
        from ap_main x,m,b,e,n,o where  x.cep_work_item_no=m.cep_work_item_no(+)
        and x.cep_work_item_no = b.cep_work_item_no(+) and x.cep_work_item_no=e.cep_work_item_no(+) and 
  x.cep_work_item_no=n.cep_work_item_no(+) and x.cep_work_item_no=o.cep_work_item_no(+)
        and x.received_date is not null';
e_sql := e_sql|| where_sql||group_by_sql;

           OPEN rrc_rectyp FOR e_sql;
              LOOP
                 FETCH rrc_rectyp INTO  res_rec;
                 EXIT WHEN rrc_rectyp%NOTFOUND;
                      v_row.MONTHS      := res_rec.MONTHS ;
                      v_row.ORDERBY_MONTHS      := res_rec.ORDERBY_MONTHS ;
                      v_row.REQ_RECEIVED        := res_rec.REQ_RECEIVED;
                      v_row.REQ_STILL_OPEN      := res_rec.REQ_STILL_OPEN;
                      v_row.REQ_AWAIT_ACCEPTANCE        := res_rec.REQ_AWAIT_ACCEPTANCE;
                      v_row.REQ_WITH_ATT        := res_rec.REQ_WITH_ATT;
                      v_row.REQ_CLOSED      := res_rec.REQ_CLOSED;
                      v_row.REQ_CANCELLED       := res_rec.REQ_CANCELLED;
                  pipe ROW(v_row);

              END LOOP;
              RETURN;                    
  END analysis;

And would also appreciate if someone can let me know as to what are the important plsql concepts used here so that I can go ahead and understand them in a better way and some small explanation would go long way.

Question:

Is above approach generic way of writing reporting function in your experience or there are some best practices in doing so ?


It looks like a reporting function. It builds an SQL statement with some conditions in it (some elements in the WHERE depend on parameters).

The query itself looks quite complex. It uses the with construct that lets you define sort of an inline view inside the query. That in itself is more an SQL (maybe Oracle SQL) feature and not PLSQL.

Then, the query (which is built up in a string variable) is opened in a cursor. A cursor can be seen as a tool to traverse the result of a query which is done here in a loop.

Then, variables from the cursor are put in properties of v_row. v_row is declared as a record type. It is an object that can respresent a record. The object is piped to the output, meaning that this functions actually returns a recordset, meaning you can call it in a query, like this:

select * from table(monthly_analysis(<parameters>))

[edit]

Addition on request: An example of how you can execute the query within plsql, fetch the results and return them, without building the query as a string. Function is typed from the heart, based on the original. I cannot test it ofcourse, because I don't have the correct database. Actually I don't have a database or editor at all at the moment, so please read between the typo's. ;)

create function Analysis2(
  REGION_ID_P            VARCHAR2,
  COUNTRY_ID_P           VARCHAR2,
  SUB_REGION_ID_P        VARCHAR2,
  CUSTOMER_TYPE_ID_P     VARCHAR2,
  RECEIVED_FROM_DATE_P   VARCHAR2,
  RECEIVED_TO_DATE_P     VARCHAR2,
  CUSTOMER_ID_P          VARCHAR2,
  PRIORITY_ID_P          VARCHAR2,
  WORK_GROUP_ID_P        VARCHAR2,
  CITY_ID_P              VARCHAR2,
  USER_ID_P              VARCHAR2)
return
  ANALYSIS_REPORT_TAB_TYPE
is
  V_RESULTSET ANALYSIS_REPORT_TAB_TYPE;
begin
  -- I hope the 'with' construct is supported within PLSQL. I don't have it here on my home laptop so I can't test it.
  with
    b AS (select cep_work_item_no from ap_main where req_accept_date is null and  ecep_ap_utils.f_business_days(received_date,''''||curent_date||'''')>30),
    e AS (select cep_work_item_no from ap_main where status_id=1 and req_accept_date is not null and stage_ID != 10 and stage_Id !=4 and
    ecep_ap_utils.f_business_days(received_date,''''||curent_date||'''')>30),
    --f AS  (select cep_work_item_no from ap_main  where received_date is not null),
    m AS (select cep_work_item_no from ap_main  where received_date is not null and status_id=1), 
    n AS (select cep_work_item_no from ap_main  where  status_id=2),
    o AS (select cep_work_item_no from ap_main  where  status_id=3)
  select
    -- You can actually use the record type constructor here to return 
    -- a specific record type instead of a bunch of loose fields
    ANALYSIS_REPORT_REC_TYPE(
      to_char(convert_time(received_date, 'GMT', 'Europe/Paris'),'mm/YYYY') MONTHS, 
      to_char(convert_time(received_date, 'GMT', 'Europe/Paris'),'yyyy/mm') ORDERBY_MONTHS,
      count(x.cep_work_item_no) REQ_RECEIVED,
      count(m.cep_work_item_no) REQ_STILL_OPEN,
      count(b.cep_work_item_no) REQ_AWAIT_ACCEPTANCE,
      count(e.cep_work_item_no) REQ_WITH_ATT,
      count(n.cep_work_item_no) REQ_CLOSED, 
      count(o.cep_work_item_no) REQ_CANCELLED)
  bulk collect into
    V_RESULTSET
  from 
    ap_main x,m,b,e,n,o 
  where 
    x.cep_work_item_no=m.cep_work_item_no(+)
    and x.cep_work_item_no = b.cep_work_item_no(+) and x.cep_work_item_no=e.cep_work_item_no(+) and 
    x.cep_work_item_no=n.cep_work_item_no(+) and x.cep_work_item_no=o.cep_work_item_no(+)
    and x.received_date is not null
    /* Additional where, based on input goes below. I did two, but you get the point */
    AND (COUNTRY_ID_P is null or x.country_id = COUNTRY_ID_P)
    AND (SUB_REGION_ID_P is null or x.SUB_REGION_ID = SUB_REGION_ID_P)
    -- and etc
  group by 
    to_char(convert_time(received_date, 'GMT', 'Europe/Paris'),'mm/YYYY'),
    to_char(convert_time(received_date, 'GMT', 'Europe/Paris'),'yyyy/mm');

  -- The entire resultset of the query is now stored in V_RESULTSET
  -- It can actually be looped using a loop like this:
  -- for i in V_RESULTSET.first..V_RESULTSET.last loop
  --   DBMS_OUTPUT.PUT_LINE(V_RESULTSET(i).Whateverfield);
  -- end loop;

  -- But its not needed. The actual query is all this function does, so return its result

  return V_RESULTSET;
end;


Are you sure you posted everything? Because as it is now, it will never run successfully. Lot of variables are declared but never used. e_sql for example is being executed but is never assigned a value.

I hope for you that you will not try to learn PL/SQL by looking at this code, because just about every line of code makes me cringe. Especially declaring variables as LONG (which you should never use anymore), the use of that record, and that clumsy date handling. Ouch, ouch, ouch! And above all, if someone writes code like that, that someone should definitely need to learn to comment what he's doing.


Update

I rewrote the function, now that it's complete. I tested it with these auxiliary objects:

SQL> create table ap_main
  2  ( cep_work_item_no number
  3  , received_date    date
  4  , req_accept_date  date
  5  , status_id        number
  6  , stage_id         number
  7  , country_id       number
  8  , sub_region_id    number
  9  , customer_type_id number
 10  , customer_id      number
 11  , priority_id      number
 12  , workgroup_id     number
 13  , city_id          number
 14  )
 15  /

Table created.

SQL> insert into ap_main
  2  select 1,sysdate,sysdate,1,4,1,1,1,1,1,1,1 from dual union all
  3  select 2,sysdate,sysdate,1,4,1,1,1,1,1,1,1 from dual union all
  4  select 3,sysdate,sysdate,1,5,1,1,1,1,1,1,1 from dual union all
  5  select 4,sysdate,sysdate,1,5,1,1,1,1,1,1,1 from dual union all
  6  select 5,sysdate,sysdate,2,5,1,1,1,1,1,1,1 from dual union all
  7  select 6,sysdate-31,sysdate-31,1,5,1,1,1,1,1,1,1 from dual union all
  8  select 7,sysdate-31,sysdate-31,1,5,1,1,1,1,1,1,1 from dual union all
  9  select 8,sysdate-31,sysdate-31,3,5,1,1,1,1,1,1,1 from dual
 10  /

8 rows created.

SQL> create table lk_customer (customer_id,customer_name)
  2  as
  3  select 1, 'Anna' from dual union all
  4  select 2, 'Bob' from dual
  5  /

Table created.

SQL> create type analysis_report_row_type as object
  2  ( months               varchar2(7)
  3  , orderby_months       varchar2(7)
  4  , req_received         number
  5  , req_still_open       number
  6  , req_await_acceptance number
  7  , req_with_att         number
  8  , req_closed           number
  9  , req_cancelled        number
 10  )
 11  /

Type created.

SQL> create type analysis_report_tab_type as table of analysis_report_row_type
  2  /

Type created.

SQL> create function convert_time
  2  ( p1 in date
  3  , p2 in varchar2
  4  , p3 in varchar2
  5  ) return date
  6  is
  7  begin
  8    return p1;
  9  end;
 10  /

Function created.

SQL> create package ecep_ap_utils
  2  as
  3    function f_business_days(p1 in date,p2 in date) return number;
  4  end ecep_ap_utils;
  5  /

Package created.

SQL> create package body ecep_ap_utils
  2  as
  3    function f_business_days(p1 in date,p2 in date) return number
  4    is
  5    begin
  6          return p2 - p1;
  7    end f_business_days;
  8  end ecep_ap_utils;
  9  /

Package body created.

Two parameters of your function are not used, so I removed those. All parameters have the wrong type it seems, so I fixed that as well. Furthermore, I removed all the unnecessary variables and made your query use bind variables. This is important, because Oracle stores each unique parsed statement in the shared pool for reuse. But by glueing in your parameters, you made every statement unique, causing a hard parse and filling up your shared pool.

Your function is a pipelined function, which seems like overkill in your situation, since your resultset won't be very large, because you are grouping by the month. So you'll only get one row per month. I left that in place. The query accessed your ap_main table six times, where one time is sufficient. You'll probably notice that by a performance gain. What still worries me is the date handling. The original coder couldn't makes his mind up whether he'd like to use strings or dates to handle dates. Of course you should be using dates for handling dates. A lot of the conversion routines that are called can probably be skipped somehow. Anyways ... here is the new function:

SQL> create function analysis
  2  ( country_id_p         in number
  3  , sub_region_id_p      in number
  4  , customer_type_id_p   in number
  5  , received_from_date_p in date
  6  , received_to_date_p   in date
  7  , customer_id_p        in number
  8  , priority_id_p        in number
  9  , work_group_id_p      in number
 10  , city_id_p            in number
 11  ) return analysis_report_tab_type pipelined
 12  is
 13    l_current_date        date;
 14    l_refcursor           sys_refcursor;
 15    l_analysis_report_row analysis_report_row_type := analysis_report_row_type(null,null,null,null,null,null,null,null);
 16  begin
 17    select sysdate + to_number(to_char(systimestamp, 'tzh')) / 24
 18      into l_current_date
 19      from dual
 20    ;
 21    open l_refcursor for
 22      'select analysis_report_row_type
 23              ( to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''mm/yyyy'')
 24              , to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''yyyy/mm'')
 25              , count(cep_work_item_no)
 26              , count(case when received_date is not null and status_id=1 then 1 end)
 27              , count(case when req_accept_date is null and ecep_ap_utils.f_business_days(received_date,:p_current_date)>30 then 1 end)
 28              , count(case when req_accept_date is not null and status_id = 1 and stage_ID not in (4,10) and ecep_ap_utils.f_business_days(received_date,:p_current_date)>30 then 1 end)
 29              , count(case when status_id = 2 then 1 end)
 30              , count(case when status_id = 3 then 1 end)
 31              )
 32         from ap_main
 33        where received_date is not null ' ||
 34      case
 35      when country_id_p is null then
 36        ' and (1=1 or :p_country_id is null)'
 37      else
 38        ' and country_id = :p_country_id'
 39      end ||
 40      case
 41      when sub_region_id_p is null then
 42        ' and (1=1 or :p_sub_region_id is null)'
 43      else
 44        ' and sub_region_id = :p_sub_region_id'
 45      end ||
 46      case
 47      when customer_type_id_p is null then
 48        ' and (1=1 or :p_customer_type_id is null)'
 49      else
 50        ' and customer_type_id = :p_customer_type_id'
 51      end ||
 52      case
 53      when received_from_date_p is null then
 54        ' and (1=1 or :p_received_from_date is null)'
 55      else
 56        ' and convert_time(received_date, ''GMT'', ''GMT'') >= convert_time(trunc(:p_received_from_date), ''Europe/Paris'', ''GMT'')'
 57      end ||
 58      case
 59      when received_to_date_p is null then
 60        ' and (1=1 or :p_received_to_date is null)'
 61      else
 62        ' and convert_time(received_date, ''GMT'', ''GMT'') <= convert_time(trunc(:p_received_to_date), ''Europe/Paris'', ''GMT'')'
 63      end ||
 64      case
 65      when customer_id_p is null then
 66        ' and (1=1 or :p_customer_id is null)'
 67      else
 68        ' and customer_id in (select customer_id from lk_customer where upper(customer_name) like upper(:p_customer_id || ''%''))'
 69      end ||
 70      case
 71      when priority_id_p is null then
 72        ' and (1=1 or :p_priority_id is null)'
 73      else
 74        ' and priority_id = :p_priority_id'
 75      end ||
 76      case
 77      when work_group_id_p is null then
 78        ' and (1=1 or :p_workgroup_id is null)'
 79      else
 80        ' and workgroup_id = :p_workgroup_id'
 81      end ||
 82      case
 83      when city_id_p is null then
 84        ' and (1=1 or :p_city_id is null)'
 85      else
 86        ' and city_id = :p_city_id'
 87      end ||
 88      ' group by to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''mm/yyyy'')
 89            , to_char(convert_time(received_date, ''GMT'', ''Europe/Paris''),''yyyy/mm'')'
 90    using l_current_date
 91    ,     l_current_date
 92    ,     country_id_p
 93    ,     sub_region_id_p
 94    ,     customer_type_id_p
 95    ,     received_from_date_p
 96    ,     received_to_date_p
 97    ,     customer_id_p
 98    ,     priority_id_p
 99    ,     work_group_id_p
100    ,     city_id_p
101    ;
102    loop
103      fetch l_refcursor into l_analysis_report_row;
104      exit when l_refcursor%notfound;
105      pipe row (l_analysis_report_row);
106    end loop;
107    return;
108  end analysis;
109  /

Function created.

And to prove that the new functions works:

SQL> select * from table(analysis(1,1,1,null,null,1,1,1,1))
  2  /

no rows selected

SQL> select * from table(analysis(null,null,null,null,null,null,null,null,null))
  2  /

MONTHS  ORDERBY REQ_RECEIVED REQ_STILL_OPEN REQ_AWAIT_ACCEPTANCE REQ_WITH_ATT REQ_CLOSED REQ_CANCELLED
------- ------- ------------ -------------- -------------------- ------------ ---------- -------------
12/2010 2010/12            5              4                    0            0          1             0
11/2010 2010/11            3              2                    0            2          0             1

2 rows selected.

Update 2

Here are two links to the two crucial constructs used here:

OPEN FOR statement: http://download.oracle.com/docs/cd/E11882_01/appdev.112/e17126/openfor_statement.htm#sthref1703

Pipelined functions: http://download.oracle.com/docs/cd/E11882_01/appdev.112/e17126/tuning.htm#sthref1129

As you can see in the OPEN FOR statement documentation, after FOR you specify a query, which I construct dynamically. The same was being done in your original code. Differences are that I'm using native dynamic SQL, so I can use bind variables (the variables starting with ":p_"). I did it in such a way that no matter what input values I provide, all the bind variables are present in the query. Here is a good explanation of why and how: http://www.oracle.com/technetwork/issue-archive/2009/09-jul/o49asktom-090487.html

If you have some more questions, don't hesitate to ask.

Regards, Rob.


A good way to figure stuff like this out is to step through the code in the debugger. Oracle offers a free tool called SQL Developer, which comes with a dubugger, so you could use that.

At initial glance, this code looks like it's building a dynamic SQL statement to fetch some data. By dynamic, I mean that the SQL statement is built at runtime, and the procedure is constructing the where clause based on passed in parameters.

At the end, they're doing:

OPEN rrc_rectyp FOR e_sql

Which basically puts the result from the query in a ref cursor, and this allows the client to get the result data.

By the way, using dynamic SQL in this manner is very bad for performance since it results in a hard parse. You can read more about hard parses and why they are evil at this link. The solution is to use a context, so you end up with the advantages of bind variables and avoid the hard parse (this is discussed at that link).

EDIT Actually, they are pipelining the result data into a collection variable. See this link, and search for "Assigning the Result of a Table Function".

0

精彩评论

暂无评论...
验证码 换一张
取 消