Converting if then to CASE stmt. Please let me know what mistake I m making here
DECLARE
salary NUMBER;
bonus NUMBER;
hdate DATE;
empno NUMBER;
BEGIN
SELECT hiredate INTO hdate FROM emp where empno=7788 ;
CASE hdate
WHEN hdate > TO_DATE('01-JAN-82') THEN bonus := 500 DBMS_OUTPUT.PUT_LINE(bonus);
WHEN hdate > TO_DATE('23-JAN-16') THEN bonus := 1000 DBMS_OUTPUT.PUT_LINE(bonus);
ELSE bonus := 1500 DBMS_OUTPUT.PUT_LINE(bonus);
END CASE;
END;
/
Use the following syntax for CASE:
DECLARE
salary NUMBER;
bonus NUMBER;
hdate DATE;
empno NUMBER;
BEGIN
SELECT sysdate INTO hdate FROM dual ;
CASE
WHEN hdate > TO_DATE('01-JAN-82') THEN bonus := 500;
WHEN hdate > TO_DATE('23-JAN-16') THEN bonus := 1000 ;
ELSE bonus := 1500 ;
END CASE;
DBMS_OUTPUT.PUT_LINE(bonus);
END;
Notice that the WHEN clauses can use different conditions rather than all testing the same variable or using the same operator.
Perhaps another proposition; instead of CASE statement in WHERE clause, it is rather in the SELECT list to deter the usage of hdate, thus a single SQL to achieve the desired output.
DECLARE
salary NUMBER;
p_bonus NUMBER;
hdate DATE;
empno NUMBER;
BEGIN
SELECT CASE
WHEN hiredate > TO_DATE ('01-JAN-82') THEN 500
WHEN hiredate > TO_DATE ('23-JAN-16') THEN 1000
ELSE 1500
END
INTO p_bonus
FROM emp
WHERE empno = 7788;
DBMS_OUTPUT.put_line (p_bonus);
END;
/
In addition to the other answers, there are a couple of other things wrong with your case statement.
Dates
When you use to_date to explicitly convert a string into a date, you should also use a format model to describe the format of the string. By not doing so, you rely on the default NLS_DATE_FORMAT parameter, which could well be different on different machines.
Also, years have 4 digits - use all of them, rather than make Oracle guess. Does the 2 digit year 16 mean 2016 or 1916?
Far better to be explicit, in both cases!
Therefore, your date conditions should actually be to_date('01-JAN-1982', 'dd-MON-yyyy', 'nls_date_language=english') and to_date('23-JAN-2016', 'dd-MON-yyyy').
Note the presence of the optional third parameter - I used that because you specified the month in words, and again, your NLS_DATE_LANGUAGE parameter might not be the same on someone else's machine. Adding the third parameter means the string will be converted to a date regardless of your NLS settings.
You can avoid the use of the 3rd parameter in to_date by using numbers for the day, month and year, e.g. to_date('23/01/2016', 'dd/mm/yyyy', 'nls_date_language=english').
CASE and logic short circuiting
CASE uses logic short circuiting, meaning that when it evaluates a condition to be true, it doesn't process any further conditions.
It seems like you intend a hiredate of 23rd Feb 2017 to get a bonus of 1000, but since it meets the first condition (it's later than 1st Jan 1982), it gets a bonus of 500.
Therefore, you need to change the order of the conditions, so that the most restrictive is at the top. In your case, your procedure becomes:
DECLARE
salary NUMBER;
bonus NUMBER;
hdate DATE;
empno NUMBER;
BEGIN
SELECT hiredate
INTO hdate
FROM emp
WHERE empno=7788;
CASE hdate
WHEN hdate > TO_DATE('23-JAN-16') THEN bonus := 1000;
WHEN hdate > TO_DATE('01-JAN-82') THEN bonus := 500;
ELSE bonus := 1500 DBMS_OUTPUT.PUT_LINE(bonus);
END CASE;
DBMS_OUTPUT.PUT_LINE(bonus);
END;
/
Related
I have a list of queries stored in an Oracle DB table. My requirement is to fetch each of those queries one by one and fire them in a procedure and log their start end and elapsed times in another table
My problem is how should I handle the column list as that's going to be different for each of those queries and the number of columns and their datatypes cannot be anticipated at runtime.
Please suggest a way out.
For now, I have written down the code below. Here I have enclosed each query fetched with a count() to avoid the problem. However, the actual time taken for the count() query will be different from the time taken for the original query to execute.
Thanks a lot!
DECLARE
before_time TIMESTAMP;
after_time TIMESTAMP;
elapsed_time_in_ms NUMBER;
stmnt CLOB; --varchar(32000);
counts NUMBER;
sql_no NUMBER;
err_mess VARCHAR2(100);
CURSOR get_queries
IS
SELECT * FROM SLOW_RUNNING_SQL WHERE curr_ind = 1;
FUNCTION get_elapsed_time(
start_time_in TIMESTAMP ,
end_time_in TIMESTAMP )
RETURN NUMBER
AS
l_days NUMBER;
hours NUMBER;
minutes NUMBER;
seconds NUMBER;
milliseconds NUMBER;
BEGIN
<calculates elapsed time in milliseconds and returns that>
RETURN milliseconds ;
END;
BEGIN
dbms_output.put_line(CURRENT_TIMESTAMP);
before_time := SYSTIMESTAMP;
FOR i IN get_queries
LOOP
stmnt := i.SQL_DESC;
sql_no := i.sql_no;
stmnt := 'SELECT count(*) FROM ('||stmnt||') a';
dbms_output.put_line(stmnt);
EXECUTE IMMEDIATE stmnt INTO counts;
after_time := SYSTIMESTAMP;
elapsed_time_in_ms:= get_elapsed_time(before_time,after_time);
dbms_output.put_line(elapsed_time_in_ms);
INSERT
INTO query_performance_log VALUES
(
i.sql_no,
stmnt,
counts,
before_time,
after_time,
elapsed_time_in_ms/1000,
'No exception',
elapsed_time_in_ms );
dbms_output.put_line(stmnt);
dbms_output.put_line(counts);
dbms_output.put_line(after_time);
dbms_output.put_line(TO_CHAR(after_time - before_time));
COMMIT;
END LOOP;
ROLLBACK;
EXCEPTION
WHEN OTHERS THEN
err_mess:= SQLERRM;
INSERT
INTO query_performance_log VALUES
(
sql_no,
stmnt,
0,
NULL,
NULL,
0,
err_mess,
0
);
dbms_output.put_line(SQLERRM);
ROLLBACK;
END;
A solution that might suit you is to select a constant for every line returned by your query and make a bulk collect INTO a collection of varchar2 variables.
Here is what you're looking for:
-- declare a list of varchar2:
CREATE OR REPLACE TYPE t_my_list AS TABLE OF VARCHAR2(100);
-- then use this type in your proc.:
[..]
declare
v_res t_my_list;
[..]
-- then run the query
execute immediate 'SELECT ''x'' FROM ('||stmnt||') '
bulk collect into v_res;
If the columns selected by your queries are "simple", the above should work fair enough to evaluate performances. But if you start calling other functions and procedures for the data you retrieve in the select, then its more complicate.
In this other case, then you should try to work something out to build a concatenation of the columns returned (and enlarge the VARCHAR2(100) in the declaration of t_my_list). This implies you start work on stmnt and extract the columns, a part being the replacement of , by ''||'' or so.
create or replace
function f_amt(date_in in varchar2) return number
as
BEGIN
DECLARE
v_at ES.AMT%TYPE;
i number := 0;
BEGIN
v_at := 0;
WHILE v_at = 0
LOOP
BEGIN
select nvl(AMT,0)
into v_at
from es
where date1 = to_date(date_in,'MM/DD/YYYY') - i;
i := i + 1;
EXCEPTION when NO_DATA_FOUND
then
v_at:=0;
END;
END LOOP;
RETURN v_at;
END;
EXCEPTION
WHEN OTHERS THEN
RETURN 0;
END;
ES table has date and amount, and I want to amount as o/p for latest date available in ES for date given.
Eg:
If date_in='20160223' and amount in ES is available for '20160220', then this amt should be returned in v_at and above while loop should exit. But it is going infinitely. Please suggest the correction in code required.
What happens if there is no prior value?
Wouldn't it be simpler, faster (and safer) to do:
select AMT
into v_at
from es
where date1 = (
select max(date1)
from es
where date1 <= to_date(date_in,'MM/DD/YYYY')
and AMT is not NULL
and AMT <> 0)
There is no loop, only two index seeks (provided there is an index on date1).
Also you don't mention if date1 is unique (but your code would also fail if not).
change:
EXCEPTION when NO_DATA_FOUND
then
v_at:=0;
with:
EXCEPTION when NO_DATA_FOUND
then
exit;
Infinite loop happens, because at some i there is always no_data_found exception and v_at is always 0. You can write it
CREATE OR REPLACE FUNCTION f_amt (date_in IN VARCHAR2)
RETURN NUMBER
AS
BEGIN
FOR c1 IN ( SELECT amt
FROM es
WHERE date1 <= TO_DATE (date_in, 'MM/DD/YYYY')
AND nvl(amt,0) <> 0
ORDER BY date1 DESC)
LOOP
RETURN c1.amt;
END LOOP;
RETURN 0;
END;
Try not to use EXCEPTION WHEN OTHERS. When OTHERS happens, you want to see it. And if a function is often called, try to avoid exceptions. They have to be actually exception. They are expensive. When you expect NO_DATA_FOUND, instead of select into open cursor and use %NOTFOUND or use for loop.
In case there is no date1 <= date_in you will search on forever. In order to find the last amt for date1 <= date_in you should use Oracle SQL's keep dense_rank last.
create or replace function f_amt(date_in in varchar2) return number as
v_amt es.amt%type;
begin
select max(amt) keep (dense_rank last order by date1)
into v_amt
from es
where date1 <= to_date(date_in,'mm/dd/yyyy')
and amt <> 0;
return v_amt;
exception when others then
return 0;
end;
As you see, the PL/SQL function is only needed now to react on invalid date strings. Otherwise pure SQL would suffice. You may want to consider validating the date string somewhere else in PL/SQL (and give a proper error message in case it is invalid) and then use the mere SQL query with the date got instead of a PL/SQL function. (See also Mottor's comment on WHEN OTHERS and my answer to that.)
I have a table that has a lot of columns. For some reason, I need to fetch data from it and then do some stuff on each column value separately. So I'd like to do something like this :
SELECT record FROM table
FOREACH field of the record
do some stuff
The "do some stuff" part shall do something according to the column name.
Is there an easy way to perform such a browse in PL/SQL ?
Thanks !
Unfortunately, you can't loop like this through fields of a record. However, you could use DBMS_SQL to loop through all the returned columns and do whatever you need. Check my simple example below based on this thread: Loop through columns of a record with DBMS_SQL
CREATE TABLE my_iter_tab_test (
id NUMBER,
name VARCHAR2(20),
salary NUMBER
);
INSERT INTO my_iter_tab_test VALUES (1, 'Smith', 5000);
INSERT INTO my_iter_tab_test VALUES (2, 'Brown', 6000);
COMMIT;
DECLARE
v_cur NUMBER;
v_temp NUMBER;
v_col_cnt NUMBER;
v_desc_tab_rec DBMS_SQL.DESC_TAB;
v_ret NUMBER;
v_v_val VARCHAR2(4000);
BEGIN
v_cur := DBMS_SQL.OPEN_CURSOR;
DBMS_SQL.PARSE(v_cur, 'SELECT * FROM my_iter_tab_test', DBMS_SQL.NATIVE);
v_temp := DBMS_SQL.EXECUTE(v_cur);
DBMS_SQL.DESCRIBE_COLUMNS(v_cur, v_col_cnt, v_desc_tab_rec);
FOR v_i IN 1..v_col_cnt
LOOP
dbms_output.put_line(v_desc_tab_rec(v_i).col_name);
DBMS_SQL.DEFINE_COLUMN(v_cur ,v_i, v_v_val, 2000);
END LOOP;
LOOP
v_ret := DBMS_SQL.FETCH_ROWS(v_cur);
EXIT WHEN v_ret = 0;
FOR v_i IN 1..v_col_cnt
LOOP
DBMS_SQL.COLUMN_VALUE(v_cur, v_i, v_v_val);
DBMS_OUTPUT.PUT_LINE(v_desc_tab_rec(v_i).col_name || ' : ' || v_v_val);
END LOOP;
END LOOP;
DBMS_SQL.CLOSE_CURSOR(v_cur);
END;
Output:
ID
NAME
SALARY
ID : 1
NAME : Smith
SALARY : 5000
ID : 2
NAME : Brown
SALARY : 6000
I guess you'll want to change the DEFINE_COLUMN to proper types in your final solution.
This question was asked in one practical exams at my university.
Q: A table has following columnheads/columns:
medicine_name,medicine_manufacture_date,medicine_expiry_date.
As the columnhead names describe they have the respective data about a medicine in them.
Now they asked to write a pl/sql block to make list of all medicines' names which have expired(i.e.current system date is greater than the expirydate values of the medicine).
I strongly think that the column of sequential consecutive integers(like 1,2,3,...n) must be added to the table, which will act as a primary key to each medicine name.Medicine names are unique but to perform iterative operations they should posses an integer primary key.
Is it still possible to get the result without assigning an ineteger primary key?
I altered the table and assigned primary key to each record and wrote following block... It worked
but i want to do it without assigning a primary key. All possible smart ways will be adored!Plz suggest correction of any type in my code... My code is as follows:
declare
a date;
b date;
diff number(10);
medicine varchar(25);
begin
a:=sysdate;
for i in 1..5
loop
select medicine_expiry_date into b from med_details where med_id=i ;
diff:=trunc(a-b);
if
diff>0 then
select medicine_name into medicine from med_details where med_id=i;
dbms_output.put_line(medicine);
end if;
end loop;
end;
/
Try
BEGIN
DBMS_OUTPUT.PUT_LINE('The following medicines have expired:');
FOR aRow IN (SELECT MEDICINE_NAME
FROM MEDICINE
WHERE EXPIRY_DATE < SYSDATE)
LOOP
DBMS_OUTPUT.PUT_LINE(aRow.MEDICINE_NAME);
END LOOP;
END;
You could put an artificial primary key on this table if there was some need to have some other table refer to this one and no other practical candidate key was available, but it's not needed to answer the question as asked above.
Share and enjoy.
It looks straightforward... I suspect that there are some extra details.
/*
Q: A table has following columnheads/columns:
medicine_name,
medicine_manufacture_date,
medicine_expiry_date.
Now they asked to write a pl/sql block to make list of all medicines' names which have expired
(i.e.current system date is greater than the expirydate values of the medicine).
*/
-- SQL:
SELECT medicine_name
FROM a_table
WHERE medicine_expiry_date < SYSDATE;
-- PL/SQL v.1:
DECLARE
TYPE medicine_ntt IS TABLE OF a_table%ROWTYPE;
l_medicine medicine_ntt;
BEGIN
SELECT medicine_name
, medicine_expiry_date
BULK COLLECT INTO l_medicine
FROM a_table;
FOR indx IN 1..l_medicine.COUNT LOOP
IF l_medicine(indx).medicine_expiry_date < SYSDATE THEN
DBMS_OUTPUT.PUT_LINE(l_medicine(indx).medicine_name);
END IF;
END LOOP;
END;
-- PL/SQL v.2:
DECLARE
CURSOR medicine_cur IS
SELECT medicine_name
, medicine_expiry_date
FROM a_table;
TYPE medicine_ntt IS TABLE OF medicine_cur%ROWTYPE;
l_medicine medicine_ntt;
l_medicine_expired medicine_ntt := medicine_ntt();
BEGIN
OPEN medicine_cur;
FETCH medicine_cur BULK COLLECT INTO l_medicine;
CLOSE medicine_cur;
FOR indx IN 1..l_medicine.COUNT LOOP
IF l_medicine(indx).medicine_expiry_date < SYSDATE THEN
l_medicine_expired.EXTEND;
l_medicine_expired(l_medicine_expired.LAST) := l_medicine(indx);
END IF;
END LOOP;
FOR indx IN 1..l_medicine_expired.COUNT LOOP
DBMS_OUTPUT.PUT_LINE(l_medicine_expired(indx).medicine_name);
END LOOP;
END;
I'm trying to write a simple PL/SQL procedure, but I keep getting errors I don't understand. there are some
syntax failures, and I would appreciate if someone could help me with them.
I need to create a procedure that will allow me insert in a table particular person(varchar2) with two dates(DATE), his contract duration.
However his contract CAN'T be between existing dates, thus can't overlap, same person can have number of contacts but only between completely different periods.
I understand that I need to use INTERSECT, it's just I'm confused how to use it here, since I we have only one table. I've intersection before with two tables, how do I intersect it here? I;m also not sure if I can have 2 if statements
Thank you!
CREATE OR REPLACE PROCEDURE test1
(name VARCHAR2,
startDate DATE,
endDate DATE)
AS
overlap NUMBER := 0;
CURSOR cursor IS
SELECT date_from, date_to FROM contractTable;
BEGIN
FOR row IN cursor LOOP
/*check if there is an overlap between dates*/
IF (startDate, endDate) INTERESECT (SELECT date_from, date_to FROM ContractTable) THEN
overlap := overlap + 1;
END IF;
/*if there isn't, then insert new contact for this person*/
IF overlap <= 0 THEN
INSERT INTO ContractTable VALUES(name, startDate, endDate);
END IF;
END test1;
CREATE PROCEDURE test1 (l_name IN VARCHAR2, l_start_date IN DATE, l_end_date IN DATE)
IS
l_overlap_check NUMBER;
BEGIN
SELECT count(*) INTO l_overlap_check
FROM contract
WHERE (l_start_date between start_date and end_date
OR
l_end_date between start_date and end_date)
AND l_name = name;
IF l_overlap_check = 0 THEN
INSERT INTO contract (name, start_date, end_date)
VALUES (l_name, l_start, l_end);
END IF;
END;
I don't have an Oracle environment handy but that should be what you need.