Why is no_data_found ORA-01403 an exception in Oracle? - sql

If the SELECT INTO statement doesn't return at least one row, ORA-01403 is thrown.
For every other DBMS I know this is normal on a SELECT.
Only Oracle treats a SELECT INTO like this.
CREATE OR REPLACE PROCEDURE no_data_proc IS
dummy dual.dummy%TYPE;
BEGIN
BEGIN
SELECT dummy
INTO dummy
FROM dual
WHERE dummy = 'Y';
EXCEPTION
WHEN no_data_found THEN
dbms_output.put_line('Why is this needed?');
END;
END no_data_proc;
Why?
In my opinion you don't need this exception really. It is too much overhead.
Sometimes it is handy but you have to write a whole BEGIN, EXCEPTION, WHEN, END Block.
Are there any essential reasons I don't see?

The exception block is not needed, you might use it or not, depending on the context.
Here you are actively ignoring the exception (the procedure will return successfully) but most of the time if you're doing a SELECT INTO you want it to fail if it doesn't return a row, consider:
PROCEDURE update_employee_salary (p_empno) IS
l_salary NUMBER;
BEGIN
SELECT sal INTO l_salary FROM emp WHERE empno = p_empno FOR UPDATE;
/* do something with emp data */
END;
Here I want my function to fail if it is called with an empno that doesn't exist in the EMP table. I might catch the exception to raise a meaningful error message (with raise_application_error) but most of the time I'm happy with the ORA-01403.
In general, the only exceptions you should catch are the expected exceptions (i.e. this should not be the standard to catch all ORA-01403, or all exceptions for that matter).

But we still need to answer the question of "why is an exception thrown in the case where a SELECT has no data to be retrieved".
I believe this is done because it's a common situation which might otherwise be overlooked. Writing code as though it always expects to find data is a common thing to do, and if we were supposed to put in error checks such as
SELECT <something...>
IF SQLCODE = 100 THEN -- No data found
<no-data handler>
END IF
it is likely IMHO that the check for SQLCODE = 100 would be skipped frequently. Having an exception raised rams it right up your nose that A) an important condition (no data found) occurred, and B) NO ALLOWANCE WAS MADE FOR THIS. IMO having the PL/SQL engine raise an exception is better than having the program continue merrily on its way under the assumption that data was retrieved when in fact it wasn't, which can lead to all sorts of other-than-merry problems.
Share and enjoy.

You can try use MIN for avoid use EXCEPTION clause.
SELECT MIN(dummy)
INTO dummy
FROM dual
WHERE dummy = 'Y';
then dummy variable will be NULL

Because you are doing SELECT INTO which requires exactly one row (more rows would also be an error).
If you can have one or no row, you can use a cursor.
It is not the database's job to decide for you that a missing row is not an error, and just set the value to null.

You can also use the sql MAX or MIN functions. If no row is return then these functions will return a NULL.
For example:
Select MAX(column1)
Into variable
From Table
Where Column1 = 'Value';
The MAX function will return the Maximum value or if no row is returned then it will return NULL.

MAX function works it does not throws error ORA-01403 works when NULL is returned by select INTO

Because it's not clear what the PL/SQL engine should do - should it exit the block? Should it press on with NULL in the variable? What if in the next block you try to insert that into a NOT NULL column, how should it report the location of the error? Making it an exception forces you to be explicit about it.

Related

Using COALESCE vs EXCEPTION when no rows are returned (Oracle)

I am currently running into an error where I receive a 'NO DATA FOUND' error message when I'm running a stored procedure that is defining/selecting into Variables that I have set-up.
I've tried two different methods. One coalescing the column and null (as shown below):
BEGIN
BEGIN
SELECT COALESCE (max_value, 0) AS MAX_VALUE
INTO v_max_v
FROM table1
WHERE car_code = rcar;
--
SELECT COALESCE (min_value, 0) AS MIN_VALUE
INTO v_in_v
FROM table1
WHERE car_code = rcar;
END
END;
Another Method is to handle the exception where no rows are returned for EACH variable.
BEGIN
BEGIN
SELECT max_value AS MAX_VALUE
INTO v_max_v
FROM table1
WHERE car_code = rcar;
EXCEPTION
WHEN NO_DATA_FOUND
THEN v_max_v := NULL;
END
--
SELECT min_value AS MIN_VALUE
INTO v_min_v
FROM table1
WHERE car_code = rcar;
EXCEPTION
WHEN NO_DATA_FOUND
THEN v_min_v := NULL;
END
END;
Which is the more efficient / less costly method? Is there a objective way to check for myself? (explain plans only work on queries i believe)
Thanks in advance.
NO_DATA_FOUND doesn't simply correspond to whether some value as null or not. It;s rather a default exception type used to notify/deal with if records exist or not.
Your comparison is invalid. The first one is not related to any exception at all: it's simply checking and replacing the value if null, and isn't dealing with any kind of exception. But in the other case the exception block is there but is of no use until the situation arises where no rows exist, not that the data value or column value is null.
In terms of performance the first one is faster though as there's no extra exception code added but you can embed the same also there won't be any difference.

