Sütunu birden çok satırdan tek satıra birleştirin


14

Bazı customer_commentsveritabanı tasarımı nedeniyle birden çok satıra ayrıldım ve bir rapor için commentsher birinden tek idbir satıra birleştirmem gerekiyor . Daha önce SELECT yan tümcesi ve COALESCE numarasından bu sınırlı liste ile çalışan bir şey denedim ama hatırlayamıyorum ve kaydetmemiş olmalıyım. Bu durumda da işe yarayamıyorum, sadece tek bir satırda çalışıyor gibi görünüyor.

Veriler şöyle görünür:

id  row_num  customer_code comments
-----------------------------------
1   1        Dilbert        Hard
1   2        Dilbert        Worker
2   1        Wally          Lazy

Sonuçlarımın şöyle görünmesi gerekiyor:

id  customer_code comments
------------------------------
1   Dilbert        Hard Worker
2   Wally          Lazy

Yani her biri için row_numgerçekten sadece bir satır sonuç var; yorumlar sırayla birleştirilmelidir row_num. Yukarıdaki bağlantılı SELECThile belirli bir sorgu için tüm değerleri bir satır olarak almak için çalışır, ancak SELECTtüm bu satırları tüküren bir ifadenin bir parçası olarak çalışmasını nasıl anlayamıyorum.

Benim sorgu tüm tablo kendi başına gitmek ve bu satırları çıktı gerekir. Bunları birden çok sütunda birleştirmiyorum, her satır için bir tane, bu yüzden PIVOTuygulanabilir görünmüyor.

Yanıtlar:


18

Bu, ilişkili bir alt sorgu ile nispeten önemsizdir. Kullanıcı tanımlı bir işleve ayıklamadığınız sürece (veya her seferinde yalnızca bir satır döndürmek istemediğiniz sürece) bahsettiğiniz blog yayında vurgulanan COALESCE yöntemini kullanamazsınız. Ben tipik olarak bunu nasıl yapılır:

DECLARE @x TABLE 
(
  id INT, 
  row_num INT, 
  customer_code VARCHAR(32), 
  comments VARCHAR(32)
);

INSERT @x SELECT 1,1,'Dilbert','Hard'
UNION ALL SELECT 1,2,'Dilbert','Worker'
UNION ALL SELECT 2,1,'Wally','Lazy';

SELECT id, customer_code, comments = STUFF((SELECT ' ' + comments 
    FROM @x AS x2 WHERE id = x.id
     ORDER BY row_num
     FOR XML PATH('')), 1, 1, '')
FROM @x AS x
GROUP BY id, customer_code
ORDER BY id;

Yorumlardaki veriler güvensiz-için-XML karakterlerini içerebilir nerede bir kılıf varsa ( >, <, &), bunu değiştirmek gerekir:

     FOR XML PATH('')), 1, 1, '')

Bu daha ayrıntılı yaklaşıma:

     FOR XML PATH(''), TYPE).value(N'(./text())[1]', N'varchar(max)'), 1, 1, '')

(Sağ hedef veri türünü, kullandığınızdan emin olun varcharveya nvarchartüm dize hazır ve sağ uzunluğu ve ön ek Nkullanılıyorsa nvarchar.)


3
1 Bir hızlı bir bakış için bunun için bir keman creadted sqlfiddle.com/#!3/e4ee5/2
MarlonRibunal

3
Evet, bu bir cazibe gibi çalışıyor. @MarlonRibunal SQL Fiddle gerçekten şekilleniyor!
Ben Brocka

@NickChammas - Ben boynumu sopa ve sipariş order byalt sorguda kullanılarak garanti olduğunu söyleyeceğim . Bu XML kullanarak inşa for xmlve TSQL kullanarak XML oluşturmak için bir yoldur. XML dosyalarındaki öğelerin sırası önemli bir konudur ve buna güvenilebilir. Bu yüzden bu teknik siparişi garanti etmiyorsa, TSQL'deki XML desteği ciddi şekilde bozulur.
Mikael Eriksson

2
Ben temel tablodaki kümelenmiş dizin (hatta bir kümelenmiş dizin bile Mikael önerilen row_num descuymak gerekir) ne olursa olsun sorgu sonuçları doğru sırayla döndürecek doğruladı order by. Ben şimdi sorgu doğru içeriyor order byve @JonSeigel aynı şeyi düşünüyor umut içerdiğini düşündüren yorumları kaldıracağım .
Aaron Bertrand

