Bir diziyi SQL Server saklı yordamına geçirme


294

Bir diziyi SQL Server saklı yordamına nasıl geçirilir?

Örneğin, bir çalışan listem var. Bu listeyi tablo olarak kullanmak ve başka bir tabloya katılmak istiyorum. Ancak çalışanların listesi C # 'dan parametre olarak geçirilmelidir.


efendim bu bağlantı size yardımcı olacağını umuyoruz SQL Server SP bir liste / dizi geçen
patrick choi

Yanıtlar:


438

SQL Server 2008 (veya daha yenisi)

İlk olarak, veritabanınızda aşağıdaki iki nesneyi oluşturun:

CREATE TYPE dbo.IDList
AS TABLE
(
  ID INT
);
GO

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List AS dbo.IDList READONLY
AS
BEGIN
  SET NOCOUNT ON;

  SELECT ID FROM @List; 
END
GO

Şimdi C # kodunuzda:

// Obtain your list of ids to send, this is just an example call to a helper utility function
int[] employeeIds = GetEmployeeIds();

DataTable tvp = new DataTable();
tvp.Columns.Add(new DataColumn("ID", typeof(int)));

// populate DataTable from your List here
foreach(var id in employeeIds)
    tvp.Rows.Add(id);

using (conn)
{
    SqlCommand cmd = new SqlCommand("dbo.DoSomethingWithEmployees", conn);
    cmd.CommandType = CommandType.StoredProcedure;
    SqlParameter tvparam = cmd.Parameters.AddWithValue("@List", tvp);
    // these next lines are important to map the C# DataTable object to the correct SQL User Defined Type
    tvparam.SqlDbType = SqlDbType.Structured;
    tvparam.TypeName = "dbo.IDList";
    // execute query, consume results, etc. here
}

SQL Server 2005

SQL Server 2005 kullanıyorsanız, yine de XML üzerinden bölünmüş bir işlev öneriyoruz. İlk olarak, bir işlev oluşturun:

