Not enough values selecting into a table value - sql

Here is the code:
create type emp_high_sal_ot is object
(
full_name varchar2(64),
phone_number varchar(20),
salary number(10,2)
);
create type emp_high_sal_nt is table of emp_high_sal_ot;
and the following function doesn't compile:
create or replace function get_highest_paid_emps return emp_high_sal_nt
AS
rec emp_high_sal_nt;
avg_sal NUMBER;
begin
SELECT AVG(salary)
INTO avg_sal
FROM employees;
SELECT last_name || ' ' || first_name, phone_number, salary
INTO rec
FROM employees
WHERE salary > avg_sal;
return rec;
end;
It says:
PL/SQL: ORA-00947: not enough values
Why is that?

Why is that?
You are trying to put 3 values (and multiple rows) into a single variable.
Use BULK COLLECT INTO and wrap the values in the object type:
create or replace function get_highest_paid_emps return emp_high_sal_nt
AS
rec emp_high_sal_nt;
avg_sal NUMBER;
begin
SELECT AVG(salary)
INTO avg_sal
FROM employees;
SELECT emp_high_sal_ot(
last_name || ' ' || first_name,
phone_number,
salary
)
BULK COLLECT INTO rec
FROM employees
WHERE salary > avg_sal;
return rec;
end;
/
Given the sample data:
CREATE TABLE employees (first_name, last_name, phone_number, salary) AS
SELECT 'Alice', 'Abbot', '0123456', 100000 FROM DUAL UNION ALL
SELECT 'Betty', 'Baron', '1111111', 250000 FROM DUAL UNION ALL
SELECT 'Carol', 'Chris', '9876543', 300000 FROM DUAL;
Then:
SELECT * FROM TABLE(get_highest_paid_emps());
Outputs:
FULL_NAME
PHONE_NUMBER
SALARY
Baron Betty
1111111
250000
Chris Carol
9876543
300000
db<>fiddle here

Related

Oracle PLSQL problems creating a procedure

