Для иллюстрации предположим, что у меня есть две следующие таблицы:
VehicleID Name
1 Chuck
2 Larry
LocationID VehicleID City
1 1 New York
2 1 Seattle
3 1 Vancouver
4 2 Los Angeles
5 2 Houston
Я хочу написать запрос, чтобы получить следующие результаты:
VehicleID Name Locations
1 Chuck New York, Seattle, Vancouver
2 Larry Los Angeles, Houston
Я знаю, что это можно сделать с помощью курсоров на стороне сервера, то есть:
DECLARE @VehicleID int
DECLARE @VehicleName varchar(100)
DECLARE @LocationCity varchar(100)
DECLARE @Locations varchar(4000)
DECLARE @Results TABLE
(
VehicleID int
Name varchar(100)
Locations varchar(4000)
)
DECLARE VehiclesCursor CURSOR FOR
SELECT
[VehicleID]
, [Name]
FROM [Vehicles]
OPEN VehiclesCursor
FETCH NEXT FROM VehiclesCursor INTO
@VehicleID
, @VehicleName
WHILE @@FETCH_STATUS = 0
BEGIN
SET @Locations = ''
DECLARE LocationsCursor CURSOR FOR
SELECT
[City]
FROM [Locations]
WHERE [VehicleID] = @VehicleID
OPEN LocationsCursor
FETCH NEXT FROM LocationsCursor INTO
@LocationCity
WHILE @@FETCH_STATUS = 0
BEGIN
SET @Locations = @Locations + @LocationCity
FETCH NEXT FROM LocationsCursor INTO
@LocationCity
END
CLOSE LocationsCursor
DEALLOCATE LocationsCursor
INSERT INTO @Results (VehicleID, Name, Locations) SELECT @VehicleID, @Name, @Locations
END
CLOSE VehiclesCursor
DEALLOCATE VehiclesCursor
SELECT * FROM @Results
Однако, как видите, для этого требуется много кода. Мне бы хотелось иметь общую функцию, которая позволила бы мне делать что-то вроде этого:
SELECT VehicleID
, Name
, JOIN(SELECT City FROM Locations WHERE VehicleID = Vehicles.VehicleID, ', ') AS Locations
FROM Vehicles
Это возможно? Или что-то подобное?


