Übergeben eines Array von Parametern an eine gespeicherte Prozedur

74

Ich muss ein Array von "IDs" an eine gespeicherte Prozedur übergeben, um alle Zeilen aus der Tabelle zu löschen, AUSSER die Zeilen, die mit den IDs im Array übereinstimmen.

Wie kann ich das auf einfachste Weise tun?

markiz
quelle
2
Ist dies nicht ein Duplikat von stackoverflow.com/questions/114504/… ?
John Saunders
3
@ John Saunders, es gibt viele Fragen zum SQL-Server "Array als Parameter übergeben". Dies hat jedoch eine zusätzliche Wendung, nämlich das Löschen aller Zeilen mit Ausnahme des übergebenen Parameterteils der Frage. Daher denke ich nicht, dass es sich um ein Duplikat handelt.
KM.
John Saunders, ich weiß nicht, dass ich gesucht habe, aber nicht gefunden habe, wonach ich gesucht habe. Gibt es ein Problem damit?
Markiz
1
Wenn Sie eine große Anzahl von Prozeduraufrufen ausführen und jedes Mal eine XML-Zeichenfolge erstellen müssen, sind Sie nicht effizient und die reine SQL-Methode ist besser. Wenn Sie bereits über die XML-Zeichenfolge verfügen oder nur wenige Prozeduraufrufe ausführen, ist die XML-Datei in Ordnung.
Rennfahrer x

Antworten:

43

Verwenden Sie eine gespeicherte Prozedur:

BEARBEITEN: Eine Ergänzung zum Serialisieren der Liste (oder irgendetwas anderem):

List<string> testList = new List<int>();

testList.Add(1);
testList.Add(2);
testList.Add(3);

XmlSerializer xs = new XmlSerializer(typeof(List<int>));
MemoryStream ms = new MemoryStream();
xs.Serialize(ms, testList);

string resultXML = UTF8Encoding.UTF8.GetString(ms.ToArray());

Das Ergebnis (bereit zur Verwendung mit XML-Parametern):

<?xml version="1.0"?>
<ArrayOfInt xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <int>1</int>
  <int>2</int>
  <int>3</int>
</ArrayOfInt>

ORIGINAL POST:

XML als Parameter übergeben:

<ids>
    <id>1</id>
    <id>2</id>
</ids>

CREATE PROCEDURE [dbo].[DeleteAllData]
(
    @XMLDoc XML
)
AS
BEGIN

DECLARE @handle INT

EXEC sp_xml_preparedocument @handle OUTPUT, @XMLDoc

DELETE FROM
    YOURTABLE
WHERE
    YOUR_ID_COLUMN NOT IN (
        SELECT * FROM OPENXML (@handle, '/ids/id') WITH (id INT '.') 
    )
EXEC sp_xml_removedocument @handle

Zanoni
quelle
4
+1 Dies ist eine großartige Lösung, wenn sich Ihre Daten bereits in einer XML-Struktur befinden - nicht so heiß, wenn Sie den clientseitigen Aufwand für die Erstellung des XML hinzufügen.
RolandTumble
1
Ein RolandTumble erwähnte, dies erfordert die Konvertierung meines iput-Datenarrays (List) in XML, daher denke ich, dass es in meinem Fall nicht die beste Lösung ist.
Markiz
Sie könnten Serialize verwenden, um dies zu tun ... Es ist zuverlässiger, XML zu verwenden als geteilte Zeichenfolgen ...
Zanoni
Können Sie bitte erklären, wie ich List <strings> oder arraylist serialisieren kann? Ich möchte kein Wrapper-Objekt nur für die XML-Serialisierung erstellen.
Markiz
8
Nur um das Update hier hinzuzufügen. Sie müssen xpath verwenden, um die Liste der Ints zu erhalten: SELECT * FROM OPENXML (@xmlHandle, '/ ArrayOfInt / int') WITH (id INT '.')
56

