Übergeben eines Arrays an eine gespeicherte SQL Server-Prozedur

293

Wie übergebe ich ein Array an eine gespeicherte SQL Server-Prozedur?

Zum Beispiel habe ich eine Liste von Mitarbeitern. Ich möchte diese Liste als Tabelle verwenden und sie mit einer anderen Tabelle verbinden. Die Liste der Mitarbeiter sollte jedoch als Parameter von C # übergeben werden.

Sergey
quelle
Sir, ich hoffe, dieser Link hilft Ihnen beim Übergeben einer Liste / eines Arrays an SQL Server SP
Patrick Choi
Es ist dieselbe Klasse wie Parametrisieren einer SQL IN-Klausel
Lukasz Szozda

Antworten:

437

SQL Server 2008 (oder neuer)

Erstellen Sie zunächst in Ihrer Datenbank die folgenden zwei Objekte:

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

Jetzt in Ihrem C # -Code:

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

Wenn Sie SQL Server 2005 verwenden, würde ich weiterhin eine Split-Funktion über XML empfehlen. Erstellen Sie zunächst eine Funktion:

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

Jetzt kann Ihre gespeicherte Prozedur einfach sein:

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

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

Und in Ihrem C # -Code müssen Sie nur die Liste als '1,2,3,12'...


Ich finde, dass die Methode zum Durchlaufen von Parametern mit Tabellenwerten die Wartbarkeit einer Lösung, die sie verwendet, vereinfacht und im Vergleich zu anderen Implementierungen, einschließlich XML und Aufteilen von Zeichenfolgen, häufig eine höhere Leistung aufweist.

Die Eingaben sind klar definiert (niemand muss raten, ob das Trennzeichen ein Komma oder ein Semikolon ist), und wir haben keine Abhängigkeiten von anderen Verarbeitungsfunktionen, die nicht offensichtlich sind, ohne den Code für die gespeicherte Prozedur zu überprüfen.

Im Vergleich zu Lösungen mit benutzerdefiniertem XML-Schema anstelle von UDTs umfasst dies eine ähnliche Anzahl von Schritten, aber meiner Erfahrung nach ist das Verwalten, Verwalten und Lesen von Code weitaus einfacher.

In vielen Lösungen benötigen Sie möglicherweise nur einen oder einige dieser UDTs (benutzerdefinierte Typen), die Sie für viele gespeicherte Prozeduren wiederverwenden. Wie in diesem Beispiel besteht die allgemeine Anforderung darin, eine Liste von ID-Zeigern zu durchlaufen. Der Funktionsname beschreibt, welchen Kontext diese IDs darstellen sollen. Der Typname sollte generisch sein.

Aaron Bertrand
quelle
3
Ich mag die Idee mit den Tabellenparametern - daran habe ich nie gedacht - Prost. Für das, was es wert ist, muss das Trennzeichen an den Funktionsaufruf SplitInts () übergeben werden.
Drammy
Wie kann ich den Tabellenparameter verwenden, wenn ich nur Zugriff auf eine durch Kommas getrennte Zeichenfolge habe
bdwain
@bdwain würde den Zweck zunichte machen - Sie müssten eine Split-Funktion verwenden, um sie in Zeilen zu unterteilen, die in den TVP eingefügt werden sollen. Teilen Sie es in Ihrem Anwendungscode auf.
Aaron Bertrand
1
@ AaronBertrand danke für die Antwort, ich habe es gerade herausgefunden. Ich muss eine Unterauswahl in den Klammern verwenden : SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz
3
@ th1rdey3 Sie sind implizit optional. stackoverflow.com/a/18926590/61305
Aaron Bertrand
44

Nach meiner Erfahrung gibt es eine knifflige und nette Lösung für dieses Problem, indem aus den Mitarbeiter-IDs ein abgegrenzter Ausdruck erstellt wird. Sie sollten nur einen Zeichenfolgenausdruck wie ';123;434;365;'in-which erstellen123 , 434und 365einige employeeIDs. Wenn Sie die folgende Prozedur aufrufen und diesen Ausdruck an ihn übergeben, können Sie die gewünschten Datensätze abrufen. Sie können dieser Abfrage ganz einfach die "andere Tabelle" hinzufügen. Diese Lösung ist für alle Versionen von SQL Server geeignet. Im Vergleich zur Verwendung von Tabellenvariablen oder temporären Tabellen ist dies eine sehr schnelle und optimierte Lösung.

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
Hamed Nazaktabar
quelle
Nett! Ich mag diesen Ansatz wirklich, bei dem ich nach int-Schlüsseln filtere! +1
MDV2000
@ MDV2000 danke :) Auf String-Schlüsseln hat dies auch eine gute Leistung, da keine int-Spalten in varchar umgewandelt werden ...
Hamed Nazaktabar
Ich bin zu spät zum Spiel, aber das ist sehr klug! Funktioniert hervorragend für mein Problem.
user441058
Das ist großartig! Ich werde das auf jeden Fall nutzen, danke
Omar Ruder
26

