How to use a WHILE loop in PL/SQL with decrementing - plsql

I've been trying to do a lot of research on this problem I need to do for a class, but I'm not having much luck. IE, I can't find a good example. I'm an example person.
Create a PL/SQL block that uses a WHILE loop structure to generate a payment schedule for a donor's pledge, which is to be paid monthly in equal increments. Values available for the block are starting payment due date, monthly payment amount, and number of total monthly payments for the pledge. The list that is generated should display a line for each monthly payment showing payment number, date due, payment amount and donation balance (remaining amount of pledge owed).
Instead of displaying the donation balance on each line of output, display the total paid to date.
After some consideration, it's obvious that the payments have to be decremented on a monthly basis until the balance is zero at which the loop will exit. But I am not sure how to do it properly. I've looked all over the Internet and there's nothing that seems to help me finish the pl/sql block, example wise.
This is what I have so far.
declare
lv_paymentnumber_num number(3,0);
lv_paymentamount_num number(4,2);
lv_datepaymentpaid_date date;
lv_amountpaidtodate_num number(4,2);
lv_balanceremaining_num number(4,2);
lv_nextduedate_date date;
begin
lv_balanceremaining_num :-lv_paymentamount_num - lv_amountpaidtodate_num;
dbms_output.put_line(lv_balanceremaining_num);
loop
after that, that's where I get lost.

To terminate a loop use the EXIT statement as demonstrated below:
declare
lv_paymentnumber_num number(3,0) := 0;
lv_paymentamount_num number(4,2);
lv_datepaymentpaid_date date;
lv_amountpaidtodate_num number(4,2);
lv_balanceremaining_num number(4,2);
lv_nextduedate_date date;
nPayment NUMBER;
begin
loop
nPayment := LEAST(lv_paymentamount_num, lv_balanceremaining_num);
lv_balanceremaining_num := lv_balanceremaining_num - nPayment;
lv_amountpaidtodate_num := lv_amountpaidtodate_num + nPayment;
lv_paymentnumber_num := lv_paymentnumber_num + 1;
dbms_output.put_line('Payment = ' || nPayment ||
' Balance remaining = ' ||
lv_balanceremaining_num);
IF lv_balanceremaining_num <= 0 THEN
EXIT;
END IF;
END LOOP;
END;
Amend as needed.
Share and enjoy.

Related

PL/SQL if then else statements not running

I have written following code in oracle pl/sql
create or replace procedure sorting_criteria(criteria in varchar)
as
begin
if(criteria='lowest price')
then
declare
p_name product.p_name%type;
cursor ptr is select p_name from product order by amount ASC;
begin
open ptr;
loop
fetch ptr into p_name;
exit when ptr%notfound;
dbms_output.put_line(p_name);
end loop;
close ptr;
end;
else if(criteria='highest price')
then
declare
p_name product.p_name%type;
cursor ptr is select p_name from product order by amount DESC;
begin
open ptr;
loop
fetch ptr into p_name;
exit when ptr%notfound;
dbms_output.put_line(p_name);
end loop;
close ptr;
end;
else
dbms_output.put_line('Enter valid criteria!');
end if;
end;
/
But it is giving following error: Error at line 35: PLS-00103: Encountered the symbol ";" when expecting one of the following: Please help
The ELSE-IF statement in PL/SQL has to be written as ELSIF. Otherwise, you should close the second IF with an other END IF; statement.
You can solve the issue by changing the ELSE IF at line 17 to an ELSIF
The answer by #GregorioPalamà correctly addresses your issues. But you can drastically reduce the workload by changing your thinking away from "If...then...else" to the "set of" and letting SQL do the work. In this case the only difference is sorting either ascending or descending on amount. The same effect can be achieved by sorting ascending on amount or minus amount; and SQL can make that decision. So you can reduce the procedure to validating the parameter and a single cursor for loop:
create or replace procedure sorting_criteria(criteria in varchar2)
as
cursor ptr(c_sort_criteria varchar2) is
select p_name
from product
order by case when c_sort_criteria = 'lowest price'
then amount
else -amount
end ;
begin
if criteria in ('lowest price', 'highest price')
then
for rec in ptr(criteria)
loop
dbms_output.put_line('Product: ' || rec.p_name );
end loop;
else
dbms_output.put_line('Enter valid criteria!');
end if;
end sorting_criteria;
/
See demo here. For demonstration purposed I added the amount to the dbms_output.
A couple notes:
While it is not incorrect using p_... as a column name, it is also
not a good idea. A very common convention (perhaps almost a
standard) to use p_... to indicate parameters. This easily leads to
confusion; confusion amongst developers is a bad thing.
IMHO it is a bug to name a local variable the same as a table
column name. While the compiler has scoping rules which one to use
it again leads to confusion. The statement "where table.name = name"
is always true, except when at least one of them is null, which possible could lead to updating/deleting every row in your table. In this
case p_name is both a column and a local variable.

