Procedures in PLSQL using ORACLE DB - oracle

CREATE OR REPLACE PROCEDURE ABC ( REG_NO IN CO_ENROLMENT.S_REGNO%TYPE,
TERM IN COURSEOFFERING.CO_TERMNUMBER%TYPE,
YEAR IN COURSEOFFERING.CO_YEAR%TYPE,
CO_TITLE IN COURSE.C_TITLE%TYPE,
EN_DATE OUT CO_ENROLMENT.COE_ENROLDATE%TYPE,
COM_ST OUT CO_ENROLMENT.COE_COMPLETIONSTATUS%TYPE)
AS
BEGIN
SELECT M.COE_COMPLETIONSTATUS, M.COE_ENROLDATE
INTO COM_ST, EN_DATE
FROM COURSEOFFERING O
INNER JOIN COURSE C
ON C.C_ID = O.C_ID
INNER JOIN CO_ENROLMENT M
ON M.CO_ID = O.CO_ID
WHERE M.S_REGNO LIKE REG_NO AND
O.CO_TERMNUMBER LIKE TERM AND
O.CO_YEAR LIKE YEAR AND
C.C_TITLE LIKE CO_TITLE;
END ABC;
I have written down above procedure.
Below is PL/SQL block to call above procedure,
DECLARE
COMPL_STATUS CO_ENROLMENT.COE_COMPLETIONSTATUS%TYPE;
ENROL_DATE CO_ENROLMENT.COE_ENROLDATE%TYPE;
BEGIN
ABC (44444444, 2009, 2, 'PLSQL Programming',
EN_DATE => enrol_date, COM_ST =>compl_status);
DBMS_OUTPUT.PUT_LINE ('STUDENT COMPLETION STATUS AND ENROLMENT DATE IS '
|| ENROL_DATE
|| ' '
|| compl_status );
END;
It returns me error as no data found, but when I run query separately I get the output. I couldn't figure out what is wrong. Did I wrote procedure block correctly and Parameters passed in PL/SQL block are correct?

In proc you have year on third place buth you call it on second place. And how are types
(COURSEOFFERING.CO_TERMNUMBER%TYPE, COURSEOFFERING.CO_TERMNUMBER%TYPE) declared?

When select statement in your ABC procedure returns no rows the NO_DATA_FOUND exception will be raised and execution of your stored-procedure will be halted. To avoid such behavior you need to add EXCEPTION section in the stored procedure to catch the exception and react appropriately. To that end the execution section of your stored procedure might look:
BEGIN
SELECT M.COE_COMPLETIONSTATUS, M.COE_ENROLDATE
INTO COM_ST, EN_DATE
FROM COURSEOFFERING O
INNER JOIN COURSE C
ON C.C_ID = O.C_ID
INNER JOIN CO_ENROLMENT M
ON M.CO_ID = O.CO_ID
WHERE M.S_REGNO LIKE REG_NO AND
O.CO_TERMNUMBER LIKE TERM AND
O.CO_YEAR LIKE YEAR AND
C.C_TITLE LIKE CO_TITLE;
EXCEPTION
WHEN NO_DATA_FOUND
THEN DBMS_OUTPUT.PUT_LINE('No data found') -- for example
END ABC
Or if you want to proceed even if select statement raises exception you might enclose your select statement with nested BEGIN .. END block.
BEGIN
-- some code before
BEGIN
SELECT M.COE_COMPLETIONSTATUS, M.COE_ENROLDATE
INTO COM_ST, EN_DATE
FROM COURSEOFFERING O
INNER JOIN COURSE C
ON C.C_ID = O.C_ID
INNER JOIN CO_ENROLMENT M
ON M.CO_ID = O.CO_ID
WHERE M.S_REGNO LIKE REG_NO AND
O.CO_TERMNUMBER LIKE TERM AND
O.CO_YEAR LIKE YEAR AND
C.C_TITLE LIKE CO_TITLE;
EXCEPTION
WHEN NO_DATA_FOUND
THEN DBMS_OUTPUT.PUT_LINE('No data found') -- for example
END;
-- some code after
END ABC

