How can I retrieve first second and third word of a String in SQL? - sql

I need a query which would extract the first second and third word of a string.
I have approximately 5 words in each row and I need only the first three words out of 5 in the same row (1 row). Example "ATV BDSG 232 continue with other words".
I need only the first three words together in one row (in the same row) like "ATV BDSG 232" as a first row. The table has about 1000 rows and at the end of it I should have 1000 rows again but each row should contain only the first three words of the string.
I found a query which works fine for extracting first two like "ATV BDSG" discussed in stack overflow. The query is
"SELECT SUBSTRING(field1, 0, CHARINDEX(' ', field1, CHARINDEX(' ', field1, 0)+1))
FROM Table"
Can we derive this for extracting first three words?
Thanks in advance

If you don't want to create a dedicated function, you can use successive CROSS APPLYs:
SELECT
T.s,
FirstSpace.i,
SecondSpace.j,
ThirdSpace.k,
CASE
When ThirdSpace.k > 0 THEN LEFT(T.s, Thirdspace.k - 1)
ELSE T.S
END AS Phrase
FROM t
CROSS APPLY (SELECT CHARINDEX(' ', T.s, 1)) AS FirstSpace(i)
CROSS APPLY (SELECT CHARINDEX(' ', T.S, FirstSpace.i + 1)) AS SecondSpace(j)
CROSS APPLY (SELECT CHARINDEX(' ', T.s, SecondSpace.j + 1)) AS ThirdSpace(k)
gives you the results you need:
| s | i | j | k | phrase |
|----------------------------------------|---|---|----|------------------|
| ATV BDSG 232 Continue with other words | 4 | 9 | 13 | ATV BDSG 232 |

Things are easy, SQL Server provide STRING_SPLIT() function make that too easy
DECLARE #Var VARCHAR(100) = 'ATV BDSG 232 Continue with other words';
SELECT Word
FROM
(
SELECT Value AS Word,
ROW_NUMBER()OVER(ORDER BY (SELECT NULL)) RN
FROM STRING_SPLIT(#Var, ' ')
) T
WHERE RN <= 3;
But since you are working on 2012 version, you need to define your own function.
You can also take the hard way, first you need to get the first word, then replace it with '' and get the second word, then do the same for the 3rd word as
DECLARE #Var VARCHAR(100) = 'ATV BDSG 232 Continue with other words';
WITH FW AS
(
SELECT LEFT(#Var, CHARINDEX(' ', #Var)) FirstWord
),
SW AS
(
SELECT LEFT(REPLACE(#Var, FirstWord, ''),
CHARINDEX(' ', REPLACE(#Var, FirstWord, ''))) SecondWord
FROM FW
)
SELECT FirstWord,
SecondWord,
LEFT(REPLACE(REPLACE(V, FirstWord, ''), SecondWord, ''),
CHARINDEX(' ', REPLACE(REPLACE(V, FirstWord, ''), SecondWord, ''))
) ThirdWord
FROM
(
SELECT *, #Var V
FROM FW CROSS APPLY SW
) T
Demo
UPDATE
If you want to select the three first words then simply
SELECT SUBSTRING(Str, 0, CHARINDEX(' ', Str, CHARINDEX(' ', Str, CHARINDEX(' ', Str, 0)+1)+1)) Words
FROM Strings
Demo

--make some test data
declare #test as nvarchar(100) = 'my test string for words';
select 1 id, cast('my test string for words' as nvarchar(max)) word into #test;
insert #test (id,word) values (2,'a b c d e f g hhh yyyyyy') ;
insert #test (id,word) values (3,' a required test string d e f g hhh yyyyyy') ;
insert #test (id,word) values (4,'a quick test') ;
insert #test (id,word) values (5,'a test') ;
insert #test (id,word) values (6,'last') ;
--break up letters, count the first 3 words
;WITH CTE AS (SELECT 1 x, substring(#test,1,1) charx
UNION ALL
SELECT X + 1, substring(#test,x + 1,1) from CTE WHERE x < len(#test)
)
select * from cte c3 where (SELECT count(0) cnt FROM CTE c1 JOIN CTE c2 on c1.x <= c3.x and c1.x + 1 = c2.x and c1.charx =' ' and c2.charx != ' ') < 3
;WITH tabx as (select id, cast(ltrim(word) as nvarchar(max)) 'word' from #test), --do some ltrim
CTE AS (
SELECT id, 1 x, substring(word,1,1) charx from tabx
UNION ALL
SELECT t.id, c.X + 1, substring(t.word,x + 1,1)
from tabx t
JOIN CTE c on c.id = t.id and x < len(t.word)
),
disj as
(select * from cte c3 where
(SELECT count(0) cnt
FROM CTE c1
JOIN CTE c2 on c1.id = c3.id and c1.id = c2.id and c1.x <= c3.x and c1.x + 1 = c2.x and c1.charx =' ' and c2.charx != ' '
) < 3
),
rj as
(select disj.id,disj.x, disj.charx z
from disj
where disj.x = 1
UNION ALL
select d.id, d.x, r.z + d.charx
FROM rj r
join disj d on r.id = d.id and r.x + 1 = d.x
)
select *
from rj r1
cross apply (select max(r2.x) TheRow from rj r2 where r1.id = r2.id) dq
where r1.x = dq.TheRow
order by r1.id;
--delete test data
drop table #test

/* This is not perfect - but interesting */
declare #t table (fullname varchar(100))
insert #t values('Mr Jones'),('Mrs Amy smith'),('Jim Smith'),('Dr Harry Web '),('Paul Fred andrew jones')
select fullname,
a.value as a ,
b.Value as b,
c.Value as c,
d.Value as d,
e.Value as e,
f.value as f
from #t
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ')) a
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value )) b
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value ) ) c
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value,c.value )) d
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value,c.value,d.value) ) e
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value ,c.value,d.value,e.value) ) f