Verwenden Sie einen tabellenwertigen Parameter für Ihre gespeicherte Prozedur.

Wenn Sie es von C # übergeben, fügen Sie den Parameter mit dem Datentyp SqlDb.Structured hinzu.

Siehe hier: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Beispiel:

// 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();
}
Levi W.
quelle
17

Sie müssen es als XML-Parameter übergeben.

Bearbeiten: Schnellcode aus meinem Projekt, um Ihnen eine Idee zu geben:

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)

Dann können Sie die temporäre Tabelle verwenden, um sich mit Ihren Tabellen zu verbinden. Wir haben arrayOfUlong als integriertes XML-Schema definiert, um die Datenintegrität aufrechtzuerhalten, aber das müssen Sie nicht tun. Ich würde empfehlen, es zu verwenden. Hier ist ein kurzer Code, um sicherzustellen, dass Sie immer ein XML mit Longs erhalten.

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
Fedor Hajdu
quelle
Ich dachte, es wäre eine schlechte Idee, Tabellenvariablen zu verwenden, wenn es viele Zeilen gibt? Ist es nicht besser, stattdessen eine temporäre Tabelle (#table) zu verwenden?
Ganders
@ganders: Ich würde umgekehrt sagen.
Abatishchev
14

Der Kontext ist immer wichtig, z. B. die Größe und Komplexität des Arrays. Für kleine bis mittelgroße Listen sind einige der hier veröffentlichten Antworten in Ordnung, obwohl einige Klarstellungen vorgenommen werden sollten:

  • Zum Teilen einer begrenzten Liste ist ein SQLCLR-basierter Splitter am schnellsten. Es gibt zahlreiche Beispiele, wenn Sie Ihre eigenen schreiben möchten, oder Sie können einfach die kostenlose SQL # herunterladen -Bibliothek mit CLR-Funktionen (die ich geschrieben habe, aber die String_Split-Funktion und viele andere sind völlig kostenlos).
  • Das Aufteilen von XML-basierten Arrays kann schnell sein, Sie müssen jedoch Attribut-basiertes XML verwenden, nicht elementbasiertes XML (dies ist der einzige Typ, der in den Antworten hier gezeigt wird, obwohl das XML-Beispiel von @ AaronBertrand das beste ist, da sein Code das verwendet text()XML-Funktion. Weitere Informationen (z. B. Leistungsanalyse) zur Verwendung von XML zum Teilen von Listen finden Sie unter "Verwenden von XML zum Übergeben von Listen als Parameter in SQL Server" von Phil Factor.
  • Die Verwendung von TVPs ist großartig (vorausgesetzt, Sie verwenden mindestens SQL Server 2008 oder neuer), da die Daten zum Prozess gestreamt werden und vorab analysiert und als Tabellenvariable stark typisiert angezeigt werden. In den meisten Fällen DataTablebedeutet das Speichern aller Daten jedoch, dass die Daten im Speicher dupliziert werden, wenn sie aus der ursprünglichen Sammlung kopiert werden. Daher DataTablefunktioniert die Verwendung der Methode zur Übergabe von TVPs für größere Datenmengen nicht gut (dh sie lässt sich nicht gut skalieren).
  • XML kann im Gegensatz zu einfachen, durch Trs oder Strings getrennten Listen mehr als eindimensionale Arrays verarbeiten, genau wie TVPs. Aber genau wie die DataTableTVP-Methode lässt sich XML nicht gut skalieren, da es die Datengröße im Speicher mehr als verdoppelt, da zusätzlich der Overhead des XML-Dokuments berücksichtigt werden muss.

Wenn die von Ihnen verwendeten Daten groß sind oder noch nicht sehr groß sind, aber stetig wachsen, ist die IEnumerableTVP-Methode die beste Wahl, da sie die Daten an SQL Server überträgt (wie die DataTableMethode), ABER nicht erfordern eine Duplizierung der Sammlung im Speicher (im Gegensatz zu allen anderen Methoden). Ich habe in dieser Antwort ein Beispiel für den SQL- und C # -Code gepostet:

Übergeben Sie das Wörterbuch an die gespeicherte Prozedur T-SQL

Solomon Rutzky
quelle
6

Es gibt keine Unterstützung für Arrays in SQL Server, aber es gibt verschiedene Möglichkeiten, wie Sie die Sammlung an einen gespeicherten Prozess übergeben können.

  1. Durch die Verwendung von datatable
  2. Verwenden Sie XML.Versuchen Sie, Ihre Sammlung in ein XML-Format zu konvertieren, und übergeben Sie sie dann als Eingabe an eine gespeicherte Prozedur

Der folgende Link kann Ihnen helfen

Übergabe der Sammlung an eine gespeicherte Prozedur

praveen
quelle
5

Ich habe alle Beispiele und Antworten durchsucht, wie ein Array an einen SQL Server übergeben werden kann, ohne dass ein neuer Tabellentyp erstellt werden muss, bis ich diesen Link gefunden habe. Im Folgenden habe ich ihn auf mein Projekt angewendet:

- Der folgende Code ruft ein Array als Parameter ab und fügt die Werte dieses Arrays in eine andere Tabelle ein

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 

Hoffe es gefällt euch

Adam
quelle
2
@zaitsman: CLEANEST bedeutet nicht am besten oder am besten geeignet. Oft gibt man Flexibilität und / oder "angemessene" Komplexität und / oder Leistung auf, um "sauberen" Code zu erhalten. Diese Antwort hier ist "ok", aber nur für kleine Datenmengen. Wenn das eingehende Array @sCSV ist, ist es schneller, dies einfach zu teilen (dh INSERT IN ... SELECT FROM SplitFunction). Die Konvertierung in XML ist langsamer als CLR, und attributbasiertes XML ist ohnehin viel schneller. Und dies ist eine einfache Liste, aber die Übergabe von XML oder TVP kann auch komplexe Arrays verarbeiten. Ich bin mir nicht sicher, was durch das Vermeiden eines einfachen, einmaligen Ereignisses erreicht wird CREATE TYPE ... AS TABLE.
Solomon Rutzky
5

Das wird dir helfen. :) Befolgen Sie die nächsten Schritte,

  1. Öffnen Sie den Abfrage-Designer
  2. Kopieren Fügen Sie den folgenden Code so ein, wie er ist. Dadurch wird die Funktion erstellt, die den String in Int konvertiert

    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. Erstellen Sie die folgende gespeicherte Prozedur

     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. Führen Sie diesen SP aus. exec sp_DeleteId '1,2,3,12'Dies ist eine Zeichenfolge von IDs, die Sie löschen möchten.

  5. Sie konvertieren Ihr Array in C # in einen String und übergeben es als Parameter für die gespeicherte Prozedur

    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 ;

Dadurch werden mehrere Zeilen gelöscht. Alles Gute

Charan Ghate
quelle
Ich habe diese durch Kommas getrennte Analysefunktion verwendet. Sie würde für kleine Datenmengen funktionieren. Wenn Sie ihren Ausführungsplan überprüfen, verursacht dies Probleme bei großen Datenmengen und wenn Sie mehrere CSV-Listen in gespeicherten Prozeduren
benötigen
2

Ich habe lange gebraucht, um das herauszufinden, falls jemand es braucht ...

Dies basiert auf der SQL 2005-Methode in Aarons Antwort und verwendet seine SplitInts-Funktion (ich habe gerade den Delim-Parameter entfernt, da ich immer Kommas verwenden werde). Ich verwende SQL 2008, wollte aber etwas, das mit typisierten Datasets (XSD, TableAdapters) funktioniert, und ich weiß, dass String-Parameter mit diesen funktionieren.

Ich habe versucht, seine Funktion dazu zu bringen, in einer Klausel vom Typ "where in (1,2,3)" zu arbeiten, und hatte auf einfache Weise kein Glück. Also habe ich zuerst eine temporäre Tabelle erstellt und dann einen inneren Join anstelle des "where in" durchgeführt. Hier ist mein Beispiel: In meinem Fall wollte ich eine Liste von Rezepten erhalten, die bestimmte Zutaten nicht enthalten:

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)
eselk
quelle
Im Allgemeinen ist es am besten, nicht einer Tabellenvariablen beizutreten, sondern einer temporären Tabelle. Tabellenvariablen scheinen standardmäßig nur eine Zeile zu haben, obwohl es ein oder zwei Tricks gibt (siehe @ AaronBertrands ausgezeichneten und detaillierten Artikel: sqlperformance.com/2014/06/t-sql-queries/… ).
Solomon Rutzky
1

