PL/SQL Insert procedure, insert if the row doesn't exist - sql

Below is my procedure. It inserts, but every time I execute the procedure it inserts a duplicated row. I don't want that, but i have tried everything and I don't know how to resolve the issue.
My Code :
CREATE OR REPLACE PROCEDURE Insert_Cidades(p_NOME CIDADE.NOME_CIDADE%TYPE)
IS
BEGIN
INSERT INTO CIDADE(COD_CIDADE,NOME_CIDADE) VALUES(seq_id_cidade.NEXTVAL,p_NOME);
END Insert_Cidades;
/
This is in pl/slq oracle.

MERGE INTO CIDADE
USING (SELECT p_NOME as NOME FROM DUAL) x
ON (x.NOME = NOME_CIDADE)
WHEN NOT MATCHED THEN
INSERT (COD_CIDADE, NOME_CIDADE)
VALUES (seq_id_cidade.NEXTVAL, p_NOME)
or
INSERT INTO CIDADE
SELECT
seq_id_cidade.NEXTVAL,
p_NOME
FROM
dual
WHERE NOT EXISTS (SELECT 'x' FROM CIDADE WHERE NOME_CIDADE = p_NOME)
Note that the comparison NOME_CIDADE = p_NOME is case sensitive, meaning that you can still insert 'John', 'john', 'JOHN' and 'jOHN'. If you don't want that, change it to something like upper(NOME_CIDADE) = upper(p_NOME) or nlssort(NOME_CIDADE) = nlssort(p_NOME).

Related

Cant figure out this SQL Trigger

so I'm suppose to be making a trigger for my database that will limit how many classes a faculty member can be assigned. If QUALIFIED = 'Y', then they can teach up to three classes. the trouble i'm running into is that I dont know what is wrong with my SQL statement that wont let it be run.
CREATE OR REPLACE trigger "ASSIGN_T1"
BEFORE
INSERT ON "ASSIGN"
FOR EACH ROW
BEGIN
DECLARE
A_COUNT NUMBER;
A_QUALIFY CHAR(2);
SET(SELECT QUALIFY FROM QUALIFY WHERE (FID = :NEW.FID)) AS A_QUALIFY
SET(SELECT COUNT(FID) FROM QUALIFY WHERE (FID = :NEW.FID)) AS A_COUNT
IF (A_QUALIFY = 'Y' AND A_COUNT < 3) THEN
INSERT INTO ASSIGN (FID, CID) VALUES (:NEW.FID, :NEW.CID);
END IF;
END;
The two errors i'm getting are
line 8, position 8 PLS-00103: Encountered the symbol "(" when expecting one of the following: constant exception table long double ref char time timestamp
line 8, position 61 PLS-00103: Encountered the symbol "AS" when expecting one of the following: set
The 1st problem here is that the BEGIN needs to move down below the DECLARE and the variable declarations.
The 2nd problem is the way you're attempting to set those variables. Get rid of those SETs and AS's. In PL/SQL, one valid way to set a variable with the result of a SQL statement is with a SELECT INTO. Like so....
SELECT QUALIFY
INTO A_QUALIFY
FROM QUALIFY
WHERE FID = :NEW.FID;
...and you can do the same for A_COUNT
I won't guarantee everything will work right after you do that, but that's the bare minimum to fix here.
Also, even if doing the above works, watch out for SELECT INTO because you'll get a "no data found" error if there's ever a scenario where you don't already have a FID = :NEW.FID being passed in OR an "exact fetch returns more than requested number of rows" error if you have more than 1 existing record with that FID in your table. You then either have to handle those exceptions, or use a different method of assigning values to your variable (such as declaring the SQL in a cursor and then OPEN cursor, FETCH cursor INTO variable, CLOSE cursor.)
Lastly, I think there may be a problem in your logic. You're asking for the value in QUALIFY for a FID, but then you're asking for the number of records that have that FID. That implies that the FID isn't the primary key on your table, which means there could be different records with the same FID but different values in the QUALIFY field. So if you're going to be using that variable in your logic later on, then that may be a problem, since the same FID can have one record with QUALIFY = 'Y', and another record with QUALIFY = 'N'
You have used the BEGIN after DECLARE part. And am not sure why you are using SET .. AS. We can combine both selects into one and use it in IF condition.
I don't think you can trigger on the same table and do insert at the same time. You will end up with ORA-04088 error.
Instead, you can restrict the insertion by throwing an error.
(my option would be a Foreign Key Constraint over the ASSIGN table)
--Creating Tables
create table ASSIGN (FID number, CID number);
create table QUALIFY (FID number, QUALIFY char);
-- Loading sample data
insert into QUALIFY values (1, 'Y');
insert into QUALIFY values (1, 'Y');
insert into QUALIFY values (1, 'Y');
insert into QUALIFY values (2, 'Y');
insert into QUALIFY values (2, 'Y');
insert into QUALIFY values (3, 'N');
insert into QUALIFY values (4, 'Y');
CREATE OR REPLACE trigger "ASSIGN_T1"
BEFORE
INSERT ON "ASSIGNEE" --< change table name to yours
FOR EACH ROW
DECLARE
A_COUNT NUMBER;
BEGIN
SELECT COUNT(QUALIFY) into A_COUNT FROM QUALIFY WHERE QUALIFY='Y' AND FID = :NEW.FID;
-- If they are qualified and already has 3 classes. They are not allowed/record is not inserted.
IF A_COUNT = 3 THEN
Raise_Application_Error (-20343, 'FID is not Qualified or already has 3 Classes.');
END IF;
END;
/
Test by inserting data into the ASSIGNEE table
-- FID 1 already assigned to 3 classes, should not be allowed any more.
insert into ASSIGNEE values (1,3);
-- See error below
Error report -
ORA-20343: FID is not Qualified or already has 3 Classes.
-- FID 2 has only 2 classes, so allowed to insert.
insert into ASSIGNEE values (2,3);
1 row inserted.
One way to accomplish your goal is to do something like this:
CREATE OR REPLACE TRIGGER ASSIGN_T1
BEFORE INSERT ON ASSIGN
FOR EACH ROW
BEGIN
FOR aRow IN (SELECT q.QUALIFY,
COUNT(*) OVER (PARTITION BY q.FID) AS FID_COUNT
FROM QUALIFY q
WHERE q.FID = :NEW.FID)
LOOP
IF aRow.QUALIFY = 'Y' AND aRow.FID_COUNT < 3 THEN
INSERT INTO ASSIGN (FID, CID) VALUES (:NEW.FID, :NEW.CID);
END IF;
END LOOP;
END ASSIGN_T1;