To Select First Word -
Select top 1 Ltrim(Rtrim(value)) FROM STRING_SPLIT(#input,' ')
To Select Only Second Word -
Select Ltrim(Rtrim(value)) from STRING_SPLIT(#input,' ') Order by (Select NULL) OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY

Related

SQL split string (all possible combination)

I would like to transform this string:
A1+A2+A3.B1+B2.C1
into
A1.B1.C1
A1.B2.C1
A2.B1.C1
A2.B2.C1
A3.B1.C1
A3.B2.C1
How can I do that? (note that each dimension(= a group separate by .), could have x values, I mean it can be A1+A2.B1.C1 or A1+A2.B1+B2+B3+B4+B5.C1+C2)
Thanks
If you have only 3 columns, then just use STRING_SPLIT: number your groups from first split and then do a join 3 times and select each group on corresponding join.
with a as (
select s2.value as v, dense_rank() over(order by s1.value) as rn
from STRING_SPLIT('A1+A2+A3.B1+B2.C1', '.') as s1
cross apply STRING_SPLIT(s1.value, '+') as s2
)
select
a1.v + '.' + a2.v + '.' + a3.v as val
from a as a1
cross join a as a2
cross join a as a3
where a1.rn = 1
and a2.rn = 2
and a3.rn = 3
| val |
----------
|A1.B1.C1|
|A2.B1.C1|
|A3.B1.C1|
|A1.B2.C1|
|A2.B2.C1|
|A3.B2.C1|
If you have indefinite number of groups, then it's better to use recursive CTE instead of dynamic SQL. What you should do:
Start with all the values from the first group.
On recursion step crossjoin all the values of the next group (i.e. step group number is current group number + 1).
Select the last recursion step where you'll have the result.
Code is below:
with a as (
select s2.value as v, dense_rank() over(order by s1.value) as rn
from STRING_SPLIT('A1+A2+A3.B1+B2+B3+B4.C1+C2.D1+D2+D3', '.') as s1
cross apply STRING_SPLIT(s1.value, '+') as s2
)
, b (val, lvl) as (
/*Recursion base*/
select cast(v as nvarchar(1000)) as val, rn as lvl
from a
where rn = 1
union all
/*Increase concatenation on each iteration*/
select cast(concat(b.val, '.', a.v) as nvarchar(1000)) as val, b.lvl + 1 as lvl
from b
join a
on b.lvl + 1 = a.rn /*Recursion step*/
)
select *
from b
where lvl = (select max(rn) from a) /*You need the last step*/
order by val
I won't add a tabular result since it is quite big. But try it by yourself.
Here is SQL server version and fiddle:
with lst(s) as (select * from STRING_SPLIT('A1+A2.B1+B2+B3+B4+B5.C1+C2','.'))
select t1+'.'+t2+'.'+t3 as res from
(select * from STRING_SPLIT((select s from lst where s like 'A%'), '+')) s1(t1) cross join
(select * from STRING_SPLIT((select s from lst where s like 'B%'), '+')) s2(t2) cross join
(select * from STRING_SPLIT((select s from lst where s like 'C%'), '+')) s3(t3);
Of course you can grow it in a regular fashion if the number of dimensions grows.
Here is a Postgresql solution:
with x(s) as (select string_to_array('A1+A2.B1+B2+B3+B4+B5.C1+C2','.'))
select t1||'.'||t2||'.'||t3 as res from
unnest((select string_to_array(s[1],'+') from x)) t1 cross join
unnest((select string_to_array(s[2],'+') from x)) t2 cross join
unnest((select string_to_array(s[3],'+') from x)) t3;
result:
res |
--------|
A1.B1.C1|
A1.B2.C1|
A1.B3.C1|
A1.B4.C1|
A1.B5.C1|
A2.B1.C1|
A2.B2.C1|
A2.B3.C1|
A2.B4.C1|
A2.B5.C1|
A1.B1.C2|
A1.B2.C2|
A1.B3.C2|
A1.B4.C2|
A1.B5.C2|
A2.B1.C2|
A2.B2.C2|
A2.B3.C2|
A2.B4.C2|
A2.B5.C2|
Here my code with your help. I didn't mention, but I can also have more or less than 3 parts, so I'm using a dynamic SQL for this:
declare #FILTER varchar(max)='B+C+D.A+G.T+Y+R.E'
-- Works also with A.B.C
-- Works also with A+B+C.D.E+F
-- Works also with A+B+C.D+E+F+G+H
declare #NB int
declare #SQL varchar(max)=''
select #NB=count(*) from STRING_SPLIT(#FILTER,'.')
set #SQL='
;with T(A,B) as
(select *, row_number() over (order by (select NULL))
from STRING_SPLIT(''' + #FILTER + ''',''.'')
)
select '
;with T(V,N) as (
select *, row_number() over (order by (select NULL))
from STRING_SPLIT(#FILTER,'.')
)
select #SQL=#SQL + 'T' + cast(N as varchar(max)) + ' + ''.'' + ' from T
set #SQL=left(#SQL,len(#SQL)-1) + ' as res from'
;with T(V,N) as (
select *, row_number() over (order by (select NULL))
from STRING_SPLIT(#FILTER,'.')
)
select #SQL=#SQL + '
(select * from STRING_SPLIT((select A from T where B=' + cast(N as varchar(max)) + '), ''+'')) s' + cast(N as varchar(max)) + '(t' + cast(N as varchar(max)) + ') cross join'
from T
set #SQL=left(#SQL,len(#SQL)-len('cross join'))
exec(#SQL)

SQL Query on sort key values

how can you sort data in sql for each column ??
for example C1 column have value in first row as 'CAB' and you want in output as 'ABC'
Input
C1
CAB
ZSA
Output
C1
ABC
ASZ
You could try this logic, maybe there is a better solution, but it does the job
DECLARE #t TABLE (Id INT, C1 VARCHAR(255))
INSERT INTO #t VALUES (1, 'CAB'),(2, 'ZSA')
;WITH mcte AS (
SELECT split.Id, split.C1, split.c, ASCII(split.c) AS asciinr
FROM (
SELECT a.Id, a.C1, SUBSTRING(a.C1, v.number+1, 1) AS c
FROM #t AS a
join master..spt_values v on v.number < LEN(a.C1)
WHERE v.type = 'P'
) AS split
)
SELECT Id, c1, REPLACE(STUFF((SELECT ' ' + mcte2.c FROM mcte AS mcte2
WHERE mcte2.C1 = mcte.C1 and mcte2.Id = mcte.Id
ORDER BY mcte2.asciinr FOR XML PATH('') ), 1, 1, ''), ' ', '') as OrderedC1
FROM mcte
GROUP BY Id, c1
ORDER BY mcte.C1
Result
Id C1 OrderderC1
-------------------
1 CAB ABC
2 ZSA ASZ

SQL variable concatenation with xml

have the following table coming from this query:
SELECT
[Document],
[Description],
[Value],
FROM [DocDetails]
Document Description Value Line No_
120 First Row 100 1
120 Second Row 0 2
120 Third row 0 3
120 Fourth row 0 4
120 fifth row 0 5
120 sixth row 203 6
120 seventh row 256 7
120 eighth row 259 8
120 ninth row 0 9
120 tenth row 0 10
120 eleventh row 0 11
I need to concatenate the description according to the value. I would need such result:
Document Description Value
120 First Row;second row;Third row;Fourth row;fifth row 100
120 sixth row 203
120 seventh row 256
120 eighth row;ninth row;tenth row;eleventh row 259
I tried the following:
SELECT
[Document],
All_Descriptions = STUFF(
(SELECT ';' + Description AS [text()]
FROM [DocDetails] D1
WHERE D1.[Document] = D2.[Document]
FOR XML PATH('')),1,1,'')
FROM [DocDetails] D2
GROUP BY D2.[Document]
As I don't have a variable that specifies the order I am not able to concatenate properly ( the code above concatenates everything but that's not what I want). Also if I group by value I am not getting the desired result.
How can I tell SQL to basically "concatenate the row with an amount with all the following having value 0"
Thanks for your help!
This is another solution that works with SQL 2008 too.
DECLARE #DocDetails TABLE( [Document] int, [Description] varchar(20), [Value] int, [Line_No] int )
INSERT INTO #DocDetails VALUES
(120,'First Row',100,1),
(120,'Second Row',0,2),
(120,'Third row',0,3),
(120,'Fourth row',0,4),
(120,'fifth row',0,5),
(120,'sixth row',203,6),
(120,'seventh row',256,7),
(120,'eighth row',259,8),
(120,'ninth row',0,9),
(120,'tenth row',0,10),
(120,'eleventh row',0,11),
(121,'eleventh row',0,11)
;WITH
LinesWithValue AS (
SELECT *, ROW_NUMBER() OVER(PARTITION BY [Document] ORDER BY Line_No ) RN
FROM #DocDetails
WHERE Value > 0
)
,LinesWithNext AS (
SELECT L.*, L1.Line_No Next_Line_No
FROM LinesWithValue L
LEFT JOIN LinesWithValue L1 ON L.RN + 1 = L1.RN AND L.[Document] = L1.[Document]
)
,NewTable AS (SELECT
B.Document,
B.Description,
CASE B.Value WHEN 0 THEN A.Value ELSE B.Value END Value,
B.Line_No
FROM LinesWithNext A
FULL JOIN #DocDetails B ON A.[Document] = B.[Document] AND ( ( B.Line_No >= A.Line_No ) AND ( A.Next_Line_No IS NULL OR B.Line_No < A.Next_Line_No ) )
)
SELECT
[Document],
[Value],
All_Descriptions = STUFF(
(SELECT ';' + Description AS [text()]
FROM NewTable D1
WHERE D1.[Document] = D2.[Document] AND D1.[Value] = D2.[Value]
FOR XML PATH('')) , 1, 1, '')
FROM NewTable D2
GROUP BY D2.[Document], [Value]
Here's a way....
declare #table table(Document int,[Description] varchar(64), [Value] int, Line_No int)
insert into #table
values
(120,'First Row',100,1),
(120,'Second Row',0,2),
(120,'Third row',0,3),
(120,'Fourth row',0,4),
(120,'fifth row',0,5),
(120,'sixth row',203,6),
(120,'seventh row',256,7),
(120,'eighth row',259,8),
(120,'ninth row',0,9),
(120,'tenth row',0,10),
(120,'eleventh row',0,11)
--Find the end / anchor line which to stop the concatenation later
;with cte as(
select
t.Document
,t.[Value]
,t.Description
,t.Line_No
,Parent_Line = isnull(min(t2.Line_No) - 1, (select max(Line_No) from #table))
from
#table t
full join
#table t2 on t2.Document = t.Document
and t2.Line_No > t.Line_No
and t2.Value <> 0
where
t.Document is not null
group by
t.Document
,t.[Value]
,t.Line_No
,t.Description),
--Do the concatenation of the Description
cte2 as (
select
Document
,value
,All_Descriptions = STUFF((
SELECT ',' + t2.Description
FROM cte t2
WHERE t.Parent_Line = t2.Parent_Line
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
,Parent_Line
from
cte t)
--Get max [Value] for uniqueness
select
Document
,All_Descriptions
,[Value] = max([Value])
from
cte2
group by
Document
,All_Descriptions
order by
max([Value])

Split a string with no delimiters into columns

I need to split a string in a column into one character each into it's own column in SQL Server 2012.
Example: if I have a column with 'ABCDE', I need to split it into 'A', 'B', 'C', 'D', 'E', with each of these into their own columns.
The length of the column to be split may vary, so I need this to be as dynamic as possible.
My question is different from the other post (Can Mysql Split a column?) since mine doesn't have any delimiters.
Thanks
You can do this like this:
DECLARE #t TABLE(id int, n VARCHAR(50))
INSERT INTO #t VALUES
(1, 'ABCDEF'),
(2, 'EFGHIJKLMNOPQ')
;WITH cte AS
(SELECT id, n, SUBSTRING(n, 1, 1) c, 1 AS ind FROM #t
UNION ALL
SELECT id, n, SUBSTRING(n, ind + 1, 1), ind + 1 FROM cte WHERE LEN(n) > ind
)
SELECT *
FROM cte
PIVOT (MAX(c) FOR ind IN([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[12],[13],[14],[15])) p
Output:
id n 1 2 3 4 5 6 7 8 9 10 12 13 14 15
1 ABCDEF A B C D E F NULL NULL NULL NULL NULL NULL NULL NULL
2 EFGHIJKLMNOPQ E F G H I J K L M N P Q NULL NULL
Here is dynamic version:
DECLARE #l INT, #c VARCHAR(MAX) = ''
SELECT #l = MAX(LEN(n)) FROM PivotTable
WHILE #l > 0
BEGIN
SET #c = ',[' + CAST(#l AS VARCHAR(MAX)) + ']' + #c
SET #l = #l - 1
END
SET #c = STUFF(#c, 1, 1,'')
DECLARE #s NVARCHAR(MAX) = '
;WITH cte AS
(SELECT id, n, SUBSTRING(n, 1, 1) c, 1 AS ind FROM PivotTable
UNION ALL
SELECT id, n, SUBSTRING(n, ind + 1, 1), ind + 1 FROM cte WHERE LEN(n) > ind
)
SELECT *
FROM cte
PIVOT (MAX(c) FOR ind IN(' + #c + ')) p'
EXEC (#s)
I am interpreting the question as putting the characters into one column ("split a string in a column into one character each into it's own column"). However, I realize that this might be ambiguous.
One method is with a recursive CTE:
with chars as (
select left(val, 1) as c, substring(val, 2, len(val)) as rest
from (select 'ABCDE' as val union all select '123') t
union all
select left(rest, 1), substring(rest, 2, len(rest))
from chars
where rest <> ''
)
select c
from chars;
Just plug in your table and column in the subquery. Note that you might want to include other columns as well.
Here is a SQL Fiddle.
If you want multiple columns and the number is not fixed, then you will need
dynamic SQL.
If you want a new column for every character you simply need:
SELECT [1] = SUBSTRING(Col, 1, 1),
[2] = SUBSTRING(Col, 2, 1),
[3] = SUBSTRING(Col, 3, 1),
[4] = SUBSTRING(Col, 4, 1),
[5] = SUBSTRING(Col, 5, 1),
[6] = SUBSTRING(Col, 6, 1),
[7] = SUBSTRING(Col, 7, 1),
[8] = SUBSTRING(Col, 8, 1),
[9] = SUBSTRING(Col, 9, 1)
FROM (VALUES ('ABCDE'), ('FGHIJKLMN')) t (Col);
Which is fine, if you have a know number of columns. If you have an unknown number of columns, then you just need to generate the same SQL with n columns. To do this you will need a numbers table, and since many people do not have one, I will do a quick demo on how to dynamically generate one.
The below will generate a sequential list of numbers, 1 - 100,000,000.
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N3 AS N1 CROSS JOIN N3 AS N2)
SELECT Number
FROM Numbers;
It simply uses a table valued constructor to generate 10 rows (N1), then cross joins these 10 rows to get 100 rows (N2), then cross joins these 100 rows to get 10,000 rows (N3) and so on and so on. It finally uses ROW_NUMBER() to get the sequential numbers.
This probably needs to be cut down for this use, I hope you are not splitting a string that is 100,000,000 characters long, but the principle applies. You can just use TOP and the maximum length of your string to limit it. For each number you can just build up the necessary repetetive SQL required, which is:
,[n] = SUBSTRING(Col, n, 1)
So you have something like:
SELECT Number,
[SQL] = ',[' + CAST(Number AS VARCHAR(10)) + '] = SUBSTRING(Col, ' + CAST(Number AS VARCHAR(10)) + ', 1)'
FROM Numbers;
Which gives something like:
Number SQL
-----------------------------------
1 ,[1] = SUBSTRING(Col, 1, 1)
2 ,[2] = SUBSTRING(Col, 2, 1)
3 ,[3] = SUBSTRING(Col, 3, 1)
4 ,[4] = SUBSTRING(Col, 4, 1)
The final step is to build up your final statement by concatenating all the text in the column SQL; the best way to do this is using SQL Server's XML Extensions.
So your final query might end up like:
DECLARE #SQL NVARCHAR(MAX) = '';
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL DROP TABLE #T;
CREATE TABLE #T (Col VARCHAR(100));
INSERT #T (Col) VALUES ('ABCDE'), ('FGHIJKLMN');
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N3 AS N1 CROSS JOIN N3 AS N2)
SELECT #SQL = 'SELECT Col' +
( SELECT TOP (SELECT MAX(LEN(Col)) FROM #T)
',[' + CAST(Number AS VARCHAR(10)) + '] = SUBSTRING(Col, ' + CAST(Number AS VARCHAR(10)) + ', 1)'
FROM Numbers
FOR XML PATH(''), TYPE
).value('.', 'VARCHAR(MAX)') + '
FROM #T;';
EXECUTE sp_executesql #SQL;
Which gives:
Col 1 2 3 4 5 6 7 8 9
-------------------------------------------------
ABCDE A B C D E
FGHIJKLMN F G H I J K L M N
Finally, if you actually wanted to split it into rows, I would still use the same approach, with your adhoc numbers table, just join it to your original table:
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL DROP TABLE #T;
CREATE TABLE #T (Col VARCHAR(100));
INSERT #T (Col) VALUES ('ABCDE'), ('FGHIJKLMN');
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT TOP (SELECT MAX(LEN(Col)) FROM #T) ROW_NUMBER() OVER(ORDER BY N1.N) FROM N3 AS N1 CROSS JOIN N3 AS N2)
SELECT t.Col,
Position = n.Number,
Character = SUBSTRING(t.Col, n.Number, 1)
FROM #T AS t
INNER JOIN Numbers AS n
ON n.Number <= LEN(t.Col)
ORDER BY t.Col, n.Number;
Which gives something like:
Col Position Character
-------------------------------
ABCDE 1 A
ABCDE 2 B
ABCDE 3 C
ABCDE 4 D
ABCDE 5 E
One way
declare #str varchar(max) = 'ABCDE'
declare #sql nvarchar(max) = ''
declare #i int = 1
while (#i <= len(#str)) begin
set #sql += case when #i > 1 then ',' else '' end + '''' + substring(#str, #i, 1) + ''''
set #i += 1
end
exec('select ' + #sql)
(If ' can appear as a char you would need to substitute '')
This is a solution for a dynamic text length.
-- Generate demo data
CREATE TABLE #temp(col nvarchar(100))
INSERT INTO #temp(col)
VALUES(N'A'),(N'ABC'),(N'DEFGHI'),(N'AB'),(N'KLOMA')
-- Split all in multiple rows
CREATE TABLE #output (col nvarchar(100),part nchar(1), pos int)
;WITH cte AS(
SELECT col, LEFT(col, 1) as part, 1 as pos
FROM #temp
UNION ALL
SELECT col, SUBSTRING(col, pos+1,1) as part, pos+1 as part
FROM cte
WHERE LEN(col) > pos
)
INSERT INTO #output(col, part, pos)
SELECT col, part, pos
FROM cte
DECLARE #sql nvarchar(max), #columnlist nvarchar(max)
-- Generate Columlist for dynamic pivot
SELECT #columnlist = COALESCE(#columnlist + N',[' + CONVERT(nvarchar(max),pos) + ']', N'[' + CONVERT(nvarchar(max),pos) + ']')
FROM #output o
WHERE o.col = (SELECT TOP (1) col FROM #output ORDER BY LEN(col) DESC)
-- Pivoting for readability
SET #sql = N'
SELECT pvt.*
FROM #output o
PIVOT (
MAX(o.part)
FOR pos IN('+#columnlist+')
) as pvt'
EXEC (#sql)
-- Cleanup
DROP TABLE #temp
DROP TABLE #output
The keypart is the cte and the pivoting afterwards. If you have any questions, just give me a short feedback.

split comma separated values into distinct rows

I have a table which looks like this:
id fk_det userid
3 9 name1,name2
6 1 name3
9 2 name4,name5
12 3 name6,name7
I have already learned to regret having the values of the userid in comma separated values, so i want to split the rows up and end up with something that looks like
id fk_det userid
3 9 name1
x 9 name2
6 1 name3
9 2 name4
x 2 name5
12 3 name6
x 3 name7
i had been looking at something like this:
select fk_det, det, LEFT(userid, CHARINDEX(',',userid+',')-1),
STUFF(userid, 1, CHARINDEX(',',userid+','), '')
from global_permissions
but i'm not sure how to make it work when userid contains more than 2 items (which it might, some might have none, some might have multiple, just depends)
this is what I tend to use:
IF EXISTS (
SELECT 1
FROM dbo.sysobjects
WHERE id = object_id(N'[dbo].[ParseString]')
AND xtype in (N'FN', N'IF', N'TF'))
BEGIN
DROP FUNCTION [dbo].[ParseString]
END
GO
CREATE FUNCTION dbo.ParseString (#String VARCHAR(8000), #Delimiter VARCHAR(10))
RETURNS TABLE
AS
/*******************************************************************************************************
* dbo.ParseString
*
* Creator: magicmike
* Date: 9/12/2006
*
*
* Outline: A set-based string tokenizer
* Takes a string that is delimited by another string (of one or more characters),
* parses it out into tokens and returns the tokens in table format. Leading
* and trailing spaces in each token are removed, and empty tokens are thrown
* away.
*
*
* Usage examples/test cases:
Single-byte delimiter:
select * from dbo.ParseString2('|HDI|TR|YUM|||', '|')
select * from dbo.ParseString2('HDI| || TR |YUM', '|')
select * from dbo.ParseString2(' HDI| || S P A C E S |YUM | ', '|')
select * from dbo.ParseString2('HDI|||TR|YUM', '|')
select * from dbo.ParseString2('', '|')
select * from dbo.ParseString2('YUM', '|')
select * from dbo.ParseString2('||||', '|')
select * from dbo.ParseString2('HDI TR YUM', ' ')
select * from dbo.ParseString2(' HDI| || S P A C E S |YUM | ', ' ') order by Ident
select * from dbo.ParseString2(' HDI| || S P A C E S |YUM | ', ' ') order by StringValue
Multi-byte delimiter:
select * from dbo.ParseString2('HDI and TR', 'and')
select * from dbo.ParseString2('Pebbles and Bamm Bamm', 'and')
select * from dbo.ParseString2('Pebbles and sandbars', 'and')
select * from dbo.ParseString2('Pebbles and sandbars', ' and ')
select * from dbo.ParseString2('Pebbles and sand', 'and')
select * from dbo.ParseString2('Pebbles and sand', ' and ')
*
*
* Notes:
1. A delimiter is optional. If a blank delimiter is given, each byte is returned in it's own row (including spaces).
select * from dbo.ParseString3('|HDI|TR|YUM|||', '')
2. In order to maintain compatibility with SQL 2000, ident is not sequential but can still be used in an order clause
If you are running on SQL2005 or later
SELECT Ident, StringValue FROM
with
SELECT Ident = ROW_NUMBER() OVER (ORDER BY ident), StringValue FROM
*
*
* Modifications
*
*
********************************************************************************************************/
RETURN (
SELECT Ident, StringValue FROM
(
SELECT Num as Ident,
CASE
WHEN DATALENGTH(#delimiter) = 0 or #delimiter IS NULL
THEN LTRIM(SUBSTRING(#string, num, 1)) --replace this line with '' if you prefer it to return nothing when no delimiter is supplied. Remove LTRIM if you want to return spaces when no delimiter is supplied
ELSE
LTRIM(RTRIM(SUBSTRING(#String,
CASE
WHEN (Num = 1 AND SUBSTRING(#String,num ,DATALENGTH(#delimiter)) <> #delimiter) THEN 1
ELSE Num + DATALENGTH(#delimiter)
END,
CASE CHARINDEX(#Delimiter, #String, Num + DATALENGTH(#delimiter))
WHEN 0 THEN LEN(#String) - Num + DATALENGTH(#delimiter)
ELSE CHARINDEX(#Delimiter, #String, Num + DATALENGTH(#delimiter)) - Num -
CASE
WHEN Num > 1 OR (Num = 1 AND SUBSTRING(#String,num ,DATALENGTH(#delimiter)) = #delimiter)
THEN DATALENGTH(#delimiter)
ELSE 0
END
END
)))
End AS StringValue
FROM dbo.Numbers
WHERE Num <= LEN(#String)
AND (
SUBSTRING(#String, Num, DATALENGTH(ISNULL(#delimiter,''))) = #Delimiter
OR Num = 1
OR DATALENGTH(ISNULL(#delimiter,'')) = 0
)
) R WHERE StringValue <> ''
)
You would use it like this:
SELECT id, pk_det, V.StringValue as userid
FROM myTable T
OUTER APPLY dbo.ParseString(T.userId) V
The UDF requires a 'tally' or Number table which assumes the following schema:
IF NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = 'Numbers')
BEGIN
CREATE TABLE dbo.Numbers
(
Num INT NOT NULL
CONSTRAINT [PKC__Numbers__Num] PRIMARY KEY CLUSTERED (Num) on [PRIMARY]
)
;WITH Nbrs_3( n ) AS ( SELECT 1 UNION SELECT 0 ),
Nbrs_2( n ) AS ( SELECT 1 FROM Nbrs_3 n1 CROSS JOIN Nbrs_3 n2 ),
Nbrs_1( n ) AS ( SELECT 1 FROM Nbrs_2 n1 CROSS JOIN Nbrs_2 n2 ),
Nbrs_0( n ) AS ( SELECT 1 FROM Nbrs_1 n1 CROSS JOIN Nbrs_1 n2 ),
Nbrs ( n ) AS ( SELECT 1 FROM Nbrs_0 n1 CROSS JOIN Nbrs_0 n2 )
INSERT INTO dbo.Numbers(Num)
SELECT n
FROM ( SELECT ROW_NUMBER() OVER (ORDER BY n)
FROM Nbrs ) D ( n )
WHERE n <= 50000 ;
END
Numbers tables are an invaluable addition to your toolset. To quote Adam Machanic:
Numbers tables are truly invaluable. I use them all of the time for
string manipulation, simulating window functions, populating test
tables with lots of data, eliminating cursor logic, and many other
tasks that would be incredibly difficult without them.
Is using a table of numbers a hack, as I've seen some people claim?
No. Show me another way to efficiently do all of the things a numbers
table can. Does it waste space? No. The script below will use up
around 900 KB of disk space in each database. That's absolutely
nothing. You'll end up getting millions, maybe billions of times the
disk space investment back in terms of ease of development and time
saved.
http://dataeducation.com/you-require-a-numbers-table/
Try this :)
DECLARE #Name TABLE
(
id INT NULL ,
fk_det INT NULL ,
userid NVARCHAR(100) NULL
)
INSERT INTO #Name
( id, fk_det, userid)
VALUES (3,9,'name1,name2' )
INSERT INTO #Name
( id, fk_det, userid)
VALUES (6,1,'name3' )
INSERT INTO #Name
( id, fk_det, userid)
VALUES (9,2,'name4,name5' )
INSERT INTO #Name
( id, fk_det, userid)
VALUES (12,3,'name6,name7' )
SELECT *
FROM #Name
SELECT id,A.fk_det,
Split.a.value('.', 'VARCHAR(100)') AS String
FROM (SELECT id,fk_det,
CAST ('<M>' + REPLACE(userid, ',', '</M><M>') + '</M>' AS XML) AS String
FROM #Name) AS A CROSS APPLY String.nodes ('/M') AS Split(a);
As an alternative to the standard sproc call you see everywhere:
with temp as(
select id,fk_det,cast('<comma>'+replace(userid,',','</comma><comma>')+'</comma>' as XMLcomma
from global_permissions
)
select id,fk_det,a.value('comma[1]','varchar(512)')
cross apply temp.XMLcomma.nodes('/comma') t(a)