SQL for nvarchar 0 = '' & = 0? - sql

I was searching for integers in a nvarchar column. I noticed that if the row contains '' or 0 it is picked up if I search using just 0.
I'm assuming there is some implicit conversion happening which is saying that 0 is equal to ''. Why does it assign two values?
Here is a test:
--0 Test
create table #0Test (Test nvarchar(20))
GO
insert INTO #0Test (Test)
SELECT ''
UNION ALL
SELECT 0
UNION ALL
SELECT ''
Select *
from #0Test
Select *
from #0Test
Where test = 0
SELECT *
from #0Test
Where test = '0'
SELECT *
from #0Test
Where test = ''
drop table #0Test

The behavior you see is the one describe din the product documentation. The rules of Data Type Precedence specify that int has higher precedence than nvarchar therefore the operation has to occur as an int type:
When an operator combines two expressions of different data types, the
rules for data type precedence specify that the data type with the
lower precedence is converted to the data type with the higher
precedence
Therefore your query is actually as follow:
Select *
from #0Test
Where cast(test as int) = 0;
and the empty string N'' yields the value 0 when cast to int:
select cast(N'' as int)
-----------
0
(1 row(s) affected)
Therefore the expected result is the one you see, the rows with an empty string qualify for the predicate test = 0. Further proof that you should never mix types freely. For a more detailed discussion of the topic, see How Data Access Code Affects Database Performance.

You are implicitly converting the field to int with your UNION statement.
Two empty strings and the integer 0 will result in an int field. This is BEFORE you insert into the nvarchar field, so the data type in the temp table is irrelevant.
Try changing the second select in the UNION to:
SELECT '0'
And you will get the expected result.

Related

TSQL CTE error ''Types don't match between the anchor and the recursive part"

Would someone help me understand the details of the error below..? This is for SQL Server 2008.
I did fix it myself, and found many search hits which show the same fix, but none explain WHY this happens in a CTE.
Types don't match between the anchor and the recursive part in column "txt" of recursive query "CTE".
Here is an example where I resolved the issue with CAST, but why does it work?
WITH CTE(n, txt) AS
(
--SELECT 1, '1' --This does not work.
--SELECT 1, CAST('1' AS varchar) --This does not work.
--SELECT 1, CAST('1' AS varchar(1000)) --This does not work.
SELECT
1,
CAST('1' AS varchar(max)) --This works. Why?
UNION ALL
SELECT
n+1,
txt + ', ' + CAST(n+1 AS varchar) --Why is (max) NOT needed?
FROM
CTE
WHERE
n < 10
)
SELECT *
FROM CTE
I assume there are default variable types at play which I do not understand, such as:
what is the type for something like SELECT 'Hello world! ?
what is the type for the string concatenation operator SELECT 'A' + 'B' ?
what is the type for math such as SELECT n+1 ?
The info you want is all in the documentation:
When concatenating two char, varchar, binary, or varbinary expressions, the length of the resulting expression is the sum of the lengths of the two source expressions, up to 8,000 bytes.
snip ...
When comparing two expressions of the same data type but different lengths by using UNION, EXCEPT, or INTERSECT, the resulting length is the longer of the two expressions.
The precision and scale of the numeric data types besides decimal are fixed. When an arithmetic operator has two expressions of the same type, the result has the same data type with the precision and scale defined for that type.
However, a recursive CTE is not the same as a normal UNION ALL:
The data type of a column in the recursive member must be the same as the data type of the corresponding column in the anchor member.
So in answer to your questions:
'Hello world!' has the data type varchar(12) by default.
'A' + 'B' has the data type varchar(2) because that is the sum length of the two data types being summed (the actual value is not relevant).
n+1 is still an int
In a recursive CTE, the data type must match exactly, so '1' is a varchar(1). If you specify varchar without a length in a CAST then you get varchar(30), so txt + ', ' + CAST(n+1 AS varchar) is varchar(33).
When you cast the anchor part to varchar(max), that automatically means the recursive part will be varchar(max) also. You don't need to cast to max, you could also cast the recursive part directly to varchar(30) for example:
WITH CTE(n, txt) AS
(
--SELECT 1, '1' --This does not work.
SELECT 1, CAST('1' AS varchar(30)) --This does work.
--SELECT 1, CAST('1' AS varchar(1000)) --This does not work.
UNION ALL
SELECT
n+1,
CAST(CONCAT(txt, ', ', n+1) AS varchar(30))
FROM
CTE
WHERE
n < 10
)
SELECT *
FROM CTE
db<>fiddle
If you place the query into a string then you can get the result set data types like with the query :
DECLARE #query nvarchar(max) = 'SELECT * FROM table_name';
EXEC sp_describe_first_result_set #query, NULL, 0;

