Using a function to format a single column value, and calling that function in a SQL statement - sql

I want to write a function which can be used to format a faculty member's salary (FACULTY being a table, where each row is a member) to $9,999,999.99 without hard coding the exact salary datatype (i.e the function should work even if in the future minor changes are made to the salary data size/type). I want to be able to use this function in a SQL statement for displaying a faculty member's salary.
Here is what my function code looks like right now.
CREATE OR REPLACE FUNCTION FUNC_FORMAT_SAL(fid IN FACULTY.F_ID%TYPE)
RETURN NUMBER IS
sal NUMBER;
BEGIN
SELECT FACULTY.F_SALARY INTO sal FROM FACULTY
WHERE FACULTY.F_ID = fid;
RETURN sal;
END;
I understand that in PLSQL a function cannot return a void right? That is why I have taken the member's F_ID as an IN input into the function. Inside this function, I want to format the SALARY for that member so that it could be displayed when I call an SQL statement, and then return the formatted salary.
Am I making any sense? This is a tutorial question in class and I just can seem to understand even though easy it appears to be.

I would say your function should return a VARCHAR2 if you are going to use it for display purpose. Taking a step further, it would be better to parameterise the format model as well. It should include exception handling such that when there is no data for the id it should return NULL and raise an exception for other errors( such as invalid format model).
CREATE OR REPLACE FUNCTION func_format_sal(p_fid IN faculty.f_id%TYPE,
p_format IN VARCHAR2 DEFAULT '$9,999,999.99')
RETURN VARCHAR2
IS
v_sal VARCHAR2(40);
BEGIN
SELECT TO_CHAR(f.f_salary, p_format)
INTO v_sal
FROM faculty f
WHERE f.f_id = p_fid;
RETURN v_sal;
EXCEPTION
WHEN no_data_found THEN
RETURN NULL;
WHEN OTHERS THEN
RAISE;
END;
/
You may call this function in 2 ways, assuming 1,2 are valid f_ids
select func_format_sal(1) FROM dual;
select func_format_sal(2,'$99999') FROM dual;
Here's a dbfiddle demo

Related

Having a hard time with procedure not formatting the query entry correctly

Create a dynamic procedure that will change the contents of any column for any row in the AA_EMPLOYEE table using the employee id. i.e.
BEGIN
dyn_aa_employee('emp_dob', '01-jan-18', 110);
END;
Will change the date of birth for employee ID 110
CREATE OR REPLACE PROCEDURE dyn_aa_employee
(p_col VARCHAR2,
p_dob IN aa_employee.emp_dob%TYPE,
p_id NUMBER)
IS
BEGIN
EXECUTE IMMEDIATE 'UPDATE aa_employee
SET '|| p_col ||' = :ph_dob
WHERE EMP_NUM = :ph_id'
USING p_dob, p_id;
BEGIN
dyn_aa_employee('emp_dob', '01-jan-18', 110);
END;
The top code has to work for the bottom code. The issue is it's changing the emp dob to 01-jan-0018, however I want it to change to exactly 01-jan-18.
My professor gave me a 0 for this assignment I'm just trying to figure out what I did wrong.
Assuming that aa_employee.emp_dob is of type date AND assuming that by '01-jan-18' you mean January 1st, 2018, either you do this:
BEGIN
dyn_aa_employee('emp_dob', date '2018-01-01', 110);
END;
or you could change your procedure to:
CREATE OR REPLACE PROCEDURE dyn_aa_employee (
p_col VARCHAR2,
p_dob IN aa_employee.emp_dob%TYPE,
p_id NUMBER)
IS
BEGIN
EXECUTE IMMEDIATE
'UPDATE aa_employee SET ' || p_col || ' = :ph_dob WHERE EMP_NUM = :ph_id'
USING TO_DATE (p_dob, 'DD-MON-YY'), p_id;
END;
It would be interesting to see the actual assignment, though. It's a rather confused scenario overall and I don't see how it teaches you much except to identify several things you probably shouldn't do.
Well first off you didn't specify the date format you were passing. Therefore accepting whatever format the professor had setup. This is a bad plan, to be safe always specify your date format and don't depend on the default. Defaults can and are changed too often. This is a good lesson why you should always specify the actual date format you're using. Try the following to see the difference:
with ds as
(select '01-jan-18' dt_stg from dual)
select to_date(dt_stg), to_date(dt_stg, 'dd-mon-rr') from ds;
Secondly, seems the assignment was to create a procedure that could update any column. This procedure can update only a column having the same type as "aa_employee.emp_dob%TYPE",presumable a date, but it cannot update any other column type.
Finally, if your trying to figure out what professor thinks you did wrong, then try something strange: ask your professor!

