كيفية إنشاء وظيفة 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]
إنه أسهل بكثير من استخدام المؤشر، ويبدو أنه يعمل بشكل جيد إلى حد ما.
نصائح أخرى
لاحظ أن كود مات سيؤدي إلى فاصلة إضافية في نهاية السلسلة؛استخدام COALESCE (أو ISNULL لهذه المسألة) كما هو موضح في الرابط الموجود في منشور Lance يستخدم طريقة مشابهة ولكنه لا يترك لك فاصلة إضافية لإزالتها.من أجل الاكتمال، إليك الكود ذي الصلة من رابط Lance على sqlteam.com:
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...
لا يسمح بإدراج أعمدة أخرى.
تحديث:شكرا ل برمجة الحلول.نت هناك طريقة لإزالة الفاصلة "الزائدة" إلى.عن طريق تحويلها إلى فاصلة بادئة واستخدام 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 Server 2016
يمكنك استخدام ال لبناء جملة 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')
ملاحظة الإصدار:يجب أن تستخدم SQL Server 2005 أو إصدار أحدث مع تعيين مستوى التوافق على 90 أو أكبر لهذا الحل.
انظر الى هذا مقالة MSDN للحصول على المثال الأول لإنشاء دالة تجميعية معرفة من قبل المستخدم تقوم بتسلسل مجموعة من قيم السلسلة المأخوذة من عمود في جدول.
توصيتي المتواضعة هي ترك الفاصلة الملحقة حتى تتمكن من استخدام المحدد المخصص الخاص بك، إن وجد.
بالإشارة إلى إصدار C# من المثال 1:
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] المخزن المؤقت غير كافٍ."
بالنسبة للإجابات الأخرى، يجب على الشخص الذي يقرأ الإجابة أن يكون على دراية بجدول المركبات وأن يقوم بإنشاء جدول المركبات وبياناتها لاختبار الحل.
يوجد أدناه مثال يستخدم جدول 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]
جرب هذا الاستعلام
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 المخصصة للتعامل مع هذا.
نسخة سي#:
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());
}
}