كيفية إنشاء وظيفة SQL Server "لضم" صفوف متعددة من استعلام فرعي إلى حقل محدد واحد؟[ينسخ]

StackOverflow https://stackoverflow.com/questions/6899

سؤال

هذا السؤال لديه بالفعل إجابة هنا:

للتوضيح، افترض أن لدي جدولين على النحو التالي:

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());
    }
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top