Add column to SQL result with unique rows - sql

I have an query where I get a comma seperated string as result, such as;
n,n,n,n
where n can be 0 or 1, and will always be the length of four digits.
I use CROSS APPLY STRING_SPLIT to get the result as rows. I would like to add a column to this result, and on each row have an unique string, which would be one word.
Like:
Value|String
1 | description1
0 | description2
1 | description3
1 | description4
I have googled a lot, but can't seem to find how to do this. I hope it is as easy as something like:
SELECT myResultAsRows.Value, {'a','b','c','d'} AS String
FROM table
CROSS APPLY STRING_SPLIT ...
WHERE ...
I know this seems strange, but on another forum (specific for the tool) they suggested hard-coding it...
I also know it might depend on the server used, but in general, is something like this doable?
As of right now, the query is this:
SELECT tagValueRow.Value
FROM t_objectproperties tag
CROSS APPLY STRING_SPLIT(tag.Value,',') tagValueRow
WHERE tag.Object_ID = #OBJECTID# AND tag.Property = 'myTagName'
which results in
Value
1
0
1
1
for the specified #OBJECTID#.
Thank you!
edit: made the question more detailed, with example closer to reality.

I think using ROW_NUMBER() and SUBSTRING it can be acomplished easily.
Somethink like:
SELECT TOP 26 SUBSTRING('abcdefghijklmnopqrstuvwxyz',
ROW_NUMBER() OVER (ORDER BY sort_field), 1), *
FROM table
It has it limitation: the lenght of 'abc..' string, but with TOP will avoid errors.
Update
It can be done in the same way using the same approach of ROW_NUMBER and a JOIN:
SELECT TOP 5 T.Value, D.Label
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY Field) AS Position, tagValueRow.Value AS Value
FROM t_objectproperties tag
CROSS APPLY STRING_SPLIT(tag.Value,',') tagValueRow
WHERE tag.Object_ID = #OBJECTID# AND tag.Property = 'myTagName') T
LEFT JOIN (
VALUES
(1, 'description1'),
(2, 'description2'),
(3, 'description3'),
(4, 'description4'),
(5, 'description5')) D(Position, Label) ON T.Position=D.Position

CREATE TABLE Testdata
(
ID INT,
String VARCHAR(MAX)
)
CREATE TABLE TestList
(
ID INT,
String VARCHAR(MAX)
)
INSERT Testdata SELECT 1,'1,0,1,1'
INSERT TestList SELECT 1,'a,b,c,d'
;WITH tmp(ID,DataItem, String) AS
(
SELECT
ID,
LEFT(String, CHARINDEX(',', String + ',') - 1),
STUFF(String, 1, CHARINDEX(',', String + ','), '')
FROM Testdata
UNION ALL
SELECT
ID,
LEFT(String, CHARINDEX(',', String + ',') - 1),
STUFF(String, 1, CHARINDEX(',', String + ','), '')
FROM tmp
WHERE
String > ''
),
tmp2(ID,DataItem, String) AS (
SELECT
ID,
LEFT(String, CHARINDEX(',', String + ',') - 1),
STUFF(String, 1, CHARINDEX(',', String + ','), '')
FROM TestList
UNION ALL
SELECT
ID,
LEFT(String, CHARINDEX(',', String + ',') - 1),
STUFF(String, 1, CHARINDEX(',', String + ','), '')
FROM tmp2
WHERE
String > ''
)
SELECT
p.DataItem,q.DataItem
FROM tmp AS p
CROSS APPLY
(SELECT * FROM tmp2) AS q
--ORDER BY SomeID
DataItem | DataItem
:------- | :-------
1 | a
0 | a
1 | a
1 | a
1 | b
0 | b
1 | b
1 | b
1 | c
0 | c
1 | c
1 | c
1 | d
0 | d
1 | d
1 | d
db<>fiddle here

If all you need is for each split row to have a value against it in no particular order, then we can cross-join a VALUES table to it based on row-number:
SELECT tagValueRow.Value, desc.description
FROM t_objectproperties tag
CROSS APPLY (
SELECT *, ROW_NUMBER() OVER (ORDER BY (SELECT 1)) AS rn
FROM STRING_SPLIT(tag.Value,',') tagValueRow
) tagValueRow
INNER JOIN (VALUES -- or LEFT JOIN
('desciption1', 1),
('desciption2', 2),
('desciption3', 3),
('desciption4', 4),
) desc (description, rn) ON desc.rn = tagValueRow.rn
WHERE tag.Object_ID = #OBJECTID# AND tag.Property = 'myTagName'
If you may have more than 4 split avlues, but only want a description against the first 4, change the INNER JOIN to LEFT

