Oracle cursor with variables help needed - oracle

I am trying to do a cursor which does something like below, struggling with different approaches with no results. Seems, I won't be able to do it by myself, and decided to ask you for help.
Below code shows what I want to achieve rather than ready approach. Please help.
I dont know it it matters but note, that I need to update CUSTOMERS in loop. I also need to select some data from another table referencing customer in this loop, then insert something to third table and update customer table.
DECLARE
CURSOR MY_CURSOR
IS
SELECT CUSTOMERID FROM CUSTOMERS WHERE ACTIVE = 1 ;
MY_RECORD MY_CURSOR%ROWTYPE;
BEGIN
FOR MY_RECORD IN MY_CURSOR
LOOP
DECLARE TEMPORARY_TABLE TABLE (A DATE, B NUMBER, C VARCHAR)
INSERT INTO #TEMPORARY_TABLE(A,B,C) (SELECT CREATEDDATE, ID, NAME FROM ACCOUNT WHERE CUSTOMER = MY_RECORD.CUSTOMERID)
INSERT INTO SOME_EVENT_TABLE(ID, NAME, DATE, ACCOUNT_ID) VALUE (some_seq.NEXTVAL, #TEMPORARY_TABLE[C], #TEMPORARY_TABLE[A], #TEMPORARY_TABLE[B])
UPDATE CUSTOMERS SET LAST_ACCOUNT_CHECK_NAME=#TEMPORARY_TABLE(C), LAST_INSERTED_EVENT_ID = some_seq.CURRVAL WHERE ID = MY_RECORD.CUSTOMERID
END LOOP;
COMMIT;
END;

First, you can't declare a temporary table in Oracle like you do in SQL Server. However, you really don't need it here anyway.
Something like this should work:
FOR MY_RECORD IN MY_CURSOR LOOP
FOR R IN (SELECT CREATEDDATE, ID, NAME
FROM ACCOUNT WHERE CUSTOMER = MY_RECORD.CUSTOMERID) LOOP
INSERT INTO some_event_table(ID, NAME, DATE, ACCOUNT_ID)
VALUES (some_seq.NEXTVAL, R.NAME, R.CREATEDATE, R.ID);
UPDATE customers
SET last_account_check_name = R.name
, last_inserted_event_id = some_seq.CURRVAL
WHERE id = MY_RECORD.CUSTOMER_ID;
END LOOP;
END LOOP;
COMMIT;

Row by row actions in SQL are terribly inefficient. You will get vastly better performance if you do this in a set-based way.
INSERT INTO some_event_table(ID, NAME, DATE, ACCOUNT_ID)
SELECT some_seq.NEXTVAL, a.name, a.createdate, a.id
FROM ACCOUNT a
INNER JOIN customers c ON c.customerid = a.customerid
WHERE c.active = 1;
UPDATE customers
SET last_account_check_name =
( SELECT a.name FROM account a WHERE a.customerid = c.customerid ),
last_inserted_event_id = some_seq.CURRVAL
WHERE c.active = 1;
There may be concurrency issues with that (what happens if customers is updated between the two statements?), but that might be good enough for your needs.

Related

Solving Procedure with no parameters

Hey guys just here to see if you guys can help me solve this Procedure problem I am running into. Long story short I made a new table called
Create Table ClientHistoricalPurchases(
ClientID varchar2(6) constraint clientidhistorical references Clients,
DistinctProducts number (9),
TotalProducts number(9),
TotalCost number (9,2),
Primary Key (ClientID));
And I want to populate/update this table by running a procedure that reads primarily from the following table:
create table OrderDetails(
OrderID varchar2(6) CONSTRAINT orddetpk PRIMARY KEY,
ProductID varchar2(6) CONSTRAINT prdfk REFERENCES Products ,
UnitPrice number(10,2),
Quantity number(4),
Discount number(3),
ShippingDate date);
I do a couple of joins with two more tables called Orders and Clients but those are trivial joins using the Primary Key's/FK.
So the goal of this procedure is that when I run it I want to loop through order details and I want to calculate the distinct amount of products bought by a Client, the total products and the total purchase amount and I want to update an existing record with the new values if its in the new ClientHistoricalPurchases table if not I want to add a new record for it. So this is what I wrote but its giving me errors:
Create or Replace Procedure Update_ClientHistPurch as
Cursor C1 is
Select orderid, orders.clientid, productid, unitprice, quantity, discount
from orderdetails
Inner join orders on orderdetails.orderid = orders.clientid
for update of TotalCost;
PurchaseRow c1%RowType;
DistinctProducts orderdetails.quantity%type;
TotalProducts orderdetails.quantity%type;
ProposedNewBalance orderdetails.unitprice%type;
Begin
Begin
Begin
Begin
Open C1;
Fetch c1 into PurchaseRow;
While c1% Found Loop
Select count(distinct productid)
into DistinctProducts
from orderdetails
Inner join orders on orderdetails.orderid = orders.orderid
Inner join clients on orders.clientid = clients.clientid
where clients.clientid = purchaserow.clientid;
end;
Select count(ProductID)
into TotalProducts
from orderdetails
Inner join orders on orderdetails.orderid = orders.orderid
Inner join clients on orders.clientid = clients.clientid
where clients.clientid = purchaserow.clientid;
end;
Select sum((unitprice * quantity) - discount)
into ProposedNewBalance
from orderdetails
Inner join orders on orderdetails.orderid = orders.orderid
Inner join clients on orders.clientid = clients.clientid
where clients.clientid = purchaserow.clientid;
end;
If purchaserow.clientid not in ClientHistoricalpurchases.clientid then
insert into ClientHistoricalPurchases values (purchaserow.clientid,DistinctProducts, TotalProducts, ProposedNewBalance);
End if;
If purchaserow.clientid in ClientHistoricalPurchases.clientid then
Update Clienthistoricalpurchases
set clienthistoricalpurchases.distinctproducts = distinctproducts, clienthistoricalpurchases.totalproducts = totalproducts, clienthistoricalpurchases.totalcost = ProposedNewBalance
where purchaserow.clientid = clienthistoricalpurchases.clientid;
end if;
end loop;
end;
Errors are the following:
Error(27,4): PLS-00103: Encountered the symbol ";" when expecting one
of the following: loop The symbol "loop" was substituted for ";"
to continue.
Error(33,7): PLS-00103: Encountered the symbol "JOIN"
when expecting one of the following: , ; for group having
intersect minus order start union where connect
Any help is appreciated guys. Thanks!
In addition to the comments and answers you've already been given, I believe you have massively overcomplicated your procedure. You're doing things very procedurally, rather than thinking in sets as you should be. You are also getting the aggregated columns in three queries that are essentially identical (e.g. same tables, join conditions and predicates) - you could combine them all to get the three results in a single query.
It looks like you're trying to insert into the clienthistoricalpurchases table if a row doesn't already exist for that client, otherwise you update the row. That immediately screams "MERGE statement" to me.
Combining all that, I think your current procedure should contain just a single merge statement:
MERGE INTO clienthistoricalpurchases tgt
USING (SELECT clients.client_id,
COUNT(DISTINCT od.productid) distinct_products,
COUNT(od.productid) total_products,
SUM((od.unitprice * od.quantity) - od.discount) proposed_new_balance
FROM orderdetails od
INNER JOIN orders
ON orderdetails.orderid = orders.orderid
INNER JOIN clients
ON orders.clientid = clients.clientid
GROUP BY clients.client_id) src
ON (tgt.clientid = src.client_id)
WHEN NOT MATCHED THEN
INSERT (tgt.clientid,
tgt.distinctproducts,
tgt.totalproducts,
tgt.totalcost)
VALUES (src.clientid,
src.distinct_products,
src.total_products,
src.proposed_new_balance)
WHEN MATCHED THEN
UPDATE SET tgt.distinctproducts = src.distinct_products,
tgt.totalproducts = src.total_products,
tgt.totalcost = src.proposed_new_balance;
However, I have some concerns over your current logic and/or data model.
It seems like you're expecting at most one row per clientid to appear in clienthistoricalpurchases. What if a clientid has two or more different orders? Currently you would overwrite any existing row.
Also, do you really want to apply this logic across all orders every single time it gets run?
Line 28 of your code, the first END that follows WHILE, should be END LOOP

Multiple Join tables in classic report; Inserting only first row instead of checked row

I have question on classic report, which is based on multiple table joins. On which I have written process to loop and insert only checked box item. but it only select first item. However if do this on a single table it work properly. I would appreciate if someone can help me on inserted selected record when query is based on multiple joins.
My query is as below.
select apex_item.checkbox2(1, ord.rowid) sel,
apex_item.text(2,cust.name) Customer,
apex_item.text(3, it.item_id) Item,
apex_item.text(4,it.product_id) Product,
apex_item.text(5,price) price,
apex_item.text(6,quantity)||apex_item.hidden(7,ord.id) qty
from s_ord ord,
s_item it,
s_customer cust
where ord.id=it.ord_id
and cust.id=ord.customer_id
My process is as follow;
for i in 1..apex_application.g_f01.count loop
APEX_DEBUG_MESSAGE.LOG_MESSAGE(p_message => 'G_F01 : '||APEX_APPLICATION.G_F01(i), p_level => 1);
APEX_DEBUG_MESSAGE.LOG_MESSAGE(p_message => ' Q1 : '||APEX_APPLICATION.G_F02(i), p_level => 1);
APEX_DEBUG_MESSAGE.LOG_MESSAGE(p_message => ' P1 : '||APEX_APPLICATION.G_F03(i), p_level => 1);
end loop;
end;
As far as I can tell, you can't do that if a tabular form is created as a JOIN of two (or more) tables (didn't investigate why).
Here's what I do:
I base my tabular form on one table (the one I'm planning to work (insert, update) with
columns, that are normally fetched from other tables (using JOINs) are displayed using functions
That fixes the issue.
For example: you want to update employee's information, but also display department name they work in.
Don't:
select e.empno,
e.ename,
d.dname,
e.sal
from emp e join dept d on e.deptno = d.deptno;
Do:
create function f_dname (par_deptno in dept.deptno%type)
return dept.dname%type
is
retval dept.dname%type;
begin
select max(d.dname)
into retval
from dept d
where d.deptno = par_deptno;
return retval;
end;
/
select e.empno,
e.ename,
f_dname (e.deptno) dname, --> function instead of DEPT.DNAME
e.sal
from emp e; --> no join
I hope it'll help.

Refactor code without cursor PL/SQL

How can I refactor these lines of code without using CURSOR?
I am beginner in PL/SQL.
Any help would be appreciated. Thank you
DECLARE
CURSOR c_emps IS
SELECT employee_id
FROM bonus;
v_region HR.REGIONS.region_name%TYPE;
v_salary hr.employees.salary%TYPE;
BEGIN
FOR r_emps IN c_emps LOOP
SELECT reg.region_name, emp.salary
INTO v_region, v_salary
FROM hr.employees emp,
hr.departments dep,
hr.Locations loc,
hr.countries cot,
hr.regions reg
WHERE emp.department_id = dep.department_id AND
dep.location_id = loc.location_id AND
loc.country_id = cot.country_id AND
cot.region_id = reg.region_id AND
employee_id = r_emps.employee_id;
IF v_region = 'Europe' THEN
UPDATE bonus
SET bonus = bonus + (v_salary * .01)
WHERE employee_id = r_emps.employee_id;
ELSE
UPDATE bonus
SET bonus = v_salary * .01
WHERE employee_id = r_emps.employee_id;
END IF;
END LOOP;
COMMIT;
END;
/
CURSOR c_emps IS
SELECT employee_id
FROM bonus;
You don't need to explicitly declare the CURSOR. You could do it in the CURSOR FOR LOOP itself:
FOR r_emps IN (SELECT employee_id FROM bonus)
LOOP
If PL/SQL is not mandatory, then you could do it in plain SQL using CASE expression in the UPDATE statement.
Something like,
UPDATE bonus
SET bonus =
CASE
WHEN region = 'Europe'
THEN bonus + (v_salary * .01)
ELSE v_salary * .01
...
and so on
Yes, you need to rewrite the entire PL/SQL code into a SQL update statement. But, it would be much better and faster. For loop is row-by-row processing, thus it is slow-by-slow. Avoid PL/SQL if you could do the same in SQL.
When working with SQL Server, an awful lot of effort is taken to avoid cursors because they are handled very poorly. Using a cursor in SQL Server is like slogging waist-deep through molasses. Oracle handles cursors much better so you see a lot more row-by-row work in Oracle. Too much, really. Even in Oracle, if something can be done with a single SQL statement, it is far superior than using PL/SQL cursors and looping.
Unfortunately, Oracle doesn't allow joins in UPDATE statements. But not to worry, the more recent innovation, the MERGE statement does.
MERGE INTO BONUS B
USING(
SELECT EMP.EMPLOYEE_ID, EMP.SALARY, REG.REGION_NAME
FROM HR.EMPLOYEES EMP
JOIN HR.DEPARTMENTS DEP
ON DEP.DEPARTMENT_ID = EMP.DEPARTMENT_ID
JOIN HR.LOCATIONS LOC
ON LOC.LOCATION_ID = DEP.LOCATION_ID
JOIN HR.COUNTRIES COT
ON COT.COUNTRY_ID = LOC.COUNTRY_ID
JOIN HR.REGIONS REG
ON REG.REGION_ID = COT.REGION_ID ) U
ON( U.EMPLOYEE_ID = b.EMPLOYEE_ID )
WHEN MATCHED THEN
UPDATE SET B.BONUS =( u.SALARY * 0.01 ) +
CASE U.REGION_NAME WHEN 'Europe' THEN B.BONUS ELSE 0 END;
How nice that the when not matched clause is not required, effectively turning the merge into a very flexible update.

Temporary tables in Packages - Oracle

I am kind of new in Oracle.
I am trying to create a package that has several functions.
This is the pseudocode of what I want to do
function FunctionA(UserID, startdate, enddate)
/* Select TransactionDate, Amount
from TableA
where TransactionDate between startdate and enddate
and TableA.UserID = UserID */
Return TransactionDate, Amount
end FunctionA
function FunctionB(UserID, startdate, enddate)
/* Select TransactionDate, Amount
from TableB
where TransactionDate between startdate and enddate
and TableB.UserID = UserID */
Return TransactionDate, Amount
end FunctionA
TYPE TRANSACTION_REC IS RECORD(
TransactionDate DATE,
TransactionAmt NUMBER);
function MainFunction(startdate, enddate)
return TBL
is
vTrans TRANSACTION_REC;
begin
FOR rec IN
( Select UserID, UserName, UserStatus
from UserTable
where EntryDate between startdate and enddate )
LOOP
vTrans := FunctionA(rec.UserID, startdate, enddate)
if vTrans.TransactionDate is null then
vTrans := FunctionB(rec.UserID, startdate, enddate)
if vTrans.TransactionDate is null then
rec.UserStatus := 'Inactive'
endif;
endif;
END Loop;
PIPE ROW(USER_OBJ_TYPE(rec.UserID,
rec.UserName,
rec.UserStatus,
vTrans.TransactionDate,
vTtans.TransactionAmt));
end MainFunction
Running this kind of code takes a long time because TableA and TableB is a very large table, and I am only getting 1 entry per record from the tables.
I would want to create a temporary table (TempTableA, TempTableB) within the package that will temporarily store all records based on the startdate and enddate, so that when I try to retrieve the TransactionDate and Amount for each rec, I will only refer to the TempTables (which is smaller than TableA and TableB).
I also want to take into consideration if the UserID is not found in TableA and TableB. So basically, when there are no records found in TableA and TableB, I also want the entry in the output, but it is indicated that the user is inactive.
Thank you for all your help.
SQL is a set-based language. It is far more efficient to execute one statement which returns all the rows you need than to execute many statements which each return a single row.
Here is one way of getting all your rows at once. It uses a common table expression because you read the whole of the UserTable and you should only do that once.
with cte as
(select UserID
, UserStatus
from UserTable )
select cte.UserID
, cte.UserStatus
, TableA.TransactionDate
, TableA.Amount
from cte join TableA
on (cte.UserID = TableA.UserID)
where cte.UserStatus = 'A'
and TableA.TransactionDate between startdate and enddate
union
select cte.UserID
, cte.UserStatus
, TableB.TransactionDate
, TableB.Amount
from cte join TableB
on (cte.UserID = TableB.UserID)
where cte.UserStatus != 'A'
and TableB.TransactionDate between startdate and enddate
By the way, be careful with temporary tables. They aren't like temporary tables in T-SQL. They are permanent heap tables, it's just their data that's temporary. This means that populating a temporary table is an expensive process, because the database writes all those rows to disk. Consequently we need to be certain that the performance gain we get by reading a dataset from a temporary table is worth the overhead of all those writes.
That certainly would not be the case with your code. In fact, it is really pretty rare that the answer to a performance question turns out to be "Use a Global Temporary Table", at least not in Oracle. Better queries are the way to go, and in particular, embracing the Joy of Sets!
Probably better to do it in one query, e.g.:
Select UserTable.UserID, UserTable.UserName, UserTable.UserStatus
,TableA.TransactionDate AS ATransactionDate
,TableA.Amount AS AAmount
,TableB.TransactionDate AS BTransactionDate
,TableB.Amount AS BAmount
from UserTable
left join TableA
on (UserTable.UserID = TableA.UserID)
left join TableB
on (UserTable.UserID = TableB.UserID)
where UserTable.EntryDate between startdate and enddate

Binding variables in dynamic PL/SQL

I have a dynamic PL/SQL that will construct the SELECT statement based on what the searching criteria input from the users,likes:
l_sql := 'SELECT * INTO FROM TABLEA WHERE 1=1 ';
IF in_param1 IS NOT NULL THEN
l_sql := l_sql || 'AND column1 = in_param1 ';
END IF;
IF in_param2 IS NOT NULL THEN
l_sql := l_sql || 'AND column2 = in_param2 ';
END IF;
...................................
IF in_paramXX IS NOT NULL THEN
l_sql := l_sql || 'AND columnXX = in_paramXX ';
END IF;
To reduce the hard parse overhead , I consider to use the binding variables. However , it is difficult to manage when supplying the actual values to the binding variables as there are so many binding variables and combination of the generated SELECT statement . I cannot use the method of DBMS_SESSION.set_context() introduced at http://www.dba-oracle.com/plsql/t_plsql_dynamic_binds.htm because my account has no right to use this package. Besides , I want the generated SQL only contains the conditions on the fields that the user did not leave empty. So I cannot change the dynamic SQL to something likes
SELECT * INTO FROM TABLEA WHERE 1=1
and ( in_param1 is NULL or column1 = in_param1)
and ( in_param2 is NULL or column2 = in_param2)
...............................................
and ( in_paramXX is NULL or columnXX = in_paramXX)
So , I want to try to use the DBMS_SQL method .Can anyone give an example about how to use DBMS_SQL to call dynamic SQL with binding variables? Especially , how can I get the result executed from DBMS_SQL.execute() to the SYS_REFCURSOR , something like :
open refcursor for select .... from
The oracle version that I use is 10g and it seems that the oracle 10g does not have DBMS_Sql.To_Refcursor()
In your Oracle version you can apply some tricks to your query in order to do this. The idea is to use a query in the following form:
select *
from
(select
:possibleParam1 as param1
-- do the same for every possible param in your query
:possibleParamN as paramN
from dual
where rownum > 0) params
inner join
-- join your tables here
on
-- concatenate your filters here
where
-- fixed conditions
then execute it with:
open c for query using param1, ..., paramN;
It works by using DUAL to generate a fake row with every single param, then inner joining this fake row to your real query (without any filters) using only the filters you want to apply. This way, you have a fixed list of bind variables in the SELECT list of the params subquery, but can control which filters are applied by modifying the join condition between params and your real query.
So, if you have something like, say:
create table people (
first_name varchar2(20)
last_name varchar2(20)
);
you can construct the following query if you just want to filter on first name
select *
from
(select
:first_name as first_name,
:last_name as last_name
from dual
where rownum > 0) params
inner join
people
on
people.first_name = params.first_name;
and this if you want to filter on both first_name and last_name
select *
from
(select
:first_name as first_name,
:last_name as last_name
from dual
where rownum > 0) params
inner join
people
on
people.first_name = params.first_name and
people.last_name = params.last_name;
and in every case you would execute with
open c for query using filterFirstName, filterLastName;
It is important for performance to use the where rownum > 0 with DUAL as it forces Oracle to "materialize" the subquery. This usually makes DUAL stop interfering with the rest of the query. Anyway, you should check the execution plans to be sure Oracle is not doing anything wrong.
In 10g a DBMS_SQL cursor can't be changed into a Ref Cursor. Going through a result set through DBMS_SQL is tortuous since, as well as looping through the rows, you also have to loop through the columns in a row.
I want the generated SQL only contains
the conditions on the fields that the
user did not leave empty
Is that purely for performance reasons ? If so, I suggest you work out what the practical execution plans are and use separate queries for them.
For example, say I'm searching on people and the parameters are first_name, last_name. gender, date_of_birth. The table has indexes on (last_name,first_name) and (date_of_birth), so I only want to allow a query if it specifies either last_name or date_of_birth.
IF :p_firstname IS NOT NULL and :p_lastname IS NOT NULL THEN
OPEN cur FOR
'SELECT * FROM PEOPLE WHERE last_name=:a AND first_name=:b AND
(date_of_birth = :c or :c is NULL) AND (gender = :d or :d IS NULL)' USING ....
ELSIF :p_lastname IS NOT NULL THEN
OPEN cur FOR
'SELECT * FROM PEOPLE WHERE last_name=:a AND
(date_of_birth = :c or :c is NULL) AND (gender = :d or :d IS NULL)' USING ....
ELSIF :p_dateofbirth IS NOT NULL THEN
OPEN cur FOR
'SELECT * FROM PEOPLE WHERE date_of_birth=:a AND
(first_name=:b OR :b IS NULL) AND (gender = :d or :d IS NULL)' USING ....
ELSE
RAISE_APPLICATION_ERROR(-20001,'Last Name or Date of Birth MUST be supplied);
END IF;

Resources