Oracle, ROWNUM=1 with FOR UPDATE clause? - oracle

My statement:
SELECT ROW_ID DATA_T WHERE CITY_ID=2000 AND IS_FREE=0 AND ROWNUM = 1
is used to retrieve the first row for a db table that has many entries with CITY_ID equal to 2000.
The ROW_ID that is returned is then used in an UPDATE statement in order to use this row and set IS_FREE=1.
That worked very well until two threads called the SELECT statement and the got the same ROW_ID obviously... That is my problem in a few words.
I am using ORACLE DB (12.x)
How do I resolve the problem? Can I use FOR UPDATE in this case?
I want every "client" somehow to get a different row or at least lock on of them

Something like this
function get_row_id return number
as
cursor cur_upd is
SELECT ROW_ID FROM TB WHERE CITY_ID=2000 AND IS_FREE=0 AND ROWNUM = 1
FOR UPDATE SKIP LOCKED;
begin
for get_cur_upd in cur_upd
loop
update TB
set IS_FREE = 1
where ROW_ID = get_cur_upd.ROW_ID;
commit work;
return get_cur_upd.ROW_ID;
end loop;
return null;
end;
commit or not after update depends on your logic.
Also you can return row_id without update&commit and do it later outside func.

Related

Oracle equivalent query for this postgress query - CONFLICT [duplicate]

