Объединить значения строк T-SQL

Я пытаюсь собрать некоторые данные для отчета и нужно объединить значения строк одной из таблиц. Вот основная структура таблицы:

Отзывы

 ReviewID  
 ReviewDate  

Рецензенты

 ReviewerID  
 ReviewID  
 UserID  

Пользователи

UserID  
FName  
LName  

Это отношение M: M. В каждом обзоре может быть много рецензентов; каждый Пользователь может быть связан со многими Обзорами.

В принципе, все, что я хочу увидеть, это Reviews.ReviewID, Reviews.ReviewDate и объединенная строка FName всех связанных пользователей для этого обзора (с разделителями-запятыми).

Вместо:

ReviewID---ReviewDate---User  
1----------12/1/2009----Bob  
1----------12/1/2009----Joe  
1----------12/1/2009----Frank  
2----------12/9/2009----Sue  
2----------12/9/2009----Alice  

Отобразите это:

ReviewID---ReviewDate----Users  
1----------12/1/2009-----Bob, Joe, Frank  
2----------12/9/2009-----Sue, Alice

Я нашел эту статью, описывающую некоторые способы сделать это, но большинство из них, похоже, касается только одной таблицы, а не несколько; к сожалению, мой SQL-fu недостаточно силен, чтобы адаптировать их к моим обстоятельствам. Меня особенно интересует пример на этом сайте, который использует FOR XML PATH(), поскольку он выглядит самым чистым и самым прямым.

SELECT p1.CategoryId,
( SELECT ProductName + ', '
  FROM Northwind.dbo.Products p2
  WHERE p2.CategoryId = p1.CategoryId
  ORDER BY ProductName FOR XML PATH('')
) AS Products
FROM Northwind.dbo.Products p1
GROUP BY CategoryId;

Может ли кто-нибудь дать мне руку с этим? Любая помощь будет принята с благодарностью!

Ответы

Ответ 1

Посмотрите на это

DECLARE @Reviews TABLE(
        ReviewID INT,
        ReviewDate DATETIME
)

DECLARE @Reviewers TABLE(
        ReviewerID   INT,
        ReviewID   INT,
        UserID INT
)

DECLARE @Users TABLE(
        UserID  INT,
        FName  VARCHAR(50),
        LName VARCHAR(50)
)

INSERT INTO @Reviews SELECT 1, '12 Jan 2009'
INSERT INTO @Reviews SELECT 2, '25 Jan 2009'

INSERT INTO @Users SELECT 1, 'Bob', ''
INSERT INTO @Users SELECT 2, 'Joe', ''
INSERT INTO @Users SELECT 3, 'Frank', ''
INSERT INTO @Users SELECT 4, 'Sue', ''
INSERT INTO @Users SELECT 5, 'Alice', ''

INSERT INTO @Reviewers SELECT 1, 1, 1
INSERT INTO @Reviewers SELECT 2, 1, 2
INSERT INTO @Reviewers SELECT 3, 1, 3
INSERT INTO @Reviewers SELECT 4, 2, 4
INSERT INTO @Reviewers SELECT 5, 2, 5

SELECT  *,
        ( 
            SELECT  u.FName + ','
            FROM    @Users u INNER JOIN 
                    @Reviewers rs ON u.UserID = rs.UserID
            WHERE   rs.ReviewID = r.ReviewID
            FOR XML PATH('')
        ) AS Products
FROM    @Reviews r

Ответ 2

Оказывается, есть еще более простой способ сделать это, что не требует UDF:

select replace(replace(replace((cast((
        select distinct columnName as X
        from tableName 
        for xml path('')) as varchar(max))), 
   '</X><X>', ', '),'<X>', ''),'</X>','')

Ответ 3

Имел подобную проблему и нашел сладкое решение после игры с кодом в течение 15 минут

declare @result varchar(1000)
select @result = COALESCE(@result+','+A.col1, A.col1)
                FROM (  select  col1
                        from [table] 
                ) A
select @result

Возвращает результат как value1, value2, value3, value4

Наслаждайтесь;)

Ответ 4

SqlServer 2017 теперь имеет STRING_AGG, который объединяет несколько строк в один, используя данный разделитель.

Ответ 5

Есть три способа, которыми я имел дело с свертывающимися данными, как вы описали, 1. использовать курсор, 2. использовать UDF или 3. использовать пользовательскую совокупность (написанную в .NET CLR).
Курсор и UDF довольно медленные. (приблизительно 0,1 с в строке). Пользовательский агрегат CLR на удивление быстро. (приблизительно 0,001 сек за строку)

Microsoft отправляет код (чтобы сделать именно то, что вы хотите) как часть SDK для SQL 2005. Если вы его установили, вы сможете найти код в этой папке: C:\Program Files\Microsoft SQL Server\90\Samples\Engine\Programmability\CLR\StringUtilities. Вы также можете захотеть эту статью в MSDN. В нем говорится об установке пользовательского агрегата и его включении: http://msdn.microsoft.com/en-us/library/ms161551(SQL.90).aspx

После компиляции и установки настраиваемой совокупности вы должны иметь возможность запросить следующее:

SELECT Reviews.ReviewID, ReviewDate, dbo.StringUtilities.Concat(FName) AS [User]
FROM Reviews INNER JOIN Reviewers ON Reviews.ReviewID = Reviewers.ReviewID
   INNER JOIN Users ON Reviews.UserID = Users.UserID
GROUP BY ReviewID, ReviewDate;