Identifying the Weekday for an Order Date in PL/SQL as stored function

Code:
CREATE OR REPLACE FUNCTION DAY_ORD_SF
(
P_DATE_CREATED IN bb_basket.dtcreated%type
)
RETURN DATE AS
lv_date_created bb_basket.dtcreated%type;
BEGIN
SELECT to_char(to_date(P_DATE_CREATED,'yyyy-mm-dd'),'DAY') DAY_CREATED
INTO lv_date_created
FROM BB_BASKET
WHERE lv_date_created <= sysdate
ORDER BY lv_date_created ASC;
RETURN lv_date_created;
END DAY_ORD_SF;
/
SELECT IDBASKET, dtcreated date_created, to_char(DTCREATED,'DAY') DAY_CREATED, day_ord_sf(dtcreated) weekday_created
FROM BB_BASKET
order by DTCREATED asc;
This is my stored function I am working on as a task to practice stored function. I am really close to finishing this problem, but I am getting a no data found error. I'm not really understanding this error because when I run the code by itself it works. Basically this function is suppose to taking a date and return a varchar2 data type. I did have the "to_date(…,'yyyy-mm-dd'),.." gone before adding at that piece of code into the function.
First part of this task is to create a SELECT statement that lists the basket ID and weekday for every basket, and the second part of the task is to create a SELECT statement, using a GROUP BY clause to list the total number of baskets per weekday. Based on the results, what’s the most popular shopping day? Also I forgot to ask, if you can tell me why I am getting the "No data found" error that would be much appreciated!
Thanks for helping me out!
Well, there seems to be mutiple issues here.
Lets start with no data found problem. In case you use INTO within select statment and select returns no rows, you get an no data found exception. This can be handled via anonymouse begin end block with exception handler inside, but I believe this is not the case. As example:
declare
v_value number;
begin
select null
into v_value
from dual
where 1=2;
exception
when NO_DATA_FOUND then
null; -- Ignore exception and continue
end;
Condition 1=2 is never met and therefore the select always returns no rows, this will always produce no data found error. With exception handler we decide what to do next. Null will do nothing in this example.
Back to your function, your condition is if variable lv_date_created is less or equal to current date then do something. This will never work as lv_date_created will be at the moment of execution equal to null. It was just declared in your function and then used in your select. This will always lead to false and therefore select will always return no rows and no data found exception.
You also mentioned that you want the function to return varchar2 but your definition sais it returns date. Also variable lv_date_created is of type date and this is variable that is returned and you fill it with varchar2 value so apparently Oracle is doing some uncontroled conversion of the value of this is not throwing an exception about expected datatypes.
Also condition lv_date_created <= sysdate may indicate that select will find mutiple values and with INTO, this will cause too many rows exception if mutiple rows are found.
Now lets get to fixing this function. The main question is if you need to slect something in it or not. Selecting something like that usualy is used to check if entry in table exists and no data found exceptions would tell us that entry does not exist. If it is a general function to be used on mutiple places in your DB then I think select is not needed. I will include both solutions as example.
Note for this first one that you should no longer provide dtcreated colume on calling it but idbasket.
CREATE OR REPLACE FUNCTION DAY_ORD_SF
(
P_IDBASKET IN bb_basket.idbasket%type
)
RETURN VARCHAR2 AS
lv_date_created VARCHAR2(240);
BEGIN
SELECT to_char(dtcreated,'DAY') DAY_CREATED
INTO lv_date_created
FROM BB_BASKET
WHERE IDBASKET = P_IDBASKET;
RETURN lv_date_created;
END DAY_ORD_SF;
/
Or
CREATE OR REPLACE FUNCTION DAY_ORD_SF
(
P_DATE_CREATED IN bb_basket.dtcreated%type
)
RETURN VARCHAR2 AS
BEGIN
RETURN to_char(P_DATE_CREATED,'DAY');
END DAY_ORD_SF;
/