warning : function created with compilation error

Table EMP has ENAME as attribute.The following function gives error:
SET SERVEROUTPUT ON
SET ECHO ON
CREATE OR REPLACE FUNCTION count_emp(e_name varchar(20))
RETURN integer IS
total integer;
BEGIN
SELECT count(*) into total
FROM DEPARTMENTS
where ENAME = e_name;
RETURN total;
END;
/
warning:function created with compilation error.
You can run show errors; to see what compilation errors are.
The parameter's datatype should be specified without length. Also, use varchar2 instead of varchar.
From Oracle site:
The VARCHAR datatype is synonymous with the VARCHAR2 datatype. To avoid possible changes in behavior, always use the VARCHAR2 datatype to store variable-length character strings.
Try this:
CREATE OR REPLACE FUNCTION count_emp(e_name varchar2) -- here
RETURN integer IS
total integer;
BEGIN
SELECT count(*) into total
FROM DEPARTMENTS
where ENAME = e_name;
RETURN total;
END;
/
If you care about table EMP, you should use it in the function.
I would write this as:
CREATE OR REPLACE FUNCTION count_emp (
in_e_name varchar2
)
RETURN integer IS
v_total integer;
BEGIN
SELECT COUNT(*) into v_total
FROM EMP e
WHERE e.ENAME = in_e_name;
RETURN v_total;
END;
Notes:
Oracle will compile functions and stored procedures even when the objects don't (yet) exist. This is considered a "feature".
Use naming conventions to distinguish parameters and variables from columns. This is using in_ for the input parameters and v_ for the local variables.
Qualify all column name references. This further reduces the possibility of collision between a variable and column name.
You don't need a length for varchar2() inputs (which is preferable to varchar(), although perhaps one day, Oracle will cave to the standard).

Quickest way to find out if record exist

