Как создать функцию SQL Server для объединения нескольких строк из подзапроса в одно поле с разделителями?

Для иллюстрации предположим, что у меня есть две следующие таблицы:

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

Это возможно? Или что-то подобное?


person Templar    schedule 09.08.2008    source источник
comment
Аналогичный ответ с более полным ответом stackoverflow.com/a/17591536/1587302   -  person Narkha    schedule 27.11.2013


Ответы (13)


Если вы используете 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]

Это намного проще, чем использование курсора, и, похоже, работает достаточно хорошо.

person Mun    schedule 10.08.2008
comment
Это будет хорошо работать с этими данными, но если ваши данные могут содержать специальные символы xml (например, ‹,›, &), они будут заменены (‹и т. Д.) - person GilM; 12.10.2008
comment
@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 - person Mun; 24.05.2011
comment
Я немного запутался, изменяя этот код, поэтому разместил мой вопрос - person James Parish; 24.05.2011
comment
Вы можете заключить подзапрос в функцию STUFF, чтобы избавиться от запятой. Просто укажите в начале запроса ',' а затем оберните подзапрос в: STUFF (subquery, 1,2, '') - person MickJuice; 20.12.2013
comment
Символы разрыва строки могут быть вставлены следующим образом: «некоторый текст» + СИМВОЛ (13) + СИМВОЛ (10) + «текст на следующей строке». - person thefellow3j; 16.02.2016

Обратите внимание, что Код Мэтта приведет к добавлению запятой в конце строки; использование COALESCE (или ISNULL в этом отношении), как показано в ссылке в сообщении Ланса, использует аналогичный метод, но не оставляет вам лишнюю запятую для удаления. Для полноты, вот соответствующий код из ссылки Ланса на sqlteam.com:

DECLARE @EmployeeList varchar(100)
SELECT @EmployeeList = COALESCE(@EmployeeList + ', ', '') + 
    CAST(EmpUniqueID AS varchar(5))
FROM SalesCallsEmployees
WHERE SalCal_UniqueID = 1
person Mike Powell    schedule 10.08.2008
comment
Без лишних запятых, что приятно, но, на мой взгляд, гораздо легче читать и понимать, чем принятое решение. Большое спасибо! - person Beska; 25.10.2012
comment
Это не надежное решение. - person Lukáš Lánský; 26.02.2014
comment
@lukasLansky - это надежно, пока вы не заботитесь о порядке - person codeulike; 29.04.2015
comment
Он может пропустить данные из результата, даже если вас не волнует порядок. - person Der_Meister; 09.02.2016
comment
Добавьте ORDER BY для надежного заказа? - person Pete Alvin; 12.07.2017

Я не верю, что есть способ сделать это в рамках одного запроса, но вы можете использовать такие трюки с временной переменной:

declare @s varchar(max)
set @s = ''
select @s = @s + City + ',' from Locations

select @s

Это определенно меньше кода, чем перемещение курсора, и, вероятно, более эффективно.

person Matt Hamilton    schedule 10.08.2008
comment
Я почти уверен, что ты сможешь убрать последнюю строчку. - person Marc Gravell; 12.10.2008
comment
Я не верю, что есть способ сделать это в рамках одного запроса Да, есть. В SQL Server 2005 были и FOR XML, и CTE. - person T.J. Crowder; 23.05.2015
comment
Это ненадежно, зависит от плана выполнения, строки могут быть потеряны. См. КБ. - person Der_Meister; 09.02.2016
comment
Как называется эта техника или функция? Когда присваивание переменной SELECT @s = @s, включающее ее существующее значение, выполняется снова для каждой строки в наборе результатов? - person Baodad; 16.05.2016
comment
Отличный ответ, но обратите внимание, что это решение, похоже, недокументировано, поэтому возможно, что Microsoft отключит возможность делать это в будущем без предупреждения. - person Pouria Moosavi; 04.09.2020

