How to coalesce many rows into one? - sql-server

I am using SSMS 2008 R2 and am simply trying to coalesce many rows into one. This should be simple I think, but it is currently repeating data in each row. Consider:
create table test
(
Name varchar(30)
)
insert test values('A'),('B'),('C')
select * from test
select distinct Name, coalesce(Name + ', ', '')
from test
How can I rewrite this to achieve one row like:
A,B,C

SELECT STUFF(( SELECT ', ' + Name
from #test
FOR XML PATH(''), TYPE).
value('.','NVARCHAR(MAX)'),1,2,'')
RESULT: A, B, C
I am sure this not how exactly your rows look that you are trying to concatenate, therefore see below for a slightly different data set and how you would go about doing this kind of operation on that
Test Data
create table #test
(
Id INT,
Name varchar(30)
)
insert #test values
(1,'A'),(1,'B'),(1,'C'),(2,'E'),(2,'F'),(2,'G')
Query
select t.Id
, STUFF(( SELECT ', ' + Name
from #test
WHERE Id = T.Id
FOR XML PATH(''), TYPE).
value('.','NVARCHAR(MAX)'),1,2,'') AS List
FROM #test t
GROUP BY t.Id
Result Set
╔════╦═════════╗
║ Id ║ List ║
╠════╬═════════╣
║ 1 ║ A, B, C ║
║ 2 ║ E, F, G ║
╚════╩═════════╝

