Converting n columns to delimited string from text only subquery - sql-server

This is for Microsoft SQL Server 2016 (SP1) - 13.0.4001.0
I have a rather annoying bit of data that needs to be converted into a comma separated string. My options are limited due to it being a 3rd party software that replaces text in my query and runs it. For example, I will write the following:
SELECT %myvalues% for xml path('')
Which then gets turned into:
SELECT 'test1','test2','test3'...'testn' for xml path('')
Which returns
test1test2test3...testn
This works, but it doesn't separate the text with commas or spaces. Here's the result I want:
test1, test2, test3, ... testn
The problem is, I can't control how it inserts the text. I did find the STUFF function among a bunch of other solutions but none seem to work when I don't know the column names.
For example, I get:
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS.

I have no idea if this is the best way to do it, but I have had success with the following method:
IF OBJECT_ID('mytable', 'U') IS NOT NULL DROP TABLE mytable;
CREATE TABLE mytable ("#remove#'mylist', 'Ofannoying', 'comma', 'seperated values'" INT NULL);
SELECT REPLACE(REPLACE((SELECT c.Name FROM sys.columns c INNER JOIN sys.objects o ON o.object_id = c.object_id WHERE o.name = 'mytable'), '''', ''), '#remove#', '')
Where my actual code looks something like this:
CREATE TABLE mytable ("#remove##myvalues#" INT NULL);

Related

Issue converting SQL column to CSV via XML

I'm replacing a comma-delimited field in a SQL Server reporting application with a many-to-many relationship. The application code can't be replaced just yet, so I need to derive the same CSV column it currently expects. I've used the FOR XML PATH trick in the past, and it seems like a fast set-based solution that should easy to implement.
My current query looks like this:
SELECT
Report = rr.Report_ID,
RoleList = STUFF((SELECT ', ' + r.[Name] AS [text()]
FROM [dbo].[Role] r
WHERE r.Role_ID = rr.Role_ID
FOR XML PATH ('')), 1, 1, '')
FROM
[dbo].ReportRole rr
ORDER BY
rr.Report_ID;
What I expect is this:
Report RoleList
--------------------------------------------------------------------
2 Senior Application Developer
3 Senior Application Developer, Manager Information Systems
But what I get is this instead:
Report RoleList
--------------------------------------
2 Senior Application Developer
3 Senior Application Developer
3 Manager Information Systems
I'm using SQL Server 2017. Does this version not support the XML-to-CSV hack from previous versions?
You've tagged your question with SQL-Server 2017 and you ask:
Does this version not support the XML-to-CSV hack from previous versions?
Yes, it does (as Paurian's answer told you), but it is even better: This version supports STRING_AGG():
Not knowing your tables I set up a mini mockup to simulate your issue (according to your The tables are normalized and the join table is a simple many-to-many ID map):
Two tables with a m:n-mapping table in between
DECLARE #mockupA TABLE(ID INT,SomeValue VARCHAR(10));
INSERT INTO #mockupA VALUES(1,'A1'),(2,'A2');
DECLARE #mockupB TABLE(ID INT,SomeValue VARCHAR(10));
INSERT INTO #mockupB VALUES(1,'B1'),(2,'B2');
DECLARE #mockupMapping TABLE(ID_A INT,ID_B INT);
INSERT INTO #mockupMapping VALUES(1,1),(1,2),(2,2);
--The query will simply join these tables, then use GROUP BY together with STRING_AGG(). The WITHIN GROUP clause allows you to determine the sort order of the concatenated string.
SELECT a.ID,a.SomeValue
,STRING_AGG(b.SomeValue,', ') WITHIN GROUP(ORDER BY b.ID) AS B_Values
FROM #mockupA a
INNER JOIN #mockupMapping m ON a.ID=m.ID_A
INNER JOIN #mockupB b ON b.ID=m.ID_B
GROUP BY a.ID,a.SomeValue;
The result
ID SomeValue B_Values
1 A1 B1, B2
2 A2 B2
Having a table named "ReportRole" indicates a linking table between Report and Role in your case. If that's true, you could make the ReportRole table a part of your inner query and keep your Report IDs in the external query:
SELECT
Report = rpt.Report_ID,
RoleList = STUFF((SELECT ', ' + r.[Name] AS [text()]
FROM [dbo].[Role] r
JOIN [dbo].ReportRole rr
ON r.Role_ID = rr.Role_ID
WHERE rr.Report_ID = rpt.Report_ID
FOR XML PATH ('')), 1, 1, '')
FROM
[dbo].Report rpt
ORDER BY
rpt.Report_ID;

Is it possible to avoid subquery in select when concatenating columns?

I have a "main" table containing an id (plus some other columns) and an aka table which joins to it by the [main id] column to main.id. The following query returns some columns from main along with a column of concatenated comma-separated "lastName"s from aka:
SELECT m.id, m.name,
(SELECT a.[lastname] + ',' AS [text()]
FROM aka a
WHERE a.[main id] = m.[id]
FOR xml path ('')) [akas]
FROM main m
This works fine, but I'd like to know if there is a way to avoid doing this in a subquery?
Using CROSS APPLY you could move subquery from SELECT list:
SELECT m.id, m.name,
(SELECT a.[lastname] + ',' AS [text()]
FROM aka a
WHERE a.[main id] = m.[id]
FOR xml path ('')) [akas]
FROM main m;
to:
SELECT m.id, m.name, s.akas
FROM main m
CROSS APPLY (SELECT a.[lastname] + ',' AS [text()]
FROM aka a
WHERE a.[main id] = m.[id]
FOR xml path ('')) AS s(akas)
Notes:
You could refer to s.akas multiple time
You could add WHERE s.akas ...
Long subquery in SELECT list could be less readable
If it is possbile that correlated subquery return no rows you need to use OUTER APPLY instead.
Generally spoken there's nothing against a sub-query in a technical view...
You might prefer an APPLY due to readability or multi reference.
Whenever you put a sub-query directly into the column's list like here:
SELECT Column1
,Column2
,(SELECT x FROM y) AS Column3
,[...]
... this sub-select must deliver
just one column
of just one row
Using FOR XML PATH(''),TYPE lets the result be one single value of type XML. This makes it possible to return many rows/columns "as one". Without the ,TYPE it will be the XML "as text". The concatenation trick with XML is possible due to a speciality of the generation of XML with empty tag names and return "as text". But in any case: The returned value will be just one bit of information, therefore fitting into a column list.
Whenever you expect more than one row, you'd have to force this to be one bit of data (like - often seen! - SELECT TOP 1 x FROM y ORDER BY SomeSortKey, which brings back the first or the last or ...)
All other intentions to get 1:n data needs 'JOIN' or 'APPLY'. With scalar data, as in your case, there will actually be no difference, whether you use a sub-select or an APPLY.
Since you have an arbitrary number of records combining for the final string, what you have is the best option to do this in SQL. Generally, you are expected to return one row per item, and if you want a CSV string then build that in your client code.

SQL Server : make a select and test every occurrence/row inside another select

I have a SQL Server stored procedure that receives a comma separated string as parameter.
I also have a table-valued function that takes this parameter, splits it (between the commas) and returns as a 'table'.
This procedures is a 'search procedure' that uses LIKE operator to find matching terms.
How can I loop through this parameter that has been transformed into a table and compare it with LIKE?
The sequence that I'd need is something like this:
SQL Server procedure has been called and a separated comma string has been passed as parameter.
A table-valued function gets called to strip this string and transform it in a result table. (It´s not a real table, its just the results). Until here I have already done, the next part is the one I need help:
Loop through this recently created 'table' and search in a specific column of another table.
eg.
SELECT *
FROM tbl_names
WHERE col_names LIKE '%' + (the search term here) + '%'
You can join your table on result of your function:
select * from SomeTable st
join dbo.SomeFunction(#str) sf on st.SomeColumn like '%' + sf.Term +'%'
To order by occurences do something like this:
select * from SomeTable st
join(
select st.ID, count(*) as Occurence from SomeTable st
join dbo.SomeFunction(#str) sf on st.SomeColumn like '%' + sf.Term +'%'
group by st.ID) ot on st.ID = ot.ID
order by ot.Occurence desc
I'd probably use a cross or outer apply with patindex if you want to know how many items matched
select S.*, m.matches
from sometable s
cross apply (select count(1) as matches from finction where patindex ('%' + function.Column + '%', s.coltosearch) > 1) as matched
Use cross apply if you only want to return rows that have matches and outer if you want all rows with a count of terms.
Note: Code example is untested

Need help finding many float value errors in a large SQL statement

I'm having an issue inserting data from one table to another in SSMS 2008 R2. I need to figure out why about 150 columns are failing to convert from nvarchar to float. I'm getting the error:
"Error converting data type nvarchar to float."
Because these are all in a very large select statement I'm not getting details for what the issue is in each column. The method I am trying to use is:
eachcolumnname = convert(float, eachcolumnname)
This is inside an otherwise standard select statement.
Is there a way for me to get some kind of report on all of the issues rather than having to run queries on each line to find all of the issues. These columns with issues are inside a table with another several hundred columns that are ok. This data is from someone outside of my organization and it's in bad shape but I don't have a choice. I need to get is cleaned up and in my database so I can report on it.
Thanks in advance for any advice.
Try this:
SELECT 'PRINT '''
+ sc.Name
+ '''; SELECT MIN(CAST('
+ sc.Name
+ ' AS FLOAT)) FROM SourceTable'
FROM sys.columns sc
JOIN sys.types st ON st.system_type_id = sc.system_type_id
WHERE OBJECT_NAME(Object_ID) = 'TargetTable'
AND st.name = 'FLOAT'
Also, you can use output inserted.* and you will be able to see the row preceding the row with the error.
Like this:
create table #t (a float)
insert into #t
output inserted.*
select a = cast(a as float)
from
(select a = '1' union all select 'a') t
drop table #t
In the Results pane you will see one row with the value 1 and in the Messages pane there will be the conversion error.

How to get comma-separated values in SQL Server?

I have two tables called tblEmployee and tblWorkPlace. The second table consists of multiple rows for each employee. I want to get the result as follows
EmployeeName WorkPlace
Gopi India,Pakistan,...
I.e. if tblWorkPlace consists of multiple rows for an employee, I want the result in a single row, with data being separated by commas.
How will I get this result ????
You'll need to have code on the client side for this. It is possible to make this happen in sql server, but it's not trivial, the performance is horrible, and this kind of thing does not belong on the database anyway.
You're not very clear on how the tables tblWorkplace and tblEmployee are connected - I'm assuming by means of a many-to-many link table or something.
If that's the case, you can use something like this:
SELECT
e.EmpName,
(SELECT STUFF(
(SELECT ',' + w.WPName
FROM #Workplace w
INNER JOIN #WorkplaceEmployeesLink we ON w.WorkplaceID = we.WorkplaceID
WHERE we.EmpID = e.EmpID
FOR XML PATH('')
), 1, 1, '')
) AS Workplaces
FROM #Employee e
(I've replaced your tables with my own table variables #Employee and #Workplace etc. for testing)
This gives me an output something like:
EmpName Workplaces
Gopi India,Pakistan
for that one row.
Basically, the internal FOR XML PATH('') selects the list of workplaces for each employee, and prepends each workplace with a ',' so you get something like ,India,Pakistan.
The STUFF method then stuff an empty string into that resulting string, at position one, for a length of 1 - essentially wiping out the first comma, thus you get the list of workplaces as you desired.
You can assign multiple values to a variable in a select statement. I call this "flattening" the records.
declare #WorkPlace varchar(max)
select #WorkPlace = '' -- it is important to be NOT null
select #WorkPlace = #WorkPlace + WorkPlace + ', '
from YourTable
where EmployeeName = 'Gopi'
You may want to add some code to remove the final ',' from the string.
Here's a link to different ways to get comma separated values.

Resources