SQL Server - Split column data and retrieve last second value - sql

I have a column name MasterCode in XYZ Table where data is stored in below form.
.105248.105250.104150.111004.
Now first of all I want to split the data into :
105248
105250
104150
111004
Then after to retrieve only last second value from the above.
So In the above given array, value returned should be 104150.

Use a split string function, but not the built in once since it will return only the values and you will lose the location data.
You can use Jeff Moden's DelimitedSplit8K that will return the item and the item index:
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
Then you can use it to split the string and it will return a table like this:
DECLARE #string varchar(100) = '.105248.105250.104150.111004.';
SELECT *
FROM [dbo].[DelimitedSplit8K](#string, '.')
ItemNumber Item
1
2 105248
3 105250
4 104150
5 111004
6
You want only the parts where there actually is an item, so add a where clause, and you want the second from last so add row_number(), and you want the entire thing in a common table expression so that you can query it:
DECLARE #string varchar(100) = '.105248.105250.104150.111004.';
WITH CTE AS
(
SELECT Item, ROW_NUMBER() OVER(ORDER BY ItemNumber DESC) As rn
FROM [dbo].[DelimitedSplit8K](#string, '.')
WHERE Item <> ''
)
And the query:
SELECT Item
FROM CTE
WHERE rn = 2
Result: 104150

If there are always four parts, you can use PARSENAME():
DECLARE #s varchar(64) = '.105248.105250.104150.111004.';
SELECT PARSENAME(SUBSTRING(#s, 2, LEN(#s)-2),2);

Depending on your version of SQL SERVER, you can also use the STRING_SPLIT function.
DECLARE #string varchar(100) = '.105248.105250.104150.111004.';
SELECT value,
ROW_NUMBER() OVER (ORDER BY CHARINDEX('.' + value + '.', '.' + #string + '.')) AS Pos
FROM STRING_SPLIT(#string,'.')
WHERE RTRIM(value) <> '';
It doesn't return the original position like Jeff's splitter, but does compare very favourably if you check Aaron Bertrand's Article :
Performance Surprises and Assumptions : STRING_SPLIT()
Edit:
Added position, but although works in this case may have issues with duplicate values

You can create a SQL server table valued function with parameters stringvalue and delemeter and call that function for the results as expected.
ALTER function [dbo].[SplitString]
(
#str nvarchar(4000),
#separator char(1)
)
returns table
AS
return (
with tokens(p, a, b) AS (
select
1,
1,
charindex(#separator, #str)
union all
select
p + 1,
b + 1,
charindex(#separator, #str, b + 1)
from tokens
where b > 0
)
select
p-1 ID,
substring(
#str,
a,
case when b > 0 then b-a ELSE 4000 end)
AS s
from tokens
)
To call the function
SELECT * FROM [DBO].[SPLITSTRING] ('.105248.105250.104150.111004.', '.') WHERE ISNULL(S,'') <> ''
Output
ID s
1 105248
2 105250
3 104150
4 111004
To get only second value you can write your query as shown below
DECLARE #MaxID INT
SELECT #MaxID = MAX (ID) FROM (SELECT * FROM [DBO].[SPLITSTRING] ('.105248.105250.104150.111004.', '.') WHERE ISNULL(S,'') <> '') A
SELECT TOP 1 #MaxID = MAX (ID) FROM (
SELECT * FROM [DBO].[SPLITSTRING] ('.105248.105250.104150.111004.', '.') WHERE ISNULL(S,'') <> ''
)a where ID < #MaxID
SELECT * FROM [DBO].[SPLITSTRING] ('.105248.105250.104150.111004.', '.') WHERE ISNULL(S,'') <> '' AND ID = #MaxID
Output
ID s
3 104150
If you want 1 as value of ID then you can write your query as shown below in last line of query.
SELECT 1 AS ID , S FROM [DBO].[SPLITSTRING] ('.105248.105250.104150.111004.', '.') WHERE ISNULL(S,'') <> '' AND ID = #MaxID
Then the output will be
ID S
1 104150
Hope this will help you.

Try this
DECLARE #DATA AS TABLE (Data nvarchar(1000))
INSERT INTO #DATA
SELECT '.105248.105250.104150.111004.'
;WITH CTE
AS
(
SELECT Data,ROW_NUMBER()OVER(ORDER BY Data DESC) AS Rnk
FROM
(
SELECT Split.a.value('.','nvarchar(100)') Data
FROM(
SELECT CAST('<S>'+REPLACE(Data,'.','</S><S>')+'</S>' AS XML ) As Data
FROM #DATA
)DT
CROSS APPLY Data.nodes('S') AS Split(a)
) AS Fnl
WHERE Fnl.Data <>''
)
SELECT Data FROM CTE
WHERE Rnk=2
Result
Data
-----
105248
105250
104150
111004

It can also be achieve only using string functions:
IF OBJECT_ID('tempdb..#temp') IS NOT NULL
DROP TABLE #temp
SELECT '.105248.105250.104150.111004.' code INTO #temp UNION ALL
SELECT '.205248.205250.204150.211004.'
SELECT
REVERSE(LEFT(
REVERSE(LEFT(code, LEN(code) - CHARINDEX('.', REVERSE(code), 2)))
, CHARINDEX('.',REVERSE(LEFT(code, LEN(code) - CHARINDEX('.', REVERSE(code), 2)))) -1
)
) second_last_value
FROM #temp
Result:
second_last_value
-----------------------------
104150
204150

Related

SQL Server: Split string value with single quotations

Trying to modify a set of code, so that given a string, the string has to be split and passed to the code to be used.
This is the code that I have right now.
DECLARE #xml xml,
#str varchar(100),
#delimiter varchar(10)
SET #str = '100'
SET #delimiter = ','
SET #xml = cast(('<X>'+replace(#str, #delimiter, '</X><X>')+'</X>') as
xml)
SELECT C.value('.', 'varchar(10)') as value
FROM #xml.nodes('X') as X(C)
For a single-valued string, this works just fine. But I need to use more than one like, ('100', '100A', '100B'...).
The string value will not contain anything other than 3-digit numbers or 3-digit numbers + an alphabet character, or 3 alphabet letter characters.
I also tried something else, but this is too slow.
declare #values table
(
Value varchar(1000)
)
insert into #values values ('100'),('100A'),('100B'),('100C')
Select *
from table
where myField in (select value from #value)
How can I modify the code for this requirement?
You need to create a table valued function that you can pass your string to split into using a cross apply:
Function
create function [dbo].[fn_StringSplit4k]
(
#str nvarchar(4000) = ' ' -- String to split.
,#delimiter as nvarchar(20) = ',' -- Delimiting value to split on.
,#num as int = null -- Which value to return.
)
returns table
as
return
-- Start tally table with 10 rows.
with n(n) as (select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1 union all select 1)
-- Select the same number of rows as characters in #str as incremental row numbers.
-- Cross joins increase exponentially to a max possible 10,000 rows to cover largest #str length.
,t(t) as (select top (select len(isnull(#str,'')) a) row_number() over (order by (select null)) from n n1,n n2,n n3,n n4)
-- Return the position of every value that follows the specified delimiter.
--,s(s) as (select 1 union all select t+len(replace(#delimiter,' ','.')) from t where substring(isnull(#str,''),t,len(replace(#delimiter,' ','.'))) = #delimiter)
,s(s) as (select 1 union all select t+1 from t where case when #delimiter = '' and t < len(#str) then 1 else case when substring(isnull(#str,''),t,1) = #delimiter then 1 else 0 end end = 1)
-- Return the start and length of every value, to use in the SUBSTRING function.
-- ISNULL/NULLIF combo handles the last value where there is no delimiter at the end of the string.
,l(s,l) as (select s,case when #delimiter = '' then 1 else isnull(nullif(charindex(#delimiter,isnull(#str,''),s),0)-s,4000) end from s)
select rn
,item
from(select row_number() over(order by s) as rn
,substring(#str,s,l) as item
from l
) a
where rn = #num
or #num is null;
Usage
select s.item
from YourTable as t
cross apply dbo.fn_StringSplit4k(t.YourString,',',null) as s;

Ordering a Delimited Column by Section

I need to order a column sequentially by each delimited section. For example, given the sample data:
a
----------
120.1
120.2
120.12
120
130
120.2.22
120.2.41
120.3
I need to obtain the following output:
120
120.1
120.2
120.2.22
120.2.41
120.3
120.12
130
I use this query but its doesn't work
Query ;
Select a from b rpad(REPLACE(a, '.', ''),15,0),REPLACE(a, '.', '') ASC
I agree with all the comments from Sean Lange et al. Since you can have unlimited number of decimals, you need a splitter. Once the values are split, then you could apply the same ORDER BY logic I have shown. I think the ORDER BY really explains the algorithm you were looking for.
Here is a way with a splitter:
declare #table table (col varchar(64))
insert into #table
values
('120.1'),
('120.2'),
('120.12'),
('120'),
('130'),
('120.2.22'),
('120.2.41.55.64.12'),
('120.3')
;with cte as(
select
*
from #table t
cross apply dbo.DelimitedSplit8K(t.col,'.')
pivot(
max(Item) for ItemNumber in ([1],[2],[3],[4],[5],[6],[7],[8]) --enter the number of possibilities
) p)
select
cte.*
from cte
order by
cast(isnull(cte.[1],0) as int)
,cast(isnull(cte.[2],0) as int)
,cast(isnull(cte.[3],0) as int)
,cast(isnull(cte.[4],0) as int)
,cast(isnull(cte.[5],0) as int)
,cast(isnull(cte.[6],0) as int)
,cast(isnull(cte.[7],0) as int)
,cast(isnull(cte.[8],0) as int)
The function, if needed:
CREATE FUNCTION [dbo].[DelimitedSplit8K] (#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
/* "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000... enough to cover VARCHAR(8000)*/
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
GO
First create a function to split. I stole this one from https://sqlperformance.com/2012/07/t-sql-queries/split-strings
create FUNCTION dbo.SplitStrings_XML
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT rn=row_number() over (order by i), Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
declare #t table (a varchar(64))
insert into #t
values
('120.1'),
('120.2'),
('120.12'),
('120'),
('130'),
('120.2.22'),
('120.2.41'),
('120.3')
select a
, a1 = max(cast(case when rn=1 then Item else null end as int))
, a2 = max(cast(case when rn=2 then Item else null end as int))
, a3 = max(cast(case when rn=3 then Item else null end as int))
, a4 = max(cast(case when rn=4 then Item else null end as int))
from #t t
cross apply dbo.SplitStrings_XML(t.a,'.') as a
group by t.a
order by 2,3,4,5

MS SQL Server Get value between commas

I have a column in Table1 with string in it separated by commma:
Id Val
1 ,4
2 ,3,1,0
3 NULL
4 ,5,2
Is there a simple way to split and get any value from that column,
for example
SELECT Value(1) FROM Table1 should get
Id Val
1 4
2 3
3 NULL
4 5
SELECT Value(2) FROM Table1 should get
Id Val
1 NULL
2 1
3 NULL
4 2
Thank you!
Storing comma separated values in a column is always a pain, consider changing your table structure
To get this done, create a split string function. Here is one of the best possible approach to split the string to individual rows. Referred from http://www.sqlservercentral.com/articles/Tally+Table/72993/
CREATE FUNCTION [dbo].[DelimitedSplit8K]
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 0 up to 10,000...
-- enough to cover NVARCHAR(4000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
to call the function
SELECT *
FROM yourtable
CROSS apply (SELECT CASE WHEN LEFT(val, 1) = ',' THEN Stuff(val, 1, 1, '') ELSE val END) cs (cleanedval)
CROSS apply [dbo].[Delimitedsplit8k](cs.cleanedval, ',')
WHERE ItemNumber = 1
SELECT *
FROM yourtable
CROSS apply (SELECT CASE WHEN LEFT(val, 1) = ',' THEN Stuff(val, 1, 1, '') ELSE val END) cs (cleanedval)
CROSS apply [dbo].[Delimitedsplit8k](cs.cleanedval, ',')
WHERE ItemNumber = 2
Another option using a Parse/Split Function and an OUTER APPLY
Example
Declare #YourTable Table ([Id] int,[Val] varchar(50))
Insert Into #YourTable Values
(1,',4')
,(2,',3,1,0')
,(3,NULL)
,(4,',5,2')
Select A.ID
,Val = B.RetVal
From #YourTable A
Outer Apply (
Select * From [dbo].[tvf-Str-Parse](A.Val,',')
Where RetSeq = 2
) B
Returns
ID Val
1 4
2 3
3 NULL
4 5
The UDF if Interested
CREATE FUNCTION [dbo].[tvf-Str-Parse] (#String varchar(max),#Delimiter varchar(10))
Returns Table
As
Return (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#String,#Delimiter,'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
Here is an example of using a CTE combined with converting the CSV to XML:
DECLARE #Test TABLE (
CsvData VARCHAR(10)
);
INSERT INTO #Test (CsvData)
VALUES
('1,2,3'),
(',4,5,7'),
(NULL),
(',3,');
WITH XmlData AS (
SELECT CONVERT(XML, '<val>' + REPLACE(CsvData, ',', '</val><val>') + '</val>') [CsvXml]
FROM #Test
)
SELECT xd.CsvXml.value('val[2]', 'VARCHAR(10)')
FROM XmlData xd;
This would output:
2
4
NULL
3
The column to display is controlled by the XPath query. In this case, val[2].
The main advantage here is that no user-defined functions are required.
Try This Logic Using recursive CTE
DECLARE #Pos INT = 2
DECLARE #T TABLE
(
Id INT,
Val VARCHAR(50)
)
INSERT INTO #T
VALUES(1,',4'),(2,',3,1,0'),(3,NULL),(4,',5,2')
;WITH CTE
AS
(
SELECT
Id,
SeqNo = 0,
MyStr = SUBSTRING(Val,CHARINDEX(',',Val)+1,LEN(Val)),
Num = REPLACE(SUBSTRING(Val,1,CHARINDEX(',',Val)),',','')
FROM #T
UNION ALL
SELECT
Id,
SeqNo = SeqNo+1,
MyStr = CASE WHEN CHARINDEX(',',MyStr)>0
THEN SUBSTRING(MyStr,CHARINDEX(',',MyStr)+1,LEN(MyStr))
ELSE NULL END,
Num = CASE WHEN CHARINDEX(',',MyStr)>0
THEN REPLACE(SUBSTRING(MyStr,1,CHARINDEX(',',MyStr)),',','')
ELSE MyStr END
FROM CTE
WHERE ISNULL(REPLACE(MyStr,',',''),'')<>''
)
SELECT
T.Id,
CTE.Num
FROM #T t
LEFT JOIN CTE
ON T.Id = cte.Id
AND SeqNo = #Pos
My Output for the above
Test Data
Declare #t TABLE (Id INT , Val VARCHAR(100))
INSERT INTO #t VALUES
(1 , '4'),
(2 , '3,1,0'),
(3 , NULL),
(4 , '5,2')
Function Definition
CREATE FUNCTION [dbo].[fn_xml_Splitter]
(
#delimited nvarchar(max)
, #delimiter nvarchar(1)
, #Position INT = NULL
)
RETURNS TABLE
AS
RETURN
(
SELECT Item
FROM (
SELECT Split.a.value('.', 'VARCHAR(100)') Item
, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) ItemNumber
FROM
(SELECT Cast ('<X>' + Replace(#delimited, #delimiter, '</X><X>')
+ '</X>' AS XML) AS Data
) AS t CROSS APPLY Data.nodes ('/X') AS Split(a)
)x
WHERE x.ItemNumber = #Position OR #Position IS NULL
);
GO
Function Call
Now you can call this function in two different ways.
1 . to get return an Item on a specific position, specify the position in the 3rd parameter of the function:
SELECT *
FROM #t t
CROSS APPLY [dbo].[fn_xml_Splitter](t.Val , ',', 1)
2 . to get return all items, specify the key word DEFUALT in the 3rd parameter of the function:
SELECT *
FROM #t t
CROSS APPLY [dbo].[fn_xml_Splitter](t.Val , ',', DEFAULT)

SQL Server Remove multiple character strings within one string

I have the following string of text in my database:
Value1 - Value2: Value3 - Value4: Value5 - Value6:
I need to remove the dash AND everything between the dash up until the colon
The above result would become:
Value1: Value3: Value5:
Basicly, there could be endless amounts of values, but there could only be just a series of one.
Thing to note: The values could be any string!
Is there an easy way to do this? Preferably without a UDF. Could anyone help me out with this? Thanks in advance!
Edit: I agree this is a very poor implementation. The rest of the database itself isnt like this at all. It's just one table. The query I get from this will be used in a view where all values are seperated into multiple aliases. Thanks for understanding
You can use a split function... though your values shouldn't be stored like this in the first place.
declare #table table (col1 varchar(256))
insert into #table
values
('Value1 - Value2: Value3 - Value4: Value5 - Value6:')
select
ReturnVal = replace(ltrim(left(Item,charindex('-',Item))),'-',':')
from
#table
cross apply dbo.DelimitedSplit8K(col1,':')
where
Item <> ''
RETURNS
+-----------+
| ReturnVal |
+-----------+
| Value1 : |
| Value3 : |
| Value5 : |
+-----------+
Or, an ugly hack to get it back how you want it
select distinct
--ReturnVal = replace(ltrim(left(Item,charindex('-',Item))),'-',':')
ReturnVal = 'V' + STUFF((
SELECT replace(left(Item,charindex('-',Item)),'-',':')
FROM
#table
cross apply dbo.DelimitedSplit8K(col1,':')
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
from
#table
cross apply dbo.DelimitedSplit8K(col1,':')
where
Item <> ''
RETURNS
ReturnVal
Value1 : Value3 : Value5 :
JEFF MODEN SPLITTER
CREATE FUNCTION [dbo].[DelimitedSplit8K] (#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
/* "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
enough to cover VARCHAR(8000)*/
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
GO
SELECT substring(NameValue, 1, charindex('_', NameValue)-1) AS Names,
substring(NameValue, charindex('_', NameValue)+1, LEN(NameValue)) AS Values
FROM Table
EDIT: Something like this put in a function or stored procedure combined with a temp table should work for more than one line, depending on the line delimiter you should also remove CHAR(13) before you start:
DECLARE #helper varchar(512)
DECLARE #current varchar(512)
SET #helper = NAMEVALUE
WHILE CHARINDEX(CHAR(10), #helper) > 0 BEGIN
SET #current = SUBSTRING(#helper, 1, CHARINDEX(CHAR(10), NAMEVALUE)-1)
SELECT SUBSTRING(#current, 1, CHARINDEX('_', #current)-1) AS Names,
SUBSTRING(#current, CHARINDEX('_', #current)+1, LEN(#current)) AS Names
SET #helper = SUBSTRING(#helper, CHARINDEX(CHAR(10), #helper)+1, LEN(#helper))
END
SELECT SUBSTRING(#helper, 1, CHARINDEX('_', #helper)-1) AS Names,
SUBSTRING(#helper, CHARINDEX('_', #helper)+1, LEN(#helper)) AS Names

SQL split-string as (key-identity,value)

I've added a function to my DB that splits a comma separated string into separate rows.
Now in my string I have: 1,55,2,56,3,57,etc... where (1) is the rowID and (55) the value I want to enter into row 1 of my table.
How can I modify this function to pull the 1st,3rd,5th,etc... values and 2nd,4th,6th,etc... values into two different columns?
CREATE FUNCTION dbo.SplitStringToValues
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING AS
RETURN
WITH E1(N) AS ( SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),
E2(N) AS (SELECT 1 FROM E1 a, E1 b),
E4(N) AS (SELECT 1 FROM E2 a, E2 b),
E42(N) AS (SELECT 1 FROM E4 a, E2 b),
cteTally(N) AS (SELECT 0 UNION ALL SELECT TOP (DATALENGTH(ISNULL(#List,1)))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E42),
cteStart(N1) AS (SELECT t.N+1 FROM cteTally t
WHERE (SUBSTRING(#List,t.N,1) = #Delimiter OR t.N = 0))
SELECT Item = SUBSTRING(#List, s.N1, ISNULL(NULLIF(CHARINDEX(#Delimiter,#List,s.N1),0)-s.N1,8000))
FROM cteStart s;
go
-------------- Update
Thanks everyone for your examples. I'm going to try out each of these until I get something working. I will accept once i figure out which on I can make work.
Thank you,
Alexp
An attempt to help with batch script; please try it out:
DECLARE #List NVARCHAR(MAX) = '1,55,2,56,3,57,10,65,11,88';
DECLARE #Delimiter NVARCHAR(255) = ',';
DECLARE #ListDataTable TABLE
(
ID INT IDENTITY (1, 1)
,DataKey INT
,DataValue INT
)
INSERT INTO #ListDataTable (DataKey, DataValue)
SELECT
value
,LEAD(value, 1, 0) OVER(ORDER BY (SELECT 1))
FROM STRING_SPLIT(#List, #Delimiter) WHERE RTRIM(value) <> '';
-- To get odd key values
SELECT * FROM
(
SELECT DataKey, DataValue FROM #ListDataTable WHERE ID % 2 = 1
) Temp WHERE DataKey % 2 = 1;
-- To get even key values
SELECT * FROM
(
SELECT DataKey, DataValue FROM #ListDataTable WHERE ID % 2 = 1
) Temp WHERE DataKey % 2 = 0;
Modify your function to return two columns: the position and the value. This is easy enough and keeps the function general purpose. Just change the select to:
SELECT Item = SUBSTRING(#List, s.N1, ISNULL(NULLIF(CHARINDEX(#Delimiter, #List, s.N1), 0) - s.N1, 8000)),
ItemNum = row_number() over (order by s.N1)
FROM cteStart s;
Then you can use to get the information you want. Here is one method:
select max(case when ItemNum % 2 = 1 then Item end) as rownum,
max(case when ItemNum % 2 = 0 then Item end) as value
from dbo.SplitStringToValues('1,55,2,56,3,57', ',')
group by (ItemNum - 1) / 2
#Macwise was on to something with LEAD - you could do this:
SELECT rownum = item, value
FROM
(
SELECT itemnumber, item, value = LEAD(item,1) OVER (ORDER BY itemnumber)
FROM dbo.SplitStringToValues('1,44,2,55,3,456,4,123,5,0', ',')
) split
WHERE 1 = itemnumber%2;
Gordon's solution is the best, most elegant pre-2012 solution. Here's another pre-2012 solution that does not require a sort in the execution plan:
SELECT rownum = s1.Item, value = s2.Item
FROM DelimitedSplit8K(#string, ',') s1
INNER MERGE JOIN SplitStringToValues('1,44,2,55,3,456,4,123,5,0', ',') s2
ON 1 = s1.itemNumber % 2 AND s1.ItemNumber = s2.ItemNumber-1;
Instead of changing that function, to get the next row's value next to the id use the LEAD function introduced in SQL SERVER 2012:
SELECT Id, Value
FROM (SELECT
ROW_NUMBER() over (order by(select 1)) as cnt,
t.item AS Id,
Lead(t.item)
OVER (
ORDER BY (SELECT 1)) Value
FROM dbo.Splitstringtovalues('10,20,30,40,50,10,20,30,40,50,60,70', ',')
t)
keyValue
WHERE keyValue.value IS NOT NULL
and cnt % 2 = 1