Passando i parametri dell'array a una procedura memorizzata


53

Ho un processo che prende un sacco di dischi (1000) e ci opera, e quando ho finito, devo contrassegnarne un gran numero come elaborati. Posso indicarlo con un grande elenco di ID. Sto cercando di evitare il modello "aggiornamenti in un ciclo", quindi mi piacerebbe trovare un modo più efficiente per inviare questo pacchetto di ID in un proc memorizzato MS SQL Server 2008.

Proposta n. 1 - Parametri con valori di tabella. Posso definire un tipo di tabella con solo un campo ID e inviare una tabella piena di ID da aggiornare.

Proposta n. 2 - Parametro XML (varchar) con OPENXML () nel corpo del proc.

Proposta n. 3 - Elenco di analisi. Preferirei evitare questo, se possibile, poiché sembra ingombrante e soggetto a errori.

Qualche preferenza tra queste o idee che mi sono perse?


Come stai ottenendo il grande elenco di ID?
Larry Coleman,

Li sto tirando giù insieme ai dati "payload" tramite un altro proc memorizzato. Non ho bisogno di aggiornare tutti quei dati, però - basta aggiornare un flag su determinati record.
D. Lambert,

Risposte:


42

I migliori articoli in assoluto su questo argomento sono di Erland Sommarskog:

Copre tutte le opzioni e spiega abbastanza bene.

Ci scusiamo per la mancanza di risposta, ma l'articolo di Erland su Arrays è come i libri di Joe Celko sugli alberi e altre prelibatezze SQL :)


23

C'è una grande discussione di questo su StackOverflow che copre molti approcci. Quello che preferisco per SQL Server 2008+ è utilizzare parametri con valori di tabella . Questa è essenzialmente la soluzione di SQL Server al tuo problema: passare un elenco di valori a una procedura memorizzata.

I vantaggi di questo approccio sono:

  • effettuare una chiamata di procedura memorizzata con tutti i dati passati come 1 parametro
  • l'input della tabella è strutturato e fortemente tipizzato
  • nessuna creazione / analisi di stringhe o gestione di XML
  • può facilmente utilizzare l'input della tabella per filtrare, unire o altro

Tuttavia, prendi nota: se chiami una procedura memorizzata che utilizza TVP tramite ADO.NET o ODBC e dai un'occhiata all'attività con SQL Server Profiler, noterai che SQL Server riceve diverse INSERTistruzioni per caricare il TVP, una per ogni riga nel TVP , seguito dalla chiamata alla procedura. Questo è di progettazione . Questo gruppo di messaggi di posta INSERTelettronica deve essere compilato ogni volta che viene chiamata la procedura e costituisce un piccolo overhead. Tuttavia, anche con questo sovraccarico, i TVP spazzano via altri approcci in termini di prestazioni e usabilità per la maggior parte dei casi d'uso.

Se vuoi saperne di più, Erland Sommarskog ha la skinny completa su come funzionano i parametri con valori di tabella e fornisce diversi esempi.

Ecco un altro esempio che ho inventato:

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

Quando eseguo questo, viene visualizzato un messaggio di errore: Messaggio 2715, Livello 16, Stato 3, Procedura tvp_test, Riga 4 [Riga iniziale batch 4] Colonna, parametro o variabile n. 2: Impossibile trovare il tipo di dati id_list. Il parametro o la variabile '@customer_list' ha un tipo di dati non valido. Messaggio 1087, livello 16, stato 1, procedura tvp_test, riga 13 [Batch Start Line 4] Deve dichiarare la variabile di tabella "@customer_list".
Damian,

@Damian - L' CREATE TYPEistruzione all'inizio è stata eseguita correttamente? Quale versione di SQL Server stai utilizzando?
Nick Chammas,

Nel codice SP hai questa frase in linea "SELECT @ param1 AS param1;" . Qual è lo scopo? Non usi o param1 quindi perché hai inserito questo come parametro nell'intestazione SP?
EAmez, il

@EAmez - Era solo un esempio arbitrario. Il punto @customer_listnon lo è @param1. L'esempio dimostra semplicemente che è possibile mescolare diversi tipi di parametri.
Nick Chammas il

21

