Creating multiple updates on database with procedure. Problem with incrementation - sql

I'm trying to create a procedure in oracle sql in order to make multiple updates for one table. I have around 500 updates for different values but I'm only changing one column which is unique and it's task is to number rows. So i want to change number 20 to 21 and so on until number 120. So in the end number 20 will be empty and I will have 121 rows.
I created this loop but I keep getting error "unique constraint violated" so i figured that my iteration is not working.
declare
n integer := 20;
BEGIN
FOR x in 20..120 LOOP
Update my_table
SET number_of_field = n WHERE number_of_field > 20;
n:= n + 1;
END LOOP;
END;
I would really appreciate help because it's my first time using procedures in SQL and I will feel like I'm close to the solution.
Thank you!

Oracle is one of the few databases (PostgreSQL being the other one afaik) that allows you to defer constraint checks until the end of the transaction.
If you place the whole block of code into a single transaction, and also defer the check of the UNIQUE constraint to DEFERRABLE INITIALLY DEFERRED, then you can perform all updates in it, even if you temporarily store duplicate values in the column. However, once you perform the COMMIT, the constraint (for all rows/updates) will be validated, and if it fails the transaction will be rolled back. So you need to be on a good stance once you issue the commit.
For example you can change your table to something like:
create table my_table (
... other_columns here
number_of_field,
constraint uq1 unique(number_of_field) deferrable initially deferred
);

This is how I understood the question:
SQL> -- Create table
SQL> create table my_table (number_of_field number, somecol varchar2(10));
Table created.
SQL> -- Insert sample data; my end with 30, not 120 (that's too much for an example)
SQL> insert into my_table
2 select 20 + level - 1, 'X'
3 from dual
4 connect by level <= 11;
11 rows created.
SQL> -- Table contents
SQL> select number_of_field, somecol from my_table order by number_of_field nulls first;
NUMBER_OF_FIELD SOMECOL
--------------- ----------
20 X
21 X
22 X
23 X
24 X
25 X
26 X
27 X
28 X
29 X
30 X
11 rows selected.
Two-step job: update existing rows (by adding 1) and insert an empty one (somecol is here just to show that there's a row with empty number_of_Field column).
SQL> -- Update existing rows; insert an "empty" one
SQL> begin
2 update my_table set
3 number_of_field = number_of_field + 1;
4 insert into my_table (number_of_field, somecol)
5 values (null, 'X');
6 end;
7 /
PL/SQL procedure successfully completed.
SQL> -- The result
SQL> select number_of_field, somecol from my_table order by number_of_field nulls first;
NUMBER_OF_FIELD SOMECOL
--------------- ----------
X
21 X
22 X
23 X
24 X
25 X
26 X
27 X
28 X
29 X
30 X
31 X
12 rows selected.
SQL>

Related

oracle concurrent select for update and insert

I have a table with two columns: k (primary key) and value.
I'd like to:
select for update by k, if k is not found, insert a new row with a default value.
with the returned value ( existent or new inserted row value) make some processing.
update the row and commit.
Is it possible to make this "select for update and insert default value if not found"?
If implement (1) as a select/check if found/insert if not found, we have concurrency problems, since two sessions could make the select concurrently on non existent key, both will try to insert and one of the instances will fail.
In this case the desired behavior is to perform atomically the select/insert and one of the instance perform it and the second one keep locked until the first one commits, and then use the value inserted by the first one.
We implement it always doing an "insert ... if not exist.../commit" before the "select for update" but this implies always trying to insert when it is a unlikely needed.
Is there any way to implement it on one sql sentence?
Thanks!!
See if k is available
SELECT * FROM table
WHERE k = value
FOR UPDATE
If no rows returned, then it doesn't exist. Insert it:
INSERT INTO table(k, col1, col2)
VALUES (value, val1, default))
select ... for update is the first step you should make; without it, you can't "reserve" that row for further processing (unless you're willing to lock the whole table in exclusive mode; if that "processing" takes no time, that could also be an option, especially if there are not many users who will be doing it).
If row exists, the rest is simple - process it, update it, commit.
But, if it doesn't exist, you'll have to insert a new row (just as you said), and here's a problem of two (or more) users inserting the same value.
To avoid it, create a function which
will return unique ID value for a new row
is an autonomous transaction
why? Because you're performing DML in it (update or insert), and you can't do that in a function unless it is an autonomous transaction
Users will have to use that function to get the next ID value. Here's an example: you'll need a table (my_id) which holds the last used ID (and every user who accesses it via the function will create a new value).
Table:
SQL> create table my_id (id number);
Table created.
Function:
SQL> create or replace function f_id
2 return number
3 is
4 pragma autonomous_transaction;
5 l_nextval number;
6 begin
7 select id + 1
8 into l_nextval
9 from my_id
10 for update of id;
11
12 update my_id set
13 id = l_nextval;
14
15 commit;
16 return (l_nextval);
17
18 exception
19 when no_data_found then
20 lock table my_id in exclusive mode;
21
22 insert into my_id (id)
23 values (1);
24
25 commit;
26 return(1);
27 end;
28 /
Function created.
Use it as
SQL> select f_id from dual;
F_ID
----------
1
SQL>
That's it ... code you'll use will then be something like this:
SQL> create table test
2 (id number constraint pk_test primary key,
3 name varchar2(10),
4 datum date
5 );
Table created.
SQL> create or replace procedure p_test (par_id in number)
2 is
3 l_id test.id%type;
4 begin
5 select id
6 into l_id
7 from test
8 where id = par_id
9 for update;
10
11 update test set datum = sysdate where id = par_id;
12 exception
13 when no_data_found then
14 insert into test (id, name, datum)
15 values (f_id, 'Little', sysdate); --> function call is here
16 end;
17 /
Procedure created.
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:21
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:21 --> row was inserted
SQL> exec p_test (1);
PL/SQL procedure successfully completed.
SQL> select * from test;
ID NAME DATUM
---------- ---------- -------------------
1 Little 04.09.2021 20:49:30 --> row was updated
SQL>
Use a sequence to generate a surrogate primary key instead of using a natural key. If you had a real natural key, then it would be extremely unlikely for two users to submit the same value at the same time.
There are several ways to automatically generate primary keys. I prefer to use sequence defaults, like this:
create sequence test_seq;
create table test1
(
k number default test_seq.nextval,
value varchar2(4000),
constraint test1_pk primary key(k)
);
If you can't switch to a surrogate key or a real natural key:
Change the "insert ... if not exist.../commit" to a simpler "insert ... if not exist", and perform all operations in a single transaction. Inserting the same primary key in different sessions, even uncommitted, is impossible in Oracle. Although the SELECT won't cause a block, the INSERT will. This behavior is an exception to Oracle's implementation of isolation, the "I" in "ACID", and in this case that behavior can work to your advantage.
If two sessions attempt to insert the same primary key at the same time, the second session will hang, and when the first session finally commits, the second session will fail with the exception "ORA-00001: unique constraint (X.Y) violated". Let that exception become your flag for knowing when a user has submitted a duplicate value. You can catch the exception in the application and have the user try again.

