SQL - Split string multiple values, multiple values - sql

I'm looking to split a string of delimited dates into their respective values and place them within columns of a results table. I've had some success with the following, not quite there for my specific application
SQL - Split string to columns by multiple delimiters
String (from & to dates)
tblTempDates.tmpString
2019-2-11--2019-2-15,2019-2-20--2019-2-20,2019-2-23--2019-2-23,2019-3-19--2019-3-24
Delimiters
-- seperates from & to
, new record
Output required
tblTempDates2
dtFrom dtTo
2019-02-11 2019-02-15
2019-02-20 2019-02-20
2019-02-23 2019-02-23
2019-02-19 2019-03-24
Many thanks in advance :-)

Can use while statement to split the csv,
declare #a table(datefromto varchar(500))
declare #b table(fromdate varchar(500),todate varchar(500))
insert into #a values ('2019-2-11--2019-2-15,2019-2-20--2019-2-20,2019-2-23--2019-2-23,2019-3-19--2019-3-24');
declare #datefromto varchar(500) = (select max(datefromto) from #a)
declare #datesplit int
set #datesplit= (SELECT charindex(',',#datefromto))
WHILE (SELECT charindex(',',#datefromto) FROM #a) <> 0
BEGIN
insert into #b select Left(left(#datefromto,#datesplit),charindex('--',#datefromto)-1) ,
Right(left(#datefromto,#datesplit-1),charindex('--',#datefromto)-1) from #a
set #datesplit= (SELECT charindex(',',#datefromto))
set #datefromto= right(#datefromto,len(#datefromto)-#datesplit)
END
insert into #b select Left(left(#datefromto,#datesplit),charindex('--',#datefromto)-1) ,
Right(left(#datefromto,#datesplit-1),charindex('--',#datefromto)-1) from #a
select * from #b

Here is one nice way to do this using STRING_SPLIT, if your version of SQL Server supports it:
SELECT
LEFT(value, CHARINDEX('--', value) - 1) AS dtFrom,
SUBSTRING(value, CHARINDEX('--', value) + 2, 20) AS dtTo
FROM
(
SELECT STRING_SPLIT(input, ',')
FROM yourTable
) t;
This approach first splits each from/to date substring to a separate row by comma using STRING_SPLIT. Then, it uses base string functions to isolate the from and to dates.

with versions before STRING_SPLIT() (or OPENJSON) I'd suggest this approach:
DECLARE #tbl TABLE(ID INT IDENTITY, YourString VARCHAR(1000));
INSERT INTO #tbl VALUES('2019-2-11--2019-2-15,2019-2-20--2019-2-20,2019-2-23--2019-2-23,2019-3-19--2019-3-24');
WITH Casted AS
(
SELECT *
, CAST('<x><y>' + REPLACE(REPLACE(YourString,'--','</y><y>'),',','</y></x><x><y>') + '</y></x>' AS XML) AsXml
FROM #tbl
)
SELECT ID
,x.value('y[1]','date') AS Date1
,x.value('y[2]','date') AS Date2
FROM Casted
CROSS APPLY AsXml.nodes('/x') A(x);
The idea in short:
Some string replacements transform your multi-level-CSV into an XML like this
<x>
<y>2019-2-11</y>
<y>2019-2-15</y>
</x>
<x>
<y>2019-2-20</y>
<y>2019-2-20</y>
</x>
<x>
<y>2019-2-23</y>
<y>2019-2-23</y>
</x>
<x>
<y>2019-3-19</y>
<y>2019-3-24</y>
</x>
We can then use .nodes() to get all repeating <x> as derived table. Then we use .value() to fetch the <y> by their position.

Related

Loop and insert more than one comma separated List in SQL

I wish to loop through two comma-separated values and perform an insert
As an example lets consider two variables
Declare #Qid= 1,4,6,7,8 #Answers = 4,4,3,2,3
set #pos = 0
set #len = 0
WHILE CHARINDEX(',', #Answers, #pos+1)>0
BEGIN
set #len = CHARINDEX(',', #Answers, #pos+1) - #pos
set #value = SUBSTRING(#Answers, #pos, #len)
insert into table values(#fdid,#Qid,#fusid, #value) -- i need Qid also
set #pos = CHARINDEX(',', #Answers, #pos+#len) +1
END
Using this loop I am able to extract #Answers and can perform insert. But I wish to extract #Qid and insert inside the loop.
edit
for more clarity it is a feedback module. my result table have Qid and Answer field. Answers are ratings (1 to 5). The values we get in variables #Qid and #Answers are sequential. which means 1st answer will be for 1st question and so on.
edit
as per Shnugo's Answer
Declare #Qid varchar(100)= '1,4,6,7,8', #Answers varchar(100)= '4,4,3,2,3'
DECLARE #tbl TABLE(ID INT IDENTITY, Questions VARCHAR(100),Answers VARCHAR(100));
INSERT INTO #tbl VALUES(#Qid,#Answers)
INSERT INTO table(FeedbackId,QuestionId,FeedbackUserId,Answer)
SELECT 1,
A.qXml.value('(/x[sql:column("B.QuestionCount")])[1]','int') AS QuestionNumber,3
,A.aXml.value('(/x[sql:column("B.QuestionCount")])[1]','int') AS AnwerNumber
FROM #tbl t
CROSS APPLY(SELECT CAST('<x>' + REPLACE(#Qid,',','</x><x>') + '</x>' AS XML)
,CAST('<x>' + REPLACE(#Answers,',','</x><x>') + '</x>' AS XML)) A(qXml,aXml)
CROSS APPLY(SELECT TOP(A.qXml.value('count(/x)','int')) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) B(QuestionCount)
I'd prefer Zhorov's JSON answer (needs v2016+).
If you use a SQL-Server below 2016 you might use this position-safe XML-based solution:
A mockup table to simulate your issue with two different rows.
DECLARE #tbl TABLE(ID INT IDENTITY, Questions VARCHAR(100),Answers VARCHAR(100));
INSERT INTO #tbl VALUES('1,4,6,7,8','4,4,3,2,3')
,('1,2,3','4,5,6');
--The query
SELECT t.*
,A.qXml.value('(/x[sql:column("B.QuestionCount")])[1]','int') AS QuestionNumber
,A.aXml.value('(/x[sql:column("B.QuestionCount")])[1]','int') AS AnwerNumber
FROM #tbl t
CROSS APPLY(SELECT CAST('<x>' + REPLACE(t.Questions,',','</x><x>') + '</x>' AS XML)
,CAST('<x>' + REPLACE(t.Answers,',','</x><x>') + '</x>' AS XML)) A(qXml,aXml)
CROSS APPLY(SELECT TOP(A.qXml.value('count(/x)','int')) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) B(QuestionCount);
The idea in short:
We need a CROSS APPLY and some string methods to transform something like 1,2,3 to an xml like <x>1</x><x>2</x><x>3</x>.
Now we can use value() with XQuery count() to find the actual count of questions.
We need one more CROSS APPLY with a computed TOP() clause to get a set of running number from 1 to n with n=countOfQuestions. I do this against master..spt_values. This is just a well-filled standard table... We do not need the values, just any set to create the counter...
Finally we can use .value() in connection with sql:column() in order to fetch the question and the corresponding answer by their positions.
UPDATE: Non-tabular data
If you do not get these CSV parameters as a table you can use this:
Declare #Qid varchar(100)= '1,4,6,7,8', #Answers varchar(100)= '4,4,3,2,3'
--INSERT INTO table(FeedbackId,QuestionId,FeedbackUserId,Answer)
SELECT 1
,A.qXml.value('(/x[sql:column("B.QuestionCount")])[1]','int') AS QuestionNumber
,3
,A.aXml.value('(/x[sql:column("B.QuestionCount")])[1]','int') AS AnwerNumber
FROM (SELECT CAST('<x>' + REPLACE(#Qid,',','</x><x>') + '</x>' AS XML)
,CAST('<x>' + REPLACE(#Answers,',','</x><x>') + '</x>' AS XML)) A(qXml,aXml)
CROSS APPLY(SELECT TOP(A.qXml.value('count(/x)','int')) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) B(QuestionCount);
If you use SQL Server 2016 or higher, you may try to use the next JSON-based approach to map questions and answers by their positions in the input strings. You need to transform the input strings into valid JSON arrays and then use OPENJSON() with default schema to parse the arrays. The result is a table, with columns key, value and type and the key column holds the index of the element in the specified array.
Note, that STRING_SPLIT() function does not guarantee the order of the rows and the output rows might be in any order.
Statement:
DECLARE #Qid nvarchar(max) = N'1,4,6,7,8'
DECLARE #Answers nvarchar(max) = N'4,4,3,2,3'
-- Build your INSERT statement as you expect
-- INSERT INTO Table ...
SELECT j1.[value] AS Qid, j2.[value] AS Answers
FROM OPENJSON(CONCAT(N'[', #Qid, N']')) j1
JOIN OPENJSON(CONCAT(N'[', #Answers, N']')) j2 ON j1.[key] = j2.[key]
Result from the SELECT statement:
Qid Answers
1 4
4 4
6 3
7 2
8 3
You have not described relationship of question and its answers. I feel its one to one relationship and for that, I have given the answer.
declare #Qid varchar(200)= '1,4,6,7,8' , #Answers varchar(200) = '4,4,3,2,3'
;with cte
as(
select id, data qid from dbo.Split (#qid, ',')
),
cte1 as
(
select id, data ansid from dbo.Split (#answers, ',')
)
--insert into tablename
select
qid, ansid from cte join cte1 on cte.id = cte1.id
Result will be:
qid ansid
1 4
4 4
6 3
7 2
8 3
See other option for later version of sqlserver : Split function equivalent in T-SQL?

Order Concatenated field

I have a field which is a concatenation of single letters. I am trying to order these strings within a view. These values can't be hard coded as there are too many. Is someone able to provide some guidance on the function to use to achieve the desired output below? I am using MSSQL.
Current output
CustID | Code
123 | BCA
Desired output
CustID | Code
123 | ABC
I have tried using a UDF
CREATE FUNCTION [dbo].[Alphaorder] (#str VARCHAR(50))
returns VARCHAR(50)
BEGIN
DECLARE #len INT,
#cnt INT =1,
#str1 VARCHAR(50)='',
#output VARCHAR(50)=''
SELECT #len = Len(#str)
WHILE #cnt <= #len
BEGIN
SELECT #str1 += Substring(#str, #cnt, 1) + ','
SET #cnt+=1
END
SELECT #str1 = LEFT(#str1, Len(#str1) - 1)
SELECT #output += Sp_data
FROM (SELECT Split.a.value('.', 'VARCHAR(100)') Sp_data
FROM (SELECT Cast ('<M>' + Replace(#str1, ',', '</M><M>') + '</M>' AS XML) AS Data) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)) A
ORDER BY Sp_data
RETURN #output
END
This works when calling one field
ie.
Select CustID, dbo.alphaorder(Code)
from dbo.source
where custid = 123
however when i try to apply this to top(10) i receive the error
"Invalid length parameter passed to the LEFT or SUBSTRING function."
Keeping in mind my source has ~4million records, is this still the best solution?
Unfortunately i am not able to normalize the data into a separate table with records for each Code.
This doesn't rely on a id column to join with itself, performance is almost as fast
as the answer by #Shnugo:
SELECT
CustID,
(
SELECT
chr
FROM
(SELECT TOP(LEN(Code))
SUBSTRING(Code,ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),1)
FROM sys.messages) A(Chr)
ORDER by chr
FOR XML PATH(''), type).value('.', 'varchar(max)'
) As CODE
FROM
source t
First of all: Avoid loops...
You can try this:
DECLARE #tbl TABLE(ID INT IDENTITY, YourString VARCHAR(100));
INSERT INTO #tbl VALUES ('ABC')
,('JSKEzXO')
,('QKEvYUJMKRC');
--the cte will create a list of all your strings separated in single characters.
--You can check the output with a simple SELECT * FROM SeparatedCharacters instead of the actual SELECT
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,(
SELECT Chr As [*]
FROM SeparatedCharacters sc1
WHERE sc1.ID=t.ID
ORDER BY sc1.Chr
FOR XML PATH(''),TYPE
).value('.','nvarchar(max)') AS Sorted
FROM #tbl t;
The result
ID YourString Sorted
1 ABC ABC
2 JSKEzXO EJKOSXz
3 QKEvYUJMKRC CEJKKMQRUvY
The idea in short
The trick is the first CROSS APPLY. This will create a tally on-the-fly. You will get a resultset with numbers from 1 to n where n is the length of the current string.
The second apply uses this number to get each character one-by-one using SUBSTRING().
The outer SELECT calls from the orginal table, which means one-row-per-ID and use a correalted sub-query to fetch all related characters. They will be sorted and re-concatenated using FOR XML. You might add DISTINCT in order to avoid repeating characters.
That's it :-)
Hint: SQL-Server 2017+
With version v2017 there's the new function STRING_AGG(). This would make the re-concatenation very easy:
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,STRING_AGG(sc.Chr,'') WITHIN GROUP(ORDER BY sc.Chr) AS Sorted
FROM SeparatedCharacters sc
GROUP BY ID,YourString;
Considering your table having good amount of rows (~4 Million), I would suggest you to create a persisted calculated field in the table, to store these values. As calculating these values at run time in a view, will lead to performance problems.
If you are not able to normalize, add this as a denormalized column to the existing table.
I think the error you are getting could be due to empty codes.
If LEN(#str) = 0
BEGIN
SET #output = ''
END
ELSE
BEGIN
... EXISTING CODE BLOCK ...
END
I can suggest to split string into its characters using referred SQL function.
Then you can concatenate string back, this time ordered alphabetically.
Are you using SQL Server 2017? Because with SQL Server 2017, you can use SQL String_Agg string aggregation function to concatenate characters splitted in an ordered way as follows
select
t.CustId, string_agg(strval, '') within GROUP (order by strval)
from CharacterTable t
cross apply dbo.SPLIT(t.code) s
where strval is not null
group by CustId
order by CustId
If you are not working on SQL2017, then you can follow below structure using SQL XML PATH for concatenation in SQL
select
CustId,
STUFF(
(
SELECT
'' + strval
from CharacterTable ct
cross apply dbo.SPLIT(t.code) s
where strval is not null
and t.CustId = ct.CustId
order by strval
FOR XML PATH('')
), 1, 0, ''
) As concatenated_string
from CharacterTable t
order by CustId

Remove Characters in a String in SQL

I have a column u_manualdoc which contains the values are like this CGY DR# 7405. I want to remove the CGY DR#.
Here's the code:
select u_manualdoc, cardcode, cardname from ODLN
I want only the 7405 number. Thanks!
Try this:
--sample data you provided in comments
declare #tbl table(codes varchar(20))
insert into #tbl values
('CGY PST - 58277') , ('CGY RMC PST # 58083'), ('CGY DR # 7443'), ('CSI # 1304'), ('PO# 0568 , 0570'), ('CGY DR# 7446')
--actual query that you can apply to your table
select SUBSTRING(codes, PATINDEX('%[0-9]%', codes), len(codes)) from #tbl
The key point here is to use patindex, which searches for a pattern and returns index where such pattern occur. I specified %[0-9]% which means that we search for any digit - it will return first occurrence of a digit. Now- since this would be our starting point to substring, we pass it to such function. Third parameter of substring is length. Since we want the rest of a string, len function makes sure that we get that :)
Applying to your naming:
select SUBSTRING(u_manualdoc, PATINDEX('%[0-9]%', u_manualdoc), len(u_manualdoc)),
cardcode,
cardname
from ODLN
You should use string functions charindex,len and substring to get it.
See the code below.
select SUBSTRING(u_manualdoc,CHARINDEX('#',u_manualdoc)+1,LEN(u_manualdoc)- CHARINDEX('#',u_manualdoc))
EDIT
In addition to the other answers, you can use this simple method:
select
substring(
u_manualdoc,
len(u_manualdoc) - patindex('%[^0-9]%', reverse(u_manualdoc)) + 2,
len(u_manualdoc)
),
cardcode, cardname
from ODLN
In this example, patindex finds the first non-digit (as specified by ^[0-9]) from the right side of the string, and then uses that as the starting point of the substring.
This will work on all of your sample strings (including 'PO# 0568 , 0570 CGY DR# 7446').
Or use SQL Server Regex, which lets you use more powerful regular expressions within your queries.
TRY THIS
DECLARE #table TABLE(DirtyCol VARCHAR(100));
INSERT INTO #table
VALUES('AB ABCDE # 123'), ('ABCDE# 123'), ('AB: ABC# 123 AB: ABC# 123'), ('AB#'), ('AB # 1 000 000'), ('AB # 1`234`567'), ('AB # (9)(876)(543)');
WITH tally
AS (
SELECT TOP (100) N = ROW_NUMBER() OVER(ORDER BY ##spid)
FROM sys.all_columns),
data
AS (
SELECT DirtyCol,
Col
FROM #table
CROSS APPLY
(
SELECT
(
SELECT C+''
FROM
(
SELECT N,
SUBSTRING(DirtyCol, N, 1) C
FROM tally
WHERE N <= DATALENGTH(DirtyCol)
) [1]
WHERE C BETWEEN '0' AND '9'
ORDER BY N FOR XML PATH('')
)
) p(Col)
WHERE p.Col IS NOT NULL)
SELECT DirtyCol,
CAST(Col AS INT) IntCol
FROM data;

Using SQL to query numbers that have more than tenths decimal place

I have a text field that contains numbers. Some of these numbers have two digits after the decimal and others only one.
Is there a SQL expression to query out only the numbers containing the hundredths values?
My end goal is to use the "round" function to round these isolated numbers to the tenths place.
RDBMS used: sql-server
You can do this with charindex and reverse.
select val
from tbl
where charindex('.', reverse(val)) = 3
Demo
declare #tbl table(val varchar(50))
insert into #tbl select '11.02'
insert into #tbl select '411.0'
insert into #tbl select '11.44'
insert into #tbl select '1144.03'
insert into #tbl select '1441.5'
select val
from #tbl
where charindex('.', reverse(val)) = 3
output:
11.02
11.44
1144.03

T-SQL - remove chars from string beginning from specific character

from table I retrieves values, for example,
7752652:1,7752653:2,7752654:3,7752655:4
or
7752941:1,7752942:2
i.e. string may contain any quantity of substrings.
What I need: remove all occurrences of characters from char ':' to a comma char.
For example,
7752652:1,7752653:2,7752654:3,7752655:4
should be
7752652,7752653,7752654,7752655
How do it?
Replace : with start tag <X>.
Replace , with end tag </X> and an extra comma.
Add an extra end tag to the end </X>.
That will give you a string that look like 7752941<X>1</X>,7752942<X>2</X>.
Cast to XML and use query(text()) to get the root text values.
Cast the result back to string.
SQL Fiddle
MS SQL Server 2012 Schema Setup:
create table T
(
C varchar(100)
)
insert into T values
('7752652:1,7752653:2,7752654:3,7752655:4'),
('7752941:1,7752942:2')
Query 1:
select cast(cast(replace(replace(T.C, ':', '<X>'), ',', '</X>,')+'</X>' as xml).query('text()') as varchar(100)) as C
from T
Results:
| C |
|---------------------------------|
| 7752652,7752653,7752654,7752655 |
| 7752941,7752942 |
declare #query varchar(8000)
select #query= 'select '+ replace (
replace('7752652:1,7752653:2,7752654:3,7752655:4',',',' t union all select ')
,':',' t1 , ')
exec(';with cte as ( '+#query+' ) select cast(t1 as varchar)+'','' from cte for xml path('''')')
Try this:
DECLARE #Data VARCHAR(100) = '7752652:1,7752653:2,7752654:3,7752655:4'
DECLARE #Output VARCHAR(100) = ''
WHILE CHARINDEX(':', #Data) > 0
BEGIN
IF LEN(#Output) > 0 SET #Output = #Output + ','
SET #Output = #Output + LEFT(#Data, CHARINDEX(':', #Data)-1)
SET #Data = STUFF(#Data,
1,
(CASE CHARINDEX(',', #Data)
WHEN 0 THEN LEN(#Data)
ELSE CHARINDEX(',', #Data)
END) - CHARINDEX(':', #Data),
'')
END
SELECT #Output AS Result -- 7752652,7752653,7752654,7752655
Hope this will help.
I borrowed the Splitter function from here. You could use any delimiter parser you may already be using.
Parse the string to table values
Used Substring function to remove values after ':'
Use For xml to re-generate CSV
Test Data:'
IF OBJECT_ID(N'tempdb..#temp')>0
DROP TABLE #temp
CREATE TABLE #temp (id int, StringCSV VARCHAR(500))
INSERT INTO #temp VALUES ('1','7752652:1,7752653:2,7752654:3,7752655:4')
INSERT INTO #temp VALUES ('2','7752656:1,7752657:3,7752658:4')
INSERT INTO #temp VALUES ('3','7752659:1,7752660:2')
SELECT * FROM #temp t
Main Query:
;WITH cte_Remove(ID, REMOVE) AS
(
SELECT y.id AS ID,
SUBSTRING(fn.string, 1, CHARINDEX(':', fn.string) -1) AS Removed
FROM #temp AS y
CROSS APPLY dbo.fnParseStringTSQL(y.StringCSV, ',') AS fn
)
SELECT DISTINCT ID,
STUFF(
(
SELECT ',' + REMOVE
FROM cte_Remove AS t2
WHERE t2.ID = t1.ID
FOR XML PATH('')
),1,1,'') AS col2
FROM cte_Remove AS t1
Cleanup Test Data:
IF OBJECT_ID(N'tempdb..#temp') > 0
DROP TABLE #temp
I solved this problem with CLR function. It is more quickly and function can be used in complex queries
public static SqlString fnRemoveSuffics(SqlString source)
{
string pattern = #":(\d+)";
string replacement = "";
string result = Regex.Replace(source.Value, pattern, replacement);
return new SqlString(result);
}