Come passare un array in una procedura memorizzata di SQL Server


293

Come passare un array in una procedura memorizzata di SQL Server?

Ad esempio, ho un elenco di dipendenti. Voglio usare questo elenco come tabella e unirlo a un'altra tabella. Ma l'elenco dei dipendenti deve essere passato come parametro da C #.


signore spero che questo link ti aiuti a passare un elenco / array a SQL Server SP
patrick choi

Risposte:


437

SQL Server 2008 (o più recente)

Innanzitutto, nel database, creare i seguenti due oggetti:

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

Ora nel tuo codice C #:

// 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

Se si utilizza SQL Server 2005, consiglierei comunque una funzione suddivisa su XML. Innanzitutto, crea una funzione:

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

Ora la procedura memorizzata può essere solo:

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

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

E nel tuo codice C # devi solo passare l'elenco come '1,2,3,12'...


Trovo che il metodo per passare attraverso i parametri con valori di tabella semplifichi la manutenibilità di una soluzione che lo utilizza e spesso ha prestazioni migliorate rispetto ad altre implementazioni tra cui XML e suddivisione delle stringhe.

Gli input sono chiaramente definiti (nessuno deve indovinare se il delimitatore è una virgola o un punto e virgola) e non abbiamo dipendenze da altre funzioni di elaborazione che non sono ovvie senza ispezionare il codice per la procedura memorizzata.

Rispetto alle soluzioni che coinvolgono lo schema XML definito dall'utente anziché gli UDT, ciò comporta un numero simile di passaggi, ma nella mia esperienza è un codice molto più semplice da gestire, mantenere e leggere.

In molte soluzioni potrebbe essere necessario solo uno o alcuni di questi UDT (tipi definiti dall'utente) che si possono riutilizzare per molte procedure memorizzate. Come in questo esempio, il requisito comune è passare attraverso un elenco di puntatori ID, il nome della funzione descrive il contesto che tali ID dovrebbero rappresentare, il nome del tipo dovrebbe essere generico.


3
Mi piace l'idea dei parametri della tabella - non ci ho mai pensato - evviva. Per quello che vale, il delimitatore deve passare nella chiamata della funzione SplitInts ().
Drammy

Come posso utilizzare il parametro table se ho accesso solo a una stringa separata da virgole
bdwain,

@bdwain avrebbe vanificato lo scopo - dovresti usare una funzione split per dividerla in righe da inserire nel TVP. Inseriscilo nel codice dell'applicazione.
Aaron Bertrand

1
@AaronBertrand grazie per la risposta, in realtà l'ho appena capito. Devo usare un sub selezionare tra le parentesi: SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz,

3
@ th1rdey3 Sono implicitamente facoltativi. stackoverflow.com/a/18926590/61305
Aaron Bertrand

44

Sulla base della mia esperienza, creando un'espressione delimitata dagli impiegatiID, esiste una soluzione delicata e piacevole per questo problema. Dovresti solo creare un'espressione stringa come ';123;434;365;'in-quale 123, 434e 365sono alcuni impiegatiID. Chiamando la procedura seguente e passando ad essa questa espressione, è possibile recuperare i record desiderati. Puoi facilmente unire l '"altra tabella" a questa query. Questa soluzione è adatta a tutte le versioni di SQL Server. Inoltre, rispetto all'utilizzo di variabili di tabella o tabelle temporanee, è una soluzione molto più veloce e ottimizzata.

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

Bello! Mi piace molto questo approccio in cui sto filtrando i tasti int! +1
MDV2000,

@ MDV2000 grazie :) Sulle chiavi di stringa, anche queste hanno buone prestazioni a causa del fatto che non si
lanciano

Sono in ritardo al gioco, ma questo è molto intelligente! Funziona benissimo per il mio problema.
user441058

Questo è fantastico! Lo userò sicuramente, grazie
Omar Ruder,

26

Utilizzare un parametro con valori di tabella per la procedura memorizzata.

Quando lo passi da C #, aggiungi il parametro con il tipo di dati di SqlDb.Structured.

