SQL Server 2012 Comma Parsing - sql-server

I'm trying to parse some comma separated values from a column in SQL Server 2012 while still keeping the data from the columns in the left and to the right.
I have seen some similar topic solutions but none seemed to be what I am looking for.
I have this:
FirstName LastName userid Regions ViewCosts HelpReviewCosts
---------------------------------------------------------------------
Darron Peters ya00003 All y y
John Davies ya30982 NA, EM, AP, LA n n
I am trying to parse the Regions column so that I can get this:
FirstName LastName userid Regions ViewCosts HelpReviewCosts
---------------------------------------------------------------------
Darron Peters ya00003 All y y
John Davies ya30982 NA n n
John Davies ya30982 EM n n
John Davies ya30982 AP n n
John Davies ya30982 LA n n

There are thousands of examples on how to split/parse strings. Below are two samples, one with a UDF and the other without. Both use a CROSS APPLY
With a UDF
Declare #Yourtable table (FirstName varchar(25) ,LastName varchar(25),userid varchar(25), Regions varchar(50), ViewCosts varchar(25), HelpReviewCosts varchar(25))
Insert Into #Yourtable values
('Darron','Peters','ya00003','All','y','y'),
('John','Davies','ya30982','NA, EM, AP, LA','n','n')
Select A.FirstName
,A.LastName
,A.userid
,Regions =B.RetVal
,A.ViewCosts
,A.HelpReviewCosts
From #Yourtable A
Cross Apply [dbo].[udf-Str-Parse](A.Regions,',') B
Without A UDF
Select A.FirstName
,A.LastName
,A.userid
,Regions =B.RetVal
,A.ViewCosts
,A.HelpReviewCosts
From #Yourtable A
Cross Apply (
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 A.Regions as [*] For XML Path('')),',','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B
Both Returns
THE UDF if needed
CREATE FUNCTION [dbo].[udf-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 #String as [*] For XML Path('')),#Delimiter,'</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
);
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
--Select * from [dbo].[udf-Str-Parse]('this,is,<test>,for,< & >',',')

I suggest you to use STRING_SPLIT function
WITH
CTE_Sample AS
(
SELECT 'All' AS txt
UNION ALL
SELECT 'NA, EM, AP, LA' AS txt
)
SELECT
txt,
value
FROM CTE_Sample
CROSS APPLY STRING_SPLIT(txt, ',');