CREATE FUNCTION dbo.SplitInts
(
   @List      VARCHAR(MAX),
   @Delimiter VARCHAR(255)
)
RETURNS TABLE
AS
  RETURN ( SELECT Item = CONVERT(INT, Item) FROM
      ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
        FROM ( SELECT [XML] = CONVERT(XML, '<i>'
        + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
          ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
      WHERE Item IS NOT NULL
  );
GO

Şimdi saklı yordam sadece olabilir:

CREATE PROCEDURE dbo.DoSomethingWithEmployees
  @List VARCHAR(MAX)
AS
BEGIN
  SET NOCOUNT ON;

  SELECT EmployeeID = Item FROM dbo.SplitInts(@List, ','); 
END
GO

Ve C # kodunuzda sadece listeyi geçmek zorundasınız '1,2,3,12'...


Tablo değerli parametrelerden geçme yönteminin, onu kullanan bir çözümün sürdürülebilirliğini basitleştirdiğini ve XML ve dize bölünmesi de dahil olmak üzere diğer uygulamalara kıyasla genellikle performansı artırdığını görüyorum.

Girişler açıkça tanımlanmıştır (hiç kimse sınırlayıcının virgül mü yoksa noktalı virgül mü olduğunu tahmin etmek zorunda değildir) ve saklı yordamın kodunu incelemeden açık olmayan diğer işleme işlevlerine bağımlılığımız yoktur.

UDT'ler yerine kullanıcı tanımlı XML şemasını içeren çözümlerle karşılaştırıldığında, bu benzer sayıda adım içerir, ancak benim deneyimime göre yönetilmesi, bakımı ve okunması çok daha basit bir koddur.

Birçok çözümde, saklanan birçok yordam için yeniden kullandığınız bu UDT'lerden (Kullanıcı tanımlı Türler) yalnızca bir veya birkaçına ihtiyacınız olabilir. Bu örnekte olduğu gibi, ortak gereksinim bir kimlik işaretçileri listesinden geçmektir, işlev adı bu kimliklerin hangi bağlamı temsil etmesi gerektiğini, tür adının genel olması gerektiğini açıklar.


3
Tablo parametresi fikrini seviyorum - bunu hiç düşünmedim - şerefe. Ne için ayırıcı SplitInts () işlev çağrısına geçmesi gerekiyor.
Drammy

Yalnızca virgülle ayrılmış bir dizeye erişiminiz varsa table parametresini nasıl kullanabilirim
BDwain

@bdwain amacı yener - TVP'ye koymak için onu bölmelere ayırmak için bir bölme işlevi kullanmanız gerekir. Uygulama kodunuzdan çıkarın.
Aaron Bertrand

1
@AaronBertrand yanıt için teşekkürler, aslında sadece anladım. Bir alt parantez içinde seçmek kullanmak zorunda: SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz

3
@ th1rdey3 Bunlar dolaylı olarak isteğe bağlıdır. stackoverflow.com/a/18926590/61305
Aaron Bertrand

44

Deneyimlerime dayanarak, workerID'lerden sınırlandırılmış bir ifade oluşturarak, bu sorun için zor ve güzel bir çözüm var. Yalnızca gibi bir dize ifade oluşturmak gerekir ';123;434;365;'ki- 123, 434ve 365bazı employeeIDs bulunmaktadır. Aşağıdaki prosedürü çağırarak ve bu ifadeyi ona ileterek, istediğiniz kayıtları getirebilirsiniz. Kolayca bu sorguya "başka bir tablo" birleştirebilirsiniz. Bu çözüm, SQL sunucusunun tüm sürümleri için uygundur. Ayrıca, tablo değişkeni veya geçici tablo kullanımıyla karşılaştırıldığında, çok daha hızlı ve optimize edilmiş bir çözümdür.

CREATE PROCEDURE dbo.DoSomethingOnSomeEmployees  @List AS varchar(max)
AS
BEGIN
  SELECT EmployeeID 
  FROM EmployeesTable
  -- inner join AnotherTable on ...
  where @List like '%;'+cast(employeeID as varchar(20))+';%'
END
GO

Güzel! Int anahtarlarını filtrelediğim bu yaklaşımı gerçekten çok seviyorum! +1
MDV2000

@ MDV2000 teşekkürler :) Dize anahtarlarında, bu aynı zamanda int sütunları varchar döküm değil nedeniyle iyi bir performans var ...
Hamed Nazaktabar

Oyuna geç kaldım, ama bu çok akıllı! Sorunum için harika çalışıyor.
user441058

Bu harika! Bunu kesinlikle kullanacağım, teşekkür ederim
Omar Ruder

26

Saklı yordamınız için tablo değerli bir parametre kullanın.

C # 'dan ilettiğinizde SqlDb.Structured veri türüne sahip parametreyi eklersiniz.

Buraya bakın: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Misal:

// Assumes connection is an open SqlConnection object.
using (connection)
{
// Create a DataTable with the modified rows.
DataTable addedCategories =
  CategoriesDataTable.GetChanges(DataRowState.Added);

// Configure the SqlCommand and SqlParameter.
SqlCommand insertCommand = new SqlCommand(
    "usp_InsertCategories", connection);
insertCommand.CommandType = CommandType.StoredProcedure;
SqlParameter tvpParam = insertCommand.Parameters.AddWithValue(
    "@tvpNewCategories", addedCategories);
tvpParam.SqlDbType = SqlDbType.Structured;

// Execute the command.
insertCommand.ExecuteNonQuery();
}

17

Bir XML parametresi olarak iletmeniz gerekir.

Düzenleme: size bir fikir vermek için projemden hızlı kod:

CREATE PROCEDURE [dbo].[GetArrivalsReport]
    @DateTimeFrom AS DATETIME,
    @DateTimeTo AS DATETIME,
    @HostIds AS XML(xsdArrayOfULong)
AS
BEGIN
    DECLARE @hosts TABLE (HostId BIGINT)

    INSERT INTO @hosts
        SELECT arrayOfUlong.HostId.value('.','bigint') data
        FROM @HostIds.nodes('/arrayOfUlong/u') as arrayOfUlong(HostId)

Ardından, tablolarınızla birleştirmek için geçici tabloyu kullanabilirsiniz. ArrayOfUlong'u, veri bütünlüğünü korumak için yerleşik bir XML şeması olarak tanımladık, ancak bunu yapmak zorunda değilsiniz. Bu yüzden her zaman uzun bir XML almak emin olmak için hızlı bir kod kullanmanızı öneririz.

IF NOT EXISTS (SELECT * FROM sys.xml_schema_collections WHERE name = 'xsdArrayOfULong')
BEGIN
    CREATE XML SCHEMA COLLECTION [dbo].[xsdArrayOfULong]
    AS N'<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="arrayOfUlong">
        <xs:complexType>
            <xs:sequence>
                <xs:element maxOccurs="unbounded"
                            name="u"
                            type="xs:unsignedLong" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>';
END
GO

Birçok satır olduğunda tablo değişkenleri kullanmak kötü bir fikir olduğunu düşündüm? Bunun yerine temp (#table) tablosu kullanmak daha iyi bir performans değil mi?
Haziran'da

@ganders: Ben de tam tersini söyleyebilirim.
abatishchev

14

Dizinin boyutu ve karmaşıklığı gibi bağlam her zaman önemlidir . Küçük ve orta boy listeler için, burada yayınlanan cevapların birçoğu gayet iyi, ancak bazı açıklamalar yapılması gerekiyor:

  • Sınırlandırılmış bir listeyi bölmek için SQLCLR tabanlı ayırıcı en hızlısıdır. Kendiniz yazmak istiyorsanız veya CLR işlevlerinin ücretsiz SQL # kitaplığını indirebilirsiniz (yazdığım, ancak String_Split işlevi ve diğerleri, tamamen ücretsizdir).
  • XML tabanlı dizileri bölmek hızlı olabilir , ancak öğeye dayalı XML değil, öznitelik tabanlı XML kullanmanız gerekir (buradaki yanıtlarda gösterilen tek türdür, ancak @ AaronBertrand'ın XML örneği kodunu kullandığından en iyisidir text()XML işlevi Listeleri bölmek için XML kullanma hakkında daha fazla bilgi (yani performans analizi) için Phil Factor tarafından "SQL Server'da listeleri parametre olarak geçirmek için XML kullanma" konusuna bakın .
  • Veriler proc'a akış olarak aktarıldığından ve önceden ayrıştırılmış ve tablo değişkeni olarak güçlü bir şekilde yazıldığından TVP'leri kullanmak harika (en az SQL Server 2008 veya daha yeni bir sürüm kullandığınızı varsayarak). ANCAK, çoğu durumda, tüm verilerin saklanması DataTable, orijinal koleksiyondan kopyalanırken verilerin bellekteki çoğaltılması anlamına gelir. Bu nedenle DataTable, TVP'lere geçme yöntemini kullanmak daha büyük veri setleri için iyi çalışmaz (yani iyi ölçeklenmez).
  • XML, Ints veya Strings gibi basit sınırlandırılmış listelerin aksine, tıpkı TVP'ler gibi tek boyutlu dizileri işleyebilir. Ama aynı zamanda DataTableTVP yöntemi gibi , XML, ek olarak XML belgesinin ek yükünü de hesaba katması gerektiği için bellekteki veri boyutunu iki katından daha iyi ölçeklemez.

Tüm bunlarla birlikte, kullandığınız veriler büyükse veya çok büyük değilse, ancak sürekli olarak büyüyorsa, IEnumerableTVP yöntemi verileri SQL Server'a ( DataTableyöntem gibi ) aktardığı için en iyi seçimdir , ancak koleksiyonun bellekte kopyalanmasını gerektirir (diğer yöntemlerin aksine). Bu cevapta SQL ve C # kodunun bir örneğini yayınladım:

Saklı Yordam T-SQL Sözlüğü Geçer


6

Sql sunucusunda dizi için destek yoktur, ancak koleksiyonu depolanan bir proc'a geçirmenin birkaç yolu vardır.

  1. Datatable kullanarak
  2. XML kullanarak koleksiyonunuzu bir xml biçiminde dönüştürmeyi deneyin ve daha sonra depolanan yordama girdi olarak iletin

Aşağıdaki bağlantı size yardımcı olabilir

koleksiyonun saklı bir prosedüre geçirilmesi


5

Bütün aracılığıyla örnekler ve ben bu bulana kadar, yeni Tablo türü oluşturarak uğraşmadan sql sunucuya herhangi dizi geçirilecek cevaplarını aradım linki aşağıda benim projeye uyguladı nasıl:

- Aşağıdaki kod, Parametre olarak bir Dizi alacak ve bu --array'ın değerlerini başka bir tabloya ekleyecektir

Create Procedure Proc1 


@UserId int, //just an Id param
@s nvarchar(max)  //this is the array your going to pass from C# code to your Sproc

AS

    declare @xml xml

    set @xml = N'<root><r>' + replace(@s,',','</r><r>') + '</r></root>'

    Insert into UserRole (UserID,RoleID)
    select 
       @UserId [UserId], t.value('.','varchar(max)') as [RoleId]


    from @xml.nodes('//root/r') as a(t)
END 

Umarım tadını çıkarırsın


2
@zaitsman: CLEANEST en iyi ya da en uygun anlamına gelmez. Biri genellikle "temiz" kod almak için esneklik ve / veya "uygun" karmaşıklık ve / veya performanstan vazgeçer. Buradaki cevap "tamam" ama sadece küçük veri kümeleri için. Gelen dizi @sCSV ise, bunu basitçe bölmek daha hızlı olur (örneğin, INSERT INTO ... SELECT FROM SplitFunction). XML'ye dönüştürmek CLR'den daha yavaştır ve özniteliğe dayalı XML yine de çok daha hızlıdır. Ve bu basit bir liste henüz XML veya TVP geçmek de karmaşık diziler işleyebilir. Basit, bir kerelik kaçınarak ne kazanıldığından emin değilim CREATE TYPE ... AS TABLE.
Solomon Rutzky

5

Bu sana yardım edecek. :) Sonraki adımları izleyin,

  1. Sorgu Tasarımcısı'nı açma
  2. Kopyala Aşağıdaki kodu olduğu gibi yapıştırdığınızda, String'i Int'ye dönüştüren İşlev oluşturulur

    CREATE FUNCTION dbo.SplitInts
    (
       @List      VARCHAR(MAX),
       @Delimiter VARCHAR(255)
    )
    RETURNS TABLE
    AS
      RETURN ( SELECT Item = CONVERT(INT, Item) FROM
          ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')
            FROM ( SELECT [XML] = CONVERT(XML, '<i>'
            + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')
              ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y
          WHERE Item IS NOT NULL
      );
    GO
  3. Aşağıdaki saklı yordamı oluşturun

     CREATE PROCEDURE dbo.sp_DeleteMultipleId
     @List VARCHAR(MAX)
     AS
     BEGIN
          SET NOCOUNT ON;
          DELETE FROM TableName WHERE Id IN( SELECT Id = Item FROM dbo.SplitInts(@List, ',')); 
     END
     GO
  4. Bu SP'yi yürütün Bu exec sp_DeleteId '1,2,3,12', silmek istediğiniz bir dizi Kimlik,

  5. Dizinizi C # 'da dizeye dönüştürür ve bir Saklı Yordam parametresi olarak iletirsiniz

    int[] intarray = { 1, 2, 3, 4, 5 };  
    string[] result = intarray.Select(x=>x.ToString()).ToArray();

     

    SqlCommand command = new SqlCommand();
    command.Connection = connection;
    command.CommandText = "sp_DeleteMultipleId";
    command.CommandType = CommandType.StoredProcedure;
    command.Parameters.Add("@Id",SqlDbType.VARCHAR).Value=result ;

Bu, birden çok satırı siler, En iyisi


Bu virgül ayrı ayrıştırma işlevini kullandım, u küçük veri kümesi için çalışacak, eğer u uygulama planını kontrol ederseniz, büyük veri setinde soruna neden olacak ve nerede saklı yordamda birden fazla csv listesine ihtiyacınız var
Saboor Awan

2

Bunu çözmem çok uzun sürdü, bu yüzden birinin ihtiyacı olması durumunda ...

Bu, Aaron'un cevabındaki SQL 2005 yöntemine ve SplitInts işlevini kullanıyor (her zaman virgül kullanacağımdan beri delim parametresini kaldırdım). SQL 2008 kullanıyorum ama yazılan veri kümeleri (XSD, TableAdapters) ile çalışan bir şey istedim ve dize params bunlarla çalışmak biliyorum.

Ben onun işlevini bir "nerede (1,2,3)" tipi yan tümcesinde çalışmak için almaya çalışıyordum ve düz ileri yolu şanssız. Bu yüzden önce bir geçici tablo oluşturdum ve sonra "nerede" yerine bir iç birleşim yaptım. İşte benim örnek kullanım, benim durumumda belirli maddeler içermeyen tariflerin bir listesini almak istedim:

CREATE PROCEDURE dbo.SOExample1
    (
    @excludeIngredientsString varchar(MAX) = ''
    )
AS
    /* Convert string to table of ints */
    DECLARE @excludeIngredients TABLE (ID int)
    insert into @excludeIngredients
    select ID = Item from dbo.SplitInts(@excludeIngredientsString)

    /* Select recipies that don't contain any ingredients in our excluded table */
   SELECT        r.Name, r.Slug
FROM            Recipes AS r LEFT OUTER JOIN
                         RecipeIngredients as ri inner join
                         @excludeIngredients as ei on ri.IngredientID = ei.ID
                         ON r.ID = ri.RecipeID
WHERE        (ri.RecipeID IS NULL)

Genel olarak konuşursak, bir Tablo Değişkeni'ne KATILMAMAK, bunun yerine Temp Tablosuna katılmak en iyisidir. Tablo Değişkenleri, varsayılan olarak, yalnızca bir satır var gibi görünür, ancak bunun etrafında bir veya iki hile vardır (@ AaronBertrand'ın mükemmel ve ayrıntılı makalesine göz atın : sqlperformance.com/2014/06/t-sql-queries/… ).
Solomon Rutzky

1

Diğerlerinin yukarıda belirttiği gibi, bunu yapmanın bir yolu dizinizi bir dizeye dönüştürmek ve sonra dizeyi SQL Server içinde bölmektir.

SQL Server 2016'dan itibaren, dizeleri ayırmak için yerleşik bir yol var.

STRING_SPLIT ()

Geçici tablonuza (veya gerçek tablonuza) ekleyebileceğiniz bir dizi satır döndürür.

DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"
SELECT value FROM STRING_SPLIT(@str, ';')

verim verecek:

değer
-----
  123
  456
  789
  246
   22
   33
   44
   55
   66

Daha meraklı olmak istiyorsanız:

DECLARE @tt TABLE (
    thenumber int
)
DECLARE @str varchar(200)
SET @str = "123;456;789;246;22;33;44;55;66"

INSERT INTO @tt
SELECT value FROM STRING_SPLIT(@str, ';')

SELECT * FROM @tt
ORDER BY thenumber

yukarıdakiyle aynı sonuçları verir (sütun adı "thenumber" dır), ancak sıralanır. Tablo değişkenini herhangi bir tablo gibi kullanabilirsiniz, böylece isterseniz DB'deki diğer tablolarla kolayca birleştirebilirsiniz.

STRING_SPLIT()Fonksiyonun tanınabilmesi için SQL Server kurulumunuzun uyumluluk seviyesi 130 veya daha yüksek olması gerektiğini unutmayın. Uyumluluk düzeyinizi aşağıdaki sorgu ile kontrol edebilirsiniz:

SELECT compatibility_level
FROM sys.databases WHERE name = 'yourdatabasename';

Çoğu dilde (C # dahil) bir diziden dize oluşturmak için kullanabileceğiniz bir "birleştirme" işlevi vardır.

int[] myarray = {22, 33, 44};
string sqlparam = string.Join(";", myarray);

Ardından sqlparamparametreniz olarak yukarıdaki saklı yordama geçersiniz .


0
CREATE TYPE dumyTable
AS TABLE
(
  RateCodeId int,
  RateLowerRange int,
  RateHigherRange int,
  RateRangeValue int
);
GO
CREATE PROCEDURE spInsertRateRanges
  @dt AS dumyTable READONLY
AS
BEGIN
  SET NOCOUNT ON;

  INSERT  tblRateCodeRange(RateCodeId,RateLowerRange,RateHigherRange,RateRangeValue) 
  SELECT * 
  FROM @dt 
END
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.