You probably lack wildcards in your procedure query.
16:02:06 SYSTEM#dwh-prod> select * from dual where 'abc' like 'ab';
no rows selected
16:02:18 SYSTEM#dwh-prod> select * from dual where 'abc' like 'ab%';
D
-
X
Without them, C.C_TITLE LIKE CO_TITLE is equal to C.C_TITLE = CO_TITLE, which is not neccessarily what you want.
Try C.C_TITLE LIKE '%'||CO_TITLE||'%' etc.

You're calling your stored procedure incorrectly. You have to specify parameters in the correct order, or use named parameter notation, ie
ABC (44444444, 2, 2009, 'PLSQL Programming', enrol_date, compl_status);
or
ABC (REG_NO => 44444444, YEAR => 2009, TERM => 2,
CO_TITLE => 'PLSQL Programming',
EN_DATE => enrol_date, COM_ST =>compl_status);
You can see that if you use named parameter notation you can specify parameters in a different order (or not at all if the parameters have DEFAULT values).

Related

Executing the PL/SQL block gives an error which is not understandable

The question given was to write a PL/SQL block to print the details of the customers whose total order quantity is greater than 200 where the Customer table had ID number(5) primary key, Name varchar2(20), Contact_No varchar2(10) and the Order table had Order_Id number(5) primary Key, Quantity number(4) not null, C_id number(5) references Customer(ID).
If no record is found "No Records Found" is to be printed out.
This is the Code I wrote:
SET SERVEROUTPUT ON;
begin
dbms_output.put_line('Customer_Id ' || 'Customer_Name '|| 'Customer_Phone');
for cur_d in (select o.C_ID,total AS sum(o.QUANTITY) from Orders o group by o.C_ID) loop
from Customers c
where c.ID = cur_d.C_ID and cur_d.total > 200;
dbms_output.put_line(c.ID || c.Name || c.Contact_No);
end loop;
end;
/
The error I faced was -
for cur_d in (select o.C_ID,total AS sum(o.QUANTITY) from Orders o group by o.C_ID) loop
ERROR at line 2:
ORA-06550: line 2, column 41:
PL/SQL: ORA-00923: FROM keyword not found where expected
ORA-06550: line 2, column 15:
PL/SQL: SQL Statement ignored
ORA-06550: line 3, column 1:
PLS-00103: Encountered the symbol "FROM" when expecting one of the following:
( begin case declare exit for goto if loop mod null pragma
raise return select update while with '<'an identifier'>'
'<'a double-quoted delimited-identifier'> ')
Even after you correct the substantial that have been pointed out your procedure will still fail. You indicate that if no record is found printing a message to that effect. That in itself is ambiguous. Does that mean for the no records in the data table or no records for a given customer? Either way you have no code to produce such a message. How do you expect it to be written. Finally, SQL is set based processing so you need to start thinking in terms of sets instead of loops. The following reduces the db access to a single query; the loop is only to print results.
begin
dbms_output.put_line('Customer_Id ' || 'Customer_Name '|| 'Customer_Phone' || 'Total Orders');
for cur_d in (
with order_totals as
( select c_id, sum (quantity) order_total
from orders
group by c_id
having sum(quantity) > 200
)
select c.id, c.name, c.contact_no
, case when o.c_id is null
then 'No Records Found'
else to_char(o.order_total)
end order_total
from customers c
left join order_totals o
on c.id = o.c_id
order by c.id
)
loop
dbms_output.put_line(cur_d.ID || cur_d.Name || cur_d.Contact_No || cur_d.order_total);
end loop;
end;
The results are jammed together just as you initially had them. You need to workout their presentation.
There is a "select into" part missing between "for .. loop" and "from" parts. It has to be like this in order to work
for ..... loop
select some_column -- <-- this line is missing
into some_variable -- <-- this line is missing too
from ..........
This is the kind of issue where formatting your code will make the problem obvious. If you always start a new line and indent after for xxx in ( and also place the closing bracket on its own line, and include some gaps between commands, you'll get this, which is clearly wrong:
begin
dbms_output.put_line('Customer_Id ' || 'Customer_Name '|| 'Customer_Phone');
for cur_d in (
select o.c_id, total as sum(o.quantity)
from orders o
group by o.c_id
)
loop
from customers c
where c.id = cur_d.c_id
and cur_d.total > 200;
dbms_output.put_line(c.id || c.name || c.contact_no);
end loop;
end;
The first statement inside the loop seems to be missing something, as ekochergin mentioned.
total as sum(o.quantity) is backwards as Turo mentioned.
If you want id, name and contact_no to be printed in columns, you should look at lpad and rpad for formatting them. Just concatenating them together will produce something unreadable.
The dbms_output inside the loop refers to c.id, c.name and c.contact_no, but the record is called cur_d, not c.
Also cur_d is a slightly confusing name for a record (it's not a cursor). I always use r for cursor records unless there is some other r involved that it could be confused with.

Why is the output only the last value? Oracle loop cursor

I'm trying to output a list of the courses a professor teaches, by receiving the prof's id by parameter to my function, and showing all courses, each separated by a comma. For example, if a Professor teaches Humanities, Science and Math, I want the output to be: 'Humanities, Science, Math'. However, I'm getting just 'Math,'. It only shows the last field that it found that matched with the prof's id.
CREATE OR REPLACE FUNCTION listar_cursos(prof NUMBER) RETURN VARCHAR
IS
CURSOR C1 IS
SELECT subject.name AS name FROM subject
INNER JOIN course_semester
ON subject.id = course_semester.id_subject
WHERE course_semester.id_profesor = prof
ORDER BY subject.name;
test VARCHAR(500);
BEGIN
FOR item IN C1
LOOP
test:= item.name ||',';
END LOOP;
RETURN test;
END;
/
I am aware that listagg exists, however I do not wish to use it.
In your loop, you re-assign to the test variable, instead of appending to it. This is why, at the end of the loop, it will just hold the last value of item.name.
The assignment should instead be something like
test := test || ',' || item.name
Note also that this will leave a comma at the beginning of the string. Instead of returning test, you may want to return ltrim(test, ',').
Note that you don't need to declare a cursor explicitly. The code is easier to read (in my opinion) with an implicit cursor, as shown below. I create sample tables and data to test the function, then I show the function code and how it's used.
create table subject as
select 1 id, 'Humanities' name from dual union all
select 2 , 'Science' from dual union all
select 3 , 'Math' from dual
;
create table course_semester as
select 1 id_subject, 201801 semester, 1002 as id_profesor from dual union all
select 2 , 201702 , 1002 as id_profesor from dual union all
select 3 , 201801 , 1002 as id_profesor from dual
;
CREATE OR REPLACE FUNCTION listar_cursos(prof NUMBER) RETURN VARCHAR IS
test VARCHAR(500);
BEGIN
FOR item IN
(
SELECT subject.name AS name FROM subject
INNER JOIN course_semester
ON subject.id = course_semester.id_subject
WHERE course_semester.id_profesor = prof
ORDER BY subject.name
)
LOOP
test:= test || ',' || item.name;
END LOOP;
RETURN ltrim(test, ',');
END;
/
select listar_cursos(1002) from dual;
LISTAR_CURSOS(1002)
-----------------------
Humanities,Math,Science

Create simple PL/SQL variable - Use Variable in WHERE clause

Thanks for looking...
I've spent hours researching this and I can't believe it's that difficult to do something in PL/SQL that is simple in TSQL.
I have a simple query that joins 2 tables:
Select DISTINCT
to_char(TO_DATE('1899123000', 'yymmddhh24')+ seg.NOM_DATE, 'mm/dd/yyyy') AS "Record Date"
, cd.CODE
, EMP.ID
, EMP.SHORT_NAME
FROM
EWFM.GEN_SEG seg join EWFM.SEG_CODE cd ON seg.SEG_CODE_SK = cd.SEG_CODE_SK
join EMP on seg.EMP_SK = EMP.EMP_SK
where NOM_DATE = vMyDate;
I use Toad Date Point and I'm querying against an Oracle Exadata source. The resulting query will be dropped into a visualization tool like QlikView or Tableau. I'd like to create a simple variable to use the the WHERE clause as you can see in the code.
In this example, NOM_DATE is an integer such as 42793 (2/27/2017) as you can see in the first row "Record Date". Nothing new here, not very exciting... Until... I tried to create a variable to make the query more dynamic.
I've tried a surprising variety of examples found here, all have failed. Such as:
declare
myDate number(8);
Begin
myDate := 42793;
--Fail ORA-06550 INTO Clause is expected
variable nomDate NUMBER
DEFINE nomDate = 42793
EXEC : nomDate := ' & nomDate'
...where NOM_DATE = ( & nomDate) ;
--ORA-00900: invalid SQL statement
and
variable nomDate NUMBER;
EXEC nomDate := 42793;
select count(DET_SEG_SK) from DET_SEG
where NOM_DATE = :nomDate;
--ORA-00900: invalid SQL statement
and several more.. hopefully you get the idea. I've spent a few hours researching stackoverflow for a correct answer but as you can see, I'm asking you. From simple declarations like "Var" to more complex " DECLARE, BEGIN, SELECT INTO...." to actually creating Functions, using cursors to iterate the output.... I still can't make a simple variable to use in a Where clause.
Please explain the error of my ways.
--Forlorn SQL Dev
Since you are using an implicit cursor, you have to select then INTO variables. Now I d not know the data types of you variables, so I have just guessed in this example below, but hopefully you get the point.
Two other things I should mention
Why are you TO_CHARing you DATE. Just use a DATE datatype. Also, I think your format mask is wrong too 1899123000 does not match yymmddhh24.
In explicit cursor expects exactly one row; no rows and you get NO_DATA_FOUND; more than one and you get TOO_MANY_ROWS
Declare
myDate number(8) := 42793;
/* These 4 variable data types are a guess */
v_record_date varchar2(8);
v_cd_code varchar2(10);
v_emp_id number(4);
v_emp_short_name varchar2(100);
BEGIN
Select DISTINCT to_char(TO_DATE('1899123000', 'yymmddhh24')
+ eg.NOM_DATE, 'mm/dd/yyyy') AS "Record Date"
, cd.CODE
, EMP.ID
, EMP.SHORT_NAME
INTO v_record_date, v_cd_code, v_emp_id, v_emp_short_name
FROM EWFM.GEN_SEG seg
join EWFM.SEG_CODE cd
ON seg.SEG_CODE_SK = cd.SEG_CODE_SK
join EMP
on seg.EMP_SK = EMP.EMP_SK
where NOM_DATE = myDate;
END;
/
VARIABLE vMyDate NUMBER;
BEGIN
:vMyDate := 42793;
END;
/
-- or
-- EXEC :vMyDate := 42793;
SELECT DISTINCT
TO_CHAR( DATE '1899-12-30' + seg.NOM_DATE, 'mm/dd/yyyy') AS "Record Date"
, cd.CODE
, EMP.ID
, EMP.SHORT_NAME
FROM EWFM.GEN_SEG seg
join EWFM.SEG_CODE cd
ON seg.SEG_CODE_SK = cd.SEG_CODE_SK
join EMP
on seg.EMP_SK = EMP.EMP_SK
WHERE NOM_DATE = :vMyDate;
You put the variables with getter and setter in a package.
Then use a view that uses the package getter
Personally I prefer to use a collection that way I can do a select * from table (packagage.func(myparam))

Ref Cursor Exceptions

I have a couple of questions arounbd ref_cursors. Below is a ref_cursor that returns a a single row to a Unix calling script based on what is passed in and although the select looks a little untidy, it works as expected.
My first question is that in the select I join to a lookup table to retrieve a single lookup value 'trigram' and on testing found that this join will occasionally fail as no value exists. I have tried to capture this with no_data_found and when others exception but this does not appear to be working.
Ideally if the join fails I would still like to return the values to the ref_cursor but add something like 'No Trigram' into the trigram field - primarily I want to capture exception.
My second question is more general about ref_cursors - While initially I have created this in its own procedure, it is likely to get called by the main processing procedure a number of times, one of the conditions requires a seperate select but the procedure would only ever return one ref_cur when called, can the procdure ref_cur out be associated with 2 queries.
CREATE OR REPLACE PROCEDURE OPC_OP.SiteZone_status
(in_site_id IN AW_ACTIVE_ALARMS.site_id%TYPE
,in_zone_id IN AW_ACTIVE_ALARMS.zone_id%TYPE
,in_mod IN AW_ACTIVE_ALARMS.module%TYPE
,p_ResultSet OUT TYPES.cursorType
)
AS
BEGIN
OPEN p_ResultSet FOR
SELECT a.site_id,'~',a.zone_id,'~',b.trigram,'~',a.module,'~',a.message_txt,'~',a.time_stamp
FROM AW_ACTIVE_ALARMS a, AW_TRIGRAM_LOCATION b
WHERE a.site_id = b.site_id
AND a.zone_id = b.zone_id
AND a.site_id = in_site_id
AND a.zone_id = in_zone_id
AND a.module LIKE substr(in_mod,1,3)||'%'
AND weight = (select max(weight) from AW_ACTIVE_ALARMS c
WHERE c.site_id = in_site_id
AND c.zone_id = in_zone_id
AND c.module LIKE substr(in_mod,1,3)||'%');
EXCEPTION
WHEN OTHERS
THEN
DBMS_OUTPUT.PUT_LINE('No Data Found');
END SiteZone_status;
I have modified my code to adopt answers provided and this now works as expected as a standalone procedure within my package, which when called via a UNIX script using:
v_process_alarm=$(sqlplus -s user/pass <
set colsep ','
set linesize 500
set pages 0 feedback off;
set serveroutput on;
VARIABLE resultSet REFCURSOR
EXEC alarm_pkg.rtn_active_alarm($site,$zone,$module, :resultSet);
PRINT :resultSet
EOF
)
However the procedure returning the ref cursor is to be called from the main processing procedure as I only want to return values if certain criteria are met. I have add an out refcurosr to my main procedure and set a variable to match, I then call my ref cursor procedure from here but this fails to compile
with the message 'Wrong number or types of argument in call'
My question is what is the correct way to call a procedure that has out refcursor from within a procedure and then return these values from there back to the calling script.
Oracle doesn't know whether a query will return rows until you fetch from the cursor. And it is not an error for a query to return 0 rows. So you will never get a no_data_found exception from opening a cursor. You'll only get that if you do something like a select into a local variable in which case a query that returns either 0 or more than 1 row is an error.
It sounds like you want to do an outer join to the AW_TRIGRAM_LOCATION table rather than a current inner join. This will return data from the other tables even if there is no matching row in aw_trigram_location. That would look something like this (I have no idea why every other column is a hard-coded tilde character, that seems exceptionally odd)
SELECT a.site_id,'~',
a.zone_id,'~',
nvl(b.trigram, 'No Trigram Found'),'~',
a.module,'~',
a.message_txt,'~',
a.time_stamp
FROM AW_ACTIVE_ALARMS a
LEFT OUTER JOIN AW_TRIGRAM_LOCATION b
ON( a.site_id = b.site_id AND
a.zone_id = b.zone_id )
WHERE a.site_id = in_site_id
AND a.zone_id = in_zone_id
AND a.module LIKE substr(in_mod,1,3)||'%'
AND weight = (select max(weight)
from AW_ACTIVE_ALARMS c
WHERE c.site_id = in_site_id
AND c.zone_id = in_zone_id
AND c.module LIKE substr(in_mod,1,3)||'%');
I'm not quite sure that I understand your last question. You can certainly put logic in your procedure to run a different query depending on an input parameter. Something like
IF( <<some condition>> )
THEN
OPEN p_ResultSet FOR <<query 1>>
ELSE
OPEN p_ResultSet FOR <<query 2>>
END IF;
Whether it makes sense to do this rather than adding additional predicates or creating separate procedures is a question you'd have to answer.
You can use a left outer join to your look-up table, which is clearer if you use ANSI join syntax rather than Oracle's old syntax. If there is no record in AW_TRIGRAM_LOCATION then b.trigram will be null, and you can then use NVL to assign a dummy value:
OPEN p_ResultSet FOR
SELECT a.site_id,'~',a.zone_id,'~',NVL(b.trigram, 'No Trigram'),'~',
a.module,'~',a.message_txt,'~',a.time_stamp
FROM AW_ACTIVE_ALARMS a
LEFT JOIN AW_TRIGRAM_LOCATION b
ON b.site_id = a.site_id
AND b.zone_id = a.zone_id
WHERE a.zone_id = in_zone_id
AND a.module LIKE substr(in_mod,1,3)||'%'
AND weight = (select max(weight) from AW_ACTIVE_ALARMS c
WHERE c.site_id = in_site_id
AND c.zone_id = in_zone_id
AND c.module LIKE substr(in_mod,1,3)||'%');
You won't get NO_DATA_FOUND opening a cursor, only when you fetch from it (depending on what is actually consuming this). It's a bad idea to catch WHEN OTHERS anyway - you would want to catch WHEN NO_DATA_FOUND, though it wouldn't help here. And using dbms_output to report an error relies on the client enabling its display, which you can't generally assume.

Ordered iteration in an user-defined aggregate function?

I just implemented the ODCIAggregate Interface to create a custom aggregation function. It works quite well and fast, but I would like it to do a little something more. I have a statement going like this:
SELECT SomeId, myAggregationFunction(Item) FROM
(
SELECT
Foo.SomeId,
SomeType(Foo.SomeValue, Foo.SomeOtherValue) AS Item
FROM
Foo
ORDER BY Foo.SomeOrderingValue
)
GROUP BY SomeId;
My problem is that items aren't passed to the ODCIAggregateIterate function of my implementation in the same order that my inner (ordered) SELECT returns them.
I've Googled around and didn't find any Oracle-provided way to do so. Has any of you experimented a similar problem based on that requirement?
Thanks!
Have you considered using COLLECT instead of data cartridge?
At least for string aggregation, the COLLECT method is simpler and much faster. It does make your SQL a little weirder though.
Below is an example using just simple string concatenation.
--Create a type
create or replace type sometype as object
(
someValue varchar2(100),
someOtherValue varchar2(100)
);
--Create a nested table of the type.
--This is where the performance improvement comes from - Oracle can aggregate
--the types in SQL using COLLECT, and then can process all the values at once.
--This significantly reduces the context switches between SQL and PL/SQL, which
--are usually more expensive than the actual work.
create or replace type sometypes as table of sometype;
--Process all the data (it's already been sorted before it gets here)
create or replace function myAggregationFunction(p_sometypes in sometypes)
return varchar2 is
v_result varchar2(4000);
begin
--Loop through the nested table, just concatenate everything for testing.
--Assumes a dense nested table
for i in 1 .. p_sometypes.count loop
v_result := v_result || ',' ||
p_sometypes(i).someValue || '+' || p_sometypes(i).someOtherValue;
end loop;
--Remove the first delimeter, return value
return substr(v_result, 2);
end;
/
--SQL
select someId
,myAggregationFunction
(
cast
(
--Here's where the aggregation and ordering happen
collect(sometype(SomeValue, SomeOtherValue)
order by SomeOrderingValue)
as someTypes
)
) result
from
(
--Test data: note the unordered SoemOrderingValue.
select 1 someId, 3 SomeOrderingValue, '3' SomeValue, '3' SomeOtherValue
from dual union all
select 1 someId, 1 SomeOrderingValue, '1' SomeValue, '1' SomeOtherValue
from dual union all
select 1 someId, 2 SomeOrderingValue, '2' SomeValue, '2' SomeOtherValue
from dual
) foo
group by someId;
--Here are the results, aggregated and ordered.
SOMEID RESULT
------ ------
1 1+1,2+2,3+3
Oracle is very likely be rewriting your query and getting rid of the subquery. I've never done anything like what you're doing, but could you add the NO_UNNEST hint on the inner query?
SELECT SomeId, myAggregationFunction(Item) FROM
(
SELECT /*+ NO_UNNEST */
Foo.SomeId, ...
Even then, I'm really not sure what it will do with an ORDER BY inside a subquery.

Resources