How to merge rows into one row with separator - sql

I need merge rows into one row for summary of calculation (SQL Server 2014).
My SQL query is:
SELECT
[c].[Iso],
SUM([r].[Quantity]) AS [Quantity],
SUM([r].[Amount]) AS [Amount]
FROM
[CarReport].[dbo].[Refuelling] [r]
INNER JOIN
[CarReport].[dbo].[Currency] [c] ON [r].[CurrencyId] = [c].[CurrencyId]
WHERE
[r].[DataId] = 15
AND [r].[IsDeleted] = 0
GROUP BY
[r].[CurrencyId], [c].[Iso]
Result of this query is:
CZK | 50.00 | 1350,00
EUR | 23.00 | 463,20
I would like to have this result:
CZK/EUR | 50.00/23.00 | 1350,00/463,20
When add new currency (x), the result of new currency must be appended:
CZK/EUR/x | 50.00/23.00/x | 1350,00/463,20/x
Can somebody please help me with this topic?
Many thanks

Something like this should work with earlier versions of MSSQL as well.
;with subQuery as (
-- paste your base query here
-- extend it with one additional column:
-- ,ROW_NUMBER() OVER (ORDER BY ISO) RowNum
)
select
(select stuff((select '/' + convert(nvarchar(max), Iso) from subQuery order by RowNum for xml path('')), 1, 1, '')),
(select stuff((select '/' + convert(nvarchar(max), Quantity) from subQuery order by RowNum for xml path('')), 1, 1, '')),
(select stuff((select '/' + convert(nvarchar(max), Amount) from subQuery order by RowNum for xml path('')), 1, 1, ''))

If you are running SQL Server 2017, you can add another level of aggregation and use string_agg():
SELECT
STRING_AGG([Iso], '/') WITHIN GROUP(ORDER BY [Iso]) AS [Iso],
STRING_AGG([Quantity], '/') WITHIN GROUP(ORDER BY [Iso]) AS [Quantity],
STRING_AGG([Amount], '/') WITHIN GROUP(ORDER BY [Iso]) AS [Amount]
FROM (
SELECT
[c].[Iso],
SUM([r].[Quantity]) AS [Quantity]
SUM([r].[Amount]) AS [Amount]
FROM
[CarReport].[dbo].[Refuelling] [r]
INNER JOIN
[CarReport].[dbo].[Currency] [c] ON [r].[CurrencyId] = [c].[CurrencyId]
WHERE
[r].[DataId] = 15
AND [r].[IsDeleted] = 0
GROUP BY
[r].[CurrencyId], [c].[Iso]
) t
The ORDER BY clause of STRING_AGG() gives you a consistent ordering of values accross the different columns, so you can tell which quantity and amount belong to which currency (if that matters).

Related

SQL Recursive CTE Replacing records in each recursion

