I have tried to execute code the instruction below. But somehow I cant get it working. I am new to PL/SQl. Any hint will be valuable thanks
The rank table has:
rankID Number
name Varchar2(255 BYTE)
/*
Write PL/SQL program (anonymous block) that prints out a list of all ranks (ID
and name) for all rank ID from 100 to 110. If a rank ID (xxx) does not appear
in the rank table the program should print out: NO RANK AVAILABLE for ID:
xxx
*/
--set serveroutput on
DECLARE
rank_id NUMBER;
rank_name VARCHAR2(255);
loopcount NUMBER;
BEGIN
loopcount :=100;
FOR k IN 100..110
LOOP
SELECT rankID, name
INTO rank_id, rank_name
FROM rank
WHERE rankID=loopcount;
DBMS_OUTPUT.PUT_LINE(rank_id||' '|| rank_name);
loopcount := loopcount + 1;
END LOOP;
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('NO RANK AVAILABLE for ID: '||rank_id);
END;
/
Below is what I am getting. Its not working the way it should
Server output:
100 Chefpilot
NO RANK AVAILABLE for ID: 100
DECLARE executed successfully
Execution time: 0.26s
No real need for a loop in this case, you can do this with an outer join, a case statement and a numbers table:
WITH CTE (RankId) AS (
SELECT 100 RankId
FROM DUAL
UNION ALL
SELECT RankId + 1
FROM CTE
WHERE RankId < 110
)
SELECT t.RankId, COALESCE(r.Name, 'Does not exist') Name,
CASE
WHEN r.RankId IS NULL THEN 'No rank available for: ' || t.RankId
ELSE r.RankId || ' ' || r.Name
END Description
FROM CTE t
LEFT JOIN rank r ON t.RankId = r.RankId
ORDER BY t.RankId
SQL Fiddle Demo
#sgeddes's approach is better, but to explain what you are seeing, if you did want to use your mechamism then you'd need to catch the exception in an inner block. At the moment the exception handler is outside the loop, so the first error you see terminates the loop. With an inner block:
DECLARE
rank_id NUMBER;
rank_name VARCHAR2(255);
BEGIN
FOR loopID IN 100..110
LOOP
BEGIN -- inner block
SELECT rankID, name
INTO rank_id, rank_name
FROM rank
WHERE rankID=loopID;
DBMS_OUTPUT.PUT_LINE(rank_id||' '|| rank_name);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('NO RANK AVAILABLE for ID: '||loopID);
END; -- inner block
END LOOP;
END;
/
Now if the ID doesn't exist the exception is caught, the message is printed, and the inner block exits; which means the loop continues at the next iteration. (I've also removed the extra loopcount and consistently used a loopID for the for and both references).
Related
I have a package which contains a procedure in which I need to query a list of id numbers, which are varchar2. I will have to query this same list multiple times, and I'd rather not have redo the query. Ideally I'd like to make a function in my package that would return the list of id numbers. So I could load the array of id numbers into a variable, and then use that variable as a table throughout my procedure. I've been googling like crazy and it just doesn't seem like it's possible. Is there some way to do this?
Note: I'm not able to create a new type at the schema level, and it won't let me do this with a local collection type.
Also, I would prefer not to use dynamic sql; the main query in my procedure is enormous, and I don't want to deal with a string of that size.
I want to do something like this:
id_number_list array_type := my_function();
select *
from my_table mt
left join table(id_number_list) idl on mt.id_number = idl.column_value;
EDIT: Thanks for your help so far! MTO's answer works for a select statement join, like I described above. However, I also need to delete from the table where the id_number is in the list. This gives me an "invalid data type" error. What could explain this? The data type should always be the same: varchar2(10).
Here I create the type at package level:
type string_list is table of varchar2(10);
Then I create a function that returns the list of id numbers (for our purposes, the "action" is always c_action_refresh, so the if statement is true):
function get_modified_ids(scope in smallint, action in smallint) return string_list is
modified_ids string_list := string_list();
last_refreshed date;
begin
last_refreshed := get_last_refreshed_date(scope,action);
if action = c_action_refresh then
select id_number
bulk collect into modified_ids
from(
select id_number
from adv.hr_giving cg
join adv.pbi_dates d
on d.DATE_FULL = trunc(cg.processed_date)
where d.RELATIVE_DATE >= last_refreshed
and d.RELATIVE_DATE <= trunc(CURRENT_DATE)
and cg.fiscal_year >= adv.current_fiscal_year - 6
union
select gi.gift_donor_id as id_number
from adv.gift gi
where gi.date_added >= last_refreshed
or gi.date_modified >= last_refreshed
union
select p.pledge_donor_id as id_number
from adv.pledge_rev p
where p.date_added >= last_refreshed
or p.date_modified >= last_refreshed
union
select a.id_number
from adv.affiliation a
where a.date_added >= last_refreshed
or a.date_modified >= last_refreshed
);
end if;
return(modified_ids);
end get_modified_ids;
Then, in my procedure, I initialize a variable by calling the function:
modified_ids string_list := get_modified_ids(scope,action);
Then I try to use the list in a delete statement:
delete from advrpt.pbi_gvg_profile_ag p
where p.id_number in
(select column_value from table(modified_ids));
This gives the error ORA-00902: invalid datatype. The type of id_number is varchar2(10). And again, it works fine in a join in a select statement.
So why am I getting this error?
Don't use a variable in the package (as there would only be a single variable and if your procedure is called twice in short succession then the second set of values would overwrite the first and potentially cause issues if that happened mid-way through processing the first invocation).
Instead, create a user-defined collection type:
CREATE TYPE number_list IS TABLE OF NUMBER;
And pass a collection as an argument to the procedure:
CREATE PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id MEMBER OF i_numbers;
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
/
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_procedure(number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
Or create the type as part of your package:
CREATE PACKAGE your_package AS
TYPE number_list IS TABLE OF NUMBER;
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
);
END;
/
Then create the package body:
CREATE PACKAGE BODY your_package AS
PROCEDURE your_procedure (
i_numbers IN number_list,
o_cursor1 OUT SYS_REFCURSOR,
o_cursor2 OUT SYS_REFCURSOR
)
IS
BEGIN
OPEN o_cursor1 FOR
SELECT *
FROM your_table
WHERE id IN (SELECT COLUMN_VALUE FROM TABLE(i_numbers));
OPEN o_cursor2 FOR
SELECT y.*
FROM your_table y
INNER JOIN TABLE(i_numbers) n
ON (y.id = n.COLUMN_VALUE);
END;
END;
/
Note: The MEMBER OF operator only works with collections defined in the SQL scope and not collections defined locally in a PL/SQL scope.
Then call it using, for example:
DECLARE
v_cur1 SYS_REFCURSOR;
v_cur2 SYS_REFCURSOR;
v_id your_table.id%TYPE;
v_value your_table.value%TYPE;
BEGIN
your_package.your_procedure(your_package.number_list(1,5,13), v_cur1, v_cur2);
LOOP
FETCH v_cur1 INTO v_id, v_value;
EXIT WHEN v_cur1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_id || ', ' || v_value);
END LOOP;
END;
/
fiddle
To me, it looks as if you have it all. If not, here's an example.
Function:
SQL> create or replace function f_test
2 return sys.odcinumberlist
3 is
4 begin
5 return sys.odcinumberlist(10, 20);
6 end;
7 /
Function created.
How to use it?
SQL> set serveroutput on
SQL> declare
2 id_number_list sys.odcinumberlist := f_test;
3 begin
4 for cur_r in (select e.deptno, e.ename
5 from emp e join table(id_number_list) idl on idl.column_value = e.deptno
6 order by 1, 2
7 )
8 loop
9 dbms_output.put_line(cur_r.deptno ||' '|| cur_r.ename);
10 end loop;
11 end;
12 /
10 CLARK
10 KING
10 MILLER
20 ADAMS
20 FORD
20 JONES
20 SCOTT
20 SMITH
PL/SQL procedure successfully completed.
SQL>
I'd like get a count of every possible value from one table associated with each possible value from another table. So if my (combined) table basically look like:
Order ID Employee Product Category
-------------------------------------------
1 Alan Automobile
2 Barry Beauty
3 Charlie Clothing
4 Alan Beauty
I would like to be able to query and get a result of:
Employee Count Auto Count Beauty Count Clothing
------------------------------------------------------------
Alan 1 1 0
Barry 0 1 0
Charlie 0 0 1
I could manually query for each count, but then if I later add new product categories, it will no longer work. What I'm doing now is basically just:
SELECT employee, category, COUNT(*) FROM sales GROUP BY employee, category;
Which returns:
Employee Category Count
-------------------------------
Alan Automobile 1
Alan Beauty 1
Alan Clothing 0
etc. But with a large number of categories this can get a bit redundant. Is there any way to have it returned as a single row for each employee with a column for every category?
You can use JSON approach
SELECT employee,
json_object_agg(ProductCategory,total ORDER BY ProductCategory)
FROM (
SELECT employee, ProductCategory, count(*) AS total
FROM tbl
GROUP BY employee,ProductCategory
) s
GROUP BY employee
ORDER BY employee;
or with two step approach
CREATE FUNCTION dynamic_pivot(central_query text, headers_query text)
RETURNS refcursor AS
$$
DECLARE
left_column text;
header_column text;
value_column text;
h_value text;
headers_clause text;
query text;
j json;
r record;
curs refcursor;
i int:=1;
BEGIN
-- find the column names of the source query
EXECUTE 'select row_to_json(_r.*) from (' || central_query || ') AS _r' into j;
FOR r in SELECT * FROM json_each_text(j)
LOOP
IF (i=1) THEN left_column := r.key;
ELSEIF (i=2) THEN header_column := r.key;
ELSEIF (i=3) THEN value_column := r.key;
END IF;
i := i+1;
END LOOP;
-- build the dynamic transposition query (based on the canonical model)
FOR h_value in EXECUTE headers_query
LOOP
headers_clause := concat(headers_clause,
format(chr(10)||',min(case when %I=%L then %I::text end) as %I',
header_column,
h_value,
value_column,
h_value ));
END LOOP;
query := format('SELECT %I %s FROM (select *,row_number() over() as rn from (%s) AS _c) as _d GROUP BY %I order by min(rn)',
left_column,
headers_clause,
central_query,
left_column);
-- open the cursor so the caller can FETCH right away
OPEN curs FOR execute query;
RETURN curs;
END
$$ LANGUAGE plpgsql;
then
=> BEGIN;
-- step 1: get the cursor (we let Postgres generate the cursor's name)
=> SELECT dynamic_pivot(
'SELECT employee,ProductCategory,count(*)
FROM tbl GROUP BY employee,ProductCategory
ORDER BY 1',
'SELECT DISTINCT productCategory FROM tbl ORDER BY 1'
) AS cur
\gset
-- step 2: read the results through the cursor
=> FETCH ALL FROM :"cur";
Reference
I have two cursors the for loop should use the cursor based on the status.
CURSOR order_hist1 IS
SELECT id, ordernum, address FROM order_hist;
CURSOR order_hist2 IS
SELECT id, ordernum, address FROM order_hist_complete;
so for loop should use cursor order_hist2 is the variable status = 'COMPLETE'
else use order_hist1
FOR aDistinctLine in -- LOOP
-- 300 lines code in this loop
END LOOP;
I don't want o use REF Cursors
You can use implicit for loop:
For your case, it looks suitable to change the two cursors to a single one, using UNION (UNION ALL if you need to process duplicates, or performance reasons), like follows:
FOR aDistinctLine in (
-- first cursor: status <> COMPLETE
SELECT id, ordernum, address FROM order_hist
WHERE status <> 'COMPLETE'
UNION
SELECT id, ordernum, address FROM order_hist_complete
WHERE status = 'COMPLETE'
) LOOP
-- do things with
-- aDistinctLine.id,
-- aDistinctLine.ordernum,
-- aDistinctLine.address
END LOOP;
Then it's better to have status look like a local variable, e.g. call it l_status; I had to convince myself it could work to use a plsql variable inside an implicit for loop... guess I learned something today!
declare
l_status varchar2(8) := 'COMPLETE';
begin
for x in (select 'realy?' val from dual where l_status = 'COMPLETE')
loop
dbms_output.put_line(x.val);
end loop;
l_status := 'graby';
for x in (select 'here: not complete' val from dual where l_status <> 'COMPLETE')
loop
dbms_output.put_line(x.val);
end loop;
end;
/
I need to call the stored function findtotalcarmodels from this PL/SQL block. The way this code is written is not the way I would do it in production, however it is an exercise in 'lateral' thinking.
SET SERVEROUTPUT ON FORMAT WRAP SIZE 12000
Declare
v_model VARCHAR2(40);
v_cost NUMBER;
v_reg VARCHAR2(10);
v_carcategory VARCHAR2(40);
v_totalcars NUMBER;
v_count DATE;
v_maxcount DATE;
v_maxdept VARCHAR2(20);
cursor carcur IS
SELECT * FROM i_car;
v_car carcur%ROWTYPE;
Cursor c_date (p_reg i_booking.registration%TYPE) IS
SELECT date_reserved
FROM i_booking
WHERE registration = p_reg;
v_date c_date%ROWTYPE;
Begin
v_totalcars := findtotalcarmodels();
FOR v_car IN carcur LOOP
If v_cost <=50000 THEN v_carcategory := 'Budget Car';
End IF;
If v_cost BETWEEN 50000 AND 100000 THEN v_carcategory := 'Standard Car';
End IF;
If v_cost >100000 THEN v_carcategory := 'Premium Car';
End If;
FOR v_date IN c_date(v_car.registration) LOOP
v_count := v_count + 1;
END LOOP;
IF v_count > v_maxcount THEN
v_maxcount := v_count;
v_maxdept := v_car.registration;
END IF;
DBMS_OUTPUT.PUT_LINE('Registration:'|| ' '|| v_car.registration);
DBMS_OUTPUT.PUT_LINE('Cost:'|| '$' ||v_car.Cost);
DBMS_OUTPUT.PUT_LINE('Model Name:'|| ' '||v_car.model_name);
DBMS_OUTPUT.PUT_LINE('Car Category:'|| ' '||v_carcategory);
DBMS_OUTPUT.PUT_LINE('Total number of Cars:'|| ' '||v_totalcars);
DBMS_OUTPUT.PUT_LINE('Most Recent Rental Date: '|| ' '||v_maxcount);
DBMS_OUTPUT.NEW_LINE;
END LOOP;
END;
I am getting the error:
v_totalcars := findtotalcarmodels();
*
ERROR at line 19:
ORA-06550: line 19, column 16:
PLS-00306: wrong number or types of arguments in call to 'FINDTOTALCARMODELS'
ORA-06550: line 19, column 1:
PL/SQL: Statement ignored
Am I calling my function correctly in the right position?
This is the function:
CREATE OR REPLACE Function findtotalcarmodels
(model_name_in IN varchar2)
RETURN NUMBER
IS
counter INTEGER := 0;
CURSOR car_count_cur IS
SELECT model_name FROM i_car WHERE model_name = model_name_in;
Rec_car_details car_count_cur%ROWTYPE;
BEGIN
OPEN car_count_cur;
LOOP
FETCH car_count_cur INTO Rec_car_details;
EXIT WHEN car_count_cur%NOTFOUND;
counter := counter + 1;
END LOOP;
CLOSE car_count_cur;
RETURN counter;
END;
Okay, so I have no idea why you're getting that error with that function. The error indicates that you're not giving the function the correct number of arguments. Judging by the function that's clearly not what's happening, or it's not the same function.
You've just changed the function call; the function requires an argument so the "incorrect" code you had in your first revision was actually correct.
However, let's put that to one side for a second and look again at what you're doing.
Your function is a count on a table. There's no need for a cursor or looping, incrementing variables or anything. You can simplify it to
select count(*) from i_car where model_name = :model_name
You never assign the variables v_count or v_maxcount a value so incrementing them will still result in a NULL. They're dates anyway, so it's a little strange to be incrementing them.
Your cursor c_date is just another count; once again no need for a loop.
The model_name variable is never initialised so your function will not return a result.
There are a lot of ways to simplify this; though I'm going to guess a few things here. Change your carcur cursor to the following:
select i.*
, case cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(*) over ( partition by model_name ) as total_cars
from i_cars
This appears to enable you to remove your IF statements and your function. You can then remove your second loop by performing an outer join on i_booking (you need to add the primary key in yourself):
select i.*
, case c.cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(distinct c.primary_key)
over ( partition by c.model_name ) as total_cars
, count(b.date_reserved)
over ( partition by b.registration ) as reserved_ct
from i_cars c
left outer join i_booking b
on c.registration = b.registration
I'm not 100% certain on the max stuff as it's not clear at all where it's assigned (it's not) but it looks as though you might be wanting to find the maximum count by model etc, in which case you can use a sub-query on the above cursor:
select sub.*
, max(total_cars) over () as max_cars
, max(reserved_ct) over () as max_reserved
from ( select i.*
, case c.cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(distinct c.primary_key)
over ( partition by c.model_name ) as total_cars
, count(b.date_reserved)
over ( partition by b.registration ) as reserved_ct
from i_cars c
left outer join i_booking b
on c.registration = b.registration
) sub
If you then need to output it (there's rarely a need) you can loop through this single SQL statement, which gives you everything in one go:
declare
c_cursor is
select sub.*
, max(total_cars) over () as max_cars
, max(reserved_ct) over () as max_reserved
from ( select i.*
, case c.cost
when <= 50000 then 'Budget Car'
when <= 100000 then 'Standard Car'
else 'Premium Car'
end as category
, count(distinct c.primary_key)
over ( partition by c.model_name ) as total_cars
, count(b.date_reserved)
over ( partition by b.registration ) as reserved_ct
from i_cars c
left outer join i_booking b
on c.registration = b.registration
) sub
;
begin
for i in c_cursor loop
dbms_output.put_line(i.registration);
dbms_output.put_line(i.cost);
...
end loop;
end;
I've reduced a PL/SQL block and a function to a single SQL statement; it may not be spot on because there's so many unknowns but it's worth trying for yourself. Simple is almost always better.
create or replace procedure BAS_NUM_UPD is
cursor cur is
select distinct o.oi_b,mpr.pa_ke_i,ltrim(substr(convert_171_to_711(cp.p_t_num),1,7),'0') bs_nbr
from t_obj o, mat_pa_rel mp, cor_pa cp
where o.ob_t = 'something'
and o.oi_b = mp.oi_b
and mp.pa_ke_i = cp.pa_ke_i;
l_ba_num_at_i number(10) := get_attribute_id('Ba timber');
flag1 VARCHAR2(10);
type t1 is table of varchar2(10);
par_k t1;
BEGIN
for x in cur loop
BEGIN
select pa_ke_i into par_k from mat_pa_rel where oi_b=x.oi_b ;
if par_k.count=null then
insert into cs_val (oi_b, at_i, value, flag, ) values (x.oi_b, l_ba_num_at_i, null, 1);
end if;
select flag into flag1 from cs_val where at_i = l_ba_num_at_i and oi_b = x.oi_b
and value = x.bs_nbr;
EXCEPTION
when NO_DATA_FOUND THEN
insert into cs_val (oi_b, at_i, value, flag, )
values (x.oi_b, l_ba_num_at_i, x.bs_nbr, 1);
flag1 :='Nothing';
when OTHERS
then
raise_application_error(-20011,'Unknown Exception in PROCEDURE');
END;
end loop;
end BAS_NUM_UPD;
error:
PLS-00642: local collection types not allowed in SQL statements
You should get it running if you do a bulk collect
select pa_ke_i bulk collect into par_k from mat_pa_rel where oi_b=x.oi_b ;
Then I think the if is not right. I think you need to do
if par_k.count = 0 then
But to be honest you might just make a count
select count(*) into l_cnt from mat_pa_rel where oi_b=x.oi_b;
If l_cnt = 0 then ...
Of course l_cnt has to be defined.
You should create type t1 in the schema and not in the pl/sql block.