I have three functions which are all doing the same. I like to know whether SELECT ... FROM EMP WHERE DEPT_ID = v_dept returns any row.
Which one would be the fastest way?
CREATE OR REPLACE FUNCTION RecordsFound1(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
n INTEGER;
BEGIN
SELECT COUNT(*) INTO res FROM EMP WHERE DEPT_ID = v_dept;
RETURN n > 0;
END;
/
CREATE OR REPLACE FUNCTION RecordsFound2(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
CURSOR curEmp IS
SELECT DEPT_ID FROM EMP WHERE DEPT_ID = v_dept;
dept EMP.DEPT_ID%TYPE;
res BOOLEAN;
BEGIN
OPEN curEmp;
FETCH curEmp INTO dept;
res := curEmp%FOUND;
CLOSE curEmp;
RETURN res;
END;
/
CREATE OR REPLACE FUNCTION RecordsFound3(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
dept EMP.DEPT_ID%TYPE;
BEGIN
SELECT DEPT_ID INTO dept FROM EMP WHERE DEPT_ID = v_dept;
RETURN TRUE;
EXCEPTION
WHEN NO_DATA_FOUND THEN
RETURN FALSE;
WHEN TOO_MANY_ROWS THEN
RETURN TRUE;
END;
/
Assume table EMP is very big and condition WHERE DEPT_ID = v_dept could match on thousands of rows.
Usually I would expect RecordsFound2 to be the fastest, because it has to fetch (maximum) only one single row. So in terms of I/O it should be the best.
For the non-believers: the exists() version:
CREATE OR REPLACE FUNCTION RecordsFound0(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
BEGIN
RETURN EXISTS( SELECT 1 FROM EMP WHERE DEPT_ID = v_dept);
END;
The Postgresql version:
CREATE OR REPLACE FUNCTION RecordsFound0(v_dept IN EMP.DEPT_ID%TYPE) RETURNS BOOLEAN AS
$func$
BEGIN
RETURN EXISTS( SELECT 1 FROM EMP WHERE DEPT_ID = v_dept);
END
$func$ LANGUAGE plpgsql;
And in Postgres the function can be implemented in pure sql, without the need for plpgsql(in Postgres the select does not need a ... FROM DUAL
CREATE OR REPLACE FUNCTION RecordsFound0s(v_debt IN EMP.DEPT_ID%TYPE) RETURNS BOOLEAN AS
$func$
SELECT EXISTS( SELECT NULL FROM EMP WHERE DEPT_ID = v_debt);
$func$ LANGUAGE sql;
Note: the unary EXISTS(...) operator yields a Boolean, which is exactly what you want.
Note2: I hope I have the Oracle syntax correct. (keywords RETURN <-->RETURNS and AS <-->IS)
Your solution 1 Count all occurrences:
You have the DBMS do much more work than needed. why let it scan the table and count all occurences when you only want to know whether at least one exists or not? This is slow. (But on a small emp table with an index on dept_id this may still look fast :-)
Your solution 2 Open a Cursor and only fetch the first record
A good idea and probably rather fast, as you stop, once you found a record. However, the DBMS doesn't know that you only want to look for the mere existence and may decide for a slow execution plan, as it expects you to fetch all matches.
Your solution 3 Fetch the one record or get an exception
This may be a tad faster, as the DBMS expects to find one record only. However, it must test for further matches in order to raise TOO_MANY_ROWS in case. So in spite of having found a record already it must look on.
solution 4 Use COUNT and ROWNUM
By adding AND ROWNUM = 1 you show the DBMS that you want one record only. At a minimum the DBMS knows it can stop at some point, at best it even notices that it is only one record needed. So depending on the implementation the DBMS may find the optimal execution plan.
solution 5 Use EXISTS
EXISTS is made to check for mere existence, so the DBMS can find the optimal execution plan. EXISTS is an SQL word, not a PL/SQL word and the SQL engine doesn't know BOOLEAN, so the function gets a bit clumsy:
CREATE OR REPLACE FUNCTION RecordsFound1(v_dept IN EMP.DEPT_ID%TYPE) RETURN BOOLEAN IS
v_1_is_yes_0_is_no INTEGER;
BEGIN
SELECT COUNT(*) INTO v_1_is_yes_0_is_no
FROM DUAL
WHERE EXISTS (SELECT * FROM EMP WHERE DEPT_ID = v_dept);
RETURN n = 1;
END;
The absolute fastest ways is to not call the count function at all.
A typical pattern is
count the number of rows
if cnt = 0 then do something
else read chunk of data and process
Simple read the data and than perform the count test on it.
you can break a SQL by adding an extra condition to your where-clause:
where ...
and rownum = 1;
this stops immediatly if at least one record is found and it is as fast as the "exists" operator.
See the followint sample code:
create or replace function test_record_exists(pi_some_parameter in varchar2) return boolean is
l_dummy varchar2(10);
begin
select 'x'
into l_dummy
from <your table>
where <column where you want to filter for> = pi_some_parameter
and rownum = 1;
return (true);
exception
when no_data_found then
return (false);
end;
If you use:
select count(*)
from my_table
where ...
and rownum = 1;
... then the query will:
be executed in the most efficient fashion
always return a single row
return either 0 or 1
This three factors make it very fast and very easy to use in PL/SQL as you do not have to concern yourself with whether a row is returned or not.
The returned value is also amenable to use as a true/false boolean, of course.
If you wanted to list the departments that either do or do not have any records in the emp table then I would certainly use EXISTS, as the semi-(anti)join is the most efficient means of executing the query:
select *
from dept
where [NOT] exists (
select null
from emp
where emp.dept_id = dept.id);

PL/SQL stored function return with row type

CREATE OR REPLACE FUNCTION lab( f_name IN VARCHAR2 )
RETURN test%ROWTYPE
IS
total NUMBER := 0;
CURSOR c_app IS
SELECT count(*),LISTAGG(s.sname,',') WITHIN GROUP (ORDER BY s.sname)
FROM APPLICANT a INNER JOIN SPOSSESSED s ON a.A# = s.A#
WHERE a.fname = f_name;
rec_app c_app%ROWTYPE;
BEGIN
OPEN c_app;
LOOP
FETCH c_app into rec_app;
EXIT WHEN c_app%NOTFOUND;
END LOOP;
CLOSE c_app;
RETURN rec_app;
END lab;
/
Fail to compile with errors that expression wrong type?
Isn't it possible to return with rowtype result?
for example i run this function
select lab(fname) from position where fname='PETER';
so the result will be display like
PETER : aaaa,bbbb,cccc
You're declaring the return as test%rowtype, then trying to return rec_app, which is declared as c_app%rowtype - so the types don't match. You can't do that.
c_app is only in scope within this function so it would not have any meaning for any callers, and you can't use it as the return type. You can return something that is actually test%rowtype, assuming test is a table, but not an arbitrary different type. It isn't clear that there is any relationship at all between your cursor and its row type, and the test table.
You're also looping round to potentially fetch multiple rows, but only returning the last one (or trying to, anyway), which probably isn't what you mean to do.
The simplest way to get all the cursor rows back to the caller is with a ref cursor:
CREATE OR REPLACE FUNCTION lab( f_name IN VARCHAR2 )
RETURN SYS_REFCURSOR
IS
ref_cur SYS_REFCURSOR;
BEGIN
OPEN ref_cur FOR
SELECT count(*),LISTAGG(s.sname,',') WITHIN GROUP (ORDER BY s.sname)
FROM APPLICANT a INNER JOIN SPOSSESSED s ON a.A# = s.A#
WHERE a.fname = f_name;
RETURN ref_cur;
END lab;
/
If you create an external type you could use PIPELINED but that doesn't appear necessary here. neither is quite using a %rowtype though. You can only return a %rowtype if you have a table that has the columns you want to return.

Oracle Stored Procedure 'Learnings' issue

I have been tasked (as part of an assignment) to write a stored procedure in Oracle PL/SQL. There are 3 requirements that have to be met.
There must be 2 parameters, 1 IN and 1 OUT.
I must use an implicit cursor and SQL function to calculate a count of the numbers of fields of the same type (in this case the type is car models, so how many cars of each model are there).
I must use another implicit cursor to display the description of the models.
To be honest, I am at a loss. So far for the stored proc I have:
CREATE OR REPLACE Procedure model_details_sp
(p_model IN VARCHAR2,
p_noofcars OUT NUMBER)
IS
BEGIN
SELECT COUNT(Model_Name) INTO p_noofcars
FROM i_car
GROUP BY Model_Name;
END;
I really have no idea where to go from here. Any advice or direction would be most appreciated.
Many thanks.
Hi guys I appreciate all the comments. I wasn't very clear with the end requirements. I want to be able to call this procedure via an anonymous block so that the user will enter a model type (&vairalbe) and the procedure will display how many of that model types are in the database.
When dealing with this type of problems, first think about the data you're trying to capture.
Dealing with implicit cursors in PL/SQL require 1-row, so you need to make sure you understand the data.
In this case, you pass in a variable that you don't use in any of your queries, so I suggest you re-evaluate.
I don't have a database at hand to run this, but you should be able to work this out and hopefully get you a bit closer. I put it in an anonymous block so that I can write it really quick.
DECLARE
PROCEDURE model_details_sp (p_model IN VARCHAR2, p_noofcars OUT NUMBER)
IS
p_description VARCHAR2 (200);
BEGIN
--2
SELECT COUNT (model_name)
INTO p_noofcars
FROM i_car
WHERE model_name = p_model;
DBMS_OUTPUT.put_line ('No of Cars for model: ' || p_noofcars);
--3
SELECT model_description
INTO p_description
FROM i_car --the table should be the car_model table so that only one record is returned
WHERE model_name = p_model;
DBMS_OUTPUT.put_line ('Model Desc' || p_description);
END model_details_sp;
BEGIN
dbms_output.put_line('');
END;
To #David Aldridge comment:
Try running this--the result should be a failure--as you cannont select multiple rows using the into CLAUS, unless you aggregate the data:
DECLARE
p_num NUMBER;
BEGIN
SELECT LEVEL INTO p_num FROM DUAL CONNECT BY LEVEL <= 10;
dbms_output.put(p_num);
END;
The error you should see is this:
Error report:
ORA-01422: exact fetch returns more than requested number of rows
ORA-06512: at line 4
01422. 00000 - "exact fetch returns more than requested number of rows"
*Cause: The number specified in exact fetch is less than the rows returned.
*Action: Rewrite the query or change number of rows requested
From the description and subsequent comments, this is the solution I would provide:
DECLARE
PROCEDURE model_details_sp
(p_model IN VARCHAR2,
p_noofcars OUT NUMBER)
IS
BEGIN
SELECT COUNT(*)
INTO p_noofcars
FROM i_car
WHERE model_name = p_model;
END;
no_of_cars NUMBER := 0;
BEGIN
model_details_sp(:model_name, no_of_cars);
dbms_output.put_line('no of cars for ' || :model_name || ' = ' || no_of_cars);
END;
I've created the PROCEDURE in-line but you can just as easily extract this to the database by removing it from the declare section and executing it with CREATE OR REPLACE.
This example assumes use of an IDE that supports bind variable replacement (:model_name) on execute of the anonymous block. In TOAD, for example, the "user" will be prompted to provide a value for :model_name.