и получите набор результатов, как вы показали (выше)

Ответ 6

select p1.Availability ,COUNT(*),
(select  name+','  from AdventureWorks2008.Production.Location p2 where 
p1.Availability=p2.Availability for XML path(''),type).value('.','varchar(max)') 
as Name  from AdventureWorks2008.Production.Location p1 group by Availability

Результат

Availability  COUNT     Name  
---------------------------------------------------------------------------------
0.00    7   Tool Crib,Sheet Metal Racks,Paint Shop,Paint Storage,Metal 
                    Storage,Miscellaneous Storage,Finished Goods Storage,
80.00   1   Specialized Paint,
96.00   1   Frame Forming,
108.00  1   Frame Welding,
120.00  4   Debur and Polish,Paint,Subassembly,Final Assembly,

Ответ 7

A UDF будет хорошим способом решить эту проблему.

Просто определите функцию T-SQL (UDF), которая принимает int param (идентификатор продукта) и возвращает строку (объединение имен, связанных с продуктом.) Если ваше имя метода - GetProductNames, тогда ваш запрос может выглядеть следующим образом:

SELECT p1.CategoryId, dbo.GetProductNames(p1.CategoryId)
FROM Northwind.dbo.Products p1
GROUP BY CategoryId

Ответ 8

Попробуйте следующее:

 Declare @Revs Table 
 (RevId int Priimary Key Not Null,
  RevDt DateTime Null,
  users varChar(1000) default '')

 Insert @Revs (RevId, RevDt)
 Select Distinct ReviewId, ReviewDate
 From Reviews
 Declare @UId Integer
 Set @Uid = 0
 While Exists (Select * From Users
               Where UserID > @Uid)
 Begin
    Update @Revs Set
      users = users + u.fName + ', '
    From @Revs R 
       Join Reviewers uR On ur.ReviewId = R.RId
       Join users u On u.UserId = uR.UserId 
    Where uR.UserId = @UId
    Select @Uid = Min(UserId)
    From users
    Where UserId > @UId
  End
  Select * From @Revs

Ответ 9

Select R.ReviewID, ReviewDate
, (Select  FName + ', ' 
   from Users 
   where UserID = R.UserID 
   order by FName FOR XML PATH(')
) as [Users]
from Reviews
inner join Reviewers AS R
  On Reviews.ReviewID = R.ReviewID
Group By R.ReviewID, ReviewDate;

Ответ 11

Создайте временную таблицу для выгрузки данных. Затем используйте метод FOR XML PATH. Внешний запрос необходим, чтобы обрезать последнюю запятую из списка.

CREATE TABLE #ReviewInfo (
ReviewId INT,
ReviewDate DATETIME,
Reviewer VARCHAR(1000))

INSERT INTO #ReviewInfo (ReviewId, ReviewDate, Reviewer)
SELECT r.ReviewId, r.ReviewDate, u.FName
FROM Reviews r
JOIN Reviewers rs ON r.ReviewId = rs.ReviewId
JOIN Users u ON u.UserId = rs.UserId

SELECT ReviewId, ReviewDate, LEFT(Users, LEN(Users)-1)
FROM (
SELECT ReviewId, ReviewDate, 
(
    SELECT Reviewer + ', '
    FROM #ReviewInfo ri2
    WHERE ri2.ReviewId = ri1.ReviewId
    ORDER BY Reviewer
    FOR XML PATH('')
) AS Users
FROM #ReviewInfo ri1
GROUP BY ReviewId, ReviewDate
) a

DROP TABLE #ReviewInfo

Ответ 12

select 
      p1.Availability,
      COUNT(*),
      (
          select  name+',' 
          from AdventureWorks2008.Production.Location p2 
          where p1.Availability=p2.Availability 
          for XML path(''),type
      ).value('.','varchar(max)') as Name  
 from AdventureWorks2008.Production.Location p1 
 group by Availability

Ответ 13

Теперь с SQL Server 2017 появилась новая функция T-SQL STRING_AGG:
это новая агрегированная функция, которая объединяет значения строковых выражений и значения разделителей мест между ними.
Сепаратор не добавляется в конце строки.

Пример:

SELECT STRING_AGG ( ISNULL(FirstName,'N/A'), ',') AS csv 
FROM Person.Person; 

набор результатов:

John,N/A,Mike,Peter,N/A,N/A,Alice,Bob

Ответ 14

Когда количество элементов невелико, это может быть сделано с помощью ROW_NUMBER() OVER PARTITION BY:

declare @t table (col1 int, col2 varchar)
insert into @t VALUES (1,'A')
insert into @t VALUES (1,'B')
insert into @t VALUES (1,'C')
insert into @t VALUES (1,'D')
insert into @t VALUES (1,'E')
insert into @t VALUES (2,'X')
insert into @t VALUES (3,'Y')

select col1,
    MAX(CASE seq WHEN 1 THEN        col2 ELSE '' END ) + 
    MAX(CASE seq WHEN 2 THEN ', ' + col2 ELSE '' END ) + 
    MAX(CASE seq WHEN 3 THEN ', ' + col2 ELSE '' END ) +
    MAX(CASE seq WHEN 4 THEN ', ' + col2 ELSE '' END ) +
    MAX(CASE seq WHEN 5 THEN ',...' ELSE '' END ) 
    as col2
from (
    select col1, col2, ROW_NUMBER() OVER ( PARTITION BY col1 ORDER BY col2 ) seq
    from @t
    group by col1, col2
) x
group by col1