I have a PL/SQL package in Oracle that its important function is :
function checkDuplicate(in_id in varchar2) return boolean is
cnt number;
begin
select count(*)
into cnt
from tbl_Log t
where t.id = in_id
if (cnt > 0) then
// It means the request is duplicate on in_id
return false;
end if;
insert into tbl_log (id,date) values(in_id , sysdate);
return true;
end;
When two requests call this function concurrently, both of them passed this function and two the same in_id inserted in tbl_log.
Note: tbl_log doesn't have a PK for performance issues.
Are there any solutions?
" both of them passed this function and two the same in_id inserted in tbl_log"
Oracle operates at the READ COMMITTED isolation level, so the select can only find committed records. If one thread has inserted a record for a given value but hasn't committed the transaction another thread looking for the same value will come up empty.
"Note: tbl_log doesn't have a PK for performance issues. "
The lessons of history are clear: tables without integrity constraints inevitably fall into data corruption.
"I want to recognize the duplication with this function ... Are there any solutions?"
You mean apart from adding a primary key constraint? There is no more efficient way of trapping duplication than a primary key. Maybe you should look at the performance issues. Plenty of applications mange to handle millions of inserts and still enforce integrity constraints. You should also look at the Java layer: why have you got multiple threads submitting the same ID?
Note: tbl_log doesn't have a PK for performance issues.
There is no PK nor unique index on this column in order to "avoid performance issues", but there are hundreds or thousands queries like SELECT ... WHERE t.id = .. running against this table. These queries must use a full table scan due to lack of index on this column !!!!
This can cause much bigger performance issues in my opinion.
Since the values of this columns are UUIDs, then there is a very little chance of conflicted values. In this case I would prefer not to use any locks.
Just use an unique constraint (index) on this column to prevent from inserting two duplicate values.
ALTER TABLE tbl_log ADD CONSTRAINT tbl_log_id_must_be_unique UNIQUE( id );
and then use this implementation of your function:
create or replace function checkDuplicate(in_id in varchar2) return boolean is
begin
insert into tbl_log (id,"DATE") values(in_id , sysdate);
return true;
exception when dup_val_on_index then
return false;
end;
/
In the vast majority of cases the function simply inserts a new record to the table without any delay because values are UUIDs.
In seldom cases of duplicated values, when the value is already commited in the table, the insert will immediatelly fail, without any delay.
In very very rare cases (almost impossible) when two threads are trying to simultanously insert the same UUID, the second thread will be held on INSERT command and will wait some time until the first thread will commit or rollback.
As per your condition, since you are reluctant to use Primary key data integrity enforcement( which will lead to data corruption anyhow ), i would suggest that you can use MERGE statment and keep an audit log for the latest thread updating the table. This way you will be able to eliminate the entry of duplicate record as well as keep a track of when and from which thread (latest info) the id got updated. Hope the below snippet helps.
---Create dummy table for data with duplicates
DROP TABLE dummy_hist;
CREATE TABLE dummy_hist AS
SELECT LEVEL COL1,
'AVRAJIT'
||LEVEL COL2,
SYSTIMESTAMP ACTUAL_INSERTION_DT,
SYSTIMESTAMP UPD_DT,
1 thread_val
FROM DUAL
CONNECT BY LEVEL < 100;
--Update upd_dt
UPDATE dummy_hist SET upd_dt = NULL,thread_val = NULL;
SELECT * FROM dummy_hist;
--Create function
CREATE OR REPLACE
FUNCTION checkDuplicate(
in_id IN VARCHAR2,
p_thread_val IN NUMBER)
RETURN BOOLEAN
IS
cnt NUMBER;
BEGIN
MERGE INTO dummy_hist A USING
(SELECT in_id VAL FROM dual
)B ON (A.COL1 = B.VAL)
WHEN MATCHED THEN
UPDATE
SET a.upd_dt = systimestamp,
a.thread_val = p_thread_val
WHERE a.col1 = b.val WHEN NOT MATCHED THEN
INSERT
(
a.col1,
a.col2,
a.actual_insertion_dt,
a.UPD_DT,
a.thread_val
)
VALUES
(
b.val,
'AVRAJIT',
SYSTIMESTAMP,
NULL,
p_thread_val
);
COMMIT;
RETURN true;
END;
/
--Execute the fucntion
DECLARE
rc BOOLEAN;
BEGIN
FOR I IN
(SELECT LEVEL LVL FROM DUAL CONNECT BY LEVEL BETWEEN 8 AND 50
)
LOOP
rc:=checkduplicate(I.LVL,3);
END LOOP;
END;
/
Related
I am trying to prevent inserts of records into a table for scheduling. If the start date of the class is between the start and end date of a previous record, and that record is the same location as the new record, then it should not be allowed.
I wrote the following trigger, which compiles, but of course mutates, and therefore has issues. I looked into compound triggers to handle this, but either it can't be done, or my understanding is bad, because I couldn't get that to work either. I would have assumed for a compound trigger that I'd want to do these things on before statement, but I only got errors.
I also considered after insert/update, but doesn't that apply after it's already inserted? It feels like that wouldn't be right...plus, same issue with mutation I believe.
The trigger I wrote is:
CREATE OR REPLACE TRIGGER PREVENT_INSERTS
before insert or update on tbl_classes
DECLARE
v_count number;
v_start TBL_CLASS_SCHED.start_date%type;
v_end TBL_CLASS_SCHED.end_date%type;
v_half TBL_CLASS_SCHED.day_is_half%type;
BEGIN
select start_date, end_date, day_is_half
into v_start, v_end, v_half
from tbl_classes
where class_id = :NEW.CLASS_ID
and location_id = :NEW.location_id;
select count(*)
into v_count
from TBL_CLASS_SCHED
where :NEW.START_DATE >= (select start_date
from TBL_CLASS_SCHED
where class_id = :NEW.CLASS_ID
and location_id = :NEW.location_id)
and :NEW.START_DATE <= (select end_date
from TBL_CLASS_SCHED
where class_id = :NEW.CLASS_ID
and location_id = :NEW.location_id);
if (v_count = 2) THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule more than 2 classes that are a half day at the same location');
end if;
if (v_count = 1 and :NEW.day_is_half = 1) THEN
if (v_half != 1) THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule a class during another class''s time period of the same type at the same location');
end if;
end if;
EXCEPTION
WHEN NO_DATA_FOUND THEN
null;
END;
end PREVENT_INSERTS ;
Perhaps it can't be done with a trigger, and I need to do it multiple ways? For now I've done it using the same logic before doing an insert or update directly, but I'd like to put it as a constraint/trigger so that it will always apply (and so I can learn about it).
There are two things you'll need to fix.
Mutating occurs because you are trying to do a SELECT in the row level part of a trigger. Check out COMPOUND triggers as a way to mitigate this. Basically you capture info at row level, and the process that info at the after statement level. Some examples of that in my video here https://youtu.be/EFj0wTfiJTw
Even with the mutating issue resolved, there is a fundamental flaw in the logic here (trigger or otherwise) due to concurrency. All you need is (say) three or four people all using this code at the same time. All of them will get "Yes, your count checks are ok" because none of them can see each others uncommitted data. Thus they all get told they can proceed and when they finally commit, you'll have multiple rows stored hence breaking the rule your tirgger (or wherever your code is run) was trying to enforce. You'll need to look an appropriate row so that you can controlling concurrent access to the table. For an UPDATE, that is easy because this means there is already some row(s) for the location/class pairing. For an INSERT, you'll need to ensure an appropriate unique constraint is in place on a parent table somewhere. Hard to say without seeing the entire model
In principle a compound trigger could be this one:
CREATE OR REPLACE TYPE CLASS_REC AS OBJECT(
CLASS_ID INTEGER,
LOCATION_ID INTEGER,
START_DATE DATE,
END_DATE DATE,
DAY_IS_HALF INTEGER
);
CREATE OR REPLACE TYPE CLASS_TYPE AS TABLE OF CLASS_REC;
CREATE OR REPLACE TRIGGER UIC_CLASSES
FOR INSERT OR UPDATE ON TBL_CLASSES
COMPOUND TRIGGER
classes CLASS_TYPE;
v_count NUMBER;
v_half TBL_CLASS_SCHED.DAY_IS_HALF%TYPE;
BEFORE STATEMENT IS
BEGIN
classes := CLASS_TYPE();
END BEFORE STATEMENT;
BEFORE EACH ROW IS
BEGIN
classes.EXTEND;
classes(classes.LAST) := CLASS_REC(:NEW.CLASS_ID, :NEW.LOCATION_ID, :NEW.START_DATE, :NEW.END_DATE, :NEW.DAY_IS_HALF);
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
FOR i IN classes.FIRST..classes.LAST LOOP
SELECT COUNT(*), v_half
INTO v_count, v_half
FROM TBL_CLASSES
WHERE CLASS_ID = classes(i).CLASS_ID
AND LOCATION_ID = classes(i).LOCATION_ID
AND classes(i).START_DATE BETWEEN START_DATE AND END_DATE
GROUP BY v_half;
IF v_count = 2 THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule more than 2 classes that are a half day at the same location');
END IF;
IF v_count = 1 AND classes(i).DAY_IS_HALF = 1 THEN
IF v_half != 1 THEN
RAISE_APPLICATION_ERROR(-20001,'You cannot schedule a class during another class''s time period of the same type at the same location');
end if;
end if;
END LOOP;
END AFTER STATEMENT;
END;
/
But as stated by #Connor McDonald, there are several design flaws - even in a single user environment.
A user may update the DAY_IS_HALF, I don't think the procedure covers all variants. Or a user updates END_DATE and by that, the new time intersects with existing classes.
Better avoid direct insert into the table and create a PL/SQL stored procedure in which you perform all the validations you need and then, if none of the validations fail, perform the insert. And grant execute on that procedure to the applications and do not grant applications insert on that table. That is a way to have all the data-related business rules in the database and make sure that no data that violates those rules in entered into the tables, no matter by what client application, for any client application will call a stored procedure to perform insert or update and will not perform DML directly on the table.
I think the main problem is the ambiguity of the role of the table TBL_CLASS_SCHED and the lack of clear definition of the DAY_IS_HALF column (morning, afternoon ?).
If the objective is to avoid 2 reservations of the same location at the same half day, the easiest solution is to use TBL_CLASS_SCHED to enforce the constraint with (start_date, location_id) being the primary key, morning reservation having start_date truncated at 00:00 and afternoon reservation having start_date set at 12:00, and you don't need end_date in that table, since each row represents an half day reservation.
The complete solution will then need a BEFORE trigger on TBL_CLASSES for UPDATE and INSERT events to make sure start_date and end_date being clipped to match the 00:00 and 12:00 rule and an AFTER trigger on INSERT, UPDATE and DELETE where you will calculate all the half-day rows to maintain in TBL_CLASS_SCHED. (So 1 full day reservation in TBL_CLASSES, will generate 2 rows in TBL_CLASS_SCHED). Since you maintain TBL_CLASS_SCHED from triggers set on TBL_CLASSES without any need to SELECT on the later, you don't have to worry about mutating problem, and because you will constraint start_date to be either at 00:00 or 12:00, the primary key constraint will do the job for you. You may even add a unique index on (start_date, classe_id) in TBL_CLASS_SCHED to avoid a classe to be scheduled at 2 locations at the same time, if you need to.
I spent hours to find an adequate solution for this problem, so I create this question in Q&A style after I found a solution.
I've got a table like this:
CREATE TABLE SoftwareVersion
(
ID NUMBER NOT NULL,
DeviceID NUMBER NOT NULL,
ReadoutDate DATE NOT NULL,
Version VARCHAR2(20 CHAR) NOT NULL,
NextReadoutDate DATE NULL
);
This table contains software version codes of a device. Each device can have one or more software versions. The import process, which executes the INSERT statements for this table, only fills ID, DeviceID, ReadoutDate and Version. The ReadoutDate is the current timestamp of the import process.
So the first occurence of a software version is assumed as "the software version is valid since the readout date".
My problem was now that I need a range for the software version. From which timestamp to which timestamp was each software version valid?
To improve performance (and since I already got triggers for that table) I added the NextReadoutDate column which shall be maintained by the triggers. It will receive the next valid ReadoutDate value for that DeviceID. So each software version will become a range (valid from ... to).
To avoid the mutating table problem (ORA-04091) I collect all updated information of the statement I am using an AFTER ROW trigger like this:
CREATE OR REPLACE TRIGGER TRG_SoftwareVersion1
AFTER INSERT OR UPDATE OR DELETE ON SoftwareVersion
FOR EACH ROW
BEGIN
IF UPDATING THEN
IF :OLD.DeviceID = :NEW.DeviceID AND :OLD.ReadoutDate = :NEW.ReadoutDate OR
(:OLD.DeviceID IS NULL OR :OLD.ReadoutDate IS NULL) AND
(:NEW.DeviceID IS NULL OR :NEW.ReadoutDate IS NULL) THEN
-- Nothing to do
RETURN;
END IF;
END IF;
-- Evaluate later
INSERT INTO
SoftwareVersion_TrgHelper
(
OldDeviceID,
OldReadoutDate,
NewDeviceID,
NewReadoutDate
)
VALUES
(
:OLD.DeviceID,
:OLD.ReadoutDate,
:NEW.DeviceID,
:NEW.ReadoutDate
);
END;
After that I update the table within an AFTER STATEMENT trigger like this:
CREATE OR REPLACE TRIGGER TRG_SoftwareVersion2
AFTER INSERT OR UPDATE OR DELETE ON SoftwareVersion
DECLARE
CURSOR cCursorMain IS SELECT * FROM SoftwareVersion_TrgHelper FOR UPDATE;
vOldDeviceID NUMBER;
vOldReadoutDate DATE;
vNewDeviceID NUMBER;
vNewReadoutDate DATE;
BEGIN
OPEN cCursorMain;
LOOP
FETCH cCursorMain INTO vOldDeviceID, vOldReadoutDate, vNewDeviceID, vNewReadoutDate;
EXIT WHEN cCursorMain%NOTFOUND;
IF UPDATING OR DELETING THEN
UPDATE
SoftwareVersion SV
SET
SV.NextReadoutDate = (SELECT MIN(SV2.ReadoutDate) KEEP (DENSE_RANK FIRST ORDER BY SV2.ReadoutDate ASC, SV2.ID ASC) FROM SoftwareVersion SV2 WHERE SV.DeviceID = SV2.DeviceID AND SV.ReadoutDate < SV2.ReadoutDate)
WHERE
SV.DeviceID = vOldDeviceID AND
SV.ReadoutDate <= vOldReadoutDate;
END IF;
IF UPDATING OR INSERTING THEN
UPDATE
SoftwareVersion SV
SET
SV.NextReadoutDate = (SELECT MIN(SV2.ReadoutDate) KEEP (DENSE_RANK FIRST ORDER BY SV2.ReadoutDate ASC, SV2.ID ASC) FROM SoftwareVersion SV2 WHERE SV.DeviceID = SV2.DeviceID AND SV.ReadoutDate < SV2.ReadoutDate)
WHERE
SV.DeviceID = vNewDeviceID AND
SV.ReadoutDate <= vNewReadoutDate;
END IF;
DELETE FROM SoftwareVersion_TrgHelper WHERE CURRENT OF cCursorMain;
END LOOP;
CLOSE cCursorMain;
END;
/
Unfortunately I got an ORA-00036 as soon as I was executing the import process again.
I needed a way to avoid the trigger recursion.
Oracle doesn't provide functions like TRIGGER_NESTLEVEL (as SQL server does) and it seems that there are not many functions to avoid recursion at all (you could use session variables or something like this, but this can cause other errors).
My simple solution is to let my triggers only trigger when specific columns get modified. This is possible, because my UPDATE statement within the trigger only modifies the NextReadoutDate column.
Instead of:
AFTER INSERT OR UPDATE OR DELETE ON SoftwareVersion
I use:
AFTER INSERT OR UPDATE OR DELETE OF DeviceID, ReadoutDate ON SoftwareVersion
And everything is fine then.
I have been trying to find a solution to use an If_Exists() style statement in Oracle PL SQL. I am trying to create a trigger which checks to see if a certain airsoft gun exists in the guns table when a member tries to input a new gun owned in the gunsOwned table. If the gun does not exist in the guns table, then it must be inputted to the table before the gun owned is inputted to the gunsOwned table or it will violate referential integrity as the Make and Model in gunsOwned are foreign keys to the Make and Model in the Guns table. However I keep getting Trigger created with compilation errors, and all of my attribute names are correct, so don't know why the select case statement is not working. Here is the code:
CREATE TRIGGER updateGuns
BEFORE INSERT ON GunsOwned
FOR EACH ROW
DECLARE
MemberAddingGun NUMBER;
NewMake VARCHAR2(30);
NewModel VARCHAR2(30);
BEGIN
MemberAddingGun := :NEW.OwnerID;
NewMake := :NEW.MakeOwned;
NewModel := :NEW.ModelOwned;
SELECT CASE gunExists
WHEN NOT EXISTS(SELECT Make, Model FROM Guns WHERE Make=NewMake AND Model=NewModel)
THEN
INSERT INTO Guns VALUES(NewMake, NewModel);
END
UPDATE Member
SET NumOfGuns = NumOfGuns+1
WHERE MemberID = MemberAddingGun;
END updateGuns;
.
RUN;
Could anyone help?
Thanks!
Use simple INSERT ... SELECT ... WHERE instead of CASE or IF statements:
INSERT INTO Guns( colname1, colname2 )
SELECT NewMake, NewModel FROM dual
WHERE NOT EXISTS(
SELECT null FROM Guns WHERE Make=NewMake AND Model=NewModel
);
BTW - on multiuser environment checking for not-existence of a record will always fail, since not commited records are not visible to SQL, and you will get duplicate records in Guns table.
In such a case you need some kind of synchronization.
There are a couple of options. First, you can handle this using a MERGE statement:
CREATE TRIGGER updateGuns
BEFORE INSERT ON GunsOwned
FOR EACH ROW
BEGIN
MERGE INTO GUNS
USING (SELECT MAKE, MODEL FROM GUNS) g
ON (g.MAKE = :NEW.MAKEOWNED AND g.MODEL = :NEW.MODELOWNED)
WHEN NOT MATCHED THEN
INSERT (MAKE, MODEL)
VALUES (:NEW.MAKEOWNED, :NEW.MODELOWNED);
UPDATE Member
SET NumOfGuns = NumOfGuns+1
WHERE MemberID = :NEW.OWNERID;
END UPDATEGUNS;
In this case the MERGE acts as a conditional INSERT, only adding a new row to GUNS if the specified make and model don't already exist in the table.
Alternatively, assuming that MAKE and MODEL are either the primary key or are a unique key on GUNS you can just go ahead and do the INSERT, trap the DUP_VAL_ON_INDEX exception thrown if a duplicate is found, and proceed merrily on your way:
CREATE TRIGGER updateGuns
BEFORE INSERT ON GunsOwned
FOR EACH ROW
BEGIN
BEGIN
INSERT INTO GUNS
(MAKE, MODEL)
VALUES
VALUES (:NEW.MAKEOWNED, :NEW.MODELOWNED);
EXCEPTION
WHEN DUP_VAL_ON_INDEX THEN
NULL; -- ignore the DUP_VAL_ON_INDEX exception
END;
UPDATE Member
SET NumOfGuns = NumOfGuns+1
WHERE MemberID = :NEW.OWNERID;
END UPDATEGUNS;
Personally, I don't like ignoring exceptions - I'd rather write code which doesn't raise exceptions - but it's your choice.
Best of luck.
Just use IF after setting up an appropriate flag:
DECLARE
v_flag number;
BEGIN
SELECT (CASE WHEN EXISTS (SELECT 1
FROM Guns
WHERE Make = :New.MakeOwned AND Model = :New.Model AND rownum = 1;
)
THEN 1 ELSE 0
END)
INTO v_flag
FROM DUAL;
IF v_flag = 0
THEN
INSERT INTO Guns(Make, Model) VALUES (:New.Make, :New.Model);
END IF;
UPDATE Member
SET NumOfGuns = NumOfGuns + 1
WHERE MemberID = :New.OwnerId;
END; -- updateGuns
I see no advantage to copying the fields in :NEW to local variables. In fact, it makes the code a bit harder to follow, because the reader has to check if the values are different from the values in the :NEW record.
That said, an alternative is to have a unique index on Guns(Make, Model), attempt an insert and just ignore the error using exceptions.
I need to set a constraint that the user is unable to enter any records after he/she has entered 5 records in a single month. Would it be advisable that I write a trigger or procedure for that? Else is that any other ways that I can setup the constraint?
Instead of writing a trigger i have opt to write a procedure for the constraint but how do i check if the procedure is working?
Below is the procedure:
CREATE OR REPLACE PROCEDURE InsertReadingCheck
(
newReadNo In Int,
newReadValue In Int,
newReaderID In Int,
newMeterID In Int
)
AS
varRowCount Int;
BEGIN
Select Count(*) INTO varRowCount
From Reading
WHERE ReaderID = newReaderID
AND Trunc(ReadDate,'mm') = Trunc(Sysdate,'mm');
IF (varRowCount >= 5) THEN
BEGIN
DBMS_OUTPUT.PUT_LINE('*************************************************');
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE(' You attempting to enter more than 5 Records ');
DBMS_OUTPUT.PUT_LINE('');
DBMS_OUTPUT.PUT_LINE('*************************************************');
ROLLBACK;
RETURN;
END;
ELSIF (varRowCount < 5) THEN
BEGIN
INSERT INTO Reading
VALUES(seqReadNo.NextVal, sysdate, newReadValue,
newReaderID, newMeterID);
COMMIT;
END;
END IF;
END;
Anyone can help me look through
This is the sort of thing that you should avoid putting in a trigger. Especially the ROLLBACK and the COMMIT. This seems extremely dangerous (and I'm not even sure whether it's possible). You might have other transactions that you wish to commit that you rollback or vice versa.
Also, by putting this in a trigger you are going to get the following error:
ORA-04091: table XXXX is mutating, trigger/function may not see it
There are ways round this but they're excessive and involve doing something funky in order to get round Oracle's insistence that you do the correct thing.
This is the perfect opportunity to use a stored procedure to insert data into your table. You can check the number of current records prior to doing the insert meaning that there is no need to do a ROLLBACK.
It depends upon your application, if insert is already present in your application many times then trigger is better option.
This is a behavior constraint. Its a matter of opinion but I would err on the side of keeping this kind of business logic OUT of your database. I would instead keep track of who added what records in the records table, and on what day/times. You can have a SP to get this information, but then your code behind should handle whether or not the user can see certain links (or functions) based on the data that's returned. Whether that means keeping the user from accessing the page(s) where they insert records, or give them read only views is up to you.
One declarative way you could solve this problem that would obey all concurrency rules is to use a separate table to keep track of number of inserts per user per month:
create table inserts_check (
ReaderID integer not null,
month date not null,
number_of_inserts integer constraint max_number_of_inserts check (number_of_inserts <= 5),
primary key (ReaderID, month)
);
Then create a trigger on the table (or all tables) for which inserts should be capped at 5:
create trigger after insert on <table>
for each row
begin
MERGE INTO inserts_check t
USING (select 5 as ReaderID, trunc(sysdate, 'MM') as month, 1 as number_of_inserts from dual) s
ON (t.ReaderID = s.ReaderID and t.month = s.month)
WHEN MATCHED THEN UPDATE SET t.number_of_inserts = t.number_of_inserts + 1
WHEN NOT MATCHED THEN INSERT (ReaderID, month, number_of_inserts)
VALUES (s.ReaderID, s.month, s.number_of_inserts);
end;
Once the user has made 5 inserts, the constraint max_number_of_inserts will fail.
Do want to create Stored procc which updates or inserts into table based on the condition if current line does not exist in table?
This is what I have come up with so far:
PROCEDURE SP_UPDATE_EMPLOYEE
(
SSN VARCHAR2,
NAME VARCHAR2
)
AS
BEGIN
IF EXISTS(SELECT * FROM tblEMPLOYEE a where a.ssn = SSN)
--what ? just carry on to else
ELSE
INSERT INTO pb_mifid (ssn, NAME)
VALUES (SSN, NAME);
END;
Is this the way to achieve this?
This is quite a common pattern. Depending on what version of Oracle you are running, you could use the merge statement (I am not sure what version it appeared in).
create table test_merge (id integer, c2 varchar2(255));
create unique index test_merge_idx1 on test_merge(id);
merge into test_merge t
using (select 1 id, 'foobar' c2 from dual) s
on (t.id = s.id)
when matched then update set c2 = s.c2
when not matched then insert (id, c2)
values (s.id, s.c2);
Merge is intended to merge data from a source table, but you can fake it for individual rows by selecting the data from dual.
If you cannot use merge, then optimize for the most common case. Will the proc usually not find a record and need to insert it, or will it usually need to update an existing record?
If inserting will be most common, code such as the following is probably best:
begin
insert into t (columns)
values ()
exception
when dup_val_on_index then
update t set cols = values
end;
If update is the most common, then turn the procedure around:
begin
update t set cols = values;
if sql%rowcount = 0 then
-- nothing was updated, so the record doesn't exist, insert it.
insert into t (columns)
values ();
end if;
end;
You should not issue a select to check for the row and make the decision based on the result - that means you will always need to run two SQL statements, when you can get away with one most of the time (or always if you use merge). The less SQL statements you use, the better your code will perform.
BEGIN
INSERT INTO pb_mifid (ssn, NAME)
select SSN, NAME from dual
where not exists(SELECT * FROM tblEMPLOYEE a where a.ssn = SSN);
END;
UPDATE:
Attention, you should name your parameter p_ssn(distinguish to the column SSN ), and the query become:
INSERT INTO pb_mifid (ssn, NAME)
select P_SSN, NAME from dual
where not exists(SELECT * FROM tblEMPLOYEE a where a.ssn = P_SSN);
because this allways exists:
SELECT * FROM tblEMPLOYEE a where a.ssn = SSN