I have a table that looks like
ID Layout
1 hello,world,welcome,to,tsql
2 welcome,to,stackoverflow
The desired output should be
Id Splitdata
1 hello
1 world
1 welcome
1 to
1 tsql
2 welcome
2 to
2 stackoverflow
I have done this by the below query
Declare #t TABLE(
ID INT IDENTITY PRIMARY KEY,
Layout VARCHAR(MAX)
)
INSERT INTO #t(Layout)
SELECT 'hello,world,welcome,to,tsql' union all
SELECT 'welcome,to,stackoverflow'
--SELECT * FROM #t
;With cte AS(
select F1.id
,O.splitdata
from
(
select *,
cast('<X>'+replace(F.Layout,',','</X><X>')+'</X>' as XML) as xmlfilter
from #t F
)F1
cross apply
(
select fdata.D.value('.','varchar(MAX)') as splitdata
from f1.xmlfilter.nodes('X') as fdata(D)) O
)
select * from cte
But performance wise it is very bad. I am looking for a more efficient query but using CTE only.
You seem dead set on using a CTE, so try this:
DECLARE #YourTable table (RowID int, Layout varchar(200))
INSERT #YourTable VALUES (1,'hello,world,welcome,to,tsql')
INSERT #YourTable VALUES (2,'welcome,to,stackoverflow')
;WITH SplitSting AS
(
SELECT
RowID,LEFT(Layout,CHARINDEX(',',Layout)-1) AS Part
,RIGHT(Layout,LEN(Layout)-CHARINDEX(',',Layout)) AS Remainder
FROM #YourTable
WHERE Layout IS NOT NULL AND CHARINDEX(',',Layout)>0
UNION ALL
SELECT
RowID,LEFT(Remainder,CHARINDEX(',',Remainder)-1)
,RIGHT(Remainder,LEN(Remainder)-CHARINDEX(',',Remainder))
FROM SplitSting
WHERE Remainder IS NOT NULL AND CHARINDEX(',',Remainder)>0
UNION ALL
SELECT
RowID,Remainder,null
FROM SplitSting
WHERE Remainder IS NOT NULL AND CHARINDEX(',',Remainder)=0
)
SELECT * FROM SplitSting ORDER BY RowID
OUTPUT:
RowID Part
----------- -----------------------
1 hello
1 world
1 welcome
1 to
1 tsql
2 welcome
2 to
2 stackoverflow
(8 row(s) affected)
here is an excellent article on splitting strings in SQL Server: "Arrays and Lists in SQL Server 2005 and Beyond, When Table Value Parameters Do Not Cut it" by Erland Sommarskog
EDIT here's another version (but you need a numbers table) returns same results as above:
;WITH SplitValues AS
(
SELECT
RowID,ListValue
FROM (SELECT
RowID, LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(',', List2, number+1)-number - 1))) AS ListValue
FROM (
SELECT RowID, ',' + Layout + ',' AS List2
FROM #YourTable
) AS dt
INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
WHERE SUBSTRING(List2, number, 1) = ','
) dt2
WHERE ListValue IS NOT NULL AND ListValue!=''
)
SELECT * FROM SplitValues
see here for a numbers table: What is the best way to create and populate a numbers table?
From NullRef's Answer
Function without set operation will be faster according to my understanding of sql server
so this will be more efficient
CREATE FUNCTION fnSplitString(#str nvarchar(max),#sep nvarchar(max))
RETURNS TABLE
AS
RETURN
WITH a AS(
SELECT CAST(0 AS BIGINT) as idx1,CHARINDEX(#sep,#str) idx2
UNION ALL
SELECT idx2+1,CHARINDEX(#sep,#str,idx2+1)
FROM a
WHERE idx2>0
)
SELECT SUBSTRING(#str,idx1,COALESCE(NULLIF(idx2,0),LEN(#str)+1)-idx1) as value
FROM a
it's my best solution using CTE:
DECLARE #Char VARCHAR(MAX) = '10||3112||||aaaa||'
DECLARE #Separador CHAR(2) = '||'
;WITH Entrada AS(
SELECT
CAST(1 AS Int) As Inicio,
CHARINDEX(#Separador, #Char) As Fim
UNION ALL
SELECT
CAST(Fim + LEN(#Separador) AS Int) As Inicio,
CHARINDEX(#Separador, #Char, Fim + 1) As Fim
FROM Entrada
WHERE CHARINDEX(#Separador, #Char, Fim + 1) > 0
)
SELECT
SUBSTRING(#Char, Inicio, Fim - Inicio)
FROM Entrada
WHERE (Fim - Inicio) > 0
Related
In SQL I want to add 0 in front and , at the end of each character.
Example: A30F1 -> 0A,03,00,0F,01
I don't want to use cursor if possible.
Thanks!
EIDT:
I apologize for not asking the most appropriate question at the beginning.
In short, I have a table and for each value in the column name I have to convert it to the desired format. For example, we have a #Temp table:
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721')
SELECT * FROM #Temp
One method would be to use a Tally to split the string into it's individual characters, and then use concatenation to add the 0 to the start, and STRING_AGG to comma delimit the results:
DECLARE #YourValue varchar(5) = 'A30F1';
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (LEN(#YourValue))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2) --Up to 100 characters, add more cross joins for more characters
SELECT STRING_AGG(CONCAT('0',SS.C),',') WITHIN GROUP (ORDER BY T.I) AS NewString
FROM (VALUES(#YourValue))V(YourValue)
CROSS JOIN Tally T
CROSS APPLY (VALUES(SUBSTRING(V.YourValue,T.I,1)))SS(C);
It appears this is meant to be against a table, not a single value. This needs, however, very few changes to work against a table:
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT TOP (SELECT MAX(LEN(YourColumn)) FROM dbo.YourTable)
ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS I
FROM N N1, N N2) --Up to 100 characters, add more cross joins for more characters
SELECT STRING_AGG(CONCAT('0',SS.C),',') WITHIN GROUP (ORDER BY T.I) AS NewString
FROM dbo.YourTable YT
JOIN Tally T ON LEN(YT.YourColumn) >= T.I
CROSS APPLY (VALUES(SUBSTRING(YT.YourColumn,T.I,1)))SS(C)
GROUP BY YT.YourColumn;
db<>fiddle
I solved the simplest possible with a few variables, WHILE and SUBSTRING
DECLARE #var VARCHAR(20) = 'A30F1', #i INT = 1, #res NVARCHAR(20)
WHILE (#i <= LEN(#var))
BEGIN
SET #res = #res + '0' + SUBSTRING(#var, #i, 1) + ','
SET #i = #i + 1
END
SELECT LEFT(#res, LEN(#res) - 1) output
Check demo on DB<>FIDDLE.
Original answer:
A recursive CTE and a STRING_AGG() call is also an option (SQL Server 2017+ is needed):
DECLARE #text varchar(max) = 'A30F1';
WITH rCTE AS
(
SELECT 1 AS CharacterPosition, SUBSTRING(#text, 1, 1) AS Character
UNION ALL
SELECT CharacterPosition + 1, SUBSTRING(#text, CharacterPosition + 1, 1)
FROM rCTE
WHERE CharacterPosition < LEN(#text)
)
SELECT STRING_AGG('0' + Character, ',') WITHIN GROUP (ORDER BY CharacterPosition)
FROM rCTE
OPTION (MAXRECURSION 0);
Update:
You need a different statement, if the names are stored in a table, again using recursion and STRING_AGG():
Table:
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721')
Statement:
; WITH rCTE AS (
SELECT
t.id AS id,
LEFT(t.name, 1) AS Character,
STUFF(t.name, 1, 1, '') AS CharactersRemaining,
1 AS CharacterPosition
FROM #Temp t
UNION ALL
SELECT
r.id,
LEFT(r.CharactersRemaining, 1),
STUFF(r.CharactersRemaining, 1, 1, ''),
CharacterPosition + 1
FROM rCTE r
WHERE LEN(r.CharactersRemaining) > 0
)
SELECT
id,
STRING_AGG('0' + Character, ',') WITHIN GROUP (ORDER BY CharacterPosition) AS name
FROM rCTE
GROUP BY id
OPTION (MAXRECURSION 0);
Result:
id name
1 0A,03,00,0F,01
2 0B,05,01,0R,09
3 0L,01,07,02,01
If you are only applying this to English alphabet characters and digits as in your example you could do this.
CREATE TABLE #Temp (id INT, name VARCHAR(25))
INSERT INTO #Temp VALUES (1, 'A30F1'), (2, 'B51R9'), (3, 'L1721'), (4, 'A')
SELECT SUBSTRING(REPLACE(
0x00 + CAST(CAST(name AS NVARCHAR(25)) AS BINARY(50)), CHAR(0), '0,')
, 3
, LEN(name) * 3 - 1)
FROM #Temp
returns
0A,03,00,0F,01
0B,05,01,0R,09
0L,01,07,02,01
0A
This takes advantage of the fact that the binary representation of the nvarchar and varchar is the same for this limited character set except for padding out with 0x00
'A30F1' -> 0x4133304631
N'A30F1' -> 0x41003300300046003100
I have a recursive CTE that replaces multiple values from an expression, but it is too slow when there are many expressions.
CREATE TABLE #table1(IdExpresion INT, expresion VARCHAR(MAX))
CREATE TABLE #table2(IdExpresion INT, searchExpresion VARCHAR(50), replacementExpresion VARCHAR(50))
INSERT INTO #table1(IdExpresion, expresion)
VALUES(1, 'Mary had a little lamb'),
(2, 'The new student, student_name has the following grades Math - math_grade, Science - Science_grade')
INSERT INTO #table2(IdExpresion, searchExpresion, replacementExpresion)
VALUES(1, 'lamb','dog'),
(2, 'student_name','Joe Smith'),
(2, 'math_grade','A'),
(2, 'Science_grade','B+')
;WITH cte(IdExpresion, expresion, lvl) AS
(
SELECT t1.IdExpresion, t1.expresion, 1
FROM #table1 t1
UNION ALL
SELECT cte.IdExpresion, REPLACE(cte.expresion, t2.searchExpresion, t2.replacementExpresion), cte.lvl + 1
FROM cte
INNER JOIN #table2 t2
ON cte.IdExpresion = t2.IdExpresion
AND CHARINDEX(t2.searchExpresion, cte.expresion) > 0
)
SELECT DISTINCT c2.expresion
FROM (SELECT IdExpresion, MAX(lvl) AS lvl
FROM cte
GROUP BY IdExpresion) c1
INNER JOIN cte c2
ON c1.IdExpresion = c2.IdExpresion
AND c1.lvl = c2.lvl
OPTION (MAXRECURSION 0);
Anyone have any advice? I am using SQL Server by the way
Not sure if any more performant, but here is a brute force approach just for fun.
Already +1 LukStorm's answer, I suspect that is the way to go.
Example
Declare #S varchar(max) = (Select IdExpresion,expresion = replace(' '+expresion,' ',concat(' ',IdExpresion,'|||')) From #Table1 For XML Raw )
Select #S = replace(#S,concat(IdExpresion,'|||',searchExpresion),replacementExpresion) From #table2
Select IdExpresion = B.i.value('#IdExpresion', 'int')
,expresion = ltrim(replace(B.i.value('#expresion', 'varchar(max)'),B.i.value('#IdExpresion', 'varchar(25)')+'|||',''))
From (Select x = Cast(#S as xml).query('.')) as A
Cross Apply x.nodes('row') AS B(i)
Returns
IdExpresion expresion
1 Mary had a little dog
2 The new student, Joe Smith has the following grades Math - A, Science - B+
You could add another CTE to it that gets a row_number for each replacement, partitioned by the IdExpresion.
Then in the recursive CTE, instead of counting up, count down till there's no match with the replacement row_number.
The last entry in the CTE, that had all replacements, will have Lvl 0 then.
;WITH SEARCH AS (
SELECT
IdExpresion,
row_number() over (partition by IdExpresion order by searchExpresion) as rn,
searchExpresion, replacementExpresion
FROM #table2
), CTE(IdExpresion, expresion, lvl) AS
(
SELECT t1.IdExpresion, t1.expresion, count(*)
FROM #table1 t1
JOIN #table2 t2 ON t2.IdExpresion = t1.IdExpresion
GROUP BY t1.IdExpresion, t1.expresion
UNION ALL
SELECT c.IdExpresion, REPLACE(c.expresion, s.searchExpresion, s.replacementExpresion), c.lvl - 1
FROM CTE c
JOIN SEARCH s
ON s.IdExpresion = c.IdExpresion AND s.rn = c.lvl
)
SELECT IdExpresion, expresion
FROM CTE
WHERE lvl = 0
OPTION (MAXRECURSION 0);
This way, each REPLACE is only done once per IdExpresion.
And that without having to use CHARINDEX.
You could also replace that SEARCH cte with a temporary table.
One that has the records from #table2 with that row_number.
This has the benefit that with a table you can add a compound index.
On a large table it should speed up the recursive join to the replacements.
Test on rextester here
CREATE TABLE #tmpSearch (
IdExpresion INT,
rn INT,
searchExpresion VARCHAR(50),
replacementExpresion VARCHAR(50),
primary key (IdExpresion, rn));
insert into #tmpSearch (IdExpresion, rn, searchExpresion, replacementExpresion)
select
IdExpresion,
row_number() over (partition by IdExpresion order by searchExpresion) as rn,
searchExpresion,
replacementExpresion
from #table2
order by IdExpresion, searchExpresion;
;WITH CTE(IdExpresion, expresion, lvl) AS
(
SELECT t1.IdExpresion, t1.expresion, max(s.rn)
FROM #table1 t1
JOIN #tmpSearch s ON s.IdExpresion = t1.IdExpresion
GROUP BY t1.IdExpresion, t1.expresion
UNION ALL
SELECT c.IdExpresion, REPLACE(c.expresion, s.searchExpresion, s.replacementExpresion), c.lvl - 1
FROM CTE c
JOIN #tmpSearch s
ON s.IdExpresion = c.IdExpresion AND s.rn = c.lvl
)
SELECT IdExpresion, expresion
FROM CTE
WHERE lvl = 0
OPTION (MAXRECURSION 0);
Good day,
Here is another solution. Please check if this fit your needs. This solution does not use any loop but simple dynamic query.
DECLARE #SQLString nvarchar(MAX);
-- do not make mistake, this is simple CTE and not a recursive CTE (no Loop)
;With MyCTE as (
select R
From table1 t1
CROSS APPLY (
SELECT R = 'SELECT ' + CONVERT (NVARCHAR(MAX),t1.IdExpresion) + ' as IdExpresion,' + STRING_AGG ('REPLACE','(') + '(' + 't1.expresion,''' + STRING_AGG(t2.searchExpresion + ''',''' + t2.replacementExpresion , '''),''') + ''') as expresion FROM table1 t1 where t1.IdExpresion = ' + CONVERT (NVARCHAR(MAX),t1.IdExpresion)
from table2 t2
where t2.IdExpresion = t1.IdExpresion
) C
)
SELECT #SQLString = STRING_AGG(R,'
UNION ALL
')
FROM MyCTE
--PRINT #SQLString
EXECUTE sp_executesql #SQLString
GO
Note! I recommend to execute some tests to confirm that this solves all cases
Note! I am using the function STRING_AGG which was added to SQL Server 2017. In older version you can get the exact same solution using FOR XML statement.
Since we don't have the real DDL+DML we cannot really discuss about performance, but the difference in the execution plans of the solutions is 10% to 90% (In general, You should check IO and Time statistics in production in addition, before choosing your solution)
So... here is the Execution Plans Image (above query is my dynamic SQL solution and bellow is LukStorms solution using recursive CTE = Loop)
This question already has answers here:
How to split a comma-separated value to columns
(38 answers)
Closed 8 years ago.
I have the following data in a table. The number of values in each row can vary and the number of rows could also vary.
The table has 1 column with csv formatted values. The values will always be numeric
Data
1,2
4
5,12, 10
6,7,8,9,10
15,17
I would like to end up with a temp table with the following
Data Lowest Highest
1,2 1 2
4 4 4
5,12, 10 5 12
6,7,8,9,10 6 10
15,17 15 17
Can anyone help with writing a sql query or function to achieve this
Instead of function, you can achieve by this
;WITH tmp
AS (SELECT A.rn,split.a.value('.', 'VARCHAR(100)') AS String
FROM (SELECT Row_number() OVER(ORDER BY (SELECT NULL)) AS RN,
Cast ('<M>' + Replace([data], ',', '</M><M>') + '</M>' AS XML) AS String
FROM table1) AS A
CROSS apply string.nodes ('/M') AS Split(a))
SELECT X.data,Tmp.lower,Tmp.higher
FROM (SELECT rn,Min(Cast(string AS INT)) AS Lower,Max(Cast(string AS INT)) AS Higher
FROM tmp
GROUP BY rn) Tmp
JOIN (SELECT Row_number() OVER(ORDER BY (SELECT NULL)) AS RN1,data
FROM table1) X
ON X.rn1 = Tmp.rn
FIDDLE DEMO
Output would be:
Data Lower Higher
1,2 1 2
4 4 4
5,12, 10 5 12
6,7,8,9,10 6 10
15,17 15 17
First create a user defined function to convert each row of 'DATA' column to a intermediate table as:
/****** Object: UserDefinedFunction [dbo].[CSVToTable]******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[CSVToTable] (#InStr VARCHAR(MAX))
RETURNS #TempTab TABLE
(id int not null)
AS
BEGIN
;-- Ensure input ends with comma
SET #InStr = REPLACE(#InStr + ',', ',,', ',')
DECLARE #SP INT
DECLARE #VALUE VARCHAR(1000)
WHILE PATINDEX('%,%', #INSTR ) <> 0
BEGIN
SELECT #SP = PATINDEX('%,%',#INSTR)
SELECT #VALUE = LEFT(#INSTR , #SP - 1)
SELECT #INSTR = STUFF(#INSTR, 1, #SP, '')
INSERT INTO #TempTab(id) VALUES (#VALUE)
END
RETURN
END
GO
Function is explained further here.
Then Using Cross Apply we can get the desired output as:
With CTE as
(
select
T.Data, Min(udf.Id) as [Lowest],Max(udf.Id) as [Highest]
from
Test T
CROSS APPLY dbo.CSVToTable(T.Data) udf
Group By Data
)
Select * from CTE
Sample Code here...
What a Cross Apply does is : it applies the right table expression to each row from the left table and produces a result table with the unified result sets.
Create table #temp1 (name varchar(100),value int )
Declare #len int
Select #len=(select max(LEN(name)-LEN(replace(name,',',''))) from table)
Declare #i int = 1
while (#i<=#len+1)
begin
insert into #temp1
select name,PARSENAME(REPLACE(name,',','.'),#i) from table t
set #i = #i+1
end
Select name,MIN(value) MINV,MAX(value) MAXV from #temp1 group by name
declare #Testdata table ( Data varchar(max))
insert #Testdata select '1,2'
insert #Testdata select '4'
insert #Testdata select '5,12, 10'
insert #Testdata select '6,7,8,9,10'
;with tmp( DataItem, Data, RN1) as (
select LEFT(Data, CHARINDEX(',',Data+',')-1),
STUFF(Data, 1, CHARINDEX(',',Data+','), ''),
ROW_NUMBER()OVER(ORDER BY (SELECT NULL))AS RN1
from #Testdata
union all
select LEFT(Data, CHARINDEX(',',Data+',')-1),
STUFF(Data, 1, CHARINDEX(',',Data+','), ''),RN1
from tmp
where Data > ''
)
Select x.data,t.Low,t.Up FROM
(Select RN1,MIN(Cast(DataItem AS INT)) As Low,
MAX(Cast(DataItem AS INT)) As Up
FROM tmp t GROUP BY t.RN1)t
JOIN (Select ROW_NUMBER()OVER(ORDER BY (SELECT NULL))AS RN,data from #Testdata)X
ON X.RN = t.RN1
I have a bit of a tricky situation. I have a column that contains a pipe delimited set of numbers in numerous rows in a table. For example:
Courses
-------------------
1|2
1|2|3
1|2|8
10
11
11|12
What I want to achieve is to return rows where the number only appears once in my output.
Ideally, I want to try and carry this out using SQL rather than having to carry out checks at a web application level. Carrying out a DISTINCT does not achieve what I want.
The desired output would be:
Courses
-------------------
1
2
3
8
10
11
12
I would appreciated if anyone can guide me in the right direction.
Thanks.
Please try:
declare #tbl as table(Courses nvarchar(max))
insert into #tbl values
('1|2'),
('1|2|3'),
('1|2|8'),
('10'),
('11'),
('11|12')
select * from #tbl
SELECT
DISTINCT CAST(Split.a.value('.', 'VARCHAR(100)') AS INT) AS CVS
FROM
(
SELECT CAST ('<M>' + REPLACE(Courses, '|', '</M><M>') + '</M>' AS XML) AS CVS
FROM #tbl
) AS A CROSS APPLY CVS.nodes ('/M') AS Split(a)
ORDER BY 1
Try this one -
SET NOCOUNT ON;
DECLARE #temp TABLE
(
string VARCHAR(500)
)
DECLARE #Separator CHAR(1)
SELECT #Separator = '|'
INSERT INTO #temp (string)
VALUES
('1|2'),
('1|2|3'),
('1|2|8'),
('10'),
('11'),
('11|12')
-- 1. XML
SELECT p.value('(./s)[1]', 'VARCHAR(500)')
FROM (
SELECT field = CAST('<r><s>' + REPLACE(t.string, #Separator, '</s></r><r><s>') + '</s></r>' AS XML)
FROM #temp t
) d
CROSS APPLY field.nodes('/r') t(p)
-- 2. CTE
;WITH a AS
(
SELECT
start_pos = 1
, end_pos = CHARINDEX(#Separator, t.string)
, t.string
FROM #temp t
UNION ALL
SELECT
end_pos + 1
, CHARINDEX(#Separator, string, end_pos + 1)
, string
FROM a
WHERE end_pos > 0
)
SELECT d.name
FROM (
SELECT
name = SUBSTRING(
string
, start_pos
, ABS(end_pos - start_pos)
)
FROM a
) d
WHERE d.name != ''
Try this :
create table course (courses varchar(100))
insert into course values('1|2')
insert into course values('1|2|3')
insert into course values('1|2|8')
insert into course values('10')
insert into course values('11')
insert into course values('11|12')
Declare #col varchar(200)
SELECT
#col=(
SELECT DISTINCT c.courses + '|'
FROM course c
FOR XML PATH('')
);
select * from course
;with demo as(
select cast(substring(#col,1,charindex('|',#col,1)-1) AS INT) cou,charindex('|',#col,1) pos
union all
select cast(substring(#col,pos+1,charindex('|',#col,pos+1)-pos-1)AS INT) cou,charindex('|',#col,pos+1) pos
from demo where pos<LEN(#col))
select distinct cou from demo
Could not manage without recursion :( Something like this could do the trich?
WITH splitNum(num, r)
AS
(
SELECT
SUBSTRING(<field>,1, CHARINDEX('|', <field>)-1) num,
SUBSTRING(<field>,CHARINDEX('|', <field>)+1, len(<field>)) r
FROM <yourtable> as a
UNION ALL
SELECT
SUBSTRING(r,1, CHARINDEX('|', r)-1) num,
SUBSTRING(r,CHARINDEX('|', r)+1, len(r)) r
FROM <yourtable> b
WHERE CHARINDEX('|', r) > 0
inner join splitNum as c on <whatevertheprimarykeyis>
)
SELECT distinct num FROM splitNum
Didn't make it run, but it should do the trick, just replace the and with the correct info
One way would be to use a recursive CTE:
with cte as
(select cast(case charindex('|',courses) when 0 then courses
else left(courses,charindex('|',courses)-1) end as int) course,
case charindex('|',courses) when 0 then ''
else right(courses,len(courses)-charindex('|',courses)) end courses
from courses
union all
select cast(case charindex('|',courses) when 0 then courses
else left(courses,charindex('|',courses)-1) end as int) course,
case charindex('|',courses) when 0 then ''
else right(courses,len(courses)-charindex('|',courses)) end courses
from cte
where len(courses)>0)
select distinct course from cte
SQLFiddle here.
Is it possible to write a LIKE condition in T-SQL to match a comma-separated list which includes wildcards to a string. Let me explain further with an example:
Say you have the following command separated list of urls in a field:
'/, /news/%, /about/'
Now here's some examples of strings I'd like to match with the string above:
'/'
'/news/'
'/news/2/'
'/about/'
And here's some strings which would not match:
'/contact/'
'/about/me/'
I've achieved this in the past by writing a split function and then doing a like on each one. However I'm trying to get my query to work in SQL Server CE which doesn't support functions.
In case you are wondering here's how I achieved it using the split function:
SELECT Widgets.Id
FROM Widgets
WHERE (SELECT COUNT(*) FROM [dbo].[Split](Urls, ',') WHERE #Input LIKE Data) > 0
And here's the split function:
CREATE FUNCTION [dbo].[Split]
(
#RowData NVARCHAR(MAX),
#Separator 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(#Separator, #RowData)
WHILE (#FoundIndex > 0)
BEGIN
INSERT INTO #RtnValue ([Data])
SELECT Data = LTRIM(RTRIM(SUBSTRING(#RowData, 1, #FoundIndex - 1)))
SET #RowData = SUBSTRING(#RowData, #FoundIndex + DATALENGTH(#Separator) / 2, LEN(#RowData))
SET #Iterator = #Iterator + 1
SET #FoundIndex = CHARINDEX(#Separator, #RowData)
END
INSERT INTO #RtnValue ([Data])
SELECT Data = LTRIM(RTRIM(#RowData))
RETURN
END
I'd appreciate it if someone could help. Thanks
I can think of several options:
Use a session-keyed table: delete rows matching current spid, insert desired rows with current spid, read from table in SP, delete from table (again).
Make your client submit a query with many OR ... LIKE ... clauses.
Write an SP that does the same thing as your function and returns a recordset. INSERT YourTable EXEC SP #Strings and you are done!
Use the numbers-table-charindex-into-string inside of a derived table method of splitting the string.
Example
Let me flesh this out a little for you with an example combining ideas #3 and #4. Of course, your code for your function could be adapted, too.
Build a separate Numbers table. Here is example creation script:
--Numbers Table with 8192 elements (keeping it small for CE)
CREATE TABLE Numbers (
N smallint NOT NULL CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED
);
INSERT Numbers VALUES (1);
WHILE ##RowCount < 4096
INSERT Numbers SELECT N + (SELECT Max(N) FROM Numbers) FROM Numbers;
The SP:
CREATE PROCEDURE dbo.StringSplitRowset
#String varchar(8000)
AS
SELECT Substring(#String, l.StartPos, l.Chars) Item
FROM (
SELECT
S.StartPos,
IsNull(NullIf(CharIndex(',', #String, S.StartPos), 0) - S.StartPos, 8000)
FROM (
SELECT 1 UNION ALL
SELECT N.N + 1 FROM Numbers N WHERE Substring(#String, N.N, 1) = ','
) S (StartPos)
) L (StartPos, Chars);
And usage, easy as pie:
DECLARE #String varchar(8000);
SET #String = 'abc,def,ghi,jkl';
CREATE TABLE #Split (S varchar(8000));
INSERT #Split EXEC dbo.StringSplitRowset #String;
SELECT * FROM #Split;
Result:
abc
def
ghi
jkl
And finally, if you don't want to build a numbers table, you can use this SP. I think you will find that one of these two SPs performs well enough for you. There are other implementations of string splitting that could work as well.
ALTER PROCEDURE dbo.StringSplitRowset
#String varchar(8000)
AS
SELECT Substring(#String, l.StartPos, l.Chars) Item
FROM (
SELECT
S.StartPos,
IsNull(NullIf(CharIndex(',', #String, S.StartPos), 0) - S.StartPos, 8000)
FROM (
SELECT 1 UNION ALL
SELECT N.N + 1
FROM (
SELECT A.A * 4096 + B.B * 1024 + C.C * 256 + D.D * 64 + E.E * 16 + F.F * 4 + G.G N
FROM
(SELECT 0 UNION ALL SELECT 1) A (A),
(SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4) G (G),
(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) F (F),
(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) E (E),
(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) D (D),
(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) C (C),
(SELECT 0 UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3) B (B)
) N (N)
WHERE Substring(#String, N.N, 1) = ','
) S (StartPos)
) L (StartPos, Chars)
Any SQL writer serious about understanding some of the performance implications of splitting strings different ways ought to see Aaron Bertrand's blog post on splitting strings.
Also, any serious SQL Server database student ought to see Erland Sommarskog's How to Share Data between Stored Procedures.
Will SQL Server CE let you split with XML functions and use CROSS APPLY? If so, you could do something like this:
SELECT DISTINCT T1.id
FROM (
SELECT id, CAST(('<X>'+
REPLACE(REPLACE(urls,' ',''),',','</X><X>')+
'</X>'
) AS xml
) as URLsXML
FROM dbo.Widgets
) AS T1
CROSS APPLY(
SELECT N.value('.', 'varchar(50)') AS URLPattern
FROM URLsXML.nodes('X') AS S(N)
) AS T2
WHERE #Input LIKE T2.URLPattern
UPDATE: I just checked. It looks like SQL Server CE doesn't support the XML data type or CROSS APPLY.
I think you will have to populate another table with ID's and Patterns to join against.