SQL concat rows with same ID and newest date - sql

I'm looking for help querying the output of my application's messages. I have a query which takes lines of each message and concats them together to form a sentence. However some of my message numbers have two or more messages, in this case I only want the message with newest date. My current code outputs all messages in a sentence by concatenating line numbers together in order for each message number and outputting them in order by MSG_NUM. I've added ordering by MSG_START_DATE but as expected that still gives me both. How can I do a select max for each concatenated message line?
Here are the fields I'm working with:
MSG_NUM | MSG_START_DATE | MSG_LINE_NUM | MSG_TEXT
1 | 2010-01-15 | 1 | Invalid operation
1 | 2010-01-15 | 2 | try again
1 | 2014-02-21 | 1 | Invalid input
1 | 2014-02-21 | 2 | try again
Here is my current code:
Select distinct ST2.[MSG_NUM],
substring(
(
(Select ' '+LTRIM(RTRIM(ST1.[MSG_TEXT])) AS [text()]
From database..messages ST1
Where ST1.[MSG_NUM] = ST2.[MSG_NUM]
ORDER BY ST1.[MSG_START_DATE], ST1.[MSG_LINE_NUM]
For XML PATH (''),root('xmlchar'), type).value('/xmlchar[1]','varchar(max)')
), 2, 2000) [Message]
From database..messages ST2 order by ST2.[MSG_NUM]
And here is the output I receive:
1 Invalid operation try again Invalid input try again
I only want the output:
1 Invalid input try again
Any ideas on how I can do this?

What about you filtering by date ?
Select distinct ST2.[MSG_NUM],
substring(
(
(Select ' '+LTRIM(RTRIM(ST1.[MSG_LINE_NUM])) AS [text()]
From database..messages ST1
Where ST1.[MSG_NUM] = ST2.[MSG_NUM]
AND ST1.[MSG_START_DATE] = (SELECT MAX(ST3.[MSG_START_DATE])
FROM database..messages ST3
WHERE ST2.[MSG_NUM] = ST3.[MSG_NUM]
)
ORDER BY ST1.[MSG_LINE_NUM]
For XML PATH (''),root('xmlchar'), type).value('/xmlchar[1]','varchar(max)')
), 2, 2000) [Message]
From database..messages ST2 order by ST2.[MSG_NUM]
Better yet, just filter before passing to your query
WITH maxd
AS (
SELECT *
,MAX(MSG_START_DATE) OVER (PARTITION BY [MSG_NUM]) AS maxdate
FROM DATABASE..messages
)
,filter
AS (
SELECT *
FROM maxd
WHERE MSG_START_DATE = maxdate
)
SELECT DISTINCT ST2.[MSG_NUM]
,substring((
(
SELECT ' ' + LTRIM(RTRIM(ST1.[MSG_LINE_NUM])) AS [text()]
FROM filter ST1
WHERE ST1.[MSG_NUM] = ST2.[MSG_NUM]
ORDER BY ST1.[MSG_LINE_NUM]
FOR XML PATH('')
,root('xmlchar')
,type
).value('/xmlchar[1]', 'varchar(max)')
), 2, 2000) [Message]
FROM filter ST2
ORDER BY ST2.[MSG_NUM]

Related

Add column to SQL result with unique rows

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

How to merge rows into one row with separator

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).

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

Remove additional comma without knowing the length of the string

My tables
MyTable
+----+-------+---------------+
| Id | Title | DependencyIds |
+----+-------+---------------+
DependentIds contains values like 14;77;120.
MyDependentTable
+--------------+------+
| DependencyId | Name |
+--------------+------+
Background
I have to select data from MyTable with every dependency from MyDependentTable separated with a comma.
Expected output:
+---------+-------------------------------------+
| Title | Dependencies |
+---------+-------------------------------------+
| Test | ABC, One-two-three, Some Dependency |
+---------+-------------------------------------+
| Example | ABC |
+---------+-------------------------------------+
My query
SELECT t.Title,
(SELECT ISNULL((
SELECT DISTINCT
(
SELECT dt.Name + '',
CASE WHEN DependencyIds LIKE '%;%' THEN ', ' ELSE '' END AS [text()]
FROM MyDependentTable dt
WHERE dt.DependencyId IN (SELECT Value FROM dbo.fSplitIds(t.DependencyIds, ';'))
ORDER BY dt.DependencyId
FOR XML PATH('')
)), '')) Dependencies
FROM dbo.MyTable t
Problem description
The query works, but adds an additional comma when there are multiple dependencies:
+---------+---------------------------------------+
| Title | Dependencies |
+---------+---------------------------------------+
| Test | ABC, One-two-three, Some Dependency, |
+---------+---------------------------------------+
| Example | ABC |
+---------+---------------------------------------+
I can't use SUBSTRING(ISNULL(... because I can't access the length of the string and therefore I'm not able to set the length of the SUBSTRING.
Is there any possibility to get rid of that unnecessary additional comma?
Normally for group concatenation in Sql Server, people will add leading comma and remove it using STUFF function but even that looks ugly.
Outer Apply method looks neat to do this instead of correlated sub-query. In this method we don't have to wrap the SELECT query with ISNULL or STUFF
SELECT DISTINCT t.title,
Isnull(LEFT(dependencies, Len(dependencies) - 1), '')
Dependencies
FROM dbo.mytable t
OUTER apply (SELECT dt.NAME + ','
FROM mydependenttable dt
WHERE dt.dependencyid IN (SELECT value
FROM
dbo.Fsplitids(t.dependencyids,';'))
ORDER BY dt.dependencyid
FOR xml path('')) ou (dependencies)
Here is the method using STUFF.
SELECT t.Title
,STUFF((SELECT ', ' + CAST(dt.Name AS VARCHAR(10)) [text()]
FROM MyDependentTable dt
WHERE dt.DependencyId IN (SELECT Value FROM dbo.fSplitIds(t.DependencyIds, ';'))
ORDER BY dt.DependencyId
FOR XML PATH(''), TYPE).value('.','NVARCHAR(MAX)'),1,2,' ') Dependencies
FROM dbo.MyTable t