SQL Oracle - Procedure Syntax (School assignment)

I'm currently learning SQL and I'm having trouble with a procedure of mine. The procedure should calculate the average of a column called 'INDICE_METABO_PAT'. The information I need is in 3-4 different tables. Then when I do have the average calculated, I update a table to set this average to the corresponding entries. Here is the procedure. Note that everything else in my .sql file works : inserts, updates, selects, views, etc.
create or replace Procedure SP_UPDATE_INDICE_METABO_DV (P_NO_ETUDE in number)
is
V_SOMME number := 0;
V_NBPATIENT number := 0;
V_NO_ETUDE number := P_NO_ETUDE;
cursor curseur is
select PATIENT.INDICE_EFFICACITE_METABO_PAT
from ETUDE, DROGUE_VARIANT, ETUDE_PATIENT, PATIENT
where ETUDE.NO_DROGUE = DROGUE_VARIANT.NO_DROGUE
and ETUDE.NO_VAR_GEN = DROGUE_VARIANT.NO_VAR_GEN
and V_NO_ETUDE = ETUDE_PATIENT.NO_ETUDE
and ETUDE_PATIENT.NO_PATIENT = PATIENT.NO_PATIENT;
begin
open curseur;
fetch curseur into V_SOMME;
V_NBPATIENT := V_NBPATIENT + 1;
exit when curseur%NOTFOUND;
update DROGUE_VARIANT
set INDICE_EFFICACITE_METABO_DV = V_SOMME / V_NBPATIENT
where exists(select * from ETUDE, DROGUE_VARIANT, ETUDE_PATIENT, PATIENT
where ETUDE.NO_DROGUE = DROGUE_VARIANT.NO_DROGUE
and ETUDE.NO_VAR_GEN = DROGUE_VARIANT.NO_VAR_GEN
and V_NO_ETUDE = ETUDE_PATIENT.NO_ETUDE
and ETUDE_PATIENT.NO_PATIENT = PATIENT.NO_PATIENT);
end SP_UPDATE_INDICE_METABO_DV;
/
I'm getting an error : Procedure compiled , error check compiler log.
But I can't open the compiler log, and when my friend opens it, it points to weird places, like my create tables and such.
This is school stuff by the way, so it'll be nice if you could give an insight instead of a direct solution. Thanks alot.
Thanks alot in advance for your kind help !
To see the error you can do show errors after your procedure creation statement, or you can query the user_errors or all_errors views.
That will show something like:
LINE/COL ERROR
-------- ------------------------------------------------------------------------
20/4 PLS-00376: illegal EXIT/CONTINUE statement; it must appear inside a loop
20/4 PL/SQL: Statement ignored
You mentioned that when you checked the complier log, which shows the same information, "it points to weird places". Presumably you're looking at line 20 in your script. But this message is referring to line 20 of the PL/SQL code block, which is the exit when curseur%NOTFOUND;, which makes sense for the error message.
And as the message also says, and as #ammoQ said in a comment, that should be in a loop. If you're trying to manually calculate the average in a procedure as an exercise, instead of using the built-in aggregation functions, then you need to loop over all of the rows from your cursor:
open curseur;
loop
fetch curseur into V_SOMME;
exit when curseur%NOTFOUND;
V_NBPATIENT := V_NBPATIENT + 1;
end loop;
close curseur;
But as you'll quickly realise, you'll end up with the v_somme variable having the last value retrieved, not the sum of all the values. You need a separate variable keep to track of the sum - fetch each value into a variable, and add that to your running total, all within the loop. But as requested, not giving a complete solution.
As you're starting out you should really use ANSI join syntax, not the old from/where syntax you have now. It's a shame that is still being taught. So your cursor query should be something like:
select PATIENT.INDICE_EFFICACITE_METABO_PAT
from ETUDE_PATIENT
join ETUDE
-- missing on clause !
join DROGUE_VARIANT
on DROGUE_VARIANT.NO_DROGUE = ETUDE.NO_DROGUE
and DROGUE_VARIANT.NO_VAR_GEN = ETUDE.NO_VAR_GEN
join PATIENT
on PATIENT.NO_PATIENT = ETUDE_PATIENT.NO_PATIENT
where ETUDE_PATIENT.NO_ETUDE = P_NO_ETUDE;
... which shows you that you are missing a join condition between ETUDE_PATIENT and ETUDE - it's unlikely you want a cartesian product, and it's much easier to spot that missing join using this syntax than with what you had.
You need to look at your update statement carefully too, particularly the exists clause. That will basically always return true if the cursor found anything, so it will update every row in DROGUE_VARIANT with your calculated average, which presumably isn't what you want.
There is no correlation between the rows in the table you're updating and the subquery in that clause - the DROGUE_VARIANT in the subquery is independent of the DROGUE_VARIANT you're updating. By which I mean, it's the same table, obviously; but the update and the subquery are looking at the table separately and so are looking at different rows. It also has the same missing join condition as the cursor query.

