How to use Substring to pull multiple items from a field - sql-server

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

Related

Filling the ID column of a table NOT using a cursor

Tables have been created and used without and ID column, but ID column is now needed. (classic)
I heard everything could be done without cursors. I just need every row to contain a different int value so I was looking for some kind of row number function :
How do I use ROW_NUMBER()?
I can't tell exactly how to use it even with these exemples.
UPDATE [TableA]
SET [id] = (select ROW_NUMBER() over (order by id) from [TableA])
Subquery returned more than 1 value.
So... yes of course it return more than one value. Then how to mix both update and row number to get that column filled ?
PS. I don't need a precise order, just unique values. I also wonder if ROW_NUMBER() is appropriate in this situation...
You can use a CTE for the update
Example
Declare #TableA table (ID int,SomeCol varchar(50))
Insert Into #TableA values
(null,'Dog')
,(null,'Cat')
,(null,'Monkey')
;with cte as (
Select *
,RN = Row_Number() over(Order by (Select null))
From #TableA
)
Update cte set ID=RN
Select * from #TableA
Updated Table
ID SomeCol
1 Dog
2 Cat
3 Monkey
You can use a subquery too as
Declare #TableA table (ID int,SomeCol varchar(50))
Insert Into #TableA values
(null,'Dog')
,(null,'Cat')
,(null,'Monkey');
UPDATE T1
SET T1.ID = T2.RN
FROM #TableA T1 JOIN
(
SELECT ROW_NUMBER()OVER(ORDER BY (SELECT 1)) RN,
*
FROM #TableA
) T2
ON T1.SomeCol = T2.SomeCol;
Select * from #TableA

SQL Server 2012 Comma Parsing

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

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

Using Split with CTE to get Even and Odd Indexed value in sql

I do have a list of data having members Choice and IsRightAnswer for MCQ. I want to send this list to stored procedure that I did by having ',' as delimiter. Now in stored procedure I want to have choice and IsRightAnswer into two seperate table which I tried to do by separating them by odd and even index. I got stuck into ORDERBY condition of ROW_NUMBER. How can I do it efficiently?
DECLARE #temp table(Choice nvarchar(500), [rowCount] int IDENTITY(1,1))
DECLARE #tempIsRight table(IsRight bit, [rowCount] int IDENTITY(1,1))
;with tempChoice
as
(
select *,ROW_NUMBER() OVER (ORDER BY '') AS RowNumber
from dbo.Split(#Choice,',')
)
INSERT INTO #temp select * from tempChoice where RowNumber%2=0
;with tempIsRight
as
(
select *,ROW_NUMBER() OVER (ORDER BY '') AS RowNumber
from dbo.Split(#Choice,',')
)
INSERT INTO #tempIsRight select * from tempIsRight where RowNumber%2!=0
You can give dummy for ORDER BY like - (SELECT 1)
DECLARE #temp table(Choice nvarchar(500), [rowCount] int IDENTITY(1,1))
DECLARE #tempIsRight table(IsRight bit, [rowCount] int IDENTITY(1,1))
;with tempChoice
as
(
select *,ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS RowNumber
from dbo.Split(#Choice,',')
)
INSERT INTO #temp select * from tempChoice where RowNumber%2=0
;with tempIsRight
as
(
select *,ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS RowNumber
from dbo.Split(#Choice,',')
)
INSERT INTO #tempIsRight select * from tempIsRight where RowNumber%2!=0

How to create multiple columns from a delimited string

I have a an inventory field with 4 components, each separated by a "-". I'm not sure how to use charindex to parse the string so that I am returning a column for each value found between delmiters. For example: field value or string = item-color-size-shape.
My goal is to end up with a item as column, color as column, size as a column and finally shape.
If it is max 4 columns you can try like this:
declare #Table table (SomeColumn varchar(100));
insert into #Table
select 'ball-blue-small-round' union all
select 'ball-red-small-round' union all
select 'ball-green-small-round' union all
select 'ball---square' union all
select '----';
;with stage (s)
as ( select replace(SomeColumn, '-', '.')
from #Table
)
select [item] = parsename(s,4),
[color] = parsename(s,3),
[size] = parsename(s,2),
[shape] = parsename(s,1)
from stage;
If its > 4 please reply and we can work on a more dynamic solution.
Using CROSS APPLYs (used lengthy names for understanding) -
declare #Table table (SomeColumn varchar(100));
insert into #Table
select 'ball-Orange-small-round' union all
select 'bat-blue-medium-square' union all
select 'stumps-green-large-rectangle'
SELECT * FROM #Table
SELECT Sub1.FirstSub1 AS Item
,Sub2.SecondSub1 AS Color
,Sub3.ThirdSub1 AS Size
,SubAfterThirdHyphen AS Shape
FROM #Table
CROSS APPLY (SELECT CHARINDEX('-',SomeColumn) AS FirstHyphenPos) AS Pos1
CROSS APPLY (SELECT SUBSTRING(SomeColumn,1,FirstHyphenPos-1) AS FirstSub1) AS Sub1
CROSS APPLY (SELECT SUBSTRING(SomeColumn,FirstHyphenPos+1,LEN(SomeColumn)) AS SubAfterFirstHyphen) AS Substr1
CROSS APPLY (SELECT CHARINDEX('-',Substr1.SubAfterFirstHyphen) AS SecondHyphenPos) AS Pos2
CROSS APPLY (SELECT SUBSTRING(Substr1.SubAfterFirstHyphen,1,SecondHyphenPos-1) AS SecondSub1) AS Sub2
CROSS APPLY (SELECT SUBSTRING(Substr1.SubAfterFirstHyphen,SecondHyphenPos+1,LEN(Substr1.SubAfterFirstHyphen)) AS SubAfterSecondHyphen) AS Substr2
CROSS APPLY (SELECT CHARINDEX('-',Substr2.SubAfterSecondHyphen) AS ThirdHyphenPos) AS Pos3
CROSS APPLY (SELECT SUBSTRING(Substr2.SubAfterSecondHyphen,1,ThirdHyphenPos-1) AS ThirdSub1) AS Sub3
CROSS APPLY (SELECT SUBSTRING(Substr2.SubAfterSecondHyphen,ThirdHyphenPos+1,LEN(Substr2.SubAfterSecondHyphen)) AS SubAfterThirdHyphen) AS Substr3

Resources