is there a way to automatically update a column value when a new record is inserted into an oracle sql table - sql

we have an api operation that enters a row into our table with a report_type=5, is there some sort of operation i can apply to the table to make it so whenever a record gets entered or pulled
with a report_id=12 it returns the report_type as 4?

As commented, trigger would do. Here's an example.
Sample table:
SQL> create table test
2 (report_id number,
3 report_type number);
Table created.
Trigger:
SQL> create or replace trigger trg_bi_test
2 before insert on test
3 for each row
4 when (new.report_id = 12)
5 begin
6 :new.report_type := 4;
7 end;
8 /
Trigger created.
Testing:
SQL> insert into test (report_id, report_type) values (1, 13);
1 row created.
SQL> insert into test (report_id, report_type) values (12, 99);
1 row created.
SQL> select * from test;
REPORT_ID REPORT_TYPE
---------- -----------
1 13
12 4 --> I inserted report_type = 99, but trigger modified it to 4
--> because report_id = 12
SQL>

It's not clear which value you want to be STORED in the database: 12 (as entered), or 4 (as translated).
A trigger as proposed by another commenter would certainly be able to translate the value on insert or update.
If you want the original value to be stored, you'd need to set up a different column, that is derived based on the original one. An example swiped from an Oracle publication:
create table PERSON (
(employee_id integer,
employee_id_disp computed by
SUBSTRING (CAST(employee_id + 100000 as VARCHAR (6)) from 2)
);
In your case, you might do something like
create table MYTABLE (
somekey varchar(20) not null,
entered_office int,
display_office computed by decode(entered_office,12,4,entered_office)
);
Then, anything that needs to display the office number would need to use the display_office field, not the entered one. Any tool that does an insert into the table would also need to insert the entered_office field, as display_office is not updateable.

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 pass string data type to number datatype in plsql

I have procedure like this...
declare
v_psg varchar2(10);
id_no number;
begin
select value into v_psg from settings_am where key = 'PSG';
select id into id_no from product where to_char(psg) in (v_psg);
end;`
The value returned from select value into v_psg from settings_am where key = 'PSG'; would be
'1','2','3'
when i run this procedure i am returned with ora error - ORA-01403.
please advise how i should pass the v_psg value to psg column of product table?
EDIT - Tried with test case suggested
If you got ORA-01403, you were kind of lucky. It is the NO_DATA_FOUND error, which means that one (probably the first) query didn't return anything.
Those two statements could be combined into
select id
from product
where to_char(psg) in (select value
from settings_am
where key = 'PSG'
);
Why would you select value first, and then use it in another query? Besides, it just wouldn't work. v_psg is declared as VARCHAR2 variable. The way you described it, it contains the following string: '1','2','3', as if this is what you have:
SQL> create table settings_am (key varchar2(10),
2 value varchar2(20)); --> note size here
Table created.
SQL> insert into settings_am (key, value)
2 values ('PSG', q'['1','2','3']');
1 row created.
SQL> select * From settings_am;
KEY VALUE
---------- --------------------
PSG '1','2','3'
SQL>
As you can see, I enlarged the value column size, although variable you declared says 10. Why? Because of
SQL> select length(value) from settings_am where key = 'PSG';
LENGTH(VALUE)
-------------
11
i.e. you can't put something that is long 11 into something that accepts length 10.
Or, if your data actually contains 3 rows for the PSG key, are those values already enclosed into single quotes? If so, that's strange; people usually don't do that. Anyway, suppose that you managed to get string '1,2,3' (which is what I presume you actually have) into a VARCHAR2 variable, then you have to split it into rows in order to be able to use it in the IN clause:
SQL> create table product (id number, psg varchar2(10));
Table created.
SQL> insert into product (id, psg) values (100, '1');
1 row created.
SQL> insert into product (id, psg) values (200, '2');
1 row created.
SQL>
Query is then (where lines #3 - 5 represent splitting a string into rows):
SQL> select p.id
2 from product p
3 where p.psg in (select regexp_substr('&&v_psg', '[^,]+', 1, level)
4 from dual
5 connect by level <= regexp_count('&&v_psg', ',') + 1
6 );
Enter value for v_psg: 1,2,3
ID
----------
100
200
So, wouldn't it be simpler to use
SQL> select id
2 from product
3 where to_char(psg) in (select value
4 from settings_am
5 where key = 'PSG'
6 );
ID
----------
100
200
SQL>
Note that both options also show why your query is wrong: you can't put two values (rows) into a variable declared as id_no number; as you'd get TOO_MANY_ROWS error.
Finally, what is it that you'd want to do? What problem are you trying to solve? Apparently, except for special cases (only one row for each value) your query can't work. If you could provide test case (create table & insert into sample data), as well as expected output, it would be easier to help you.

In an INSERT, how to make the null values to be zero or null?

I have a table of 10 columns, and my INSERT statement only refers to specific columns in the table.
INSERT INTO SCHEMA.TABLE
(COL_1, COL_2)
VALUES
(VAL_1, VAL_2);
... or...
INSERT INTO SCHEMA.TABLE
(COL_1, COL_2)
SELECT VAL_1, VAL_2 FROM SCHEMA_2.TABLE_2;
However, when I execute it, the other columns are inserted always with a null value, instead of having the corresponding one depending on the column type (i.e. number). This is, if I have a numeric column, I should see a zero.
How can I do that insert properly?
*** Please consider I have no DDL privileges & I'm trying to insert into an existing table.
The easiest approach would probably to give your columns default values:
ALTER TABLE schema.table MODIFY (COL_1 NUMBER DEFAULT 0);
DEFAULT value it is; however, note that you have to pay attention to what you do because column might not get its default value. Here's an example:
SQL> create table test
2 (id number primary key,
3 name varchar2(10),
4 address varchar2(20) default 'Unknown', --> columns with default
5 num_val number default 0 --> values
6 );
Table created.
If you're inserting values without specifying column(s) that are supposed to get default values, everything will be as you'd want it to be:
SQL> insert into test (id, name) values (1, 'Little');
1 row created.
SQL> select * from test;
ID NAME ADDRESS NUM_VAL
---------- ---------- -------------------- ----------
1 Little Unknown 0
See? Both ADDRESS and NUM_VAL got default values.
However, if you mention those columns in INSERT statement, although setting them to NULL, they won't be set to their default values but NULL:
SQL> insert into test (id, name, address, num_val)
2 values (2, 'Foot', null, null);
1 row created.
SQL> select * from test;
ID NAME ADDRESS NUM_VAL
---------- ---------- -------------------- ----------
1 Little Unknown 0
2 Foot
As you can see, row with ID = 2 didn't get default values in ADDRESS and NUM_VAL columns.
Therefore, pay attention to what you do.
USE DEFAULT AS 0 for that column
or
use NVL( column_name, 0 ) --as per oracle syntax
--this would mean whenever theres null found for
-- that column set it to 0 (will work on insert)
or
Update column set column=0 where column IS NULL
--(will work after insert as the name suggests update)
Although frankly I don't recommend doing this, you can use a trigger to accomplish your goal:
CREATE OR REPLACE TRIGGER SCHEMA.TABLE_BI
BEFORE INSERT ON SCHEMA.TABLE
FOR EACH ROW
BEGIN
:NEW.COL_1 := COALESCE(:NEW.COL_1, 0); -- NUMBER column
:NEW.COL_2 := COALESCE(:NEW.COL_2, ' '); -- VARCHAR column
:NEW.COL_3 := COALESCE(:NEW.COL_3, SYSDATE); -- DATE column
END SCHEMA.TABLE_BI;
However, creating a trigger may require privileges you don't have.
To answer the question: that needs to be defined at table creation, determining default values. In this case, I wasn't able to do that because the table definition indicated NULL, even in the case of numbers.
Thanks anyway.

How to handle Oracle Error [ Unique Constraint ] error

I have a table named TABLE_1 which has 3 columns
row_id row_name row_descr
1 check1 checks here
2 check2 checks there
These rows are created through a front end application. Now suppose I delete the entry with row_name check2 from the front end and create another entry from front end with row_name check3, in database my entries will be as follows.
row_id row_name row_descr
1 check1 checks here
3 check3 checks
Now row_id if you observe is not a normal one time increment, Now my problem is i'm writing an insert statement to automate something and i don't know what i should insert in the row_id column. Previously i thought it is just new row_id = old row_id +1. But this is not the case here. Please help
EDIT :
Currently im inserting like this which is Wrong :
insert into TABLE1 (row_id, row_name, row_descr
) values ( (select max (row_id) + 1 from TABLE1),'check1','checks here');
row_id is not a normal one time increment.
Never ever calculate ids by max(id)+1 unless you can absolutly exclude simultaneous actions ( which is almost never ever the case). In oracle (pre version 12 see Kumars answer) create a sequence once and insert the values from that sequences afterwards.
create sequence my_sequence;
Either by a trigger which means you don't have to care about the ids during the insert at all:
CREATE OR REPLACE TRIGGER myTrigger
BEFORE INSERT ON TABLE1 FOR EACH ROW
BEGIN
SELECT my_sequence.NEXTVAL INTO :NEW.row_id FROM DUAL;
END;
/
Or directly with the insert
insert into TABLE1 (row_id, row_name, row_descr
) values ( my_sequence.nextval,'check1','checks here');
Besides using row_id as column name in oracle might be a little confusing, because of the pseudocolumn rowid which has a special meaning.
To anwser your quetstion though: If you really need to catch oracle errors as excpetions you can do this with PRAGMA EXCEPTION INIT by using a procedure for your inserts. It might look somehow like this:
CREATE OR REPLACE PROCEDURE myInsert( [...] )
IS
value_allready_exists EXCEPTION;
PRAGMA EXCEPTION_INIT ( value_allready_exists, -00001 );
--ORA-00001: unique constraint violated
BEGIN
/*
* Do your Insert here
*/
EXCEPTION
WHEN value_allready_exists THEN
/*
* Do what you think is necessary on your ORA-00001 here
*/
END myInsert;
Oracle 12c introduced IDENTITY columns. Precisely, Release 12.1. It is very handy with situations where you need to have a sequence for your primary key column.
For example,
SQL> DROP TABLE identity_tab PURGE;
Table dropped.
SQL>
SQL> CREATE TABLE identity_tab (
2 ID NUMBER GENERATED ALWAYS AS IDENTITY,
3 text VARCHAR2(10)
4 );
Table created.
SQL>
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> DELETE FROM identity_tab WHERE ID = 1;
1 row deleted.
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> INSERT INTO identity_tab (text) VALUES ('Text');
1 row created.
SQL> DELETE FROM identity_tab WHERE ID = 2;
1 row deleted.
SQL> SELECT * FROM identity_tab;
ID TEXT
---------- ----------
3 Text
4 Text
SQL>
Now let's see what's under the hood -
SQL> SELECT table_name,
2 column_name,
3 generation_type,
4 identity_options
5 FROM all_tab_identity_cols
6 WHERE owner = 'LALIT'
7 /
TABLE_NAME COLUMN_NAME GENERATION IDENTITY_OPTIONS
-------------------- --------------- ---------- --------------------------------------------------
IDENTITY_TAB ID ALWAYS START WITH: 1, INCREMENT BY: 1, MAX_VALUE: 9999999
999999999999999999999, MIN_VALUE: 1, CYCLE_FLAG: N
, CACHE_SIZE: 20, ORDER_FLAG: N
SQL>
So, there you go. A sequence implicitly created by Oracle.
And don't forget, you can get rid off the sequence only with the purge option with table drop.
If you are not worried about which values are causing the error, then you could handle it by including a /*+ hint */ in the insert statement.
Here is an example where we would be selecting from another table, or perhaps an inner query, and inserting the results into a table called TABLE_NAME which has a unique constraint on a column called IDX_COL_NAME.
INSERT /*+ ignore_row_on_dupkey_index(TABLE_NAME(IDX_COL_NAME)) */
INTO TABLE_NAME(
INDEX_COL_NAME
, col_1
, col_2
, col_3
, ...
, col_n)
SELECT
INDEX_COL_NAME
, col_1
, col_2
, col_3
, ...
, col_n);
Oracle will blow past the redundant row. This is not a great solution if you care about know WHICH row is causing the issue, or anything else. But if you don't care about that and are fine just keeping the first value that was inserted, then this should do the job.
You can use an exception build in which will raise whenever there will be duplication on unique key
DECLARE
emp_count number;
BEGIN
select count(*) into emp_count from emp;
if emp_count < 1 then
insert into emp
values(1, 'First', 'CLERK', '7839', SYSDATE, 1200, null, 30);
dbms_output.put_line('Clerk added');
else
dbms_output.put_line('No data added');
end if;
EXCEPTION
when dup_val_on_index then
dbms_output.put_line('Tried to add row with duplicated index');
END;

Creating a sequence for a varchar2 field in Oracle

I want to create a sequence for this varchar. It would have been easier had it been a number instead of varchar. In that case, I could do
seq_no := seq_no + 1;
But what can I do when I want to store next value in column as A0000002, when the previous value was A0000001 (to increment the number in the next varchar rowby 1)?
This can be done by
to_char(seq_no,'FM0000000')
your example can be done by creating sequence in oracle
create sequence seq_no start with 1 increment by 1;
then
select 'A'||to_char(seq_no.nextval,'FM0000000') from dual;
Right now i have used in dual ..but place this
'A'||to_char(seq_no.nextval,'FM0000000')
in your required query ..this will create sequence as you mentioned
sqlfiddle
Sequences are purely numeric. However, you need a trigger anyway, so simply adapt such trigger to insert the desired prefix:
CREATE OR REPLACE TRIGGER FOO_TRG1
BEFORE INSERT
ON FOO
REFERENCING NEW AS NEW OLD AS OLD
FOR EACH ROW
BEGIN
IF :NEW.FOO_ID IS NULL THEN
SELECT 'A' || TO_CHAR(FOO_SEQ1.NEXTVAL, 'FM0000000') INTO :NEW.FOO_ID FROM DUAL;
END IF;
END FOO_TRG1;
/
ALTER TRIGGER FOO_TRG1 ENABLE;
If you're able I'd actually use a virtual column as defined in the CREATE TABLE syntax. It makes it more easily extensible should the need arise.
Here's a working example.
SQL> create table tmp_test (
2 id number(7,0) primary key
3 , col1 number
4 , seq varchar2(8 char) generated always as (
5 'A' || to_char(id, 'FM0999999'))
6 );
Table created.
SQL>
SQL> create sequence tmp_test_seq;
Sequence created.
SQL>
SQL> create or replace trigger tmp_test_trigger
2 before insert on tmp_test
3 for each row
4 begin
5
6 :new.id := tmp_test_seq.nextval;
7 end;
8 /
Trigger created.
SQL> show errors
No errors.
SQL>
SQL> insert into tmp_test (col1)
2 values(1);
1 row created.
SQL>
SQL> select * from tmp_test;
ID COL1 SEQ
---------- ---------- --------------------------------
1 1 A0000001
Having said that; you would be better off if you did not do this unless you have an unbelievably pressing business need. There is little point to making life more difficult for yourself by prepending a constant value onto a number. As A will always be A it doesn't matter whether it's there or not.
If the format is always a letter followed by 7 digits you can do:
sequence = lpad(substr(sequence,2,7)+1,7,'0')