The UPSERT operation either updates or inserts a row in a table, depending if the table already has a row that matches the data:
if table t has a row exists that has key X:
update t set mystuff... where mykey=X
else
insert into t mystuff...
Since Oracle doesn't have a specific UPSERT statement, what's the best way to do this?
The MERGE statement merges data between two tables. Using DUAL
allows us to use this command. Note that this is not protected against concurrent access.
create or replace
procedure ups(xa number)
as
begin
merge into mergetest m using dual on (a = xa)
when not matched then insert (a,b) values (xa,1)
when matched then update set b = b+1;
end ups;
/
drop table mergetest;
create table mergetest(a number, b number);
call ups(10);
call ups(10);
call ups(20);
select * from mergetest;
A B
---------------------- ----------------------
10 2
20 1
The dual example above which is in PL/SQL was great becuase I wanted to do something similar, but I wanted it client side...so here is the SQL I used to send a similar statement direct from some C#
MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name")
VALUES ( 2097153,"smith", "john" )
However from a C# perspective this provide to be slower than doing the update and seeing if the rows affected was 0 and doing the insert if it was.
An alternative to MERGE (the "old fashioned way"):
begin
insert into t (mykey, mystuff)
values ('X', 123);
exception
when dup_val_on_index then
update t
set mystuff = 123
where mykey = 'X';
end;
Another alternative without the exception check:
UPDATE tablename
SET val1 = in_val1,
val2 = in_val2
WHERE val3 = in_val3;
IF ( sql%rowcount = 0 )
THEN
INSERT INTO tablename
VALUES (in_val1, in_val2, in_val3);
END IF;
insert if not exists
update:
INSERT INTO mytable (id1, t1)
SELECT 11, 'x1' FROM DUAL
WHERE NOT EXISTS (SELECT id1 FROM mytble WHERE id1 = 11);
UPDATE mytable SET t1 = 'x1' WHERE id1 = 11;
None of the answers given so far is safe in the face of concurrent accesses, as pointed out in Tim Sylvester's comment, and will raise exceptions in case of races. To fix that, the insert/update combo must be wrapped in some kind of loop statement, so that in case of an exception the whole thing is retried.
As an example, here's how Grommit's code can be wrapped in a loop to make it safe when run concurrently:
PROCEDURE MyProc (
...
) IS
BEGIN
LOOP
BEGIN
MERGE INTO Employee USING dual ON ( "id"=2097153 )
WHEN MATCHED THEN UPDATE SET "last"="smith" , "name"="john"
WHEN NOT MATCHED THEN INSERT ("id","last","name")
VALUES ( 2097153,"smith", "john" );
EXIT; -- success? -> exit loop
EXCEPTION
WHEN NO_DATA_FOUND THEN -- the entry was concurrently deleted
NULL; -- exception? -> no op, i.e. continue looping
WHEN DUP_VAL_ON_INDEX THEN -- an entry was concurrently inserted
NULL; -- exception? -> no op, i.e. continue looping
END;
END LOOP;
END;
N.B. In transaction mode SERIALIZABLE, which I don't recommend btw, you might run into
ORA-08177: can't serialize access for this transaction exceptions instead.
I'd like Grommit answer, except it require dupe values. I found solution where it may appear once: http://forums.devshed.com/showpost.php?p=1182653&postcount=2
MERGE INTO KBS.NUFUS_MUHTARLIK B
USING (
SELECT '028-01' CILT, '25' SAYFA, '6' KUTUK, '46603404838' MERNIS_NO
FROM DUAL
) E
ON (B.MERNIS_NO = E.MERNIS_NO)
WHEN MATCHED THEN
UPDATE SET B.CILT = E.CILT, B.SAYFA = E.SAYFA, B.KUTUK = E.KUTUK
WHEN NOT MATCHED THEN
INSERT ( CILT, SAYFA, KUTUK, MERNIS_NO)
VALUES (E.CILT, E.SAYFA, E.KUTUK, E.MERNIS_NO);
I've been using the first code sample for years. Notice notfound rather than count.
UPDATE tablename SET val1 = in_val1, val2 = in_val2
WHERE val3 = in_val3;
IF ( sql%notfound ) THEN
INSERT INTO tablename
VALUES (in_val1, in_val2, in_val3);
END IF;
The code below is the possibly new and improved code
MERGE INTO tablename USING dual ON ( val3 = in_val3 )
WHEN MATCHED THEN UPDATE SET val1 = in_val1, val2 = in_val2
WHEN NOT MATCHED THEN INSERT
VALUES (in_val1, in_val2, in_val3)
In the first example the update does an index lookup. It has to, in order to update the right row. Oracle opens an implicit cursor, and we use it to wrap a corresponding insert so we know that the insert will only happen when the key does not exist. But the insert is an independent command and it has to do a second lookup. I don't know the inner workings of the merge command but since the command is a single unit, Oracle could execute the correct insert or update with a single index lookup.
I think merge is better when you do have some processing to be done that means taking data from some tables and updating a table, possibly inserting or deleting rows. But for the single row case, you may consider the first case since the syntax is more common.
A note regarding the two solutions that suggest:
1) Insert, if exception then update,
or
2) Update, if sql%rowcount = 0 then insert
The question of whether to insert or update first is also application dependent. Are you expecting more inserts or more updates? The one that is most likely to succeed should go first.
If you pick the wrong one you will get a bunch of unnecessary index reads. Not a huge deal but still something to consider.
Try this,
insert into b_building_property (
select
'AREA_IN_COMMON_USE_DOUBLE','Area in Common Use','DOUBLE', null, 9000, 9
from dual
)
minus
(
select * from b_building_property where id = 9
)
;
From http://www.praetoriate.com/oracle_tips_upserts.htm:
"In Oracle9i, an UPSERT can accomplish this task in a single statement:"
INSERT
FIRST WHEN
credit_limit >=100000
THEN INTO
rich_customers
VALUES(cust_id,cust_credit_limit)
INTO customers
ELSE
INTO customers SELECT * FROM new_customers;

Oracle PL/SQL Update statement looping forever - 504 Gateway Time-out