Vedi qui: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Esempio:

// 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

Devi passarlo come parametro XML.

Modifica: codice rapido dal mio progetto per darti un'idea:

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)

Quindi puoi utilizzare la tabella temporanea per unirti ai tuoi tavoli. Abbiamo definito arrayOfUlong come uno schema XML incorporato per mantenere l'integrità dei dati, ma non è necessario farlo. Ti consiglierei di usarlo, quindi ecco un codice rapido per assicurarti di ottenere sempre un XML con long.

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

Ho pensato che fosse una cattiva idea usare le variabili di tabella quando ci sono molte righe? Invece, non è meglio usare una tabella temporanea (#table)?
Ganders,

@ganders: direi viceversa.
abatishchev,

14

Il contesto è sempre importante, come le dimensioni e la complessità dell'array. Per gli elenchi di dimensioni medio-piccole, molte delle risposte pubblicate qui vanno bene, anche se alcuni chiarimenti dovrebbero essere fatti:

  • Per dividere un elenco delimitato, uno splitter basato su SQLCLR è il più veloce. Ci sono numerosi esempi in giro se vuoi scriverne uno tuo o puoi semplicemente scaricare il codice SQL gratuito libreria gratuita di funzioni CLR (che ho scritto, ma la funzione String_Split e molte altre sono completamente gratuite).
  • La suddivisione di array basati su XML può essere rapida, ma è necessario utilizzare XML basato su attributi, non XML basato su elementi (che è l'unico tipo mostrato nelle risposte qui, sebbene l'esempio XML di @ AaronBertrand sia il migliore in quanto il suo codice sta usando il text()Funzione XML Per ulteriori informazioni (ad es. Analisi delle prestazioni) sull'uso di XML per dividere gli elenchi, consultare "Utilizzo di XML per passare elenchi come parametri in SQL Server" di Phil Factor.
  • L'uso dei TVP è fantastico (supponendo che si stia utilizzando almeno SQL Server 2008 o più recente) poiché i dati vengono trasmessi in streaming al proc e vengono visualizzati pre-analizzati e tipizzati fortemente come una variabile di tabella. TUTTAVIA, nella maggior parte dei casi, archiviare tutti i dati DataTablesignifica duplicare i dati in memoria mentre vengono copiati dalla raccolta originale. Quindi usando ilDataTable metodo di trasmissione dei TVP non funziona bene per gruppi di dati più grandi (ovvero non si adatta bene).
  • XML, a differenza dei semplici elenchi delimitati di Ints o String, può gestire più di array unidimensionali, proprio come i TVP. Ma proprio come il DataTablemetodo TVP, XML non si adatta bene in quanto raddoppia la dimensione dei dati in memoria in quanto deve inoltre tenere conto dell'overhead del documento XML.

Detto questo, SE i dati che stai utilizzando sono grandi o non molto grandi ma in costante crescita, il IEnumerablemetodo TVP è la scelta migliore in quanto trasmette i dati a SQL Server (come ilDataTable metodo), MA NON richiedere qualsiasi duplicazione della raccolta in memoria (diversamente da qualsiasi altro metodo). Ho pubblicato un esempio del codice SQL e C # in questa risposta:

Passare il dizionario alla stored procedure T-SQL


6

Non c'è supporto per l'array nel server sql ma ci sono diversi modi con cui è possibile passare la raccolta a un proc memorizzato.

  1. Utilizzando datatable
  2. Utilizzando XML. Prova a convertire la tua raccolta in un formato XML e quindi passala come input a una procedura memorizzata

Il seguente link può aiutarti

passare la raccolta a una procedura memorizzata


5

Ho cercato tutti gli esempi e le risposte su come passare qualsiasi array al server sql senza il fastidio di creare un nuovo tipo di tabella, fino a quando non ho trovato questo linK , di seguito è riportato come l'ho applicato al mio progetto:

- Il codice seguente otterrà un array come parametro e inserirà i valori di quell'array in un'altra tabella

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 

Spero ti sia piaciuto


2
@zaitsman: CLEANEST non significa il migliore o il più appropriato. Spesso si rinuncia alla flessibilità e / o alla complessità e / o alle prestazioni "appropriate" per ottenere un codice "pulito". Questa risposta qui è "ok" ma solo per piccoli set di dati. Se l'array in entrata @sè CSV, sarebbe più veloce dividerlo semplicemente (ovvero INSERT INTO ... SELECT FROM SplitFunction). La conversione in XML è più lenta di CLR e l'XML basato sugli attributi è comunque molto più veloce. E questo è un semplice elenco ma passare in XML o TVP può anche gestire array complessi. Non sono sicuro di ciò che si ottiene evitando un semplice, una volta CREATE TYPE ... AS TABLE.
Solomon Rutzky,

5

Questo ti aiuterà. :) Segui i passaggi successivi,

  1. Apri Progettazione query
  2. Copia Incolla il seguente codice così com'è, creerà la funzione che converte la stringa in Int

    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. Creare la seguente procedura memorizzata

     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. Esegui questo SP Usando exec sp_DeleteId '1,2,3,12'questa è una stringa di ID che vuoi eliminare,

  5. Converti l'array in stringa in C # e lo passi come parametro Stored Procedure

    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 ;

Questo eliminerà più righe, tutto il meglio


ho usato questa funzione di analisi separata da virgola, funzionerebbe per un piccolo set di dati, se controlli il suo piano di esecuzione, causerà problemi su un set di dati di grandi dimensioni e in cui
devi

2

Mi ci è voluto molto tempo per capirlo, quindi nel caso qualcuno ne avesse bisogno ...

Questo si basa sul metodo SQL 2005 nella risposta di Aaron e sull'uso della sua funzione SplitInts (ho appena rimosso il parametro delim dal momento che userò sempre le virgole). Sto usando SQL 2008 ma volevo qualcosa che funzioni con set di dati digitati (XSD, TableAdapters) e so che i parametri di stringa funzionano con quelli.

Stavo cercando di far funzionare la sua funzione in una clausola di tipo "where in (1,2,3)", senza avere fortuna nel modo diretto. Quindi ho creato prima una tabella temporanea, quindi ho fatto un join interno anziché "where in". Ecco il mio esempio di utilizzo, nel mio caso volevo ottenere un elenco di ricette che non contengono determinati ingredienti:

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)

