SQL Server 2017 Replacing Negative value without repeating long expression - sql-server

I have a long expression which may return a positive, negative or zero decimal value. I would like to do this:
SELECT CASE WHEN {long expression} < 0 THEN 0 ELSE {long expression} END
But I don't want to repeat the long expression. I would like something like ISNULL, such as
SELECT ISNEGATIVE({long expression}, 0)
But that doesn't seem to be a thing. Obviously GREATER would work but it's 2017.
I'm pretty sure I'm hosed, but was hoping for a miracle. Anyone?

Consider using a CROSS APPLY. They will allow you to reference aliases and stack calculations
Select CASE WHEN AVAL < 0 THEN 0 ELSE AVAL END
From YourTable A
Cross Apply ( values (longexpression) )B(AVAL)

Just for some variety...
You could round trip it through FORMAT as this allows positive values, negative values and zeroes to be treated differently.
SELECT CAST(FORMAT({long expression}, '#.########;\0;0') AS DECIMAL(18,8))
I'd just use the APPLY myself though as first port of call (rather than gratuitously calling a known slow function unnecessarily with some completely unwarranted string casting).

A CTE could be a good fit here. Something like:
with cte as (
select *,
longExpression = «long expression definition here»
from yourTable
)
select «other stuff»,
CASE WHEN longExpression < 0 THEN 0 ELSE longExpression END
from cte;

Related

SQL Server CHOOSE() function behaving unexpectedly with RAND() function

