it-swarm.com.de

Übergeben von Array-Parametern an eine gespeicherte Prozedur

Ich habe einen Prozess, der eine Reihe von Datensätzen (1000er) erfasst und bearbeitet, und wenn ich fertig bin, muss ich eine große Anzahl von ihnen als verarbeitet markieren. Ich kann dies mit einer großen Liste von IDs anzeigen. Ich versuche, das Muster "Updates in einer Schleife" zu vermeiden, daher möchte ich einen effizienteren Weg finden, um diese ID-Tasche in einen gespeicherten MS SQL Server 2008-Prozess zu senden.

Vorschlag Nr. 1 - Parameter mit Tabellenwert. Ich kann einen Tabellentyp mit nur einem ID-Feld definieren und eine Tabelle mit IDs zur Aktualisierung einsenden.

Vorschlag Nr. 2 - XML-Parameter (varchar) mit OPENXML () im Prozesskörper.

Vorschlag Nr. 3 - Listenanalyse. Ich würde dies lieber vermeiden, wenn dies möglich ist, da es unhandlich und fehleranfällig erscheint.

Gibt es eine Präferenz unter diesen oder irgendwelche Ideen, die ich vermisst habe?

54
D. Lambert

Die besten Artikel zu diesem Thema stammen von Erland Sommarskog:

Er deckt alle Möglichkeiten ab und erklärt ziemlich gut.

Entschuldigen Sie die kurze Antwort, aber Erlands Artikel über Arrays ähnelt Joe Celkos Büchern über Bäume und andere SQL-Leckereien :)

44
Marian

Es gibt eine großartige Diskussion darüber auf StackOverflow , die viele Ansätze abdeckt. Für SQL Server 2008+ bevorzuge ich die Verwendung von tabellenwertige Parameter. Dies ist im Wesentlichen die Lösung von SQL Server für Ihr Problem - Übergeben einer Werteliste an eine gespeicherte Prozedur.

Die Vorteile dieses Ansatzes sind:

  • führen Sie einen Aufruf einer gespeicherten Prozedur durch, wobei alle Ihre Daten als 1 Parameter übergeben werden
  • die Tabelleneingabe ist strukturiert und stark typisiert
  • kein String-Erstellen/Parsen oder Behandeln von XML
  • kann leicht Tabelleneingaben verwenden, um zu filtern, zu verbinden oder was auch immer

Beachten Sie jedoch Folgendes: Wenn Sie eine gespeicherte Prozedur aufrufen, die TVPs über ADO.NET oder ODBC verwendet), und nehmen Sie a Wenn Sie sich die Aktivität mit SQL Server Profiler ansehen, werden Sie feststellen, dass SQL Server mehrere INSERT Anweisungen zum Laden des TVP erhält, eine für jede Zeile im TVP , gefolgt vom Aufruf des Prozedur. Dies ist beabsichtigt . Dieser Stapel von INSERTs muss jedes Mal kompiliert werden, wenn die Prozedur aufgerufen wird, und stellt einen kleinen Overhead dar. Trotz dieses Overheads sind TVPs immer noch - wegblasen andere Ansätze in Bezug auf Leistung und Benutzerfreundlichkeit für die meisten Anwendungsfälle.

Wenn Sie mehr erfahren möchten, hat Erland Sommarskog die volle Skinny zur Funktionsweise von Parametern mit Tabellenwerten und bietet mehrere Beispiele.

Hier ist ein weiteres Beispiel, das ich zusammengestellt habe:

CREATE TYPE id_list AS TABLE (
    id int NOT NULL PRIMARY KEY
);
GO

CREATE PROCEDURE [dbo].[tvp_test] (
      @param1           INT
    , @customer_list    id_list READONLY
)
AS
BEGIN
    SELECT @param1 AS param1;

    -- join, filter, do whatever you want with this table 
    -- (other than modify it)
    SELECT *
    FROM @customer_list;
END;
GO

DECLARE @customer_list id_list;

INSERT INTO @customer_list (
    id
)
VALUES (1), (2), (3), (4), (5), (6), (7);

EXECUTE [dbo].[tvp_test]
      @param1 = 5
    , @customer_list = @customer_list
;
GO

DROP PROCEDURE dbo.tvp_test;
DROP TYPE id_list;
GO
23
Nick Chammas

Das gesamte Thema wird auf the endgültiger Artikel von Erland Sommarskog diskutiert: "Arrays und Liste in SQL Server" . Wählen Sie aus, welche Version Sie auswählen möchten.

