Querying varchars as decimals in SQL Server - sql

I have a problem in SQL Server, I am trying to query
where x.numbers >= '9'
where x.numbers was stored as varchar and some of the values I have in that field are
8.9
9.3
6.7
>10
8.3
>= 9
If I try isnumeric(x.numbers), then it is excluding those values that start with > or >=. and I tried cast(x.numbers as decimal) but it is not working as well. Please advise

This is my attempt. It's not pretty, but might get you what you're after. It increases/decreases the values of values that have either < or >:
CREATE TABLE #Sample (N varchar(5));
INSERT INTO #Sample
VALUES ('7.9'),('4.5'),('9'),('>10'),('6.7'),('11.7'),('>12'),('<=10'),('<9');
GO
SELECT *
FROM #Sample;
GO
WITH CTE AS (
SELECT *,
CASE WHEN N LIKE '>=%' OR N LIKE '<=%' THEN TRY_CONVERT(decimal(5,1),REPLACE(REPLACE(REPLACE(N,'>',''),'<',''),'=',''))
WHEN N LIKE '>%' THEN TRY_CONVERT(decimal(5,1),REPLACE(REPLACE(REPLACE(N,'>',''),'<',''),'=','')) + 0.1 --Adding .1 as it needs to be more than it's value
WHEN N LIKE '<%' THEN TRY_CONVERT(decimal(5,1),REPLACE(REPLACE(REPLACE(N,'>',''),'<',''),'=','')) - 0.1
ELSE TRY_CONVERT(decimal(5,1),N)
END AS Nr
FROM #Sample S)
SELECT N
FROM CTE
WHERE Nr >= 9;
GO
DROP TABLE #Sample;
--SQL 2008, just CONVERT
WITH CTE AS (
SELECT *,
CASE WHEN N LIKE '>=%' OR N LIKE '<=%' THEN CONVERT(decimal(5,1),REPLACE(REPLACE(REPLACE(N,'>',''),'<',''),'=',''))
WHEN N LIKE '>%' THEN CONVERT(decimal(5,1),REPLACE(REPLACE(REPLACE(N,'>',''),'<',''),'=','')) + 0.1 --Adding .1 as it needs to be more than it's value
WHEN N LIKE '<%' THEN CONVERT(decimal(5,1),REPLACE(REPLACE(REPLACE(N,'>',''),'<',''),'=','')) - 0.1
ELSE CONVERT(decimal(5,1),N)
END AS Nr
FROM #Sample S)
SELECT N
FROM CTE
WHERE Nr >= 9;

I would use try_convert():
where try_convert(decimal(38, 6), field) > 9
Now, this works for many circumstances and assumes that you want to ignore non-numeric values.
You can modify this to get rid of various "other" characters:
where try_convert(decimal(38, 6), replace(replace(replace(replace(field, ' ', ''), '=', ''), '>', ''), '<', '')) > 9
This ignores the "operator" characters.
However, your problem is incompletely specified. If the field were '< 12' or '> 7' what do you want it to return?

try cast(n as float) as shown below:
create table #tmp(numbers varchar(10))
insert into #tmp values('8.9')
insert into #tmp values('9.3')
insert into #tmp values('6.7')
insert into #tmp values('11')
insert into #tmp values('8.3')
insert into #tmp values('9')
insert into #tmp values('10')
Select * from #tmp where cast(numbers as float)> = cast('9' as float)
Drop table #tmp

Related

Add character in front and at the end of each character

In SQL I want to add 0 in front and , at the end of each character.
Example: A30F1 -> 0A,03,00,0F,01
I don't want to use cursor if possible.
Thanks!
EIDT:
I apologize for not asking the most appropriate question at the beginning.
In short, I have a table and for each value in the column name I have to convert it to the desired format. For example, we have a #Temp table:
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721')
SELECT * FROM #Temp
One method would be to use a Tally to split the string into it's individual characters, and then use concatenation to add the 0 to the start, and STRING_AGG to comma delimit the results:
DECLARE #YourValue varchar(5) = 'A30F1';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(#YourValue))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2) --Up to 100 characters, add more cross joins for more characters
SELECT STRING_AGG(CONCAT('0',SS.C),',') WITHIN GROUP (ORDER BY T.I) AS NewString
FROM (VALUES(#YourValue))V(YourValue)
CROSS JOIN Tally T
CROSS APPLY (VALUES(SUBSTRING(V.YourValue,T.I,1)))SS(C);
It appears this is meant to be against a table, not a single value. This needs, however, very few changes to work against a table:
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (SELECT MAX(LEN(YourColumn)) FROM dbo.YourTable)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2) --Up to 100 characters, add more cross joins for more characters
SELECT STRING_AGG(CONCAT('0',SS.C),',') WITHIN GROUP (ORDER BY T.I) AS NewString
FROM dbo.YourTable YT
JOIN Tally T ON LEN(YT.YourColumn) >= T.I
CROSS APPLY (VALUES(SUBSTRING(YT.YourColumn,T.I,1)))SS(C)
GROUP BY YT.YourColumn;
db<>fiddle
I solved the simplest possible with a few variables, WHILE and SUBSTRING
DECLARE #var VARCHAR(20) = 'A30F1', #i INT = 1, #res NVARCHAR(20)
WHILE (#i <= LEN(#var))
BEGIN
SET #res = #res + '0' + SUBSTRING(#var, #i, 1) + ','
SET #i = #i + 1
END
SELECT LEFT(#res, LEN(#res) - 1) output
Check demo on DB<>FIDDLE.
Original answer:
A recursive CTE and a STRING_AGG() call is also an option (SQL Server 2017+ is needed):
DECLARE #text varchar(max) = 'A30F1';
WITH rCTE AS
(
SELECT 1 AS CharacterPosition, SUBSTRING(#text, 1, 1) AS Character
UNION ALL
SELECT CharacterPosition + 1, SUBSTRING(#text, CharacterPosition + 1, 1)
FROM rCTE
WHERE CharacterPosition < LEN(#text)
)
SELECT STRING_AGG('0' + Character, ',') WITHIN GROUP (ORDER BY CharacterPosition)
FROM rCTE
OPTION (MAXRECURSION 0);
Update:
You need a different statement, if the names are stored in a table, again using recursion and STRING_AGG():
Table:
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721')
Statement:
; WITH rCTE AS (
SELECT
t.id AS id,
LEFT(t.name, 1) AS Character,
STUFF(t.name, 1, 1, '') AS CharactersRemaining,
1 AS CharacterPosition
FROM #Temp t
UNION ALL
SELECT
r.id,
LEFT(r.CharactersRemaining, 1),
STUFF(r.CharactersRemaining, 1, 1, ''),
CharacterPosition + 1
FROM rCTE r
WHERE LEN(r.CharactersRemaining) > 0
)
SELECT
id,
STRING_AGG('0' + Character, ',') WITHIN GROUP (ORDER BY CharacterPosition) AS name
FROM rCTE
GROUP BY id
OPTION (MAXRECURSION 0);
Result:
id name
1 0A,03,00,0F,01
2 0B,05,01,0R,09
3 0L,01,07,02,01
If you are only applying this to English alphabet characters and digits as in your example you could do this.
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721'), (4, 'A')
SELECT SUBSTRING(REPLACE(
0x00 + CAST(CAST(name AS NVARCHAR(25)) AS BINARY(50)), CHAR(0), '0,')
, 3
, LEN(name) * 3 - 1)
FROM #Temp
returns
0A,03,00,0F,01
0B,05,01,0R,09
0L,01,07,02,01
0A
This takes advantage of the fact that the binary representation of the nvarchar and varchar is the same for this limited character set except for padding out with 0x00
'A30F1' -> 0x4133304631
N'A30F1' -> 0x41003300300046003100

Generate a comma-separated list of numbers in a single string

Is there a way to generate a comma-separated string of a series of numbers where the "begin" and "end" numbers are provided?
For example, provide the numbers 1 and 10 and the output would be a single value of: 1,2,3,4,5,6,7,8,9,10
10/10/2019 edit explaining why I'm interested in this:
My workplace writes queries with several columns in the SELECT statement plus aggregate functions. Then a GROUP BY clause using the column numbers. I figured using a macro that creates a comma-separated list to copy/paste in would save some time.
SELECT t.colA
, t.colB
, t.colC
, t.colD
, t.colE
, t.colF
, t.colG
, t.colH
, t.colI
, t.colJ
, sum(t.colK) as sumK
, sum(t.colL) as sumL
, sum(t.colM) as sumM
FROM t
GROUP BY 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
;
You can use a recursive CTE to generate your numbers, and xml_agg to generate your string:
with recursive nums (counter) as
( select * from (select cast(1 as bigint) as counter) t
union all
select
counter + 1
from nums
where counter between 1 and 9
)
select
trim(trailing ',' from cast(xmlagg(cast(counter as varchar(2)) || ',' order by counter) as varchar(100)))
from nums
Check these methods in SQL Server-
IF OBJECT_ID('TEMPDB..#Sample') IS NOT NULL
DROP TABLE #Sample
Create table #Sample
(
NUM int
)
declare #n int
select #n=10
insert into #Sample(NUM)
SELECT NUM FROM (select row_number() over (order by (select null)) AS NUM from sys.columns) A WHERE NUM<=#N
--Method 1 (For SQL SERVER -NEW VERSION Support)
SELECT STRING_AGG(NUM,',') AS EXPECTED_RESULT FROM #Sample
--Method 1 (For SQL SERVER -OLD VERSION Support)
select DISTINCT STUFF(CAST((
SELECT ' ,' +CAST(c.num AS VARCHAR(MAX))
FROM (
SELECT num
FROM #Sample
) c
FOR XML PATH(''), TYPE) AS VARCHAR(MAX)), 1, 2, '') AS EXPECTED_RESULT
from #Sample t
While loop seems appropriate
declare #begin int=1
declare #end int=11
declare #list varchar(500)
if #begin > #end
begin
select 'error, beginning number ' + convert(varchar(500),#begin)
+ ' must not be greater than ending number '
+ convert(varchar(500),#end) + '.' err
return
end
else
set #list = convert(varchar(500),#begin)
;
while #begin < #end
begin
set #begin += 1
set #list = #list + ',' + convert(varchar(500),#begin)
end
select #list
You might want to use varchar(5000) or something depending on how big you want it to get.
disclaimer -- I don't know if this works with teradata
I'm not sure there is a good direct way to generate a series in Teradata. You can fake it a few different ways though. Here's a comma separated list of numbers from 5 to 15, for example:
SELECT TRIM(TRAILING ',' FROM (XMLAGG(TRIM(rn)|| ',' ) (VARCHAR(10000))))
FROM (SELECT 4 + ROW_NUMBER() OVER (ORDER BY Sys_Calendar."CALENDAR".day_of_calendar) as rn FROM Sys_Calendar."CALENDAR" QUALIFY rn <= 15) t
I've only used sys_calendar.calendar here because it's a big table. Any big table would do here though.
Here's one way to do it in Teradata:
SELECT ARRAY_AGG(src.RowNum)
FROM (
SELECT ROW_NUMBER() OVER() AS RowNum
FROM sys_calendar.calendar
QUALIFY RowNum BETWEEN <begin_num> AND <end_num>
) src
This will give you the output as an ARRAY data type, which you can probably cast as a VARCHAR. It also assumes begin_num > 0 and <end_num> is less than the number of rows in the sys_calendar.calendar view. You can always fiddle with this to fit your required range of values.
There are also DelimitedBuild UDFs out there (if you can find one) that can be used to convert row values into delimited strings.
The cheapest way to achieve your goal is this one (no functions, or joins to tables required):
WITH RECURSIVE NumberRanges(TheNumber,TheString) AS
(
SELECT 1 AS TheNumber,casT(1 as VARCHAR(500)) as TheString
FROM
(
SELECT * FROM (SELECT NULL AS X) X
) DUMMYTABLE
UNION ALL
SELECT
TheNumber + 1 AS TheNumber,
TheString ||',' || TRIM(TheNumber+1)
FROM NumberRanges
WHERE
TheNumber < 10
)
SELECT TheString
FROM NumberRanges
QUALIFY ROW_NUMBER() OVER ( ORDER BY TheNumber DESC) = 1;
Result String: 1,2,3,4,5,6,7,8,9,10

Find a missing values from a table

Is there a way to search in varchar column.
I got table called x with row named par and data in:
150/RXRPR1/18/0020642
150/RXRPR1/18/0020640
150/RXRPR1/18/0020639
151/RXRPR1/18/0020638
151/RXRPR1/18/0020637
151/RXRPR1/18/0020636
151/RXRPR1/18/0020634
The row is missing
150/RXRPR1/18/0020641
151/RXRPR1/18/0020635
How to write a SQL statement to search the table for search the missing data?
The data is of type varchar and I have permissions to select only in database
You can use classical gaps detection in the right 7 chars on your numbers, partitioned by the left 14 chars. Use LEAD function to find the value of the next record and check is the difference between the current value and next value greater than 1. This will detect the gaps and you can calculate the start and the end of the gap by adding 1 to the current value and subtracting one from the next value. Something like this:
declare #t table(col varchar(50))
insert into #t(col) values
('150/RXRPR1/18/0020642'),
('150/RXRPR1/18/0020640'),
('150/RXRPR1/18/0020639'),
('151/RXRPR1/18/0020638'),
('151/RXRPR1/18/0020637'),
('151/RXRPR1/18/0020636'),
('151/RXRPR1/18/0020634')
SELECT
gapStart = left([current], 14) + right('000000' + cast(cast(right([current], 7) as int) + 1 as varchar(10)), 7)
,gapEnd = left([next], 14) + right('000000' + cast(cast(right([next], 7) as int) - 1 as varchar(10)), 7)
FROM
(
SELECT
[current] = col
,[next] = LEAD(col) OVER (partition by left(col, 14) ORDER BY col)
FROM #t
) tmp
WHERE cast(right([next], 7) as int) - cast(Right([current], 7) as int) > 1;
You can try this query:
Please replace #MINVAL,#MAXVAL with respective values.
declare #t table([VALUE] varchar(50))
insert into #t([VALUE]) values
('150/RXRPR1/18/0020642'),
('150/RXRPR1/18/0020640'),
('150/RXRPR1/18/0020639'),
('151/RXRPR1/18/0020638'),
('151/RXRPR1/18/0020637'),
('151/RXRPR1/18/0020636'),
('151/RXRPR1/18/0020634')
DECLARE #MINVAL INT = 20634,
#MAXVAL INT = 20642;
WITH cte
AS (SELECT #MINVAL VAL
UNION ALL
SELECT val + 1 VAL
FROM cte
WHERE val < #MAXVAL)
SELECT missing
FROM (SELECT *,
Replace (Lead([VALUE]) OVER ( ORDER BY val)
, CONVERT (INT, RIGHT (Lead([VALUE]) OVER (ORDER BY val), 7)),
val)
MISSING
FROM cte
LEFT JOIN #t X
ON CONVERT (INT, RIGHT (X.[VALUE], 7)) = cte.val) X
WHERE [VALUE] IS NULL OPTION ( MAXRECURSION 10000)

How do I format numbers in a SQL table?

I need help formatting numbers in a specific way.
If a number has three decimal places or less, I would like it to remain the same.
If a number has more than three significant figures, I would like all numbers after the third significant figure to be the fractional part of the number.
123 --> Stays the same
1234 --> 123.4
How can this be done?
EDIT:
1234567 --> 123.4567
I am on SQL 2007, wishing to UPDATE the value in the table. The value is stored as a numeric.
Here is a numeric solution:
UPDATE T SET NUM = NUM/POWER(10,FLOOR(LOG10(NUM))-2)
WHERE NUM>=1000
Or the SELECT statement:
SELECT NUM, CASE WHEN NUM<1000 THEN NUM
ELSE NUM/POWER(10,FLOOR(LOG10(NUM))-2)
END AS NewNUM
FROM T
Note that the exact results can vary depending on the data type of NUM. If it is a FLOAT field, it might round the last decimal if NUM gets too large. If it is of type NUMERIC, it will add zero's to the end. If DECIMAL, you need to be careful of the precision. Note that this applies to all the update solutions already mentioned.
This could work
SELECT
CASE WHEN Num > 999 THEN Num/10
ELSE
Num
END As Num
There could be a better way, but this is what I could think of
You could do this with strings.
CREATE TABLE T
( NUM NUMERIC(38,19) );
INSERT INTO T (NUM) VALUES ( 123456789 );
INSERT INTO T (NUM) VALUES ( 12345 );
INSERT INTO T (NUM) VALUES ( 123 );
INSERT INTO T (NUM) VALUES ( 1 );
SELECT CAST(
CASE WHEN NUM < 999 THEN CAST(FLOOR(NUM) AS VARCHAR)
ELSE SUBSTRING(CAST(NUM AS VARCHAR), 1, 3) + '.'
+ SUBSTRING(CAST(FLOOR(NUM) AS VARCHAR), 4, LEN(CAST(NUM AS VARCHAR)) - 3)
END AS NUMERIC(38, 19))
FROM T
UPDATE T
SET NUM = CAST(CASE WHEN NUM < 999 THEN CAST(FLOOR(NUM) AS VARCHAR)
ELSE SUBSTRING(CAST(NUM AS VARCHAR), 1, 3) + '.'
+ SUBSTRING(CAST(FLOOR(NUM) AS VARCHAR), 4, LEN(CAST(NUM AS VARCHAR)) - 3)
END AS NUMERIC(38, 19));
I've put a working example on SQLFiddle.
Assuming strings of only integer values:
SELECT CASE WHEN LEN(Num) <= 3 THEN Num
ELSE STUFF(Num,4,0,'.')
END
FROM (VALUES('1234567'),('123'),('1234'),('12')) t(Num) --some sample values
Result:
123.4567
123
123.4
12
I answered this on a cross-post elsewhere, but for completeness:
WITH n(r) AS (
SELECT 123 UNION ALL SELECT 1234 UNION ALL SELECT 1234567
)
SELECT LEFT(r, 3) + CASE
WHEN LEN(r) > 3 THEN '.' + SUBSTRING(RTRIM(r),4,38) ELSE '' END
FROM n;

How do I expand comma separated values into separate rows using SQL Server 2005?

I have a table that looks like this:
ProductId, Color
"1", "red, blue, green"
"2", null
"3", "purple, green"
And I want to expand it to this:
ProductId, Color
1, red
1, blue
1, green
2, null
3, purple
3, green
Whats the easiest way to accomplish this? Is it possible without a loop in a proc?
Take a look at this function. I've done similar tricks to split and transpose data in Oracle. Loop over the data inserting the decoded values into a temp table. The convent thing is that MS will let you do this on the fly, while Oracle requires an explicit temp table.
MS SQL Split Function
Better Split Function
Edit by author:
This worked great. Final code looked like this (after creating the split function):
select pv.productid, colortable.items as color
from product p
cross apply split(p.color, ',') as colortable
based on your tables:
create table test_table
(
ProductId int
,Color varchar(100)
)
insert into test_table values (1, 'red, blue, green')
insert into test_table values (2, null)
insert into test_table values (3, 'purple, green')
create a new table like this:
CREATE TABLE Numbers
(
Number int not null primary key
)
that has rows containing values 1 to 8000 or so.
this will return what you want:
EDIT
here is a much better query, slightly modified from the great answer from #Christopher Klein:
I added the "LTRIM()" so the spaces in the color list, would be handled properly: "red, blue, green". His solution requires no spaces "red,blue,green". Also, I prefer to use my own Number table and not use master.dbo.spt_values, this allows the removal of one derived table too.
SELECT
ProductId, LEFT(PartialColor, CHARINDEX(',', PartialColor + ',')-1) as SplitColor
FROM (SELECT
t.ProductId, LTRIM(SUBSTRING(t.Color, n.Number, 200)) AS PartialColor
FROM test_table t
LEFT OUTER JOIN Numbers n ON n.Number<=LEN(t.Color) AND SUBSTRING(',' + t.Color, n.Number, 1) = ','
) t
EDIT END
SELECT
ProductId, Color --,number
FROM (SELECT
ProductId
,CASE
WHEN LEN(List2)>0 THEN LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(',', List2, number+1)-number - 1)))
ELSE NULL
END AS Color
,Number
FROM (
SELECT ProductId,',' + Color + ',' AS List2
FROM test_table
) AS dt
LEFT OUTER JOIN Numbers n ON (n.Number < LEN(dt.List2)) OR (n.Number=1 AND dt.List2 IS NULL)
WHERE SUBSTRING(List2, number, 1) = ',' OR List2 IS NULL
) dt2
ORDER BY ProductId, Number, Color
here is my result set:
ProductId Color
----------- --------------
1 red
1 blue
1 green
2 NULL
3 purple
3 green
(6 row(s) affected)
which is the same order you want...
You can try this out, doesnt require any additional functions:
declare #t table (col1 varchar(10), col2 varchar(200))
insert #t
select '1', 'red,blue,green'
union all select '2', NULL
union all select '3', 'green,purple'
select col1, left(d, charindex(',', d + ',')-1) as e from (
select *, substring(col2, number, 200) as d from #t col1 left join
(select distinct number from master.dbo.spt_values where number between 1 and 200) col2
on substring(',' + col2, number, 1) = ',') t
I arrived this question 10 years after the post.
SQL server 2016 added STRING_SPLIT function.
By using that, this can be written as below.
declare #product table
(
ProductId int,
Color varchar(max)
);
insert into #product values (1, 'red, blue, green');
insert into #product values (2, null);
insert into #product values (3, 'purple, green');
select
p.ProductId as ProductId,
ltrim(split_table.value) as Color
from #product p
outer apply string_split(p.Color, ',') as split_table;
Fix your database if at all possible. Comma delimited lists in database cells indicate a flawed schema 99% of the time or more.
I would create a CLR table-defined function for this:
http://msdn.microsoft.com/en-us/library/ms254508(VS.80).aspx
The reason for this is that CLR code is going to be much better at parsing apart the strings (computational work) and can pass that information back as a set, which is what SQL Server is really good at (set management).
The CLR function would return a series of records based on the parsed values (and the input id value).
You would then use a CROSS APPLY on each element in your table.
Just convert your columns into xml and query it. Here's an example.
select
a.value('.', 'varchar(42)') c
from (select cast('<r><a>' + replace(#CSV, ',', '</a><a>') + '</a></r>' as xml) x) t1
cross apply x.nodes('//r/a') t2(a)
Why not use dynamic SQL for this purpose, something like this(adapt to your needs):
DECLARE #dynSQL VARCHAR(max)
SET #dynSQL = 'insert into DestinationTable(field) values'
select #dynSQL = #dynSQL + '('+ REPLACE(Color,',',''',''') + '),' from Table
SET #dynSql = LEFT(#dynSql,LEN(#dynSql) -1) -- delete the last comma
exec #dynSql
One advantage is that you can use it on any SQL Server version