How to compare similar data

I want to insert the data to PL-SQL.
But I need to check whether the table has similar Data.
If the table check has similar information, it cannot be added
exanple:
Insert Data : ABCD or ABCE
Table:
enter image description here
The Tabel Device has ABC,so it cannot be added.
How can I write this program?
You can use instr in plsql
example:
declare
x number;
begin
for i in (select distinct value from table)
loop
select count(1) into x
from table2
where instr(Device, i.value) > 0;
if (x = 0) then
insert into table2(Device) values(i.value);
end if;
commit;
end loop;
end;
Similar or the same data? That makes a huge difference. Because if you don't want the same data in your columns, you can just make the column unique. But if you want to check for similar data, welp, that's kind of hard to implement, hard to understand, and that just outright kills your performance...But if you still want to go that way, you can check out Jaro-Winkler Algorithm
Isi already mentioned the algorithm; here's an example of what you might be looking for.
Current table contents:
SQL> select * from test;
COL
-----
ABC
ABD
ABR
Here's how similar current values are to certain strings (ABCD, FDGH) you'd want to insert:
SQL> select t.col,
2 utl_match.jaro_winkler_similarity('ABCD', t.col) sim_abcd,
3 utl_match.jaro_winkler_similarity('FDGH', t.col) sim_fdgh
4 from test t;
COL SIM_ABCD SIM_FDGH
----- ---------- ----------
ABC 94 0
ABD 93 52
ABR 77 0
Now, it is up to you to decide which value will represent a limit and either insert a value into that table or not. Let's presume it is 90%. Then you'd
SQL> insert into test (col)
2 select '&&par_insert'
3 from dual
4 where 90 > (select max(utl_match.jaro_winkler_similarity('&&par_insert', a.col))
5 from test a
6 );
Enter value for par_insert: ABCD
0 rows created.
SQL>
Right; ABCD is similar to any of current values more than 90% and wasn't inserted.
How about FDGH?
SQL> undefine par_insert
SQL> /
Enter value for par_insert: FDGH
1 row created.
SQL> select * from test;
COL
-----
ABC
ABD
ABR
FDGH
SQL>
Yes, it was inserted.

