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('''||RECEIVED_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".