Zusammenfassung für vor SQL Server 2008, wo TVPs den Rest übertreffen

  • CSV, teilen Sie auf, wie Sie möchten (ich verwende im Allgemeinen eine Zahlentabelle)
  • XML und Analyse (besser mit SQL Server 2005+)
  • Erstellen Sie eine temporäre Tabelle auf dem Client

Der Artikel ist sowieso lesenswert, um andere Techniken und Denkweisen zu sehen.

Bearbeiten: späte Antwort für riesig Listen an anderer Stelle: Übergeben von Array-Parametern an eine gespeicherte Prozedur

21
gbn

Ich weiß, dass ich zu spät zu dieser Party komme, aber ich hatte in der Vergangenheit ein solches Problem, weil ich bis zu 100.000 Bigint-Nummern senden musste und ein paar Benchmarks durchgeführt habe. Am Ende haben wir sie im Binärformat als Bild gesendet - das war schneller als alles andere für bis zu 100.000 Zahlen.

Hier ist mein alter Code (SQL Server 2005):

SELECT  Number * 8 + 1 AS StartFrom ,
        Number * 8 + 8 AS MaxLen
INTO    dbo.ParsingNumbers
FROM    dbo.Numbers
GO

CREATE FUNCTION dbo.ParseImageIntoBIGINTs ( @BIGINTs IMAGE )
RETURNS TABLE
AS RETURN
    ( SELECT    CAST(SUBSTRING(@BIGINTs, StartFrom, 8) AS BIGINT) Num
      FROM      dbo.ParsingNumbers
      WHERE     MaxLen <= DATALENGTH(@BIGINTs)
    )
GO

Der folgende Code packt ganze Zahlen in einen binären Blob. Ich kehre hier die Reihenfolge der Bytes um:

static byte[] UlongsToBytes(ulong[] ulongs)
{
int ifrom = ulongs.GetLowerBound(0);
int ito   = ulongs.GetUpperBound(0);
int l = (ito - ifrom + 1)*8;
byte[] ret = new byte[l];
int retind = 0;
for(int i=ifrom; i<=ito; i++)
{
ulong v = ulongs[i];
ret[retind++] = (byte) (v >> 0x38);
ret[retind++] = (byte) (v >> 0x30);
ret[retind++] = (byte) (v >> 40);
ret[retind++] = (byte) (v >> 0x20);
ret[retind++] = (byte) (v >> 0x18);
ret[retind++] = (byte) (v >> 0x10);
ret[retind++] = (byte) (v >> 8);
ret[retind++] = (byte) v;
}
return ret;
}
14
A-K

Ich bin hin- und hergerissen zwischen dem Verweisen auf SO oder dem Beantworten hier, weil dies fast eine Programmierfrage ist. Aber da ich bereits eine Lösung habe, die ich verwende ... werde ich posten Das ;)

Die Funktionsweise besteht darin, dass Sie eine durch Kommas getrennte Zeichenfolge (einfache Aufteilung, keine Aufteilung im CSV-Stil) als Varchar (4000) in die gespeicherte Prozedur eingeben und diese Liste dann in diese Funktion einspeisen und eine praktische Tabelle wieder herausholen. eine Tabelle mit nur Varcharen.

Auf diese Weise können Sie nur die Werte der IDs senden, die verarbeitet werden sollen, und an diesem Punkt können Sie einen einfachen Join durchführen.

Alternativ können Sie etwas mit einer CLR-Datentabelle tun und diese einspeisen, aber die Unterstützung ist etwas aufwendiger und jeder versteht CSV-Listen.

USE [Database]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER FUNCTION [dbo].[splitListToTable] (@list      nvarchar(MAX), @delimiter nchar(1) = N',')
      RETURNS @tbl TABLE (value     varchar(4000)      NOT NULL) AS
