Split string into words in columns - sql

I am looking to split a string into words in columns in SQL Server 2014. I have found a few solutions but all of them are giving the results in rows. How can I break the below string into columns?
"First Second Third Fourth Fifth"

You can use XML and grab the elements by their position:
DECLARE #YourString VARCHAR(100)='First Second Third Fourth Fifth';
WITH StringAsXML AS
(
SELECT CAST('<x>' + REPLACE((SELECT #YourString AS [*] FOR XML PATH('')),' ','</x><x>') + '</x>' AS XML) TheXml
)
SELECT TheXml.value('x[1]/text()[1]','nvarchar(max)') AS FirstElement
,TheXml.value('x[2]/text()[1]','nvarchar(max)') AS SecondElement
,TheXml.value('x[3]/text()[1]','nvarchar(max)') AS ThirdElement
,TheXml.value('x[4]/text()[1]','nvarchar(max)') AS FourthElement
,TheXml.value('x[5]/text()[1]','nvarchar(max)') AS FifthElement
FROM StringAsXML;
Remark
You can use PIVOT, conditional aggregation, FROM(VALUES()) or the above. but any of these approaches will need a known set of columns (a known count of elements or at least a maximum count of elements).
If you cannot rely on such a knowledge, you can use dynamically created SQL. This would mean to create one of the working statements on string base and use EXEC for a dynamic execution.
UPDATE: A dynamic approach
This approach will deal with a variable number of elements
DECLARE #YourString VARCHAR(100)='First Second Third Fourth Fifth';
DECLARE #Delimiter CHAR(1)=' ';
DECLARE #countElements INT = LEN(#YourString)-LEN(REPLACE(#YourString,#Delimiter,''));
DECLARE #Statement VARCHAR(MAX)=
'WITH StringAsXML AS
(
SELECT CAST(''<x>'' + REPLACE((SELECT ''ReplaceYourString'' AS [*] FOR XML PATH('''')),'' '',''</x><x>'') + ''</x>'' AS XML) TheXml
)
SELECT ReplaceColumnList
FROM StringAsXML;';
DECLARE #columnList VARCHAR(MAX);
WITH cte AS
(
SELECT 1 AS ElementCounter
,CAST('TheXml.value(''x[1]/text()[1]'',''nvarchar(max)'') AS Element_01' AS VARCHAR(MAX)) AS ColStatement
UNION ALL
SELECT cte.ElementCounter+1
,cte.ColStatement + CAST(',TheXml.value(''x[' + CAST(cte.ElementCounter+1 AS VARCHAR(10)) + ']/text()[1]'',''nvarchar(max)'') AS Element_' + REPLACE(STR(cte.ElementCounter + 1,2),' ','0') AS VARCHAR(MAX))
FROM cte
WHERE cte.ElementCounter <= #countElements
)
SELECT #columnList=(SELECT TOP 1 cte.ColStatement FROM cte ORDER BY cte.ElementCounter DESC)
--replace the string you want to split
SET #Statement = REPLACE(#Statement,'ReplaceYourString',#YourString);
--replace the columnList
SET #Statement = REPLACE(#Statement,'ReplaceColumnList',#columnList);
EXEC(#Statement);
UPDATE 2: The smallest fully inlined and position-safe splitter I know of
Try this out:
DECLARE #inp VARCHAR(200) = 'First Second Third Fourth Fifth';
DECLARE #dlmt VARCHAR(100)=' ';
;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(#dlmt, #inp, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(#inp, i+1, IIF(j>0, j, LEN(#inp)+1)-i-1) s FROM a WHERE i >= 0)
SELECT * FROM b;
And just to get it complete: The above tiny splitter combined with PIVOT:
;WITH
a AS (SELECT n=0, i=-1, j=0 UNION ALL SELECT n+1, j, CHARINDEX(#dlmt, #inp, j+1) FROM a WHERE j > i),
b AS (SELECT n, SUBSTRING(#inp, i+1, IIF(j>0, j, LEN(#inp)+1)-i-1) s FROM a WHERE i >= 0)
SELECT p.*
FROM b
PIVOT(MAX(s) FOR n IN([1],[2],[3],[4],[5])) p;

You can use a SQL split string function to seperate the string into words and using the order of the word in the original string, you can use CASE statements like a PIVOT query and display as columns
Here is a sample
declare #string varchar(max) = 'First Second Third Fourth Fifth'
;with cte as (
select
case when id = 1 then val end as Col1,
case when id = 2 then val end as Col2,
case when id = 3 then val end as Col3,
case when id = 4 then val end as Col4,
case when id = 5 then val end as Col5
from dbo.split( #string,' ')
)
select
max(Col1) as Col1,
max(Col2) as Col2,
max(Col3) as Col3,
max(Col4) as Col4,
max(Col5) as Col5
from cte
If you cannot create a UDF, you can use the logic in your SQL code as follows
Please note that if you have your data in a database table column, you can simply replace column content in the first SQL CTE expression
declare #string varchar(max) = 'First Second Third Fourth Fifth'
;with cte1 as (
select convert(xml, N'<root><r>' + replace(#string,' ','</r><r>') + '</r></root>') as rawdata
), cte2 as (
select
ROW_NUMBER() over (order by getdate()) as id,
r.value('.','varchar(max)') as val
from cte1
cross apply rawdata.nodes('//root/r') as records(r)
)
select
max(Col1) as Col1,
max(Col2) as Col2,
max(Col3) as Col3,
max(Col4) as Col4,
max(Col5) as Col5
from (
select
case when id = 1 then val end as Col1,
case when id = 2 then val end as Col2,
case when id = 3 then val end as Col3,
case when id = 4 then val end as Col4,
case when id = 5 then val end as Col5
from cte2
) t

You may use parsename function as :
create table tab ( str varchar(100));
insert into tab values('First Second Third Fourth Fifth');
with t as
(
select replace(str,' ','.') as str
from tab
)
Select substring(str,1,charindex('.',str)-1) as col_first,
parsename(substring(str,charindex('.',str)+1,len(str)),4) as col_second,
parsename(substring(str,charindex('.',str)+1,len(str)),3) as col_third,
parsename(substring(str,charindex('.',str)+1,len(str)),2) as col_fourth,
parsename(substring(str,charindex('.',str)+1,len(str)),1) as col_fifth
from t;
col_first col_second col_third col_fourth col_fifth
--------- ---------- --------- ---------- ---------
First Second Third Fourth Fifth
P.S. firstly, need to split the main string into the parts with at most 3 three dot(.) character(otherwise the function doesn't work). It's a restriction for parsename.
Rextester Demo

Related

Concatenate rows into columns (NO FOR XML PATH('') and recursive CTEs) - SQL Server 2012

I have a very particular problem at hand.
Brief introduction: I have two columns at a database that I need to "group concatenate", in MySQL I would simply use GROUP_CONCAT to get the desired result, in SQL Server 2017 and on I would use STRING_AGG, the problem that I have is in the SQL Server 2012, which doesn't have this function.
Now, under normal circumstances I would use FOR XML PATH('') to get the solution, this is not viable since I'm running the query from the editor inside a third source application, the error that I get is
FOR XML PATH('') can't be used inside a cursor
For the sake of the argument let's assume that it's completely out of question to use this function.
I have tried using recursive CTE, however, it's not viable due to execution time, UNION ALL takes too much resources and can't execute properly (I am using the data for reporting).
I will no post the screenshots of the data due to the sensitivity of the same, imagine just having two columns, one with an id (multiple same id's), and a column with the data that needs to be concatenated (some string). The goal is to concatenate the second columns for all of the same id's in the first columns, obviously make it distinct in the process.
Example:
Input:
col1 col2
1 a
1 b
2 a
3 c
Output:
col1 col2
1 a/b
2 a
3 c
Does anyone have a creative idea on how to do this?
If you know the maximum number of values that need to be concatenated together, you can use conditional aggregation:
select col1,
stuff( concat(max(case when seqnum = 1 then '/' + col2 end),
max(case when seqnum = 2 then '/' + col2 end),
max(case when seqnum = 3 then '/' + col2 end),
max(case when seqnum = 4 then '/' + col2 end),
max(case when seqnum = 5 then '/' + col2 end)
), 1, 1, ''
) as col2s
from (select t.*,
row_number() over (partition by col1 order by col2) as seqnum
from t
) t
group by col1;
You can get the maximum number using:
select top (1) count(*)
from t
group by col1;
Your sample output seems wrong as 'a/b' should come for value 2.
try the following:
declare #t table (col1 int, col2 varchar(100))
insert into #t select 1, 'a'
insert into #t select 2, 'b'
insert into #t select 2, 'a'
insert into #t select 3, 'c'
declare #final_table table (col1 int, col2 varchar(100), col2_all varchar(1000))
insert into #final_table (col1, col2)
select * from #t
declare #col2_all varchar(1000)
declare #Name sysname
update #final_table
SET #col2_all = col2_all = COALESCE(CASE COALESCE(#Name, '')
WHEN col1 THEN #col2_all + '/' + col2
ELSE col2 END, ''),
#Name = col1;
select col1, col2_grouped = MAX(col2_all)
from #final_table
group by col1
Using CTE:
;with cte(col1,col2_grouped,rn)
as
(
select col1, col2 , rn=ROW_NUMBER() over (PARTITION by col1 order by col1)
from #t
)
,cte2(col1,final_grouped,rn)
as
(
select col1, convert(varchar(max),col2_grouped), 1 from cte where rn=1
union all
select cte2.col1, convert(varchar(max),cte2.final_grouped+'/'+cte.col2_grouped), cte2.rn+1
from cte2
inner join cte on cte.col1 = cte2.col1 and cte.rn=cte2.rn+1
)
select col1, MAX(final_grouped) col2_grouped from cte2 group by col1
Please see db<>fiddle here.

How to force SQL to treat " - - " as a single delimiter?

I need to delimit #uid by "-". The issue is my data set has "--1" and I need it be treated as "-1"
I need #uid = '1585-1586--1-5417-2347-8865' to output this:
Instead of:
How can I achieve this in SQL?
The answer you have helps you a little here, however, with no definition of [dbo].[fnSplit] doesn't help any one else.
If we can assume that the data is well defined (has 6 columns), then we could "spam" some CHARINDEX functions to do this. You will, as shown in the answer, need to replace all the delimiters and then reinsert the value of - for the double delimiter:
DECLARE #UID varchar(30) = '1585-1586--1-5417-2347-8865';
DECLARE #Delimiter char(1) = '-';
SELECT SUBSTRING(ca.FixedUID,1,C1.I-1) AS Col1,
SUBSTRING(ca.FixedUID,C1.I+1, C2.I-C1.I-1) AS Col2,
SUBSTRING(ca.FixedUID,C2.I+1, C3.I-C2.I-1) AS Col3,
SUBSTRING(ca.FixedUID,C3.I+1, C4.I-C3.I-1) AS Col4,
SUBSTRING(ca.FixedUID,C4.I+1, C5.I-C4.I-1) AS Col5,
SUBSTRING(ca.FixedUID,C5.I+1, LEN(ca.FixedUID)-C5.I) AS Col6
FROM (VALUES(#UID))V([UID])
CROSS APPLY (VALUES(REPLACE(REPLACE(V.UID,#Delimiter,'|'),'||','|' + #Delimiter)))ca(FixedUID)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID)))C1(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C1.I+1)))C2(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C2.I+1)))C3(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C3.I+1)))C4(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C4.I+1)))C5(I);
Of course, if you had a "empty" value, then this will fail:
DECLARE #UID varchar(30) = '1585--71-5417-2347-8865';
DECLARE #Delimiter char(1) = '-';
SELECT SUBSTRING(ca.FixedUID,1,C1.I-1) AS Col1,
SUBSTRING(ca.FixedUID,C1.I+1, C2.I-C1.I-1) AS Col2,
SUBSTRING(ca.FixedUID,C2.I+1, C3.I-C2.I-1) AS Col3,
SUBSTRING(ca.FixedUID,C3.I+1, C4.I-C3.I-1) AS Col4,
SUBSTRING(ca.FixedUID,C4.I+1, C5.I-C4.I-1) AS Col5,
SUBSTRING(ca.FixedUID,C5.I+1, LEN(ca.FixedUID)-C5.I) AS Col6
FROM (VALUES(#UID))V([UID])
CROSS APPLY (VALUES(REPLACE(REPLACE(V.UID,#Delimiter,'|'),'||','|' + #Delimiter)))ca(FixedUID)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID)))C1(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C1.I+1)))C2(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C2.I+1)))C3(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C3.I+1)))C4(I)
CROSS APPLY (VALUES(CHARINDEX('|',ca.FixedUID,C4.I+1)))C5(I);
Invalid length parameter passed to the LEFT or SUBSTRING function.
And hence why a delimiter than can appear in your data should never be used (however, one can hope that as these all appear to be integer values then a NULL value wouldn't exist and there would be a 0 instead: '1585-0-71-5417-2347-8865').
If you used a string splitter like DelimitedSpluit8K_LEAD then you could Pivot (and unpivot) the data fine, but the values would be in the wrong positions with the above example:
SELECT MAX(CASE DS.ItemNumber WHEN 1 THEN DS.Item END) AS Col1,
MAX(CASE DS.ItemNumber WHEN 2 THEN DS.Item END) AS Col2,
MAX(CASE DS.ItemNumber WHEN 3 THEN DS.Item END) AS Col3,
MAX(CASE DS.ItemNumber WHEN 4 THEN DS.Item END) AS Col4,
MAX(CASE DS.ItemNumber WHEN 5 THEN DS.Item END) AS Col5,
MAX(CASE DS.ItemNumber WHEN 6 THEN DS.Item END) AS Col6
FROM (VALUES(#UID))V([UID])
CROSS APPLY (VALUES(REPLACE(REPLACE(V.UID,#Delimiter,'|'),'||','|' + #Delimiter)))ca(FixedUID)
CROSS APPLY dbo.DelimitedSplit8K_LEAD(ca.FixedUID,'|') DS;
Which will result in the below:
Col1 Col2 Col3 Col4 Col5 Col6
---- ---- ---- ---- ---- ----
1585 -71 5417 2347 8865 NULL
Basically what I'm doing is a recursive cte from 6 to 1. each iteration I am removing the last delimited number and moving it to col_val column.
I decided to use reverse so that I could then use patindex to find the hyphen then the number. Doing that made it possible to get the negative values. In reverse the string looks like 1--6851-5851-0 then patindex('%-[0-9]%', <string>) returns 2 and because I used right function of the string 0-1585-1586--1 it will return -1
I added '0-' to the beginning of the delim_column because I want to use patindex without having to account for the last delimited column.
The column col_val is repeating all the above but instead of using #uid it is using delim_column
Here is what each iteration looks like:
col_num delim_column col_val loc
6 0-1585-1586--1-5417-2347 8865 4
5 0-1585-1586--1-5417 2347 4
4 0-1585-1586--1 5417 4
3 0-1585-1586 -1 2
2 0-1585 1586 4
1 0 1585 4
Then I'm pivoting the columns using a simple choose function. That will make the column names clean.
DECLARE
#uid VARCHAR(MAX) = '1585-1586--156-5417-2347-8865',
#delim_count INT = 0
--First, count the number of delimiters. We do this by temporarily replacing '--' with a single '-'
--and then count the difference in lengths of the two strings (one with '-' and one without)
SELECT #delim_count = LEN(REPLACE(#uid, '--', '-')) - LEN(REPLACE(REPLACE(#uid, '--', '-'), '-','')) - IIF(#uid LIKE '-%', 1, 0)
--next a recursive cte that will lop off the last number each iteration and move the last value to col_val
;WITH fnsplit(col_num, delim_column, col_val, loc)
AS
(
SELECT
#delim_count+1 --start with 6 and then go to 1. remove the +1 and replace col_num > 0 for a zero index
,'0-'+SUBSTRING(#uid,0, LEN(#uid) - LEN(RIGHT(#uid, PATINDEX('%-[0-9]%', reverse(#uid)) - 1)) )
,RIGHT(#uid, PATINDEX('%-[0-9]%', REVERSE(#uid)) - 1)
,PATINDEX('%-[0-9]%', REVERSE(#uid)) - 1
UNION ALL
SELECT
col_num - 1
,SUBSTRING(delim_column,0, LEN(delim_column) - LEN(RIGHT(delim_column, PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1)) )
,RIGHT(delim_column, PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1)
,PATINDEX('%-[0-9]%', REVERSE(delim_column)) - 1
FROM
fnsplit
WHERE
col_num > 1
)
--select * from fnsplit -- uncomment here and comment all below to see the recursion
SELECT
*
FROM
(
SELECT
column_name
,col_val
FROM
fnsplit
CROSS APPLY
(SELECT CHOOSE(col_num, 'Col_A','Col_B','Col_C', 'Col_D', 'Col_E', 'Col_F')) tbl(column_name)
)PVT
PIVOT
(
MAX(col_val)
FOR column_name IN ([Col_A], [Col_B], [Col_C], [Col_D], [Col_E], [Col_F])
) PVT1
The desired result can be achieve with the script below. The script relies on a User Defined Function called [fnSplit]. The [fnSplit] UDF is defined later in the post.
declare #uid nvarchar(100)
set #uid = '1585-1586--1-5417-2347-8865'
select
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (#uid, '-', '|')),'||', '|-'), '|') where id = 1) as [Col_A],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (#uid, '-', '|')),'||', '|-'), '|') where id = 2) as [Col_B],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (#uid, '-', '|')),'||', '|-'), '|') where id = 3) as [Col_C],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (#uid, '-', '|')),'||', '|-'), '|') where id = 4) as [Col_D],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (#uid, '-', '|')),'||', '|-'), '|') where id = 5) as [Col_E],
(select data from [dbo].[fnSplit] (REPLACE ((REPLACE (#uid, '-', '|')),'||', '|-'), '|') where id = 6) as [Col_F]
CREATE FUNCTION [dbo].[fnSplit]
(
#Line nvarchar(MAX),
#SplitOn nvarchar(5) = ','
)
RETURNS #RtnValue table
(
Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
Data nvarchar(1000) NOT NULL
)
AS
BEGIN
IF #Line IS NULL RETURN
DECLARE #split_on_len INT = LEN(#SplitOn)
DECLARE #start_at INT = 1
DECLARE #end_at INT
DECLARE #data_len INT
WHILE 1=1
BEGIN
SET #end_at = CHARINDEX(#SplitOn,#Line,#start_at)
SET #data_len = CASE #end_at WHEN 0 THEN LEN(#Line) ELSE #end_at-#start_at END
INSERT INTO #RtnValue (data) VALUES( SUBSTRING(#Line,#start_at,#data_len) );
IF #end_at = 0 BREAK;
SET #start_at = #end_at + #split_on_len
END
RETURN
END

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

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;

SQL split or substring column value

I have an sql column and value/structure as per below:
ColumnA
ROOT/South America/Lima/Test/Test2
Running a select query I want to extract "Lima" As a column value. I couldn't get the split string to work, or substring.
Any thoughts?
This is my approach to get the nth part of any delimited string:
DECLARE #mockupTable TABLE(ID INT IDENTITY, YourColumn VARCHAR(1000));
INSERT INTO #mockupTable VALUES('ROOT/South America/Lima/Test/Test2')
,('Too/short')
,('Three/parts/valid');
--The splitting is a one-liner:
SELECT *
,CAST('<x>' + REPLACE(YourColumn,'/','</x><x>') + '</x>' AS XML).value('/x[3]','nvarchar(max)') AS ThirdPart
FROM #mockupTable;
If your delimited strings might include XML-forbidden characters (namely &, < and >, you'd have to escape them (but that's easy):
Just use this instead
,CAST('<x>' + REPLACE((SELECT YourColumn [*] FOR XML PATH('')),'/','</x><x>') + '</x>' AS XML).value('/x[3]','nvarchar(max)') AS ThirdPart
Some explanation
The replacements of your delimiter / with </x><x> allow to get an XML like string, which can be casted to
<x>ROOT</x>
<x>South America</x>
<x>Lima</x>
<x>Test</x>
<x>Test2</x>
The XML's method .value() allows to use XQuery to get the third <x>. One advantage: If there is no third element, this won't break, just return NULL.
A little out there but it works. Based on a recursive cte. You can set the delimiter, start and end.
declare #T table (iden int identity, col1 varchar(100));
insert into #T(col1) values
('ROOT/South America/Lima/Test/Test2')
, ('ROOT/South America/Peru/Test/Test2')
, ('ROOT/South America/Venuzuala')
, ('ROOT/South America/');
declare #split char(1) = '/';
declare #start int = 2;
declare #end int = 3
select #split, #start, #end;
with cte as
( select t.iden, t.col1, charindex(#split, t.col1) as pos , 1 as cnt
from #T t
union all
select t.iden, t.col1, charindex(#split, t.col1, t.pos + 1), cnt + 1
from cte t
where charindex(#split, t.col1, t.pos + 1) > 0
and cnt+1 <= #end
)
--select * from cte order by iden, cnt;
select --t1.*, t2.*,
SUBSTRING(t1.col1, t1.pos+1, t2.pos-t1.pos-1) as bingo
from cte t1
join cte t2
on t2.iden = t1.iden
and t1.cnt = #start
and t2.cnt = #end
order by t1.iden;
declare #t varchar(max) = 'ROOT/South America/Lima/Test/Test2'
select * from (
select
[value]
,ROW_NUMBER() Over (Order by (select null )) [Level]
from string_split( #t , '/')
) d
where d.Level = 3
the code above dose the same with much simpler logic, split the text by '/' results to rows "String_Split", then row_number() to get the order of the results, each number is a level if no sorting has been done in the order by statment "(select null)"
as a result by adding the level number in the where statment will get the level value we want, above will show level 3