How do I split a string which can contain 2 names into Person1 and Person2 ? There is no delimiter between the names, there is not always a second person for each row and not necessarily a middle initial/name for either first or second person and only sometimes the second name will be separated with an “AND”
Examples of Names are as follows
JANE MIDDLETON John MIDDLETON
SUE FRACARO BOB FRACARO
TONY FRENCH
JOHN EDUARDO OCHOA AND JANE ADRIANA OCHOA
TONY JOHN CARPENTER TONYA CARPENTER
Desired Output Design
Person 1 First Name
Person 1 Middle Name
Person 1 Last Name
Person 2 First Name
Person 2 Middle Name
Person 2 Last Name
Create a function for splitting string values in question from some table column:
CREATE FUNCTION [dbo].[Fn_Splittemp]
(
#text VARCHAR(8000)
, #delimiter VARCHAR(20) = ' '
)
RETURNS #String TABLE
(
Position INT IDENTITY PRIMARY KEY
, StringValue VARCHAR(8000)
)
AS
BEGIN
DECLARE #index INT
SET #index = -1
WHILE (LEN(#text) >0)
BEGIN
SET #index = CHARINDEX(#delimiter, #text)
IF (#index = 0) AND (LEN(#text) > 0)
BEGIN
INSERT INTO #string VALUES (#text)
BREAK
END
IF (#index > 1)
BEGIN
INSERT INTO #String VALUES (LEFT(#text, (#index-1)))
SET #text = RIGHT(#text, (LEN(#text)-#index))
END
ELSE
SET #text = RIGHT(#text, (LEN(#text)-#index))
END
RETURN
END
GO
Splitting, cleaning and assigning to respective name_columns:
CREATE TABLE #t0 (rid INT IDENTITY, rawnames VARCHAR(8000));
GO
INSERT INTO #t0 VALUES ('JANE MIDDLETON John MIDDLETON'),
('SUE FRACARO BOB FRACARO'),
('TONY FRENCH'),
('JOHN EDUARDO OCHOA AND JANE ADRIANA OCHOA'),
('TONY JOHN CARPENTER TONYA CARPENTER');
GO
SELECT n.rid, n.rawnames, fn.StringValue AS Names,
COUNT(*) OVER(PARTITION BY rawnames) AS wordcount,
ROW_NUMBER() OVER(PARTITION BY fn.stringvalue,rawnames ORDER BY fn.stringvalue) AS LastNameids,
fn.Position
INTO #t1
FROM #t0 n
cross apply dbo.Fn_Splittemp(n.rawnames, ' ') AS fn
GO
SELECT rid, rawnames, Position AS Pid,
PersonName, LastName INTO #t2
FROM
(SELECT t.rid, t.rawnames, t.names AS Lastname, LTRIM(RTRIM(REPLACE(f.StringValue,'and',''))) AS PersonName, f.Position
FROM
(SELECT replace(sqa.rawnames,sqa.Names,sqa.Names+',') AS delimstr , sqa.*
FROM #t1 sqa
WHERE wordcount<=3 AND position = (SELECT MAX(position) from #t1 crq where crq.rid = sqa.rid)
)t
cross apply dbo.Fn_Splittemp(delimstr,',') f
UNION ALL
SELECT b.rid, b.rawnames, b.Names AS Lastname, LTRIM(RTRIM(REPLACE(f.StringValue,'and',''))) AS PersonName, f.Position
FROM
(SELECT replace(rawnames,names,names+',') AS delimstr, *
FROM #t1
WHERE wordcount>3 AND LastNameids>1)b
cross apply dbo.Fn_Splittemp(delimstr,',') f
)sqt
GO
SELECT * INTO #t3 FROM #t2 cross apply dbo.Fn_Splittemp(personname, ' ');
GO
SELECT t.rawnames, fina.Firstname, fina.MiddleName, fina.LastName
FROM #t0 t
JOIN (
SELECT rid, pid, [1] AS Firstname, NULL AS MiddleName, [2] AS LastName
FROM
(SELECT * FROM (SELECT rid, pid, position, stringvalue,
COUNT(*) OVER(PARTITION BY rid, pid) AS cnt FROM #t3)a
WHERE a.cnt <=2)apiv
PIVOT
(MAX(stringvalue)
FOR position IN ([1],[2])
)piva
UNION ALL
SELECT rid, pid, [1] AS Firstname, [2] AS MiddleName, [3] AS LastName
FROM
(SELECT * FROM (SELECT rid, pid, position, stringvalue,
COUNT(*) OVER(PARTITION BY rid, pid) AS cnt FROM #t3)a
WHERE a.cnt >2)apiv
PIVOT
(MAX(stringvalue)
FOR position IN ([1],[2],[3])
)piva
)fina
ON fina.rid = t.rid;
I have a table of data that I am attempting to transpose/pivot rows to columns with.
I'm au fait with PIVOT/UNPIVOT and I tried to do this however due to the aggregation used with PIVOT I was only returning 1 result which was the first Endorsement "M06" and not the other 2 Endorsements for that PolRef#.
Here is my data example:
CREATE TABLE #temptable (
[B#] int,
[key#] varchar(24),
[Ref#] varchar(6),
[PolRef#] varchar(10),
[Sequence#] int,
[Date] datetime,
[Endnumber] varchar(4),
[Desc] varchar(76),
[Value] int,
[Specdrivers] varchar(76),
[Reg] varchar(76)
)
INSERT INTO #temptable VALUES
( 6, '484F445830314D4330310132', 'HODX01', 'HODX01MC01', 1050, N'2019-09-20T00:00:00', 'M06', 'Garaging/storage', 0, NULL, 'All' ),
( 6, '484F445830314D433031013C', 'HODX01', 'HODX01MC01', 1060, N'2019-09-20T00:00:00', '046', 'NCB deleted', 0, NULL, 'All' ),
( 6, '484F445830314D4330310146', 'HODX01', 'HODX01MC01', 1070, N'2019-09-20T00:00:00', '099', 'Limited mileage', 1500, NULL, 'All' )
DROP TABLE #temptable
Essentially I need a column for each row that says "Applicable Endorsement" for just the Endnumber value.
Output would look like:
| B# | PolRef# | Applicable Endorsement | Applicable Endorsement | Applicable Endorsement |
| 6 | HODX01MC01 | M06 | 046 | 099 |
Any thoughts on how I can go about this, note there can be any number of endorsements it isn't a fixed amount.
You can use the following PIVOT:
;WITH PrePivot AS
(
SELECT
T.B#,
T.PolRef#,
T.Endnumber,
PivotRanking = ROW_NUMBER() OVER (
PARTITION BY
T.B#,
T.PolRef#
ORDER BY
(SELECT NULL)) -- Determine the order here, maybe T.Date?
FROM
#temptable AS T
)
SELECT
P.B#,
P.PolRef#,
[Applicable Endorsement] = P.[1],
[Applicable Endorsement] = P.[2],
[Applicable Endorsement] = P.[3],
[Applicable Endorsement] = P.[4],
[Applicable Endorsement] = P.[5]
FROM
PrePivot AS V
PIVOT (
MAX(V.Endnumber) FOR V.PivotRanking IN ([1],[2],[3],[4],[5])
) AS P
I've written up to 5 endorsements, you can add as many as you want. If you want a dynamic amount, you need to use a dynamic pivot.
B# PolRef# Applicable Endorsement Applicable Endorsement Applicable Endorsement Applicable Endorsement Applicable Endorsement
6 HODX01MC01 M06 046 099 NULL NULL
Try this Dynamic Sql
IF OBJECT_ID('tempdb..#FormatedTable')IS NOT NULL
DROP TABLE #FormatedTable
Go
SELECT ROW_NUMBER()OVER(ORDER BY (SELECT 1)) As SeqId,'Applicable Endorsement'+CAST(ROW_NUMBER()OVER(ORDER BY (SELECT 1)) AS VARCHAR(100)) AS ReqColumn,*
INTO #FormatedTable
FROM #temptable
DECLARE #Sql nvarchar(max),
#DynamicColumn nvarchar(max),
#MaxDynamicColumn nvarchar(max)
SELECT #DynamicColumn = STUFF((SELECT ', '+QUOTENAME(ReqColumn)
FROM #FormatedTable FOR XML PATH ('')),1,1,'')
SELECT #MaxDynamicColumn = STUFF((SELECT ', '+'MAX('+(QUOTENAME(ReqColumn))+') AS '+QUOTENAME(CAST(ReqColumn AS VARCHAR(100)))
FROM #FormatedTable ORDER BY SeqId FOR XML PATH ('')),1,1,'')
SELECT #MaxDynamicColumn
SET #Sql=' SELECT [B#],[PolRef#], '+ #MaxDynamicColumn+'
FROM
(
SELECT * FROM #FormatedTable
) AS src
PIVOT
(
MAX(Endnumber) FOR [ReqColumn] IN ('+#DynamicColumn+')
) AS Pvt
GROUP BY [B#],[PolRef#]
'
PRINT (#Sql)
EXEC (#Sql)
Result
B# PolRef# Applicable Endorsement1 Applicable Endorsement2 Applicable Endorsement3
--------------------------------------------------------------------------------------------
6 HODX01MC01 M06 046 099
I have data like the following table. First two columns are list of country codes with pipe separator. There are two group of rows with RANK as 1 and 2.
I am trying to identify the country codes which are present in CountryList1 but not present in the column CountryList1 over a give RANK. For Rank 1 rows, HN JP SK and KY is present in CountryList1 but not present in CountryList2. Likewise, for Rank 2 rows. HN is present in CountryList1 but not present in CountryList2.
I am expecting Output like second table. I do not want to use a function or a procedure but trying to accomplish it using select statement.
Input
CountryList1 || CountryList2 || RANK
================||==============||=======
HN|IN|US || GB|CA|CH|CA || 1
JP|CH || IN|US|LU || 1
HN|SK|KY || GB|CA || 1
FI || IN|MO || 1
HN|IN|US || HN || 2
JP|CH || CH|IN|US || 2
HN || NO || 2
Output
DistinctCountry1 || RAN
====================||========
HN || 1
JP || 1
SK || 1
KY || 1
JP || 2
You have an abominable data structure. You should be storing elements of a list as separate values on rows. But you can do something by splitting the values. SQL Server 2016 has string_split(). For earlier versions you can find one on the web.
with tc as (
select t.*, s.country1
from t cross apply
(string_split(t.countrylist1, '|') s(country1)
)
select distinct t.country1, t.rnk
from tc
where not exists (select 1
from t t2
where tc.rnk = t2.rnk and
tc.country in (select value from string_split(t2.country_list))
);
This will not be efficient. And with the data structure you have, there is little scope for improving performance.
Here's a nice loop you can use for this:
declare #holding table (country1 varchar(max), country2 varchar(max), rank int)
declare #iterator int=1
declare #countrylistoriginal1 varchar(max)
declare #countrylistoriginal2 varchar(max)
declare #countrylist1 varchar(max)
declare #countrylist2 varchar(max)
declare #rank int
while #iterator<=(select max(rowid) from #temp2)
begin
select #countrylistoriginal1=countrylist1+'|', #rank=[rank]
from yourtable where rowid=#iterator
while #countrylistoriginal1<>''
begin
set #countrylist1=left(#countrylistoriginal1,(charindex('|',#countrylistoriginal1)))
set #countrylistoriginal1=replace(#countrylistoriginal1, #countrylist1,'')
select #countrylistoriginal2=countrylist2+'|'
from yourtable where rowid=#iterator
while #countrylistoriginal2<>''
begin
set #countrylist2=left(#countrylistoriginal2,(charindex('|',#countrylistoriginal2)))
set #countrylistoriginal2=replace(#countrylistoriginal2, #countrylist2,'')
insert #holding
select replace(#countrylist1,'|',''), replace(#countrylist2,'|',''), #rank
end
end
set #iterator=#iterator+1
end
select distinct a.country1, a.rank from #holding a
left join #holding b on a.country1=b.country2 and a.rank=b.rank where b.country2 is null
Try this...
Table Schema and data
CREATE TABLE [tableName](
[CountryList1] [nvarchar](50) NULL,
[CountryList2] [nvarchar](50) NULL,
[RANK] [int] NULL
)
INSERT [tableName] ([CountryList1], [CountryList2], [RANK]) VALUES (N'HN|IN|US', N'GB|CA|CH|CA', 1)
INSERT [tableName] ([CountryList1], [CountryList2], [RANK]) VALUES (N'JP|CH ', N'IN|US|LU', 1)
INSERT [tableName] ([CountryList1], [CountryList2], [RANK]) VALUES (N'HN|SK|KY', N'GB|CA', 1)
INSERT [tableName] ([CountryList1], [CountryList2], [RANK]) VALUES (N'FI', N'IN|MO', 1)
INSERT [tableName] ([CountryList1], [CountryList2], [RANK]) VALUES (N'HN|IN|US', N'HN ', 2)
INSERT [tableName] ([CountryList1], [CountryList2], [RANK]) VALUES (N'JP|CH', N'CH|IN|US', 2)
INSERT [tableName] ([CountryList1], [CountryList2], [RANK]) VALUES (N'HN', N'NO', 2)
SQL Query
;WITH cte AS
( SELECT DISTINCT *
FROM (SELECT [value] AS DistinctCountry1,
[rank],
Rtrim(Ltrim([value])) + Cast([rank] AS NVARCHAR(max)) AS colX
FROM tablename
CROSS apply String_split([countrylist1], '|')) tmp
WHERE colx NOT IN (SELECT Rtrim(Ltrim([value])) + Cast([rank] AS NVARCHAR(max)) AS colX
FROM tablename
CROSS apply String_split([countrylist2], '|'))
)
SELECT [distinctcountry1], [rank]
FROM cte
ORDER BY [rank]
Output
+------------------+------+
| distinctcountry1 | rank |
+------------------+------+
| FI | 1 |
| HN | 1 |
| JP | 1 |
| KY | 1 |
| SK | 1 |
| JP | 2 |
+------------------+------+
Demo: http://www.sqlfiddle.com/#!18/19acb/2/0
Note: As others already suggested, you should really consider fixing your table or you'll have to put extra hours when manipulating data.
I have a SQL Table like this:
| SomeID | OtherID | Data
+----------------+-------------+-------------------
| abcdef-..... | cdef123-... | 18,20,22
| abcdef-..... | 4554a24-... | 17,19
| 987654-..... | 12324a2-... | 13,19,20
is there a query where I can perform a query like SELECT OtherID, SplitData WHERE SomeID = 'abcdef-.......' that returns individual rows, like this:
| OtherID | SplitData
+-------------+-------------------
| cdef123-... | 18
| cdef123-... | 20
| cdef123-... | 22
| 4554a24-... | 17
| 4554a24-... | 19
Basically split my data at the comma into individual rows?
I am aware that storing a comma-separated string into a relational database sounds dumb, but the normal use case in the consumer application makes that really helpful.
I don't want to do the split in the application as I need paging, so I wanted to explore options before refactoring the whole app.
It's SQL Server 2008 (non-R2).
You can use the wonderful recursive functions from SQL Server:
Sample table:
CREATE TABLE Testdata
(
SomeID INT,
OtherID INT,
String VARCHAR(MAX)
);
INSERT Testdata SELECT 1, 9, '18,20,22';
INSERT Testdata SELECT 2, 8, '17,19';
INSERT Testdata SELECT 3, 7, '13,19,20';
INSERT Testdata SELECT 4, 6, '';
INSERT Testdata SELECT 9, 11, '1,2,3,4';
The query
WITH tmp(SomeID, OtherID, DataItem, String) AS
(
SELECT
SomeID,
OtherID,
LEFT(String, CHARINDEX(',', String + ',') - 1),
STUFF(String, 1, CHARINDEX(',', String + ','), '')
FROM Testdata
UNION all
SELECT
SomeID,
OtherID,
LEFT(String, CHARINDEX(',', String + ',') - 1),
STUFF(String, 1, CHARINDEX(',', String + ','), '')
FROM tmp
WHERE
String > ''
)
SELECT
SomeID,
OtherID,
DataItem
FROM tmp
ORDER BY SomeID;
-- OPTION (maxrecursion 0)
-- normally recursion is limited to 100. If you know you have very long
-- strings, uncomment the option
Output
SomeID | OtherID | DataItem
--------+---------+----------
1 | 9 | 18
1 | 9 | 20
1 | 9 | 22
2 | 8 | 17
2 | 8 | 19
3 | 7 | 13
3 | 7 | 19
3 | 7 | 20
4 | 6 |
9 | 11 | 1
9 | 11 | 2
9 | 11 | 3
9 | 11 | 4
Finally, the wait is over with SQL Server 2016. They have introduced the Split string function, STRING_SPLIT:
select OtherID, cs.Value --SplitData
from yourtable
cross apply STRING_SPLIT (Data, ',') cs
All the other methods to split string like XML, Tally table, while loop, etc.. have been blown away by this STRING_SPLIT function.
Here is an excellent article with performance comparison: Performance Surprises and Assumptions: STRING_SPLIT.
For older versions, using tally table here is one split string function(best possible approach)
CREATE FUNCTION [dbo].[DelimitedSplit8K]
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 0 up to 10,000...
-- enough to cover NVARCHAR(4000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
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 (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
Referred from Tally OH! An Improved SQL 8K “CSV Splitter” Function
Check this
SELECT A.OtherID,
Split.a.value('.', 'VARCHAR(100)') AS Data
FROM
(
SELECT OtherID,
CAST ('<M>' + REPLACE(Data, ',', '</M><M>') + '</M>' AS XML) AS Data
FROM Table1
) AS A CROSS APPLY Data.nodes ('/M') AS Split(a);
Very late but try this out:
SELECT ColumnID, Column1, value --Do not change 'value' name. Leave it as it is.
FROM tbl_Sample
CROSS APPLY STRING_SPLIT(Tags, ','); --'Tags' is the name of column containing comma separated values
So we were having this:
tbl_Sample :
ColumnID| Column1 | Tags
--------|-----------|-------------
1 | ABC | 10,11,12
2 | PQR | 20,21,22
After running this query:
ColumnID| Column1 | value
--------|-----------|-----------
1 | ABC | 10
1 | ABC | 11
1 | ABC | 12
2 | PQR | 20
2 | PQR | 21
2 | PQR | 22
Thanks!
select t.OtherID,x.Kod
from testData t
cross apply (select Code from dbo.Split(t.Data,',') ) x
As of Feb 2016 - see the TALLY Table Example - very likely to outperform my TVF below, from Feb 2014. Keeping original post below for posterity:
Too much repeated code for my liking in the above examples. And I dislike the performance of CTEs and XML. Also, an explicit Id so that consumers that are order specific can specify an ORDER BY clause.
CREATE FUNCTION dbo.Split
(
#Line nvarchar(MAX),
#SplitOn nvarchar(5) = ','
)
RETURNS #RtnValue table
(
Id INT NOT NULL IDENTITY(1,1) PRIMARY KEY CLUSTERED,
Data nvarchar(100) NOT NULL
)
AS
BEGIN
IF #Line IS NULL RETURN;
DECLARE #split_on_len INT = LEN(#SplitOn);
DECLARE #start_at INT = 1;
DECLARE #end_at INT;
DECLARE #data_len INT;
WHILE 1=1
BEGIN
SET #end_at = CHARINDEX(#SplitOn,#Line,#start_at);
SET #data_len = CASE #end_at WHEN 0 THEN LEN(#Line) ELSE #end_at-#start_at END;
INSERT INTO #RtnValue (data) VALUES( SUBSTRING(#Line,#start_at,#data_len) );
IF #end_at = 0 BREAK;
SET #start_at = #end_at + #split_on_len;
END;
RETURN;
END;
Nice to see that it have been solved in the 2016 version, but for all of those that is not on that, here are two generalized and simplified versions of the methods above.
The XML-method is shorter, but of course requires the string to allow for the xml-trick (no 'bad' chars.)
XML-Method:
create function dbo.splitString(#input Varchar(max), #Splitter VarChar(99)) returns table as
Return
SELECT Split.a.value('.', 'VARCHAR(max)') AS Data FROM
( SELECT CAST ('<M>' + REPLACE(#input, #Splitter, '</M><M>') + '</M>' AS XML) AS Data
) AS A CROSS APPLY Data.nodes ('/M') AS Split(a);
Recursive method:
create function dbo.splitString(#input Varchar(max), #Splitter Varchar(99)) returns table as
Return
with tmp (DataItem, ix) as
( select #input , CHARINDEX('',#Input) --Recu. start, ignored val to get the types right
union all
select Substring(#input, ix+1,ix2-ix-1), ix2
from (Select *, CHARINDEX(#Splitter,#Input+#Splitter,ix+1) ix2 from tmp) x where ix2<>0
) select DataItem from tmp where ix<>0
Function in action
Create table TEST_X (A int, CSV Varchar(100));
Insert into test_x select 1, 'A,B';
Insert into test_x select 2, 'C,D';
Select A,data from TEST_X x cross apply dbo.splitString(x.CSV,',') Y;
Drop table TEST_X
XML-METHOD 2: Unicode Friendly 😀 (Addition courtesy of Max Hodges)
create function dbo.splitString(#input nVarchar(max), #Splitter nVarchar(99)) returns table as
Return
SELECT Split.a.value('.', 'NVARCHAR(max)') AS Data FROM
( SELECT CAST ('<M>' + REPLACE(#input, #Splitter, '</M><M>') + '</M>' AS XML) AS Data
) AS A CROSS APPLY Data.nodes ('/M') AS Split(a);
Please refer below TSQL. STRING_SPLIT function is available only under compatibility level 130 and above.
TSQL:
DECLARE #stringValue NVARCHAR(400) = 'red,blue,green,yellow,black';
DECLARE #separator CHAR = ',';
SELECT [value] As Colour
FROM STRING_SPLIT(#stringValue, #separator);
RESULT:
Colour
red
blue
green
yellow
black
I know it has a lot of answers, but I want to write my version of split function like others and like string_split SQL Server 2016 native function.
create function [dbo].[Split]
(
#Value nvarchar(max),
#Delimiter nvarchar(50)
)
returns #tbl table
(
Seq int primary key identity(1, 1),
Value nvarchar(max)
)
as begin
declare #Xml xml = cast('<d>' + replace(#Value, #Delimiter, '</d><d>') + '</d>' as xml);
insert into #tbl
(Value)
select a.split.value('.', 'nvarchar(max)') as Value
from #Xml.nodes('/d') a(split);
return;
end;
Seq column is primary key to support fast join with other real table or Split function returned table.
Used XML function to support large data (looping version will slow down significantly when you have large data)
Here's a answer to question.
CREATE TABLE Testdata
(
SomeID INT,
OtherID INT,
String VARCHAR(MAX)
);
INSERT Testdata SELECT 1, 9, '18,20,22';
INSERT Testdata SELECT 2, 8, '17,19';
INSERT Testdata SELECT 3, 7, '13,19,20';
INSERT Testdata SELECT 4, 6, '';
INSERT Testdata SELECT 9, 11, '1,2,3,4';
select t.SomeID, t.OtherID, s.Value
from Testdata t
cross apply dbo.Split(t.String, ',') s;
--Output
SomeID OtherID Value
1 9 18
1 9 20
1 9 22
2 8 17
2 8 19
3 7 13
3 7 19
3 7 20
4 6
9 11 1
9 11 2
9 11 3
9 11 4
Joining Split with other split
declare #Names nvarchar(max) = 'a,b,c,d';
declare #Codes nvarchar(max) = '10,20,30,40';
select n.Seq, n.Value Name, c.Value Code
from dbo.Split(#Names, ',') n
inner join dbo.Split(#Codes, ',') c on n.Seq = c.Seq;
--Output
Seq Name Code
1 a 10
2 b 20
3 c 30
4 d 40
Split two times
declare #NationLocSex nvarchar(max) = 'Korea,Seoul,1;Vietnam,Kiengiang,0;China,Xian,0';
with rows as
(
select Value
from dbo.Split(#NationLocSex, ';')
)
select rw.Value r, cl.Value c
from rows rw
cross apply dbo.Split(rw.Value, ',') cl;
--Output
r c
Korea,Seoul,1 Korea
Korea,Seoul,1 Seoul
Korea,Seoul,1 1
Vietnam,Kiengiang,0 Vietnam
Vietnam,Kiengiang,0 Kiengiang
Vietnam,Kiengiang,0 0
China,Xian,0 China
China,Xian,0 Xian
China,Xian,0 0
Split to columns
declare #Numbers nvarchar(50) = 'First,Second,Third';
with t as
(
select case when Seq = 1 then Value end f1,
case when Seq = 2 then Value end f2,
case when Seq = 3 then Value end f3
from dbo.Split(#Numbers, ',')
)
select min(f1) f1, min(f2) f2, min(f3) f3
from t;
--Output
f1 f2 f3
First Second Third
Generate rows by range
declare #Ranges nvarchar(50) = '1-2,4-6';
declare #Numbers table (Num int);
insert into #Numbers values (1),(2),(3),(4),(5),(6),(7),(8);
with t as
(
select r.Seq, r.Value,
min(case when ft.Seq = 1 then ft.Value end) ValueFrom,
min(case when ft.Seq = 2 then ft.Value end) ValueTo
from dbo.Split(#Ranges, ',') r
cross apply dbo.Split(r.Value, '-') ft
group by r.Seq, r.Value
)
select t.Seq, t.Value, t.ValueFrom, t.ValueTo, n.Num
from t
inner join #Numbers n on n.Num between t.ValueFrom and t.ValueTo;
--Output
Seq Value ValueFrom ValueTo Num
1 1-2 1 2 1
1 1-2 1 2 2
2 4-6 4 6 4
2 4-6 4 6 5
2 4-6 4 6 6
DECLARE #id_list VARCHAR(MAX) = '1234,23,56,576,1231,567,122,87876,57553,1216';
DECLARE #table TABLE ( id VARCHAR(50) );
DECLARE #x INT = 0;
DECLARE #firstcomma INT = 0;
DECLARE #nextcomma INT = 0;
SET #x = LEN(#id_list) - LEN(REPLACE(#id_list, ',', '')) + 1; -- number of ids in id_list
WHILE #x > 0
BEGIN
SET #nextcomma = CASE WHEN CHARINDEX(',', #id_list, #firstcomma + 1) = 0
THEN LEN(#id_list) + 1
ELSE CHARINDEX(',', #id_list, #firstcomma + 1)
END;
INSERT INTO #table
VALUES ( SUBSTRING(#id_list, #firstcomma + 1, (#nextcomma - #firstcomma) - 1) );
SET #firstcomma = CHARINDEX(',', #id_list, #firstcomma + 1);
SET #x = #x - 1;
END;
SELECT *
FROM #table;
;WITH tmp(SomeID, OtherID, DataItem, Data) as (
SELECT SomeID, OtherID, LEFT(Data, CHARINDEX(',',Data+',')-1),
STUFF(Data, 1, CHARINDEX(',',Data+','), '')
FROM Testdata
WHERE Data > ''
)
SELECT SomeID, OtherID, Data
FROM tmp
ORDER BY SomeID
with only tiny little modification to above query...
By creating this function ([DelimitedSplit]) which splits a string, you could do an OUTER APPLY to your SELECT.
CREATE FUNCTION [dbo].[DelimitedSplit]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a INNER JOIN E1 b ON b.N = a.N), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a INNER JOIN E2 b ON b.N = a.N), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
TEST
CREATE TABLE #Testdata
(
SomeID INT,
OtherID INT,
String VARCHAR(MAX)
);
INSERT #Testdata SELECT 1, 9, '18,20,22';
INSERT #Testdata SELECT 2, 8, '17,19';
INSERT #Testdata SELECT 3, 7, '13,19,20';
INSERT #Testdata SELECT 4, 6, '';
INSERT #Testdata SELECT 9, 11, '1,2,3,4';
SELECT
*
FROM #Testdata
OUTER APPLY [dbo].[DelimitedSplit](String,',');
DROP TABLE #Testdata;
RESULT
SomeID OtherID String ItemNumber Item
1 9 18,20,22 1 18
1 9 18,20,22 2 20
1 9 18,20,22 3 22
2 8 17,19 1 17
2 8 17,19 2 19
3 7 13,19,20 1 13
3 7 13,19,20 2 19
3 7 13,19,20 3 20
4 6 1
9 11 1,2,3,4 1 1
9 11 1,2,3,4 2 2
9 11 1,2,3,4 3 3
9 11 1,2,3,4 4 4
Function
CREATE FUNCTION dbo.SplitToRows (#column varchar(100), #separator varchar(10))
RETURNS #rtnTable TABLE
(
ID int identity(1,1),
ColumnA varchar(max)
)
AS
BEGIN
DECLARE #position int = 0;
DECLARE #endAt int = 0;
DECLARE #tempString varchar(100);
set #column = ltrim(rtrim(#column));
WHILE #position<=len(#column)
BEGIN
set #endAt = CHARINDEX(#separator,#column,#position);
if(#endAt=0)
begin
Insert into #rtnTable(ColumnA) Select substring(#column,#position,len(#column)-#position);
break;
end;
set #tempString = substring(ltrim(rtrim(#column)),#position,#endAt-#position);
Insert into #rtnTable(ColumnA) select #tempString;
set #position=#endAt+1;
END;
return;
END;
Use case
select * from dbo.SplitToRows('T14; p226.0001; eee; 3554;', ';');
Or just a select with multiple result set
DECLARE #column varchar(max)= '1234; 4748;abcde; 324432';
DECLARE #separator varchar(10) = ';';
DECLARE #position int = 0;
DECLARE #endAt int = 0;
DECLARE #tempString varchar(100);
set #column = ltrim(rtrim(#column));
WHILE #position<=len(#column)
BEGIN
set #endAt = CHARINDEX(#separator,#column,#position);
if(#endAt=0)
begin
Select substring(#column,#position,len(#column)-#position);
break;
end;
set #tempString = substring(ltrim(rtrim(#column)),#position,#endAt-#position);
select #tempString;
set #position=#endAt+1;
END;
When using this approach you have to make sure that none of your values contains something that would be illegal XML – user1151923
I always use the XML method. Make sure you use VALID XML. I have two functions to convert between valid XML and Text. (I tend to strip out the carriage returns as I don't usually need them.
CREATE FUNCTION dbo.udf_ConvertTextToXML (#Text varchar(MAX))
RETURNS varchar(MAX)
AS
BEGIN
SET #Text = REPLACE(#Text,CHAR(10),'');
SET #Text = REPLACE(#Text,CHAR(13),'');
SET #Text = REPLACE(#Text,'<','<');
SET #Text = REPLACE(#Text,'&','&');
SET #Text = REPLACE(#Text,'>','>');
SET #Text = REPLACE(#Text,'''',''');
SET #Text = REPLACE(#Text,'"','"');
RETURN #Text;
END;
CREATE FUNCTION dbo.udf_ConvertTextFromXML (#Text VARCHAR(MAX))
RETURNS VARCHAR(max)
AS
BEGIN
SET #Text = REPLACE(#Text,'<','<');
SET #Text = REPLACE(#Text,'&','&');
SET #Text = REPLACE(#Text,'>','>');
SET #Text = REPLACE(#Text,''','''');
SET #Text = REPLACE(#Text,'"','"');
RETURN #Text;
END;
Below works on sql server 2008
select *, ROW_NUMBER() OVER(order by items) as row#
from
( select 134 myColumn1, 34 myColumn2, 'd,c,k,e,f,g,h,a' comaSeperatedColumn) myTable
cross apply
SPLIT (rtrim(comaSeperatedColumn), ',') splitedTable -- gives 'items' column
Will get all Cartesian product with the origin table columns plus "items" of split table.
You can use the following function to extract data
CREATE FUNCTION [dbo].[SplitString]
(
#RowData NVARCHAR(MAX),
#Delimeter NVARCHAR(MAX)
)
RETURNS #RtnValue TABLE
(
ID INT IDENTITY(1,1),
Data NVARCHAR(MAX)
)
AS
BEGIN
DECLARE #Iterator INT;
SET #Iterator = 1;
DECLARE #FoundIndex INT;
SET #FoundIndex = CHARINDEX(#Delimeter,#RowData);
WHILE (#FoundIndex>0)
BEGIN
INSERT INTO #RtnValue (data)
SELECT
Data = LTRIM(RTRIM(SUBSTRING(#RowData, 1, #FoundIndex - 1)));
SET #RowData = SUBSTRING(#RowData,
#FoundIndex + DATALENGTH(#Delimeter) / 2,
LEN(#RowData));
SET #Iterator = #Iterator + 1;
SET #FoundIndex = CHARINDEX(#Delimeter, #RowData);
END;
INSERT INTO #RtnValue (Data)
SELECT Data = LTRIM(RTRIM(#RowData));
RETURN;
END;
I am trying to write a stored procedure which returns a result combining 2 table variables which looks something like this.
Name | LastName | course | course | course | course <- Columns
Name | LastName | DVA123 | DVA222 | nothing | nothing <- Row1
Pete Steven 200 <- Row2
Steve Lastname 50 <- Row3
From these 3 tables
Table Staff:
Name | LastName | SSN |
Steve Lastname 234
Pete Steven 132
Table Course Instance:
Course | Year | Period |
DVA123 2013 1
DVA222 2014 2
Table Attended by:
Course | SSN | Year | Period | Hours |
DVA123 234 2013 1 200
DVA222 132 2014 2 50
I am taking #year as a parameter that will decide what year in the course will be displayed in the result.
ALTER proc [dbo].[test4]
#year int
as
begin
-- I then declare the 2 tables which I will then store the values from the tables
DECLARE #Table1 TABLE(
Firstname varchar(30) NOT NULL,
Lastname varchar(30) NOT NULL
);
DECLARE #Table2 TABLE(
Course varchar(30) NULL
);
Declare #variable varchar(max) -- variable for saving the cursor value and then set the course1 to 4
I want at highest 4 results/course instances which I later order by the period of the year
declare myCursor1 CURSOR
for SELECT top 4 period from Course instance
where year = #year
open myCursor1
fetch next from myCursor1 into #variable
--print #variable
while ##fetch_status = 0
Begin
UPDATE #Table2
SET InstanceCourse1 = #variable
where current of myCursor1
fetch next from myCursor1 into #variable
print #variable
End
Close myCursor1
deallocate myCursor1
insert into #table1
select 'Firstname', 'Lastname'
insert into #table1
select Firstname, Lastname from staff order by Lastname
END
select * from #Table1 -- for testing purposes
select * from #Table2 -- for testing purposes
--Then i want to combine these tables into the output at the top
This is how far I've gotten, I don't know how to get the courses into the columns and then get the amount of hours for each staff member.
If anyone can help guide me in the right direction I would be very grateful. My idea about the cursor was to get the top (0-4) values from the top4 course periods during that year and then add them to the #table2.
Ok. This is not pretty. It is a really ugly dynamic sql, but in my testing it seems to be working. I have created an extra subquery to get the courses values as the first row and then Union with the rest of the result. The top four courses are gathered by using ROW_Number() and order by Year and period. I had to make different versions of the courses string I am creating in order to use them for both column names, and in my pivot. Give it a try. Hopefully it will work on your data as well.
DECLARE #Year INT
SET #Year = 2014
DECLARE #Query NVARCHAR(2000)
DECLARE #CoursesColumns NVARCHAR(2000)
SET #CoursesColumns = (SELECT '''' + Course + ''' as c' + CAST(ROW_NUMBER() OVER(ORDER BY Year, Period) AS nvarchar(50)) + ',' AS 'data()'
FROM AttendedBy where [Year] = #Year
for xml path(''))
SET #CoursesColumns = LEFT(#CoursesColumns, LEN(#CoursesColumns) -1)
SET #CoursesColumns =
CASE
WHEN CHARINDEX('c1', #CoursesColumns) = 0 THEN #CoursesColumns + 'NULL as c1, NULL as c2, NULL as c3, NULL as c4'
WHEN CHARINDEX('c2', #CoursesColumns) = 0 THEN #CoursesColumns + ',NULL as c2, NULL as c3, NULL as c4'
WHEN CHARINDEX('c3', #CoursesColumns) = 0 THEN #CoursesColumns + ', NULL as c3, NULL as c4'
WHEN CHARINDEX('c4', #CoursesColumns) = 0 THEN #CoursesColumns + ', NULL as c4'
ELSE #CoursesColumns
END
DECLARE #Courses NVARCHAR(2000)
SET #Courses = (SELECT Course + ' as c' + CAST(ROW_NUMBER() OVER(ORDER BY Year, Period) AS nvarchar(50)) + ',' AS 'data()'
FROM AttendedBy where [Year] = #Year
for xml path(''))
SET #Courses = LEFT(#Courses, LEN(#Courses) -1)
SET #Courses =
CASE
WHEN CHARINDEX('c1', #Courses) = 0 THEN #Courses + 'NULL as c1, NULL as c2, NULL as c3, NULL as c4'
WHEN CHARINDEX('c2', #Courses) = 0 THEN #Courses + ',NULL as c2, NULL as c3, NULL as c4'
WHEN CHARINDEX('c3', #Courses) = 0 THEN #Courses + ', NULL as c3, NULL as c4'
WHEN CHARINDEX('c4', #Courses) = 0 THEN #Courses + ', NULL as c4'
ELSE #Courses
END
DECLARE #CoursePivot NVARCHAR(2000)
SET #CoursePivot = (SELECT Course + ',' AS 'data()'
FROM AttendedBy where [Year] = #Year
for xml path(''))
SET #CoursePivot = LEFT(#CoursePivot, LEN(#CoursePivot) -1)
SET #Query = 'SELECT Name, LastName, c1, c2, c3, c4
FROM (
SELECT ''Name'' as name, ''LastName'' as lastname, ' + #CoursesColumns +
' UNION
SELECT Name, LastName,' + #Courses +
' FROM(
SELECT
s.Name
,s.LastName
,ci.Course
,ci.Year
,ci.Period
,CAST(ab.Hours AS NVARCHAR(100)) AS Hours
FROM Staff s
LEFT JOIN AttendedBy ab
ON
s.SSN = ab.SSN
LEFT JOIN CourseInstance ci
ON
ab.Course = ci.Course
WHERE ci.Year=' + CAST(#Year AS nvarchar(4)) +
' ) q
PIVOT(
MAX(Hours)
FOR
Course
IN (' + #CoursePivot + ')
)q2
)q3'
SELECT #Query
execute(#Query)
Edit: Added some where clauses so only courses from given year is shown. Added Screenshot of my results.
try this
DECLARE #CourseNameString varchar(max),
#query AS NVARCHAR(MAX);
SET #CourseNameString=''
select #CourseNameString = STUFF((SELECT distinct ',' + QUOTENAME(Course)
FROM Attended where [Year]= 2013
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = '
select Name,LastName,'+#CourseNameString+' from Staff as e inner join (
SELECT * FROM
(SELECT [Hours],a.SSN,a.Course as c FROM Attended as a inner JOIN Staff as s
ON s.SSN = s.SSN) p
PIVOT(max([Hours])FOR c IN ('+#CourseNameString+')) pvt)p
ON e.SSN = p.SSN'
execute(#query)
Use subquery like this one :
SELECT Firstname, Lastname, (select instanceCourse1 from table2) as InstanceCourse1 from Table1