It’s a long time since I wrote a note with the “philosophy” theme, but a recent complaint about Oracle prompted me to suggest the following thought:
“If you write SQL that is technically incorrect it’s not Oracle’s fault if sometimes the SQL completes without an error.”
Consider the following fragment of code:
drop table t1 purge;
create table t1 (n1 number, n2 number);
insert into t1 values (1,1);
commit;
select n1 from t1 where n2 = to_number('x');
select n1 from t1 where n2 = to_number('x') and n1 = 2;
select
case
when
0 in (select n1 from t1 where n2 = to_number('x'))
then
1
else
0
end
from
dual
;
Clearly the first query must raise an error because ‘x’ can’t be converted to a number (until Oracle supplies a format option to read it as a Roman Numeral).
Clearly the second query must raise an error because it’s just the first query with an extra predicate on the end.
Clearly the third query must raise an error because it’s going to execute a subquery that must raise an error.
Here’s the output from running the code from SQL*Plus (on 12.2.0.1).
Table dropped.
Table created.
1 row created.
Commit complete.
select n1 from t1 where n2 = to_number('x')
*
ERROR at line 1:
ORA-01722: invalid number
no rows selected
CASEWHEN0IN(SELECTN1FROMT1WHEREN2=TO_NUMBER('X'))THEN1ELSE0END
--------------------------------------------------------------
0
1 row selected.
The first query fails with the expected conversion error: ORA-01722: invalid number. The second and third queries run to completion because SQL is a declarative language, not a procedural language, and the optimizer is allowed to transform your query in any way that it thinks might make it complete more quickly.
Here’s the execution plan – with predicate information – for the second query:
-------------------------------------+-----------------------------------+
| Id | Operation | Name | Rows | Bytes | Cost | Time |
-------------------------------------+-----------------------------------+
| 0 | SELECT STATEMENT | | | | 2 | |
| 1 | TABLE ACCESS FULL | T1 | 1 | 26 | 2 | 00:00:01 |
-------------------------------------+-----------------------------------+
Predicate Information:
----------------------
1 - filter(("N1"=2 AND "N2"=TO_NUMBER('x')))
The optimizer has decided that it’s more efficent to test for the constant 2 than it is to call the to_number() function, so it’s evaluated n1 = 2 first for each row and never had to check the second predicate because nothing got past the first.
The explanation for the successful completion of the third query is slightly different, but again it revolves around transforming for efficiency. Oracle will (very often) convert an IN subquery to an EXISTS subquery. In my example the resulting SQL looks like this (taken from the CBO (10053) trace file, with some cosmeticy enhancement):
SELECT
CASE
WHEN
EXISTS (SELECT 0 FROM T1 WHERE N1=0 AND N2=TO_NUMBER('x'))
THEN 1
ELSE 0
END "CASEWHEN0IN(SELECTN1FROMT1WHEREN2=TO_NUMBER('X'))THEN1ELSE0END"
FROM
DUAL
Note how the “guaranteed to fail” subquery has an extra predicate added as the IN subquery is transformed into an EXISTS subquery and, just like the previous example, the extra predicate is applied before the to_number() predicate, and there’s no data to match the first predicate so the to_number() function never gets called and never gets a chance to raise an exception.
You could argue, or course, that the optimizer should spot the attempt to generate a constant and evaluate it (if possible) at parse time and raise the error before the runtime engine even sees the query – and it wouldn’t be too hard to make a case for that – but it would only take a few seconds of thought to create slightly more complex examples to demonstrate the point I’ve been making.
Bottom line(s):
1) Use the correct datatypes for your data.
2) Don’t assume that a statement that raises an exception in some circumstances will result in an exception if it is subsequently embedded in a more complex statement. The optimizer is non-procedural and may transform your statement in a way that bypasses your bad design.
Footnote:
I probably shouldn’t say this – it will only encourage someone to do the wrong thing – but for the very simplest examples you can show that the ordered_predicates hint still works:
SQL> select /*+ ordered_predicates */ n1 from t1 where n2 = to_number('x') and n1 = 2;
select /*+ ordered_predicates */ n1 from t1 where n2 = to_number('x') and n1 = 2
*
ERROR at line 1:
ORA-01722: invalid number

nicely explained.
Comment by rsiz — January 23, 2020 @ 1:19 pm GMT Jan 23,2020 |