Transact-SQL Select statement results with bad GUID

We have a table with GUID primary keys. When I search for a specific key, I can use either:
SELECT * FROM products WHERE productID='34594289-16B9-4EEF-9A1E-B35066531DE6'
SELECT * FROM products WHERE productID LIKE '34594289-16B9-4EEF-9A1E-B35066531DE6'
RESULT (for both):
product_ID Prd_Model
------------------------------------ --------------------------------------------------
34594289-16B9-4EEF-9A1E-B35066531DE6 LW-100
(1 row affected)
We have a customer who uses our ID but adds more text to it to create some kind of compound field in their own system. They sent me one of these values to look up and I had an unexpected result. I meant to trim the suffix but forgot, so I ran this:
SELECT * FROM products WHERE productID='34594289-16B9-4EEF-9A1E-B35066531DE6_GBR_USD'
When I ran it, I unexpectedly got the same result:
product_ID Prd_Model
------------------------------------ --------------------------------------------------
34594289-16B9-4EEF-9A1E-B35066531DE6 LW-062
(1 row affected)
Now if I trim a value off the end of the GUID when searching I get nothing (GUID is 1 digit short):
SELECT * FROM products WHERE productID='34594289-16B9-4EEF-9A1E-B35066531DE'
Result:
product_ID Prd_Model
------------------------------------ --------------------------------------------------
(0 rows affected)
When using the LIKE command instead of '=' and if I add the suffix to the end, the statement returns zero results. This is what I would expect.
So why does the longer string with the suffix added to the end return a result when using '=' in the statement? It's obviously ignoring anything beyond the 36-character GUID length but I'm not sure why.
This behaviour is documented:
Converting uniqueidentifier Data
The uniqueidentifier type is considered a character type for the purposes of conversion from a character expression, and therefore is subject to the truncation rules for converting to a character type. That is, when character expressions are converted to a character data type of a different size, values that are too long for the new data type are truncated. See the Examples section.
So, the string value '34594289-16B9-4EEF-9A1E-B35066531DE6_GBR_USD' is truncated to '34594289-16B9-4EEF-9A1E-B35066531DE6' when it is implicitly cast (due to Data Type Precedence) to a uniqueidentifier and, unsurprisingly, '34594289-16B9-4EEF-9A1E-B35066531DE6' equals itself so the row is returned.
And the documentation does indeed give an example:
The following example demonstrates the truncation of data when the value is too long for the data type being converted to. Because the uniqueidentifier type is limited to 36 characters, the characters that exceed that length are truncated.
DECLARE #ID NVARCHAR(max) = N'0E984725-C51C-4BF4-9960-E1C80E27ABA0wrong';
SELECT #ID, CONVERT(uniqueidentifier, #ID) AS TruncatedValue;
Here is the result set.
String TruncatedValue
-------------------------------------------- ------------------------------------
0E984725-C51C-4BF4-9960-E1C80E27ABA0wrong 0E984725-C51C-4BF4-9960-E1C80E27ABA0
I, however, find it odd that you say that the statement below returns no rows:
SELECT *
FROM products
WHERE productID='34594289-16B9-4EEF-9A1E-B35066531DE'
Though true, it won't return rows, it will also error:
Conversion failed when converting from a character string to uniqueidentifier.
The fact it doesn't implies your column isn't a uniqueidentifier which would mean that your first statement isn't true; as the longer string would not be truncated. This means that one of the statements in the question is likely wrong; either your column is a uniqueidentifier and thus you get results but get an error in the latter, or it isn't and neither statement would return a result set. As you can see in this demonstration:
CREATE TABLE dbo.YourTable (UID uniqueidentifier, String varchar(36));
INSERT INTO dbo.YourTable (UID,String)
VALUES('34594289-16B9-4EEF-9A1E-B35066531DE6','34594289-16B9-4EEF-9A1E-B35066531DE6');
GO
--Returns data
SELECT *
FROM dbo.YourTable
WHERE UID = '34594289-16B9-4EEF-9A1E-B35066531DE6_GBR_USD'
GO
--Errors
SELECT *
FROM dbo.YourTable
WHERE UID = '34594289-16B9-4EEF-9A1E-B35066531DE';
GO
--Returns no data
SELECT *
FROM dbo.YourTable
WHERE String = '34594289-16B9-4EEF-9A1E-B35066531DE6_GBR_USD'
GO
--Returns no data
SELECT *
FROM dbo.YourTable
WHERE String = '34594289-16B9-4EEF-9A1E-B35066531DE'
GO
DROP TABLE dbo.YourTable;
db<>fiddle