Wie bereits erwähnt, besteht eine Möglichkeit darin, das Array in eine Zeichenfolge zu konvertieren und die Zeichenfolge dann in SQL Server aufzuteilen.

Ab SQL Server 2016 gibt es eine integrierte Möglichkeit, aufgerufene Zeichenfolgen aufzuteilen

STRING_SPLIT ()

Es gibt eine Reihe von Zeilen zurück, die Sie in Ihre temporäre Tabelle (oder echte Tabelle) einfügen können.

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

ergäbe:

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

Wenn Sie schicker werden möchten:

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

würde Ihnen die gleichen Ergebnisse wie oben geben (außer dass der Spaltenname "thenumber" ist), aber sortiert. Sie können die Tabellenvariable wie jede andere Tabelle verwenden, sodass Sie sie bei Bedarf problemlos mit anderen Tabellen in der Datenbank verknüpfen können.

Beachten Sie, dass Ihre SQL Server-Installation auf Kompatibilitätsstufe 130 oder höher sein muss, damit die STRING_SPLIT()Funktion erkannt wird. Sie können Ihre Kompatibilitätsstufe mit der folgenden Abfrage überprüfen:

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

Die meisten Sprachen (einschließlich C #) verfügen über eine "Join" -Funktion, mit der Sie eine Zeichenfolge aus einem Array erstellen können.

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

Dann übergeben Sie sqlparamals Parameter die oben gespeicherte Prozedur.

Patrick Chu
quelle
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
Shabir Mustafa
quelle