If you don't want to 'udf' and 'string_split' function,then you can use this query.and it's suitable for large strings with comma separated and also much faster compared to others...
`CREATE TABLE TB (Number INT)
DECLARE #I INT=0
WHILE #I<1000
BEGIN
INSERT INTO TB VALUES (#I)
SET #I=#I+1
END
SELECT
FirstName
,LastName
,userid
,S_DATA
,ViewCosts
,HelpReviewCosts
FROM (
SELECT
FirstName
,LastName
,userid
,CASE WHEN LEN(LIST2)>0 THEN LTRIM(RTRIM(SUBSTRING(LIST2, NUMBER+1, CHARINDEX(',', LIST2, NUMBER+1)-NUMBER - 1)))
ELSE NULL
END AS S_DATA
,ViewCosts
,HelpReviewCosts
,NUMBER
FROM(
SELECT FirstName
,LastName
,userid
,','+Regions+',' LIST2
,ViewCosts
,HelpReviewCosts
FROM Tb1
)DT
LEFT OUTER JOIN TB N ON (N.NUMBER < LEN(DT.LIST2)) OR (N.NUMBER=1 AND DT.LIST2 IS NULL)
WHERE SUBSTRING(LIST2, NUMBER, 1) = ',' OR LIST2 IS NULL
) DT2
WHERE S_DATA<>''
this is my Output

Related

SQL Server : convert multiple rows in single column

I am running into an issue with T-SQL code. There is a CSV file that I need to import and transform into a SQL Server table. The problem is that the CSV file is not correctly format and looks like this:
Recipe,Recipe,Recipe,Recipe,...
0,1,3,4,...
Data1,Data2,Data3,Data4,...
...
The final result would need to be at least like this:
Recipe,0,Data1,...
Recipe,1,Data2,...
Recipe,3,Data3,...
Recipe,4,Data4,...
...
I have used FOR XML PATH to get all rows into one single string but I did not end up with anything good.
The information I have :
I always know the number of rows and columns that the file has.
I am using SQL Server 2016
I do not have sysadmin rights
Any help to show me the right path would be greatly appreciated!
Thanks!
Example
Declare #S varchar(max) = 'Recipe,Recipe,Recipe,Recipe
0,1,3,4
Data1,Data2,Data3,Data4'
;with cte as (
Select CN=A.RetSeq
,RN=B.RetSeq
,Value=B.RetVal
From [dbo].[tvf-Str-Parse](#S,char(13)+char(10)) A
Cross Apply [dbo].[tvf-Str-Parse](A.RetVal,',') B
)
Select Str = Stuff((Select ',' +Value From cte Where RN=A.RN Order By CN For XML Path ('')),1,1,'')
From (Select Distinct RN from cte) A
Order By A.RN
Returns
Str
Recipe,0,Data1
Recipe,1,Data2
Recipe,3,Data3
Recipe,4,Data4
The Function 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 1/0)
,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)
);
EDIT - OPTION WITHOUT FUNCTION
Declare #S varchar(max) = 'Recipe,Recipe,Recipe,Recipe
0,1,3,4
Data1,Data2,Data3,Data4'
;with cte as (
Select CN=A.RetSeq
,RN=B.RetSeq
,Value=B.RetVal
From (
Select RetSeq = row_number() over (order by 1/0)
,RetVal = ltrim(rtrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(#S,char(13)+char(10),'§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) A
Cross Apply (
Select RetSeq = row_number() over (order by 1/0)
,RetVal = ltrim(rtrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(A.RetVal,',','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) B
)
Select Str = Stuff((Select ',' +Value From cte Where RN=A.RN Order By CN For XML Path ('')),1,1,'')
From (Select Distinct RN from cte) A
Order By RN
Edit JSON OPTION -- Correcting for Double Quotes
Declare #S varchar(max) = 'Recipe,Recipe,Recipe,Recipe
1,,3,4
Data1,Data2,Data"3,Data4'
;with cte as (
Select CN = A.[key]
,RN = B.[Key]
,Value = replace(B.Value,'||','"')
From OpenJSON('["'+replace(replace(#S,'"','||'),char(13)+char(10),'","')+'"]') A
Cross Apply (
Select *
From OpenJSON('["'+replace(A.Value,',','","')+'"]')
) B
)
Select Str = Stuff((Select ',' +Value From cte Where RN=A.RN Order By CN For XML Path ('')),1,1,'')
From (Select Distinct RN from cte) A
Order By RN
Returns
Str
Recipe,1,Data1
Recipe,,Data2 -- null (2 is missing
Recipe,3,Data"3 -- has double quote
Recipe,4,Data4

How to use Substring to pull multiple items from a field

First post - I am trying to pull ten different pieces of information from a single field. Let me start with this is not my table, just what I was given to work with. This is a varchar max field.
'3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|~|10000.00|~|3354|#||~|3355|#||~3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212'
I know that the numbers that start with 33 are keys telling me what information is in that section. 3350 has the invoice #1234567. 3351 has the invoice date of 8/1/17. etc. 3354 and 3355 were left null. The keys are unchanging and will be the same for every record in the table.
I need to pull the data from between 3350|#| and |~|3351 to get my invoice# and between 3351|#| and |~|3352 to get my date, etc, but I am struggling with how to word this. Any help would be appreciated and any critiques on my first post will be taken constructively.
The #YourTable is just a table variable used for demonstration / illustration
For Rows - Example
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|#|10000.00|~|3354|#||~|3355|#||~|3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212')
Select A.ID
,Item = left(RetVal,charindex('|#|',RetVal+'|#|')-1)
,Value = right(RetVal,len(RetVal)-charindex('|#|',RetVal+'|#|')-2)
From #YourTable A
Cross Apply [dbo].[udf-Str-Parse](A.SomeCol,'|~|') B
Returns
For Columns - Example
Declare #YourTable table (ID int,SomeCol varchar(max))
Insert Into #YourTable values
(1,'3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|#|10000.00|~|3354|#||~|3355|#||~|3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212')
Select *
From (
Select A.ID
,Item = left(RetVal,charindex('|#|',RetVal+'|#|')-1)
,Value = right(RetVal,len(RetVal)-charindex('|#|',RetVal+'|#|')-2)
From #YourTable A
Cross Apply [dbo].[udf-Str-Parse](A.SomeCol,'|~|') B
) A
Pivot (max([Value]) For [Item] in ([3350],[3351],[3352],[3353],[3354],[3355],[3356],[3357],[3358],[3359]) ) p
Returns
The UDF if Interested
CREATE FUNCTION [dbo].[udf-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)
);
--Thanks Shnugo for making this XML safe
--Select * from [dbo].[udf-Str-Parse]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse]('John Cappelletti was here',' ')
You can try a tally based splitter like below
declare #t table ( id int, col nvarchar(max));
insert into #t values
(1, '3350|#|1234567|~|3351|#|8/1/2017|~|3352|#|Acme|~|3353|~|10000.00|~|3354|#||~|3355|#||~3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212')
,(2, '3350|#|123334567|~|3351|#|8/2/2017|~|3352|#|Acme|~|3353|~|10000.00|~|3354|#||~|3355|#||~3356|#|Yes|~|3357|#|Doe,John|~|3358|#|CA|~|3359|#|5551212');
select
id,
case
when split_values like '3350|#|%' then 'id'
when split_values like '3351|#|%' then 'date'
end as fieldname,
SUBSTRING(split_values,8,LEN(split_values)-7) as value
from
(
select
--t.col as col,
row_number() over (partition by t.col order by t1.N asc) as row_num,
t.id,
SUBSTRING( t.col, t1.N, ISNULL(NULLIF(CHARINDEX('|~|',t.col,t1.N),0)-t1.N,8000)) as split_values
from #t t
join
(
select
t.col,
1 as N
from #t t
UNION ALL
select
t.col,
t1.N + 3 as N
from #t t
join
(
select
top 8000
row_number() over(order by (select NULL)) as N
from
sys.objects s1
cross join
sys.objects s2
) t1
on SUBSTRING(t.col,t1.N,3) = '|~|'
) t1
on t1.col=t.col
)a
where
split_values like '3350|#|%' or
split_values like '3351|#|%'
Live demo

Convert strings to integers using PatIndex

I want to return integers from rather complex strings which combined unicode characters such as - and . with characters and integers.
I've come a long way in achieving this, but I still have troubles with some strings of a more complex structure. For instance:
DECLARE #Tabl as table
(
dats nvarchar(15)
)
INSERT INTO #Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4')
select LEFT(SUBSTRING(REPLACE(REPLACE(dats, '.',''),'-',''), PATINDEX('%[0-9.-]%', REPLACE(REPLACE(dats, '.',''),'-','')), 8000),
PATINDEX('%[^0-9.-]%', SUBSTRING(REPLACE(REPLACE(dats, '.',''),'-',''), PATINDEX('%[0-9.-]%', REPLACE(REPLACE(dats, '.',''),'-','')), 8000) + 'X')-1)
from #tabl
Gives
Raw Input Actual return: Desired return:
103-P705hh 103 103705
115-xxx-44 115 11544
103-705.13 10370513 10370513
525-hheef4 525 5254
I had a topic regarding this yesterday to cover the case when multiple - or . are present, but as seen in the return this is actually taken care of now. However, expanding the databases I work with I encountered much more complex string such as those I presented here.
Does anyone have any idea what to do when characters and integers are "mixed up" in the string?
Regards,
Cenderze
I have seen loads of solutions that use a scalar udf with a loop, but I don't like either of these things, so throwing my hat into the ring with a different approach.
With the help of a numbers table you can deconstruct each value into its individual characters, remove non-numeric characters, then reconstruct it using FOR XML to concatenate rows, e.g.
WITH Numbers (Number) AS
( SELECT ROW_NUMBER() OVER(ORDER BY N1.N)
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N2 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N3 (N) -- 1,000
--CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N4 (N) -- 10,000
--CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N5 (N) -- 100,000
--COMMENT OR UNCOMMENT ROWS AS NECESSARY DEPENDING ON YOU MAX STRING LENGTH
)
SELECT t.dats,
Stripped = x.data.value('.', 'INT')
FROM #tabl AS t
CROSS APPLY
( SELECT SUBSTRING(t.dats, n.Number, 1)
FROM Numbers n
WHERE n.Number <= LEN(t.dats)
AND SUBSTRING(t.dats, n.Number, 1) LIKE '[0-9]'
ORDER BY n.Number
FOR XML PATH(''), TYPE
) x (data);
Gives:
dats Stripped
----------------------
103-P705hh 103705
115-xxx-44 11544
103-705.13 10370513
525-hheef4 5254
I haven't done any testing so it could be that the added overhead of expanding each string into individual characters and reconstructing it is actually a lot more overhead than than a UDF with a loop.
I decided to bench mark this
1. Set up functions
CREATE FUNCTION dbo.ExtractNumeric_TVF (#Input VARCHAR(8000))
RETURNS TABLE
AS
RETURN
( WITH Numbers (Number) AS
( SELECT TOP (LEN(#Input)) ROW_NUMBER() OVER(ORDER BY N1.N)
FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N1 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N2 (N) -- 100
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N3 (N) -- 1,000
CROSS JOIN (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) AS N4 (N) -- 10,000
)
SELECT Stripped = x.data.value('.', 'VARCHAR(MAX)')
FROM ( SELECT SUBSTRING(#Input, n.Number, 1)
FROM Numbers n
WHERE n.Number <= LEN(#Input)
AND SUBSTRING(#Input, n.Number, 1) LIKE '[0-9]'
ORDER BY n.Number
FOR XML PATH(''), TYPE
) x (data)
);
GO
create function dbo.ExtractNumeric_UDF(#s varchar(8000))
returns varchar(8000)
as
begin
declare #out varchar(max) = ''
declare #c char(1)
while len(#s) > 0 begin
set #c = left(#s,1)
if #c like '[0123456789]' set #out += #c
set #s = substring(#s, 2, len(#s) -1)
end
return #out
end
GO
2. Create first set of sample data and log table
CREATE TABLE dbo.T (Value VARCHAR(8000) NOT NULL);
INSERT dbo.T (Value)
SELECT TOP 1000 LEFT(NEWID(), CEILING(RAND(CHECKSUM(NEWID())) * 36))
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
CREATE TABLE dbo.TestLog (Fx VARCHAR(255), NumberOfRows INT, TimeStart DATETIME2(7), TimeEnd DATETIME2(7))
3. Run Tests
GO
DECLARE #T TABLE (Val VARCHAR(8000));
INSERT dbo.TestLog (fx, NumberOfRows, TimeStart)
VALUES ('dbo.ExtractNumeric_UDF', 1000, SYSDATETIME());
INSERT #T (Val)
SELECT dbo.ExtractNumeric_UDF(Value)
FROM dbo.T;
UPDATE dbo.TestLog
SET TimeEnd = SYSDATETIME()
WHERE TimeEnd IS NULL;
GO 100
DECLARE #T TABLE (Val VARCHAR(8000));
INSERT dbo.TestLog (fx, NumberOfRows, TimeStart)
VALUES ('dbo.ExtractNumeric_TVF', 1000, SYSDATETIME());
INSERT #T (Val)
SELECT f.Stripped
FROM dbo.T
CROSS APPLY dbo.ExtractNumeric_TVF(Value) f;
UPDATE dbo.TestLog
SET TimeEnd = SYSDATETIME()
WHERE TimeEnd IS NULL;
GO 100
4. Get Results
SELECT Fx,
NumberOfRows,
RunTime = AVG(DATEDIFF(MILLISECOND, TimeStart, TimeEnd))
FROM dbo.TestLog
GROUP BY fx, NumberOfRows;
I did the following (using just NEWID() so only a maximum of 36 characters) over 1,000 and 10,000 rows, the results were:
Fx NumberOfRows RunTime
--------------------------------------------------------
dbo.ExtractNumeric_TVF 1000 31
dbo.ExtractNumeric_UDF 1000 56
dbo.ExtractNumeric_TVF 10000 280
dbo.ExtractNumeric_UDF 10000 510
So the TVF coming in at just under half the time of the UDF.
I wanted to test edge cases so put 1,000 rows of longer strings (5,400 characters)
TRUNCATE TABLE dbo.T;
INSERT dbo.T (Value)
SELECT TOP 1000
REPLICATE(CONCAT(NEWID(), NEWID(), NEWID(), NEWID(), NEWID()), 30)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b;
And this is where the TVF came into its own, running over 5x faster:
Fx NumberOfRows RunTime
------------------------------------------------
dbo.ExtractNumeric_TVF 1000 2485
dbo.ExtractNumeric_UDF 1000 12955
I also really don't like the looping solutions so I decided to try my hand at one. This is using a predefined tally table but is quite similar to others posted here already.
This is my tally table. I keep this as a view on my system.
create View [dbo].[cteTally] as
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
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
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
GO
Because I don't like looping I decided to use the table valued function approach which let me reuse this functionality in other queries with little to no effort. Here is one way to write such a function.
create function GetOnlyNumbers
(
#SearchVal varchar(8000)
) returns table as return
with MyValues as
(
select substring(#SearchVal, N, 1) as number
, t.N
from cteTally t
where N <= len(#SearchVal)
and substring(#SearchVal, N, 1) like '[0-9]'
)
select distinct NumValue = STUFF((select number + ''
from MyValues mv2
order by mv2.N
for xml path('')), 1, 0, '')
from MyValues mv
That looks good but the proof is in the pudding. Let's take this out with our sample data and kick the tires a few times.
DECLARE #Tabl as table
(
dats nvarchar(15)
)
INSERT INTO #Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4')
select *
from #Tabl t
cross apply dbo.GetOnlyNumbers(t.dats) x
Sure looks nice and tidy. I tested against several of the other solutions posted here and without going into deep testing this appears to be significantly faster than the other approaches posted at this time.
DECLARE #Tabl as table
(
ID INT,
dats nvarchar(15)
)
INSERT INTO #Tabl VALUES
(1, '103-P705hh'),
(2, '115-xxx-44'),
(3, '103-705.13'),
(4, '525-hheef4')
SELECT T.ID, t.dats
,(
SELECT SUBSTRING(tt.dats,V.number,1)
FROM #Tabl tt
JOIN master.dbo.spt_values V ON V.type='P' AND V.number BETWEEN 1 AND LEN(tt.dats)
WHERE tt.ID=T.ID AND SUBSTRING(TT.dats,V.number,1) LIKE '[0-9]'
ORDER BY V.number
FOR XML PATH('')
) S
FROM #Tabl t
ORDER BY T.ID;
Can you use a udf ? If so, try this
create alter function numerals(#s varchar(max))
returns varchar(max)
as
begin
declare #out varchar(max) = ''
declare #c char(1)
while len(#s) > 0 begin
set #c = left(#s,1)
if #c like '[0123456789]' set #out += #c
set #s = substring(#s, 2, len(#s) -1)
end
return #out
end
to use it on your temp table...
select dbo.numerals(dats) from #Tabl
another solution, that does not use a UDF, but will work only if your table has a primary key, uses a recursive CTE. It is:
DECLARE #Tabl as table
(pk int identity not null, -- <=== added a primary key
dats nvarchar(max) )
INSERT INTO #Tabl VALUES
('103-P705hh'),
('115-xxx-44'),
('103-705.13'),
('525-hheef4');
with newVals(pk, pos, newD) as
(select pk, 1,
case when left(Dats,1) like '[0123456789]'
then left(Dats,1) else '' end
from #tabl
Union All
Select t.pk, pos + 1, n.newD +
case when substring(dats, pos+1, 1) like '[0123456789]'
then substring(dats, pos+1, 1) else '' end
from #tabl t join newVals n on n.pk = t.pk
where pos+1 <= len(dats) )
Select newD from newVals x
where pos = (Select Max(pos)
from newVals
where pk = x.pk)

Split Data and transforming them into Columns

I have an Input table as under
Id Data
1 Column1: Value1
2 Column2: Value11
3 Column3: Value111
4 Column1: Value2
5 Column2: Value22
6 Column3: Value222
I am looking for an output as under
Column1 Column2 Column3
Value1 Value11 Value111
Value2 Value22 Value222
How can I achieve so? It could have been done easily by using a WHILE LOOP and by a bit of mathematical logic, but I am looking for a more optimized one if possible by only SELECT queries (no LOOPS).
I have tried also by splitting using (':') as delimiter and then transforming ROWS to COLUMNS (PIVOT) but somewhat could not be able to proceed. (That's my thought, peoples may have more better thoughts).
My shot so far
Declare #t table(Id int identity(1,1),Data varchar(1000))
Insert into #t Values
('Column1: Value1'),('Column2: Value11'),('Column3: Value111')
,('Column1: Value2'),('Column2: Value22'),('Column3: Value222')
Select *
FROM #t
SELECT
F1.id,
F1.Data,
O.splitdata
FROM
(
SELECT *,
cast('<X>'+replace(F.Data,':','</X><X>')+'</X>' as XML) as xmlfilter from #t F
)F1
CROSS APPLY
(
SELECT fdata.D.value('.','varchar(50)') as splitdata
FROM f1.xmlfilter.nodes('X') as fdata(D)) O
This will work if you want a pure SQL solution:
Select [Column1], [Column2], [Column3] From (
Select col, val, id = ROW_NUMBER() over(partition by d.col order by d.id)
From (
Select id
, col = LEFT(Data, CHARINDEX(':', Data)-1)
, val = RIGHT(Data, LEN(DATA) - CHARINDEX(':', Data))
From #t
) as d
) as p
pivot(
MAX(val)
FOR col in([Column1], [Column2], [Column3])
) as piv
But it supposes that data for Row 1 are always before data for Row 2. There is no way to distinguish them using your sample.
If the number of column is not fixed, it has to use Dynamic SQL.
SQL Server may not be the best options for this kind of thing.
With Dynamic SQL, the above query would be like this one:
create table #t(Id int identity(1,1),Data varchar(1000))
Insert into #t Values
('Column1: Value1'),('Column2: Value11'),('Column3: Value111')
,('Column1: Value2'),('Column2: Value22'),('Column3: Value222')
Declare #sql nvarchar(max)
Select #sql = '
Select '+left(c, len(c)-1)+' From (
Select col, val, id = ROW_NUMBER() over(partition by d.col order by d.id)
From (
Select id
, col = LEFT(Data, CHARINDEX('':'', Data)-1)
, val = RIGHT(Data, LEN(DATA) - CHARINDEX('':'', Data))
From #t
) as d
) as p
pivot(
MAX(val)
FOR col in('+left(c, len(c)-1)+')
) as piv
'
From (
Select Distinct '['+LEFT(Data, CHARINDEX(':', Data)-1)+'], '
From #t
FOR XML PATH('')
) as d(c)
EXEC sp_executesql #sql
SQL Fiddle
This should work:
Declare #t table(Id int identity(1,1),Data varchar(1000))
Insert into #t Values
('Column1: Value1'),('Column2: Value11'),('Column3: Value111')
,('Column1: Value2'),('Column2: Value22'),('Column3: Value222');
WITH Splitted AS
(
SELECT *
,CAST('<X>'+REPLACE(F.Data,':','</X><X>')+'</X>' AS XML) AS xmlfilter
FROM #t AS F
)
SELECT p.*
FROM
(
SELECT ROW_NUMBER() OVER(PARTITION BY xmlfilter.value('X[1]','varchar(max)') ORDER BY Id) AS Inx
,xmlfilter.value('X[1]','varchar(max)') AS ColName
,xmlfilter.value('X[2]','varchar(max)') AS ColVal
FROM Splitted
) AS tbl
PIVOT
(
MAX(ColVal) FOR ColName IN(Column1,Column2,Column3)
) AS p

Reversing the sort order of a cte

In Microsoft SQL Server, the following works, but produces:
,Son,Dad,Granddad,Great Granddad
whereas I need it to say:
Great Granddad,Granddad,Dad,Son
declare #Family Table(
ID Int Identity(100,1) Primary Key
,Person varchar(128)
,ParentID Int default 0
)
insert into #Family(Person,ParentID) values('Great Granddad',0)
insert into #Family(Person,ParentID) values('Granddad',100)
insert into #Family(Person,ParentID) values('Dad',101)
insert into #Family(Person,ParentID) values('Son',102)
DECLARE #ID Int = 103
;with cte1 as (
select
#ID AS cteID
,ID
,ParentID
,Person as ctePerson
from #Family
where ID = #ID -- this is the starting point you want in your recursion
UNION ALL
select #ID, F.ID, F.ParentID, F.Person
from #Family F
join cte1 P on P.ParentID = F.ID -- this is the recursion
)
-- cte2 can reverse the sort order based on something built in (OVER?)
-- ROW_NUMBER() OVER(ORDER BY ? DESC) AS Row
,cte3 AS(
select ID as cte3ID,(
SELECT ',' + ctePerson
FROM cte1
WHERE cteID = F.ID
FOR XML PATH ('')
) as People
from #Family F
where ID=#ID
)
SELECT * FROM CTE3
I would not order the result of a recursive CTE by using another CTE, as the results of CTEs are semantically tables, and therfore the order is not guaranteed. Instead order when selecting from a CTE, just als like with normal tables.
I would suggest to insert a field representing the level or relationship and order by that:
;with cte1 as (
select
#ID AS cteID
,ID
,ParentID
,Person as ctePerson
,0 lvl -- starting level with 0
from #Family
where ID = #ID -- this is the starting point you want in your recursion
UNION ALL
select #ID, F.ID, F.ParentID, F.Person
, lvl + 1 -- increase level by 1
from #Family F
join cte1 P on P.ParentID = F.ID -- this is the recursion
)
,cte3 AS(
select ID as cte3ID,STUFF(( -- stuff removes the first ','
SELECT ',' + ctePerson
FROM cte1
WHERE cteID = F.ID
ORDER by lvl DESC -- order by level DESC to start with latest ancestor
FOR XML PATH ('')
), 1, 1, '') as People
from #Family F
where ID=#ID
)
SELECT * FROM CTE3

Resources