Different aggregate functions depending on datatype - database

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

Related

SQL Select column tinyint

I have a table with a column:
txntype (tinyint, not null)
I'm doing a select where value of txntype is equal to 9:
where CAST(txntype as varchar(3)) = '9'
but is throwing an error:
Insufficient result space to convert uniqueidentifier value to char.
I also tried:
where ISNUMERIC(txntype) = 9
but no records are selected when query is executed. Any ideas?
Can you add the create statement of that table and the entire select statement, because it seems that either the column has been declared as a uniqueidentifier column or your select is doing something with the value of another column than the one you are using in your where clause.
Also, the ISNUMERIC() function returns a bit (0 or 1) indicating if a value can actually be converted to a numeric datatype. Comparing it with the value 9 will always yield "false" for that piece of the where clause.
If the column is actually a numeric type, you don't have to cast the value in the where clause either way.
where [txntype] = 9
That is enough if the column is really a tinyint. And that's also the reason you need to be looking at other parts of the query in order to find the cause of the error.
You don't need to use cast or isnumeric
Just simply txntype = 9

Safe convert from string to date in Transact

I have the following code in a stored procedure in MSSQL Transact:
CAST(CONVERT(Datetime, aof.Transactiondate) AS date)
aof.Transactiondate is a varchar(8) and should be written in the form of '20160202' or '20160117'.
Today I found out that in rare instances aof.Transactiondate can be a '1' and thus my code crashes completely.
Can I do anything to make sure that the above don't crash and sets a defult date or something instead of crashing??
Theoretically I could make sure that there is 8 chars in the varchar and then seperate the char into 4, 2 and 2 block and finally make sure that block one is between 2015 and 2016 (only relevant at the moment), second block is between 1 and 12 and third block is between 1 and 31 but that seems to be a huge amount of work.
TRY_CAST
Returns a value cast to the specified data type if the cast succeeds; otherwise, returns null.
You can use the ISDATE() function to test for a valid date before converting the data:
DECLARE #TransactionDate VARCHAR(8)= 1;
SELECT CONVERT( DATE,
CASE
WHEN ISDATE(#TransactionDate) = 1
THEN #TransactionDate
ELSE NULL
END);
SET #TransactionDate = '20160202';
SELECT CONVERT( DATE,
CASE
WHEN ISDATE(#TransactionDate) = 1
THEN #TransactionDate
ELSE NULL
END);
Output:
2016-02-02
There are several functions available in T-SQL under the Conversion category, all of which have variants prefixed with TRY_ which:
Returns a value cast to the specified data type if the cast succeeds; otherwise, returns null.
CAST / TRY_CAST
CONVERT / TRY_CONVERT
PARSE / TRY_PARSE
You can use them like this:
SELECT TRY_CAST('2020-05-27' AS DATE)
SELECT TRY_CONVERT(DATE, '2020-05-27')
SELECT TRY_PARSE('2020-05-27' AS DATE)

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.

CASE in sql server -implementation Clarification?

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.

TSQL to identify long Float values

I'm dealing with a legacy system where I need to identify some bad records based on a column with a data type of Float.
Good records have a value of...
1
2
1.01
2.01
Bad records are anything such as..
1.009999999999999
2.003423785643000
3.009999990463260
I've tried a number of select statements where I Convert to Decimal and cast to a varchar and use the LEN() function but this don't seem to work as the good records that are 1.01 become 1.0100000000000000
--Edit
I'm a little closer now as I have discovered I can do (Weight * 100) and all of the good records become whole number values such as 101,201,265,301,500, etc...
and bad ones such as 2.00999999046326 become 200.999999046326
This works on my SQL Server 2005 DB:
select len(cast(cast(1.01 as float) as varchar))
Result:
4
In fact, it even lets me skip the explicit varchar cast if I want to:
select len(cast(1.011 as float))
Result:
5
Update: First of all, I still needed the cast to varchar. Thinking otherwise was wrong. That said, I had this all working using strings and was about to post how. Then you I your update on mulitpling by 100 and realized that was the way to go. So here's my code for testing both ways:
declare #test table ( c float)
insert into #test
select * from
( select 14.0100002288818 as c union
select 1.01 union
select 2.00999999046326 union
select 14.01
) t
select c,
case when c = cast(cast(c as varchar) as float) AND LEN(cast(c as varchar))<=5 then 1 else 0 end,
case when c * 100 = floor(c * 100) then 1 else 0 end
from #test
something like this, maybe? (adjust the precision/scale in the where clause, of course)
select val from mytable WHERE CONVERT(decimal(5,2), val) <> val
Have you thought about using CLR integration and using .net to handle the validation
see this link Basics of Using a .NET Assembly in MS SQL - User Functions
basically you use .net methods as a user defined function to do the validation; .NET is better at working with numbers.
You could do something like this:
SELECT *
FROM YourTable
WHERE CAST(YourFloatField AS DECIMAL(15,2)) <> YourFloatField
I'm assuming that anything "bad" has more than 2 decimal places given.
This really will become a pain in the neck because floats are an imprecise datatype and you will get implicit conversions when casting.
it also depends where you run something like the following
select convert(float,1.33)
in query analyzer the output is 1.3300000000000001
in SSMS the output is 1.33
when you convert to decimal you need to specify scale and precision
so if you do
select convert(decimal(10,6),convert(float,1.33))
you get this 1.330000 because you specified a scale of 6
you could do something like this where after converting to decimal you drop the trailing 0s
select replace(rtrim(replace(convert(varchar(30),
(convert(decimal(10,6),convert(float,1.33)))),'0',' ')),' ','0')
for a value of 3.00999999046326 you need a scale of at least 14
select replace(rtrim(replace(convert(varchar(30),
(convert(decimal(30,14),convert(float,3.00999999046326)))),'0',' ')),' ','0')
Run this:
DECLARE #d FLOAT;
SET #d = 1.23;
SELECT ABS(CAST(#d AS DECIMAL(10,2)) - CAST(#d AS DECIMAL(15,8)));
SET #d = 1.230000098;
SELECT ABS(CAST(#d AS DECIMAL(10,2)) - CAST(#d AS DECIMAL(15,8)));
Use some threshold such as:
ABS(CAST(#d AS DECIMAL(10,2)) - CAST(#d AS DECIMAL(15,8)))<0.00001

Resources