Я не верю, что есть способ сделать это в рамках одного запроса, но вы можете использовать такие трюки с временной переменной:
declare @s varchar(max)
set @s = ''
select @s = @s + City + ',' from Locations
select @s
Это определенно меньше кода, чем перемещение курсора, и, вероятно, более эффективно.
Я почти уверен, что вы можете вынести «вероятно» из последней строчки.
FOR XML and CTEs.
Это ненадежно, зависит от плана выполнения, строки могут быть потеряны. См. КБ.
Как называется эта техника или функция? Когда выполняется присвоение переменной SELECT @s = @s, которое включает ее существующее значение, и выполняется снова для каждой строки в наборе результатов?
Отличный ответ, но обратите внимание, что это решение, похоже, недокументировано, поэтому возможно, что Microsoft отключит возможность делать это в будущем без предупреждения.
Если вы используете SQL Server 2005, вы можете использовать команду FOR XML PATH.
SELECT [VehicleID]
, [Name]
, (STUFF((SELECT CAST(', ' + [City] AS VARCHAR(MAX))
FROM [Location]
WHERE (VehicleID = Vehicle.VehicleID)
FOR XML PATH ('')), 1, 2, '')) AS Locations
FROM [Vehicle]
Это намного проще, чем использование курсора, и, похоже, работает достаточно хорошо.
Это будет хорошо работать с этими данными, но если ваши данные могут содержать специальные символы xml (например, &), они будут заменены (
@James. Для этого можно использовать CTE: WITH MyCTE (VehicleId, Name, Locations) AS (SELECT [VehicleID], [Name], (SELECT CAST (City + ',' AS VARCHAR (MAX)) FROM [Location] ГДЕ (VehicleID = Vehicle.VehicleID) ДЛЯ ПУТИ XML ('')) AS Locations FROM [Vehicle]) SELECT VehicleId, Name, REPLACE (Locations, ',', CHAR (10)) AS Locations FROM MyCTE
Я немного запутался, изменяя этот код, поэтому сейчас опубликовал мой собственный вопрос
Вы можете заключить подзапрос в функцию STUFF, чтобы избавиться от запятой. Просто укажите в начале запроса ',' а затем оберните подзапрос в: STUFF (подзапрос, 1,2, '')
Символы разрыва строки могут быть вставлены следующим образом: «некоторый текст» + СИМВОЛ (13) + СИМВОЛ (10) + «текст на следующей строке».
Обратите внимание, что Код Мэтта приведет к появлению дополнительной запятой в конце строки; использование COALESCE (или ISNULL в этом отношении), как показано в ссылке в сообщении Ланса, использует аналогичный метод, но не оставляет вам лишнюю запятую для удаления. Для полноты, вот соответствующий код из ссылки Ланса на sqlteam.com:
DECLARE @EmployeeList varchar(100)
SELECT @EmployeeList = COALESCE(@EmployeeList + ', ', '') +
CAST(EmpUniqueID AS varchar(5))
FROM SalesCallsEmployees
WHERE SalCal_UniqueID = 1
Без лишних запятых, что приятно, но, на мой взгляд, гораздо легче читать и понимать, чем принятое решение. Большое спасибо!
Это не надежное решение.
@lukasLansky - это надежно, пока вы не заботитесь о порядке
Он может пропустить данные из результата, даже если вас не волнует порядок.
Добавьте ORDER BY для надежного заказа?
Если вы используете SQL Server 2005, вы можете написать настраиваемая агрегатная функция CLR, чтобы справиться с этим.
Версия C#:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using System.Text;
using Microsoft.SqlServer.Server;
[Serializable]
[Microsoft.SqlServer.Server.SqlUserDefinedAggregate(Format.UserDefined,MaxByteSize=8000)]
public class CSV:IBinarySerialize
{
private StringBuilder Result;
public void Init() {
this.Result = new StringBuilder();
}
public void Accumulate(SqlString Value) {
if (Value.IsNull) return;
this.Result.Append(Value.Value).Append(",");
}
public void Merge(CSV Group) {
this.Result.Append(Group.Result);
}
public SqlString Terminate() {
return new SqlString(this.Result.ToString());
}
public void Read(System.IO.BinaryReader r) {
this.Result = new StringBuilder(r.ReadString());
}
public void Write(System.IO.BinaryWriter w) {
w.Write(this.Result.ToString());
}
}
Приведенный ниже код будет работать для Sql Server 2000/2005/2008.
CREATE FUNCTION fnConcatVehicleCities(@VehicleId SMALLINT)
RETURNS VARCHAR(1000) AS
BEGIN
DECLARE @csvCities VARCHAR(1000)
SELECT @csvCities = COALESCE(@csvCities + ', ', '') + COALESCE(City,'')
FROM Vehicles
WHERE VehicleId = @VehicleId
return @csvCities
END
-- //Once the User defined function is created then run the below sql
SELECT VehicleID
, dbo.fnConcatVehicleCities(VehicleId) AS Locations
FROM Vehicles
GROUP BY VehicleID
Этот VARCHAR (1000), это какой-то предел, не так ли? Потому что, когда я запускаю аналогичный запрос на конкатенацию в списке столбцов, он останавливается примерно на ~ 950 символах, независимо от указанного размера.
ПРИМЕЧАНИЕ К ВЕРСИИ. Для этого решения необходимо использовать SQL Server 2005 или выше с уровнем совместимости 90 или выше.
См. Этот Статья MSDN для первого примера создания определяемой пользователем агрегатной функции, которая объединяет набор строковых значений, взятых из столбца в таблице.
Моя скромная рекомендация - опустить добавленную запятую, чтобы вы могли использовать свой собственный специальный разделитель, если таковой имеется.
Ссылаясь на версию примера 1 для C#:
change: this.intermediateResult.Append(value.Value).Append(',');
to: this.intermediateResult.Append(value.Value);
И
change: output = this.intermediateResult.ToString(0, this.intermediateResult.Length - 1);
to: output = this.intermediateResult.ToString();
Таким образом, когда вы используете свой собственный агрегат, вы можете выбрать использование собственного разделителя или его отсутствие, например:
SELECT dbo.CONCATENATE(column1 + '|') from table1
ПРИМЕЧАНИЕ: Будьте осторожны с объемом данных, которые вы пытаетесь обработать в своем агрегате. Если вы попытаетесь объединить тысячи строк или много очень больших типов данных, вы можете получить ошибку .NET Framework, в которой говорится: «[t] буфера недостаточно».
Из того, что я вижу, FOR XML (как было опубликовано ранее) - единственный способ сделать это, если вы хотите также выбрать другие столбцы (что, как я предполагаю, большинство), как это делает OP.
Использование COALESCE(@var... не позволяет включать другие столбцы.
Обновлять:
Благодаря programmingsolutions.net есть способ убрать "завершающую" запятую до.
Превратив его в начальную запятую и используя функцию STUFF MSSQL, вы можете заменить первый символ (начальную запятую) пустой строкой, как показано ниже:
stuff(
(select ',' + Column
from Table
inner where inner.Id = outer.Id
for xml path('')
), 1,1,'') as Values
SELECT Stuff(
(SELECT N', ' + Name FROM Names FOR XML PATH(''),TYPE)
.value('text()[1]','nvarchar(max)'),1,2,N'')
вы можете использовать ДЛЯ синтаксиса JSON
т.е.
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 [email protected]
2 NULL
3 [email protected], [email protected]
Это будет работать, даже если ваши данные содержат недопустимые символы XML.
'"}, {" ":" 'безопасно, потому что если ваши данные содержат' "}, {" ":"', он будет экранирован в "}, {\" _ \ ": \"
Вы можете заменить ',' любым разделителем строк
Вы можете использовать новый Функция STRING_AGG
Я вынул этот кусок: TYPE ).value('text()[1]', 'nvarchar(max)'), и он по-прежнему отлично работает ... не уверен, что он должен делать.
должен декодировать xml, если [City] имеет символ типа &, вывод будет & <>, если вы уверены, что [City] не имеет этих специальных символов, то его можно безопасно удалить. - Стивен Чонг
+1. Этот ответ недооценен. Вы должны отредактировать его, указав, что это один из немногих ответов, который не экранирует специальные символы, такие как & и т. д. Кроме того, не будут ли результаты такими же, если мы используем: .value('.', 'nvarchar(max)')?
Привет, Баодад, результаты такие же, но, поскольку я тестировал, производительность лучше при использовании 'text () [1]' вместо '.', Большой разницы нет.
В одном запросе SQL без использования предложения FOR XML. Общее табличное выражение используется для рекурсивного объединения результатов.
-- rank locations by incrementing lexicographical order
WITH RankedLocations AS (
SELECT
VehicleID,
City,
ROW_NUMBER() OVER (
PARTITION BY VehicleID
ORDER BY City
) Rank
FROM
Locations
),
-- concatenate locations using a recursive query
-- (Common Table Expression)
Concatenations AS (
-- for each vehicle, select the first location
SELECT
VehicleID,
CONVERT(nvarchar(MAX), City) Cities,
Rank
FROM
RankedLocations
WHERE
Rank = 1
-- then incrementally concatenate with the next location
-- this will return intermediate concatenations that will be
-- filtered out later on
UNION ALL
SELECT
c.VehicleID,
(c.Cities + ', ' + l.City) Cities,
l.Rank
FROM
Concatenations c -- this is a recursion!
INNER JOIN RankedLocations l ON
l.VehicleID = c.VehicleID
AND l.Rank = c.Rank + 1
),
-- rank concatenation results by decrementing length
-- (rank 1 will always be for the longest concatenation)
RankedConcatenations AS (
SELECT
VehicleID,
Cities,
ROW_NUMBER() OVER (
PARTITION BY VehicleID
ORDER BY Rank DESC
) Rank
FROM
Concatenations
)
-- main query
SELECT
v.VehicleID,
v.Name,
c.Cities
FROM
Vehicles v
INNER JOIN RankedConcatenations c ON
c.VehicleID = v.VehicleID
AND c.Rank = 1
Спасибо за это. Это одно из немногих решений этой проблемы, в котором не используются переменные, функции, предложение FOR XML или код CLR. Это означает, что я смог адаптировать ваше решение для решения Задача для начинающих TSQL 4 - Объединение значений из нескольких строк.
Спасибо! Мне нужно преобразовать серию фрагментов кода SQL, выраженных в виде отдельных строк логических фраз, в одно сложное кодовое выражение, и я очень рад попробовать ваш метод.
Есть ли у этого преимущества в производительности по сравнению с другими решениями?
@PeonProgrammer нет, он работает очень плохо для больших наборов результатов и может выдать вам ошибку: «Максимальное количество рекурсии 100 было исчерпано до завершения оператора». (Вы можете обойти это, указав OPTION (MAXRECURSION 0) в конце, но тогда выполнение вашего запроса может занять вечность.
Я нашел решение, создав следующую функцию:
CREATE FUNCTION [dbo].[JoinTexts]
(
@delimiter VARCHAR(20) ,
@whereClause VARCHAR(1)
)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE @Texts VARCHAR(MAX)
SELECT @Texts = COALESCE(@Texts + @delimiter, '') + T.Texto
FROM SomeTable AS T
WHERE T.SomeOtherColumn = @whereClause
RETURN @Texts
END
GO
Использование:
SELECT dbo.JoinTexts(' , ', 'Y')
Это очень похоже на ответы Майк Пауэлл и Биной Антоний.
Отличное решение, потому что удобочитаемость превосходила другие ответы +1
Попробуйте этот запрос
SELECT v.VehicleId, v.Name, ll.LocationList
FROM Vehicles v
LEFT JOIN
(SELECT
DISTINCT
VehicleId,
REPLACE(
REPLACE(
REPLACE(
(
SELECT City as c
FROM Locations x
WHERE x.VehicleID = l.VehicleID FOR XML PATH('')
),
'</c><c>',', '
),
'<c>',''
),
'</c>', ''
) AS LocationList
FROM Locations l
) ll ON ll.VehicleId = v.VehicleId
Что касается других ответов, человек, читающий ответ, должен знать таблицу транспортных средств и создать таблицу транспортных средств и данные для проверки решения.
Ниже приведен пример, в котором используется таблица 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
Ответ Мана не сработал для меня, поэтому я внес некоторые изменения в этот ответ, чтобы заставить его работать. Надеюсь, это кому-то поможет. Использование SQL Server 2012:
SELECT [VehicleID]
, [Name]
, STUFF((SELECT DISTINCT ',' + CONVERT(VARCHAR,City)
FROM [Location]
WHERE (VehicleID = Vehicle.VehicleID)
FOR XML PATH ('')), 1, 2, '') AS Locations
FROM [Vehicle]
Аналогичный ответ с более полным ответом stackoverflow.com/a/17591536/1587302