Insert or update table

I have a list of 100k ids in a file. I want to iterate through these ids:
for each id, check if id is in a table:
If it is, update its updated_date flag
If not, add a new record (id, updated_date)
I have researched and found MERGE clause. The downside is, MERGE requires the ids to be in a table. I am only allowed to create a temporary table if necessary.
Can anyne point me in the right direction? It must be a script that I can run on my database, not in code.
merge into MyTable x
using ('111', '222', all my ids) b
on (x.id = b.id)
when not matched then
insert (id, updated_date) values (b.id, sysdate)
when matched then
update set x.updated_date = sysdate;
EDIT: I am now able to use a temporary table if it's my only option.
Given that you say you can't create a temporary table, one way might be to convert your list of ids into a set of union all'd selects, eg:
123,
234,
...
999
becomes
select 123 id from dual union all
select 234 id from dual union all
...
select 999 id from dual
You could then use that in your merge statement:
merge into MyTable x
using (select 123 id from dual union all
select 234 id from dual union all
...
select 999 id from dual) b
on (x.id = b.id)
when not matched then insert (id, updated_date) values (b.id, sysdate)
when matched then update set x.updated_date = sysdate;
If you've really got 100k ids, it might take a while to parse the statement, however! You might want to split up the queries and have several merge statements.
One other thought - is there not an existing GTT that you could "borrow" to store your data?
If you have access to the file from your Oracle server then you can define an external table, which will allow you to read from the file using SQL.
The syntax is based on SQL*Loader, and it's maybe not something you'd want to do for a casual job, more of a recurring task.
Alternatively you could use SQL*Loader itself to load it into a table, or even ODBC from a Microsoft Access or similar database.
Another option is to run 100,000 inserts. You can make this perform much better by taking each 100 or so inserts and wrapping them in an anonymous block, which saves roundtrips to the database, so instead of:
insert into tmp values(1);
insert into tmp values(12);
insert into tmp values(145);
insert into tmp values(234);
insert into tmp values(245);
insert into tmp values(345);
....
insert into tmp values(112425);
use ...
begin
insert into tmp values(1);
insert into tmp values(12);
insert into tmp values(145);
insert into tmp values(234);
...
insert into tmp values(245);
end;
/
begin
insert into tmp values(345);
...
insert into tmp values(112425);
end;
/
If it was a regular task I'd definitely go for an external table though.

SQL to insert only if a certain value is NOT already present in the table?