L'intero soggetto è discusso sul l' articolo definitivo per Erland Sommarskog: "Array e List in SQL Server" . Scegli quale versione scegliere.

Riepilogo, per pre SQL Server 2008 in cui i TVP vincono il resto

  • CSV, dividi come preferisci (generalmente utilizzo una tabella Numbers)
  • XML e analisi (meglio con SQL Server 2005+)
  • Creare una tabella temporanea sul client

Vale comunque la pena leggere l'articolo per vedere altre tecniche e pensieri.

Modifica: risposta tardiva a grandi elenchi altrove: passaggio dei parametri dell'array a una procedura memorizzata


14

So di essere in ritardo per questa festa, ma ho avuto un problema del genere in passato, dovendo inviare fino a 100.000 numeri bigint e fare alcuni benchmark. Abbiamo finito per inviarli in formato binario, come un'immagine, che era più veloce di tutto il resto per un massimo di 100K numeri.

Ecco il mio vecchio codice (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

Il codice seguente sta comprimendo numeri interi in un BLOB binario. Sto invertendo l'ordine dei byte qui:

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;
}

9

Sono combattuto tra il fatto di riferirti a SO o di rispondere qui, perché questa è quasi una domanda di programmazione. Ma dal momento che ho già una soluzione che uso ... lo posterò;)

Il modo in cui funziona è quello di inserire una stringa delimitata da virgole (divisione semplice, non esegue suddivisioni in stile CSV) nella procedura memorizzata come varchar (4000) e quindi inserire tale elenco in questa funzione e ottenere una tabella utile indietro, una tabella di soli varchars.

Ciò consente di inviare i valori degli ID che si desidera elaborare e in quel momento è possibile eseguire un semplice join.

In alternativa, potresti fare qualcosa con una DataTable CLR e inserirla, ma è un po 'più oneroso da supportare e tutti comprendono gli elenchi CSV.

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

Beh, stavo specificamente cercando di evitare l'elenco delimitato da virgole in modo da non dover scrivere qualcosa del genere, ma dal momento che è già scritto, immagino che dovrei buttare quella soluzione nel mix. ;-)
D. Lambert

1
Dico provato e vero è il più semplice. Puoi sputare un elenco separato da virgole in C # in pochi secondi di codice, e puoi lanciarlo in questa funzione (dopo averlo inserito nel tuo sproc) abbastanza rapidamente, e non devi nemmeno pensarci. ~ E so che hai detto che non volevi usare una funzione, ma penso che sia il modo più semplice (forse non il più efficace)
jcolebrand

5

Ricevo regolarmente serie di migliaia di righe e 10000 di righe inviate dalla nostra applicazione per essere elaborate da varie procedure memorizzate di SQL Server.

Per soddisfare le esigenze di prestazioni, utilizziamo i TVP, ma è necessario implementare il proprio abstract di dbDataReader per superare alcuni problemi di prestazioni nella modalità di elaborazione predefinita. Non entrerò nei modi e perché perché sono fuori portata per questa richiesta.

Non ho considerato l'elaborazione XML poiché non ho trovato un'implementazione XML che rimane performante con più di 10.000 "righe".

L'elaborazione dell'elenco può essere gestita mediante l'elaborazione di tabelle tally (numeri) a dimensione singola e doppia. Li abbiamo usati con successo in varie aree, ma i TVP ben gestiti sono più performanti quando ci sono più di duecento "file".

Come per tutte le scelte relative all'elaborazione di SQL Server, è necessario effettuare la selezione in base al modello di utilizzo.


5

Ho finalmente avuto la possibilità di fare alcuni TableValuedParameters e funzionano alla grande, quindi ho intenzione di incollare un sacco di codice che mostra come li sto usando, con un campione da alcuni dei miei codici attuali: (nota: usiamo ADO .NETTO)

Nota anche: sto scrivendo del codice per un servizio e ho molti bit di codice predefiniti nell'altra classe, ma sto scrivendo questo come un'app console in modo da poter eseguire il debug, quindi ho strappato tutto da l'app console. Scusa il mio stile di codifica (come stringhe di connessione hardcoded) in quanto era una specie di "costruisci uno da buttare via". Volevo mostrare come uso a List<customObject>e inserirlo facilmente nel database come tabella, che posso usare nella procedura memorizzata. Codice C # e TSQL di seguito:

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