/*
http://www.sommarskog.se/arrays-in-sql.html
This guy is apparently THE guy in SQL arrays and lists 

Need an easy non-dynamic way to split a list of strings on input for comparisons

Usage like thus:

DECLARE @sqlParam VARCHAR(MAX)
SET @sqlParam = 'a,b,c'

SELECT * FROM (

select 'a' as col1, '1' as col2 UNION
select 'a' as col1, '2' as col2 UNION
select 'b' as col1, '3' as col2 UNION
select 'b' as col1, '4' as col2 UNION
select 'c' as col1, '5' as col2 UNION
select 'c' as col1, '6' as col2 ) x 
WHERE EXISTS( SELECT value FROM splitListToTable(@sqlParam,',') WHERE x.col1 = value )

*/
BEGIN
   DECLARE @endpos   int,
           @startpos int,
           @textpos  int,
           @chunklen smallint,
           @tmpstr   nvarchar(4000),
           @leftover nvarchar(4000),
           @tmpval   nvarchar(4000)

   SET @textpos = 1
   SET @leftover = ''
   WHILE @textpos <= datalength(@list) / 2
   BEGIN
      SET @chunklen = 4000 - datalength(@leftover) / 2
      SET @tmpstr = @leftover + substring(@list, @textpos, @chunklen)
      SET @textpos = @textpos + @chunklen

      SET @startpos = 0
      SET @endpos = charindex(@delimiter, @tmpstr)

      WHILE @endpos > 0
      BEGIN
         SET @tmpval = ltrim(rtrim(substring(@tmpstr, @startpos + 1,
                                             @endpos - @startpos - 1)))
         INSERT @tbl (value) VALUES(@tmpval)
         SET @startpos = @endpos
         SET @endpos = charindex(@delimiter, @tmpstr, @startpos + 1)
      END

      SET @leftover = right(@tmpstr, datalength(@tmpstr) / 2 - @startpos)
   END

   INSERT @tbl(value) VALUES (ltrim(rtrim(@leftover)))
   RETURN
END
9
jcolebrand

Ich erhalte regelmäßig Sätze von 1000 Zeilen und 10000 Zeilen, die von unserer Anwendung gesendet wurden, um von verschiedenen gespeicherten SQL Server-Prozeduren verarbeitet zu werden.

Um die Leistungsanforderungen zu erfüllen, verwenden wir TVPs. Sie müssen jedoch Ihre eigene Zusammenfassung des dbDataReader implementieren, um einige Leistungsprobleme im Standardverarbeitungsmodus zu beheben. Ich werde nicht auf das Wie und Warum eingehen, da sie für diese Anfrage nicht in Frage kommen.

Ich habe die XML-Verarbeitung nicht in Betracht gezogen, da ich keine XML-Implementierung gefunden habe, die mit mehr als 10.000 "Zeilen" performant bleibt.

Die Listenverarbeitung kann durch eindimensionale und zweidimensionale Tally (Zahlen) -Tabellenverarbeitung erfolgen. Wir haben diese in verschiedenen Bereichen erfolgreich eingesetzt, aber gut verwaltete TVPs sind leistungsfähiger, wenn mehr als ein paar hundert "Zeilen" vorhanden sind.

Wie bei allen Auswahlmöglichkeiten bezüglich der SQL Server-Verarbeitung müssen Sie Ihre Auswahl basierend auf dem Verwendungsmodell treffen.

5
Robert Miller

Ich hatte endlich die Möglichkeit, einige TableValuedParameters zu erstellen, und sie funktionieren hervorragend. Daher werde ich einen ganzen Code einfügen, der zeigt, wie ich sie verwende, mit einem Beispiel aus einem Teil meines aktuellen Codes: (Hinweis: Wir verwenden ADO .NETZ)

Beachten Sie auch: Ich schreibe Code für einen Dienst und habe viele vordefinierte Codebits in der anderen Klasse, aber ich schreibe dies als Konsolen-App, damit ich es debuggen kann, also habe ich all dies herausgerissen die Konsolen-App. Entschuldigen Sie meinen Codierungsstil (wie fest codierte Verbindungszeichenfolgen), da er sozusagen "einen zum Wegwerfen erstellen" war. Ich wollte zeigen, wie ich einen List<customObject> Verwende und ihn einfach als Tabelle in die Datenbank schiebe, die ich in der gespeicherten Prozedur verwenden kann. C # und TSQL-Code unten:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using a;

namespace a.EventAMI {
    class Db {
        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static void Update(List<Current> currents) {
            const string CONSTR = @"just a hardwired connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );
            cmd.Parameters.Add( "@CurrentTVP", SqlDbType.Structured ).Value = Converter.GetDataTableFromIEnumerable( currents, typeof( Current ) ); //my custom converter class

            try {
                using ( con ) {
                    con.Open();
                    cmd.ExecuteNonQuery();
                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }
        }
    }
    class Current {
        public string Identifier { get; set; }
        public string OffTime { get; set; }
        public DateTime Off() {
            return Convert.ToDateTime( OffTime );
        }