How to insert a number into the table, only if the table does not already have that number in it?
I am looking for specific SQL code, not really sure how to approach this. Tried several things, nothing's working.
EDIT
Table looks like this:
PK ID Value
1 4 500
2 9 3
So if I am trying to INSERT (ID, Value) VALUES (4,100) it should not try to do it!
If ID is supposed to be unique, there should be a unique constraint defined on the table. That will throw an error if you try to insert a value that already exists
ALTER TABLE table_name
ADD CONSTRAINT uk_id UNIQUE( id );
You can catch the error and do whatever you'd like if an attempt is made to insert a duplicate key-- anything from ignoring the error to logging and re-raising the exception to raising a custom exception
BEGIN
INSERT INTO table_name( id, value )
VALUES( 4, 100 );
EXCEPTION
WHEN dup_val_on_index
THEN
<<do something>>
END;
You can also code the INSERT so that it inserts 0 rows (you would still want the unique constraint in place both from a data model standpoint and because it gives the optimizer more information and may make future queries more efficient)
INSERT INTO table_name( id, value )
SELECT 4, 100
FROM dual
WHERE NOT EXISTS(
SELECT 1
FROM table_name
WHERE id = 4 )
Or you could code a MERGE instead so that you update the VALUE column from 500 to 100 rather than inserting a new row.
Try MERGE statement:
MERGE INTO tbl USING
(SELECT 4 id, 100 value FROM dual) data
ON (data.id = tbl.id)
WHEN NOT MATCHED THEN
INSERT (id, value) VALUES (data.id, data.value)
INSERT INTO YOUR_TABLE (YOUR_FIELD)
SELECT '1' FROM YOUR_TABLE YT WHERE YT.YOUR_FIELD <> '1' LIMIT 1
Of course, that '1' will be your number or your variable.
You can use INSERT + SELECT to solve this problem.

Regarding sql substitution

When i ran the below queries it's failing in the second query becuase prev_test_ref1 variable is not defined. If i remove the insert statement in the first query ,run again then it's working and using the prev_test_ref1 value from the first sql query in second query. Is it because of variable scope? How can i resolve this with the insert statement.
QUERY1
column prev_test_ref1 new_value prev_test_ref1 ;
insert into testing.test_ref_details(TEST_TYPE,TEST_REF_NO)
select '1',max(test_ref_no) as prev_test_ref1
from testing.test_runs_status
where test_type = 1
and run_status = 1
and test_end_dt = (select last_day(add_months(trunc(sysdate),-6))+2 from dual)
group by test_end_dt
;
QUERY2
column last_test_end_dt new_value last_test_end_dt;
select to_char(test_completion_dt,'DD-MON-YYYY HH24:MI:SS') as last_test_end_dt
from testing.test_runs_status
where test_ref_no = '&prev_test_ref1';
In SQLPlus substitution variables will only be defined with SELECT statements. Your first insert doesn't return rows so it won't work (think about it: it only returns 1 row inserted., SQLPlus has no way to know the value inserted.)
I suggest you add a step to save the value into the variable (or use a PL/SQL block):
column prev_test_ref1 new_value prev_test_ref1 ;
SELECT MAX(test_ref_no) AS prev_test_ref1
FROM testing.test_runs_status
WHERE test_type = 1
AND run_status = 1
AND test_end_dt = (SELECT last_day(add_months(trunc(SYSDATE), -6)) + 2
FROM dual)
GROUP BY test_end_dt;
INSERT INTO testing.test_ref_details(TEST_TYPE,TEST_REF_NO)
VALUES ('1', &prev_test_ref1);
SELECT ...
declare
prev_test_ref1 number(10);
begin
insert into ...select ...;
select ... into prev_test_ref1 from ...;
end;
/
The INSERT statement has a RETURNING clause. We can use this to get access to "unknown" values from the table. The following examples uses RETURNING to get the assigned nextval from a sequence, but we could return any column from the row:
SQL> var prev_id number
SQL> insert into t23 (id, name) values (my_seq.nextval, 'MAISIE')
2 returning id into :prev_id
3 /
1 row created.
SQL> select * from t23
2 where id = :prev_id
3 /
NAME ID
---------- ----------
MAISIE 122
SQL>
Unfortunately the RETURNING clause only works with single-row SQL.
It isn't really clear what the purpose of the whole script is, especially in light of the comment "i have similar sql query which returns multiple rows. In that case i cant have separate insert statement."
If you want to use the results of a select, see if Multi-Table Inserts fit the bill. Your select statement can insert into both the primary table and also a second table (eg a global temporary table). You can then query the global temporary table to see what rows were inserted.

Oracle: how to UPSERT (update or insert into a table?)

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;