I'm trying to update a table based on another one's information:
Source_Table (Table 1) columns:
TABLE_ROW_ID (Based on trigger-sequence when insert)
REP_ID
SOFT_ASSIGNMENT
Description (Table 2) columns:
REP_ID
NEW_SOFT_ASSIGNMENT
This is my loop statement:
SELECT count(table_row_id) INTO V_ROWS_APPROVED FROM Source_Table;
FOR i IN 1..V_ROWS_APPROVED LOOP
SELECT REQUESTED_SOFT_MAPPING INTO V_SOFT FROM Source_Table WHERE ROW_ID = i;
SELECT REP_ID INTO V_REP_ID FROM Source_Table WHERE ROW_ID = i;
UPDATE Description_Table D
SET D.NEW_SOFT_ASSIGNMENT = V_SOFT
WHERE D.REP_ID = V_REP_ID;
END LOOP;
END;
The ending result of this loop is a beautiful ''504 Gateway Time-out''.
I know the issue is on the Update query but there's no other way (I can think about) of doing it.
Can someone give me a hand please?
Thanks
Unless your row_id values are contiguous - i.e. count(row_id) == max(row_id) - then this will get a no-data-found. Sequences aren't gapless, so this seems fairly likely. We have no way of telling if that is happening and somehow that is leaving your connection hanging until it times out, or if it's just taking a long time because you're doing a lot of individual queries and updates over a large data set. (And you may be squashing any errors that do occur, though you haven't shown that.)
You don't need to query and update in a loop though, or even use PL/SQL; you can apply all the values in the source table to the description table with a single update or merge:
merge into description_table d
using source_table s
on (s.rep_id = d.rep_id)
when matched then
update set d.new_soft_assignment = s.requested_soft_mapping;
db<>fiddle with some dummy data, including a non-contiguous row_id to show that erroring.

Update after calculate for each record ORACLE

