Accept uni or multi-dimensional parameter in PL/SQL function - oracle

I'm trying to write a function nbrJobs() in PL/SQL that counts the number of jobs that a user has had in the past.
In order to do this I first need to determine the "employee id", which can be determined by a pair (first name, last name).
I manage to do this when the arguments of the function are:
nbrJobs(firstName IN VARCHAR2(20), lastName IN VARCHAR2(25))
Then, this simple test runs without any problem:
DECLARE
nbrJobsTotal NUMBER;
BEGIN
SELECT nbrJobs(first_name, last_name) INTO nbrJobsTotal
FROM employees
WHERE employee_id = 101
END
Now, the problem is that my function should also work with that kind of call:
SELECT nbrJobs(first_name, last_name) INTO nbrJobsTotal FROM employees
with table employees containing multiple tuples.
So, now I'm confused about the input parameters type.. Should I use a VARRAY, a nested TABLE, a CURSOR, something else ?
What does a SELECT actually returns if multiple rows are selected?

A PL/SQL function is executed for each row of the SELECT statement. Therefore, if you call your function in a regular SELECT SQL statement you will get a value for each record.
Here is an example by concatenating the first and last name together:
CREATE OR REPLACE FUNCTION NAME (p_FIRST_NAME IN VARCHAR2, p_LAST_NAME IN VARCHAR2)
RETURN VARCHAR2
AS
BEGIN
RETURN p_FIRST_NAME || ' ' || p_LAST_NAME;
END;
/
SELECT first_name, last_name, name(first_name, last_name) FROM HR.employees;
FIRST_NAME LAST_NAME NAME(FIRST_NAME,LAST_NAME)
---------- --------- --------------------------
Ellen Abel Ellen Abel
Sundar Ande Sundar Ande
Mozhe Atkinson Mozhe Atkinson
David Austin David Austin
As you can see, for each row the PL/SQL function is executed and concatenates the first and last name and then is returning the result as a new column to the SELECT statement. There is no need for you to change the function to make it work with multiple row.
Now how would you be executing this inside PL/SQL with the SELECT statement that you have used as example above. You will either have to loop over the results with a cursor or you could use a collection type if you just want to fetch the result of all of the rows into a variable.
Using a cursor (
I'm demonstrating this on my example above by using the Cursor FOR LOOP):
BEGIN
FOR result IN (SELECT first_name, last_name, name(first_name, last_name) name FROM HR.employees) LOOP
DBMS_OUTPUT.PUT_LINE(result.first_name || ', ' || result.last_name || ', ' || result.name);
END LOOP;
END;
/
Ellen, Abel, Ellen Abel
Sundar, Ande, Sundar Ande
Mozhe, Atkinson, Mozhe Atkinson
David, Austin, David Austin
What happens here is that I'm executing the very same SELECT statement but now inside the Cursor FOR LOOP which allows me to loop over each individual row that has been return. In this case I just print the result out into the console.
If you want to just save all the rows into a variable you will have to use a PL/SQL Collection:
DECLARE
-- Specify cursor with expected results
CURSOR c1 IS
SELECT first_name, last_name, name(first_name, last_name) name
FROM HR.employees;
-- Create PL/SQL type for a nested table of the rowtype of the cursor (first_name, last_name, name)
TYPE NameSet IS TABLE OF c1%ROWTYPE;
employees NameSet; -- Instantiate a variable of the nested table of records
BEGIN
-- Assign values to nested table of records:
SELECT first_name, last_name, name(first_name, last_name) name
BULK COLLECT INTO employees
FROM HR.employees;
-- Print nested table of records:
FOR i IN employees.FIRST .. employees.LAST LOOP
DBMS_OUTPUT.PUT_LINE (
employees(i).first_name || ' ' ||
employees(i).last_name || ', ' ||
employees(i).name
);
END LOOP;
END;
/
Ellen Abel, Ellen Abel
Sundar Ande, Sundar Ande
Mozhe Atkinson, Mozhe Atkinson
David Austin, David Austin
As you can see here the same SELECT is executed but here we use BULK COLLECT INTO rather than just INTO. This is because the SELECT is returning more than one row, hence we need to tell the compiler that we do indeed expect that so that the compiler doesn't throw an error that more rows have been returned.
Last but not least, given that you use the variable name nbrJobsTotal in your example SELECT nbrJobs(first_name, last_name) INTO nbrJobsTotal FROM employees above, I think what you really want to try to do here is to sum up all the number of different jobs that employees had in your company. You can accomplish just that but using the built-in SUM() function which is an aggregation function, i.e it will only return one row without a GOUP BY clause:
SELECT SUM(nbrJobs(first_name, last_name)) INTO nbrJobsTotal FROM employees

You can use a Table of Type - to bulk collect all rows returned by the select statement.
Based on your example it would look like this:
DECLARE
nbrJobsTotal NUMBER;
TYPE jobsTotalTable_type IS TABLE OF nbrJobsTotal%TYPE;
jobsTotalTable jobsTotalTable_type ;
BEGIN
--bulk collect results
SELECT nbrJobs(first_name, last_name) BULK COLLECT INTO jobsTotalTable
FROM employees;
--print results
FOR indx IN 1 .. jobsTotalTable.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(jobsTotalTable (indx));
END LOOP;
END

Related

ORA-00947:not enough values

guys i am getting error that PL/SQL: ORA-00947: not enough values while running my code.
declare
type e_type is record ( last_name employees.last_name%type,
email employees.email%type,
hire_date employees.hire_date%type,
job_id employees.job_id%type);
type e_list is table of e_type index by pls_integer;
emps e_list;
begin
for x in 100 .. 110 loop
select last_name,email,hire_date,job_id into emps(x) from employees
where employee_id = x ;
---dbms_output.put_line(emps(x).email);
insert into emp(last_name,email,hire_date,job_id) values emps(x);
end loop;
end;
#WilliamRobertson presents an interesting solution (I had actually forgotten that format). However there is still a much simpler solution. That is to use a single sql statement for the select and insert while avoiding the type and variable declarations and the loop altogether:
insert into emp(last_name,email,hire_date,job_id)
select last_name,email,hire_date,job_id
from employees
where employee_id between 100 and 110;
The problem is here:
insert into emp(last_name,email,hire_date,job_id)
values emp(x);
The PL/SQL INSERT Statement Extension where you use a record variable for the values clause does not accept a column list, so it needs to be either
insert into emp values emp(x);
where emp(x) evaluates to a record matching the entire emp row, or else
insert into
( select last_name, email, hire_date, job_id from emp )
values emp(x);
where emp(x) is a record exactly matching the select list.

pl sql insert into within a procedure and dynamic variables

I need some help with PL SQL. I have to insert some data into table. Another application is calling my procedure and caller is passing few details which I also need to insert into my table.
here is the syntax I am struggling with:
PROCEDURE invform_last2orders_item_insert( p_userId IN NUMBER
,p_accountId IN NUMBER
,p_site_Id IN NUMBER
,p_return_message OUT VARCHAR2) IS
Begin
insert into mytable
(p_userId , p_accountId , p_site_Id , sku, description, 'Cart', 1, unitId)
as
select sku, description, unitId
from mycatalogtable where site_id= p_site_Id ) ;
End;
Can you help me with syntax? I need to pass three parameters from called in parameter and some values returned from select query. How can I achieve this?
thank you for your help.
That would be something like this; see comments within code:
PROCEDURE invform_last2orders_item_insert
( p_userId IN NUMBER
,p_accountId IN NUMBER
,p_site_Id IN NUMBER
,p_return_message OUT VARCHAR2)
IS
Begin
insert into mytable
-- first name all columns you'll be inserting into; I don't know their
-- names so I just guessed
(userid,
accountid,
siteid,
sku,
description,
col1,
col2,
unitid
)
-- if you were to insert only values you got via parameters, you'd use the
-- VALUE keyword and insert those values separately.
-- As some of them belong to a table, use SELECT statement
(select p_userid,
p_accountid,
p_siteid,
c.sku,
c.description,
'Cart',
1,
c.unitid
from mycatalogtable c
where c.site_id = p_site_Id
);
-- I don't know what you are supposed to return; this is just an example
p_return_message := sql%rowcount || ' row(s) inserted';
End;
in your select statement you should have the same number of columns as you are inserting into the table, your code should be something like this example,
DECLARE
userid varchar2(20) := 'Jack';
Begin
INSERT INTO mytable (SELECT userid, SPORT from OLYM.OLYM_SPORTS);
commit;
end;