Convert INT to String when using STUFF Function in SQL Server 2016

I'm trying to convert an INT but it is having an issue with the conversion.
Conversion failed when converting the nvarchar value '245428,246425' to data type int.
The query I am using:
SELECT STUFF
(
(
SELECT DISTINCT ',' + CONVERT(VARCHAR(20), NumField)
FROM Table A
WHERE ID = 218554
FOR XML PATH('')
) ,1,1,''
)
I use this as a subquery in a larger table like so:
SELECT
Field1,
Field2,
CASE WHEN criteria = '1'
THEN (SELECT STUFF(
(
SELECT DISTINCT ',' + CONVERT(VARCHAR(20), NumField)
FROM Table A
WHERE ID = 218554
FOR XML PATH('')
) ,1,1,''
))
END
FROM
Table B
The STUFF query runs fine when it's executed on it's own but when I run it in the full query it comes up with the conversion error.
I don't think you are not showing the full query -- or at least the full case expression. A case expression returns a single value with a single type.
When there are type conflicts, then SQL Server has to determine the single overall type, according to its rules. If one then returns an integer and another returns a string, then the case expression is an integer (not a string). So, the string is converted to an integer.
You can see this problem with much simpler logic:
select (case when 1=1 then 'a' else 0 end)
Even though the else is never execution, the type of the expression is determined at compile time -- and 'a' cannot be converted to an integer.

Data type error when passing dynamic column names into SQL Server