I have a table like this:
ItemID ItemFormula
100 'ID_3+ID_5'
110 'ID_2+ID_6'
120 'ID_100+ID_110'
130 'ID_120+ID_4'
This is the simplified version of a formula table with nearly 1000 records and up to 40 levels of reference (items used in other items). The task is breaking down the formulas to just one level reference where no other items are in one item. For example in the table above for id=130 I should have '((ID_3+ID_5)+(ID_2+ID_6))+ID_4'
EDIT: The operations do not limit to "+" and items have a character between them to be recognizable. For the sake of simplicity, I removed that character.
I can use recursive CTE for that. but my problem is that due to high levels of reference, my recursive select has lots of records joining so it takes a lot to complete.
My question is that: Can I keep the previous recursion only each time the recursion happens?
Here is my CTE Code
WITH Formula
AS (SELECT A.ItemID
,'ID_' + CONVERT(VARCHAR(20), A.ItemID) AS ItemText
,CONVERT(VARCHAR(MAX), A.ItemFormula) AS ItemFormula
FROM (VALUES (100,'ID_3+ID_5'),
(110,'ID_2+ID_6'),
(120,'ID_100+ID_110'),
(130,'ID_120+ID_4')
) A (ItemID,ItemFormula)
)
,REC
AS
(
SELECT A.ItemID
,A.ItemText
,A.ItemFormula
,1 AS LevelID
FROM Formula A
UNION ALL
SELECT A.ItemID
,A.ItemText
,' '
+ TRIM (REPLACE (REPLACE (A.ItemFormula, B.ItemText, ' ( ' + B.ItemFormula + ' ) '), ' ', ' '))
+ ' ' AS ItemFormula
,A.LevelID + 1 AS LevelID
FROM REC A
CROSS APPLY
(
SELECT *
FROM
(
SELECT *
,ROW_NUMBER () OVER (ORDER BY GETDATE ()) AS RowNum
FROM Formula B2
WHERE CHARINDEX (B2.ItemText, A.ItemFormula) > 0
) B3
WHERE B3.RowNum = 1
) B
)
,FinalQ
AS
(
SELECT A2.ItemID
,A2.ItemFormula
,A2.LevelID
FROM
(
SELECT A.ItemID
,REPLACE (TRIM (A.ItemFormula), ' ', '') AS ItemFormula
,A.LevelID
,ROW_NUMBER () OVER (PARTITION BY A.ItemID ORDER BY A.LevelID DESC) AS RowNum
FROM REC A
) A2
WHERE A2.RowNum = 1
)
SELECT * FROM FinalQ A2 ORDER BY A2.ItemID;
Thanks in advance.
My question is that: Can I keep the previous recursion only each time the recursion happens?
No. The recursive CTE will keep adding rows to the ones found in previous iterations. You don't have some kind of control that would allow you to remove rows of the recursive CTE during its iterations.
You can, however, filter them out after the recursive CTE is complete, maybe on a secondary CTE that takes into account only the last meaninful rows (by some kind of rule to be defined).
The only vaguely similar idea is found in PostgreSQL where you can use the UNION clause in addition to UNION ALL, to avoid producing more identical rows. But this is different to what you need, anyway.
This is an enormously complicated problem. Here are the ideas:
Find which items do not need any insertions. These are the ones that have no references to any others.
Build an ordering for item insertion. An insertion can go into an item, assuming that the item is already defined. A recursive CTE can be used for this.
Enumerate the insertions. Everything from (1) gets a "1". The rest are in order.
Process the insertions in the insertion order.
Here is my solution:
with ordering as (
select itemid, itemtext, itemformula, convert(varchar(max), null) as otheritemtext, 1 as lev
from formula f
where not exists (select 1
from formula f2 join
string_split(f.itemformula, '+') s
on f2.itemtext = s.value
where f2.itemid <> f.itemid
)
union all
select f.itemid, f.itemtext, f.itemformula, convert(varchar(max), s.value), lev + 1
from formula f cross apply
string_split(f.itemformula, '+') s join
ordering o
on o.itemtext = s.value
-- where lev <= 2
),
ordered as (
select distinct o.*,
dense_rank() over (order by (case when lev = 1 then -1 else lev end), (case when lev = 1 then '' else otheritemtext end)) as seqnum
from ordering o
),
cte as (
select o.itemid, o.itemtext, o.itemformula, convert(varchar(max), o.otheritemtext) as otheritemtext,
o.itemformula as newformula, o.seqnum, 1 as lev
from ordered o
where seqnum = 1
union all
select cte.itemid, o.itemtext, o.itemformula, convert(varchar(max), cte.itemtext),
replace(o.itemformula, o.otheritemtext, concat('(', cte.newformula, ')')), o.seqnum, cte.lev + 1
from cte join
ordered o
on cte.itemtext = o.otheritemtext and cte.seqnum < o.seqnum
)
select *
from cte;
And the db<>fiddle.
You could take advantage of the logical order of the formulas if any (Item_100 can not reference Item_150) and process items in a descending order.
The following uses LIKE and it will not work for formulas which have overlapping patterns (eg ID_10 & ID_100) you could fix that by some string manipulation or by keeping ItemIDs of fixed length (eg. ID_10010 & ID_10100: start numbering of items from a high number like 10000)
declare #f table
(
ItemId int,
ItemFormula varchar(1000)
);
insert into #f(ItemId, ItemFormula)
values
(100, 'ID_3+ID_5'),
(110, 'ID_2+ID_6'),
(120, 'ID_100+ID_110'),
(130, 'ID_120+ID_4'),
(140, '(ID_130+ID_110)/ID_100'),
(150, 'sqrt(ID_140, ID_130)'),
(160, 'ID_150-ID_120+ID_140');
;with cte
as
(
select f.ItemId, replace(cast(f.ItemFormula as varchar(max)), isnull('ID_' + cast(r.ItemId as varchar(max)), ''), isnull('(' + r.ItemFormula+ ')', '')) as therepl, 1 as lvl
from #f as f
outer apply (
select *
from
(
select rr.*, row_number() over(order by rr.ItemId desc) as rownum
from #f as rr
where f.ItemFormula like '%ID_' + cast(rr.ItemId as varchar(1000)) + '%'
) as src
where rownum = 1
) as r
union all
select c.ItemId, replace(c.therepl, 'ID_' + cast(r.ItemId as varchar(max)), '(' + r.ItemFormula+ ')'), c.lvl+1
from cte as c
cross apply (
select *
from
(
select rr.*, row_number() over(order by rr.ItemId desc) as rownum
from #f as rr
where c.therepl like '%ID_' + cast(rr.ItemId as varchar(1000)) + '%'
) as src
where rownum = 1
) as r
),
rown
as
(
select *, row_number() over (partition by itemid order by lvl desc) as rownum
from cte
)
select *
from rown
where rownum = 1;