Insert into not working on plsql in oracle

declare
vquery long;
cursor c1 is
select * from temp_name;
begin
for i in c1
loop
vquery :='INSERT INTO ot.temp_new(id)
select '''||i.id||''' from ot.customers';
dbms_output.put_line(i.id);
end loop;
end;
/
Output of select * from temp_name is :
ID
--------------------------------------------------------------------------------
customer_id
1 row selected.
I have customers table which has customer_id column.I want to insert all the customer_id into temp_new table but it is not being inserted. The PLSQL block executes successfully but the temp_new table is empty.
The output of dbms_output.put_line(i.id); is
customer_id
What is wrong there?
The main problem is that you generate a dynamic statement that you never execute; at some point you need to do:
execute immediate vquery;
But there are other problems. If you output the generated vquery string you'll see it contains:
INSERT INTO ot.temp_new(id)
select 'customer_id' from ot.customers
which means that for every row in customers you'll get one row in temp_new with ID set to the same fixed literal 'customer_id'. It's unlikely that's what you want; if customer_id is a column name from customers then it shouldn't be in single quotes.
As #mathguy suggested, long is not a sensible data type to use; you could use a CLOB but only really need a varchar2 here. So something more like this, where I've also switched to use an implicit cursor:
declare
l_stmt varchar2(4000);
begin
for i in (select id from temp_name)
loop
l_stmt := 'INSERT INTO temp_new(id) select '||i.id||' from customers';
dbms_output.put_line(i.id);
dbms_output.put_line(l_stmt);
execute immediate l_stmt;
end loop;
end;
/
db<>fiddle
The loop doesn't really make sense though; if your temp_name table had multiple rows with different column names, you'd try to insert the corresponding values from those columns in the customers table into multiple rows in temp_new, all in the same id column, as shown in this db<>fiddle.
I guess this is the starting point for something more complicated, but still seems a little odd.