I am trying to wrap some SQL into a PLSQL procedure so a user can pass parameters instead of manually editing a WHERE clause, which would give them the potential to break the working code. The SQL code, which I'm porting is embedded in the PROCEDURE with the exception of the INTO clause.
I know in PLSQL in order to SELECT rows there needs to be an INTO clause. After looking around I saw an example, which creates an object and table type. Something I didn't want to do and seems overly complicated to me. If possible I want to keep everything local to the procedure.
I'm also open to perhaps using BULK collect on the access_history table if that would be a more efficient method.
When I try creating the procedure below It doesn't work and this is where I can use some help and PLSQL expertise to guide me in the best direction to produce the desired data.
Secondly, is there a way to use a DEFAULT value to determine the number of rows to be retrieved.
If the procedure is called like this:
EXEC LAST_EMPLOYEE_HISTORY(1) this means get the last 20 (DEFAULT) rows for employee_id=1, where 20 is the default value.
If the procedure is called like this:
EXEC LAST_EMPLOYEE_HISTORY(1, 50) means get the last 50 rows for employee_id=1.
Any help and expertise in explaininf and helping me fix my issues would be greatly appreciated. Thanks in advance to all who answer.
Below is my test CASE.
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE access_history_obj AS OBJECT(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
location_id NUMBER(6),
location_name VARCHAR2(30),
access_date DATE
);
CREATE OR REPLACE TYPE access_history_table IS TABLE OF access_history_obj;
Create table employees(
employee_id NUMBER(6),
first_name VARCHAR2(20),
last_name VARCHAR2(20),
card_num VARCHAR2(10),
work_days VARCHAR2(7)
);
INSERT INTO employees (
employee_id,
first_name,
last_name,
card_num,
work_days
)
WITH names AS (
SELECT 1, 'John', 'Doe', 'D564311','YYYYYNN' FROM dual UNION ALL
SELECT 2, 'Justin', 'Case', 'C224311','YYYYYNN' FROM dual UNION ALL
SELECT 3, 'Mike', 'Jones', 'J288811','YYYYYNN' FROM dual UNION ALL
SELECT 4, 'Jane', 'Smith', 'S564661','YYYYYNN' FROM dual
) SELECT * FROM names;
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 5;
create table access_history(
seq_num integer GENERATED BY DEFAULT AS IDENTITY (START WITH 1) NOT NULL,
employee_id NUMBER(6),
card_num varchar2(10),
location_id number(4),
access_date date,
processed NUMBER(1) default 0
);
INSERT INTO access_history(
employee_id,
card_num,
location_id,
access_date
)
WITH rws AS (
SELECT 1,'J11111',2,TO_DATE('2021/08/15 08:30:25', 'YYYY/MM/DD HH24:MI:SS') FROM dual UNION ALL
SELECT 1,'J11111',3,TO_DATE('2021/08/15 18:30:35', 'YYYY/MM/DD HH24:MI:SS') FROM dual UNION ALL
SELECT 2,'E11111',2,TO_DATE('2021/08/15 11:20:35', 'YYYY/MM/DD HH24:MI:SS') FROM dual) SELECT * FROM rws;
CREATE OR REPLACE PROCEDURE LAST_EMPLOYEE_HISTORY(
p_employee_id IN NUMBER,
p_rws IN number)
AS
BEGIN
with rws as (
select e.employee_id,
e.first_name,
e.last_name,
e.card_num,
l.location_id,
l.location_name,
a.access_date,
row_number () over
(
partition by e.employee_id
order by a.access_date DESC
) rn
FROM employees e
JOIN access_history a ON a.employee_id = e.employee_id
JOIN locations l ON l.location_id = a.location_id
)
select employee_id,
first_name,
last_name,
card_num,
location_id,
location_name,
access_date INTO access_history_table
from rws
where
employee_id = p_employee_id AND
rn <= p_rws
order by employee_id, access_date desc;
END;
EXEC LAST_EMPLOYEE_HISTORY(1)
Have a cursor as an OUT parameter and use DEFAULT in the signature of the procedure:
CREATE PROCEDURE LAST_EMPLOYEE_HISTORY(
i_employee_id IN EMPLOYEES.EMPLOYEE_ID%TYPE,
i_rws IN PLS_INTEGER DEFAULT 20,
o_cursor OUT SYS_REFCURSOR
)
AS
BEGIN
OPEN o_cursor FOR
SELECT e.employee_id,
e.first_name,
e.last_name,
e.card_num,
l.location_id,
l.location_name,
a.access_date
FROM employees e
INNER JOIN access_history a
ON a.employee_id = e.employee_id
INNER JOIN locations l
ON l.location_id = a.location_id
WHERE e.employee_id = i_employee_id
ORDER BY access_date DESC
FETCH FIRST i_rws ROWS ONLY;
END;
/
Then in SQL/Plus or SQL Developer:
VARIABLE cur REFCURSOR;
EXECUTE LAST_EMPLOYEE_HISTORY(1, 50, :cur);
PRINT cur;
db<>fiddle here
Note: From Oracle 12, you can the use FETCH FIRST n ROWS ONLY syntax.

PL/SQL procedure that should output the following