Wenn Sie SQL Server 2008 oder besser verwenden, können Sie einen so genannten Table-Valued Parameter (TVP) verwenden, anstatt Ihre Listendaten jedes Mal zu serialisieren und zu deserialisieren, wenn Sie sie an eine gespeicherte Prozedur übergeben möchten.

Beginnen wir mit der Erstellung eines einfachen Schemas als Spielplatz:

CREATE DATABASE [TestbedDb]
GO


USE [TestbedDb]
GO

    /* First, setup the sample program's account & credentials*/
CREATE LOGIN [testbedUser] WITH PASSWORD=N'µ×?
?S[°¿Q­¥½q?_Ĭ¼Ð)3õļ%dv', DEFAULT_DATABASE=[master], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=ON
GO

CREATE USER [testbedUser] FOR LOGIN [testbedUser] WITH DEFAULT_SCHEMA=[dbo]
GO

EXEC sp_addrolemember N'db_owner', N'testbedUser'
GO


    /* Now setup the schema */
CREATE TABLE dbo.Table1 ( t1Id INT NOT NULL PRIMARY KEY );
GO

INSERT INTO dbo.Table1 (t1Id)
VALUES
    (1),
    (2),
    (3),
    (4),
    (5),
    (6),
    (7),
    (8),
    (9),
    (10);
GO

Mit unserem Schema und den Beispieldaten können wir jetzt unsere gespeicherte TVP-Prozedur erstellen:

CREATE TYPE T1Ids AS Table (
        t1Id INT
);
GO


CREATE PROCEDURE dbo.FindMatchingRowsInTable1( @Table1Ids AS T1Ids READONLY )
AS
BEGIN
        SET NOCOUNT ON;

        SELECT Table1.t1Id FROM dbo.Table1 AS Table1
        JOIN @Table1Ids AS paramTable1Ids ON Table1.t1Id = paramTable1Ids.t1Id;
END
GO

Wenn sowohl unser Schema als auch unsere API vorhanden sind, können wir die gespeicherte TVP-Prozedur aus unserem Programm wie folgt aufrufen:

        // Curry the TVP data
        DataTable t1Ids = new DataTable( );
        t1Ids.Columns.Add( "t1Id",
                           typeof( int ) );

        int[] listOfIdsToFind = new[] {1, 5, 9};
        foreach ( int id in listOfIdsToFind )
        {
            t1Ids.Rows.Add( id );
        }
        // Prepare the connection details
        SqlConnection testbedConnection =
                new SqlConnection(
                        @"Data Source=.\SQLExpress;Initial Catalog=TestbedDb;Persist Security Info=True;User ID=testbedUser;Password=letmein12;Connect Timeout=5" );

        try
        {
            testbedConnection.Open( );

            // Prepare a call to the stored procedure
            SqlCommand findMatchingRowsInTable1 = new SqlCommand( "dbo.FindMatchingRowsInTable1",
                                                                  testbedConnection );
            findMatchingRowsInTable1.CommandType = CommandType.StoredProcedure;

            // Curry up the TVP parameter
            SqlParameter sqlParameter = new SqlParameter( "Table1Ids",
                                                          t1Ids );
            findMatchingRowsInTable1.Parameters.Add( sqlParameter );

            // Execute the stored procedure
            SqlDataReader sqlDataReader = findMatchingRowsInTable1.ExecuteReader( );

            while ( sqlDataReader.Read( ) )
            {
                Console.WriteLine( "Matching t1ID: {0}",
                                   sqlDataReader[ "t1Id" ] );
            }
        }
        catch ( Exception e )
        {
            Console.WriteLine( e.ToString( ) );
        }
  /* Output:
   * Matching t1ID: 1
   * Matching t1ID: 5
   * Matching t1ID: 9
   */

Es gibt wahrscheinlich einen weniger schmerzhaften Weg, dies mit einer abstrakteren API wie Entity Framework zu tun. Derzeit habe ich jedoch keine Zeit, mich selbst davon zu überzeugen.