PL/SQL: Executing Procedure

I have created a procedure. It is giving an error ( ORA-01422: exact fetch returns more than requested number of rows). As for a specific department_id there are more than one employees. But how to solve this problem ?
Create Procedure PP1
(ID in number, Percent in number, Sal out number, increase_sal out number) IS
Begin
Select salary, salary *(1+percent/100) into sal, increase_sal
From employees
where department_id= id;
DBMS_OUTPUT.PUT_LINE (sal || ' ' || increase_sal);
END;
/
Variable a number
Variable b number
Exec PP1 (100, 10, :a, :b)
Print a b
Thanks,
Kuntal Roy
My guess is that you want something like (untested)
CREATE TYPE num_tbl IS TABLE OF NUMBER;
CREATE PROCEDURE raise_dept_salaries( p_dept_id IN employees.department_id%type,
p_raise_pct IN NUMBER,
p_old_sals OUT num_tbl,
p_new_sals OUT num_tbl )
AS
BEGIN
SELECT salary
BULK COLLECT INTO p_old_sals
FROM employees
WHERE department_id = p_dept_id;
UPDATE employees
SET salary = salary * (1 + p_raise_pct)
WHERE department_id = p_dept_id
RETURNING salary
BULK COLLECT INTO p_new_sals;
END;
Now, splitting things up this way does introduce the possibility that some other session will modify the data between your first SELECT and your UPDATE so this isn't really safe to use in a multi-user environment. Of course, you really wouldn't want to return both the old and the new salaries in the first place since you already know that they are going to be directly related to each other. If you only returned the collection of new salaries, then you would only need a single UPDATE statement and you wouldn't have the race condition.

Iterate over a column in PL/SQL

I have a table Emp with EmpID, Empname, Salary and I am trying to do a calculation for each employee. But I am having problems trying to iterate over each emp to do the calculation. I cant use explicit cursors though.
So right now I am just trying to create the list of empIDs:
Declare
aRows Number;
eid emp_ID%TYPE;
Begin
Select Count(*)
Into aRows
from emp;
Select emp_ID
Into eid
From emp;
FOR days IN 1..Tot_Rows
Loop
Dbms_Output.Put_Line(eid);
eid := eid + 1;
End Loop;
END;
But I get the error:
PLS-00320: the declaration of the type of this expression is incomplete or malformed
The simplest way to iterate over the rows in a table in PL/SQL is to do something like
BEGIN
FOR employees IN (SELECT emp_id FROM emp)
LOOP
dbms_output.put_line( employees.emp_id );
END LOOP;
END;
Alternately, you could fetch all the EID values into a PL/SQL collection and iterate over the collection, as in this example
DECLARE
TYPE emp_id_tbl IS TABLE OF emp.emp_id%type;
l_emp_ids emp_id_tbl ;
BEGIN
SELECT emp_id
BULK COLLECT INTO l_emp_ids
FROM emp;
FOR i IN l_emp_ids .FIRST .. l_empnos.LAST
LOOP
dbms_output.put_line( l_emp_ids (i) );
END LOOP;
END;
If your query can return thousands of rows, however, fetching all the data into the collection may use more of the PGA memory than you'd like and you may need to fetch rows in chunks using the LIMIT clause. But that would seem to be getting ahead of ourselves at this point.
Justin Cave has explained how to do it, but to specifically look at the error you got, that was because of this:
eid emp_ID%TYPE;
When using the %TYPE you have to specify the table name as well as the column name:
eid emp.emp_ID%TYPE;
If you were selecting all the columns in the row you could also look at %ROWTYPE.
Your approach was also making two assumptions: that the initial select into eid found the lowest ID, which is by no means guaranteed; and that all the subsequent ID values are sequential. And you're declaring and populating aRows but referring to Tot_Rows.

Resources