Oracle procedure, placement of multiple records in variables - sql

I'm trying to create my first oracle procedure. The select will return multiple records; I need to be able to place each record in the variables and use the record in later actions in the procedure. Any help please?
key number;
keyCount number;
rub varchar2(50);
srub varchar2(100);
type varchar2(200);
date varchar2(14);
note varchar2(500);
BEGIN
SELECT KEY,COUNT(KEY),RUB,
SRUB,TYPE ,DATE,NOTE FROM Student
WHERE S_KEY = {key};
END;

In PL/SQL we need to select results into matching variables. One way is separate variables for each column (as shown). The alternative is to use a row variable which matches the project of the query; find out more.
You've got an aggregating function - COUNT() so you need a GROUP BY clause which defines the non-aggregating columns. You say you have more than one record so you need to populate a collection not scalar variables. Find out more.
Your procedure should look something like this
create or replace procedure my_first_proc
( p_key in student.s_key%type )
as
type my_rec is record (
key number ,
keyCount number ,
rub varchar2(50); ,
srub varchar2(100) ,
type varchar2(200) ,
date varchar2(14),
note varchar2(500)
);
type my_rec_coll is table of my_rec;
l_student_recs my_rec_coll;
BEGIN
SELECT KEY,COUNT(KEY),RUB,SRUB,TYPE ,DATE,NOTE
bulk collect into l_student_recs
FROM Student
WHERE S_KEY = p_key
group by KEY,RUB,SRUB,TYPE ,DATE,NOTE
;
for idx in l_student_recs.first() .. l_student_recs.last()
loop
-- do some processing here
dbms_output.put_line('RUB = '||l_student_recs(idx).rub);
end loop;
EXCEPTION
when no_data_found then
raise_application_error(-01403, 'no student records for key='||p_key);
END;
Get into good habits:
use sensible variable names
distinguish parameter names from local variables
handle predictable exceptions

Related

Use column name as key for PL/SQL associative array when updating another column

