考虑一个包含名称的数据库表,其中包含三行:
Peter
Paul
Mary
有没有一种简单的方法可以把它变成一个 Peter, Paul, Mary
字符串?
如果您使用的是 SQL Server 2017 或 Azure,请参阅 Mathieu Renda answer。
当我尝试加入两个具有一对多关系的表时,我遇到了类似的问题。在 SQL 2005 中,我发现 XML PATH
方法可以非常轻松地处理行的连接。
如果有一个名为 STUDENTS
的表
SubjectID StudentName
---------- -------------
1 Mary
1 John
1 Sam
2 Alaina
2 Edward
我预期的结果是:
SubjectID StudentName
---------- -------------
1 Mary, John, Sam
2 Alaina, Edward
我使用了以下 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]
如果您可以在开头连接逗号并使用 substring
跳过第一个逗号,那么您可以以更紧凑的方式执行相同的操作,这样您就不需要执行子查询:
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
此答案可能会返回意外结果为了获得一致的结果,请使用其他答案中详述的 FOR XML PATH 方法之一。
使用 COALESCE
:
DECLARE @Names VARCHAR(8000)
SELECT @Names = COALESCE(@Names + ', ', '') + Name
FROM People
只是一些解释(因为这个答案似乎得到了相对常规的观点):
Coalesce 实际上只是一个有用的作弊工具,它完成了两件事:
1) 无需用空字符串值初始化 @Names
。
2) 无需在最后剥离额外的分隔符。
如果一行有一个 NULL Name 值,上面的解决方案将给出不正确的结果(如果有一个 NULL,NULL 将使 @Names 在该行之后为 NULL,并且下一行将再次作为空字符串重新开始。用一个轻松修复两种解决方案:
DECLARE @Names VARCHAR(8000)
SELECT @Names = COALESCE(@Names + ', ', '') + Name
FROM People
WHERE Name IS NOT NULL
或者:
DECLARE @Names VARCHAR(8000)
SELECT @Names = COALESCE(@Names + ', ', '') +
ISNULL(Name, 'N/A')
FROM People
根据您想要的行为(第一个选项只是将 NULL 过滤掉,第二个选项使用标记消息将它们保留在列表中 [用适合您的任何内容替换 'N/A'])。
SQL Server 2017+ 和 SQL Azure:STRING_AGG
从 SQL Server 的下一个版本开始,我们终于可以跨行连接,而无需求助于任何变量或 XML 巫术。
不分组
SELECT STRING_AGG(Name, ', ') AS Departments
FROM HumanResources.Department;
分组:
SELECT GroupName, STRING_AGG(Name, ', ') AS Departments
FROM HumanResources.Department
GROUP BY GroupName;
具有分组和子排序
SELECT GroupName, STRING_AGG(Name, ', ') WITHIN GROUP (ORDER BY Name ASC) AS Departments
FROM HumanResources.Department
GROUP BY GroupName;
通过 SQL Server 中的 XML
data()
命令尚未显示的一种方法是:
假设有一个名为 NameList 的表,其中有一列名为 FName,
SELECT FName + ', ' AS 'data()'
FROM NameList
FOR XML PATH('')
返回:
"Peter, Paul, Mary, "
只有多余的逗号必须处理。
正如@NReilingh 的评论所采用的那样,您可以使用以下方法删除尾随逗号。假设相同的表和列名:
STUFF(REPLACE((SELECT '#!' + LTRIM(RTRIM(FName)) AS 'data()' FROM NameList
FOR XML PATH('')),' #!',', '), 1, 2, '') as Brands
+ ', '
它仍然会在每个连接元素之间添加一个空格。
SELECT STUFF(REPLACE((SELECT '#!'+city AS 'data()' FROM #cityzip FOR XML PATH ('')),' #!',', '),1,2,'')
在 SQL Server 2005 中
SELECT Stuff(
(SELECT N', ' + Name FROM Names FOR XML PATH(''),TYPE)
.value('text()[1]','nvarchar(max)'),1,2,N'')
在 SQL Server 2016 中
您可以使用 FOR JSON syntax
IE
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
结果会变成
Id Emails
1 abc@gmail.com
2 NULL
3 def@gmail.com, xyz@gmail.com
即使您的数据包含无效的 XML 字符,这也可以工作
'"},{"_":"'
是安全的,因为如果您的数据包含 '"},{"_":"',
,它将被转义到 "},{\"_\":\"
您可以将 ', '
替换为任何字符串分隔符
而在 SQL Server 2017 中,Azure SQL 数据库
您可以使用新的 STRING_AGG function
<
、>
、&
等,FOR XML PATH('')
将自动转义。
在 MySQL 中,有一个函数 GROUP_CONCAT(),它允许您连接多行的值。例子:
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
CHAR
,则需要强制转换它,例如通过 GROUP_CONCAT( CAST(id AS CHAR(8)) ORDER BY id ASC SEPARATOR ',')
2)如果您有很多值,您应该增加 stackoverflow.com/a/1278210/1498405 中所写的 group_concat_max_len
使用 COALESCE - Learn more from here
例如:
102 103 104
然后在 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
输出将是:
102,103,104
Declare @Numbers AS Nvarchar(MAX)
,效果很好。你能解释一下为什么你不建议使用它吗?
PostgreSQL 数组很棒。例子:
创建一些测试数据:
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)
将它们聚合到一个数组中:
test=# select array_agg(name) from names;
array_agg
-------------------
{Peter,Paul,Mary}
(1 row)
将数组转换为逗号分隔的字符串:
test=# select array_to_string(array_agg(name), ', ') from names;
array_to_string
-------------------
Peter, Paul, Mary
(1 row)
完毕
从 PostgreSQL 9.0 开始就更容易了,引用“无名马”的已删除答案:
select string_agg(name, ',')
from names;
select array_to_string(array_agg(name||'('||id||')'
Oracle 11g 第 2 版支持 LISTAGG 函数。文档 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.
警告
如果结果字符串有可能超过 4000 个字符,请小心执行此函数。它会抛出异常。如果是这种情况,那么您需要处理异常或滚动您自己的函数来防止连接的字符串超过 4000 个字符。
LISTAGG
完美!只需阅读此处链接的文档。 wm_concat
从版本 12c 开始删除。
在 SQL Server 2005 及更高版本中,使用下面的查询来连接行。
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
<
或 &
等 XML 符号时,这会失败。
建议使用递归 CTE 解决方案,但未提供代码。下面的代码是递归 CTE 的示例。
请注意,尽管结果与问题匹配,但数据与给定的描述并不完全匹配,因为我假设您确实希望对行组执行此操作,而不是表中的所有行。将其更改为匹配表中的所有行留给读者作为练习。
;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
name
列展平为 4 个 groups< 的逗号分隔字符串/i> id
秒。乍一看,我认为这比大多数其他 SQL Server 解决方案所做的工作更多。
我无法访问家里的 SQL Server,所以我猜测这里的语法,但或多或少:
DECLARE @names VARCHAR(500)
SELECT @names = @names + ' ' + Name
FROM Names
SELECT @names = @names + CASE WHEN LEN(@names)=0 THEN '' ELSE ' ' END + Name FROM Names
SELECT @names = @names + ISNULL(' ' + Name, '')
您需要创建一个变量来保存您的最终结果并选择它,就像这样。
最简单的解决方案
DECLARE @char VARCHAR(MAX);
SELECT @char = COALESCE(@char + ', ' + [column], [column])
FROM [table];
PRINT @char;
在 SQL Server 2017 或更高版本中,您可以使用 STRING_AGG() 函数生成 逗号分隔 值。请看下面的一个例子。
SELECT
VendorId,STRING_AGG(FirstName,',') UsersName FROM
Users
where VendorId!=9 GROUP BY VendorId
https://i.stack.imgur.com/Z5m6r.png
即用型解决方案,没有额外的逗号:
select substring(
(select ', '+Name AS 'data()' from Names for xml path(''))
,3, 255) as "MyList"
空列表将导致 NULL 值。通常您会将列表插入到表格列或程序变量中:根据需要调整 255 最大长度。
(Diwakar 和 Jens Frandsen 提供了很好的答案,但需要改进。)
', '
替换为 ','
。
使用 XML 帮助我用逗号分隔行。对于多余的逗号,我们可以使用 SQL Server 的替换功能。代替添加逗号,使用 AS 'data()' 将使用空格连接行,稍后可以用逗号替换,如下所示。
REPLACE(
(select FName AS 'data()' from NameList for xml path(''))
, ' ', ', ')
这对我有用(SQL Server 2016):
SELECT CarNamesString = STUFF((
SELECT ',' + [Name]
FROM tbl_cars
FOR XML PATH('')
), 1, 1, '')
以下是来源:https://www.mytecbits.com/
以及 MySQL 的解决方案(因为此页面出现在 Google for MySQL 中):
SELECT [Name],
GROUP_CONCAT(DISTINCT [Name] SEPARATOR ',')
FROM tbl_cars
SELECT STUFF((SELECT ', ' + name FROM [table] FOR XML PATH('')), 1, 2, '')
这是一个示例:
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
对于其他答案,阅读答案的人必须知道特定的域表,例如车辆或学生。必须创建该表并用数据填充该表以测试解决方案。
下面是一个使用 SQL Server“Information_Schema.Columns”表的示例。通过使用此解决方案,无需创建表或添加数据。此示例为数据库中的所有表创建一个逗号分隔的列名列表。
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
DECLARE @Names VARCHAR(8000)
SELECT @name = ''
SELECT @Names = @Names + ',' + Names FROM People
SELECT SUBSTRING(2, @Names, 7998)
这将杂散逗号放在开头。
但是,如果您需要其他列,或者要 CSV 一个子表,您需要将其包装在一个标量用户定义字段 (UDF) 中。
您也可以在 SELECT 子句中使用 XML 路径作为相关子查询(但我必须等到我回去工作,因为谷歌不在家做工作:-)
MySQL完整示例:
我们有可以拥有大量数据的用户,我们希望有一个输出,我们可以在列表中看到所有用户的数据:
结果:
___________________________
| id | rowList |
|-------------------------|
| 0 | 6, 9 |
| 1 | 1,2,3,4,5,7,8,1 |
|_________________________|
表设置:
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);
询问:
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
GROUP BY
的重要性
为避免空值,您可以使用 CONCAT()
DECLARE @names VARCHAR(500)
SELECT @names = CONCAT(@names, ' ', name)
FROM Names
select @names
我真的很喜欢 Dana's answer 的优雅,只是想让它变得完整。
DECLARE @names VARCHAR(MAX)
SET @names = ''
SELECT @names = @names + ', ' + Name FROM Names
-- Deleting last two symbols (', ')
SET @sSql = LEFT(@sSql, LEN(@sSql) - 1)
SELECT @names = @names + CASE WHEN LEN(@names)=0 THEN '' ELSE ', ' END + Name FROM Names
然后你就不必在之后截断它。
在 Chris Shaffer's answer 之上:
如果您的数据可能会重复,例如
Tom
Ali
John
Ali
Tom
Mike
而不是 Tom,Ali,John,Ali,Tom,Mike
您可以使用 DISTINCT 避免重复并获得 Tom,Ali,John,Mike
:
DECLARE @Names VARCHAR(8000)
SELECT DISTINCT @Names = COALESCE(@Names + ',', '') + Name
FROM People
WHERE Name IS NOT NULL
SELECT @Names
如果要处理空值,可以通过添加 where 子句或在第一个子句周围添加另一个 COALESCE 来实现。
DECLARE @Names VARCHAR(8000)
SELECT @Names = COALESCE(COALESCE(@Names + ', ', '') + Name, @Names) FROM People
对于 Oracle 数据库,请参阅以下问题:How can multiple rows be concatenated into one in Oracle without creating a stored procedure?
最佳答案似乎是 @Emmanuel,使用 Oracle 11g 第 2 版及更高版本中提供的内置 LISTAGG() 函数。
SELECT question_id,
LISTAGG(element_id, ',') WITHIN GROUP (ORDER BY element_id)
FROM YOUR_TABLE;
GROUP BY question_id
正如@user762952 所指出的,并且根据Oracle 的文档http://www.oracle-base.com/articles/misc/string-aggregation-techniques.php,WM_CONCAT() 函数也是一个选项。它看起来很稳定,但 Oracle 明确建议不要将它用于任何应用程序 SQL,因此使用风险自负。
除此之外,您必须编写自己的函数;上面的 Oracle 文档有一个关于如何做到这一点的指南。
这个答案需要服务器上的一些权限才能工作。
Assemblies 对您来说是一个不错的选择。有很多网站解释了如何创建它。我认为解释得很好的是这个one。
如果需要,我已经创建了程序集,可以下载 DLL 文件 here。
下载后,您需要在 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
请注意,服务器可以访问程序集的路径。由于您已成功完成所有步骤,您可以使用如下功能:
SELECT dbo.Concat(field1, ',')
FROM Table1
由于 SQL Server 2017,可以使用 STRING_AGG 函数。
我通常使用这样的 select 来连接 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
<
或&
),显然这不起作用。请参阅@BenHinman 的评论。FOR XML PATH ('')
的未记录行为。这意味着它不应该被认为是可靠的,因为任何补丁或更新都可能改变它的功能。它基本上依赖于已弃用的功能。FOR XML
旨在生成 XML,而不是连接任意字符串。这就是为什么它将&
、<
和>
转义为 XML 实体代码(&
、<
、>
)。我假设它也会在属性中将"
和'
转义为"
和'
。 不是GROUP_CONCAT()
、string_agg()
、array_agg()
、listagg()
等,即使您可以让它这样做。我们应该花时间要求微软实现适当的功能。string_agg
in v.Next.,所有这些都可以消失。