私は、異なる行から単一の行に文字列を集約する方法を見つけています。私はこれをさまざまな場所で実行したいと考えているので、これを容易にする機能があるといいでしょう。 COALESCE
とFOR XML
を使用したソリューションを試しましたが、それらは私のためにそれをカットしません。
文字列の集計は次のようになります。
id | Name Result: id | Names
-- - ---- -- - -----
1 | Matt 1 | Matt, Rocks
1 | Rocks 2 | Stylus
2 | Stylus
COALESCE
およびFOR XML
の代わりとして CLR定義の集計関数 を見てきましたが、明らかにSQL AzureはCLR定義のものをサポートしていませんこれは私にとって苦痛です。 。
可能な回避策、または同様の最適な方法(CLRほど最適ではないかもしれませんが、ちょっと私は得ることができるものを取ります)それを使用して集計することができますもの?
ソリューション
optimalの定義は異なる場合がありますが、ここでは、Azureで正常に動作する通常のTransact SQLを使用して異なる行の文字列を連結する方法を示します。
;WITH Partitioned AS
(
SELECT
ID,
Name,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
COUNT(*) OVER (PARTITION BY ID) AS NameCount
FROM dbo.SourceTable
),
Concatenated AS
(
SELECT
ID,
CAST(Name AS nvarchar) AS FullName,
Name,
NameNumber,
NameCount
FROM Partitioned
WHERE NameNumber = 1
UNION ALL
SELECT
P.ID,
CAST(C.FullName + ', ' + P.Name AS nvarchar),
P.Name,
P.NameNumber,
P.NameCount
FROM Partitioned AS P
INNER JOIN Concatenated AS C
ON P.ID = C.ID
AND P.NameNumber = C.NameNumber + 1
)
SELECT
ID,
FullName
FROM Concatenated
WHERE NameNumber = NameCount
説明
アプローチは、次の3つのステップに要約されます。
OVER
およびPARTITION
のグループ化を使用して行に番号を付け、連結に必要な順番に並べます。結果はPartitioned
CTEです。後で結果をフィルタリングするために、各パーティションの行数を保持します。
再帰CTE(Concatenated
)を使用して、行番号(NameNumber
列)を反復処理し、Name
値をFullName
列に追加します。
NameNumber
が最も高い結果を除くすべての結果を除外します。
このクエリを予測可能にするためには、グループ化(たとえば、同じID
が連結されたシナリオ行で)と並べ替え(単に文字列をアルファベット順に並べると仮定した)を定義する必要があることに注意してください連結)。
次のデータを使用して、SQL Server 2012でソリューションをすばやくテストしました。
INSERT dbo.SourceTable (ID, Name)
VALUES
(1, 'Matt'),
(1, 'Rocks'),
(2, 'Stylus'),
(3, 'Foo'),
(3, 'Bar'),
(3, 'Baz')
クエリ結果:
ID FullName
----------- ------------------------------
2 Stylus
3 Bar, Baz, Foo
1 Matt, Rocks
以下のようなFOR XML PATHを使用するメソッドは本当に遅いですか? Itzik Ben-Ganは、T-SQLクエリブックでこのメソッドのパフォーマンスが優れていると書いています(筆者の見方では、Mr。Ben-Ganは信頼できる情報源です)。
create table #t (id int, name varchar(20))
insert into #t
values (1, 'Matt'), (1, 'Rocks'), (2, 'Stylus')
select id
,Names = stuff((select ', ' + name as [text()]
from #t xt
where xt.id = t.id
for xml path('')), 1, 2, '')
from #t t
group by id
これを見つけた私たちにとって azure SQL Databaseを使用していない:
STRING_AGG()
PostgreSQL、SQL Server 2017およびAzure SQL
https://www.postgresql.org/docs/current/static/functions-aggregate.html
https://docs.Microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql
GROUP_CONCAT()
MySQLで
http://dev.mysql.com/doc/refman/5.7/en/group-by-functions.html#function_group-concat
(Azureの更新について@Brianjordenと@milanioに感謝します)
select Id
, STRING_AGG(Name, ', ') Names
from Demo
group by Id
SQLフィドル: http://sqlfiddle.com/#!18/89251/1
@sergeの答えは正しいですが、私は彼の方法の時間消費をxmlpathと比較しましたが、xmlpathは非常に速いことがわかりました。比較コードを作成し、自分で確認できます。これは@sergeの方法です:
DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;
set nocount on;
declare @YourTable table (ID int, Name nvarchar(50))
WHILE @counter < 1000
BEGIN
insert into @YourTable VALUES (ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
SET @counter = @counter + 1;
END
SET @startTime = GETDATE()
;WITH Partitioned AS
(
SELECT
ID,
Name,
ROW_NUMBER() OVER (PARTITION BY ID ORDER BY Name) AS NameNumber,
COUNT(*) OVER (PARTITION BY ID) AS NameCount
FROM @YourTable
),
Concatenated AS
(
SELECT ID, CAST(Name AS nvarchar) AS FullName, Name, NameNumber, NameCount FROM Partitioned WHERE NameNumber = 1
UNION ALL
SELECT
P.ID, CAST(C.FullName + ', ' + P.Name AS nvarchar), P.Name, P.NameNumber, P.NameCount
FROM Partitioned AS P
INNER JOIN Concatenated AS C ON P.ID = C.ID AND P.NameNumber = C.NameNumber + 1
)
SELECT
ID,
FullName
FROM Concatenated
WHERE NameNumber = NameCount
SET @endTime = GETDATE();
SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 54 milliseconds
そして、これはxmlpathの方法です:
DECLARE @startTime datetime2;
DECLARE @endTime datetime2;
DECLARE @counter INT;
SET @counter = 1;
set nocount on;
declare @YourTable table (RowID int, HeaderValue int, ChildValue varchar(5))
WHILE @counter < 1000
BEGIN
insert into @YourTable VALUES (@counter, ROUND(@counter/10,0), CONVERT(NVARCHAR(50), @counter) + 'CC')
SET @counter = @counter + 1;
END
SET @startTime = GETDATE();
set nocount off
SELECT
t1.HeaderValue
,STUFF(
(SELECT
', ' + t2.ChildValue
FROM @YourTable t2
WHERE t1.HeaderValue=t2.HeaderValue
ORDER BY t2.ChildValue
FOR XML PATH(''), TYPE
).value('.','varchar(max)')
,1,2, ''
) AS ChildValues
FROM @YourTable t1
GROUP BY t1.HeaderValue
SET @endTime = GETDATE();
SELECT DATEDIFF(millisecond,@startTime, @endTime)
--Take about 4 milliseconds
更新:Ms SQL Server 2017 +、Azure SQL Database
使用できます: STRING_AGG
。
OPのリクエストの使い方は非常に簡単です。
SELECT id, STRING_AGG(name, ', ') AS names
FROM some_table
GROUP BY id
さて、私の古い非回答は正当に削除されました(下のインタクトは残されています)が、将来誰かが偶然ここに着くと、良いニュースがあります。 Azure SQL DatabaseでもSTRING_AGG()を実装しています。これにより、この投稿で最初に要求された正確な機能とネイティブおよび組み込みのサポートが提供されます。 @hrobkyは、これを以前SQL Server 2016の機能として言及していました。
---過去の投稿:@hrobkyに直接返信するための評判はここにありませんが、STRING_AGGは素晴らしく見えますが、現在SQL Server 2016 vNextでのみ利用可能です。 Azure SQL Datababseにもすぐに続くことを願っています。
たとえば、+ =を使用して文字列を連結できます。
declare @test nvarchar(max)
set @test = ''
select @test += name from names
@testを選択すると、すべての名前が連結されます。
Sergeの答えは非常に有望であることがわかりましたが、書かれたままでパフォーマンスの問題も発生しました。ただし、一時テーブルを使用し、二重CTEテーブルを含まないように再構築すると、1000レコードを結合した場合のパフォーマンスは1分40秒から1秒未満になりました。これは、古いバージョンのSQL ServerでFOR XMLを使用せずにこれを実行する必要がある人向けです。
DECLARE @STRUCTURED_VALUES TABLE (
ID INT
,VALUE VARCHAR(MAX) NULL
,VALUENUMBER BIGINT
,VALUECOUNT INT
);
INSERT INTO @STRUCTURED_VALUES
SELECT ID
,VALUE
,ROW_NUMBER() OVER (PARTITION BY ID ORDER BY VALUE) AS VALUENUMBER
,COUNT(*) OVER (PARTITION BY ID) AS VALUECOUNT
FROM RAW_VALUES_TABLE;
WITH CTE AS (
SELECT SV.ID
,SV.VALUE
,SV.VALUENUMBER
,SV.VALUECOUNT
FROM @STRUCTURED_VALUES SV
WHERE VALUENUMBER = 1
UNION ALL
SELECT SV.ID
,CTE.VALUE + ' ' + SV.VALUE AS VALUE
,SV.VALUENUMBER
,SV.VALUECOUNT
FROM @STRUCTURED_VALUES SV
JOIN CTE
ON SV.ID = CTE.ID
AND SV.VALUENUMBER = CTE.VALUENUMBER + 1
)
SELECT ID
,VALUE
FROM CTE
WHERE VALUENUMBER = VALUECOUNT
ORDER BY ID
;