Extract strings till the second delim SQL - sql

I wanted to extract all the details till the second /(forward slash)from my table in SQL Server. Any ideas?
website
AA.AA/AB/123
www.google.com/en/abcd/
yahoo.com/us/dev
gmail.com
ouput
website
AA.AA/AB
www.google.com/en
yahoo.com/us
gmail.com

Perhaps this will suit your needs:
DECLARE #Table TABLE (Col1 NVARCHAR(100))
INSERT #Table VALUES
('website'),
('AA.AA/AB/123'),
('www.google.com/en/abcd/'),
('yahoo.com/us/dev'),
('gmail.com')
SELECT
COALESCE(
NULLIF(
SUBSTRING(Col1,1,CHARINDEX('/',Col1,CHARINDEX('/',Col1)+1))
,'')
,Col1
) AS Col1
FROM #Table

If you are using SQL Server 2017 or 2019, you can use STRING_AGG() to reassemble the output from STRING_SPLIT():
SELECT STRING_AGG(x.value, '/')
FROM dbo.table_name CROSS APPLY
(
SELECT value, ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM STRING_SPLIT(Col1, '/') AS ss
) AS x(value, rn)
WHERE x.rn <= 2
GROUP BY Col1;
You might say:
"But Aaron, the output of STRING_SPLIT() isn't guaranteed to be in order; in fact the documentation warns about that."
This is true; the documentation does say that. But in current versions the output is extremely unlikely to be in anything but left-to-right order. I still suggest you be wary of relying on this, since it could break at any time (I warn about this in more detail here).
If you are on an older version, or don't trust it, you can use a table-valued function that preserves the order of the input string, for example from this answer:
CREATE FUNCTION [dbo].[SplitString]
(
#List NVARCHAR(MAX),
#Delim VARCHAR(255)
)
RETURNS TABLE
AS
RETURN ( SELECT [Value], idx = RANK() OVER (ORDER BY n) FROM
(
SELECT n = Number,
[Value] = LTRIM(RTRIM(SUBSTRING(#List, [Number],
CHARINDEX(#Delim, #List + #Delim, [Number]) - [Number])))
FROM (SELECT Number = ROW_NUMBER() OVER (ORDER BY name)
FROM sys.all_objects) AS x
WHERE Number <= LEN(#List)
AND SUBSTRING(#Delim + #List, [Number], LEN(#Delim)) = #Delim
) AS y
);
With that function in place, you can then do the following, and now feel safer about relying on order (at the cost of a more expensive query):
;WITH src AS
(
SELECT Col1, idx, Value
FROM dbo.table_name CROSS APPLY dbo.SplitString(Col1, '/')
)
SELECT STUFF((SELECT '/' + Value
FROM src
WHERE src.idx <= 2 AND Col1 = t.Col1
ORDER BY idx
FOR XML PATH(''), TYPE).value(N'./text()[1]', N'nvarchar(max)'), 1, 1, '')
FROM dbo.table_name AS t
GROUP BY Col1;

I find cross apply handy for these situations
select case when str like '%/%' then left(str, i2-1) else str end as str
from t
cross apply (select charindex( '/', str ) as i1) t2 --position of first slash
cross apply (select charindex( '/', str, (i1 + 1)) as i2 ) t3 --position of second slash

Below is the simple query you can try. In the below query please replace 'colName' with your column name and Table_1 with your table name.
SELECT LEFT([colName], charindex('/', [colName], charindex('/', [colName])+1)-1) AS [AfterSecondPipe]
FROM [Table_1]

Related

How to replace anything between 2 specific characters in SQL Server

I'm trying to replace anything between 2 specific characters in a string that contains multiples of those 2 caracters. Take it as a csv format.
Here an example of what i got as data in that field:
0001, ABCD1234;0002, EFGH432562;0003, IJKL1345hsth;...
What I need to retreive from it is all parts before the ',' but not what are between ',' and ';'
I tried with those formula but no success
SELECT REPLACE(fieldname, ',[A-Z];', ' ') FROM ...
or
SELECT REPLACE(fieldname, ',*;', ' ') FROM ...
I need to get
0001 0002 0003
Is there a way to achieve that?
You can CROSS APPLY to a STRING_SPLIT that uses STRING_AGG (since Sql Server 2017) to stick the numbers back together.
select id, codes
from your_table
cross apply (
select string_agg(left(value, patindex('%_,%', value)), ' ') as codes
from string_split(fieldname, ';') s
where value like '%_,%'
) ca;
GO
id
codes
1
0001 0002 0003
Demo on db<>fiddle here
Extra
Here is a version that also works in Sql Server 2014.
Inspired by the research from #AaronBertrand
The UDF uses a recursive CTE to split the string.
And the FOR XML trick is used to stick the numbers back together.
CREATE FUNCTION dbo.fnString_Split
(
#str nvarchar(4000),
#delim nchar(1)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH RCTE AS (
SELECT
1 AS ordinal
, ISNULL(NULLIF(CHARINDEX(#delim, #str),0), LEN(#str)) AS pos
, LEFT(#str, ISNULL(NULLIF(CHARINDEX(#delim, #str),0)-1, LEN(#str))) AS value
UNION ALL
SELECT
ordinal+1
, ISNULL(NULLIF(CHARINDEX(#delim, #str, pos+1), 0), LEN(#str))
, SUBSTRING(#str, pos+1, ISNULL(NULLIF(CHARINDEX(#delim, #str, pos+1),0)-pos-1, LEN(#str)-pos ))
FROM RCTE
WHERE pos < LEN(#str)
)
SELECT ordinal, value
FROM RCTE
);
SELECT id, codes
FROM your_table
CROSS APPLY (
SELECT RTRIM((
SELECT LEFT(value, PATINDEX('%_,%', value))+' '
FROM dbo.fnString_Split(fieldname, ';') AS spl
WHERE value LIKE '%_,%'
ORDER BY ordinal
FOR XML PATH(''), TYPE).value(N'./text()[1]', N'nvarchar(max)')
) AS codes
) ca
OPTION (MAXRECURSION 250);
id
codes
1
0001 0002 0003
Demo on db<>fiddle here
Alternative version of the UDF (no recursion)
CREATE FUNCTION dbo.fnString_Split
(
#str NVARCHAR(4000),
#delim NCHAR(1)
)
RETURNS #tbl TABLE (ordinal INT, value NVARCHAR(4000))
WITH SCHEMABINDING
AS
BEGIN
DECLARE #value NVARCHAR(4000)
, #pos INT = 0
, #ordinal INT = 0;
WHILE (LEN(#str) > 0)
BEGIN
SET #ordinal += 1;
SET #pos = ISNULL(NULLIF(CHARINDEX(#delim, #str),0), LEN(#str)+1);
SET #value = LEFT(#str, #pos-1);
SET #str = SUBSTRING(#str, #pos+1, LEN(#str));
INSERT INTO #tbl (ordinal, value)
VALUES (#ordinal, #value);
END;
RETURN;
END;
If you're on SQL Server 2017 and don't need a guarantee that the order will be maintained, then LukStorms' answer is perfectly adequate.
However, if you:
care about an order guarantee; or,
are on an older version than 2017 (and can't use STRING_AGG); or,
are on an even older version than 2016 or are in an older compatibility level (and can't use STRING_SPLIT):
Here's an ordered split function that can help (it's long and ugly but you only have to create it once):
CREATE FUNCTION dbo.SplitOrdered
(
#list nvarchar(max),
#delim nvarchar(10)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH w(n) AS (SELECT 0 FROM (VALUES (0),(0),(0),(0)) w(n)),
k(n) AS (SELECT 0 FROM w a, w b),
r(n) AS (SELECT 0 FROM k a, k b, k c, k d, k e, k f, k g, k h),
p(n) AS (SELECT TOP (COALESCE(LEN(#list), 0))
ROW_NUMBER() OVER (ORDER BY ##SPID) -1 FROM r),
spots(p) AS
(
SELECT n FROM p
WHERE (SUBSTRING(#list, n, LEN(#delim + 'x') - 1) LIKE #delim OR n = 0)
),
parts(p,val) AS
(
SELECT p, SUBSTRING(#list, p + LEN(#delim + 'x') - 1,
LEAD(p, 1, 2147483647) OVER (ORDER BY p) - p - LEN(#delim))
FROM spots AS s
)
SELECT listpos = ROW_NUMBER() OVER (ORDER BY p),
Item = LTRIM(RTRIM(val))
FROM parts
);
Then the query can become:
;WITH x AS
(
SELECT id, listpos,
codes = LEFT(Item, COALESCE(NULLIF(CHARINDEX(',', Item),0),1)-1)
FROM dbo.your_table
CROSS APPLY dbo.SplitOrdered(fieldname, ';') AS c
)
SELECT id, codes = (
(SELECT x2.codes + ' '
FROM x AS x2
WHERE x2.id = x.id
ORDER BY x2.listpos
FOR XML PATH(''), TYPE).value(N'./text()[1]', N'nvarchar(max)')
)
FROM x GROUP BY id;
Example borrowing from LukStorms' db<>fiddle
Note that, in addition to guaranteeing order and being backward compatible (well, only back so many versions), it also ignores garbage data, e.g. try:
0001, ABCD1234;0002 but no comma

How to split multiple strings and insert SQL Server FN_SplitStr

I have 2 strings and one integer:
#categoryID int = 163,
#Ids nvarchar(2000) = '1,2,3',
#Names nvarchar(2000) = 'Bob,Joe,Alex'
I need to select 3 columns 3 rows; The most accomplished is 3 rows 2 columns:
select #categoryID,items from FN_SplitStr(#Ids,',')
resulting:
163,1
163,2
163,3
But I can't figure out how to split both strings.
I tried many ways like:
select #categoryID,items from FN_SplitStr((#Ids,#Names),',')
select #categoryID,items from FN_SplitStr(#Ids,','),items from FN_SplitStr(#Names,',')
EXPECTED OUTPUT:
163,1,Bob
163,2,Joe
163,3,Alex
NOTE1: I looked over tens of questions the most similar is:
How to split string and insert values into table in SQL Server AND SQL Server : split multiple strings into one row each but this question is different.
NOTE2: FN_SplitStr is a function for spliting strings in SQL. And I'm trying to create a stored procedure.
Based on your expected output, you have to use cross apply twice and then create some sort of ranking to make sure that you are getting the right value. As IDs and Names don't seem to have any relationship cross apply will create multiple rows (when you split the string to Names and ID)
There might be better way but this also gives your expected output. You can change this string split to your local function.
1st Dense rank is to make sure that we get three unique names and 2nd dense rank is the rank within the name based on order by with ID and outside of the sub query you have to do some comparison to get only 3 rows.
Declare #categoryID int = 163,
#Ids nvarchar(2000) = '1,2,3',
#Names nvarchar(2000) = 'Bob,Joe,Alex'
select ConcatenatedValue, CategoryID, IDs, Names from (
select concat(#categoryID,',',a.value,',',b.value) ConcatenatedValue, #categoryID CategoryID,
A.value as IDs, b.value as Names , DENSE_RANK() over (order by b.value) as Rn,
DENSE_RANK() over (partition by b.value order by a.value) as Ranked
from string_split(#IDs,',') a
cross apply string_split(#names,',') B ) t
where Rn - Ranked = 0
Output:
Inside your stored procedure do a string split of #Ids and insert into #temp1 table with an identity(1,1) column rowed. You will get:
163,1,1
163,2,2
163,3,3
Then do the second string split of #Names and insert into #temp2 table with an identity(1,1) column rowed. You will get:
Bob,1
Joe,2
Alex,3
You can then do an inner join with #temp1 and #temp2 on #temp1.rowid = #temp2.rowid and get:
163,1,Bob
163,2,Joe
163,3,Alex
I hope this solves your problem.
You can do this with a recursive CTE:
with cte as (
select #categoryId as categoryId,
convert(varchar(max), left(#ids, charindex(',', #ids + ',') - 1)) as id,
convert(varchar(max), left(#names, charindex(',', #names + ',') - 1)) as name,
convert(varchar(max), stuff(#ids, 1, charindex(',', #ids + ','), '')) as rest_ids,
convert(varchar(max), stuff(#names, 1, charindex(',', #names + ','), '')) as rest_names
union all
select categoryId,
convert(varchar(max), left(rest_ids, charindex(',', rest_ids + ',') - 1)) as id,
convert(varchar(max), left(rest_names, charindex(',', rest_names + ',') - 1)) as name,
convert(varchar(max), stuff(rest_ids, 1, charindex(',', rest_ids + ','), '')) as rest_ids,
convert(varchar(max), stuff(rest_names, 1, charindex(',', rest_names + ','), '')) as rest_names
from cte
where rest_ids <> ''
)
select categoryid, id, name
from cte;
Here is a db<>fiddle.
You need to split CSV value with record number. For that you need to use ROW_NUMBER() function to generate record wise unique ID as column like "RID", while you split CSV columns in row.
You can use table value split function or XML as used below.
Please check this let us know your solution is found or not.
DECLARE
#categoryID int = 163,
#Ids nvarchar(2000) = '1,2,3',
#Names nvarchar(2000) = 'Bob,Joe,Alex'
SELECT
#categoryID AS categoryID,
q.Id,
w.Names
FROM
(
SELECT
ROW_NUMBER() OVER (ORDER BY f.value('.','VARCHAR(10)')) AS RID,
f.value('.','VARCHAR(10)') AS Id
FROM
(
SELECT
CAST('<a>' + REPLACE(#Ids,',','</a><a>') + '</a>' AS XML) AS idXML
) x
CROSS APPLY x.idXML.nodes('a') AS e(f)
) q
INNER JOIN
(
SELECT
ROW_NUMBER() OVER (ORDER BY h.value('.','VARCHAR(10)')) AS RID,
h.value('.','VARCHAR(10)') AS Names
FROM
(
SELECT
CAST('<a>' + REPLACE(#Names,',','</a><a>') + '</a>' AS XML) AS namesXML
) y
CROSS APPLY y.namesXML.nodes('a') AS g(h)
) w ON w.RID = q.RID

Order Concatenated field

I have a field which is a concatenation of single letters. I am trying to order these strings within a view. These values can't be hard coded as there are too many. Is someone able to provide some guidance on the function to use to achieve the desired output below? I am using MSSQL.
Current output
CustID | Code
123 | BCA
Desired output
CustID | Code
123 | ABC
I have tried using a UDF
CREATE FUNCTION [dbo].[Alphaorder] (#str VARCHAR(50))
returns VARCHAR(50)
BEGIN
DECLARE #len INT,
#cnt INT =1,
#str1 VARCHAR(50)='',
#output VARCHAR(50)=''
SELECT #len = Len(#str)
WHILE #cnt <= #len
BEGIN
SELECT #str1 += Substring(#str, #cnt, 1) + ','
SET #cnt+=1
END
SELECT #str1 = LEFT(#str1, Len(#str1) - 1)
SELECT #output += Sp_data
FROM (SELECT Split.a.value('.', 'VARCHAR(100)') Sp_data
FROM (SELECT Cast ('<M>' + Replace(#str1, ',', '</M><M>') + '</M>' AS XML) AS Data) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)) A
ORDER BY Sp_data
RETURN #output
END
This works when calling one field
ie.
Select CustID, dbo.alphaorder(Code)
from dbo.source
where custid = 123
however when i try to apply this to top(10) i receive the error
"Invalid length parameter passed to the LEFT or SUBSTRING function."
Keeping in mind my source has ~4million records, is this still the best solution?
Unfortunately i am not able to normalize the data into a separate table with records for each Code.
This doesn't rely on a id column to join with itself, performance is almost as fast
as the answer by #Shnugo:
SELECT
CustID,
(
SELECT
chr
FROM
(SELECT TOP(LEN(Code))
SUBSTRING(Code,ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),1)
FROM sys.messages) A(Chr)
ORDER by chr
FOR XML PATH(''), type).value('.', 'varchar(max)'
) As CODE
FROM
source t
First of all: Avoid loops...
You can try this:
DECLARE #tbl TABLE(ID INT IDENTITY, YourString VARCHAR(100));
INSERT INTO #tbl VALUES ('ABC')
,('JSKEzXO')
,('QKEvYUJMKRC');
--the cte will create a list of all your strings separated in single characters.
--You can check the output with a simple SELECT * FROM SeparatedCharacters instead of the actual SELECT
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,(
SELECT Chr As [*]
FROM SeparatedCharacters sc1
WHERE sc1.ID=t.ID
ORDER BY sc1.Chr
FOR XML PATH(''),TYPE
).value('.','nvarchar(max)') AS Sorted
FROM #tbl t;
The result
ID YourString Sorted
1 ABC ABC
2 JSKEzXO EJKOSXz
3 QKEvYUJMKRC CEJKKMQRUvY
The idea in short
The trick is the first CROSS APPLY. This will create a tally on-the-fly. You will get a resultset with numbers from 1 to n where n is the length of the current string.
The second apply uses this number to get each character one-by-one using SUBSTRING().
The outer SELECT calls from the orginal table, which means one-row-per-ID and use a correalted sub-query to fetch all related characters. They will be sorted and re-concatenated using FOR XML. You might add DISTINCT in order to avoid repeating characters.
That's it :-)
Hint: SQL-Server 2017+
With version v2017 there's the new function STRING_AGG(). This would make the re-concatenation very easy:
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,STRING_AGG(sc.Chr,'') WITHIN GROUP(ORDER BY sc.Chr) AS Sorted
FROM SeparatedCharacters sc
GROUP BY ID,YourString;
Considering your table having good amount of rows (~4 Million), I would suggest you to create a persisted calculated field in the table, to store these values. As calculating these values at run time in a view, will lead to performance problems.
If you are not able to normalize, add this as a denormalized column to the existing table.
I think the error you are getting could be due to empty codes.
If LEN(#str) = 0
BEGIN
SET #output = ''
END
ELSE
BEGIN
... EXISTING CODE BLOCK ...
END
I can suggest to split string into its characters using referred SQL function.
Then you can concatenate string back, this time ordered alphabetically.
Are you using SQL Server 2017? Because with SQL Server 2017, you can use SQL String_Agg string aggregation function to concatenate characters splitted in an ordered way as follows
select
t.CustId, string_agg(strval, '') within GROUP (order by strval)
from CharacterTable t
cross apply dbo.SPLIT(t.code) s
where strval is not null
group by CustId
order by CustId
If you are not working on SQL2017, then you can follow below structure using SQL XML PATH for concatenation in SQL
select
CustId,
STUFF(
(
SELECT
'' + strval
from CharacterTable ct
cross apply dbo.SPLIT(t.code) s
where strval is not null
and t.CustId = ct.CustId
order by strval
FOR XML PATH('')
), 1, 0, ''
) As concatenated_string
from CharacterTable t
order by CustId

How to SORT in order as entered in SQL Server?

I'm using SQL Server and I'm trying to find results but I would like to get the results in the same order as I had input the conditions.
My code:
SELECT
AccountNumber, EndDate
FROM
Accounts
WHERE
AccountNumber IN (212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689) -- I would like the results to be in the same order as these numbers.
Here is an in-line approach
Example
Declare #List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689'
Select A.AccountNumber
,A.EndDate
From Accounts A
Join (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = v.value('(./text())[1]', 'int')
From (values (convert(xml,'<x>' + replace(#List,',','</x><x>')+'</x>'))) x(n)
Cross Apply n.nodes('x') node(v)
) B on A.AccountNumber = B.RetVal
Order By B.RetSeq
EDIT - the subquery Returns
RetSeq RetVal
1 212345
2 312345
3 145687
4 658975
5 256987
6 365874
7 568974
8 124578
9 125689
You can replace IN with a JOIN, and set a field for ordering, like this:
SELECT AccountNumber , EndDate
FROM Accounts a
JOIN (
SELECT 212345 AS Number, 1 AS SeqOrder
UNION ALL
SELECT 312345 AS Number, 2 AS SeqOrder
UNION ALL
SELECT 145687 AS Number, 3 AS SeqOrder
UNION ALL
... -- and so on
) AS inlist ON inlist.Number = a.AccountNumber
ORDER BY inlist.SeqOrder
I will offer one more approach I just found out, but this needs v2016. Regrettfully the developers forgot to include the index into the resultset of STRING_SPLIT(), but this would work and is documented:
A solution via FROM OPENJSON():
DECLARE #str VARCHAR(100) = 'val1,val2,val3';
SELECT *
FROM OPENJSON('["' + REPLACE(#str,',','","') + '"]');
The result
key value type
0 val1 1
1 val2 1
2 val3 1
The documentation tells clearly:
When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.
This is not an answer, just some test-code to check John Cappelletti's approach.
DECLARE #tbl TABLE(ID INT IDENTITY,SomeGuid UNIQUEIDENTIFIER);
--Create more than 6 mio rows with an running number and a changing Guid
WITH tally AS (SELECT ROW_NUMBER()OVER(ORDER BY (SELECT NULL)) AS Nmbr
FROM master..spt_values v1
CROSS JOIN master..spt_values v2)
INSERT INTO #tbl
SELECT NEWID() from tally;
SELECT COUNT(*) FROM #tbl; --6.325.225 on my machine
--Create an XML with nothing more than a list of GUIDs in the order of the table's ID
DECLARE #xml XML=
(SELECT SomeGuid FRom #tbl ORDER BY ID FOR XML PATH(''),ROOT('root'),TYPE);
--Create one invalid entry
UPDATE #tbl SET SomeGuid = NEWID() WHERE ID=10000;
--Read all GUIDs out of the XML and number them
DECLARE #tbl2 TABLE(Position INT,TheGuid UNIQUEIDENTIFIER);
INSERT INTO #tbl2(Position,TheGuid)
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
,g.value(N'text()[1]',N'uniqueidentifier')
FROM #xml.nodes(N'/root/SomeGuid') AS A(g);
--then JOIN them via "Position" and check,
--if there are rows, where not the same values get into the same row.
SELECT *
FROM #tbl t
INNER JOIN #tbl2 t2 ON t2.Position=t.ID
WHERE t.SomeGuid<>t2.TheGuid;
At least in this simple case I always get exactly only the one record back which was invalidated...
Okay, after some re-thinking I'll offer the ultimative XML based type-safe and sort-safe splitter:
Declare #List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689';
DECLARE #delimiter VARCHAR(10)=', ';
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML) AS ListXml
)
,Tally(Nmbr) As
(
SELECT TOP((SELECT ElementCount FROM Casted)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values v1 CROSS JOIN master..spt_values v2
)
SELECT Tally.Nmbr AS Position
,(SELECT ListXml.value('(/x[sql:column("Tally.Nmbr")])[1]','int') FROM Casted) AS Item
FROM Tally;
The trick is to create a list of running numbers with the fitting number of element (a number's table was even better) and to pick the elements according to their position.
Hint: This is rather slow...
UPDATE: even better:
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML)
.query('
for $x in /x
return <x p="{count(/x[. << $x])}">{$x/text()[1]}</x>
') AS ListXml
)
SELECT x.value('#p','int') AS Position
,x.value('text()[1]','int') AS Item
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);
Elements are create as
<x p="99">TheValue</x>
Regrettfully the XQuery function position() is not available to retrieve the value. But you can use the trick to count all elements before a given node. this is scaling badly, as this count must be performed over and over. The more elements the worse it goes...
UPDATE2: With a known count of elements one might use this (much better performance)
Use XQuery to iterate a literally given list:
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML)
.query('
for $i in (1,2,3,4,5,6,7,8,9)
return <x p="{$i}">{/x[$i]/text()[1]}</x>
') AS ListXml
)
SELECT x.value('#p','int') AS Position
,x.value('text()[1]','int') AS Item
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);
In Azure SQL, there is now extended version of STRING_SPLIT which also can return the order of items if the third optional argument enable_ordinal is set to 1.
Then this simple task is finally easy:
DECLARE #string AS varchar(200) = 'a/b/c/d/e'
DECLARE #position AS int = 3
SELECT value FROM STRING_SPLIT(#string, '/', 1) WHERE ordinal = #position
Unfortunately not available in SQL Server 2019, only in Azure for now, lets hope it will be in SQL Server 2022.

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