In generale, è meglio non unirsi a una variabile di tabella, ma piuttosto a una tabella temporanea. Le variabili di tabella, per impostazione predefinita, sembrano avere solo una riga, anche se c'è un trucco o due intorno a questo (controlla l'articolo eccellente e dettagliato di @ AaronBertrand: sqlperformance.com/2014/06/t-sql-queries/… ).
Solomon Rutzky,

1

Come altri hanno già notato, un modo per farlo è convertire l'array in una stringa e quindi dividere la stringa all'interno di SQL Server.

A partire da SQL Server 2016, esiste un modo integrato per dividere le stringhe chiamato

STRING_SPLIT ()

Restituisce un set di righe che è possibile inserire nella tabella temporanea (o tabella reale).

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

produrrebbe:

valore
-----
  123
  456
  789
  246
   22
   33
   44
   55
   66

Se vuoi diventare più elaborato:

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

ti darebbe gli stessi risultati di cui sopra (tranne che il nome della colonna è "thenumber"), ma ordinato. È possibile utilizzare la variabile di tabella come qualsiasi altra tabella, quindi è possibile unirla facilmente con altre tabelle nel DB, se lo si desidera.

Si noti che l'installazione di SQL Server deve essere al livello di compatibilità 130 o superiore per STRING_SPLIT()poter riconoscere la funzione. Puoi verificare il tuo livello di compatibilità con la seguente query:

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

Molte lingue (incluso C #) hanno una funzione "join" che puoi usare per creare una stringa da un array.

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

Quindi si passa sqlparamcome parametro alla procedura memorizzata sopra.


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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.