rmiesen
quelle
Ich denke, das ist der beste Weg, dies zu tun. Ich bin schockiert, dass Sie dafür keine Stimmen bekommen haben. Also habe ich dir eins gegeben.
1
Ich mag diesen Ansatz. Danke für das Teilen.
Justin
Für die Wiederverwendbarkeit halte ich dies bei weitem für den besten Ansatz. Betrachten Sie die folgende gängige reale Anwendung: Ich habe eine Suche, die eine Ergebnismenge mit einem eindeutigen Primärschlüssel generiert. Ich möchte diese Ergebnismenge dann an eine andere Operation weiterleiten (z. B. weitere Informationen für eine Excel-Tabelle abrufen). Mit dieser Methode kann ich leicht eine Liste von IDs aus der anfänglichen Ergebnismenge abrufen und die Liste an meine gespeicherte Prozedur der zweiten Phase übergeben, die einen einzelnen Parameter von "@IDs T1IDs READONLY" akzeptiert. Dadurch gehe ich der schweren Last der anfänglichen Suche aus dem Weg.
Mrmillsy
20

Dies ist die beste Quelle:

http://www.sommarskog.se/arrays-in-sql.html

Erstellen Sie eine Split-Funktion über den Link und verwenden Sie sie wie folgt:

DELETE YourTable
    FROM YourTable                           d
    LEFT OUTER JOIN dbo.splitFunction(@Parameter) s ON d.ID=s.Value
    WHERE s.Value IS NULL

Ich bevorzuge den Nummerntabellenansatz

Dies ist Code basierend auf dem obigen Link, der es für Sie tun sollte ...

Bevor Sie meine Funktion verwenden, müssen Sie eine "Hilfstabelle" einrichten. Sie müssen dies nur einmal pro Datenbank tun:

CREATE TABLE Numbers
(Number int  NOT NULL,
    CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number ASC)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
DECLARE @x int
SET @x=0
WHILE @x<8000
BEGIN
    SET @x=@x+1
    INSERT INTO Numbers VALUES (@x)
END

Verwenden Sie diese Funktion, um Ihren String zu teilen, der keine Schleife durchläuft und sehr schnell ist:

CREATE FUNCTION [dbo].[FN_ListToTable]
(
     @SplitOn              char(1)              --REQUIRED, the character to split the @List string on
    ,@List                 varchar(8000)        --REQUIRED, the list to split apart
)
RETURNS
@ParsedList table
(
    ListValue varchar(500)
)
AS
BEGIN

/**
Takes the given @List string and splits it apart based on the given @SplitOn character.
A table is returned, one row per split item, with a column name "ListValue".
This function workes for fixed or variable lenght items.
Empty and null items will not be included in the results set.


Returns a table, one row per item in the list, with a column name "ListValue"

EXAMPLE:
----------
SELECT * FROM dbo.FN_ListToTable(',','1,12,123,1234,54321,6,A,*,|||,,,,B')

    returns:
        ListValue  
        -----------
        1
        12
        123
        1234
        54321
        6
        A
        *
        |||
        B

        (10 row(s) affected)

**/



----------------
--SINGLE QUERY-- --this will not return empty rows
----------------
INSERT INTO @ParsedList
        (ListValue)
    SELECT
        ListValue
        FROM (SELECT
                  LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(@SplitOn, List2, number+1)-number - 1))) AS ListValue
                  FROM (
                           SELECT @SplitOn + @List + @SplitOn AS List2
                       ) AS dt
                      INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
                  WHERE SUBSTRING(List2, number, 1) = @SplitOn
             ) dt2
        WHERE ListValue IS NOT NULL AND ListValue!=''



RETURN

END --Function FN_ListToTable

Sie können diese Funktion als Tabelle in einem Join verwenden:

SELECT
    Col1, COl2, Col3...
    FROM  YourTable
        INNER JOIN dbo.FN_ListToTable(',',@YourString) s ON  YourTable.ID = s.ListValue

Hier ist dein Löschen:

DELETE YourTable
    FROM YourTable                                d
    LEFT OUTER JOIN dbo.FN_ListToTable(',',@Parameter) s ON d.ID=s.ListValue
    WHERE s.ListValue IS NULL