How to combine return results of query in one row

I have a table that save personnel code.
When I select from this table I get 3 rows result such as:
2129,3394,3508,3534
2129,3508
4056
I want when create select result combine in one row such as:
2129,3394,3508,3534,2129,3508,4056
or distinct value such as:
2129,3394,3508,3534,4056
You should ideally avoid storing CSV data at all in your tables. That being said, for your first result set we can try using STRING_AGG:
SELECT STRING_AGG(col, ',') AS output
FROM yourTable;
Your second requirement is more tricky, and we can try going through a table to remove duplicates:
WITH cte AS (
SELECT DISTINCT VALUE AS col
FROM yourTable t
CROSS APPLY STRING_SPLIT(t.col, ',')
)
SELECT STRING_AGG(col, ',') WITHIN GROUP (ORDER BY CAST(col AS INT)) AS output
FROM cte;
Demo
I solved this by using STUFF and FOR XML PATH:
SELECT
STUFF((SELECT ',' + US.remain_uncompleted
FROM Table_request US
WHERE exclusive = 0 AND reqact = 1 AND reqend = 0
FOR XML PATH('')), 1, 1, '')
Thank you Tim

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

Reshape SQL date

following problem
I am using the command to join Names with project numbers
SELECT DDR_namen.vorname_nachname, DDR_Erfinder_final.pubnr
FROM DDR_namen
RIGHT JOIN DDR_Erfinder_final
ON DDR_namen.vorname_nachname=DDR_Erfinder_final.vorname_nachname
which gives me someting like this (small example)
vorname_nachname Pubnr (ID)
A. Heinrich 100
B. Müller 100
B. Müller 101
B. Müller 105
C. Krüger 120
C. Krüger 100
Now I want to reshape the data, so that the rows are unique and the ID are combined into a new collumn. Like this.
vorname_nachname Pubnr (ID)
A. Heinrich 100
B. Müller 100;101;105
C. Krüger 120;100
Anybody and ideas?
Try grouping like
select vorname_nachname,
(
SELECT STUFF((SELECT ';' + CAST(Pubnr AS VARCHAR(MAX))
FROM TestTable
WHERE vorname_nachname = t1.vorname_nachname
FOR XML PATH('') ), 1, 1, '')
) AS Pubnr
from TestTable t1
group by vorname_nachname
SQL Fiddle Demo : http://sqlfiddle.com/#!3/d0916/1
UPDATE : For JOIN you can use CTE like below :
;WITH CTE AS
(
SELECT DDR_namen.vorname_nachname, DDR_Erfinder_final.pubnr
FROM DDR_namen
RIGHT JOIN DDR_Erfinder_final
ON DDR_namen.vorname_nachname=DDR_Erfinder_final.vorname_nachname
)
select vorname_nachname,
(
SELECT STUFF((SELECT ';' + CAST(Pubnr AS VARCHAR(MAX))
FROM CTE
WHERE vorname_nachname = t1.vorname_nachname
FOR XML PATH('') ), 1, 1, '')
) AS Pubnr
from CTE t1
group by vorname_nachname