ORACLE PL/SQL Variable Function Scope - Need Explanation

I just tripped over an answer to a problem I was having with a PL/SQL variable not being recognized by a function and I was hoping someone could explain to me why my solution worked and what is happening "underneath the hood".
Background
As part of an optimization project, I am trying to collect metrics on individual SQL scripts within a Stored Procedure. The Stored Proc that I am dissecting has an In-type date parameter that I need to define in order to run each individual SQL Script:
CREATE OR REPLACE myStoredProc (DATE_IN DATE, ERROR_OUT OUT VARCHAR2)
IS
BEGIN
--Truncate Temp Tables
--6 Individual SQL Scripts
EXCEPTION
--Error Handling
END;
To run each script individually, I decided to just drop each SQL statement into a PL/SQL block and feed the DATE_IN parameter in as a variable:
DECLARE
DATE_IN DATE := TO_DATE('16-JUL-2014','DD-MON-RR');
BEGIN
--Place individual script here
END;
The Problem
This approach worked fine for a couple of the queries that referred to this DATE_IN variable but one query with a reference to an outside function which takes DATE_IN as a parameter started throwing an ORA-00904 error:
DECLARE
DATE_IN DATE := TO_DATE('16-JUL-2014','DD-MON-RR');
BEGIN
insert into temp_table
SELECT table1.field1,
table1.field2,
table2.fieldA,
MyFunction(table1.field1, DATE_IN) --This was the problem line
FROM
table1,
table2
WHERE EXISTS (inner query)
AND table1.keys = table2.keys
AND table2.date <= DATE_IN
END;
Solution
At the advice of another Developer, I was able to get around this error by adding a colon (:) in front of the DATE_IN variable that I was passing into the function so that the problem line read MyFunction(table1.field1, :DATE_IN). As soon as I did that, my error disappeared and I was able to run the query without issue.
I was happy for the result but the other Developer wasn't able to explain why it was needed, only that it was necessary to call any functions or other stored procs from a PL/SQL statement. I assume this has something to do with scope but I would like to get a better explanation as to why this colon was necessary for the function to see the variable.
Questions
I've tried to do a little research looking over Oracle documentation on parameters, variables, binding/declaring and constants but my research has only given me more questions:
After reading up on variables, I now question if that is the correct term for what I have been using (since I didn't actually use the VARIABLE command and I'm passing in a date - which is not an allowable data type). If my DATE_IN DATE := statement is not a variable, then what is it?
Why were the rest of my references to DATE_IN recognized by the compiler but passing the value to the function was out of scope?
What exactly is the colon (:) doing here? Is this turning that into a bind variable?
Thanks in advance. I appreciate any guidance you can provide!
----------------------------------EDIT--------------------------------------
I was asked to provide additional information. My Db version is 11G, 11.2.0.2.0. The query that I was able to reproduce this error is below.
DECLARE
EXTRACT_DT_IN DATE := TO_DATE('16-JUL-2014','DD-MON-RR');
BEGIN
--This begins the pre-optimized query that I'm testing
insert into AELI_COV_TMP_2_OPT
SELECT /*+ ordered use_nl(CM MAMT) INDEX (CM CSMB_CSMB2_UK) INDEX (MAMT (MBAM_CSMB_FK_I) */
CM.CASE_MBR_KEY
,CM.pyrl_no
,MAMT.AMT
,MAMT.FREQ_CD
,MAMT.HOURS
,aeli$cov_pdtodt(CM.CASE_MBR_KEY, EXTRACT_DT_IN)
FROM
CASE_MEMBERS CM
,MEMBER_AMOUNTS MAMT
WHERE EXISTS (select /*+ INDEX(SDEF SLRY_BCAT_FK_I) */
'x'
from SALARY_DEF SDEF
where SDEF.CASE_KEY = CM.CASE_KEY
AND SDEF.TYP_CD = '04'
AND SDEF.SLRY_KEY = MAMT.SLRY_KEY)
AND CM.CASE_MBR_KEY = MAMT.CASE_MBR_KEY
AND MAMT.STAT_CD = '00'
AND (MAMT.xpir_dt is null or MAMT.xpir_dt > EXTRACT_DT_IN)
AND MAMT.eff_dt <= EXTRACT_DT_IN;
--This ends the pre-optimized query that I'm testing
END;
Here is the error I'm encountering when trying to run an Explain Plan on this statement. I am able to get past this error if I remove reference to line 13 or I add a colon (:) to the EXTRACT_DT_IN on that line.
----------------------EDIT 2-------------------
Here is the function signature of aeli$.cov_pdtodt. (I've replaced the owner for security reasons).
CREATE OR REPLACE function __owner__.aeli$cov_pdtodt
(CASE_MBR_KEY_IN IN NUMBER, EXTRACT_EFF_DT_IN DATE)
RETURN DATE IS
PDTODT DATE;
Your anonymous block is fine as it is, as long as you execute the whole block. If you try to execute just the insert, or its select, as a standalone command then it will indeed fail with ORA-00904.
That isn't quite a scope problem, it's a context problem. You're trying to refer to a PL/SQL variable in an SQL context, and that is never going to work.
In a PL/SQL context this would work:
declare
some_var dual.dummy%type := 'X';
begin
insert into some_table
select dummy from dual where dummy = some_var;
end;
/
... because the insert has access to the PL/SQL some_var.
In an SQL context this will error:
select * from dual where dummy = some_var;
... because it's looking for a column called SOME_VAR, and there isn't one.
If you do this instead:
select * from dual where dummy = :some_var;
... the some_var is now a client-managed bind variable. If you execute that you'll either be prompted for the bind value, or given a not-all-variables-bound error, or bind-variable-not-declared, or similar, depending on your client.
If you only do an explain plan of that though, e.g. with
set auto trace traceonly explain
select * from dual where dummy = :some_var;
... then the bind variable doesn't necessarily have to be populated for the plan to be calculated. Some clients may still complain and want a bind value, but the parser would be OK with it - enough to produce a plan anyway. Though not able to take advantage of bind variable peeking or histograms etc.
For example, SQL Developer happily produces a plan for your original sample query if both references are turned into bind variables, just the insert ... part of the block is selected, and you press Explain Plan (F10).
I'm not sure what you read, but you're mixed up on a few things here.
Your DATE_IN is a variable. You don't need to type 'VARIABLE' anywhere to declare a variable, all you need is the name of the variable and the datatype.
All of the below are legitimate variables in PL/SQL (although poorly named).
variable_1 NUMBER;
variable_2 VARCHAR2(100);
variable_3 DATE;
It's hard to tell what you're doing in your code without seeing it all. Do you have two DATE_IN variables declared within the same block? Is DATE_IN the name of a column in your table?
If you have a column named DATE_IN in table1 or table2, that's likely your problem. Oracle doesn't know if you want to use your variable or your column, and it will always default to the column name. Your function would be expecting a DATE and receiving a column, hence the error.

How bad is ignoring Oracle DUP_VAL_ON_INDEX exception?

I have a table where I'm recording if a user has viewed an object at least once, hence:
HasViewed
ObjectID number (FK to Object table)
UserId number (FK to Users table)
Both fields are NOT NULL and together form the Primary Key.
My question is, since I don't care how many times someone has viewed an object (after the first), I have two options for handling inserts.
Do a SELECT count(*) ... and if no records are found, insert a new record.
Always just insert a record, and if it throws a DUP_VAL_ON_INDEX exceptions (indicating that there already was such a record), just ignore it.
What's the downside of choosing the second option?
UPDATE:
I guess the best way to put it is : "Is the overhead caused by the exception worse than the overhead caused by the initial select?"
I would normally just insert and trap the DUP_VAL_ON_INDEX exception, as this is the simplest to code. This is more efficient than checking for existence before inserting. I don't consider doing this a "bad smell" (horrible phrase!) because the exception we handle is raised by Oracle - it's not like raising your own exceptions as a flow-control mechanism.
Thanks to Igor's comment I have now run two different benchamrks on this: (1) where all insert attempts except the first are duplicates, (2) where all inserts are not duplicates. Reality will lie somewhere between the two cases.
Note: tests performed on Oracle 10.2.0.3.0.
Case 1: Mostly duplicates
It seems that the most efficient approach (by a significant factor) is to check for existence WHILE inserting:
prompt 1) Check DUP_VAL_ON_INDEX
begin
for i in 1..1000 loop
begin
insert into hasviewed values(7782,20);
exception
when dup_val_on_index then
null;
end;
end loop
rollback;
end;
/
prompt 2) Test if row exists before inserting
declare
dummy integer;
begin
for i in 1..1000 loop
select count(*) into dummy
from hasviewed
where objectid=7782 and userid=20;
if dummy = 0 then
insert into hasviewed values(7782,20);
end if;
end loop;
rollback;
end;
/
prompt 3) Test if row exists while inserting
begin
for i in 1..1000 loop
insert into hasviewed
select 7782,20 from dual
where not exists (select null
from hasviewed
where objectid=7782 and userid=20);
end loop;
rollback;
end;
/
Results (after running once to avoid parsing overheads):
1) Check DUP_VAL_ON_INDEX
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.54
2) Test if row exists before inserting
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.59
3) Test if row exists while inserting
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.20
Case 2: no duplicates
prompt 1) Check DUP_VAL_ON_INDEX
begin
for i in 1..1000 loop
begin
insert into hasviewed values(7782,i);
exception
when dup_val_on_index then
null;
end;
end loop
rollback;
end;
/
prompt 2) Test if row exists before inserting
declare
dummy integer;
begin
for i in 1..1000 loop
select count(*) into dummy
from hasviewed
where objectid=7782 and userid=i;
if dummy = 0 then
insert into hasviewed values(7782,i);
end if;
end loop;
rollback;
end;
/
prompt 3) Test if row exists while inserting
begin
for i in 1..1000 loop
insert into hasviewed
select 7782,i from dual
where not exists (select null
from hasviewed
where objectid=7782 and userid=i);
end loop;
rollback;
end;
/
Results:
1) Check DUP_VAL_ON_INDEX
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.15
2) Test if row exists before inserting
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.76
3) Test if row exists while inserting
PL/SQL procedure successfully completed.
Elapsed: 00:00:00.71
In this case DUP_VAL_ON_INDEX wins by a mile. Note the "select before insert" is the slowest in both cases.
So it appears that you should choose option 1 or 3 according to the relative likelihood of inserts being or not being duplicates.
I don't think there is a downside to your second option. I think it's a perfectly valid use of the named exception, plus it avoids the lookup overhead.
Try this?
SELECT 1
FROM TABLE
WHERE OBJECTID = 'PRON_172.JPG' AND
USERID='JCURRAN'
It should return 1, if there is one there, otherwise NULL.
In your case, it looks safe to ignore, but for performance, one should avoid exceptions on the common path. A question to ask, "How common will the exceptions be?"
Few enough to ignore? or so many another method should be used?
IMHO it is best to go with Option 2: Other than what is already been said, you should consider thread safety. If you go with option 1 and If multiple threads are executing your PL/SQL block then its possible that two or more threads fire select at the same time and at that time there is no record, this will end up leading all threads to insert and you will get unique constraint error.
Usually, exception handling is slower; however if it would happen only seldom, then you would avoid the overhead of the query.
I think it mainly depends on the frequency of the exception, but if performance is important, I would suggest some benchmarking with both approaches.
Generally speaking, treating common events as exception is a bad smell; for this reason you could see also from another point of view.
If it is an exception, then it should be treated as an exception - and your approach is correct.
If it is a common event, then you should try to explicitly handle it - and then checking if the record is already inserted.