В одном запросе 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
person ZunTzu    schedule 06.01.2011
comment
Спасибо за это. Это одно из немногих решений этой проблемы, в котором не используются переменные, функции, предложение FOR XML или код CLR. Это означает, что я смог адаптировать ваше решение для решения Задача для начинающих TSQL 4 - Объединение значений из нескольких строк. - person Iain Samuel McLean Elder; 14.08.2011
comment
Спасибо! Мне нужно преобразовать серию фрагментов кода SQL, выраженных в виде отдельных строк логических фраз, в одно сложное кодовое выражение, и я очень рад попробовать ваш метод. - person Paul Chernoch; 18.02.2013
comment
Есть ли у этого преимущества в производительности по сравнению с другими решениями? - person PeonProgrammer; 10.09.2015
comment
@PeonProgrammer нет, он очень плохо работает для больших наборов результатов и может выдать вам ошибку. Максимальная рекурсия 100 была исчерпана до завершения оператора. (Вы можете обойти это, указав OPTION (MAXRECURSION 0) в конце, но тогда выполнение вашего запроса может занять вечность. - person Kirk Woll; 25.03.2016

Из того, что я вижу, 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
person John B    schedule 23.06.2010

В 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

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

И результат станет

Id  Emails
1   [email protected]
2   NULL
3   [email protected], [email protected]

Это будет работать, даже если ваши данные содержат недопустимые символы XML.

'"}, {" ":"' безопасно, потому что если ваши данные содержат '"}, {" ":"', они будут заменены на "}, {\" _ \ " : \ "

Вы можете заменить ',' любым разделителем строк


А в SQL Server 2017 База данных SQL Azure

Вы можете использовать новую функцию STRING_AGG

person Steven C    schedule 09.09.2010
comment
Я вынул этот кусок: TYPE ).value('text()[1]', 'nvarchar(max)'), и он по-прежнему отлично работает ... не уверен, что он должен делать. - person Adam Nofsinger; 07.07.2011
comment
должен декодировать xml, если [City] имеет символ типа & ‹›, вывод будет таким, если вы уверены, что [City] не имеет этих специальных символов, тогда его можно безопасно удалить. - Стивен Чонг - person Steven C; 04.06.2012
comment
+1. Этот ответ недооценен. Вы должны отредактировать его, указав, что это один из немногих ответов, который не экранирует специальные символы, такие как & ‹› и т. Д. Кроме того, не будут ли результаты такими же, если мы используем: .value('.', 'nvarchar(max)')? - person Baodad; 27.04.2016
comment
Привет, Баодад, результаты такие же, но, как я тестировал, производительность лучше при использовании 'text () [1]' вместо '.', Большой разницы нет. - person Steven C; 28.04.2016

Приведенный ниже код будет работать для 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
person Binoj Antony    schedule 18.06.2009
comment
Этот VARCHAR (1000), это какой-то предел, не так ли? Потому что, когда я запускаю аналогичный запрос конкатенации в списке столбцов, он останавливается примерно на 950 символов, независимо от указанного размера. - person John Leidegren; 25.08.2009

Я нашел решение, создав следующую функцию:

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')
person Gil    schedule 30.05.2011
comment
Это очень похоже на Майка Пауэлла и ответы Биной ​​Антоний. - person Andriy M; 30.05.2011
comment
Отличное решение, потому что удобочитаемость превосходила другие ответы +1 - person PeonProgrammer; 10.09.2015

Ответ Мана не сработал для меня, поэтому я внес некоторые изменения в этот ответ, чтобы заставить его работать. Надеюсь, это кому-то поможет. Использование 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]
person nurseybushc    schedule 13.10.2016

ПРИМЕЧАНИЕ К ВЕРСИИ. Вы должны использовать 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] буфера недостаточно».

person JustinStolle    schedule 09.02.2010

Что касается других ответов, человек, читающий ответ, должен знать таблицу транспортных средств и создать таблицу транспортных средств и данные для проверки решения.

Ниже приведен пример, в котором используется таблица 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 
person Mike Barlow - BarDev    schedule 04.05.2016

Попробуйте этот запрос

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
person Ilya Rudenko    schedule 04.09.2015

Если вы используете 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());
    }
}
person HS.    schedule 10.08.2008