views:

149

answers:

5

Oracle 10g DB. I have a table called s_contact. This table has a field called person_uid. This person_uid field is a varchar2 but contains valid numbers for some rows and in-valid numbers for other rows. For instance, one row might have a person_uid of '2-lkjsdf' and another might be 1234567890.

I want to return just the rows with valid numbers in person_uid. The SQL I am trying is...

select person_uid 
from s_contact 
where decode(trim(translate(person_uid, '1234567890', ' ')), null, 'n', 'c') = 'n'

The translate replaces all numbers with spaces so that a trim will result in null if the field only contained numbers. Then I use a decode statement to set a little code to filter on. n=number, c=char.

This seems to work when I run just a preview, but I get an 'invalid number' error when I add a filter of...

and person_uid = 100
-- or
and to_number(person_uid) = 100

I just don't understand what is happening! It should be filtering out all the records that are invalid numbers and 100 is obviously a number...

Any ideas anyone? Greatly Appreciated!

+3  A: 

When you add those numbers to the WHERE clause it's still doing those checks. You can't guarantee the ordering within the WHERE clause. So, it still tries to compare 100 to '2-lkjsdf'.

Can you use '100'?

Tom H.
An external system actually writes the value that will be filtered by and this value is always a number. Thus I need to convert the string to a number in this SQL which will be part of a view. Thanks though!
Nate
Are you saying that the 100 is provided by an external system? Then just change the query to:WHERE person_uid = CAST(@value_from_external_system AS VARCHAR)
Tom H.
A: 

Use the first part of your query to generate a temp table. Then query the temp table based on person_uid = 100 or whatever.

The problem is that oracle tries to convert each person_uid to an int as it gets to it due to the additional and statement in your where clause. This behavior may or may not show up in the preview depending on what records where picked.

Chris Lively
Unfortunately, this SQL is part of a view so I need this to be one statement. Thanks for the idea though!
Nate
+2  A: 

Another option is to apply a subselect

SELECT * FROM (
 select person_uid 
 from s_contact 
 where decode(trim(translate(person_uid, '1234567890', ' ')), null, 'n', 'c') = 'n'
)
WHERE TO_NUMBER(PERSON_UID) = 100
Dan
Unfortunately, this doesn't guarantee that the inner subquery is executed first and that only the rows with all numeric data are considered before applying the PERSON_UID filter
Justin Cave
Right, I had the same idea, but I still get the 'invalid number' error. Thanks for trying!
Nate
+3  A: 

Unfortunately, the various subquery approaches that have been proposed are not guaranteed to work. Oracle is allowed to push the predicate into the subquery and then evaluate the conditions in whatever order it deems appropriate. If it happens to evaluate the PERSON_UID condition before filtering out the non-numeric rows, you'll get an error. Jonathan Gennick has an excellent article Subquery Madness that discusses this issue in quite a bit of detail.

That leaves you with a few options

1) Rework the data model. It's generally not a good idea to store numbers in anything other than a NUMBER column. In addition to causing this sort of issue, it has a tendency to screw up the optimizer's cardinality estimates which leads to less than ideal query plans.

2) Change the condition to specify a string value rather than a number. If PERSON_UID is supposed to be a string, your filter condition could be PERSON_UID = '100'. That avoids the need to perform the implicit conversion.

3) Write a custom function that does the string to number conversion and ignores any errors and use that in your code, i.e.

CREATE OR REPLACE FUNCTION my_to_number( p_arg IN VARCHAR2 )
  RETURN NUMBER
IS
BEGIN
  RETURN to_number( p_arg );
EXCEPTION
  WHEN others THEN
    RETURN NULL;
END;

and then my_to_number(PERSION_UID) = 100

4) Use a subquery that prevents the predicate from being pushed. This can be done in a few different ways. I personally prefer throwing a ROWNUM into the subquery, i.e. building on OMG Ponies' solution

WITH valid_persons AS (
  SELECT TO_NUMBER(c.person_uid) 'person_uid',
         ROWNUM rn
    FROM S_CONTACT c
   WHERE REGEXP_LIKE(c.personuid, '[[:digit:]]'))
SELECT *
  FROM valid_persons vp
 WHERE vp.person_uid = 100

Oracle can't push the vp.person_uid = 100 predicate into the subquery here because doing so would change the results. You could also use hints to force the subquery to be materialized or to prevent predicate pushing.

Justin Cave
1) Reworking the model it not an option.2) I cant change the value being filtered on to be a string because it is actually an external system that is doing the filtering and it's dtype is a number.3, 4) I will try the WITH clause. If that is not compatible with a view (which is actually where this SQL will end up) then I will give the custom to_number function a try.Thanks! Ill let ya know.
Nate
The WITH isn't special here, you'd get the same result with an inline view, either using regular expressions or using your translate logic. The critical bit is that you need to do something in the inline view to prevent the predicate from being pushed into the inline view. That can be a hint or, as I showed, by adding something like ROWNUM that prevents predicate pushing.
Justin Cave
+1  A: 

Another alternative is to combine the predicates:

where case when translate(person_uid, '1234567890', ' ')) is null 
  then to_number(person_uid) end = 100
Gary