6

Ortamınızda CLR kullanma izniniz varsa, bu, kullanıcı tanımlı bir toplama için özel olarak hazırlanmış bir durumdur.

Özellikle, kaynak veriler önemsiz derecede büyükse ve / veya bu tür bir şeyi uygulamanızda çok fazla yapmanız gerekiyorsa, muhtemelen bu yoldur. Aaron'un çözümü için sorgu planının girdi boyutu büyüdükçe iyi ölçeklenmeyeceğinden şüpheleniyorum . (Geçici tabloya bir dizin eklemeyi denedim, ancak bu yardımcı olmadı.)

Bu çözüm, diğer birçok şey gibi, bir ödünleşmedir:

  • CLR Entegrasyonunu sizin veya müşterinizin ortamında bile kullanmak için politika / politika.
  • CLR işlevi daha hızlıdır ve gerçek bir veri kümesi göz önüne alındığında daha iyi ölçeklendirilir.
  • CLR işlevi diğer sorgularda yeniden kullanılabilir ve bu tür şeyleri her yapmanız gerektiğinde karmaşık bir alt sorguyu çoğaltmanız (ve hata ayıklamanız) gerekmez.
  • Düz T-SQL bir parça harici kod yazmaktan ve yönetmekten daha kolaydır.
  • Belki de C # veya VB'de nasıl programlayacağınızı bilmiyorsunuz.
  • vb.

DÜZENLEME: Eh, bunun gerçekten daha iyi olup olmadığını görmek için gittim ve yorumların belirli bir sırayla olması şartıyla bir toplama işlevi kullanarak tatmin etmek mümkün değildir. :(

Bkz. SqlUserDefinedAggregateAttribute.IsInvariantToOrder . Temelde, ne yapmak gerekir olduğunu OVER(PARTITION BY customer_code ORDER BY row_num)ancak ORDER BYdesteklenmez OVERtoplama yaparken maddesi. Bu işlevselliği SQL Server'a ekleyerek yürütme planında değiştirilmesi gerekecek çünkü solucanlar bir kutu açar varsayıyorum. Yukarıda adı geçen bağlantı, bunun gelecekteki kullanım için ayrıldığını söylüyor, bu nedenle bu gelecekte uygulanabilir (2005'te muhtemelen şansınız kalmadı).

Bu olabilir hala paketleme ve ayrıştırma ile gerçekleştirilebilir row_numoldukça hackish görünüyor CLR nesne içinde çeşit ... yapıyor sonra toplanan dizeye değer ve.

Her halükarda, başka birinin bu sınırlamada bile yararlı bulması durumunda kullandığım kod aşağıdadır. Bilgisayar korsanlığını okuyucu için bir egzersiz olarak bırakacağım. Test verileri için AdventureWorks (2005) kullandığımı unutmayın.

Agrega montajı:

using System;
using System.IO;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;

namespace MyCompany.SqlServer
{
    [Serializable]
    [SqlUserDefinedAggregate
    (
        Format.UserDefined,
        IsNullIfEmpty = false,
        IsInvariantToDuplicates = false,
        IsInvariantToNulls = true,
        IsInvariantToOrder = false,
        MaxByteSize = -1
    )]
    public class StringConcatAggregate : IBinarySerialize
    {
        private string _accum;
        private bool _isEmpty;

        public void Init()
        {
            _accum = string.Empty;
            _isEmpty = true;
        }

        public void Accumulate(SqlString value)
        {
            if (!value.IsNull)
            {
                if (!_isEmpty)
                    _accum += ' ';
                else
                    _isEmpty = false;

                _accum += value.Value;
            }
        }

        public void Merge(StringConcatAggregate value)
        {
            Accumulate(value.Terminate());
        }

        public SqlString Terminate()
        {
            return new SqlString(_accum);
        }

        public void Read(BinaryReader r)
        {
            this.Init();

            _accum = r.ReadString();
            _isEmpty = _accum.Length == 0;
        }

        public void Write(BinaryWriter w)
        {
            w.Write(_accum);
        }
    }
}