How to make increment if same name occurs using triggers?

I have a table:
id
name
amount
If the same name was inserted, then amount should be incremented.
Else, insert with 0 amount.
How to create a trigger with a given condition?
You shouldn't really be doing that. AMOUNT column effectively counts number of NAME appearances in that table. You can always count it, can't you? So, what's the purpose of doing that?
If you want to know which name was inserted prior (or after) some other name (equal to the previous one), sort them by ID (if it is incremental, such as an identity column or if it gets its value from a sequence).
If ID isn't incremental, add DATE_INSERTED column (and sort by it; or apply ROW_NUMBER analytic function which orders values by DATE_INSERTED).
Also, what happens if you delete one of those duplicate names? Will you retroactively decrement AMOUNT column for all previous instances of that NAME?
But, if you insist, here's one option. As you can't just select from a table you're inserting into (because of the mutating table error), I'm using a compound trigger. The part of "insert amount 0" is done by setting the column's default value (you don't need any code for that).
SQL> create table test
2 (id number primary key,
3 name varchar2(20),
4 amount number default 0);
Table created.
SQL> create or replace trigger trg_ai_test
2 for insert on test
3 compound trigger
4
5 type test_rt is record (id test.id%type,
6 name test.name%type);
7 type rli_t is table of test_rt index by pls_integer;
8 g_rli rli_t;
9
10 after each row is
11 begin
12 g_rli(g_rli.count + 1).id := :new.id;
13 g_rli(g_rli.count).name := :new.name;
14 end after each row;
15
16 after statement is
17 l_cnt number;
18 begin
19 for i in 1 .. g_rli.count loop
20 dbms_output.put_Line('x');
21 dbms_output.put_line(i ||' '|| g_rli(i).id ||' '||g_rli(i).name);
22 select count(*) into l_cnt
23 from test
24 where name = g_rli(i).name;
25
26 update test set
27 amount = l_cnt
28 where id = g_rli(i).id;
29 end loop;
30
31 end after statement;
32 end trg_ai_test;
33 /
Trigger created.
SQL>
Testing:
SQL> insert into test (id, name) values (1, 'little');
1 row created.
SQL> insert into test (id, name) values (2, 'foot');
1 row created.
SQL> insert into test (id, name) values (3, 'little');
1 row created.
SQL> insert into test (id, name) values (9, 'little');
1 row created.
SQL> select * from test;
ID NAME AMOUNT
---------- -------------------- ----------
1 little 1
2 foot 1
3 little 2
9 little 3
SQL>
Or, a simpler solution (described above), without using that much code:
SQL> select id, name,
2 row_number() over (partition by name order by id) as amount
3 from test;
ID NAME AMOUNT
---------- -------------------- ----------
2 foot 1
1 little 1
3 little 2
9 little 3
SQL>

Timetable allocation using SQL

I have these two tables in my database:
Session(startTime,endTime,date)
Allocation(startTime,endTime,date)
There are already existing sessions in the timetable, and I need to allocate a new session in a way that there are no confusions between all the sessions. I thought about something like:
ALTER TABLE allocation ADD CONSTRAINT timeC
check (startTime not between (select startTime from session)
and (select endTime from session))
The problem is that it's impossible to do so as we can't use the keyword "between" for two sets of values (same thing with the endTime).
How can I manage to add this constraint ? (I use Oracle 11g)
That can't be done via a check constraint; a trigger might help, though (as Gordon has already said). Here's an example:
SQL> create table tsession
2 (id number constraint pk_tsess primary key,
3 starttime date,
4 endtime date);
Table created.
SQL>
SQL> create table tallocation
2 (id number constraint fk_all_sess references tsession (id),
3 starttime date,
4 endtime date);
Table created.
SQL>
SQL> create or replace trigger trg_biu_all
2 before insert or update on tallocation
3 for each row
4 declare
5 l_dummy varchar2(1);
6 begin
7 select 'x'
8 into l_dummy
9 from tsession s
10 where s.id = :new.id
11 and :new.starttime between s.starttime and s.endtime;
12
13 raise_application_error(-20001, 'Can not set such a start time as it collides with TSESSION values');
14 exception
15 when no_data_found then
16 -- OK, no problem - no collision
17 null;
18 end;
19 /
Trigger created.
Now, testing:
SQL> -- Insert master record, ID = 1; it'll take whole February
SQL> insert into tsession values (1, date '2018-02-01', date '2018-02-28');
1 row created.
SQL> -- I don't want to allow this date to "jump in" between 2018-02-01 and 2018-02-28
SQL> insert into tallocation (id, starttime) values (1, date '2018-02-13');
insert into tallocation (id, starttime) values (1, date '2018-02-13')
*
ERROR at line 1:
ORA-20001: Can not set such a start time as it collides with TSESSION values
ORA-06512: at "HR.TRG_BIU_ALL", line 10
ORA-04088: error during execution of trigger 'HR.TRG_BIU_ALL'
SQL> -- This one should be OK, as it is in March
SQL> insert into tallocation (id, starttime) values (1, date '2018-03-22');
1 row created.
SQL>