In SQL Server Transact-SQL, the easiest way to accomplish this is the following.
A table like this:
create table #foo
(
id int not null identity(1,1) primary key clustered ,
name varchar(32) not null ,
)
insert #foo (name) values ( 'a' )
insert #foo (name) values ( 'b' )
insert #foo (name) values ( 'c' )
insert #foo (name) values ( 'd' )
go
Can flattened using this seemingly dubious (but documented) technique:
declare #text varchar(max) = ''
select #text = #text
+ case len(#text)
when 0 then ''
else ','
end
+ t.name
from #foo t
select list_of_names = #text
go
yielding
list_of_names
-------------
a,b,c,d
Easy!

in the old days of SQL Server 7.0 and SQL Server 2000, I Used to do this with a variable and using COALESCE like this:
DECLARE #List VARCHAR(8000)
SELECT #List = COALESCE(#List + ',', '') + CAST(Color AS VARCHAR)
FROM NameColorTable
SELECT #List
But after SQL Server 2005 and XPATH appearance, this the way I prefer to use:
SELECT STUFF((
select ','+ cast(Color as nvarchar(255))
from NameColorTable b
WHERE a.Name = b.Name
FOR XML PATH('')
)
,1,1,'') AS COLUMN2
FROM NameColorTable a
GROUP BY a.Name
I have a blog post about this here:
https://koukia.ca/stuff-vs-coalesce-in-tsql-for-concatenating-row-values-aefb078536f8#.f4iggl22y

Here is the standard solution for concatenation using XML PATH()
SELECT
STUFF(
(
SELECT
',' + Name
FROM test
FOR XML PATH(''),TYPE
).value('.','VARCHAR(MAX)'
), 1, 1, ''
) As list
Another option can be using the new SQL CONCAT() function introduced with SQL Server 2012.
I used SQL Concat() in the below sample
declare #namelist nvarchar(max)
select #namelist = concat(isnull(#namelist+',',''), name) from test2
select #namelist

Related

SQL Server extract data from XML column without tag names

I have an XML string:
<XML>
<xml_line>
<col1>1</col1>
<col2>foo 1</col2>
</xml_line>
<xml_line>
<col1>2</col1>
<col2>foo 2</col2>
</xml_line>
</XML>
I am extracting data from that string (stored in #data_xml) by storing it in SQL Server table and parsing it:
-- create temp table, insert XML string
CREATE TABLE table1 (data_xml XML)
INSERT table1
SELECT #data_xml
-- parse XML string into temp table
SELECT
N.C.value('col1[1]', 'int') col1_name,
N.C.value('col2[1]', 'varchar(31)') col2_name,
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
I would like to know if there is a generic way to accomplish the same without specifying column names (i.e. col1[1], col2[1])
You can use something like:
SELECT
N.C.value('let $i := . return count(//xml_line[. << $i]) + 1', 'int') as LineNumber,
Item.Node.value('local-name(.)', 'varchar(max)') name,
Item.Node.value('.', 'varchar(max)') value
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
CROSS APPLY
N.C.nodes('*') Item(Node)
To get:
LineNumber
name
value
1
col1
1
1
col2
foo 1
2
col1
2
2
col2
foo 2
See this db<>fiddle.
However, to spread columns horizontally, you will need to generate dynamic SQL after querying for distinct element names.
ADDENDUM: Here is an updated db<>fiddle that also shows a dynamic SQL example.
The above maps all values as VARCHAR(MAX). If you have NVARCHAR data you can make the appropriate changes. If you have a need to map specific columns to specific types, you will need to explicitly define and populate a name-to-type mapping table and incorporate that into the dynamic SQL logic. The same may be necessary if you prefer that the result columns be in a specific order.
ADDENDUM 2: This updated db<>fiddle now includes column type and ordering logic.
--------------------------------------------------
-- Extract column names
--------------------------------------------------
DECLARE #Names TABLE (name VARCHAR(100))
INSERT #Names
SELECT DISTINCT Item.Node.value('local-name(.)', 'varchar(max)')
FROM table1
CROSS APPLY data_xml.nodes('//xml_line/*') Item(Node)
--SELECT * FROM #Names
--------------------------------------------------
-- Define column-to-type mapping
--------------------------------------------------
DECLARE #ColumnTypeMap TABLE ( ColumnName SYSNAME, ColumnType SYSNAME, ColumnOrder INT)
INSERT #ColumnTypeMap
VALUES
('col1', 'int', 1),
('col2', 'varchar(10)', 2)
DECLARE #ColumnTypeDefault SYSNAME = 'varchar(max)'
--------------------------------------------------
-- Define SQL Templates
--------------------------------------------------
DECLARE #SelectItemTemplate VARCHAR(MAX) =
' , N.C.value(<colpath>, <coltype>) <colname>
'
DECLARE #SqlTemplate VARCHAR(MAX) =
'SELECT
N.C.value(''let $i := . return count(//xml_line[. << $i]) + 1'', ''int'') as LineNumber
<SelectItems>
FROM
table1
CROSS APPLY
data_xml.nodes(''//xml_line'') N(C)
'
--------------------------------------------------
-- Expand SQL templates into SQL
--------------------------------------------------
DECLARE #SelectItems VARCHAR(MAX) = (
SELECT STRING_AGG(SI.SelectItem, '')
WITHIN GROUP(ORDER BY ISNULL(T.ColumnOrder, 999), N.Name)
FROM #Names N
LEFT JOIN #ColumnTypeMap T ON T.ColumnName = N.name
CROSS APPLY (
SELECT SelectItem = REPLACE(REPLACE(REPLACE(
#SelectItemTemplate
, '<colpath>', QUOTENAME(N.name + '[1]', ''''))
, '<colname>', QUOTENAME(N.name))
, '<coltype>', QUOTENAME(ISNULL(T.ColumnType, #ColumnTypeDefault), ''''))
) SI(SelectItem)
)
DECLARE #Sql VARCHAR(MAX) = REPLACE(#SqlTemplate, '<SelectItems>', #SelectItems)
--------------------------------------------------
-- Execute
--------------------------------------------------
SELECT DynamicSql = #Sql
EXEC (#Sql)
Result (with some additional data):
LineNumber
col1
col2
bar
foo
1
1
foo 1
null
More
2
2
foo 2
Stuff
null

Separated by Comma in SQL Server [duplicate]

Consider a database table holding names, with three rows:
Peter
Paul
Mary
Is there an easy way to turn this into a single string of Peter, Paul, Mary?
If you are on SQL Server 2017 or Azure, see Mathieu Renda answer.
I had a similar issue when I was trying to join two tables with one-to-many relationships. In SQL 2005 I found that XML PATH method can handle the concatenation of the rows very easily.
If there is a table called STUDENTS
SubjectID StudentName
---------- -------------
1 Mary
1 John
1 Sam
2 Alaina
2 Edward
Result I expected was:
SubjectID StudentName
---------- -------------
1 Mary, John, Sam
2 Alaina, Edward
I used the following T-SQL:
SELECT Main.SubjectID,
LEFT(Main.Students,Len(Main.Students)-1) As "Students"
FROM
(
SELECT DISTINCT ST2.SubjectID,
(
SELECT ST1.StudentName + ',' AS [text()]
FROM dbo.Students ST1
WHERE ST1.SubjectID = ST2.SubjectID
ORDER BY ST1.SubjectID
FOR XML PATH (''), TYPE
).value('text()[1]','nvarchar(max)') [Students]
FROM dbo.Students ST2
) [Main]
You can do the same thing in a more compact way if you can concat the commas at the beginning and use substring to skip the first one so you don't need to do a sub-query:
SELECT DISTINCT ST2.SubjectID,
SUBSTRING(
(
SELECT ','+ST1.StudentName AS [text()]
FROM dbo.Students ST1
WHERE ST1.SubjectID = ST2.SubjectID
ORDER BY ST1.SubjectID
FOR XML PATH (''), TYPE
).value('text()[1]','nvarchar(max)'), 2, 1000) [Students]
FROM dbo.Students ST2
This answer may return unexpected results For consistent results, use one of the FOR XML PATH methods detailed in other answers.
Use COALESCE:
DECLARE #Names VARCHAR(8000)
SELECT #Names = COALESCE(#Names + ', ', '') + Name
FROM People
Just some explanation (since this answer seems to get relatively regular views):
Coalesce is really just a helpful cheat that accomplishes two things:
1) No need to initialize #Names with an empty string value.
2) No need to strip off an extra separator at the end.
The solution above will give incorrect results if a row has a NULL Name value (if there is a NULL, the NULL will make #Names NULL after that row, and the next row will start over as an empty string again. Easily fixed with one of two solutions:
DECLARE #Names VARCHAR(8000)
SELECT #Names = COALESCE(#Names + ', ', '') + Name
FROM People
WHERE Name IS NOT NULL
or:
DECLARE #Names VARCHAR(8000)
SELECT #Names = COALESCE(#Names + ', ', '') +
ISNULL(Name, 'N/A')
FROM People
Depending on what behavior you want (the first option just filters NULLs out, the second option keeps them in the list with a marker message [replace 'N/A' with whatever is appropriate for you]).
SQL Server 2017+ and SQL Azure: STRING_AGG
Starting with the next version of SQL Server, we can finally concatenate across rows without having to resort to any variable or XML witchery.
STRING_AGG (Transact-SQL)
Without grouping
SELECT STRING_AGG(Name, ', ') AS Departments
FROM HumanResources.Department;
With grouping:
SELECT GroupName, STRING_AGG(Name, ', ') AS Departments
FROM HumanResources.Department
GROUP BY GroupName;
With grouping and sub-sorting
SELECT GroupName, STRING_AGG(Name, ', ') WITHIN GROUP (ORDER BY Name ASC) AS Departments
FROM HumanResources.Department
GROUP BY GroupName;
One method not yet shown via the XML data() command in SQL Server is:
Assume a table called NameList with one column called FName,
SELECT FName + ', ' AS 'data()'
FROM NameList
FOR XML PATH('')
returns:
"Peter, Paul, Mary, "
Only the extra comma must be dealt with.
As adopted from #NReilingh's comment, you can use the following method to remove the trailing comma. Assuming the same table and column names:
STUFF(REPLACE((SELECT '#!' + LTRIM(RTRIM(FName)) AS 'data()' FROM NameList
FOR XML PATH('')),' #!',', '), 1, 2, '') as Brands
In SQL Server 2005
SELECT Stuff(
(SELECT N', ' + Name FROM Names FOR XML PATH(''),TYPE)
.value('text()[1]','nvarchar(max)'),1,2,N'')
In SQL Server 2016
you can use the FOR JSON syntax
i.e.
SELECT per.ID,
Emails = JSON_VALUE(
REPLACE(
(SELECT _ = em.Email FROM Email em WHERE em.Person = per.ID FOR JSON PATH)
,'"},{"_":"',', '),'$[0]._'
)
FROM Person per
And the result will become
Id Emails
1 abc#gmail.com
2 NULL
3 def#gmail.com, xyz#gmail.com
This will work even your data contains invalid XML characters
the '"},{"_":"' is safe because if you data contain '"},{"_":"', it will be escaped to "},{\"_\":\"
You can replace ', ' with any string separator
And in SQL Server 2017, Azure SQL Database
You can use the new STRING_AGG function
In MySQL, there is a function, GROUP_CONCAT(), which allows you to concatenate the values from multiple rows. Example:
SELECT 1 AS a, GROUP_CONCAT(name ORDER BY name ASC SEPARATOR ', ') AS people
FROM users
WHERE id IN (1,2,3)
GROUP BY a
Use COALESCE - Learn more from here
For an example:
102
103
104
Then write the below code in SQL Server,
Declare #Numbers AS Nvarchar(MAX) -- It must not be MAX if you have few numbers
SELECT #Numbers = COALESCE(#Numbers + ',', '') + Number
FROM TableName where Number IS NOT NULL
SELECT #Numbers
The output would be:
102,103,104
PostgreSQL arrays are awesome. Example:
Create some test data:
postgres=# \c test
You are now connected to database "test" as user "hgimenez".
test=# create table names (name text);
CREATE TABLE
test=# insert into names (name) values ('Peter'), ('Paul'), ('Mary');
INSERT 0 3
test=# select * from names;
name
-------
Peter
Paul
Mary
(3 rows)
Aggregate them in an array:
test=# select array_agg(name) from names;
array_agg
-------------------
{Peter,Paul,Mary}
(1 row)
Convert the array to a comma-delimited string:
test=# select array_to_string(array_agg(name), ', ') from names;
array_to_string
-------------------
Peter, Paul, Mary
(1 row)
DONE
Since PostgreSQL 9.0 it is even easier, quoting from deleted answer by "horse with no name":
select string_agg(name, ',')
from names;
Oracle 11g Release 2 supports the LISTAGG function. Documentation here.
COLUMN employees FORMAT A50
SELECT deptno, LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename) AS employees
FROM emp
GROUP BY deptno;
DEPTNO EMPLOYEES
---------- --------------------------------------------------
10 CLARK,KING,MILLER
20 ADAMS,FORD,JONES,SCOTT,SMITH
30 ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD
3 rows selected.
Warning
Be careful implementing this function if there is possibility of the resulting string going over 4000 characters. It will throw an exception. If that's the case then you need to either handle the exception or roll your own function that prevents the joined string from going over 4000 characters.
In SQL Server 2005 and later, use the query below to concatenate the rows.
DECLARE #t table
(
Id int,
Name varchar(10)
)
INSERT INTO #t
SELECT 1,'a' UNION ALL
SELECT 1,'b' UNION ALL
SELECT 2,'c' UNION ALL
SELECT 2,'d'
SELECT ID,
stuff(
(
SELECT ','+ [Name] FROM #t WHERE Id = t.Id FOR XML PATH('')
),1,1,'')
FROM (SELECT DISTINCT ID FROM #t ) t
A recursive CTE solution was suggested, but no code was provided. The code below is an example of a recursive CTE.
Note that although the results match the question, the data doesn't quite match the given description, as I assume that you really want to be doing this on groups of rows, not all rows in the table. Changing it to match all rows in the table is left as an exercise for the reader.
;WITH basetable AS (
SELECT
id,
CAST(name AS VARCHAR(MAX)) name,
ROW_NUMBER() OVER (Partition BY id ORDER BY seq) rw,
COUNT(*) OVER (Partition BY id) recs
FROM (VALUES
(1, 'Johnny', 1),
(1, 'M', 2),
(2, 'Bill', 1),
(2, 'S.', 4),
(2, 'Preston', 5),
(2, 'Esq.', 6),
(3, 'Ted', 1),
(3, 'Theodore', 2),
(3, 'Logan', 3),
(4, 'Peter', 1),
(4, 'Paul', 2),
(4, 'Mary', 3)
) g (id, name, seq)
),
rCTE AS (
SELECT recs, id, name, rw
FROM basetable
WHERE rw = 1
UNION ALL
SELECT b.recs, r.ID, r.name +', '+ b.name name, r.rw + 1
FROM basetable b
INNER JOIN rCTE r ON b.id = r.id AND b.rw = r.rw + 1
)
SELECT name
FROM rCTE
WHERE recs = rw AND ID=4
OPTION (MAXRECURSION 101)
I don't have access to a SQL Server at home, so I'm guess at the syntax here, but it's more or less:
DECLARE #names VARCHAR(500)
SELECT #names = #names + ' ' + Name
FROM Names
In SQL Server 2017 or later versions, you can use the STRING_AGG() function to generate comma-separated values. Please have a look below at one example.
SELECT
VendorId, STRING_AGG(FirstName,',') UsersName
FROM Users
WHERE VendorId != 9
GROUP BY VendorId
You need to create a variable that will hold your final result and select into it, like so.
Easiest Solution
DECLARE #char VARCHAR(MAX);
SELECT #char = COALESCE(#char + ', ' + [column], [column])
FROM [table];
PRINT #char;
In SQL Server vNext this will be built in with the STRING_AGG function. Read more about it in STRING_AGG (Transact-SQL).
A ready-to-use solution, with no extra commas:
select substring(
(select ', '+Name AS 'data()' from Names for xml path(''))
,3, 255) as "MyList"
An empty list will result in NULL value.
Usually you will insert the list into a table column or program variable: adjust the 255 max length to your need.
(Diwakar and Jens Frandsen provided good answers, but need improvement.)
This worked for me (SQL Server 2016):
SELECT CarNamesString = STUFF((
SELECT ',' + [Name]
FROM tbl_cars
FOR XML PATH('')
), 1, 1, '')
Here is the source: https://www.mytecbits.com/
And a solution for MySQL (since this page show up in Google for MySQL):
SELECT [Name],
GROUP_CONCAT(DISTINCT [Name] SEPARATOR ',')
FROM tbl_cars
From MySQL documentation.
Using XML helped me in getting rows separated with commas. For the extra comma we can use the replace function of SQL Server. Instead of adding a comma, use of the AS 'data()' will concatenate the rows with spaces, which later can be replaced with commas as the syntax written below.
REPLACE(
(select FName AS 'data()' from NameList for xml path(''))
, ' ', ', ')
SELECT STUFF((SELECT ', ' + name FROM [table] FOR XML PATH('')), 1, 2, '')
Here's a sample:
DECLARE #t TABLE (name VARCHAR(10))
INSERT INTO #t VALUES ('Peter'), ('Paul'), ('Mary')
SELECT STUFF((SELECT ', ' + name FROM #t FOR XML PATH('')), 1, 2, '')
--Peter, Paul, Mary
With the other answers, the person reading the answer must be aware of a specific domain table such as vehicle or student. The table must be created and populated with data to test a solution.
Below is an example that uses SQL Server "Information_Schema.Columns" table. By using this solution, no tables need to be created or data added. This example creates a comma separated list of column names for all tables in the database.
SELECT
Table_Name
,STUFF((
SELECT ',' + Column_Name
FROM INFORMATION_SCHEMA.Columns Columns
WHERE Tables.Table_Name = Columns.Table_Name
ORDER BY Column_Name
FOR XML PATH ('')), 1, 1, ''
)Columns
FROM INFORMATION_SCHEMA.Columns Tables
GROUP BY TABLE_NAME
On top of Chris Shaffer's answer:
If your data may get repeated, such as
Tom
Ali
John
Ali
Tom
Mike
Instead of having Tom,Ali,John,Ali,Tom,Mike
You can use DISTINCT to avoid duplicates and get Tom,Ali,John,Mike:
DECLARE #Names VARCHAR(8000)
SELECT DISTINCT #Names = COALESCE(#Names + ',', '') + Name
FROM People
WHERE Name IS NOT NULL
SELECT #Names
MySQL complete example:
We have users who can have much data and we want to have an output, where we can see all users' data in a list:
Result:
___________________________
| id | rowList |
|-------------------------|
| 0 | 6, 9 |
| 1 | 1,2,3,4,5,7,8,1 |
|_________________________|
Table Setup:
CREATE TABLE `Data` (
`id` int(11) NOT NULL,
`user_id` int(11) NOT NULL
) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=latin1;
INSERT INTO `Data` (`id`, `user_id`) VALUES
(1, 1),
(2, 1),
(3, 1),
(4, 1),
(5, 1),
(6, 0),
(7, 1),
(8, 1),
(9, 0),
(10, 1);
CREATE TABLE `User` (
`id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `User` (`id`) VALUES
(0),
(1);
Query:
SELECT User.id, GROUP_CONCAT(Data.id ORDER BY Data.id) AS rowList FROM User LEFT JOIN Data ON User.id = Data.user_id GROUP BY User.id
DECLARE #Names VARCHAR(8000)
SELECT #name = ''
SELECT #Names = #Names + ',' + Names FROM People
SELECT SUBSTRING(2, #Names, 7998)
This puts the stray comma at the beginning.
However, if you need other columns, or to CSV a child table you need to wrap this in a scalar user defined field (UDF).
You can use XML path as a correlated subquery in the SELECT clause too (but I'd have to wait until I go back to work because Google doesn't do work stuff at home :-)
To avoid null values you can use CONCAT()
DECLARE #names VARCHAR(500)
SELECT #names = CONCAT(#names, ' ', name)
FROM Names
select #names
I really liked elegancy of Dana's answer and just wanted to make it complete.
DECLARE #names VARCHAR(MAX)
SET #names = ''
SELECT #names = #names + ', ' + Name FROM Names
-- Deleting last two symbols (', ')
SET #sSql = LEFT(#sSql, LEN(#sSql) - 1)
If you want to deal with nulls you can do it by adding a where clause or add another COALESCE around the first one.
DECLARE #Names VARCHAR(8000)
SELECT #Names = COALESCE(COALESCE(#Names + ', ', '') + Name, #Names) FROM People
This answer will require some privilege on the server to work.
Assemblies are a good option for you. There are a lot of sites that explain how to create it. The one I think is very well explained is this one.
If you want, I have already created the assembly, and it is possible to download the DLL file here.
Once you have downloaded it, you will need to run the following script in your SQL Server:
EXEC sp_configure 'show advanced options', 1
RECONFIGURE;
EXEC sp_configure 'clr strict security', 1;
RECONFIGURE;
CREATE Assembly concat_assembly
AUTHORIZATION dbo
FROM '<PATH TO Concat.dll IN SERVER>'
WITH PERMISSION_SET = SAFE;
GO
CREATE AGGREGATE dbo.concat (
#Value NVARCHAR(MAX)
, #Delimiter NVARCHAR(4000)
) RETURNS NVARCHAR(MAX)
EXTERNAL Name concat_assembly.[Concat.Concat];
GO
sp_configure 'clr enabled', 1;
RECONFIGURE
Observe that the path to assembly may be accessible to server. Since you have successfully done all the steps, you can use the function like:
SELECT dbo.Concat(field1, ',')
FROM Table1
Since SQL Server 2017 it is possible to use the STRING_AGG function.
I usually use select like this to concatenate strings in SQL Server:
with lines as
(
select
row_number() over(order by id) id, -- id is a line id
line -- line of text.
from
source -- line source
),
result_lines as
(
select
id,
cast(line as nvarchar(max)) line
from
lines
where
id = 1
union all
select
l.id,
cast(r.line + N', ' + l.line as nvarchar(max))
from
lines l
inner join
result_lines r
on
l.id = r.id + 1
)
select top 1
line
from
result_lines
order by
id desc
In Oracle, it is wm_concat. I believe this function is available in the 10g release and higher.
For Oracle DBs, see this question: How can multiple rows be concatenated into one in Oracle without creating a stored procedure?
The best answer appears to be by #Emmanuel, using the built-in LISTAGG() function, available in Oracle 11g Release 2 and later.
SELECT question_id,
LISTAGG(element_id, ',') WITHIN GROUP (ORDER BY element_id)
FROM YOUR_TABLE;
GROUP BY question_id
as #user762952 pointed out, and according to Oracle's documentation http://www.oracle-base.com/articles/misc/string-aggregation-techniques.php, the WM_CONCAT() function is also an option. It seems stable, but Oracle explicitly recommends against using it for any application SQL, so use at your own risk.
Other than that, you will have to write your own function; the Oracle document above has a guide on how to do that.

Based on a variable: SQL Query to fetch data for multiple values from one column OR get all values from that column

Problem Statement :
when #a has a single word(Ex. 'name1') OR comma separated string (Example 'name1,name2,name3') then the query should return the manager names of employees with name1 and name2 and name3
when #a has an empty string then return the manager names of all the employees in the emp_master table
I have defined a stored procedure where I pass a variable.
This variable can be a comma separated string, a single word or an empty string.
If the string is comma separated then I split that string and get values based on the return table of split statement
else
I get the related value of the non comma separated data using normal subquery
I have tried to achieve this in the following way
Declare #a varchar(50)= ''
select emp.Name from
emp_master emp
where
(LEN(#a)=0 AND emp.Name in
(
SELECT DISTINCT [Name] FROM
[dbo].[Emp_Master] WHERE [EmpId] IN
(
SELECT
DISTINCT [MGR_ID]
FROM [dbo].[Emp_Master]
)
)
)
OR
emp.Name in (Select * from [dbo].[SplitString](#a, ','))
Details for the above sample:
[dbo].[SplitString] - custom written function : returns a table of split values. So
Select * from [dbo].SplitString
will return
SplitTable
----------
name1
name2
name3
and
Select * from [dbo].[SplitString](',','name1')
will return
SplitTable
----------
name1
[dbo].[Emp_Master] contains data for all the employees
[MGR_ID] is the column which has the employeeID of the employee manager
#a is the input variable
The Database is MS SQL 2008
My current solution(the above insane query) solves my purpose but it is very slow, it would be helpful to get an optimized and faster working solution for the problem
Emp_master Table has 400 000 rows, 30 columns
There are 18 000 managers in that table
CREATE NONCLUSTERED INDEX ix ON dbo.Emp_Master ([MGR_ID])
GO
DECLARE #a VARCHAR(50) = ''
DECLARE #t TABLE (val VARCHAR(50) PRIMARY KEY WITH(IGNORE_DUP_KEY=ON))
INSERT INTO #t
SELECT item = t.c.value('.', 'INT')
FROM (
SELECT txml = CAST('<r>' + REPLACE(#a, ',', '</r><r>') + '</r>' AS XML)
) r
CROSS APPLY txml.nodes('/r') t(c)
SELECT /*DISTINCT*/ [Name]
FROM dbo.Emp_Master e1
WHERE (
#a = ''
AND
e1.[EmpId] IN (SELECT DISTINCT MGR_ID FROM dbo.Emp_Master)
)
OR (
#a != ''
AND
e.Name IN (SELECT * FROM #t)
)
OPTION(RECOMPILE)
TRY THIS
CREATE NONCLUSTERED INDEX IX_MGR_ID_Emp_Master ON dbo.Emp_Master ([MGR_ID])
GO
Create Procedure searchname (#a varchar(255))
as
IF (#a = '')
BEGIN
EXEC Searchname1 #a
END
ELSE
BEGIN
EXEC Searchname2 #a
END
GO
Create Procedure Searchname1 (#a varchar(255))
AS
SELECT DISTINCT [Name] FROM
[dbo].[Emp_Master] m1 WHERE
exists
(
SELECT
*
FROM [dbo].[Emp_Master] m2
WHERE
m1.[EmpId]= m2.[MGR_ID]
)
GO
Create Procedure Searchname2 (#a varchar(max))
AS
Select #a = ' SELECT '''+replace( #a,',',''' Union ALL SELECT ''')+' '''
Create table #names (name varchar(255))
insert into #names
EXEC ( #a )
select emp.Name from
emp_master emp
WHERE
emp.Name in( Select name FRom #names)
option (recompile)
IF YOU ARE ALREADY DEALING WITH SQL INJECTION AT APPLICATION LEVEL
THEN
ALTER procedure [dbo].[Searchname2] (#a varchar(max))
AS
select #a = ''''+replace ( #a,',',''',''')+''''
DECLARE #sql NVARCHAR(MAX) = N'
select distinct emp.Name from
emp_master emp
WHERE
emp.Name in( '+#a+')'
EXEC (#sql)

SQL Server group by count eliminate duplicates [duplicate]

How do I get:
id Name Value
1 A 4
1 B 8
2 C 9
to
id Column
1 A:4, B:8
2 C:9
No CURSOR, WHILE loop, or User-Defined Function needed.
Just need to be creative with FOR XML and PATH.
[Note: This solution only works on SQL 2005 and later. Original question didn't specify the version in use.]
CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)
SELECT
[ID],
STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
FROM #YourTable
WHERE (ID = Results.ID)
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID
DROP TABLE #YourTable
If it is SQL Server 2017 or SQL Server Vnext, SQL Azure you can use STRING_AGG as below:
SELECT id, STRING_AGG(CONCAT(name, ':', [value]), ', ')
FROM #YourTable
GROUP BY id
using XML path will not perfectly concatenate as you might expect... it will replace "&" with "&" and will also mess with <" and ">
...maybe a few other things, not sure...but you can try this
I came across a workaround for this... you need to replace:
FOR XML PATH('')
)
with:
FOR XML PATH(''),TYPE
).value('(./text())[1]','VARCHAR(MAX)')
...or NVARCHAR(MAX) if thats what youre using.
why the hell doesn't SQL have a concatenate aggregate function? this is a PITA.
I ran into a couple of problems when I tried converting Kevin Fairchild's suggestion to work with strings containing spaces and special XML characters (&, <, >) which were encoded.
The final version of my code (which doesn't answer the original question but may be useful to someone) looks like this:
CREATE TABLE #YourTable ([ID] INT, [Name] VARCHAR(MAX), [Value] INT)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'Oranges & Lemons',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'1 < 2',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)
SELECT [ID],
STUFF((
SELECT ', ' + CAST([Name] AS VARCHAR(MAX))
FROM #YourTable WHERE (ID = Results.ID)
FOR XML PATH(''),TYPE
/* Use .value to uncomment XML entities e.g. > < etc*/
).value('.','VARCHAR(MAX)')
,1,2,'') as NameValues
FROM #YourTable Results
GROUP BY ID
DROP TABLE #YourTable
Rather than using a space as a delimiter and replacing all the spaces with commas, it just pre-pends a comma and space to each value then uses STUFF to remove the first two characters.
The XML encoding is taken care of automatically by using the TYPE directive.
Another option using Sql Server 2005 and above
---- test data
declare #t table (OUTPUTID int, SCHME varchar(10), DESCR varchar(10))
insert #t select 1125439 ,'CKT','Approved'
insert #t select 1125439 ,'RENO','Approved'
insert #t select 1134691 ,'CKT','Approved'
insert #t select 1134691 ,'RENO','Approved'
insert #t select 1134691 ,'pn','Approved'
---- actual query
;with cte(outputid,combined,rn)
as
(
select outputid, SCHME + ' ('+DESCR+')', rn=ROW_NUMBER() over (PARTITION by outputid order by schme, descr)
from #t
)
,cte2(outputid,finalstatus,rn)
as
(
select OUTPUTID, convert(varchar(max),combined), 1 from cte where rn=1
union all
select cte2.outputid, convert(varchar(max),cte2.finalstatus+', '+cte.combined), cte2.rn+1
from cte2
inner join cte on cte.OUTPUTID = cte2.outputid and cte.rn=cte2.rn+1
)
select outputid, MAX(finalstatus) from cte2 group by outputid
Install the SQLCLR Aggregates from http://groupconcat.codeplex.com
Then you can write code like this to get the result you asked for:
CREATE TABLE foo
(
id INT,
name CHAR(1),
Value CHAR(1)
);
INSERT INTO dbo.foo
(id, name, Value)
VALUES (1, 'A', '4'),
(1, 'B', '8'),
(2, 'C', '9');
SELECT id,
dbo.GROUP_CONCAT(name + ':' + Value) AS [Column]
FROM dbo.foo
GROUP BY id;
Eight years later... Microsoft SQL Server vNext Database Engine has finally enhanced Transact-SQL to directly support grouped string concatenation. The Community Technical Preview version 1.0 added the STRING_AGG function and CTP 1.1 added the WITHIN GROUP clause for the STRING_AGG function.
Reference: https://msdn.microsoft.com/en-us/library/mt775028.aspx
SQL Server 2005 and later allow you to create your own custom aggregate functions, including for things like concatenation- see the sample at the bottom of the linked article.
This is just an addition to Kevin Fairchild's post (very clever by the way). I would have added it as a comment, but I don't have enough points yet :)
I was using this idea for a view I was working on, however the items I was concatinating contained spaces. So I modified the code slightly to not use spaces as delimiters.
Again thanks for the cool workaround Kevin!
CREATE TABLE #YourTable ( [ID] INT, [Name] CHAR(1), [Value] INT )
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'A', 4)
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (1, 'B', 8)
INSERT INTO #YourTable ([ID], [Name], [Value]) VALUES (2, 'C', 9)
SELECT [ID],
REPLACE(REPLACE(REPLACE(
(SELECT [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) as A
FROM #YourTable
WHERE ( ID = Results.ID )
FOR XML PATH (''))
, '</A><A>', ', ')
,'<A>','')
,'</A>','') AS NameValues
FROM #YourTable Results
GROUP BY ID
DROP TABLE #YourTable
An example would be
In Oracle you can use LISTAGG aggregate function.
Original records
name type
------------
name1 type1
name2 type2
name2 type3
Sql
SELECT name, LISTAGG(type, '; ') WITHIN GROUP(ORDER BY name)
FROM table
GROUP BY name
Result in
name type
------------
name1 type1
name2 type2; type3
This kind of question is asked here very often, and the solution is going to depend a lot on the underlying requirements:
https://stackoverflow.com/search?q=sql+pivot
and
https://stackoverflow.com/search?q=sql+concatenate
Typically, there is no SQL-only way to do this without either dynamic sql, a user-defined function, or a cursor.
Just to add to what Cade said, this is usually a front-end display thing and should therefore be handled there. I know that sometimes it's easier to write something 100% in SQL for things like file export or other "SQL only" solutions, but most of the times this concatenation should be handled in your display layer.
Don't need a cursor... a while loop is sufficient.
------------------------------
-- Setup
------------------------------
DECLARE #Source TABLE
(
id int,
Name varchar(30),
Value int
)
DECLARE #Target TABLE
(
id int,
Result varchar(max)
)
INSERT INTO #Source(id, Name, Value) SELECT 1, 'A', 4
INSERT INTO #Source(id, Name, Value) SELECT 1, 'B', 8
INSERT INTO #Source(id, Name, Value) SELECT 2, 'C', 9
------------------------------
-- Technique
------------------------------
INSERT INTO #Target (id)
SELECT id
FROM #Source
GROUP BY id
DECLARE #id int, #Result varchar(max)
SET #id = (SELECT MIN(id) FROM #Target)
WHILE #id is not null
BEGIN
SET #Result = null
SELECT #Result =
CASE
WHEN #Result is null
THEN ''
ELSE #Result + ', '
END + s.Name + ':' + convert(varchar(30),s.Value)
FROM #Source s
WHERE id = #id
UPDATE #Target
SET Result = #Result
WHERE id = #id
SET #id = (SELECT MIN(id) FROM #Target WHERE #id < id)
END
SELECT *
FROM #Target
Let's get very simple:
SELECT stuff(
(
select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb
FOR XML PATH('')
)
, 1, 2, '')
Replace this line:
select ', ' + x from (SELECT 'xxx' x union select 'yyyy') tb
With your query.
You can improve performance significant the following way if group by contains mostly one item:
SELECT
[ID],
CASE WHEN MAX( [Name]) = MIN( [Name]) THEN
MAX( [Name]) NameValues
ELSE
STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
FROM #YourTable
WHERE (ID = Results.ID)
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
END
FROM #YourTable Results
GROUP BY ID
didn't see any cross apply answers, also no need for xml extraction. Here is a slightly different version of what Kevin Fairchild wrote. It's faster and easier to use in more complex queries:
select T.ID
,MAX(X.cl) NameValues
from #YourTable T
CROSS APPLY
(select STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX))
FROM #YourTable
WHERE (ID = T.ID)
FOR XML PATH(''))
,1,2,'') [cl]) X
GROUP BY T.ID
Using the Stuff and for xml path operator to concatenate rows to string :Group By two columns -->
CREATE TABLE #YourTable ([ID] INT, [Name] CHAR(1), [Value] INT)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'A',4)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',8)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (1,'B',5)
INSERT INTO #YourTable ([ID],[Name],[Value]) VALUES (2,'C',9)
-- retrieve each unique id and name columns and concatonate the values into one column
SELECT
[ID],
STUFF((
SELECT ', ' + [Name] + ':' + CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES EACH APPLICATION : VALUE SET
FROM #YourTable
WHERE (ID = Results.ID and Name = results.[name] )
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID
SELECT
[ID],[Name] , --these are acting as the group by clause
STUFF((
SELECT ', '+ CAST([Value] AS VARCHAR(MAX)) -- CONCATONATES THE VALUES FOR EACH ID NAME COMBINATION
FROM #YourTable
WHERE (ID = Results.ID and Name = results.[name] )
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'') AS NameValues
FROM #YourTable Results
GROUP BY ID, name
DROP TABLE #YourTable
Using Replace Function and FOR JSON PATH
SELECT T3.DEPT, REPLACE(REPLACE(T3.ENAME,'{"ENAME":"',''),'"}','') AS ENAME_LIST
FROM (
SELECT DEPT, (SELECT ENAME AS [ENAME]
FROM EMPLOYEE T2
WHERE T2.DEPT=T1.DEPT
FOR JSON PATH,WITHOUT_ARRAY_WRAPPER) ENAME
FROM EMPLOYEE T1
GROUP BY DEPT) T3
For sample data and more ways click here
If you have clr enabled you could use the Group_Concat library from GitHub
Another example without the garbage: ",TYPE).value('(./text())[1]','VARCHAR(MAX)')"
WITH t AS (
SELECT 1 n, 1 g, 1 v
UNION ALL
SELECT 2 n, 1 g, 2 v
UNION ALL
SELECT 3 n, 2 g, 3 v
)
SELECT g
, STUFF (
(
SELECT ', ' + CAST(v AS VARCHAR(MAX))
FROM t sub_t
WHERE sub_t.g = main_t.g
FOR XML PATH('')
)
, 1, 2, ''
) cg
FROM t main_t
GROUP BY g
Input-output is
************************* -> *********************
* n * g * v * * g * cg *
* - * - * - * * - * - *
* 1 * 1 * 1 * * 1 * 1, 2 *
* 2 * 1 * 2 * * 2 * 3 *
* 3 * 2 * 3 * *********************
*************************
I used this approach which may be easier to grasp. Get a root element, then concat to choices any item with the same ID but not the 'official' name
Declare #IdxList as Table(id int, choices varchar(max),AisName varchar(255))
Insert into #IdxLIst(id,choices,AisName)
Select IdxId,''''+Max(Title)+'''',Max(Title) From [dbo].[dta_Alias]
where IdxId is not null group by IdxId
Update #IdxLIst
set choices=choices +','''+Title+''''
From #IdxLIst JOIN [dta_Alias] ON id=IdxId And Title <> AisName
where IdxId is not null
Select * from #IdxList where choices like '%,%'
For all my healthcare folks out there:
SELECT
s.NOTE_ID
,STUFF ((
SELECT
[note_text] + ' '
FROM
HNO_NOTE_TEXT s1
WHERE
(s1.NOTE_ID = s.NOTE_ID)
ORDER BY [line] ASC
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,
1,
2,
'') AS NOTE_TEXT_CONCATINATED
FROM
HNO_NOTE_TEXT s
GROUP BY NOTE_ID

Inserting a dynamically built 'for xml' statement into a table or variable

I've got a situation where I'm trying to get a list of unfilled fields from a temp table into a comma separated statement.
So given the example data (which will always be a single row, and probably in a temp table (as the actual data will come from a multitude of source tables)):
Field1 Field2 Field3 Field4
'aaa' null '' null
And the mapping table of
FieldName Question Section
'Field1' 'Q1' 'Sec1'
'Field2' 'Q2' 'Sec1'
'Field3' 'Q3' 'Sec2'
'Field4' 'Q4' 'Sec2'
I would like the following result:
Section UnansweredQs
'Sec1' 'Q2'
'Sec2' 'Q3, Q4'
I've got as far as the comma separated list of questions by doing:
create table #testData (f1 varchar(50), f2 int, f3 varchar(50), f4 varchar(50))
create table #qlist (fieldName varchar(5), question varchar(3), section varchar(5))
insert into #qlist values ('f1', 'q1', 'sec1'), ('f2', 'q2', 'sec1'), ('f3', 'q3', 'sec2'), ('f4', 'q4', 'sec2')
insert into #testData values ('asda', null, '', null)
Then
declare #usql nvarchar(max) = ''
declare #sql nvarchar(max)
declare #xml xml
--build a gargantuan set of union statements, comparing the column value to null/'' and putting q# if it is
set #usql =
(
select 'select case when ' + c.name + ' is null or ' + c.Name + ' = '''' then ''' + q.question + ', '' else '''' end from #testData union '
from tempdb..syscolumns c
inner join #qlist q
on c.name = q.fieldName
where c.id = object_id('tempdb..#testData')
for xml path('')
);
--remove the last 'union', append for xml path to pivot the rows into a single column of concatenated rows
set #usql = left(#usql, len(#usql) - 6) + ' for xml path('''')'
print #usql
--remove final comma
--get the position of the last comma in the select statment (ie after the final unanswered question)
declare #lastComma int = charindex(',', reverse(#usql))
--add the bit before the last comma, and the bit after the last comma but skip the actual comma :)
set #usql = left(#usql, len(#usql) - #lastComma) + right(#usql, #lastComma - 2)
exec (#usql)
With this I get
XML_F52E2B61-18A1-11d1-B105-00805F49916B
----------------------------------------
q2, q3, q4
But I can't get that result set into another table or variable (via insert into #tmpresult exec (#usql) approach).
Usually with the Msg 1086, Level 15, State 1, Line 1
The FOR XML clause is invalid in views, inline functions, derived tables, and subqueries when they contain a set operator. To work around, wrap the SELECT containing a set operator using derived table syntax and apply FOR XML on top of it. error.
I've tried various things, wrapping, removing the unions, CTE's but can't get it to work.
I have a query for you:
with cte as (
select
N.Name
from Table1
cross apply (values
('Field1', Field1),
('Field2', Field2),
('Field3', Field3),
('Field4', Field4)
) as N(Name,Value)
where N.Value is null or N.Value = ''
)
select distinct
T2.Section,
stuff(
(
select ', ' + TT.Question
from Table2 as TT
inner join cte as c on c.Name = TT.FieldName
where TT.Section = T2.Section
for xml path(''), type
).value('.', 'nvarchar(max)')
, 1, 2, '') as UnansweredQs
from Table2 as T2
you can turn it into dynamic by yourself :)
sql fiddle demo
There is no need to use dynamic SQL to do this.
declare #X xml
set #X = (
select *
from #testData
for xml path('root'), elements xsinil, type
)
select section,
(
select ', '+Q2.question
from #qlist as Q2
where Q1.section = Q2.section and
#X.exist('/root/*[local-name() = sql:column("Q2.fieldName")][. = ""]') = 1
for xml path(''), type
).value('substring(text()[1], 2)', 'varchar(max)') as UnansweredQs
from #qlist as Q1
group by Q1.section
SQL Fiddle

Resources