I've encountered an interesting SQL server behaviour while trying to generate random values in T-sql using RAND and CHOOSE functions.
My goal was to try to return one of two given values using RAND() as rng. Pretty easy right?
For those of you who don't know it, CHOOSE function accepts in an index number(int) along with a collection of values and returns a value at specified index. Pretty straightforward.
At first attempt my SQL looked like this:
select choose(ceiling((rand()*2)) ,'a','b')
To my surprise, this expression returned one of three values: null, 'a' or 'b'. Since I didn't expect the null value i started digging. RAND() function returns a float in range from 0(included) to 1 (excluded). Since I'm multiplying it by 2, it should return values anywhere in range from 0(included) to 2 (excluded). Therefore after use of CEILING function final value should be one of: 0,1,2. After realising that i extended the value list by 'c' to check whether that'd be perhaps returned. I also checked the docs page of CEILING and learnt that:
Return values have the same type as numeric_expression.
I assumed the CEILINGfunction returned int, but in this case would mean that the value is implicitly cast to int before being used in CHOOSE, which sure enough is stated on the docs page:
If the provided index value has a numeric data type other than int,
then the value is implicitly converted to an integer.
Just in case I added an explicit cast. My SQL query looks like this now:
select choose(cast(ceiling((rand()*2)) as int) ,'a','b','c')
However, the result set didn't change. To check which values cause the problem I tried generating the value beforehand and selecting it alongside the CHOOSE result. It looked like this:
declare #int int = cast(ceiling((rand()*2)) as int)
select #int,choose( #int,'a','b','c')
Interestingly enough, now the result set changed to (1,a), (2,b) which was my original goal. After delving deeper in the CHOOSE docs page and some testing i learned that 'null' is returned in one of two cases:
Given index is a null
Given index is out of range
In this case that would mean that index value when generated inside the SELECT statement is either 0 or above 2/3 (I'm assuming that negative numbers are not possible here and CHOOSE function indexes from 1). As I've stated before 0 should be one of possibilities of:
ceiling((rand()*2))
,but for some reason it's never 0 (at least when i tried it 1 million+ times like this)
set nocount on
declare #test table(ceiling_rand int)
declare #counter int = 0
while #counter<1000000
begin
insert into #test
select ceiling((rand()*2))
set #counter=#counter+1
end
select distinct ceiling_rand from #test
Therefore I assume that the value generated in SELECT is greater than 2/3 or NULL. Why would it be like this only when generated in SELECT statement? Perhaps order of resolving CAST, CELING or RAND inside SELECT is different than it would seem? It's true I've only tried it a limited number of times, but at this point the chances of it being a statistical fluctuation are extremely small. Is it somehow a floating-point error? I truly am stumbled and looking forward to any explanation.
TL;DR: When generating a random number inside a SELECT statement result set of possible values is different then when it's generated before the SELECT statement.
Cheers,
NFSU
EDIT: Formatting
You can see what's going on if you look at the execution plan.
SET SHOWPLAN_TEXT ON
GO
SELECT (select choose(ceiling((rand()*2)) ,'a','b'))
Returns
|--Constant Scan(VALUES:((CASE WHEN CONVERT_IMPLICIT(int,ceiling(rand()*(2.0000000000000000e+000)),0)=(1) THEN 'a' ELSE CASE WHEN CONVERT_IMPLICIT(int,ceiling(rand()*(2.0000000000000000e+000)),0)=(2) THEN 'b' ELSE NULL END END)))
The CHOOSE is expanded out to
SELECT CASE
WHEN ceiling(( rand() * 2 )) = 1 THEN 'a'
ELSE
CASE
WHEN ceiling(( rand() * 2 )) = 2 THEN 'b'
ELSE NULL
END
END
and rand() is referenced twice. Each evaluation can return a different result.
You will get the same problem with the below rewrite being expanded out too
SELECT CASE ceiling(( rand() * 2 ))
WHEN 1 THEN 'a'
WHEN 2 THEN 'b'
END
Avoid CASE for this and any of its variants.
One method would be
SELECT JSON_VALUE ( '["a", "b"]' , CONCAT('$[', FLOOR(rand()*2) ,']') )

Divide in query and error Arithmetic overflow error converting expression to data type int

I want to write this query (all of the fields are int):
select SUM(Service.discount+((Service.price*Factor.discount)/(Factor.amount-Factor.discount)))
But sometimes I get this error:
Arithmetic overflow error converting expression to data type int.
Other times I get this error:
Divide by zero error encountered.
How can I rewrite this query to avoid these errors?
//I Use this but agan overflow
select case when(Factor.amount-Factor.discount)<>0 then
Service.discount+((Service.price*Factor.discount)/(Factor.amount-Factor.discount))
else
Service.discount
end
from Factor inner join Service on Factor.code=Service.factorCode
Arithmetic overflow: don't use sum at all, take SUM off and take the brackets off either end.
Divide by zero: see Jonny's answer (I think he means //something as in whatever you want to do when factor.amount-factor.discount is zero....)
so maybe:
select case when discount2 <> 0 then discount+((price*discount)/(discount2)) else
discount+(price*discount) end FROM SERVICE
SELECT CASE
WHEN (Factor.amount-Factor.discount) <> 0
THEN
CONVERT(FLOAT,Service.discount+((Service.price*Factor.discount)/(Factor.amount-
Factor.discount)))
ELSE
Service.discount
END
FROM Factor INNER JOIN Service ON Factor.code=Service.factorCode
It might be better to decide how many decimal places you want to see:
CONVERT(decimal(10,2),Service.discount+((Service.price*Factor.discount)/(Factor.amount-Factor.discount)))
select
CASE (Factor.amount-Factor.discount)
WHEN 0
-- choose the result when Factor.amount-Factor.discount = 0 and replace this line
ELSE
SUM(Service.discount+((Service.price*Factor.discount)/
(Factor.amount-Factor.discount)))
END
...

Can I write a conditional AND to test for "ISNUMERIC" and then CAST a string to an int in T-SQL?

I thought (hoped) this would work but it doesn't, it evaluates both sided of the AND statement even if the left side is going to be false:
SELECT PropertyNumber
FROM Properties
WHERE PropertyNumber = '203a'
AND
(
NOT PropertyNumber LIKE '[^0-9]' AND CONVERT(INT,PropertyNumber) > 0
)
So I get:
Conversion failed when converting the nvarchar value '203a' to data type int.
Is there anyway to do conditional AND or any other way to solve this problem?
You can do this in two steps:
SELECT
OtherField, CAST(PropertyNumber AS INT) AS PropertyNumber
FROM
(SELECT OtherField, PropertyNumber FROM Properties WHERE ISNUMERIC(PropertyNumber) = 1) AS X
This will discard records with non-numeric property numbers. If you want to keep them...
SELECT
P.OtherField, P.PropertyNumber AS PropertyNumberAsText, CAST(X.PropertyNumber AS INT) AS PropertyNumberAsInt
FROM
Properties AS P
LEFT JOIN (SELECT DISTINCT PropertyNumber FROM Properties WHERE ISNUMERIC(PropertyNumber) = 1) AS X ON P.PropertyNumber = X.PropertyNumber
Because ISNUMERIC() approves of some values which cannot be cast to INT, I tend to use something like this:
CASE WHEN Field IN ('-', '.') THEN NULL
WHEN Field LIKE '%,%' THEN NULL
WHEN ISNUMERIC(Field) = 1 THEN CAST(Field AS INT) END
Note that this will reject values like "1,234", which is a valid integer in some regions. Code to identify and handle all possible formats for numeric values is a separate question, and has probably been asked on SO before.
try this:
SELECT PropertyNumber
FROM #Temp
WHERE PropertyNumber = '203a'
AND
(
case When IsNumeric('-' + PropertyNumber + 'e0') = 1
Then Convert(Int, PropertyNumber)
Else NULL
End > 0
)
The trick here is to use the knowledge that IsNumeric will return 1 only under certain conditions. By putting a '-' sign in front of your data, you are effectively making sure that property numbers are positive. By putting 'e0' after the column name, you are preventing floating point values.

T-sql - determine if value is integer

I want to determine if a value is integer (like TryParse in .NET). Unfortunatelly ISNUMERIC does not fit me because I want to parse only integers and not every kind of number. Is there such thing as ISINT or something?
Here is some code to make things clear. If MY_FIELD is not int, this code would fail:
SELECT #MY_VAR = CAST(MY_FIELD AS INT)
FROM MY_TABLE
WHERE MY_OTHER_FIELD = 'MY_FILTER'
Thank you
Here's a blog post describing the creation of an IsInteger UDF.
Basically, it recommends adding '.e0' to the value and using IsNumeric. In this way, anything that already had a decimal point now has two decimal points, causing IsNumeric to be false, and anything already expressed in scientific notation is invalidated by the e0.
In his article Can I convert this string to an integer?, Itzik Ben-Gan provides a solution in pure T-SQL and another that uses the CLR.
Which solution should you choose?
Is the T-SQL or CLR Solution Better? The advantage of using the T-SQL
solution is that you don’t need to go outside the domain of T-SQL
programming. However, the CLR solution has two important advantages:
It's simpler and faster. When I tested both solutions against a table
that had 1,000,000 rows, the CLR solution took two seconds, rather
than seven seconds (for the T-SQL solution), to run on my laptop. So
the next time you need to check whether a given string can be
converted to an integer, you can include the T-SQL or CLR solution
that I provided in this article.
If you only want to maintain T-SQL, then use the pure T-SQL solution. If performance is more important than convenience, then use the CLR solution.
The pure T-SQL Solution is tricky. It combines the built-in ISNUMERIC function with pattern-matching and casting to check if the string represents an int.
SELECT keycol, string, ISNUMERIC(string) AS is_numeric,
CASE
WHEN ISNUMERIC(string) = 0 THEN 0
WHEN string LIKE '%[^-+ 0-9]%' THEN 0
WHEN CAST(string AS NUMERIC(38, 0))
NOT BETWEEN -2147483648. AND 2147483647. THEN 0
ELSE 1
END AS is_int
FROM dbo.T1;
The T-SQL part of the CLR solution is simpler. You call the fn_IsInt function just like you would call ISNUMERIC.
SELECT keycol, string, ISNUMERIC(string) AS is_numeric,
dbo.fn_IsInt(string) AS is_int
FROM dbo.T1;
The C# part is simply a wrapper for the .NET's parsing function Int32.TryParse. This works because the SQL Server int and the .NET Int32 are both 32-bit signed integers.
using System;
using System.Data.SqlTypes;
public partial class UserDefinedFunctions
{
[Microsoft.SqlServer.Server.SqlFunction]
public static SqlBoolean fn_IsInt(SqlString s)
{
if (s.IsNull)
return SqlBoolean.False;
else
{
Int32 i = 0;
return Int32.TryParse(s.Value, out i);
}
}
};
Please read Itzik's article for a full explanation of these code samples.
With sqlserver 2005 and later you can use regex-like character classes with LIKE operator. See here.
To check if a string is a non-negative integer (it is a sequence of decimal digits) you can test that it doesn't contain other characters.
SELECT numstr
FROM table
WHERE numstr NOT LIKE '%[^0-9]%'
Note1: This will return empty strings too.
Note2: Using LIKE '%[0-9]%' will return any string that contains at least a digit.
See fiddle
WHERE IsNumeric(MY_FIELD) = 1 AND CAST(MY_FIELD as VARCHAR(5)) NOT LIKE '%.%'
That is probably the simplest solution. Unless your MY_FIELD contains .00 or something of that sort. In which case, cast it to a float to remove any trailing .00s
Necromancing.
As of SQL-Server 2012+, you can use TRY_CAST, which returns NULL if the cast wasn't successful.
Example:
DECLARE #foo varchar(200)
SET #foo = '0123'
-- SET #foo = '-0123'
-- SET #foo = '+0123'
-- SET #foo = '+-0123'
-- SET #foo = '+-0123'
-- SET #foo = '.123'
-- SET #foo = '1.23'
-- SET #foo = '.'
-- SET #foo = '..'
-- SET #foo = '0123e10'
SELECT CASE WHEN TRY_CAST(#foo AS integer) IS NULL AND #foo IS NOT NULL THEN 0 ELSE 1 END AS isInteger
This is the only really reliable way.
Should you need support for SQL-Server 2008, then fall back to Sam DeHaan's answer:
SELECT CASE WHEN ISNUMERIC(#foo + '.e0') = 1 THEN 1 ELSE 0 END AS isInteger
SQL-Server < 2012 (aka 2008R2) will reach end of (extended) support by 2019-07-09.
At this time, which is very soon, support for < 2012 can be dropped.
I wouldn't use any of the other hacks at this point in time anymore.
Just tell your frugal customers to update - it's been over 10 years since 2008.
See whether the below query will help
SELECT *
FROM MY_TABLE
WHERE CHARINDEX('.',MY_FIELD) = 0 AND CHARINDEX(',',MY_FIELD) = 0
AND ISNUMERIC(MY_FIELD) = 1 AND CONVERT(FLOAT,MY_FIELD) / 2147483647 <= 1
The following is correct for a WHERE clause; to make a function wrap it in CASE WHEN.
ISNUMERIC(table.field) > 0 AND PATINDEX('%[^0123456789]%', table.field) = 0
This work around with IsNumeric function will work:
select * from A where ISNUMERIC(x) =1 and X not like '%.%'
or Use
select * from A where x **not like** '%[^0-9]%'
declare #i numeric(28,5) = 12.0001
if (#i/cast(#i as int) > 1)
begin
select 'this is not int'
end
else
begin
select 'this is int'
end
As of SQL Server 2012, the TRY_CONVERT and TRY_CAST functions were implemented. Thise are vast improvements over the ISNUMERIC solution, which can (and does) give false positives (or negatives). For example if you run the below:
SELECT CONVERT(int,V.S)
FROM (VALUES('1'),
('900'),
('hello'),
('12b'),
('1.1'),
('')) V(S)
WHERE ISNUMERIC(V.S) = 1;
Using TRY_CONVERT (or TRY_CAST) avoids that:
SELECT TRY_CONVERT(int,V.S),
V.S,
ISNUMERIC(V.S)
FROM (VALUES('1'),
('900'),
('hello'),
('12b'),
('1.1'),
('')) V(S)
--WHERE TRY_CONVERT(int,V.S) IS NOT NULL; --To filter to only convertable values
Notice that '1.1' returned NULL, which cause the error before (as a string represtation of a decimal cannot be converted to an int) but also that '' returned 0, even though ISNUMERIC states the value "can't be converted".
Use TRY_CONVERT which is an SQL alternative to TryParse in .NET. IsNumeric() isn’t aware that empty strings are counted as (integer)zero, and that some perfectly valid money symbols, by themselves, are not converted to (money)zero. reference
SELECT #MY_VAR = CASE WHEN TRY_CONVERT(INT,MY_FIELD) IS NOT NULL THEN MY_FIELD
ELSE 0
END
FROM MY_TABLE
WHERE MY_OTHER_FIELD = 'MY_FILTER'
I think that there is something wrong with your database design. I think it is a really bad idea to mix varchar and numbers in one column? What is the reason for that?
Of course you can check if there are any chars other than [0-9], but imagine you have a 1m rows in table and your are checking every row. I think it won't work well.
Anyway if you really want to do it I suggest doing it on the client side.
I have a feeling doing it this way is the work of satan, but as an alternative:
How about a TRY - CATCH?
DECLARE #Converted as INT
DECLARE #IsNumeric BIT
BEGIN TRY
SET #Converted = cast(#ValueToCheck as int)
SET #IsNumeric=1
END TRY
BEGIN CATCH
SET #IsNumeric=0
END CATCH
select IIF(#IsNumeric=1,'Integer','Not integer') as IsInteger
This works, though only in SQL Server 2008 and up.
I tried this script and got the answer
ISNUMERIC(Replace(Replace([enter_your_number],'+','A'),'-','A') + '.0e0')
for example for up question this is answer:
SELECT #MY_VAR = CAST(MY_FIELD AS INT)
FROM MY_TABLE
WHERE MY_OTHER_FIELD = 'MY_FILTER' and ISNUMERIC(Replace(Replace(MY_FIELD,'+','A'),'-','A') + '.0e0') = 1
Why not just do something like:
CASE
WHEN ROUND(MY_FIELD,0)=MY_FIELD THEN CAST(MY_FIELD AS INT)
ELSE MY_FIELD
END
as MY_FIELD2
Sometimes you don't get to design the database, you just have to work with what you are given. In my case it's a database located on a computer that I only have read access to which has been around since 2008.
I need to select from a column in a poorly designed database which is a varchar with numbers 1-100 but sometimes a random string. I used the following to get around it (although I wish I could have re designed the entire database).
SELECT A from TABLE where isnumeric(A)=1
I am not a Pro in SQL but what about checking if it is devideable by 1 ?
For me it does the job.
SELECT *
FROM table
WHERE fieldname % 1 = 0
Use PATINDEX
DECLARE #input VARCHAR(10)='102030.40'
SELECT PATINDEX('%[^0-9]%',RTRIM(LTRIM(#input))) AS IsNumber
reference
http://www.intellectsql.com/post-how-to-check-if-the-input-is-numeric/
Had the same question. I finally used
where ATTRIBUTE != round(ATTRIBUTE)
and it worked for me
WHERE IsNumeric(value + 'e0') = 1 AND CONVERT(FLOAT, value) BETWEEN -2147483648 AND 2147483647
Seeing as this is quite old, but my solution isn't here, i thought to add another possible way to do this:
--This query only returns values with decimals
SELECT ActualCost
FROM TransactionHistory
where cast(ActualCost as int) != ActualCost
--This query only returns values without decimals
SELECT ActualCost
FROM TransactionHistory
where cast(ActualCost as int) = ActualCost
The easy part here is checking if the selected value is the same when cast as an integer.
we can check if its a non integer by
SELECT number2
FROM table
WHERE number2 LIKE '%[^0-9]%' and (( right(number2 ,len(number2)-1) LIKE '%[^0-9]%' and lefT(number2 ,1) <> '-') or ( right(number2 ,len(number2)-1) LIKE '%[^0-9]%' and lefT(number2 ,1) in ( '-','+') ) )
DECLARE #zip_code NCHAR(10)
SET #zip_code = '1239'
IF TRY_PARSE( #zip_code AS INT) / TRY_PARSE( #zip_code AS INT) = 1 PRINT 'integer'
ELSE PRINT 'not integer'
This works fine in SQL Server
SELECT (SELECT ISNUMERIC(2) WHERE ISNUMERIC(2)=1 AND 2 NOT LIKE '%.%')
Case
When (LNSEQNBR / 16384)%1 = 0 then 1
else 0
end

Natural sort for SQL Server?

I have a column that is typically only numbers (sometimes it's letters, but that's not important).
How can I make it natural sort?
Currently sorts like this: {1,10,11,12,2,3,4,5,6,7,8,9}
I want it to sort like this: {1,2,3,4,5,6,7,8,9,10,11,12}
IsNumeric is "broken", ISNUMERIC(CHAR(13)) returns 1 and CAST will fail.
Use ISNUMERIC(textval + 'e0'). Final code:
ORDER BY
PropertyName,
CASE ISNUMERIC(MixedField + 'e0') WHEN 1 THEN 0 ELSE 1 END, -- letters after numbers
CASE ISNUMERIC(MixedField + 'e0') WHEN 1 THEN CAST(MixedField AS INT) ELSE 0 END,
MixedField
You can mix order parameters...
Cast it. Also, don't forget to use IsNumeric to make sure you only get the numbers back (if they include letters it IS important ;).
SELECT textval FROM tablename
WHERE IsNumeric(textval) = 1
ORDER BY CAST(textval as int)
Also, cast to the datatype that will hold the largest value.
If you need the non-numbers in the result set too then just append a UNION query where IsNumeric = 0 (order by whatever you want) either before or after.
Have you tied using:
'OrderBy ColumnName Asc'
at the end of your query.

Resources