Oracle Constraints

I am working on a small school project using oracle database. I have created some tables and two of them are Mobile (Mobile_Number,Status_Flag) Status_Flag shows if a number is active or not and there is another table Owner_Mobile(Owner_Id FK,Mobile_ID FK). Now I should write a Constraint that prohibits the insert operation if the corresponding Status_Flag is N for the specified number. I tried to make it using sub query but this is not possible.
the constrain should be applied to OWNER_MOBILE table of course. For example if I say: INSERT INTO OWNER_MOBILE(25541,042536) the constrain should check the Mobile table and see if the Mobile 042536 is active or not . If the number is not active the insert statement should generate a error
You can use the trigger or another PL/SQL API for this but you should take into account ACID transaction principles. Let's consider the case when flag value = 0 should prevent insertion:
SQL> create table mobile (mobile_id int primary key, flag int)
2 /
SQL> create table owner_mobile(owner_id int,
2 mobile_id int references mobile(mobile_id))
3 /
SQL> insert into mobile values (1,1)
2 /
SQL> commit
2 /
SQL> create or replace trigger
2 tr_owner_mobile
3 before insert on owner_mobile
4 for each row
5 declare
6 l_flag mobile.flag%type;
7 begin
8 select flag into l_flag
9 from mobile where mobile_id = :new.mobile_id;
10
11 if l_flag = 0 then
12 raise_application_error(-20000, 'Unavalable mobile');
13 end if;
14 end;
15 /
In the code above I simply select flag and rely on the retrieved value - I don't care of ACID.
In the first transaction I update flag value but don't commit:
SQL> update mobile set flag = 0 where mobile_id = 1;
In the second transaction I insert into owner_mobile and get the success:
SQL> insert into owner_mobile values(1,1);
1 row inserted.
Next, I commit the first transaction and later - the second one. What I get then:
SQL> select * from mobile;
MOBILE_ID FLAG
---------- ----------
1 0
SQL> select * from owner_mobile;
OWNER_ID MOBILE_ID
---------- ----------
1 1
Seems this is not what I expect.
I can use select for update to prevent inconsistent behavoiur:
SQL> update mobile set flag = 1;
1 row updated.
SQL> delete from owner_mobile;
1 row deleted.
SQL> commit;
SQL> create or replace trigger
2 tr_owner_mobile
3 before insert on owner_mobile
4 for each row
5 declare
6 l_flag mobile.flag%type;
7 begin
8 select flag into l_flag
9 from mobile where mobile_id = :new.mobile_id
10 for update;
11
12 if l_flag = 0 then
13 raise_application_error(-20000, 'Unavalable mobile');
14 end if;
15 end;
16 /
Now do the same:
SQL> update mobile set flag = 0 where mobile_id = 1;
1 row updated.
Second transaction is waiting because parent row is locked:
SQL> insert into owner_mobile values(1,1);
After commit in first transaction I get in the second one:
SQL> insert into owner_mobile values(1,1);
insert into owner_mobile values(1,1)
*
error in line 1:
ORA-20000: Unavalable mobile
ORA-06512: at "SCOTT.TR_OWNER_MOBILE", line 9
ORA-04088: error in trigger 'SCOTT.TR_OWNER_MOBILE'
So whatever you do to achieve requirements you will have to consider transaction isolation.