CASE in sql server -implementation Clarification? - sql-server

I have this simple table in sql server :
DECLARE #tbl table( a int , b NVARCHAR(100), isCalcByA bit)
INSERT INTO #tbl
SELECT 1,'c',1
UNION ALL
SELECT 2,'d',0
Ok.
If I run this :
SELECT CASE
WHEN isCalcByA = 1 THEN a
ELSE b
END FROM #tbl
It yields an error :
Msg 245, Level 16, State 1, Line 9
Conversion failed when converting the nvarchar value 'd' to data type int.
I can understand why it is happening :
Because the data which is being accumulated (to be displayed) can't attach both int and string at the same column .
Ok
But what about this :
SELECT 'dummy'
FROM #tbl
WHERE CASE
WHEN isCalcByA = 1 THEN a
ELSE b
END IS NOT NULL
Here -
I always display string
I don't accumulate different displaying results of different types.
I'm checking them against not null rather than a string or int value.
But still I get the same error .
What am I missing ?
NB
I know I can/should do this :
SELECT 'dummy'
FROM #tbl
WHERE
(isCalcByA = 1 AND a IS NOT NULL)
OR
(isCalcByA <> 1 AND b IS NOT NULL)
(which works fine)
But I'm asking why it is not working in the first CASE situation

CASE is an expression - it returns a value of a specific type. All possible values it might return must all be convertible to some common type. The system uses the type precedences rules to consider the types of all possible return values and decide what that common type is. int has higher precedence and wins.
CASE
WHEN isCalcByA = 1 THEN CONVERT(nvarchar(100),a)
ELSE b
END
would work because now the common type selected is unambiguously nvarchar(100).

No matter if you use CASE in the SELECT or the WHERE clause. CASE expressions should return the same datatype always. So, convert both columns to a datatype that can hold both:
CASE
WHEN isCalcByA = 1 THEN CAST(a AS NVARCHAR(100))
ELSE b
END
From the CASE expression documentation:
Returns the highest precedence type from the set of types in result_expressions and the optional else_result_expression.
When the various WHEN and the ELSE part have different datatypes as results, the highest precedence is chosen from this list: Data Type Precedence and all results are converted to that datatype.
Your queries fail because int has higher precedence than nvarchar.

DECLARE #tbl table( a int , b NVARCHAR(100), isCalcByA bit)
INSERT INTO #tbl
SELECT 1,'c',1
UNION ALL
SELECT 2,'d',0
UNION ALL
SELECT null,'d',1
SELECT CASE
WHEN isCalcByA = 1 THEN CAST(a AS VARCHAR(30))
ELSE b
END FROM #tbl
Above, you are selecting two different data types based on the select.
SELECT 'dummy'
FROM #tbl
WHERE CASE
WHEN isCalcByA = 1 THEN CAST(a AS VARCHAR(30))
ELSE b
END IS NOT NULL
Above, you are selecting 'dummy' every time, regardless of condition.
So, in the first statement, you are setting the return type based on the case and the case can return two different types. In the second query, the return type is always the same type.

Don't think about CASE like it is built-in IF from regular language. It's more like ... ? ... : ... operator with strong typing - it has to result in a specific singular type. If you want to mix columns you need to cast it to for example nvarchar.
You can also think about it like the result of SELECT should be possible to be defined by CREATE TABLE.

Related

Different aggregate functions depending on datatype