How Can I Sort A 'Version Number' Column Generically Using a SQL Server Query

I wonder if the SQL geniuses amongst us could lend me a helping hand.
I have a column VersionNo in a table Versions that contains 'version number' values like
VersionNo
---------
1.2.3.1
1.10.3.1
1.4.7.2
etc.
I am looking to sort this, but unfortunately, when I do a standard order by, it is treated as a string, so the order comes out as
VersionNo
---------
1.10.3.1
1.2.3.1
1.4.7.2
Intead of the following, which is what I am after:
VersionNo
---------
1.2.3.1
1.4.7.2
1.10.3.1
So, what I need to do is to sort by the numbers in reverse order (e.g. in a.b.c.d, I need to sort by d,c,b,a to get the correct sort ourder).
But I am stuck as to how to achieve this in a GENERIC way. Sure, I can split the string up using the various sql functions (e.g. left, right, substring, len, charindex), but I can't guarantee that there will always be 4 parts to the version number. I may have a list like this:
VersionNo
---------
1.2.3.1
1.3
1.4.7.2
1.7.1
1.10.3.1
1.16.8.0.1
Can, does anyone have any suggestions? Your help would be much appreciated.
If You are using SQL Server 2008
select VersionNo from Versions order by cast('/' + replace(VersionNo , '.', '/') + '/' as hierarchyid);
What is hierarchyid
Edit:
Solutions for 2000, 2005, 2008: Solutions to T-SQL Sorting Challenge here.
The challenge
Depending on SQL engine for MySQL would be sth like this:
SELECT versionNo FROM Versions
ORDER BY
SUBSTRING_INDEX(versionNo, '.', 1) + 0,
SUBSTRING_INDEX(SUBSTRING_INDEX(versionNo, '.', -3), '.', 1) + 0,
SUBSTRING_INDEX(SUBSTRING_INDEX(versionNo, '.', -2), '.', 1) + 0,
SUBSTRING_INDEX(versionNo, '.', -1) + 0;
For MySQL version 3.23.15 an above
SELECT versionNo FROM Versions ORDER BY INET_ATON(ip);
Another way to do it:
Assuming you only have a,b,c,d only you may as well separate the data out to columns and do an order by a,b,c,d(all desc) and get the top 1 row
If you need to scale to more than d to say e,f,g... just change 1,2,3,4, to 1,2,3,4,5,6,7 and so on in the query
Query :
see demo
create table t (versionnumber varchar(255))
insert into t values
('1.0.0.505')
,('1.0.0.506')
,('1.0.0.507')
,('1.0.0.508')
,('1.0.0.509')
,('1.0.1.2')
; with cte as
(
select
column1=row_number() over (order by (select NULL)) ,
column2=versionnumber
from t
)
select top 1
CONCAT([1],'.',[2],'.',[3],'.',[4])
from
(
select
t.column1,
split_values=SUBSTRING( t.column2, t1.N, ISNULL(NULLIF(CHARINDEX('.',t.column2,t1.N),0)-t1.N,8000)),
r= row_number() over( partition by column1 order by t1.N)
from cte t
join
(
select
t.column2,
1 as N
from cte t
UNION ALL
select
t.column2,
t1.N + 1 as N
from cte t
join
(
select
top 8000
row_number() over(order by (select NULL)) as N
from
sys.objects s1
cross join
sys.objects s2
) t1
on SUBSTRING(t.column2,t1.N,1) = '.'
) t1
on t1.column2=t.column2
)a
pivot
(
max(split_values) for r in ([1],[2],[3],[4])
)p
order by [1] desc,[2] desc,[3] desc,[4] desc
If you can, alter the schema so that the version has 4 columns instead of one. Then sorting is easy.