        private static SqlCommand SqlCommandFactory(string sprocName, SqlConnection con) { return new SqlCommand { CommandType = CommandType.StoredProcedure, CommandText = sprocName, CommandTimeout = 0, Connection = con }; }

        public static List<Current> GetAll() {
            List<Current> l = new List<Current>();

            const string CONSTR = @"just a hardcoded connection string while I'm debugging";
            SqlConnection con = new SqlConnection( CONSTR );

            SqlCommand cmd = SqlCommandFactory( "sprocname", con );

            try {
                using ( con ) {
                    con.Open();
                    using ( SqlDataReader reader = cmd.ExecuteReader() ) {
                        while ( reader.Read() ) {
                            l.Add(
                                new Current {
                                    Identifier = reader[0].ToString(),
                                    OffTime = reader[1].ToString()
                                } );
                        }
                    }

                }
            } catch ( Exception ex ) {
                ErrHandler.WriteXML( ex );
                throw;
            }

            return l;
        }
    }
}

-------------------
the converter class
-------------------
using System;
using System.Collections;
using System.Data;
using System.Reflection;

namespace a {
    public static class Converter {
        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable) {
            return GetDataTableFromIEnumerable( aIEnumerable, null );
        }

        public static DataTable GetDataTableFromIEnumerable(IEnumerable aIEnumerable, Type baseType) {
            DataTable returnTable = new DataTable();

            if ( aIEnumerable != null ) {
                //Creates the table structure looping in the in the first element of the list
                object baseObj = null;

                Type objectType;

                if ( baseType == null ) {
                    foreach ( object obj in aIEnumerable ) {
                        baseObj = obj;
                        break;
                    }

                    objectType = baseObj.GetType();
                } else {
                    objectType = baseType;
                }

                PropertyInfo[] properties = objectType.GetProperties();

                DataColumn col;

                foreach ( PropertyInfo property in properties ) {
                    col = new DataColumn { ColumnName = property.Name };
                    if ( property.PropertyType == typeof( DateTime? ) ) {
                        col.DataType = typeof( DateTime );
                    } else if ( property.PropertyType == typeof( Int32? ) ) {
                        col.DataType = typeof( Int32 );
                    } else {
                        col.DataType = property.PropertyType;
                    }
                    returnTable.Columns.Add( col );
                }

                //Adds the rows to the table

                foreach ( object objItem in aIEnumerable ) {
                    DataRow row = returnTable.NewRow();

                    foreach ( PropertyInfo property in properties ) {
                        Object value = property.GetValue( objItem, null );
                        if ( value != null )
                            row[property.Name] = value;
                        else
                            row[property.Name] = "";
                    }

                    returnTable.Rows.Add( row );
                }
            }
            return returnTable;
        }

    }
}

USE [Database]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROC [dbo].[Event_Update]
    @EventCurrentTVP    Event_CurrentTVP    READONLY
AS

/****************************************************************
    author  cbrand
    date    
    descrip I'll ask you to forgive me the anonymization I've made here, but hope this helps
    caller  such and thus application
****************************************************************/

BEGIN TRAN Event_Update

DECLARE @DEBUG INT

SET @DEBUG = 0 /* test using @DEBUG <> 0 */

/*
    Replace the list of outstanding entries that are still currently disconnected with the list from the file
    This means remove all existing entries (faster to truncate and insert than to delete on a join and insert, yes?)
*/
TRUNCATE TABLE [database].[dbo].[Event_Current]

INSERT INTO [database].[dbo].[Event_Current]
           ([Identifier]
            ,[OffTime])
SELECT [Identifier]
      ,[OffTime]
  FROM @EventCurrentTVP

IF (@@ERROR <> 0 OR @DEBUG <> 0) 
BEGIN
ROLLBACK TRAN Event_Update
END
ELSE
BEGIN
COMMIT TRAN Event_Update
END

USE [Database]
GO

CREATE TYPE [dbo].[Event_CurrentTVP] AS TABLE(
    [Identifier] [varchar](20) NULL,
    [OffTime] [datetime] NULL
)
GO

Außerdem werde ich meinen Codierungsstil konstruktiv kritisieren, wenn Sie dies zu bieten haben (allen Lesern, die auf diese Frage stoßen), aber bitte halten Sie es konstruktiv;) ... Wenn Sie mich wirklich wollen, finden Sie mich hier im Chatroom . Hoffentlich kann man mit diesem Codestück sehen, wie sie den List<Current> Verwenden können, wie ich ihn als Tabelle in der Datenbank und als List<T> In ihrer App definiert habe.