I have a T-SQL script that returns all columns in a table, along with datatype and max value MAX(DATALENGTH)) fetching it from sys.columns and sys.types.
However the max value will always be 4 for ints, since ints uses 4 bytes. In this case I'd rather have the highest numeric value of the column.
I figured I might change my query to use DataLength for string-based columns, and a MAX() for number based columns, however I run into some problems before I even get there:
Minified example code
DECLARE #A bit = 1
SELECT CASE WHEN 1=1 THEN MAX(DATALENGTH(#A)) ELSE MAX(#A) END
I would expect to receive the number 1 given that 1=1 is true.
Instead I get an error
Operand data type bit is invalid for max operator.
I understand that you can't run MAX(#A) on a bit, but that's not what I'm trying to do. My goal is to run different aggregate functions depending on the datatype.
How can I solve this?
My goal is to run different aggregate functions depending on the datatype.
This will fail because you will get invalid cast errors or will get implicit conversions to the highest precedence data type
Your use of bit is irrelevant here
smalldatetime has the highest precedence so this code gives odd results when mixing datatypes
DECLARE #foo table (
intval int,
floatval float,
datetimeval smalldatetime)
INSERT #foo VALUES
(1, 1.567E2, '2017-07-31'),
(2, 2.0, '2017-08-01');
DECLARE #Switch int;
SELECT
CASE
WHEN #Switch=1 THEN MAX(intval)
WHEN #Switch=2 THEN MAX(floatval)
ELSE MAX(datetimeval)
END
FROM
#foo
SET #Switch = 1
1900-01-03 00:00:00
SET #Switch = 2
1900-06-06 16:48:00
SET #Switch = 3
2017-08-01 00:00:00
In this case, you are missing a cast :
SELECT CASE WHEN 1=1 THEN MAX(DATALENGTH(#A)) ELSE MAX(CAST(#A as bigint)) END

Case Statement in Select Not Working

This is for SQL Server 2012 database...
I'm reading a varchar data column from a table, and depending on user-selected options the data could be either alphanumeric or numeric. I need to sort by this column so I'm trying to use a case statement, but it doesn't seem to be working. Below is a simplified example of what I'm doing, but as you can see, it's falling through to the Else of the case statement in both scenarios...any ideas on what I'm doing wrong?
Select '1st Grade Math' topic Into #temp
Declare #rptView int
Set #rptView = 1
Select Case #rptView
When 1 Then topic
Else cast(topic as int)
End
From #temp
Order by Case #rptView
When 1 Then topic
Else cast(topic as int)
End
Select Case
When #rptView = 1 Then topic
Else cast(topic as int)
End
From #temp
Order by Case #rptView
When 1 Then topic
Else cast(topic as int)
End
drop table #temp
Consider the following example based on your table:
Select Case 1 When 1 Then topic
Else 5
End
From #temp
It also fails with the following error:
Conversion failed when converting the varchar value '1st Grade Math' to data type int.
Why? Because every expression must have a well-defined data type. SQL Server deduces that the type of your first column is int, since the ELSE clause contains an int. Thus, it tries to convert topic to int as well, which fails.
In other words: You can't do it like that. The field in your result set can be varchar or int, not both.
Adding few more examples which may help..
declare #a int=1
declare #b varchar='b'
--this works
select
case when #a=1 then #a else #b end
--this also works
select
case when #a=2 then #b else #b end
--this fails
select
case when #a=1 then #b else #a end
--this fails
select
case when #a=2 then #a else #b end
Why ..?Because of data type precedence ,SQL tries to convert everything to type with higher precedence

SQL Server: Case statement is displaying Msg 8115, Arithmetic overflow error converting varchar to data type numeric

I have the following query which is throwing:
Arithmetic overflow error converting varchar to data type numeric.
Query:
Select
#Fee = Case
When IsNull(Fee, '') = '' Then 0.00
Else Fee
End
#Fee is of type Money, and Fee is Varchar type.
I have also observer that for following types of data in Then clause no error is being displayed.
Select #Fee = Case When IsNull(Fee, '') = '' Then 1 Else Fee End
Select #Fee = Case When IsNull(Fee, '') = '' Then 1.0 Else Fee End
So only for values 0.00 or 0.0 in Then clause I am getting error.
I have also tested with below query and worked fine:
Select #Fee = Case When IsNull(Fee, '') = '' Then Cast(0.00 as money) Else Fee End
And more interesting thing is that, as per data we have in table, Then part of the Case statement will never be executed. Please help me understanding this behavior of Case statement.
I have played around this and this is what happens:
DECLARE #v VARCHAR(20) = '1'
SELECT CASE WHEN '' <> '' THEN 0.00 ELSE #v END col1 INTO tempTable
When you will execute the above query you will see error but the table will be created and the type of the column created col1 is numeric(2,2). If you change to 0.0000 the type will be numeric(4,4). This means that actually the type of an expression depends on that value. Also (2,2) means that you can store only values with length 2 and everything goes after dot(.12, .25 etc). So it can not cast 1.00 to numeric(2,2) because the type doesn't allow to have digits before dot.
The best rule here is to always return the same types from different paths of case expression.
This is from Microsoft about return type of case expression (https://msdn.microsoft.com/en-us/library/ms181765.aspx):
Returns the highest precedence type from the set of types in
result_expressions and the optional else_result_expression. For more
information, see Data Type Precedence (Transact-SQL).
This is about type precedence where you can see that numeric precedes varchar(https://msdn.microsoft.com/en-us/library/ms190309.aspx). So the return type of your case expression becomes numeric(2,2) and this is the answer to your question.
I will also give you an advise: never store money values in varchar columns. Always store values in appropriate type(there are so many types available that all your needs will be satisfied).
You have a CASE expression that returns two different datatypes - that's always a really bad idea....
Select
#Fee = Case
When IsNull(Fee, '') = '' Then 0.00
Else Fee
End
When Fee is in fact NULL, you return 0.00 - a numerical value
When Fee (varchar) is NOT NULL, then you return that value - a string
Since both cases are assigned to one and the same #Fee variable - SQL Server must coerce these into the same datatype - whatever #Fee dictates (money in your case).
And for some reason, in the case of Fee being NOT NULL, that seems to fail at times.
So the point is: whenever possible, return the same datatype from all your possible values in a CASE statement - and do so explicitly (using a CAST or CONVERT) - don't force SQL Server to handle this for you
the code will reproduce your issue
DECLARE #Fee MONEY
DECLARE #test VARCHAR
SELECT #Fee = ISNULL(#test, 0.00)
Select #fee
but this one is the fix
DECLARE #Fee MONEY
DECLARE #test VARCHAR
SELECT #Fee = ISNULL(#test, '0.00')
Select #fee

t-sql view conversion failed when filtering on converted column

I am trying to create view by filtering some table, and include some converted to different type column into select list. View filter excludes from result set rows in which this column can not be converted to that type. Then I select rows from this view and filter rows using this converted column. And I always get error Conversion failed when converting the nvarchar value '2aaa' to data type int
SQL Fiddle
MS SQL Server 2008 Schema Setup:
create table _tmp_aaa (id int identity(1, 1), value nvarchar(max) not null)
go
insert _tmp_aaa (value) values ('1111'), ('11'), ('2aaa')
go
create view _tmp_v_aaa
as
select id, cast(value as int) as value from _tmp_aaa where value like '1%'
go
Query 1:
select * from _tmp_v_aaa where value = 11
Are there any workarounds?
Add to your view ISNUMERIC to check if string is numeric value:
CREATE VIEW _tmp_v_aaa
AS
SELECT
id,
[value] = CAST((CASE WHEN ISNUMERIC([value]) = 1 THEN [value] ELSE NULL END) AS INT)
FROM _tmp_aaa
WHERE [value] LIKE '1%'
AND ISNUMERIC([value]) = 1
I tried some tricks... Obviously the optimizer tries to hand down your where criterium where it is not yet tranformed. This is one problem to be solved with a. multi-statement function. Their biggest disadvantage is the advantage in this case: the optimizer will not look into it, but just take their result "as is":
create function fn_tmp_v_aaa()
returns #tbl table(id INT, value INT)
as
BEGIN
INSERT INTO #tbl
select id, cast(value as int) as value from _tmp_aaa where value like '1%'
RETURN;
END
select * from dbo.fn_tmp_v_aaa() where value=11;
If you look at the execution plan , predicates are passed down to the table something like....
And your query gets translated to something like .....
select id, cast(value as int) as value
from tmp_aaa
where CONVERT(INT, value,0) like '1%'
AND CONVERT(INT, value,0) = CONVERT(INT, 11,0)
Now if you run this query you will get the same error you get when you query against the view.
Conversion failed when converting the nvarchar value '2aaa' to data type int.
When the predicate CONVERT(INT, value,0) like '1%' is converted , you have INT on one side of the expressions and varchar on another, INT being the higher precedence, sql server tries to convert whole expression to INT and fails hence the error message.

SQL Server - Cast invalid value to int

Is there any way to deal with SQL casts if the input data is corrupt?
Let's say I have a column of datatype NVarchar(10) and want to cast this column to int.
Let's also say that some of the nvarchar values are corrupt, so they can't be converted to int.
Is there any way to silently ignore these, default them to 0 or some such?
DECLARE #t TABLE (Numbers VARCHAR(20))
INSERT INTO #t
VALUES
('30a'),('30'),('100'),
('100a'),('200'),('200a')
SELECT CASE
WHEN ISNUMERIC(Numbers) = 1
THEN CAST(Numbers AS INT) ELSE NULL END AS Number
FROM #t
ISNUMERIC Function returns 1 when it is an integer value you can use this function.
Result
Number
NULL
30
100
NULL
200
NULL
it will cast the integer values to INT and ignore the values that cannot be cast to Int
Try this with PatIndex() function:
select id, val
from t
where patindex('%[^0-9]%',val) = 0
Note: above query is filtering out corrupted values, if you need to bring them in with 0 values, please use a case expression as below.
select id, case when patindex('%[^0-9]%',val) = 0
then convert(int, val)
else 0 end val
from t
Fiddle demo for both queries
I'll be the unpopular one and advise REGEX because ISNUMERIC, while sometimes useful, doesn't catch everything. This answer on SO excellently covers some REGEX concepts, for instance:
One numeric digit
Probably the easiest one of the bunch:
WHERE Column LIKE '[0-9]'
For more details, here's a useful REGEX workbench by Phil Factor and Robyn Pae.

Resources