Transform two rows in one

We have an auditing system which logs all the changes that occur in all the system tables. Basically, this is how the AuditLog table looks like:
Currently I am creating a couple of sql views to query different kind information. Everything is ok except for one point. If you take a look at the image again, you will see I have a SubscriptionMetadata table which is a key-value pairs table with 2 fields (MetaName and MetaValue). What the immage shows is that the subscription has a new watermark which value is 'Licensed copy: Fullname, Company, V...'.
What I need is transform, in my view, these two rows in just one with the following form:
41 - Insert - SubscriptionMetadata - 2012-10-19 - 53DA4XXXXXX - Watermark - Licensed copy: Fullname, Company, V...
I really cannot imagine how I can do it or search for it neither.
There is another problem (I think), these rows comes always in that order: MetaName first and then MetaValue. That´s the only way to know they are related.
Could you help me, please?
While I cannot see your full table structure you can transform the data the following way. Both of these solutions will place the data in separate columns:
;with data(id, [action], [type], [date], [col], metatype, value) as
(
select 41, 'Insert', 'SubscriptionMetaData', '2012-10-19', '53DA4XXX','Metaname', 'Watermark'
union all
select 41, 'Insert', 'SubscriptionMetaData', '2012-10-19', '53DA4XXX','MetaValue', 'Licensed copy: Fullname, Company'
)
select id, action, type, date, col,
MAX(case when metatype = 'Metaname' then value end) Name,
MAX(case when metatype = 'MetaValue' then value end) Value
from data
group by id, action, type, date, col
See SQL Fiddle with Demo
Or you can use a PIVOT on the data to get the same result:
;with data(id, [action], [type], [date], [col], metatype, value) as
(
select 41, 'Insert', 'SubscriptionMetaData', '2012-10-19', '53DA4XXX','Metaname', 'Watermark'
union all
select 41, 'Insert', 'SubscriptionMetaData', '2012-10-19', '53DA4XXX','MetaValue', 'Licensed copy: Fullname, Company'
)
select *
from
(
select id, [action], [type], [date], [col], metatype, value
from data
) src
pivot
(
max(value)
for metatype in (Metaname, MetaValue)
) piv
See SQL Fiddle with Demo
Both produce the same result:
| ID | ACTION | TYPE | DATE | COL | NAME | VALUE |
-------------------------------------------------------------------------------------------------------------
| 41 | Insert | SubscriptionMetaData | 2012-10-19 | 53DA4XXX | Watermark | Licensed copy: Fullname, Company |
You can do this via a stored procedure or scalar-valued function using the coalesce function as follows:
DECLARE #Results NVARCHAR(MAX);
DECLARE #Token NVARCHAR(5) = '-'; -- separator token
SELECT #Results = coalesce(#Results + #Token,'') + t.MetaValue from (select * from TableName where MetaName = 'SubscriptionMetadata') t;
RETURN #Results; -- variable containing the concatenated values
Here is a working example. Please replace column names as required. Col3 = your string concatenating column.
SELECT t1.col1,t1.col2,
NameValue =REPLACE( (SELECT col3 AS [data()]
FROM mytable t2
WHERE t2.col1 = t1.col1
ORDER BY t2.col1
FOR XML PATH('')
), ' ', ' : ')
FROM mytable t1
GROUP BY col1,col2 ;
--DATA
1 | X | Name
1 | X | Value
--RESULTS
1 | X | Name:Value --Name Value pair here
EDIT: If you don't need concatenation (as per your comment)
SELECT t1.col1, t1.col2, t1.col3 NameColumn, t2.col3 ValueColumn
FROM (SELECT * FROM myTable WHERE col3 = 'Watermark') t1 JOIN
(SELECT * FROM myTable WHERE NOT (col3 = 'Watermark')) t2
ON t1.col1 = t2.col1