Inoltre, prenderò critiche costruttive sul mio stile di programmazione se lo hai da offrire (a tutti i lettori che si imbattono in questa domanda) ma ti prego di mantenerlo costruttivo;) ... Se mi vuoi davvero, trovami nella chat qui . Spero che con questo pezzo di codice si possa vedere come possono usare List<Current>come ho definito come una tabella nel db e List<T>nella loro app.


3

Vorrei andare con la proposta n. 1 o, in alternativa, creare una tabella scratch che contiene solo ID elaborati. Inserisci in quella tabella durante l'elaborazione, quindi una volta terminato, chiama un proc simile al seguente:

BEGIN TRAN

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

TRUNCATE TABLE processedIds

COMMIT TRAN

Farai molti inserti, ma saranno su un tavolino, quindi dovrebbe essere veloce. Puoi anche raggruppare i tuoi inserti utilizzando ADO.net o qualsiasi adattatore dati che stai utilizzando.


2

Il titolo della domanda include l'attività di trasmissione dei dati da un'applicazione alla procedura memorizzata. Quella parte è esclusa dal corpo della domanda, ma lasciami provare a rispondere anche a questa.

Nel contesto di sql-server-2008, come specificato dai tag, c'è un altro grande articolo di E. Sommarskog Matrici ed elenchi in SQL Server 2008 . A proposito, l'ho trovato nell'articolo a cui Marian faceva riferimento nella sua risposta.

Invece di dare semplicemente il link, cito il suo elenco di contenuti:

  • introduzione
  • sfondo
  • Parametri con valori di tabella in T-SQL
  • Passaggio di parametri con valori di tabella da ADO .NET
    • Utilizzando un elenco
    • Utilizzando una DataTable
    • Utilizzando un DataReader
    • Osservazioni finali
  • Utilizzo dei parametri con valori di tabella da altre API
    • ODBC
    • OLE DB
    • ADO
    • LINQ ed Entity Framework
    • JDBC
    • PHP
    • Perl
    • Cosa succede se l'API non supporta i TVP
  • Considerazioni sulle prestazioni
    • Lato server
    • Dalla parte del cliente
    • Chiave primaria o no?
  • Ringraziamenti e feedback
  • Cronologia delle revisioni

Al di là delle tecniche menzionate qui, ho la sensazione che in alcuni casi la massa e l'inserto di massa meritino di essere menzionati nell'ambito del caso generale.


1

Passando i parametri dell'array a una procedura memorizzata

Per l'ultima versione di MS SQL 2016

Con MS SQL 2016 introducono una nuova funzione: SPLIT_STRING () per analizzare più valori.

Questo può risolvere facilmente il tuo problema.

Per MS SQL versione precedente

Se stai utilizzando una versione precedente, segui questo passaggio:

Prima fai una funzione:

 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

Dopo aver effettuato l'operazione, basta passare la stringa a questa funzione con separatore.

Spero che questo ti sia di aiuto. :-)


-1

Utilizzare questo per creare "Crea tabella dei tipi". semplice esempio per l'utente

CREATE TYPE unit_list AS TABLE (
    ItemUnitId int,
    Amount float,
    IsPrimaryUnit bit
);

GO
 CREATE TYPE specification_list AS TABLE (
     ItemSpecificationMasterId int,
    ItemSpecificationMasterValue varchar(255)
);

GO
 declare @units unit_list;
 insert into @units (ItemUnitId, Amount, IsPrimaryUnit) 
  values(12,10.50, false), 120,100.50, false), (1200,500.50, true);

 declare @spec specification_list;
  insert into @spec (ItemSpecificationMasterId,temSpecificationMasterValue) 
   values (12,'test'), (124,'testing value');

 exec sp_add_item "mytests", false, @units, @spec


//Procedure definition
CREATE PROCEDURE sp_add_item
(   
    @Name nvarchar(50),
    @IsProduct bit=false,
    @UnitsArray unit_list READONLY,
    @SpecificationsArray specification_list READONLY
)
AS


BEGIN
    SET NOCOUNT OFF     

    print @Name;
    print @IsProduct;       
    select * from @UnitsArray;
    select * from @SpecificationsArray;
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.