SELECT CIF_ID,
SUM (IN_VERIFIED_DEBT + IN_FAC_WITH_OTHER + IN_FAC_WITH_BANK)
from LOS_CIF_INDV
WHERE STATUS= 'ACTIVE'
GROUP By CIF_ID;
I want to update the total column again after the user manipulates the client as update, insert but it gives an error
ORA-04098: trigger 'RLOS138.UPDATE_IN_TOTAL_COMMIT' is invalid and failed re-validation
CREATE OR REPLACE TRIGGER UPDATE_IN_TOTAL_COMMIT
AFTER UPDATE ON
LOS_CIF_INDV
FOR EACH ROW
DECLARE
inactive_id number;
BEGIN
inactive_id:=
:new.IN_VERIFIED_DEBT + :new.IN_FAC_WITH_OTHER + :new.IN_FAC_WITH_BANK;
UPDAte LOS_CIF_INDV
SET IN_TOTAL_COMMIT = inactive_id
WHERE CIF_ID = :NEW.CIF_ID;
END ;
/
I have tried this again
CREATE OR REPLACE TRIGGER RLOS138.UPDATE_IN_TOTAL_COMMIT
AFTER UPDATE ON RLOS138.LOS_CIF_INDV
FOR EACH ROW
DECLARE
inactive_id number;
BEGIN
SELECT SUM (IN_VERIFIED_DEBT+IN_FAC_WITH_OTHER+IN_FAC_WITH_BANK)
into inactive_id
from LOS_CIF_INDV
WHERE STATUS= 'ACTIVE'
and CIF_ID=:NEw.CIF_ID;
update LOS_CIF_INDV
set IN_TOTAL_COMMIT = inactive_id
where CIF_ID = :NEW.CIF_ID;
END ;
/
yes [CIF_ID] is primary key
In which case this trigger has the logic you need:
CREATE OR REPLACE TRIGGER RLOS138.UPDATE_IN_TOTAL_COMMIT
BEFORE UPDATE ON RLOS138.LOS_CIF_INDV
FOR EACH ROW
BEGIN
if :new.status = 'ACTIVE'
then
:new.IN_TOTAL_COMMIT := :new.IN_VERIFIED_DEBT + :new.IN_FAC_WITH_OTHER + :new.IN_FAC_WITH_BANK;
end if;
END ;
/
I have included the check on status because you used it in your aggregation queries, even though you omitted from the first version of the trigger. I haven't included an ELSE branch, but you may wish to add one. Also, I have assumed that the three columns in the addition are guaranteed to be not null; if that's not the case you'll need to handle that.
I have put a working demo on db<>fiddle. This includes a version of the trigger which fires on inserts as well as updates, and handles null values too....
CREATE OR REPLACE TRIGGER UPDATE_IN_TOTAL_COMMIT
-- handle INSERT as well as UPDATE
BEFORE INSERT OR UPDATE ON LOS_CIF_INDV
FOR EACH ROW
BEGIN
if :new.status = 'ACTIVE'
then
-- handle any of these columns being null
:new.IN_TOTAL_COMMIT := nvl(:new.IN_VERIFIED_DEBT,0)
+ nvl(:new.IN_FAC_WITH_OTHER,0)
+ nvl(:new.IN_FAC_WITH_BANK,0);
end if;
END ;
/
Why not after you could explain it to me
Because Oracle have written triggers that way: the AFTER EACH ROW trigger uses the finalised version of the record, the state which will be written to the database. Consequently, if we want to change any values we need to use a BEFORE EACH ROW trigger. Oracle enforces this with the error you got, ORA-04084: cannot change NEW values for this trigger type.
Just a reminder: ORA-04098 is telling you there are compilation errors in your trigger code. If you're not using an IDE which tells you what these errors are you can find them with this query:
select * from all_errors
where owner = 'RLOS138'
and name = 'UPDATE_IN_TOTAL_COMMIT' ;
(Not sure if you're connecting as RLOS138 - if you are, query USER_ERRORS instead.)
If I understood correctly, You want to update all the records having CIF_ID as an updated record with the same value in the IN_TOTAL_COMMIT column.
This is not a good idea. If you have some derived column then you should use the views instead of updating its value for every insert/update using the trigger.
If you really want to update the column then you must use the combination of Row level trigger, Statement trigger, and package variables. (Search for mutating table error in the SO)
But according to me, the best solution is to use the view, something like follows:
CREATE OR REPLACE VIEW LOS_CIF_INDV_VW AS
SELECT L.*,
COALESCE(
SUM(
CASE
WHEN STATUS = 'ACTIVE' THEN
IN_VERIFIED_DEBT + IN_FAC_WITH_OTHER + IN_FAC_WITH_BANK
END
) OVER(
PARTITION BY L.CIF_ID
),
0
) AS IN_TOTAL_COMMIT
FROM LOS_CIF_INDV L;

Oracle Trigger Performance is to slow

Given the following trigger:
CREATE OR REPLACE TRIGGER TR_MY_TRG_NAME
AFTER UPDATE OF COL_A, COL_B, COL_C ON T_MY_TABLE_Y
FOR EACH ROW
BEGIN
UPDATE T_MY_TABLE_X X
SET
X.COL_A = :NEW.COL_A,
X.COL_B = :NEW.COL_B,
X.COL_C = :NEW.COL_C
WHERE
X.ID = :NEW.ID;
END;
... and given 2 million existing records in T_MY_TABLE_Y.
Problem:
if my app is changing all of the 2 mio records (e.g. COL_A), then without the trigger it runs 2-3 minutes, but with the trigger it took 40min.
Question:
are there some alternative approaches that I could try?
An alternative is to update T_MY_TABLE_X in a single statement, without forcing a trigger to fire for each of 2 million rows and (probably) perform context switching.
So: as you update T_MY_TABLE_Y, reuse the same UPDATE for T_MY_TABLE_X (with some modifications, if necessary).
I think you might benefit to decrease the load for DML by splitting into three parts so as to update for each individual columns :
CREATE OR REPLACE TRIGGER TR_MY_TRG_NAME
AFTER UPDATE OF COL_A, COL_B, COL_C ON T_MY_TABLE_Y
FOR EACH ROW
BEGIN
IF :NEW.COL_A != :OLD.COL_A THEN
UPDATE T_MY_TABLE_X SET COL_A = :NEW.COL_A WHERE ID = :NEW.ID;
END IF;
IF :NEW.COL_B != :OLD.COL_B THEN
UPDATE T_MY_TABLE_X SET COL_B = :NEW.COL_B WHERE ID = :NEW.ID;
END IF;
IF :NEW.COL_C != :OLD.COL_C THEN
UPDATE T_MY_TABLE_X SET COL_C = :NEW.COL_C WHERE ID = :NEW.ID;
END IF;
END;
This way, Update might not incur every column for each occurence.
Moreover, be sure T_MY_TABLE_X.ID has index on it, preferably Unique Index.
I don't have the time to write the code out, but an outline approach I can suggest trying is to create a package with a ARRAY of a RECORD of ID, COL_A, COL_B and COL_C.
The BEFORE statement trigger should instantiate and initialise the package and the array within, e.g.:
the_package.pr_init
The ROW LEVEL trigger should simply write the :NEW.ID, :NEW.COL_A, :NEW.COL_B, :NEW.COL_C to the array within the package, e.g:
the_package.pr_save ( :NEW.ID, :NEW.COL_A, :NEW.COL_B, :NEW.COL_C )
The AFTER statement trigger should then issue a BULK UPDATE on the driving off the array within the package, and clear out the ARRAY, eg:
the_package.pr_do_update.
The benefit of this approach is you only execute ONE additional UPDATE statement regardless of how many rows.
The solution is also contained with in a single package, albeit splayed across 3 triggers, though the trigger code itself will be much simplified.

Why I'm getting the ORA-01003: no statement parsed error?

Why am I getting this error and what does it mean by no statement parsed.
ORA-01003: no statement parsed
Here is the code:
PROCEDURE ORIGINAL_TABLE.UPDATE_GROUPS IS
-- cursor loaded with the swam groups
CURSOR cursor1 IS
SELECT ID, NEW_DESCRIPTION
FROM NEW_TABLE.NEW_GROUP_TABLE#DB_LINK.X;
BEGIN
FOR C1_REC IN cursor1 LOOP
UPDATE
ORIGINAL_TABLE."GROUPS"
SET
GROUP_ID = C1_REC.ID
WHERE
ORIGINAL_TABLE."GROUPS".DESCRIPTION = C1_REC.NEW_DESCRIPTION;
IF (SQL%ROWCOUNT = 0) THEN
INSERT INTO
ORIGINAL_TABLE.GROUPS("GROUP_ID", "DESCRIPTION")
VALUES (C1_REC.ID, C1_REC.NEW_DESCRIPTION);
END IF;
END LOOP;
EXCEPTION
WHEN OTHERS THEN
dbms_output.put_line(SQLERRM);
END;
What I try to do with the code above is to update and old table with the values from a new table and in case that the new group doesn't exist insert it.
Update: Changed %ROWCOUNT > 0 for %ROWCOUNT = 0
Use MERGE statement, it does update/insert stuff more efficiently and pay attention your plsql doesn't provide it is intended for. It tries to make an update statement and if a record found it inserts another record. In order to fix it use
IF (SQL%ROWCOUNT = 0)
I presume the reason of the issue is the . in DBLINK name.
Moreover I would suggest to get rid of quotes for tables/fields just in case as well as schema name.
Another words delete all ORIGINAL_TABLE.
merge into groups g
using (
SELECT ID, NEW_DESCRIPTION
FROM NEW_TABLE.NEW_GROUP_TABLE#DB_LINK.X
) nt
on (nt.NEW_DESCRIPTION = g.description )
when matched then update set g.group_id = nt.id
when non matched then insert(GROUP_ID, DESCRIPTION)
values(nt.id, nt.NEW_DESCRIPTION)

Resources