KM.
quelle
@KM: Warum maximal 8000 Zeichen? Können wir Strings länger verwenden?
Pandincus
Sie können den Eingabeparameter varchar (max) festlegen. Stellen Sie jedoch sicher, dass Ihre Numbers-Tabelle genügend Zeilen enthält. Ich benutze nur 8000, weil ich nie lange Saiten geteilt habe. Sie können diese andere Antwort von mir sehen: stackoverflow.com/questions/4227552/… wo ich die Teilungsfunktion aktualisiert habe, um eine Tabellenfunktion zu verwenden, und auch die Generierung der Zahlentabelle verbessert habe.
KM.
12

Sie könnten dies versuchen:



DECLARE @List VARCHAR(MAX)

SELECT @List = '1,2,3,4,5,6,7,8'

EXEC(
'DELETE
FROM TABLE
WHERE ID NOT IN (' + @List + ')'
)

Adriaan Stander
quelle
6
... solange Sie sicher sind, dass der Parameter @List nicht aus Benutzereingaben ausgefüllt wird
Joe
Schönheit ist Einfachheit.
wwmbes
4
declare @ids nvarchar(1000)

set @ids = '100,2,3,4,5' --Parameter passed

set @ids = ',' + @ids + ','

select   *
from     TableName 
where    charindex(',' + CAST(Id as nvarchar(50)) + ',', @ids) > 0
Vitalivs
quelle
Bingo Vitalivs! Der erste Preis ist der kleinste Code und produziert genau das, was in kürzester Zeit benötigt wird. Eingebettete Leerzeichen können Probleme in "@ids" verursachen.
wwmbes
@ Vitalivs Danke für Ihre Antwort .. es rettet mich :)
Arebhy Sridaran
3

Sie können eine temporäre Tabelle verwenden, deren Existenz die gespeicherte Prozedur erwartet. Dies funktioniert mit älteren Versionen von SQL Server, die XML usw. nicht unterstützen.

CREATE TABLE #temp
(INT myid)
GO
CREATE PROC myproc
AS
BEGIN
    DELETE YourTable
    FROM YourTable                    
    LEFT OUTER JOIN #temp T ON T.myid=s.id
    WHERE s.id IS NULL
END
Simon
quelle
0

Was ist mit der Verwendung des XML-Datentyps anstelle der Übergabe eines Arrays? Ich finde das eine bessere Lösung und funktioniert gut in SQL 2005

Cody C.
quelle
1
@ Zanoni, warum nicht geteilten Saiten vertrauen?
KM.
0

Ich mag dieses, weil es geeignet ist, als XElement übergeben zu werden, das für SqlCommand geeignet ist

(Sorry, es ist VB.NET, aber Sie haben die Idee)

<Extension()>
Public Function ToXml(Of T)(array As IEnumerable(Of T)) As XElement
   Return XElement.Parse(
           String.Format("<doc>{0}</doc>", String.Join("", array.Select(Function(s) String.Concat("<d>", s.ToString(), "</d>")))), LoadOptions.None)
 End Function

Dies ist der SQL Stored Proc, verkürzt, nicht vollständig!

CREATE PROCEDURE [dbo]. [Myproc] (@blah xml)
AS ... WHERE SomeID IN (SELECT doc.t.value ('.', 'Int') von @ netwerkids.nodes (N '/ doc / d') ) als doc (t))

Egbert Nierop
quelle
0

Sie können die Funktion STRING_SPLIT in SQL Server verwenden. Sie können die Dokumentation hier überprüfen .

DECLARE @YourListOfIds VARCHAR(1000) -- Or VARCHAR(MAX) depending on what you need

SET @YourListOfIds = '1,2,3,4,5,6,7,8'

SELECT * FROM YourTable
WHERE Id IN(SELECT CAST(Value AS INT) FROM STRING_SPLIT(@YourListOfIds, ','))
Juan Salas
quelle