5
jcolebrand

Ich würde entweder mit Vorschlag Nr. 1 gehen oder alternativ eine Arbeitstabelle erstellen, die nur verarbeitete IDs enthält. Fügen Sie während der Verarbeitung in diese Tabelle ein und rufen Sie nach Abschluss einen Vorgang auf, der dem folgenden ähnelt:

BEGIN TRAN

UPDATE dt
SET processed = 1
FROM dataTable dt
JOIN processedIds pi ON pi.id = dt.id;

TRUNCATE TABLE processedIds

COMMIT TRAN

Sie werden viele Einfügungen machen, aber sie werden an einem kleinen Tisch sein, also sollte es schnell gehen. Sie können Ihre Beilagen auch mit ADO.net oder einem von Ihnen verwendeten Datenadapter stapeln.

Der Fragentitel enthält die Aufgabe, Daten aus einer Anwendung in die gespeicherte Prozedur zu übertragen. Dieser Teil wird vom Fragetext ausgeschlossen, aber lassen Sie mich auch versuchen, dies zu beantworten.

Im Zusammenhang mit SQL-Server-2008, wie durch die Tags angegeben, gibt es einen weiteren großartigen Artikel von E. Sommarskog Arrays und Listen in SQL Server 2008 . Übrigens habe ich es in dem Artikel gefunden, auf den Marian in seiner Antwort Bezug genommen hat.

Anstatt nur den Link anzugeben, zitiere ich die Inhaltsliste:

  • Einführung
  • Hintergrund
  • Tabellenwertparameter in T-SQL
  • Übergeben von tabellenwertigen Parametern von ADO .NET
    • Verwenden einer Liste
    • Verwenden einer Datentabelle
    • Verwenden eines DataReader
    • Schlussbemerkungen
  • Verwenden von tabellenwertigen Parametern aus anderen APIs
    • ODBC
    • OLE DB
    • ADO
    • LINQ und Entity Framework
    • JDBC
    • PHP
    • Perl
    • Was ist, wenn Ihre API keine TVPs unterstützt?
  • Leistungsüberlegungen
    • Serverseitig
    • Client-Seite
    • Primärschlüssel oder nicht?
  • Danksagung und Feedback
  • Versionsgeschichte

Über die dort genannten Techniken hinaus habe ich das Gefühl, dass in einigen Fällen Bulkcopy und Bulk-Insert erwähnt werden sollten, um den allgemeinen Fall zu berücksichtigen.

2
bernd_k

Übergeben von Array-Parametern an eine gespeicherte Prozedur

Für MS SQL 2016 neueste Version

Mit MS SQL 2016 führen sie eine neue Funktion ein: SPLIT_STRING (), um mehrere Werte zu analysieren.

Dies kann Ihr Problem leicht lösen.

Für ältere MS SQL-Version

Wenn Sie eine ältere Version verwenden, gehen Sie folgendermaßen vor:

Zuerst mache eine Funktion :

 ALTER FUNCTION [dbo].[UDF_IDListToTable]
 (
    @list          [varchar](MAX),
    @Seperator     CHAR(1)
  )
 RETURNS @tbl TABLE (ID INT)
 WITH 

 EXECUTE AS CALLER
 AS
  BEGIN
    DECLARE @position INT
    DECLARE @NewLine CHAR(2) 
    DECLARE @no INT
    SET @NewLine = CHAR(13) + CHAR(10)

    IF CHARINDEX(@Seperator, @list) = 0
    BEGIN
    INSERT INTO @tbl
    VALUES
      (
        @list
      )
END
ELSE
BEGIN
    SET @position = 1
    SET @list = @list + @Seperator
    WHILE CHARINDEX(@Seperator, @list, @position) <> 0
    BEGIN
        SELECT @no = SUBSTRING(
                   @list,
                   @position,
                   CHARINDEX(@Seperator, @list, @position) - @position
               )

        IF @no <> ''
            INSERT INTO @tbl
            VALUES
              (
                @no
              )

        SET @position = CHARINDEX(@Seperator, @list, @position) + 1
    END
END
RETURN
END

Übergeben Sie anschließend einfach Ihren String mit einem Trennzeichen an diese Funktion.

Ich hoffe das ist hilfreich für dich. : -)

1
Ankit Bhalala