SQL Server : comma-separated List based on offset values - sql

The SQL query looks like this:
Declare #Values nvarchar(max) , #From int , #To int
Set #Values = 'a,b,c,d,e,f,g,h,i,j'
Set #From = 3 , #To = 7
The values must be split into rows based on #From and #To. Based on the above query, required output is
c d e f g
Any help would be appreciated.

Disclaimer: I'm the owner of the project Eval SQL.NET
If you have a C# background, you may might consider this library which let you use C# syntax directly in T-SQL
DECLARE #Values NVARCHAR(MAX) ,
#From INT ,
#To INT
SELECT #Values = 'a,b,c,d,e,f,g,h,i,j' ,
#From = 3 ,
#To = 7
-- The index start at 0 and not at 1 in C#
DECLARE #sqlnet SQLNET = SQLNET::New('
values.Split('','').Skip(from - 1).Take(to - from + 1)')
.ValueString('values', #Values)
.Val('from', #From)
.Val('to', #to)
-- In multiple rows
SELECT *
FROM [dbo].[SQLNET_EvalTVF_1](#sqlnet)
-- In same rows
SELECT SQLNET::New('
var splits = values.Split('','').Skip(from - 1).Take(to - from + 1).ToList();
return string.Join('' '', splits);')
.ValueString('values', #Values)
.Val('from', #From)
.Val('to', #to)
.Eval()

You need a string splitter for this. Here is DelimitedSplit8k by Jeff Moden.
DECLARE #Values NVARCHAR(MAX),
#From INT,
#To INT
SELECT #Values = 'a,b,c,d,e,f,g,h,i,j',
#From = 3,
#To = 7
SELECT STUFF((
SELECT ' ' + Item
FROM dbo.DelimitedSplit8K(#Values, ',')
WHERE ItemNumber BETWEEN #From AND #To
FOR XML PATH('')
), 1, 1, '')

You can use this query:
declare #Values nvarchar(max)
, #From int
, #To int
set #Values = 'a,b,c,d,e,f,g,h,i,j'
set #From = 3
set #To = 7
;with cte as(
select substring(#Values, charindex(',', #Values, 1) + 1, 4000) as st
, substring(#Values, 1, charindex(',', #Values, 1) - 1) ch
, 1 as rn
union all
select substring(st, t + 1, 4000) as st
, substring(st, 1, t - 1) ch
, rn + 1 as rn
from cte
cross apply (select case when charindex(',', st, 1) = 0 then 8000 else charindex(',', st, 1) end) t(t)
where rn < #To
and datalength(st) > 0)
select stuff((
select ' ' + ch as 'text()'
from cte
where rn between #From and #To
order by rn
for xml path('')), 1, 1, '')
option (maxrecursion 0)

Related

For loop in SQL to find 1's position in a sequence of 0's and 1's

Below is the for loop in c# which returns 1's position in a sequence of 0's and 1's here is my code
public string OnesPosition(string statusBits)
{
string onePos = "";
for (int i = 0; i < statusBits.Length; i++)
{
if (statusBits[i] == '1')
{
onePos = onePos + Convert.ToSingle(i + 1) + ",";
}
}
onePos = string.IsNullOrEmpty(onePos ) ? "0," : onePos ;
return onePos;
}
var result = OnesPosition("00000000000101");
This will return: result = 12,14
How I can do this in sql query or using SQL function?
Using SQL Server Management Studio v17.9
Just another option
Example
Declare #S varchar(50) = '00000000000101'
Select Stuff((Select concat(',',N )
From (
Select Top (len(#S)) N=Row_Number() Over (Order By (Select NULL))
From master..spt_values
) s
Where substring(#S,N,1)='1'
Order By N
For XML Path ('')),1,1,'')
Returns
12,14
Loops don't play well in SQL Server, but here is a method.
declare #ones varchar(16) = '00000000100101'
declare #pos int = 1
declare #result varchar(256) = ''
while #pos <= len(#ones)
begin
set #result = #result + case when substring(#ones,#pos,1) = 1 then ',' + cast(#pos as varchar) else '' end
set #pos = #pos + 1
end
select right(#result,len(#result) - 1)
Solution:
Another possible approach is using recursive CTE (which returns each digit and digit positon) and group concatenation.
Using STRING_AGG() (from SQL Server 2017):
DECLARE #ones varchar(16)
SET #ones = '1000000001001011';
WITH Digits AS (
SELECT 1 AS DigitPosition, SUBSTRING(#ones, 1, 1) AS Digit
UNION ALL
SELECT DigitPosition + 1, SUBSTRING(#ones, DigitPosition + 1, 1)
FROM Digits
WHERE DigitPosition < LEN(#ones)
)
SELECT STRING_AGG(DigitPosition, ',')
FROM Digits
WHERE Digit = '1'
Using FOR XML:
DECLARE #ones varchar(16)
SET #ones = '1000000001001011';
WITH Digits AS (
SELECT 1 AS DigitPosition, SUBSTRING(#ones, 1, 1) AS Digit
UNION ALL
SELECT DigitPosition + 1, SUBSTRING(#ones, DigitPosition + 1, 1)
FROM Digits
WHERE DigitPosition < LEN(#ones)
)
SELECT CONVERT(varchar(max), DigitPosition) + ','
FROM Digits
WHERE Digit = '1'
FOR XML PATH('')
Output:
1,10,13,15,16
One more method using a tally table and just straight tsql:
WITH Tally(i) AS (
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS i
FROM (VALUES (0), (0), (0), (0), (0), (0), (0)) a(n)
CROSS JOIN (VALUES (0), (0), (0), (0), (0), (0)) b(n)
)
SELECT bitloc
FROM
(
SELECT SUBSTRING(x.d, i, 1) as bitset, i bitloc
FROM (VALUES ('00000000000101')) x(d)
CROSS JOIN Tally
) sub
WHERE bitset = 1
Here is function based on #scsimon response and it worked.
CREATE FUNCTION [dbo].[ConvertTo1Positions]
(
#ones AS varchar(16),
#pos AS INT = 1,
#result varchar(256)
) RETURNS VARCHAR(MAX) AS BEGIN
while #pos <= len(#ones)
begin
set #result = #result + iif(substring(#ones,#pos,1) = 1,cast(#pos as char),'')
set #pos = #pos + 1
end
RETURN #result;
END
I don't think it's a good idea to use loop, but since you want it
DECLARE #Str VARCHAR(45) = '000001000000101',
#OutPut VARCHAR(45) = '',
#I INT = 1;
WHILE #I <= LEN(#Str)
BEGIN
IF (SELECT SUBSTRING(#Str, #I, 1)) = '1'
SET #OutPut = #OutPut + (SELECT CAST(#I AS VARCHAR(10))) + ',';
--Convert.ToSingle(i + 1) why +1?
SET #I = #I + 1;
END
SELECT #OutPut;
or this...
DECLARE #string VARCHAR(100) = '00000000000101';
WITH
cte_n1 (n) AS (SELECT 1 FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (n)),
cte_n2 (n) AS (SELECT 1 FROM cte_n1 a CROSS JOIN cte_n1 b),
cte_Tally (n) AS (
SELECT TOP (LEN(#string))
ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM
cte_n2 a CROSS JOIN cte_n2 b
)
SELECT
STRING_AGG(t.n, ',')
FROM
cte_Tally t
WHERE
SUBSTRING(#string, t.n, 1) = '1';

How to extract URL querystring parameters in SQL Server without writing a function?

I'm having trouble identifying all the querystring parameters that are used on a site. I want to write a T-SQL query that extracts all parameters and counts them, but I don't have permission to write SQL functions, so this solution isn't much help.
The field that I'm working with (Query) includes data that looks like this:
_=1457999955221
tab=profile
tab=tags&sort=votes&page=13
page=5&sort=newest&pagesize=15
...
The query I need to write would return the result:
querystring | count
___________________
_ | 1
tab | 2
sort | 2
page | 2
pagesize | 1
...
Any help is greatly appreciated.
You can borrow one of the functions from here and just inline it into the query.
An example below. I would not expect good performance. Creating a CLR function is by far the most efficient way of splitting strings prior to SQL Server 2016.
DECLARE #QueryStrings Table
(
Query VARCHAR(8000)
)
INSERT INTO #QueryStrings
VALUES
('INVALID'),
('_=1457999955221'),
('tab=profile'),
('tab=tags&sort=votes&page=13'),
('page=5&sort=newest&pagesize=15');
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),
E2(N) AS (SELECT 1 FROM E1 a, E1 b),
E4(N) AS (SELECT 1 FROM E2 a, E2 b),
E42(N) AS (SELECT 1 FROM E4 a, E2 b)
SELECT parameter, count(*)
FROM #QueryStrings qs
CROSS APPLY (SELECT SUBSTRING(qs.Query, t.N + 1, ISNULL(NULLIF(CHARINDEX('&', qs.Query, t.N + 1), 0) - t.N - 1, 8000))
FROM (SELECT 0
UNION ALL
SELECT TOP (DATALENGTH(ISNULL(qs.Query, 1))) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM E42) t(N)
WHERE ( SUBSTRING(qs.Query, t.N, 1) = '&'
OR t.N = 0 )) ca1(split_result)
CROSS APPLY (SELECT CHARINDEX('=',split_result)) ca2(pos)
CROSS APPLY (SELECT CASE WHEN pos > 0 THEN LEFT(split_result,pos-1) END,
CASE WHEN pos > 0 THEN SUBSTRING(split_result, pos+1,8000) END
WHERE pos > 0) ca3(parameter,value)
GROUP BY parameter
More sexy way to approach that:
DECLARE #xml xml
;WITH cte AS (
SELECT *
FROM (VALUES
('_=1457999955221'),
('tab=profile'),
('tab=tags&sort=votes&page=13'),
('page=5&sort=newest&pagesize=15')
) as T(Query))
SELECT #xml = (
SELECT CAST(
(
SELECT '<d><param>' + REPLACE(REPLACE((STUFF((
SELECT '/' + REPLACE(REPLACE(Query,'&','/'),'=','!')
FROM cte
FOR XML PATH('')
),1,1,'')),'/','</value><param>'),'!','</param><value>') + '</value></d>') as xml))
;WITH final AS (
SELECT t.v.value('.','nvarchar(20)') as querystring
FROM #xml.nodes('/d/param') as t(v)
)
SELECT querystring, COUNT(*) as [count]
FROM final
GROUP BY querystring
Result:
querystring count
-------------------- -----------
_ 1
page 2
pagesize 1
sort 2
tab 2
(5 row(s) affected)
Necromancing.
This can now be done easily with SQL Server 2016+ (13.x+)
-- Required for STRING_SPLIT:
-- ALTER DATABASE <db_name> SET COMPATIBILITY_LEVEL = 130 -- >= 130
BEGIN TRY
DECLARE #sql nvarchar(MAX);
-- https://learn.microsoft.com/en-us/sql/t-sql/statements/alter-database-transact-sql-compatibility-level?view=sql-server-ver15
-- SET #sql = N'ALTER DATABASE [COR_Basic_Demo_V4] SET COMPATIBILITY_LEVEL = 130; ';
SET #sql = N'ALTER DATABASE ' + QUOTENAME(DB_NAME()) + N' SET COMPATIBILITY_LEVEL = ' + (SELECT CAST(MAX(compatibility_level) AS nvarchar(10)) FROM sys.databases) + '; ';
-- PRINT #sql;
EXECUTE(#sql);
END TRY
BEGIN CATCH
-- Execute error retrieval routine.
-- EXECUTE usp_GetErrorInfo;
SELECT
ERROR_NUMBER() AS ErrorNumber
,ERROR_SEVERITY() AS ErrorSeverity
,ERROR_STATE() AS ErrorState
,ERROR_PROCEDURE() AS ErrorProcedure
,ERROR_LINE() AS ErrorLine
,ERROR_MESSAGE() AS ErrorMessage;
;
END CATCH
-- Here comes the actual computation
DECLARE #input nvarchar(4000)
SET #input = N'_=1457999955221
tab=profile
tab=tags&sort=votes&page=13
page=5&sort=newest&pagesize=15'
;WITH CTE AS
(
SELECT
value
,SUBSTRING(splitted.value, 1, NULLIF(CHARINDEX('=', splitted.value), 0) -1) AS k
,SUBSTRING(splitted.value, NULLIF(CHARINDEX('=', splitted.value), 0) + 1, LEN(splitted.value)) AS v
FROM STRING_SPLIT
(
REPLACE
(
REPLACE(#input, CHAR(13), '')
,CHAR(10)
,'&'
)
, '&'
) AS splitted
)
SELECT
k
,COUNT(v) AS cnt
,COUNT(DISTINCT v) AS dist_cnt
FROM CTE
GROUP BY k
For earlier versions, or if you actually need to decompose a full-url:
DECLARE #String nvarchar(4000)
DECLARE #path nvarchar(MAX)
DECLARE #hash nvarchar(MAX)
DECLARE #Delimiter nchar(1)
SET #String = 'http://localhost:10004/Kamikatze/ajax/AnySelect.ashx?sql=Maps.ObjectBounds.sql&BE_ID=123&obj_uid=fd4ea870-82eb-4c37-bb67-3e8d5b7b7ac2&&in_stichtag=1589528927178&no_cache=1589528927178&no_cache=1589528927178&moo=moo#foobar'
SET #String = 'sql=Maps.ObjectBounds.sql&BE_ID=123&obj_uid=fd4ea870-82eb-4c37-bb67-3e8d5b7b7ac2&&in_stichtag=1589528927178&no_cache=1589528927178&no_cache=1589528927178&moo=moo#foobar'
-- SET #String = 'http://localhost:10004/Kamikatze/ajax/AnySelect.ashx?sql=Maps.ObjectBounds.sql&BE_ID=123&obj_uid=fd4ea870-82eb-4c37-bb67-3e8d5b7b7ac2&&in_stichtag=1589528927178&no_cache=1589528927178&no_cache=1589528927178&moo=moo'
SET #Delimiter = '&'
SELECT
#path = SUBSTRING(#String, 1, NULLIF(CHARINDEX('?', #String) - 1, -1)) -- AS path
,#hash = RIGHT(#String, LEN(#String) - NULLIF(CHARINDEX(N'#', #String), 0) ) -- AS hash
SELECT -- remove hash
#String = SUBSTRING
(
#String
,1
,COALESCE(NULLIF(CHARINDEX(N'#', #String), 0) - 1, LEN(#String) )
) -- AS xxx
;
SELECT -- remove path
#String = SUBSTRING
( #String
,CHARINDEX(N'?', #String) + 1
,100000
)
;
;WITH Split(id, stpos, endpos, data)
AS
(
SELECT
0 AS id
,0 AS stpos
,CHARINDEX(#Delimiter, #String) AS endpos
,SUBSTRING(#String, 0, COALESCE(NULLIF(CHARINDEX(#Delimiter, #String), 0), LEN(#String)+1) ) AS data
UNION ALL
SELECT
Split.id + 1 AS id
,Split.endpos + 1 AS stpos
,CHARINDEX(#Delimiter, #String, Split.endpos+1) AS endpos
,SUBSTRING(#String, Split.endpos + 1, COALESCE(NULLIF(CHARINDEX(#Delimiter, #String, Split.endpos+1), 0), LEN(#String)+1) - Split.endpos - 1) AS data
FROM Split
WHERE endpos > 0
)
SELECT
id
-- ,stpos
-- ,endpos
-- ,SUBSTRING(#String, stpos, COALESCE(NULLIF(endpos, 0), LEN(#String)+1) - stpos) AS data_simple
,data
,#path AS path
,#hash AS hash
,SUBSTRING(data, 1, NULLIF(charindex('=', data), 0) -1) AS k
,SUBSTRING(data, NULLIF(charindex('=', data), 0) + 1, LEN(data)) AS v
FROM Split
And if you want a function:
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[tfu_DecomposeUrl]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
EXECUTE('CREATE FUNCTION dbo.tfu_DecomposeUrl( ) RETURNS TABLE AS RETURN ( SELECT 123 AS abc) ');
GO
ALTER FUNCTION dbo.tfu_DecomposeUrl
(
#input_string NVARCHAR(4000)
)
RETURNS TABLE
AS
RETURN
(
WITH CTE AS
(
SELECT
SUBSTRING(#input_string, 1, NULLIF(CHARINDEX('?', #input_string) - 1, -1)) AS query_path
,RIGHT(#input_string, LEN(#input_string) - NULLIF(CHARINDEX(N'#', #input_string), 0) ) AS query_hash
,SUBSTRING
(
#input_string
,1
,COALESCE(NULLIF(CHARINDEX(N'#', #input_string), 0) - 1, LEN(#input_string) )
) AS PathWithoutHash
)
,CTE2 AS
(
SELECT
CTE.query_path
,CTE.query_hash
,SUBSTRING
( PathWithoutHash
,CHARINDEX(N'?', PathWithoutHash) + 1
,100000
) AS KeyValueString
FROM CTE
)
,Split(id, stpos, endpos, data, query_path, query_hash)
AS
(
SELECT
0 AS id
,0 AS stpos
,CHARINDEX(N'&', CTE2.KeyValueString) AS endpos
,SUBSTRING(CTE2.KeyValueString, 0, COALESCE(NULLIF(CHARINDEX(N'&', CTE2.KeyValueString), 0), LEN(CTE2.KeyValueString)+1) ) AS data
,CTE2.query_path
,CTE2.query_hash
FROM CTE2
UNION ALL
SELECT
Split.id + 1 AS id
,Split.endpos + 1 AS stpos
,CHARINDEX(N'&', CTE2.KeyValueString, Split.endpos+1) AS endpos
,SUBSTRING(CTE2.KeyValueString, Split.endpos + 1, COALESCE(NULLIF(CHARINDEX(N'&', CTE2.KeyValueString, Split.endpos+1), 0), LEN(CTE2.KeyValueString)+1) - Split.endpos - 1) AS data
,CTE2.query_path
,CTE2.query_hash
FROM Split
CROSS JOIN CTE2
WHERE endpos > 0
)
SELECT
id
-- ,stpos
-- ,endpos
-- ,SUBSTRING(#String, stpos, COALESCE(NULLIF(endpos, 0), LEN(#String)+1) - stpos) AS data_simple
,data
,query_path
,query_hash
,SUBSTRING(data, 1, NULLIF(CHARINDEX('=', data), 0) -1) AS k
,SUBSTRING(data, NULLIF(CHARINDEX('=', data), 0) + 1, LEN(data)) AS v
FROM Split
)
GO
Which can be simplified into this
DECLARE #input_string nvarchar(4000)
SET #input_string = 'http://localhost:10004/Kamikatze/ajax/AnySelect.ashx?sql=Maps.ObjectBounds.sql&BE_ID=123&obj_uid=fd4ea870-82eb-4c37-bb67-3e8d5b7b7ac2&&in_stichtag=1589528927178&no_cache=1589528927178&no_cache=1589528927178&moo=moo#foobar'
-- SET #input_string = 'sql=Maps.ObjectBounds.sql&BE_ID=123&obj_uid=fd4ea870-82eb-4c37-bb67-3e8d5b7b7ac2&&in_stichtag=1589528927178&no_cache=1589528927178&no_cache=1589528927178&moo=moo#foobar'
-- SET #input_string = 'http://localhost:10004/Kamikatze/ajax/AnySelect.ashx?sql=Maps.ObjectBounds.sql&BE_ID=123&obj_uid=fd4ea870-82eb-4c37-bb67-3e8d5b7b7ac2&&in_stichtag=1589528927178&no_cache=1589528927178&no_cache=1589528927178&moo=moo'
-- SET #input_string = 'sql=Maps.ObjectBounds.sql&BE_ID=123&obj_uid=fd4ea870-82eb-4c37-bb67-3e8d5b7b7ac2&&in_stichtag=1589528927178&no_cache=1589528927178&no_cache=1589528927178&moo=moo'
;WITH CTE AS
(
SELECT
SUBSTRING(#input_string, 1, NULLIF(CHARINDEX('?', #input_string) - 1, -1)) AS query_path
,RIGHT(#input_string, LEN(#input_string) - NULLIF(CHARINDEX(N'#', #input_string), 0) ) AS query_hash
,SUBSTRING
(
#input_string
,1
,COALESCE(NULLIF(CHARINDEX(N'#', #input_string), 0) - 1, LEN(#input_string) )
) AS PathWithoutHash
)
,CTE2 AS
(
SELECT
CTE.query_path
,CTE.query_hash
,SUBSTRING
( PathWithoutHash
,CHARINDEX(N'?', PathWithoutHash) + 1
,100000
) AS KeyValueString
FROM CTE
)
SELECT
t.id
,t.data
,CTE2.query_path
,CTE2.query_hash
,SUBSTRING(t.data, 1, NULLIF(CHARINDEX('=', t.data), 0) -1) AS k
,SUBSTRING(t.data, NULLIF(CHARINDEX('=', t.data), 0) + 1, LEN(t.data)) AS v
FROM CTE2
OUTER APPLY dbo.tfu_FastSplitString(CTE2.KeyValueString, N'&') AS t
And a table-valued string-splitting function:
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'dbo.tfu_FastSplitString') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
EXECUTE('CREATE FUNCTION dbo.tfu_FastSplitString( ) RETURNS TABLE AS RETURN ( SELECT 123 AS abc) ');
GO
ALTER FUNCTION dbo.tfu_FastSplitString
(
#input_string nvarchar(4000)
,#delimiter nchar(1)
)
RETURNS TABLE
AS
RETURN
(
WITH Split(id, stpos, endpos) -- , data)
AS
(
SELECT
0 AS id
,0 AS stpos
,CHARINDEX(#delimiter, #input_string) AS endpos
-- ,SUBSTRING(#input_string, 0, COALESCE(NULLIF(CHARINDEX(#delimiter, #input_string), 0), LEN(#input_string)+1) ) AS data
UNION ALL
SELECT
Split.id + 1 AS id
,Split.endpos + 1 AS stpos
,CHARINDEX(#delimiter, #input_string, Split.endpos+1) AS endpos
-- ,SUBSTRING(#input_string, Split.endpos + 1, COALESCE(NULLIF(CHARINDEX(#delimiter, #input_string, Split.endpos+1), 0), LEN(#input_string)+1) - Split.endpos - 1) AS data
FROM Split
WHERE endpos > 0
)
SELECT
id
-- ,stpos
-- ,endpos
-- ,data
,SUBSTRING(#input_string, stpos, COALESCE(NULLIF(endpos, 0), LEN(#input_string)+1) - stpos) AS data
FROM Split
)
GO
These are inline table-valued functions, so they should be fast.
If it were a multi-statement table-valued function, it would be slow.

Hot to convert a variable with value '1,2,3' to a table (every number as a record)

Working on SQL (2005 and 2008)
the variable with value '1,2,3' would be call #cedis and this could to have N number for example
set #cedis='1' or set #cedis='1,2,3,4,5,6,7' or set #cedis='125,98,91'
so important, its this must to be a select only, a loop could not to be use, only a select!
this must to return a (result as ) table with values for example
set #cedis='1,2,3,4' this must to return a result
number 1 2 3 4
declare #cedis varchar(max)
set #cedis='1,58,123,8'
;with datos as
(
my select with is going to return me the table
)
select * from datos
result set is
number
1
58
123
8
If am not wrong this is what you need
DECLARE #cedis VARCHAR(500)='1,2,3,4'
SELECT Split.a.value('.', 'VARCHAR(100)') Numbers
FROM (SELECT Cast ('<M>' + Replace(#cedis, ',', '</M><M>') + '</M>' AS XML) AS Numbers) AS A
CROSS APPLY Numbers.nodes ('/M') AS Split(a)
Result:
Numbers
-------
1
2
3
4
A table valued function would do it.
CREATE FUNCTION [dbo].[fn_Split](#text VARCHAR(MAX), #delimiter VARCHAR(5) = ',')
RETURNS #Strings TABLE
(
position int IDENTITY PRIMARY KEY,
value 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 #Strings VALUES (#text)
BREAK
END
IF (#index > 1)
BEGIN
INSERT INTO #Strings VALUES (LEFT(#text, #index - 1))
END
SET #text = RIGHT(#text, (LEN(#text) - (#index+LEN(#delimiter)-1)))
END
RETURN
END
You can call it as follows:
SELECT *
FROM dbo.fn_Split(#cedis,',')
Here is a more generic solution that breaks any given string into a table based on any given separator:
http://rextester.com/VSRDLS48817
Not an original idea, but I've found it very useful.
create function [dbo].[SplitString]
(
#str nvarchar(255),
#separator char(1)
)
returns table
AS
return (
with tokens(p, a, b) AS (
select
cast(1 as int),
cast(1 as int),
charindex(#separator, #str)
union all
select
p + 1,
b + 1,
charindex(#separator, #str, b + 1)
from tokens
where b > 0
)
select
p-1 ItemIndex,
substring(
#str,
a,
case when b > 0 then b-a ELSE LEN(#str) end)
AS Item
from tokens
);
This is another one approach to get required output result
DECLARE #cedis VARCHAR(MAX) ,
#delimeter VARCHAR(10)
SET #cedis = '1,58,123,8,14144,15,155231,15,3647,2347,45,76,68,2354,577,5'
SET #delimeter = ','
SET #cedis = #cedis + #delimeter;
WITH datos
AS ( SELECT n = 1
UNION ALL
SELECT n + 1
FROM datos
WHERE n <= LEN(#cedis)
),
cte
AS ( SELECT T.N ,
ROW_NUMBER() OVER ( ORDER BY T.N ) AS RN
FROM datos AS T
WHERE SUBSTRING(#cedis, T.N, LEN(#delimeter)) = #delimeter
AND LEN(#cedis) >= T.N
)
SELECT SUBSTRING(#cedis, COALESCE(R.N + LEN(#delimeter), 1),
L.N - COALESCE(R.N + LEN(#delimeter), 1)) AS part ,
L.RN AS ID
FROM cte AS L
LEFT JOIN cte AS R ON L.RN = R.RN + 1
OPTION ( MAXRECURSION 1000 )

create while loop with cte

how to create sql server cte from a while loop
my loop like this
declare #ind as int
declare #code as nvarchar
set #ind = 0
while #ind < 884
begin
select #ind = #ind + 1
--here execute Procedure
--and set return value to variable
set #code = cast (#ind as nvarchar)
end
If you need table:
;WITH Sec(Number) AS
(
SELECT 0 AS Number
UNION ALL
SELECT Number + 1
FROM Sec
WHERE Number < 884
)
SELECT * FROM Sec
OPTION(MAXRECURSION 0)
If you need one string:
;WITH Sec(Number) AS
(
SELECT 0 AS Number
UNION ALL
SELECT Number + 1
FROM Sec
WHERE Number < 884
)
SELECT STUFF(a.[Str], 1, 1, '')
FROM
(
SELECT (SELECT ',' + CAST(Number AS NVARCHAR(3))
FROM Sec
FOR XML PATH(''), TYPE
).value('.','varchar(max)') AS [Str]
) AS a
OPTION(MAXRECURSION 0)
Below query selects values from 0 to 884:
;WITH T(Num)AS
(
SELECT 0
UNION ALL
SELECT Num+1 FROM T WHERE T.Num < 884
)SELECT Num FROM T
OPTION (MAXRECURSION 0);

Replace values in a CSV string

I have a list of products in comma separated fashion and since the item list was replaced with new product items, I am trying to modify this CSV list with new product item list.
create table #tmp
(
id int identity(1,1) not null,
plist varchar(max) null
);
create table #tmpprod
(
oldid int null,
newid int null
);
insert into #tmp(plist) values
('10,11,15,17,19'),
('22,34,44,25'),
('5,6,8,9');
insert into #tmpprod(oldid, newid) values
(5, 109),
(9, 110),
(10, 111),
(15, 112),
(19, 113),
(30, 114),
(34, 222),
(44, 333);
I am trying to use a split fn to convert into rows and then replace these values and then convert columns to rows again. Is it possible in any other manner?
The output will be as:
id
newlist
1
111,11,112,17,113
2
22,222,333,25
3
109,6,8,110
Convert your comma separated list to XML. Use a numbers table, XQuery and position() to get the separate ID's with the position they have in the string. Build the comma separated string using the for xml path('') trick with a left outer join to #tempprod and order by position().
;with C as
(
select T.id,
N.number as Pos,
X.PList.value('(/i[position()=sql:column("N.Number")])[1]', 'int') as PID
from #tmp as T
cross apply (select cast('<i>'+replace(plist, ',', '</i><i>')+'</i>' as xml)) as X(PList)
inner join master..spt_values as N
on N.number between 1 and X.PList.value('count(/i)', 'int')
where N.type = 'P'
)
select C1.id,
stuff((select ','+cast(coalesce(T.newid, C2.PID) as varchar(10))
from C as C2
left outer join #tmpprod as T
on C2.PID = T.oldid
where C1.id = C2.id
order by C2.Pos
for xml path(''), type).value('.', 'varchar(max)'), 1, 1, '')
from C as C1
group by C1.id
Try on SE-Data
Assuming SQL Server 2005 or better, and assuming order isn't important, then given this split function:
CREATE FUNCTION [dbo].[SplitInts]
(
#List VARCHAR(MAX),
#Delimiter CHAR(1)
)
RETURNS TABLE
AS
RETURN ( SELECT Item FROM ( SELECT Item = x.i.value('(./text())[1]', 'int')
FROM
( SELECT [XML] = CONVERT(XML, '<i>' + REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.') ) AS a CROSS APPLY [XML].nodes('i') AS x(i)
) AS y WHERE Item IS NOT NULL);
GO
You can get this result in the following way:
;WITH x AS
(
SELECT id, item, oldid, [newid], rn = ROW_NUMBER() OVER
(PARTITION BY id
ORDER BY PATINDEX('%,' + RTRIM(s.Item) + ',%', ',' + t.plist + ','))
FROM #tmp AS t CROSS APPLY dbo.SplitInts(t.plist, ',') AS s
LEFT OUTER JOIN #tmpprod AS p ON p.oldid = s.Item
)
SELECT id, newlist = STUFF((SELECT ',' + RTRIM(COALESCE([newid], Item))
FROM x AS x2 WHERE x2.id = x.id
FOR XML PATH(''),
TYPE).value(N'./text()[1]', N'varchar(max)'), 1, 1, '')
FROM x GROUP BY id;
Results:
id
newlist
1
111,11,112,17,113
2
22,222,333,25
3
109,6,8,110
Note that the ROW_NUMBER() / OVER / PARTITION BY / ORDER BY is only there to try to coerce the optimizer to return the rows in that order. You may observe this behavior today and it can change tomorrow depending on statistics or data changes, optimizer changes (service packs, CUs, upgrade, etc.) or other variables.
Long story short: if you're depending on that order, just send the set back to the client, and have the client construct the comma-delimited list. It's probably where this functionality belongs anyway.
That said, in SQL Server 2017+, we can guarantee retaining the order by splitting with OPENJSON() and reassembling with STRING_AGG():
;WITH x AS
(
SELECT o.id, val = COALESCE(n.newid, p.value), p.[key]
FROM #tmp AS o CROSS APPLY
OPENJSON('["' + REPLACE(o.pList, ',', '","') + '"]') AS p
LEFT OUTER JOIN #tmpprod AS n
ON p.value = n.oldid
)
SELECT id, newlist = STRING_AGG(val, ',')
WITHIN GROUP (ORDER BY [key])
FROM x GROUP BY id;
Example db<>fiddle
Thanks for this question - I've just learned something new. The following code is an adaptation of an article written by Rob Volk on exactly this topic. This is a very clever query! I won't copy all of the content down here. I have adapted it to create the results you're looking for in your example.
CREATE TABLE #nums (n INT)
DECLARE #i INT
SET #i = 1
WHILE #i < 8000
BEGIN
INSERT #nums VALUES(#i)
SET #i = #i + 1
END
CREATE TABLE #tmp (
id INT IDENTITY(1,1) not null,
plist VARCHAR(MAX) null
)
INSERT INTO #tmp
VALUES('10,11,15,17,19'),('22,34,44,25'),('5,6,8,9')
CREATE TABLE #tmpprod (
oldid INT NULL,
newid INT NULL
)
INSERT INTO #tmpprod VALUES(5, 109),(9, 110),(10, 111),(15, 112),(19, 113),(30, 114),(34, 222),(44, 333)
;WITH cte AS (SELECT ID, NULLIF(SUBSTRING(',' + plist + ',' , n , CHARINDEX(',' , ',' + plist + ',' , n) - n) , '') AS prod
FROM #nums, #tmp
WHERE ID <= LEN(',' + plist + ',') AND SUBSTRING(',' + plist + ',' , n - 1, 1) = ','
AND CHARINDEX(',' , ',' + plist + ',' , n) - n > 0)
UPDATE t SET plist = (SELECT CAST(CASE WHEN tp.oldid IS NULL THEN cte.prod ELSE tp.newid END AS VARCHAR) + ','
FROM cte LEFT JOIN #tmpprod tp ON cte.prod = tp.oldid
WHERE cte.id = t.id FOR XML PATH(''))
FROM #tmp t WHERE id = t.id
UPDATE #tmp SET plist = SUBSTRING(plist, 1, LEN(plist) -1)
WHERE LEN(plist) > 0 AND SUBSTRING(plist, LEN(plist), 1) = ','
SELECT * FROM #tmp
DROP TABLE #tmp
DROP TABLE #tmpprod
DROP TABLE #nums
The #nums table is a table of sequential integers, the length of which must be greater than the longest CSV you have in your table. The first 8 lines of the script create this table and populate it. Then I've copied in your code, followed by the meat of this query - the very clever single-query parser, described in more detail in the article pointed to above. The common table expression (WITH cte...) does the parsing, and the update script recompiles the results into CSV and updates #tmp.
Adam Machanic's blog contains this posting of a T-SQL only UDF which can accept T-SQL's wildcards for use in replacement.
http://dataeducation.com/splitting-a-string-of-unlimited-length/
For my own use, I adjusted the varchar sizes to max. Also note that this UDF performs rather slowly, but if you cannot use the CLR, it may be an option. The minor changes I made to the author's code may limit use of this to SQL Server 2008r2 and later.
CREATE FUNCTION dbo.PatternReplace
(
#InputString VARCHAR(max),
#Pattern VARCHAR(max),
#ReplaceText VARCHAR(max)
)
RETURNS VARCHAR(max)
AS
BEGIN
DECLARE #Result VARCHAR(max) = ''
-- First character in a match
DECLARE #First INT
-- Next character to start search on
DECLARE #Next INT = 1
-- Length of the total string -- 0 if #InputString is NULL
DECLARE #Len INT = COALESCE(LEN(#InputString), 0)
-- End of a pattern
DECLARE #EndPattern INT
WHILE (#Next <= #Len)
BEGIN
SET #First = PATINDEX('%' + #Pattern + '%', SUBSTRING(#InputString, #Next, #Len))
IF COALESCE(#First, 0) = 0 --no match - return
BEGIN
SET #Result = #Result +
CASE --return NULL, just like REPLACE, if inputs are NULL
WHEN #InputString IS NULL
OR #Pattern IS NULL
OR #ReplaceText IS NULL THEN NULL
ELSE SUBSTRING(#InputString, #Next, #Len)
END
BREAK
END
ELSE
BEGIN
-- Concatenate characters before the match to the result
SET #Result = #Result + SUBSTRING(#InputString, #Next, #First - 1)
SET #Next = #Next + #First - 1
SET #EndPattern = 1
-- Find start of end pattern range
WHILE PATINDEX(#Pattern, SUBSTRING(#InputString, #Next, #EndPattern)) = 0
SET #EndPattern = #EndPattern + 1
-- Find end of pattern range
WHILE PATINDEX(#Pattern, SUBSTRING(#InputString, #Next, #EndPattern)) > 0
AND #Len >= (#Next + #EndPattern - 1)
SET #EndPattern = #EndPattern + 1
--Either at the end of the pattern or #Next + #EndPattern = #Len
SET #Result = #Result + #ReplaceText
SET #Next = #Next + #EndPattern - 1
END
END
RETURN(#Result)
END