Test için T-SQL ( CREATE ASSEMBLYve sp_configureCLR'yi atlamak için):

CREATE TABLE [dbo].[Comments]
(
    CustomerCode int NOT NULL,
    RowNum int NOT NULL,
    Comments nvarchar(25) NOT NULL
)

INSERT INTO [dbo].[Comments](CustomerCode, RowNum, Comments)
    SELECT
        DENSE_RANK() OVER(ORDER BY FirstName),
        ROW_NUMBER() OVER(PARTITION BY FirstName ORDER BY ContactID),
        Phone
        FROM [AdventureWorks].[Person].[Contact]
GO

CREATE AGGREGATE [dbo].[StringConcatAggregate]
(
    @input nvarchar(MAX)
)
RETURNS nvarchar(MAX)
EXTERNAL NAME StringConcatAggregate.[MyCompany.SqlServer.StringConcatAggregate]
GO


SELECT
    CustomerCode,
    [dbo].[StringConcatAggregate](Comments) AS AllComments
    FROM [dbo].[Comments]
    GROUP BY CustomerCode

1

İşte yorumların sırasını garanti eden imleç tabanlı bir çözüm row_num. ( Tablonun nasıl doldurulduğuna dair diğer cevabımı görün [dbo].[Comments].)

SET NOCOUNT ON

DECLARE cur CURSOR LOCAL FAST_FORWARD FOR
    SELECT
        CustomerCode,
        Comments
        FROM [dbo].[Comments]
        ORDER BY
            CustomerCode,
            RowNum

DECLARE @curCustomerCode int
DECLARE @lastCustomerCode int
DECLARE @curComment nvarchar(25)
DECLARE @comments nvarchar(MAX)

DECLARE @results table
(
    CustomerCode int NOT NULL,
    AllComments nvarchar(MAX) NOT NULL
)


OPEN cur

FETCH NEXT FROM cur INTO
    @curCustomerCode, @curComment

SET @lastCustomerCode = @curCustomerCode


WHILE @@FETCH_STATUS = 0
BEGIN

    IF (@lastCustomerCode != @curCustomerCode)
    BEGIN
        INSERT INTO @results(CustomerCode, AllComments)
            VALUES(@lastCustomerCode, @comments)

        SET @lastCustomerCode = @curCustomerCode
        SET @comments = NULL
    END

    IF (@comments IS NULL)
        SET @comments = @curComment
    ELSE
        SET @comments = @comments + N' ' + @curComment

    FETCH NEXT FROM cur INTO
        @curCustomerCode, @curComment

END

IF (@comments IS NOT NULL)
BEGIN
    INSERT INTO @results(CustomerCode, AllComments)
        VALUES(@curCustomerCode, @comments)
END

CLOSE cur
DEALLOCATE cur


SELECT * FROM @results

0
-- solution avoiding the cursor ...

DECLARE @idMax INT
DECLARE @idCtr INT
DECLARE @comment VARCHAR(150)

SELECT @idMax = MAX(id)
FROM [dbo].[CustomerCodeWithSeparateComments]

IF @idMax = 0
    return
DECLARE @OriginalTable AS Table
(
    [id] [int] NOT NULL,
    [row_num] [int] NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

DECLARE @FinalTable AS Table
(
    [id] [int] IDENTITY(1,1) NOT NULL,
    [customer_code] [varchar](50) NULL,
    [comment] [varchar](120) NULL
)

INSERT INTO @FinalTable 
([customer_code])
SELECT [customer_code]
FROM [dbo].[CustomerCodeWithSeparateComments]
GROUP BY [customer_code]

INSERT INTO @OriginalTable
           ([id]
           ,[row_num]
           ,[customer_code]
           ,[comment])
SELECT [id]
      ,[row_num]
      ,[customer_code]
      ,[comment]
FROM [dbo].[CustomerCodeWithSeparateComments]
ORDER BY id, row_num

SET @idCtr = 1
SET @comment = ''

WHILE @idCtr < @idMax
BEGIN

    SELECT @comment = @comment + ' ' + comment
    FROM @OriginalTable 
    WHERE id = @idCtr
    UPDATE @FinalTable
       SET [comment] = @comment
    WHERE [id] = @idCtr 
    SET @idCtr = @idCtr + 1
    SET @comment = ''

END 

SELECT @comment = @comment + ' ' + comment
        FROM @OriginalTable 
        WHERE id = @idCtr

UPDATE @FinalTable
   SET [comment] = @comment
WHERE [id] = @idCtr

SELECT *
FROM @FinalTable

2
Bir imleçten kaçınmadınız. Bunun yerine imlecinizi bir while döngüsü olarak adlandırdınız.
Aaron Bertrand
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.