하위 쿼리의 여러 행을 단일 구분 필드로 "결합"하는 SQL Server 함수를 만드는 방법은 무엇입니까?[복제하다]
-
08-06-2019 - |
문제
이 질문에는 이미 답변이 있습니다.
설명을 위해 다음과 같은 두 개의 테이블이 있다고 가정합니다.
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
이것이 가능한가?아니면 비슷한 것?
해결책
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]
커서를 사용하는 것보다 훨씬 쉽고 꽤 잘 작동하는 것 같습니다.
다른 팁
참고하세요 매트의 코드 문자열 끝에 추가 쉼표가 생깁니다.Lance 게시물의 링크에 표시된 대로 COALESCE(또는 해당 문제의 경우 ISNULL)를 사용하면 비슷한 방법을 사용하지만 제거할 추가 쉼표가 남지 않습니다.완전성을 기하기 위해 sqlteam.com에 있는 Lance 링크의 관련 코드는 다음과 같습니다.
DECLARE @EmployeeList varchar(100)
SELECT @EmployeeList = COALESCE(@EmployeeList + ', ', '') +
CAST(EmpUniqueID AS varchar(5))
FROM SalesCallsEmployees
WHERE SalCal_UniqueID = 1
하나의 쿼리 내에서 이를 수행할 수 있는 방법이 없다고 생각하지만 임시 변수를 사용하여 다음과 같은 트릭을 수행할 수 있습니다.
declare @s varchar(max)
set @s = ''
select @s = @s + City + ',' from Locations
select @s
커서 위를 걷는 것보다 확실히 코드가 적고 아마도 더 효율적일 것입니다.
단일 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
(이전에 게시한 대로) OP와 마찬가지로 다른 열(대부분 그럴 것이라고 생각함)을 선택하려는 경우 이를 수행할 수 있는 유일한 방법입니다.사용 COALESCE(@var...
다른 열의 포함을 허용하지 않습니다.
업데이트:덕분에 프로그래밍 솔루션 .net "후행"쉼표를 제거하는 방법이 있습니다.선행 쉼표로 만들고 STUFF
MSSQL의 함수에서는 아래와 같이 첫 번째 문자(선행 쉼표)를 빈 문자열로 바꿀 수 있습니다.
stuff(
(select ',' + Column
from Table
inner where inner.Id = outer.Id
for xml path('')
), 1,1,'') as Values
~ 안에 SQL 서버 2005
SELECT Stuff(
(SELECT N', ' + Name FROM Names FOR XML PATH(''),TYPE)
.value('text()[1]','nvarchar(max)'),1,2,N'')
SQL 서버 2016에서
당신은 사용할 수 있습니다 FOR 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 abc@gmail.com
2 NULL
3 def@gmail.com, xyz@gmail.com
데이터에 잘못된 XML 문자가 포함되어 있어도 작동합니다.
'"},{"":"'는 데이터에 '"},{"가 포함되어 있으므로 안전합니다.":"', "},{\"_\":\"로 이스케이프됩니다.
','를 문자열 구분 기호로 바꿀 수 있습니다.
그리고 SQL Server 2017에서는 Azure SQL 데이터베이스
새로운 것을 사용할 수 있습니다 STRING_AGG 함수
아래 코드는 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
다음 함수를 만들어 해결책을 찾았습니다.
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')
버전 참고:이 솔루션의 경우 호환성 수준이 90 이상으로 설정된 SQL Server 2005 이상을 사용해야 합니다.
이것 좀 봐 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 오류가 발생할 수 있습니다.
기타 답변의 경우 답변을 읽는 사람이 차량 테이블을 인지하고 차량 테이블과 데이터를 생성하여 솔루션을 테스트해야 합니다.
다음은 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
Mun의 답변이 저에게 효과적이지 않았기 때문에 해당 답변을 일부 변경하여 작동하게 했습니다.이것이 누군가에게 도움이 되기를 바랍니다.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]
이 쿼리를 사용해 보세요
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 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());
}
}