20

So I just spent 5 hours troubleshooting a problem which turned out to be due not only to the old unreliable ISNUMERIC but it looks like my problem only appears when the UDF in which ISNUMERIC is declared WITH SCHEMABINDING and is called within a stored proc (I've got a lot of work to do to distill it down into a test case, but my first need is to replace it with something reliable).

Any recommendations on good, efficient replacements for ISNUMERIC(). Obviously there really need to be variations for int, money, etc., but what are people using (preferably in T-SQL, because on this project, I'm restricted to SQL Server because this is a high-volume SQL Server to SQL Server data processing task)?

Tieson T.
  • 20,774
  • 6
  • 77
  • 92
Cade Roux
  • 88,164
  • 40
  • 182
  • 265
  • I have not been able to try it, but given my experience with EXTREMELY poor performing SQL UDFs (admittedly called for millions of rows), I'm inclined to give the .NET CLR a try and have awarded the bounty to that answer. – Cade Roux Feb 03 '09 at 04:06
  • So you awarded the bounty to an answer that was given back in November? Seems like the bounty was wasted then, no? – Sam Schutte Feb 03 '09 at 14:51
  • I was hoping that the bounty would spur someone to post a great solution that no one had thought of, yet I didn't think wasting half the bounty would be good. – Cade Roux Feb 03 '09 at 14:58
  • Well, I thought my extended stored procedure rocked! :) Hehehe...j/k. – Sam Schutte Feb 03 '09 at 17:14

11 Answers11

26

You can use the T-SQL functions TRY_CAST() or TRY_CONVERT() if you're running SQL Server 2012 as Bacon Bits mentions in the comments:

SELECT CASE WHEN TRY_CAST('foo' AS INT) IS NULL THEN 0 ELSE 1 END

SELECT CASE WHEN TRY_CAST(1 AS INT) IS NULL THEN 0 ELSE 1 END

If you're using SQL 2008 R2 or older, you'll have to use a .NET CLR function, and wrap System.Decimal.TryParse().

Dave Markle
  • 95,573
  • 20
  • 147
  • 170
  • 1
    @le dorfier: Is it? I would expect this to be faster than anything but the most trivial T-SQL expressions. But I have no numbers, it's a gut feeling only. Can you point to a resource that has some numbers? – Tomalak Jan 27 '09 at 23:14
  • 1
    Note because this showed up on the front page in 2015: As of SQL Server 2012 you can use [`TRY_CAST()`](https://msdn.microsoft.com/en-us/library/hh974669.aspx) or [`TRY_CONVERT()`](https://msdn.microsoft.com/en-us/library/hh230993.aspx). – Bacon Bits Nov 30 '15 at 00:01
  • @BaconBits: I did not know this. Thank you! Edited! – Dave Markle Dec 01 '15 at 14:45
17

Depending on the circumstances and the performance characteristics of the validation, I sometimes use a variation of the LIKE expression instead. For example:

NOT LIKE '%[^0-9]%'

Note that this specific example is fairly naive. It doesn't guarantee that the value is valid for conversion to a particular data type. It also doesn't allow for +/- signs or decimal points if you need those.

HTTP 410
  • 17,300
  • 12
  • 76
  • 127
  • 1
    This might actually be closest to what the original logic does in the system we are replacing. – Cade Roux Nov 23 '08 at 17:23
  • 1
    The asker stated this is a high volume task, to which NOT LIKE will definitely not scale well. – karlgrz Jan 28 '09 at 05:03
  • 4
    IsNumeric doesn't scale well either, and nor will writing your own UDF. This is inherently going to be a relatively slow check. – HTTP 410 Jan 28 '09 at 20:40
6

Another option might be to write an extended stored procedure in a language such as C, make it into a DLL and make it available to SQL Server.

I don't imagine it would take too many lines of code to do, and it probably would be faster than writing a Managed Stored Procedure in .NET, because you wouldn't incur the extra overheard from loading the CLR.

Here's a tidbit of information: http://msdn.microsoft.com/en-us/library/ms175200.aspx

Here's some C++ code that might work for you:

using namespace std;

int checkNumber() {
  int number = 0;
  cin >> number;
  cin.ignore(numeric_limits<int>::max(), '\n');

  if (!cin || cin.gcount() != 1)
    cout << "Not a number.";
  else
    cout << "Your entered: " << number;
  return 0;
}
Sam Schutte
  • 6,666
  • 6
  • 44
  • 54
3

Generally as a practice, I try not to let untyped data into the database, as it is better more suited to either handle it at the application layer, or for batch imports handle it in SQL Integration Services so that the data comes in typed correctly from the start.

I have had to do it many times in the past and usually the fastest way is to write your own user defined function to verify the data is in the format you expect, as most of the time the overhead to calling out to an extended stored proc or managed code for simple validation is slower than just doing it in T-SQL.

John Lemp
  • 5,029
  • 3
  • 28
  • 36
  • 1
    Yes, unfortunately, the business logic I have to duplicate on these (freeform columns) has not been applied at the ETLlevel of the datawarehouse and I have no choice. – Cade Roux Nov 23 '08 at 17:22
3

Following the .NET CLR route, you could go with a regular expression, particularly if you expect a certain range of numbers.

SQL 2005 and Regular Expressions

Lurker Indeed
  • 1,521
  • 1
  • 12
  • 21
2

According to Microsoft support the only efficient way to replace UDF function is to write your own .NET function version.

Of course, if your database admin allows that :).

Mine doesn't :(.

Svante
  • 50,694
  • 11
  • 78
  • 122
Grzegorz Gierlik
  • 11,112
  • 4
  • 47
  • 55
1

For SQL Server 2005 and above.... take advantage of try/catch...

declare @test varchar(10), @num decimal
select @test = '0123A'

begin try
    select @num = cast(@test as decimal)
    print '1'
end try 
begin catch
    print '0'
end catch

prints 0.

Change @test = '01234' or @test = '01234.5' and it prints 1.

RememberME
  • 2,092
  • 4
  • 37
  • 62
c'est moi
  • 19
  • 1
  • 2
    So do you want me to put this in a while loop and iterate over every record I'm checking? This is incompatible with set-based operations. You know, the kind of operations that SQL is designed to do. – marknuzz Oct 29 '15 at 01:40
1

Are you ever going to be handling number systems outside of your own (human) language, like Chinese etc? If so, I'd suggest using the libuninum library.

bugmagnet
  • 7,631
  • 8
  • 69
  • 131
1

How about implementing these two functions:

CREATE FUNCTION dbo.isReallyNumeric  
(  
    @num VARCHAR(64)  
)  
RETURNS BIT  
BEGIN  
    IF LEFT(@num, 1) = '-'  
        SET @num = SUBSTRING(@num, 2, LEN(@num))  

    DECLARE @pos TINYINT  

    SET @pos = 1 + LEN(@num) - CHARINDEX('.', REVERSE(@num))  

    RETURN CASE  
    WHEN PATINDEX('%[^0-9.-]%', @num) = 0  
        AND @num NOT IN ('.', '-', '+', '^') 
        AND LEN(@num)>0  
        AND @num NOT LIKE '%-%' 
        AND  
        (  
            ((@pos = LEN(@num)+1)  
            OR @pos = CHARINDEX('.', @num))  
        )  
    THEN  
        1  
    ELSE  
    0  
    END  
END  
GO  

CREATE FUNCTION dbo.isReallyInteger  
(  
    @num VARCHAR(64)  
)  
RETURNS BIT  
BEGIN  
    IF LEFT(@num, 1) = '-'  
        SET @num = SUBSTRING(@num, 2, LEN(@num))  

    RETURN CASE  
    WHEN PATINDEX('%[^0-9-]%', @num) = 0  
        AND CHARINDEX('-', @num) <= 1  
        AND @num NOT IN ('.', '-', '+', '^') 
        AND LEN(@num)>0  
        AND @num NOT LIKE '%-%' 
    THEN  
        1  
    ELSE  
        0  
    END  
END  
GO

Original Source

GateKiller
  • 74,180
  • 73
  • 171
  • 204
0

IsNumeric() seems to have problems with spaces, 'D', 'E', dollar signs and all sorts of other characters. What we typically want is something that tells us whether a CAST or CONVERT will succeed. This UDF, while not the fastest solution, has worked very well for me.

create function dbo.udf_IsNumeric(@str varchar(50))
  returns int
as
begin
  declare @rtn int
  select @rtn =
    case
      when ltrim(rtrim(@str)) in('.', '-', '-.', '+', '+.') then 0
      when ltrim(rtrim(@str)) like '%[^-+.0-9]%' then 0
      else isnumeric(@str)
    end
  return @rtn
end
mattmc3
  • 17,595
  • 7
  • 83
  • 103
  • I will add a tab to the line "when ltrim(rtrim(@str)) in ('.', '-', '-.', '+', '+.', CHAR(9)) then 0" Otherwise it is v. good. – AviFarah Dec 21 '17 at 23:26
0

SQL 2012 onwards, you can use TRY_PARSE() function instead of ISNUMERIC().

SELECT
 TRY_PARSE('123' as int) as '123'
,TRY_PARSE('abc' as int) as 'abc'
CleanBold
  • 1,551
  • 1
  • 14
  • 37