Related

SQL Server 2014 : Convert two comma separated string into two columns

I have two comma-separated string which needs to be converted into a temptable with two columns synchronized based on the index.
If the input string as below
a = 'abc,def,ghi'
b = 'aaa,bbb,ccc'
then output should be
column1 | column2
------------------
abc | aaa
def | bbb
ghi | ccc
Let us say I have function fnConvertCommaSeparatedStringToColumn which takes in comma-separated string and delimiter as a parameter and returns a column with values. I use this on both strings and get two columns to verify if the count is the same on both sides. But it would be nice two have them in a single temp table. How can i do that?
Let us say I have function which ... returns a column with values.
At that point, the basic idea is to select the column and use the row_number() function with both of your strings. Then you can JOIN the two together using the row_number() result as the matching field for the join.
One method is a recursive CTE:
with cte as (
select convert(varchar(max), null) as a_part, convert(varchar(max), null) as b_part,
convert(varchar(max), 'abc,def,ghi') + ',' as a,
convert(varchar(max), 'aaa,bbb,ccc') + ',' as b,
0 as lev
union all
select convert(varchar(max), left(a, charindex(',', a) - 1)),
convert(varchar(max), left(b, charindex(',', b) - 1)),
stuff(a, 1, charindex(',', a), ''),
stuff(b, 1, charindex(',', b), ''),
lev + 1
from cte
where a <> '' and lev < 10
)
select a_part, b_part
from cte
where lev > 0;
Here is a db<>fiddle.
Here's something a bit sneaky you can try.
I don't have your bespoke function so have used the built-in string_split function (SQL2016+) - for quickly testing, but assuming the parameters are the same. Ideally, your bespoke function should return its own row number in which case you'd use that instead of a rownumber function.
declare #a varchar(20)='abc,def,ghi', #b varchar(20)='aaa,bbb,ccc';
with v as (
select a.value A,b.value B,
row_number() over(partition by a.value order by (select 1/0))Arn,
row_number() over(partition by b.value order by (select 1/0))Brn
from fnConvertCommaSeparatedStringToColumn (#a,',')a
cross apply fnConvertCommaSeparatedStringToColumn (#b,',')b
)
select A,B from v
where Arn=Brn
I would suggest getting a (set based) function that can split a string, based on a delimiter, that returns the ordinal position as well. For example DelimitedSplit8k_LEAD. Then you can trivially split the value, and JOIN on the ordinal position:
DECLARE #a varchar(100) = 'abc,def,ghi';
DECLARE #b varchar(100) = 'aaa,bbb,ccc';
SELECT A.Item AS A,
B.Item AS B
FROM dbo.delimitedsplit8k_lead(#a,',') A
FULL OUTER JOIN dbo.delimitedsplit8k_lead(#a,',') B ON A.ItemNumber = B.ItemNumber;
db<>fiddle
I use a FULL OUTER JOIN and then if either column has a NULL value you know that the 2 delimited lists don't have the same number of delimited values.

How to get the result in below format in SQL Server?

I have a table called recipes with following data.
page_no title
-----------------
1 pancake
2 pizza
3 pasta
5 cookie
page_no 0 is always blank, and missing page_no are blank, I want output as below, for the blank page NULL values in the result.
left_title right_title
------------------------
NULL pancake
Pizza pasta
NULL cookie
I have tried this SQL statement, but it's not returning the desired output:
SELECT
CASE WHEN id % 2 = 0
THEN title
END AS left_title,
CASE WHEN id %2 != 0
THEN title
END AS right_title
FROM
recipes
You are quite close. You just need aggregation:
select max(case when id % 2 = 0 then title end) as left_title,
max(case when id % 2 = 1 then title end) as right_title
from recipes
group by id / 2
order by min(id);
SQL Server does integer division, so id / 2 is always an integer.
Using CTE.. this should be give you a good CTE overview
DECLARE #table TABLE (
pageno int,
title varchar(30)
)
INSERT INTO #table
VALUES (1, 'pancake')
, (2, 'pizza')
, (3, 'pasta')
, (5, 'cookie')
;
WITH cte_pages
AS ( -- generate page numbers
SELECT
0 n,
MAX(pageno) maxpgno
FROM #table
UNION ALL
SELECT
n + 1 n,
maxpgno
FROM cte_pages
WHERE n <= maxpgno),
cte_left
AS ( --- even
SELECT
n,
ROW_NUMBER() OVER (ORDER BY n) rn
FROM cte_pages
WHERE n % 2 = 0),
cte_right
AS ( --- odd
SELECT
n,
ROW_NUMBER() OVER (ORDER BY n) rn
FROM cte_pages
WHERE n % 2 <> 0)
SELECT
tl.title left_title,
tr.title right_title --- final output
FROM cte_left l
INNER JOIN cte_right r
ON l.rn = r.rn
LEFT OUTER JOIN #table tl
ON tl.pageno = l.n
LEFT OUTER JOIN #table tr
ON tr.pageno = r.n

Splitting out semicolon separated values with percentage in SQL into new rows

I have a datatable in MS-SQL (using SQL Server 2014 Standard) with the following illustrative layout (and unfortunately have no ability to change):
ID Score Segmentation
1 | 500 | GROUP 1 - SET A - 50%; GROUP 2 - 25%; GROUP 1 - SET G - 25%
2 | 200 | GROUP 1 - SET B - 25%; GROUP 5 - SET A - SET B - 50%; GROUP 6 - 25%
Where I need to create a query with the following output:
---------------------------
Segmentation | Total Score
---------------------------
GROUP 1 | 425
GROUP 2 | 125
GROUP 3 | 125
GROUP 5 | 100
GROUP 6 | 50
---------------------------
That is, the result set should display each unique group with a total summed score, built by the group's percentage of total in each row (i.e. Group 1 total score is 500*0.5 + 500*0.25 + 200*.25 = 425). Parameters on the data are;
All groups in each row always add up to 100%
There is not a static list of groups
Groups may be listed more than once in each row
New groups may be added during the course of use
Groups may have multiple sets, but do not need to be considered in the result
Group names may include spaces and the '&' character
I had previously found a solution posted at: https://blog.sqlauthority.com/2015/04/21/sql-server-split-comma-separated-list-without-using-a-function/ however it seems to break and does't like the '&' character.
I would appreciate any ideas on how to solve this.
Thanks!
SQL Fiddle
MS SQL Server 2014 Schema Setup:
CREATE TABLE t(ID INT , Score INT , Segmentation VARCHAR(1000))
INSERT INTO t VALUES
(1 , 500 , 'GROUP 1 - SET A - 50%; GROUP 2 - 25%; GROUP 1 - SET G - 25%'),
(2 , 200 , 'GROUP 1 - SET B - 25%; GROUP 5 - SET A - SET B - 50%; GROUP 6 - 25%')
Query 1:
SELECT Groups
,SUM(CAST( Score * REPLACE(Percentage , '%','') /100.0 AS decimal(18,2))) Total_Score
FROM
(
SELECT ID
,Score
,LEFT(RTRIM(LTRIM(Split.a.value('.', 'VARCHAR(100)'))), 8) Groups
,RIGHT(RTRIM(LTRIM(Split.a.value('.', 'VARCHAR(100)'))),4) Percentage
FROM
(SELECT ID
, Score
, Cast ('<X>' + Replace(Segmentation, ';', '</X><X>') + '</X>' AS XML) AS Data
FROM t
) AS t CROSS APPLY Data.nodes ('/X') AS Split(a)
) q
GROUP BY Groups
Results:
| Groups | Total_Score |
|----------|-------------|
| GROUP 1 | 425 |
| GROUP 2 | 125 |
| GROUP 5 | 100 |
| GROUP 6 | 50 |
There are basically three ways to do this in SQL Server:
A user-defined function.
A recursive CTE.
XML processing.
The specialized characters might affect XML processing, but they should be fine for the first two.
For instance:
with cte as (
select id, score,
left(segmentation, charindex(';', segmentation + ';') - 1) as segment,
substring(segmentation, charindex(';', segmentation + ';') + 1, len(segmentation)) + ';' as rest
from t
union all
select id, score,
left(rest, charindex(';', rest + ';') - 1) as segment,
substring(rest, charindex(';', rest) + 1, len(rest))
from cte
where rest like '%;'
)
select left(segment, charindex(' - ', segment)) as segmentation,
sum(score * cast(replace(right(segment, charindex(' ', reverse(segment, ' ')) - 1), '%', '') as float) / 100.0) as TotalScore
from cte
group by left(segment, charindex(' - ', segment));
This is doing a lot of weird string processing because the data structure is simply awful. I would encourage you to work to fix the data structure so the data is more useful.
Yet another option using a CROSS APPLY. I should note, that this split/parse is XML safe.
I don't see where GROUP 3 is coming from (perhaps a typo?)
Declare #YourTable table (ID int,Score int,Segmentation varchar(100))
Insert Into #YourTable values
(1 , 500 , 'GROUP 1 - SET A - 50%; GROUP 2 - 25%; GROUP 1 - SET G - 25%'),
(2 , 200 , 'GROUP 1 - SET B - 25%; GROUP 5 - SET A - SET B - 50%; GROUP 6 - 25%')
Select B.Segmentation
,Total_Score = sum((A.Score*B.Value)/100)
From #YourTable A
Cross Apply (
Select Segmentation = left(RetVal,charindex(' - ',RetVal))
,Value = cast(replace(reverse(left(reverse(RetVal),charindex(' - ',reverse(RetVal)))),'%','') as float)
From (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = LTrim(RTrim(B.i.value('(./text())[1]', 'varchar(max)')))
From (Select x = Cast('<x>' + replace((Select replace(A.Segmentation,';','§§Split§§') as [*] For XML Path('')),'§§Split§§','</x><x>')+'</x>' as xml).query('.')) as A
Cross Apply x.nodes('x') AS B(i)
) C1
) B
Group By B.Segmentation
Returns
Segmentation Total_Score
GROUP 1 425
GROUP 2 125
GROUP 5 100
GROUP 6 50
You could try following XML / XQuery based solution:
DECLARE #Table1 TABLE (
ID INT,
Score INT,
Segmentation VARCHAR(1000)
)
INSERT INTO #Table1
VALUES
(1 , 500 , 'GROUP 1 - SET A - 50%; GROUP 2 - 25%; GROUP 1 - SET G - 25%'),
(2 , 200 , 'GROUP 1 - SET B - 25%; GROUP 5 - SET A - SET B - 50%; GROUP 6 - 25%')
SELECT t.GroupName, TotalScore = SUM((t.Score * t.GroupPercent) / 100.00)
FROM (
SELECT Score = y.Score,
GroupName = LTRIM(z.XmlCol.value('(item/text())[1]', 'VARCHAR(100)')),
GroupPercent= TRY_CONVERT(NUMERIC(5, 2), REPLACE(LTRIM(z.XmlCol.value('(item/text())[last()]', 'VARCHAR(100)')), '%', ''))
FROM (
SELECT *, SegmentationAsXML = CONVERT(XML, '<row><group><item>' + REPLACE(REPLACE(x.Segmentation, ';', '</item></group><group><item>'), '-', '</item><item>') + '</item></group></row>')
FROM #Table1 x
) y
OUTER APPLY y.SegmentationAsXML.nodes('row/group') z(XmlCol)
) t
GROUP BY t.GroupName
Note #1: I assume that source strings (Segmentation doesn't include XML reserved chars).
If source strings contains XML reserved chars then you could test following function:
REPLACE(x.Segmentation, ';',
->
REPLACE( (SELECT x.Segmentation AS '*' FOR XML PATH('')), '; '
Note #2: It converts Segmentation(s) strings into XML thus:
'GROUP 1 - SET A - 50%; GROUP 2 - 25%' becomes
'<row><group><item>GROUP 1</item><item>SET A</item><item>50%</item></group><group><item>GROUP 2</item><item>25%</item>'
Note #3: It uses last() XQuery function in order to access last item within every group.

SQL: Pivoting on more than one column

I have a table
Name | Period | Value1 | Value2
-----+---------+---------+-------
A 1 2 3
A 2 5 4
A 3 6 7
B 1 2 3
B 2 5 4
B 3 6 7
I need results like
Name | Value1 | Value2
-----+--------+------
A | 2,5,6 | 3,4,7
B | 2,5,6 | 3,4,7
Number of periods is dynamic but I know how to handle it so, for simplicity, let's say there are 3 periods
The query below gives me results for Value1. How can I get results for both?
I can always do them separately and then do a join but the table is really big and I need "combine" four values, not two. Can I do it in one statement?
SELECT Name,
[1]+','+ [2] + ','+ [3] ValueString
FROM (
select Name, period, cpr from #MyTable
) as s
PIVOT(SUM(Value1)
FOR period IN ([1],[2],[3])
Use conditional aggregation. Combining the values into strings is a bit tricky, requiring XML logic in SQL Server:
select n.name,
stuff((select ',' + cast(value1 as varchar(max))
from t
where n.name = t.name
order by t.period
for xml path ('')
), 1, 1, ''
) as values1,
stuff((select ',' + cast(value2 as varchar(max))
from t
where n.name = t.name
order by t.period
for xml path ('')
), 1, 1, ''
) as values2
from (select distinct name
from t
) n;
Your values look like numbers, hence the explicit cast and the lack of concern for XML special characters.
You may ask why this does the distinct in a subquery rather than in the outer query. If done in the outer query, then the SQL engine will probably do the aggregation for every row before doing the distinct. I'm not sure if the optimizer is good enough run the subqueries only once per name.
Using Group By with stuff function and get expected result
SELECT Name , STUFF((SELECT ',' + CAST(Value1 AS VARCHAR) FROM #MyTable T2 WHERE T1.Name = T2.Name FOR XML PATH('')),1,1,'') Value1
, STUFF((SELECT ',' + CAST(Value2 AS VARCHAR) FROM #MyTable T3 WHERE T1.Name = T3.Name FOR XML PATH('')),1,1,'') Value2 FROM #MyTable T1 GROUP BY Name

Query Split string into rows

I have a table that looks like this:
ID Value
1 1,10
2 7,9
I want my result to look like this:
ID Value
1 1
1 2
1 3
1 4
1 5
1 6
1 7
1 8
1 9
1 10
2 7
2 8
2 9
I'm after both a range between 2 numbers with , as the delimiter (there can only be one delimiter in the value) and how to split this into rows.
Splitting the comma separated numbers is a small part of this problem. The parsing should be done in the application and the range stored in separate columns. For more than one reason: Storing numbers as strings is a bad idea. Storing two attributes in a single column is a bad idea. And, actually, storing unsanitized user input in the database is also often a bad idea.
In any case, one way to generate the list of numbers is to use a recursive CTE:
with t as (
select t.*, cast(left(value, charindex(',', value) - 1) as int) as first,
cast(substring(value, charindex(',', value) + 1, 100) as int) as last
from table t
),
cte as (
select t.id, t.first as value, t.last
from t
union all
select cte.id, cte.value + 1, cte.last
from cte
where cte.value < cte.last
)
select id, value
from cte
order by id, value;
You may need to fiddle with the value of MAXRECURSION if the ranges are really big.
Any table that a field with multiple values such as this is a problem in terms of design. The only way to deal with these records as it is is to split the values on the delimiter and put them into a temporary table, implement custom splitting code, integrate a CTE as noted, or redesign the original table to put the comma-delimited fields into separate fields, eg
ID LOWLIMIT HILIMIT
1 1 10
similar with Gordon Linoff variant, but has some difference
--create temp table for data sample
DECLARE #Yourdata AS TABLE ( id INT, VALUE VARCHAR(20) )
INSERT #Yourdata
( id, VALUE )
VALUES ( 1, '1,10' ),
( 2, '7,9' )
--final query
;WITH Tally
AS ( SELECT MIN(CONVERT(INT, SUBSTRING(y.VALUE, 1, CHARINDEX(',', y.value) - 1))) AS MinV ,
MAX(CONVERT(INT, SUBSTRING(y.VALUE, CHARINDEX(',', y.value) + 1, 18))) AS MaxV
FROM #yourdata AS y
UNION ALL
SELECT MinV = MinV + 1 , MaxV
FROM Tally
WHERE MinV < Maxv
)
SELECT y.id , t.minV AS value
FROM #yourdata AS y
JOIN tally AS t ON t.MinV BETWEEN CONVERT(INT, SUBSTRING(y.VALUE, 1, CHARINDEX(',', y.value) - 1))
AND CONVERT(INT, SUBSTRING(y.VALUE, CHARINDEX(',', y.value) + 1, 18))
ORDER BY id, minV
OPTION ( MAXRECURSION 999 ) --change it if required
output