I've run into an issue while executing a stored procedure from VBA: I want to pass in a column name as a string for the parameter, and then use a case statement to select the actual column name in the data.
This query works fine when the column name (#FACTOR) i'm passing through is an integer, but not when it's a varchar. I get a conversion error:
Error converting data type nvarchar to float.
Here's my code:
WITH T0 AS (
SELECT DISTINCT
CASE #FACTOR
WHEN 'DRVREC' THEN DRIVINGRECORD --OK
WHEN 'POAGE' THEN POAGE
WHEN 'ANNUALKM' THEN AMC_VH_ANNL_KM
WHEN 'DAILYKM' THEN AMC_VH_KM_TO_WRK
WHEN 'RATETERR' THEN AMC_VH_RATE_TERR --OK
WHEN 'BROKERNAME' THEN MASTERBROKER_NAME
WHEN 'DRVCLASS' THEN DRIVINGCLASS -- OK
WHEN 'VEHAGE' THEN VEH_AGE -- OK
WHEN 'YEARSLIC' THEN YRSLICENSE
WHEN 'COVERAGECODE' THEN COVERAGECODE
ELSE NULL END AS FACTOR FROM DBO.Automation_Data
),
...
...
Or perhaps the example below is more concise:
DECLARE #FACTOR varchar(50)
SELECT #FACTOR = 'NOT_A_VARCHAR'
SELECT CASE #FACTOR
WHEN 'A_VARCHAR' THEN COLUMNNAME1
WHEN 'NOT_A_VARCHAR' THEN COLUMNNAME2
ELSE NULL END AS FACTOR FROM dbo.myTable
^ This would work, but if #FACTOR = 'A_VARCHAR' then i get the error.
Thanks in advance!
UPDATE **********************************:
It appears to be an issue with the case statement itself?
When I only have the varchar option in my case statement, the query runs. When I un-comment the second part of the case statement I get the error.
DECLARE #FACTOR varchar(50)
SELECT #FACTOR = 'A_VARCHAR'
SELECT CASE #FACTOR
WHEN 'A_VARCHAR' THEN COLUMNNAME1
--WHEN 'NOT_A_VARCHAR' THEN COLUMNNAME2 ELSE NULL
END AS FACTOR FROM dbo.myTable
When you are selecting from multiple columns as a single column like you are doing, SQL returns the result as the highest precedence type. Same goes with coalesce etc. when a single result is to be returned from multiple data types.
If you try the code below for example, 3rd select will return the error you're getting, as it tries to convert abc to int (higher precedence). If you set #V to '123', error will go away, as the convert from '123' to int/float works. When you check the 'BaseType' of the result, you can see it shows the highest precedence data type of the mixed types.
DECLARE #F int = 1 --if you use float here error message will show ...'abc' to data type float.
DECLARE #V varchar(5) = 'abc'
DECLARE #O varchar = '1'
SELECT CASE WHEN #O = '1' THEN #F ELSE #V END --no error
SELECT SQL_VARIANT_PROPERTY((SELECT CASE WHEN #O = '1' THEN #F ELSE #V END), 'BaseType') --int/float
SET #O = '2'
SELECT CASE WHEN #O = '1' THEN #F ELSE #V END --error: Conversion failed when converting the varchar value 'abc' to data type int.
When you converted all your selects to nvarchar, nvarchar became the highest precedence data type, so it worked. But if you know some of your columns are float and some of them nvarchar, you only need to convert float columns to nvarchar. So this will work as well:
SET #O = '2'
SELECT CASE WHEN #O = '1' THEN CONVERT(NVARCHAR(5), #F) ELSE #V END
See SQL Data Type Precedence

VARCHAR vs VARCHAR(X)

This results in 'B':
DECLARE #NAME VARCHAR(20)=' s'
IF (#NAME IS NULL OR #NAME='')
SELECT 'A'
ELSE
SELECT 'B'
Whereas, this results in 'A'.
DECLARE #NAME VARCHAR=' s'
IF (#NAME IS NULL OR #NAME='')
SELECT 'A'
ELSE
SELECT 'B'
The only difference is VARCHAR(20) vs VARCHAR.
What is the reason of this odd behaviour?
Sql Server defaults a VARCHAR of unspecified length to a length of 1. And when taken in conjunction with Microsoft's interpretation of ANSI/ISO SQL-92 (ref here) which results in padding compared strings to equal length during equality comparisons, resulting in ' ' being = to '', hence the non-intuitive 'A' in the second test.
VARCHAR needs a size to be specified when there is no size specified it assume that the declaration is of one character length.
To confirm this, you can check the length of the variables.
Since, your if condition is checking for '' it satisfies the condition.
DECLARE #NAME VARCHAR=' s'
select datalength(#name)
returns 1
DECLARE #NAME VARCHAR(20)=' s'
select datalength(#name)
returns 2