I have an Excel table with two columns of data. Column A are codes, column B are the corresponding country names. I turned it into an associative array, ueln_country.
Now the task is to update HORSE table's column COUNTRY_OF_RESIDENCE. Horses have a column UELN where first three letters correspond to the codes in the Excel table.
I have to check if the code exists in the Excel table. If it does, I have to update HORSE.country_of_residence with a CLASSIFICATOR.code where CLASSIFICATOR.name = **the corresponding country in column B** andCLASSIFICATOR.dom_code = 'ISOCODE'`.
First try gets the error
PLS-00201: identifier 'UELN' must be declared
As I understood, it's because I can only use declared variables in PL/SQL statement.
declare
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test TBL_UELN_COUNTRY;
ueln_country TBL_UELN_COUNTRY;
begin
ueln_country('008') := 'ALBAANIA';
ueln_country('010') := 'ANTARKTIS';
ueln_country('011') := 'ANTARKTIS';
....
update HORSE
set COUNTRY_OF_RESIDENCE=
when (...dummy_case...) then
(select code from meta.classifcator
where dom_code = 'ISOCODE'
and name = ueln_country(substr(UELN, 1, 3)))
where UELN is not null;
end;
/
Second try.
So because of the first error I tried to somehow declare the variables.
I knew it wouldn't work (ORA-01422: exact fetch returns more than requested number of rows) but made it to show where my idea is going:
declare
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test TBL_UELN_COUNTRY;
ueln_country TBL_UELN_COUNTRY;
v_ueln horse.UELN%TYPE;
begin
select UELN into v_ueln from HORSE;
ueln_country('008') := 'ALBAANIA';
ueln_country('010') := 'ANTARKTIS';
ueln_country('011') := 'ANTARKTIS';
....
update HORSE
set COUNTRY_OF_RESIDENCE=
when (...dummy_case...) then
(select code from meta.classifcator
where dom_code = 'ISOCODE'
and name = ueln_country(substr(v_ueln, 1, 3)))
where UELN is not null;
end;
/
So I want pick a value from associative array where the key = substr(specific_horse.UELN, 1, 3).
Searched through Google and Stack for hours and didn't find an answer.
The ugly and very slow working solution was just where I didn't make the associate array and made 400+ cases for every Excel table row in the form like when -key- then select code from meta.classificator where dom_code = 'ISOKOOD' and name = -value-
associative array can not be used in SQL.
If you use expression like array(index) in SQL then, in fact, PL/SQL engine gets value by index and then result is bound into SQL engine before execution of the SQL statement.
More specifically
declare
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test TBL_UELN_COUNTRY;
dummy varchar2(30);
begin
test('GBP') := 'UK';
test('USD') := 'USA';
select /*+ qwerty */ test('GBP')
into dummy
from dual;
end;
/
If we check binds for a cursor we see that actual bind value has a type VARCHAR(128) - :B1. test('GBP') in PL/SQL code is passed as bind variable B1.
SQL> column sql_text format a50
SQL> select sbc.datatype_string, sql_text
2 from v$sql s join v$sql_bind_capture sbc
3 on s.sql_id = sbc.sql_id
4 where lower(sql_text) not like '%v$sql%'
5 and lower(sql_fulltext) like 'select %qwerty%';
DATATYPE_STRING SQL_TEXT
--------------- --------------------------------------------------
VARCHAR2(128) SELECT /*+ qwerty */ :B1 FROM DUAL
SQL engine knows nothing about associative array and apparently it cannot pass and index value to array and get an element of the array back.
If you still want to use associative array to look-up some values you can declare package variable and a getter function (you may also want to implement the logic to handle a case when there is no element in array for a given index - otherwise you'll get run-time exception in such case).
create or replace package pkg as
function GetCountry(idx in varchar2) return varchar2;
end pkg;
/
sho err
create or replace package body pkg as
type TBL_UELN_COUNTRY is table of varchar2(50) index by varchar2 (3);
test pkg.TBL_UELN_COUNTRY;
function GetCountry(idx in varchar2) return varchar2 as
begin return test(idx); end;
-- initializing
begin
test('GBP') := 'UK';
test('USD') := 'USA';
end pkg;
/
sho err
And finally
SQL> set serveroutput on
SQL> declare
2 dummy varchar2(30);
3 begin
4 with t(idx) as (select 'GBP' from dual)
5 select pkg.GetCountry(t.idx)
6 into dummy
7 from t;
8 dbms_output.put_line(dummy);
9 end;
10 /
UK
PL/SQL procedure successfully completed.

PL/SQL Statement Ignored. ORA-22905: cannot access rows from a non-nested table item

I tried to select values from the nested table and bulk collecting into an associative array collection. When I try to bulk collect oracle throwing the above exception(PL/SQL: SQL Statement ignored
PL/SQL: ORA-22905: cannot access rows from a non-nested table
item) though I fetch the data from the nested table.
It is not happening in all the cases. When the same package compiled in the different client database, Some case it is not throwing an error and in some environment, it is throwing an error. Can you please help what was the exact issue.
I have not attached the entire package. Instead provided the case where the issue occurs.
DECLARE
TYPE rc_rec_multiset IS record (
rc_id NUMBER,
doc_num VARCHAR2(100)
);
TYPE rc_type IS TABLE OF rc_rec_multiset;
TYPE tab_rec_type_multiset IS
TABLE OF rc_rec_multiset INDEX BY pls_integer;
rc_tab_type rc_type := rc_type() ;
rc_tab_type_dist_rc tab_rec_type_multiset;
BEGIN
SELECT DISTINCT rc_id,
doc_num BULK COLLECT
INTO rc_tab_type_dist_rc
FROM TABLE(rc_tab_type);
END;
You cannot do this using SQL; an associative array is a PL/SQL data type and cannot be used in the SQL scope. In a similar vein, collections defined in the PL/SQL scope cannot be used in SQL scope (in 11g and earlier) - you either need to define collections in the SQL scope (Note - you cannot do this for associative arrays as they are purely PL/SQL) or just use PL/SQL.
Assuming the rc_tab_type collection is not sparse then you can use pure PL/SQL like this:
DECLARE
TYPE rc_rec_multiset IS record (
rc_id NUMBER,
doc_num VARCHAR2(100)
);
TYPE rc_type IS TABLE OF rc_rec_multiset;
TYPE tab_rec_type_multiset IS
TABLE OF rc_rec_multiset INDEX BY pls_integer;
rc_tab_type rc_type := rc_type();
rc_tab_type_dist_rc tab_rec_type_multiset;
BEGIN
/*
* Populate rc_tab_type here.
*/
FOR i IN 1 .. rc_tab_type.COUNT LOOP
rc_tab_type_dist_rc( rc_tab_type(i).rc_id ) := rc_tab_type(i).doc_num;
END LOOP;
END;
If it is sparse then, instead of the FOR loop, you will have to use:
i := rc_tab_type.FIRST;
WHILE i IS NOT NULL LOOP
rc_tab_type_dist_rc( rc_tab_type(i).rc_id ) := rc_tab_type(i).doc_num;
i := rc_tab_type.NEXT(i);
END LOOP;

How to write a procedure to display the contents of a table in sql

I have a created a procedure as
create or replace procedure availability(num in number) as
begin
delete from vehicle_count;
insert into vehicle_count from select engine_no,count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
end;
/
The procedure was created successfully but now i have to write a separate query to view the contents of vehicle_count as
select * from vehicle_count;
I tried inserting the select statement into the procedure after insertion but it showed a error stating "an INTO clause is expected in the select statement".
How can I create procedure to select the required contents and display it in a single execute statement?
Table schema
vehicle(vehicle_no,engine_no,offence_count,license_status,owner_id);
vehicle_count(engine_no,engine_count);
Check this (MS SQL SERVER)-
create or alter procedure availability(#num as int) as
begin
delete from vehicle_count;
insert into vehicle_count
output inserted.engine_no,inserted.count_engine_no
select engine_no,count(engine_no) as count_engine_no
from vehicle
where engine_no=#num
group by engine_no;
end;
If you want to use a SELECT into a PL/SQL block you should use either a SELECT INTO or a loop (if you want to print more rows).
You could use something like this:
BEGIN
SELECT engine_no, engine_count
INTO v_engine, v_count
FROM vehicle_count
WHERE engine_no = num;
EXCEPTION
WHEN NO_DATA_FOUND THEN
v_engine := NULL;
v_count := NULL;
END;
v_engine and v_count are two variables. You can declare them in your procedure, and they will contain the values you want to print.
You said that the procedure you wrote (actually, you posted here) compiled successfully. Well, sorry to inform you - that's not true. This is not a valid syntax:
insert into vehicle_count from select engine_no,count(engine_no)
----
from? Here?
Consider posting true information.
As of your question (if we suppose that that INSERT actually inserted something into a table):
at the beginning, you delete everything from the table
as SELECT counts number of rows that share the same ENGINE_NO (which is equal to the parameter NUM value), INSERT inserts none (if there's no such NUM value in the table) or maximum 1 row (because of aggregation)
therefore, if you want to display what's in the table, all you need is a single SELECT ... INTO statement whose result is displayed with a simple DBMS_OUTPUT.PUT_LINE which will be OK if you're doing it interactively (in SQL*Plus, SQL Developer, TOAD and smilar tools). Regarding table description, I'd say that ENGINE_NO should be a primary key (i.e. that not more than a single row with that ENGINE_NO value can exist in a table).
create or replace procedure availability (num in number) as
l_engine_no vehicle_count.engine_no%type;
l_engine_count vehicle_count.engine_count%type;
begin
delete from vehicle_count;
insert into vehicle_count (engine_no, engine_count)
select engine_no, count(engine_no)
from vehicle
where engine_no = num
group by engine_no;
-- This query shouldn't return TOO-MANY-ROWS if ENGINE_NO is a primary key.
-- However, it might return NO-DATA-FOUND if there's no such NUM there, so you should handle it
select engine_no, engine_count
into l_engine_no, l_engine_count
from vehicle_count
where engine_no = num;
dbms_output.put_line(l_engine_no ||': '|| l_engine_count);
exception
when no_data_found then
dbms_output.put_line('Nothing found for ENGINE_NO = ' || num);
end;
/
There are numerous alternatives to that (people who posted their answers/comments before this one mentioned some of those), and the final result you'd be satisfied with depends on where you want to display that information.

Execute a Select inside a loop PL/SQL and return cursor?

This is probably a simple question for who knows PL/SQL.
I have a stored procedure who takes an array of varchar in input:
TYPE MULTI is table of VARCHAR(15) index by BINARY_INTEGER;
PROCEDURE MYPROC(
SINGLE IN MULTI,
P_RESULT OUT MY_PCK.MYCURSOR,
P_SOMETHING OUT VARCHAR2,
);
The cursor works because i have tested it in other cases but this is the first with an array parameter.
I have a problem with the body, how can i assign each value i get from the select to the cursor?
BEGIN
FOR i IN SINGLE.first .. SINGLE.last
LOOP
--BEGIN OPEN P_RESULT FOR this --this on left gives me error
SELECT MT.DESCR INTO P_SOMETHING
FROM MYTABLE1 MT
WHERE MT.IDS = SINGLE(i)
AND and rownum < 2;
--dbms_output.put_line(SINGLE(i)); --if i use this instead of select i get the values i send to this procedure.
END LOOP;
i tried also:
SELECT MT.DESCR INTO P_RESULT but gives error
What i'm doing wrong?
Thanks in advice.
You can't use your PL/SQL collection type in a SQL statement in 11g. You could create a SQL collection type, or find one you already have access to (which has a suitable string length) and use that to at least verify the mechanism.
For instance, this uses a local variable of type SYS.HSBLKNAMLST, which is defined as a table of varchar2(30), more than enough to match your own PL/SQL type's string length. That variable is populated from your passed-in PL/SQL type, and that is then used in a query to open the cursor:
PROCEDURE MYPROC(
SINGLE IN MULTI,
P_RESULT OUT MY_PCK.MYCURSOR
) IS
LOCAL_COLL SYS.HSBLKNAMLST := SYS.HSBLKNAMLST();
BEGIN
FOR i IN SINGLE.first .. SINGLE.last
LOOP
LOCAL_COLL.extend();
LOCAL_COLL(LOCAL_COLL.last) := SINGLE(i);
END LOOP;
OPEN P_RESULT FOR
SELECT MT.DESCR
FROM MYTABLE1 MT
LEFT JOIN TABLE(LOCAL_COLL) LC
ON LC.COLUMN_VALUE = MT.IDS
WHERE LC.COLUMN_VALUE IS NULL;
END;
I'm a bit confused about the query you showed in your loop though; you seem to be attempting to re-open the cursor for each element of the array, though maybe you were trying to append the result of the query for each element to the same cursor - thought the != would mean that all rows would be included at some point. I've guess that you're trying to get all records with IDS values that are not in the array. If you actually want all that are then that would be:
OPEN P_RESULT FOR
SELECT MT.DESCR
FROM TABLE(LOCAL_COLL) LC
JOIN MYTABLE1 MT
ON MT.IDS = LC.COLUMN_VALUE;
You can see which built-in types are available to you by querying the data dictionary, e.g:
select owner, type_name, coll_type, elem_type_name, length
from all_coll_types
where elem_type_name = 'VARCHAR2'
and coll_type = 'TABLE'
and owner = 'SYS'
order by length;
It would be preferable to create your own SQL type if you're able to.

How to use in statement with nested table

Hey there I have a function, and part of the function is to make sure that the selected value is within the passed in table of varchar2s. To start I declare a varchar2 table type like so.
create or replace type Varchar2Table is table of varchar2(200)
Then I have the function which accepts the nested table parameter and has a select statement on them.
function SelectPeople(inputNames Varchar2Table) return People
begin
--stuff
select * from person_table where name in inputNames; --line of interest
--more stuff
end;
This doesn't seem to work though, I get the following error:
ORA-00932: inconsistent datatypes: expected NUMBER got
ENGSPL5.VARCHAR2TABLE
Any suggestions?
The TABLE operator allows nested tables to be used in SQL statements. The function was also missing an IS and an INTO.
create or replace type Varchar2Table is table of varchar2(200);
create table person_table(id number, name varchar2(100));
create or replace function SelectPeople(inputNames Varchar2Table) return number
is --Missing "IS".
type numberTable is table of number; --Need a collection to store results.
numbers numberTable;
begin
select id
bulk collect into numbers --Missing "INTO".
from person_table
where name in (select column_value from table(inputNames)); --Missing "TABLE".
--Alternatively a multiset condition can be used.
--where name member of inputNames;
--Dummy return value to make the function compile.
return 1;
end;
/