Remove additional comma without knowing the length of the string - sql

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

Related

How to search whole words in a string that has delimiter ";"?

I have a column that has values like this 'Blood work;MRI;ICC', which can be a string with some words separated by ';'.
I wonder with a like clause, how can I make a query that returns results that when you search by 'Blood work', 'mri', 'icc' but not by 'blood' or 'mr' or 'ic'?
To search for a field in a CSV list, one method is:
where ';' + mycol + ';' like '%;mri;%'
Demo on DB Fiddle:
with
csv as (select 'Blood work;MRI;ICC' v),
match as (select 'mri' m union all select 'Blood work' union all select 'Blood')
select csv.v, match.m,
case when ';' + csv.v + ';' like '%;' + match.m + ';%'
then 'match'
else 'no match'
end matched
from csv
cross join match
v | m | matched
:----------------- | :--------- | :-------
Blood work;MRI;ICC | mri | match
Blood work;MRI;ICC | Blood work | match
Blood work;MRI;ICC | Blood | no match
I would personally use a string splitter:
SELECT {Columns}
FROM dbo.YourTable YT
CROSS APPLY STRING_SPLIT (YT.YourColumn,';') SS
WHERE SS.[Value] = 'mri';
If you're not using SQL Sevrer 2016+, then you can use a custom splitter, like DelimitedSplit8K_LEAD.

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

Merge multiple rows in a single

I need merge multiple rows in a single row with data concatenated on the columns.
This three lines are result is from my query with INNER JOIN
Name | SC | Type
----------------------
name1 | 121212 | type1
name2 | 123456 | null
name3 | null | type1
I want display result like this:
Name | SC | Type
----------------------
name1; 121212; type1;
name2; 123456; ;
name3; ; type1;
It's a single row, each column with data concatenated with ; and a \n in the end of each data.
The final query need run in SQL Server and Oracle.
I honestly doubt you can use the same query in both oracle and SQL-Server since they both have different functions when it comes to dealing with null values.
For Oracle:
SELECT NVL(Name,'') || ';' as name,
NVL(SC,'') || ';' as SC,
NVL(type,'') || ';' as type
FROM (YourQueryHere)
For SQL-Server
SELECT isnull(Name,'') + ';' as name,
isnull(SC,'') + ';' as SC,
isnull(type,'') + ';' as type
FROM (YourQueryHere)
Note that as #jarlh said, in concatenating side you can use concat(value,value2) which should work both on SQL-Server and Oracle, depending on your version.
You could simply concatenate the fields:
SELECT ISNULL(Name,'') + ';' as Name,
ISNULL(SC, '') + ';' as SC,
ISNULL(Type, '') + ';' as Type
FROM
(
-- whatever is your query goes here...
);

SQL concat rows with same ID and newest date

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]

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