Just cant get the output correct, maybe a loop problem ?
Have tried a few things but
Just cant get the output correct
should look like
Department 1 SALES managed by Alvin
Total number of employees: 5
00110 Alvin
00103 Ami
00109 Michael
00101 Peter
00107 Wendy
Department 2 ACCOUNTING managed by Alice
Total number of employees: 2
00120 Alice
00125 Angela
Department 3 GAMES managed by Bob
Total number of employees: 2
00150 Bob
00105 Robert
CREATE TABLE Department (
D# NUMBER(5) NOT NULL, /* Department number */
DName VARCHAR2(30) NOT NULL, /* Department name */
Manager# CHAR(5) NOT NULL, /* Department manager number */
MSDate DATE, /* Manager start date */
CONSTRAINT Department_PK PRIMARY KEY(D#),
CONSTRAINT Department_CK UNIQUE(DName)
);
CREATE TABLE Employee (
E# CHAR(5) NOT NULL, /* Employee number */
Name VARCHAR2(30) NOT NULL, /* Employee name */
DOB Date, /* Date of birth */
Address VARCHAR2(50), /* Home address */
Sex CHAR, /* M-Male, F-Female */
Salary NUMBER(7,2), /* Salary */
Super# CHAR(5), /* Supervisor number */
D# NUMBER(5), /* Department number */
CONSTRAINT Employee_PK PRIMARY KEY(E#),
CONSTRAINT Employee_FK1 FOREIGN KEY (Super#) REFERENCES Employee(E#),
CONSTRAINT Employee_FK2 FOREIGN KEY (D#) REFERENCES Department (D#)
);
ARE THE TABLES
THE CODE I HAVE BEEN TRYING IS
CREATE OR REPLACE PROCEDURE
INSERT_MANAGER IS
MANAGER_NAME VARCHAR(40);
DEPT_# NUMBER(5);
DEPT_NAME VARCHAR(40);
EMP_# NUMBER(5);
EMP_NAME VARCHAR(40);
EMP_TOTAL NUMBER(6);
CURSOR MANAGER IS
SELECT Name, Department.D#,DName
INTO MANAGER_NAME, DEPT_#, DEPT_NAME
FROM Employee
JOIN Department
ON
Department.D# = Employee.D#
WHERE E# = Manager#
ORDER BY DEPT_# ASC;
CURSOR EMPLOYEE IS
SELECT COUNT(NAME),NAME,E#
INTO EMP_TOTAL,EMP_NAME,EMP_#
FROM Employee
JOIN Department ON
Department.D# = Employee.D#
WHERE E# = Manager#
GROUP BY NAME,E#;
BEGIN
OPEN MANAGER;
OPEN EMPLOYEE;
LOOP
FETCH MANAGER INTO MANAGER_NAME, DEPT_#, DEPT_NAME;
FETCH EMPLOYEE INTO EMP_TOTAL,EMP_NAME,EMP_#;
EXIT WHEN MANAGER%notfound;
DBMS_OUTPUT.PUT_LINE('Department ' || DEPT_# || ' ' || DEPT_NAME || ' Managed By: ' || MANAGER_NAME );
DBMS_OUTPUT.PUT_LINE('Total Number Of Emploees ' || EMP_TOTAL);
DBMS_OUTPUT.PUT_LINE(EMP_# || ' ' || EMP_NAME);
END LOOP;
CLOSE MANAGER;
END;
/
My current output lhas the correct manager
but the incorrect TOTAL NUMBER OF EMPLOYEES
and is only showing 1 of the Employees under that manager
Department 1 SALES Managed By: Alvin
Total Number Of Emploees 1
150 Bob
Department 2 ACCOUNTING Managed By: Alice
Total Number Of Emploees 1
338 Helmus
Department 3 GAMES Managed By: Bob
Total Number Of Emploees 1
110 Alvin
THANKS IN ADVANCE FOR ANY HELP.
LUKE
The following produces your desired output while for the most part maintaining your logic flow (except for a 2nd loop for the Employee cursor). But I have made a couple syntactical changes.
First, names are important. Attempt to name objects according to the function being preformed. For example the MANAGER cursor really gets nothing about the manager (except for name). What is gets is information about the department; also
renamed the employee cursor (just because 2 objects with the same name - cursor and table). Structurally, I moved getting the employee count to the department cursor (manager) as it pertains to the department not the employee. As far as naming goes the procedure name itself is misleading as the procedure has nothing to do with inserting managers (but I let it); unless of course it's just part of a larger process.
Definition consistence is also important, and there are problems with that here. For example: Look at table column employee.name vs. the variable manager_name. And what's with employee.e# vs. emp_#? A method to stay consistent is to anchor procedure variables to the table columns they represent.
So instead of "manager_name varchar(40)" use "manager_name employee.name%type"
BTW: Varchar is frowned upon use varchar2.
But I left these with your original definitions. (Just for fun I played around this the data values a little - just values not structure.) The result then:
create or replace procedure insert_manager is
-- department cursor variables
manager_name varchar(40);
dept_# number(5);
dept_name varchar(40);
emp_total number(6);
-- employee cursor variables
emp_# number(5);
emp_name varchar(40);
-- cursors definitions
cursor department_cur is
select name, d#, dname, demps
from ( select e.name
, d.d#
, d.dname
, count(e.e#) over (partition by d.d#) demps
, e.e#
, d.manager# m#
from employee e
join department d on d.d# = e.d#
)
where e# = m#
order by d# asc;
cursor employee_cur(ed# integer) is
select e.name, e.e#
from employee e
where e.d# = ed#;
begin
dbms_output.put_line( 'Chipmunks Gaming, LLC: Department Employee List as of ' || to_char(sysdate, 'dd-Mon-yyyy'));
open department_cur;
loop
fetch department_cur into manager_name, dept_#, dept_name, emp_total;
exit when department_cur%notfound;
dbms_output.put_line('Department ' || dept_# || ' ' || dept_name || ' Managed By: ' || manager_name);
dbms_output.put_line('Total Number Of Employees ' || emp_total);
open employee_cur (dept_#);
loop
fetch employee_cur into emp_name, emp_#;
exit when employee_cur%notfound;
dbms_output.put_line(emp_# || ' ' || emp_name);
end loop;
close employee_cur;
end loop;
close department_cur;
end insert_manager;
-- Generate test data
insert into Department (D#, DName, Manager#)
select 1, 'SALES', '00150' from dual union all
select 2, 'ACCOUNTING','00120' from dual union all
select 3, 'GAMES', '00110' from dual ;
insert into employee(E#, Name ,D# )
select '00150', 'Simon' ,1 from dual union all
select '00103', 'Ami' ,1 from dual union all
select '00109', 'Michael' ,1 from dual union all
select '00101', 'Peter' ,1 from dual union all
select '00107', 'Wendy' ,1 from dual union all
select '00120', 'Theodore' ,2 from dual union all
select '00125', 'Angela' ,2 from dual union all
select '00110', 'Alvin' ,3 from dual union all
select '00105', 'Robert' ,3 from dual ;
---------------------------------------------------------------------------------
-- run it
begin
insert_manager;
end;
The next big step would be using Implicit cursors (cursor for loops) instead of Explicit cursors. That would take of Opening, Looping through, and Closing the cursors. I'll leave that for you.
Good Luck. Hope this helps.

querying a function that returns record in oracle

How do I see the values returned by a function that returns record datatype ?
Here is the function
TYPE employee_record_info IS RECORD (
employee_id NUMBER,
employee_name VARCHAR2(100),
manager_id NUMBER,
location VARCHAR2(100)
);
FUNCTION function1(in_employee_id NUMBER) RETURN employee_record_info AS
l_record employee_record_info;
BEGIN
SELECT employee_id, employee_name, manager_id, location
INTO
l_record
FROM all_employees where employee_id = in_employee_id;
RETURN l_record;
END function1;
I tried
select * from table(function1(123));
and
select function1(123) from dual;
I am getting invalid type error in both cases ? Is there a way to get these values.
I just need this to test my function and this is not for using in any code.
Also need some help in the case where function is returning array please.
Thanks.
Your code is for pl/sql, you won't be able to use SQL to select. You could rewrite it to make it workable for SQL selects, something like:
SQL> create or replace type emp_rec_typ as object(
employee_id NUMBER,
employee_name VARCHAR2(100),
manager_id NUMBER,
location VARCHAR2(100)
);
Type created.
SQL> create or replace type emp_tab_typ as table of emp_rec_typ;
Type created.
SQL> create or replace function get_emps
return emp_tab_typ
as
emps emp_tab_typ := emp_tab_typ();
begin
select emp_rec_typ(x.employee_id, x.employee_name, x.manager_id, x.location)
bulk collect into emps
from (
select 1 as employee_id, 'Joe Blow' as employee_name, 1 as manager_id, 'Some Place' as location from dual
union all
select 2 as employee_id, 'Jane Doe' as employee_name, 1 as manager_id, 'Some Other Place' as location from dual
union all
select 3 as employee_id, 'Fred Smith' as employee_name, 2 as manager_id, 'Some Strange Place' as location from dual
) x;
return emps;
end;
Function created.
SQL> select * from table(get_emps);
EMPLOYEE_ID EMPLOYEE_NAME MANAGER_ID LOCATION
1 'Joe Blow' 1 'Some Place'
2 'Jane Doe' 1 'Some Other Place'
3 'Fred Smith' 2 'Some Strange Place'

Check if there is a location_id and manager_id with the same name (they are from different tables)

So on that code I checked the uniqueness of department_id and department_name. Now I would like to do the same with manager_id and locations_id. Please keep in mind that manager_id is from table EMPLOYEES and locations_id is from table LOCATIONS.
I wonder if I can just continue that statement:
upper(s.department_id) = upper(d.department_id)
OR upper(s.department_name) = upper(d.department_name)
But I think that will not be enough, because it will check only in the DEPARTMENTS table. Not from LOCATIONS and EMPLOYEES like I want. Please advise.
CREATE OR REPLACE PROCEDURE add_dep(p_id NUMBER,
p_name VARCHAR2,
p_mgr NUMBER,
p_loc NUMBER) IS
BEGIN
MERGE INTO departments d
USING (
SELECT
p_id department_id,
p_name department_name,
p_mgr manager_id,
p_loc location_id
FROM dual) s
ON ( upper(s.department_id) = upper(d.department_id)
OR upper(s.department_name) = upper(d.department_name))
WHEN NOT MATCHED THEN
INSERT VALUES (s.department_id, s.department_name, s.manager_id, s.location_id);
IF SQL%ROWCOUNT = 0 THEN
INSERT INTO error_depa VALUES (p_id, p_name, p_mgr, p_loc);
END IF;
END;
EDIT - additional information
I got an error that there are not enough values for LOCATIONS and EMPLOYEES table. But I also tried to make this with exception and no_data_found. Can you modify this code and help me with that please? I would like to check the same for locations_id and manager_id.
create or replace procedure add_de(
p_id NUMBER,
p_name VARCHAR2,
p_mgr NUMBER,
p_loc NUMBER
)
is
v_dummy number;
begin
select 1
into v_dummy
from departments
where department_name = p_name OR DEPARTMENT_ID = p_id ;
insert
into error_depa
values(
p_id,
p_name,
p_mgr,
p_loc
);
exception
when no_data_found
then
insert
into departments
values(
p_id,
upper(p_name),
p_mgr,
p_loc
);
end;
I think that you want something like this:
CREATE OR REPLACE PROCEDURE add_dep(p_id NUMBER,
p_name VARCHAR2,
p_mgr NUMBER,
p_loc NUMBER) IS
BEGIN
MERGE INTO departments d
USING (
SELECT
p_id department_id,
p_name department_name,
p_mgr manager_id,
p_loc location_id
FROM dual) s
ON ( upper(s.department_id) = upper(d.department_id)
OR upper(s.department_name) = upper(d.department_name))
WHEN NOT MATCHED THEN
***INSERT(department_id, department_name,manager_id,location_id)*** VALUES (s.department_id, s.department_name, s.manager_id, s.location_id);
COMMIT;
MERGE INTO employees trg
USING (
SELECT
*
FROM employees
where
manager_id = p_mgr) src
ON ( upper(trg.manager_id) = upper(src.manager_id))
WHEN NOT MATCHED THEN
***INSERT(department_id, ...)*** VALUES (src.department_id, ....);
COMMIT;
MERGE INTO locations trg
USING (
SELECT
*
FROM locations
where
locations_id = p_loc) src
ON ( upper(trg.locations_id) = upper(src.locations_id))
WHEN NOT MATCHED THEN
***INSERT(locations_id, ...)*** VALUES (src.locations_id, ....);
COMMIT;
IF SQL%ROWCOUNT = 0 THEN
INSERT INTO error_depa VALUES (p_id, p_name, p_mgr, p_loc);
END IF;
END;
/

PL/SQL help. How to write a anonymous block that inserts 100 new rows

I am new to PL/SQL (and programming in general) and been set a few tasks. I am working my way through them and getting on OKish but am stumped with this particular task. I using the Oracle Application Express and have created the table that I am working with which is called emp2
Here is the task in it's entirety:
Write a PL/SQL anonymous block that inserts 100 (new) employee IDs,
starting at number 2000. Use a FOR loop, and declaration block for
defining the lower/upper limits of the loop. In addition to
the employee IDs, also add code that inserts placeholders in
the first_name and last_name columns (avoid NULLs!), eg
"FName_2000" and "LName_2000" for employee ID 2000.
(Hint: Use the concatenation operator).
This is the code that I have written so far (which I thought was spot on) minus the VALUES, as I just could not get this part to work?
DECLARE
lower constant pls_integer := 2000;
upper constant pls_integer := 2099;
BEGIN
FOR i in lower..upper
LOOP
INSERT INTO emp2 ( EMPLOYEE_ID, FIRST_NAME, LAST_NAME, HIRE_DATE, SALARY,
DEPARTMENT_ID )
VALUES ( //UNSURE WHAT GOES HERE! );
DBMS_OUTPUT.PUT_LINE('Inserted' || SQL%ROWCOUNT || ' row');
END LOOP;
END;
Your insert statement should look like this:
INSERT INTO emp2
( EMPLOYEE_ID, FIRST_NAME, LAST_NAME, HIRE_DATE, SALARY, DEPARTMENT_ID )
VALUES
( i, 'Fname', 'Lname', sysdate, 100, 10 );
You need to add an IF statement for the part "also add code that inserts placeholders in the first_name and last_name columns for employee ID 2000". Like this:
IF i = 2000
THEN
INSERT INTO emp2
( EMPLOYEE_ID, FIRST_NAME, LAST_NAME, HIRE_DATE, SALARY, DEPARTMENT_ID )
VALUES
( i, 'Fname ' || i, 'Lname ' || i, sysdate, 100, 10 );
ELSE
INSERT INTO emp2
( EMPLOYEE_ID, FIRST_NAME, LAST_NAME, HIRE_DATE, SALARY, DEPARTMENT_ID )
VALUES
( i, 'Fname', 'Lname', sysdate, 100, 10 );
END IF;