PLSQL Converting if then to CASE stmt

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;
/

pl/sql need help undestanding

trying to get total sales for a specific zip; I have done a couple of these, but cant seem to get the total and I'm missing something.
Tables are Customers that has the zip,
sales that has the gross sale amount.
This one executes complete but no total; so what am I missing?
Also for the sake of asking how could I modify this to ask me in an interface, where I can input the zip? I started that too but; again, I get the question when I insert the zip it gives me errors.
SET SERVEROUTPUT ON
DECLARE
V_SALES NUMBER (10,2);
V_ZIP VARCHAR2(5) NOT NULL := 48228;
BEGIN
SELECT SUM(S.GROSS_SALE_PRICE) -- GROUP FUNCTION
INTO V_SALES
FROM SALES S
WHERE CUST_ID = V_ZIP;
DBMS_OUTPUT.PUT_LINE ('TOTAL SALES FOR ZIP 48228, IS'|| TO_NUMBER(V_SALES));
END;
/
Try including a group by clause
SET SERVEROUTPUT ON
DECLARE
V_SALES NUMBER (10,2);
V_ZIP VARCHAR2(5) NOT NULL := 48228;
BEGIN
SELECT SUM(S.GROSS_SALE_PRICE) -- GROUP FUNCTION
INTO V_SALES
FROM SALES S
WHERE CUST_ID = V_ZIP
GROUP BY CUST_ID;
DBMS_OUTPUT.PUT_LINE ('TOTAL SALES FOR ZIP 48228, IS'|| TO_NUMBER(V_SALES));
END;

pl/sql block going in infinite loop

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.)

PLSQL - Measure the execution duration of a procedure

I have a procedure that runs every one hour populating a table. The records handled from the procedure are many so it takes approximately 12~17 mins each time it is executed.
Do you now if there is a way (i.e. trigger) to record the duration of each execution (i.e. into a table)?
I don't know of a trigger that would allow this to be done automatically. One way to do this would be something like
PROCEDURE MY_PROC IS
tsStart TIMESTAMP;
tsEnd TIMESTAMP;
BEGIN
tsStart := SYSTIMESTAMP;
-- 'real' code here
tsEnd := SYSTIMESTAMP;
INSERT INTO PROC_RUNTIMES (PROC_NAME, START_TIME, END_TIME)
VALUES ('MY_PROC', tsStart, tsEnd);
END MY_PROC;
If you only need this for a few procedures this might be sufficient.
Share and enjoy.
I typically use a log table with a date or timestamp column that uses a default value of sysdate/systimestamp. Then I call an autonomous procedure that does the log inserts at certain places I care about (starting/ending a procedure call, after a commit, etc):
See here (look for my answer).
If you are inserting millions of rows, you can control when (how often) you insert to the log table. Again, see my example.
To add to the first answer, once you have start and end timestamps, you can use this function to turn them into a number of milliseconds. That helps with readability if nothing else.
function timestamp_diff(
start_time_in timestamp,
end_time_in timestamp) return number
as
l_days number;
l_hours number;
l_minutes number;
l_seconds number;
l_milliseconds number;
begin
select extract(day from end_time_in-start_time_in)
, extract(hour from end_time_in-start_time_in)
, extract(minute from end_time_in-start_time_in)
, extract(second from end_time_in-start_time_in)
into l_days, l_hours, l_minutes, l_seconds
from dual;
l_milliseconds := l_seconds*1000 + l_minutes*60*1000
+ l_hours*60*60*1000 + l_days*24*60*60*1000;
return l_milliseconds;
end;

Resources