Pasar parámetros de matriz a un procedimiento almacenado


53

Tengo un proceso que toma un montón de registros (1000) y los opera, y cuando termine, necesito marcar una gran cantidad de ellos como procesados. Puedo indicar esto con una gran lista de ID. Estoy tratando de evitar el patrón de "actualizaciones en un bucle", así que me gustaría encontrar una forma más eficiente de enviar esta bolsa de ID a un proceso almacenado de MS SQL Server 2008.

Propuesta n. ° 1: Parámetros de valores de tabla. Puedo definir un tipo de tabla con solo un campo de ID y enviar una tabla llena de ID para actualizar.

Propuesta n. ° 2: parámetro XML (varchar) con OPENXML () en el cuerpo del proceso.

Propuesta n. ° 3: análisis de la lista. Prefiero evitar esto, si es posible, ya que parece difícil de manejar y propenso a errores.

¿Alguna preferencia entre estas o alguna idea que me haya perdido?


¿Cómo está obteniendo la gran lista de ID?
Larry Coleman

Los estoy bajando junto con los datos de "carga útil" a través de otro proceso almacenado. Sin embargo, no necesito actualizar todos esos datos, solo actualice una marca en ciertos registros.
D. Lambert

Respuestas:


42

Los mejores artículos sobre este tema son de Erland Sommarskog:

Cubre todas las opciones y explica bastante bien.

Perdón por la brevedad de la respuesta, pero el artículo de Erland sobre matrices es como los libros de Joe Celko sobre árboles y otras delicias de SQL :)


23

Hay una gran discusión sobre esto en StackOverflow que cubre muchos enfoques. El que prefiero para SQL Server 2008+ es usar parámetros con valores de tabla . Esta es esencialmente la solución de SQL Server para su problema: pasar una lista de valores a un procedimiento almacenado.

Las ventajas de este enfoque son:

  • realice una llamada a procedimiento almacenado con todos sus datos pasados ​​como 1 parámetro
  • la entrada de la tabla está estructurada y fuertemente tipada
  • sin creación de cadenas / análisis o manejo de XML
  • puede usar fácilmente la entrada de la tabla para filtrar, unir o lo que sea

Sin embargo, tome nota: si llama a un procedimiento almacenado que usa TVP a través de ADO.NET u ODBC y observa la actividad con SQL Server Profiler, notará que SQL Server recibe varias INSERTinstrucciones para cargar el TVP, una para cada fila en el TVP , seguido de la llamada al procedimiento. Esto es por diseño . Este lote de INSERTs debe compilarse cada vez que se llama al procedimiento, y constituye una pequeña sobrecarga. Sin embargo, incluso con esta sobrecarga, los TVP aún eliminan otros enfoques en términos de rendimiento y usabilidad para la mayoría de los casos de uso.

Si desea obtener más información, Erland Sommarskog tiene toda la información sobre cómo funcionan los parámetros con valores de tabla y ofrece varios ejemplos.

Aquí hay otro ejemplo que inventé:

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

Cuando ejecuto esto, aparece un error: Msg 2715, Nivel 16, Estado 3, Procedimiento tvp_test, Línea 4 [Batch Start Line 4] Columna, parámetro o variable # 2: No se puede encontrar el tipo de datos id_list. El parámetro o variable '@customer_list' tiene un tipo de datos no válido. Mensaje 1087, Nivel 16, Estado 1, Procedimiento tvp_test, Línea 13 [Línea de inicio de lote 4] Debe declarar la variable de tabla "@customer_list".
Damian

@Damian: ¿la CREATE TYPEdeclaración al principio se ejecutó correctamente? ¿Qué versión de SQL Server está ejecutando?
Nick Chammas

En el código SP tiene esta oración en línea `SELECT @ param1 AS param1; ' . ¿Cuál es el propósito? No utiliza o param1, entonces, ¿por qué puso esto como parámetro en el encabezado SP?
EAmez

@EAmez: fue solo un ejemplo arbitrario. El punto @customer_listno es @param1. El ejemplo simplemente demuestra que puede mezclar diferentes tipos de parámetros.
Nick Chammas

21

Todo el tema se discute en el artículo definitivo de Erland Sommarskog: "Matrices y listas en SQL Server" . Elija cuál versión elegir.

Resumen, para pre SQL Server 2008 donde los TVP triunfan sobre el resto

  • CSV, divide como quieras (generalmente uso una tabla de números)
  • XML y análisis (mejor con SQL Server 2005+)
  • Crear una tabla temporal en el cliente

Vale la pena leer el artículo de todos modos para ver otras técnicas y pensamientos.

Editar: respuesta tardía para grandes listas en otros lugares: pasar parámetros de matriz a un procedimiento almacenado


14

Sé que llego tarde a esta fiesta, pero tuve un problema en el pasado, tuve que enviar hasta 100K números bigint e hice algunos puntos de referencia. Terminamos enviándolos en formato binario, como una imagen, que fue más rápido que todo lo demás para números de hasta 100K.

Aquí está mi viejo código (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

El siguiente código está empacando enteros en un blob binario. Estoy invirtiendo el orden de bytes aquí:

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

Estoy dividido entre remitirlo a SO o responderlo aquí, porque esta es casi una pregunta de programación. Pero como ya tengo una solución que uso ... la publicaré;)

La forma en que funciona es alimentar una cadena delimitada por comas (división simple, no hace divisiones de estilo CSV) en el procedimiento almacenado como varchar (4000) y luego alimentar esa lista en esta función y obtener una tabla práctica de nuevo, una tabla de solo varchars.

Esto le permite enviar los valores de solo los identificadores que desea procesar, y puede hacer una unión simple en ese punto.

Alternativamente, podría hacer algo con un CLR DataTable y alimentarlo, pero eso es un poco más costoso de soportar y todos entienden las listas 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

Bueno, estaba tratando específicamente de evitar la lista delimitada por comas para no tener que escribir algo así, pero como ya está escrito, creo que tendría que volver a poner esa solución en la mezcla. ;-)
D. Lambert

1
Digo probado y verdadero es lo más fácil. Puede escupir una lista separada por comas en C # en segundos de código, y puede incluirla en esta función (después de incluirla en su sproc) lo suficientemente rápido, y no tiene que pensar en ello. ~ Y sé que dijiste que no querías usar una función, pero creo que es la forma más simple (quizás no la más efectiva)
jcolebrand

5

Regularmente recibo conjuntos de miles de filas y 10000 de filas enviadas desde nuestra aplicación para ser procesadas por varios procedimientos almacenados de SQL Server.

Para satisfacer las demandas de rendimiento, utilizamos TVP, pero debe implementar su propio resumen de dbDataReader para superar algunos problemas de rendimiento en su modo de procesamiento predeterminado. No entraré en los cómo y los porqués, ya que están fuera del alcance de esta solicitud.

No consideré el procesamiento de XML, ya que no he encontrado una implementación de XML que siga funcionando con más de 10,000 "filas".

El procesamiento de la lista se puede manejar mediante el procesamiento de tablas de conteo (números) de una y dos dimensiones. Los hemos usado con éxito en varias áreas, pero los TVP bien administrados son más eficaces cuando hay más de un par de cientos de "filas".

Al igual que con todas las opciones relacionadas con el procesamiento de SQL Server, debe realizar su selección según el modelo de uso.


5

Finalmente tuve la oportunidad de hacer algunos TableValuedParameters y funcionan muy bien, así que voy a pegar un código de lotta completo que muestre cómo los estoy usando, con una muestra de algunos de mis códigos actuales: (nota: usamos ADO .RED)

También tenga en cuenta: estoy escribiendo un código para un servicio, y tengo muchos bits de código predefinidos en la otra clase, pero estoy escribiendo esto como una aplicación de consola para poder depurarlo, así que eliminé todo esto de La aplicación de consola. Disculpe mi estilo de codificación (como cadenas de conexión codificadas) ya que era como "construir uno para tirar". Quería mostrar cómo uso a List<customObject>e insertarlo en la base de datos fácilmente como una tabla, que puedo usar en el procedimiento almacenado. C # y el código TSQL a continuación:

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

Además, tomaré críticas constructivas sobre mi estilo de codificación si tienes algo que ofrecer (a todos los lectores que se encuentren con esta pregunta) pero por favor mantenlo constructivo;) ... Si realmente me quieres, encuéntrame aquí en la sala de chat . Esperemos que con este fragmento de código se pueda ver cómo pueden usar el List<Current>como lo tengo definido como una tabla en el db y a List<T>en su aplicación.


3

Yo iría con la propuesta n. ° 1 o, como alternativa, crearía una tabla de borrador que solo contenga identificadores procesados. Inserte en esa tabla durante el procesamiento, luego, una vez terminado, llame a un proceso similar al siguiente:

BEGIN TRAN

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

TRUNCATE TABLE processedIds

COMMIT TRAN

Hará muchas inserciones, pero estarán en una mesa pequeña, por lo que debería ser rápido. También puede agrupar sus insertos usando ADO.net o cualquier adaptador de datos que esté utilizando.


2

El título de la pregunta incluye la tarea de transmitir datos desde una aplicación al procedimiento almacenado. Esa parte está excluida por el cuerpo de la pregunta, pero permítanme tratar de responderla también.

En el contexto de sql-server-2008 según lo especificado por las etiquetas, hay otro gran artículo de E. Sommarskog Arrays and Lists en SQL Server 2008 . Por cierto, lo encontré en el artículo al que se refería Marian en su respuesta.

En lugar de simplemente dar el enlace, cito su lista de contenido:

  • Introducción
  • Antecedentes
  • Parámetros con valores de tabla en T-SQL
  • Pasar parámetros con valores de tabla desde ADO .NET
    • Usando una lista
    • Usando una tabla de datos
    • Usando un DataReader
    • Observaciones finales
  • Uso de parámetros con valores de tabla de otras API
    • ODBC
    • OLE DB
    • ALHARACA, PREAMBULO, PROBLEMA
    • LINQ y Entity Framework
    • JDBC
    • PHP
    • Perl
    • ¿Qué sucede si su API no admite TVP?
  • Consideraciones de rendimiento
    • Lado del servidor
    • Lado del cliente
    • Clave principal o no?
  • Agradecimientos y comentarios
  • Revisión histórica

Más allá de las técnicas mencionadas allí, tengo la sensación de que, en algunos casos, la copia masiva y el inserto a granel merecen ser mencionados al alcance del caso general.


1

Pasar parámetros de matriz a un procedimiento almacenado

Para MS SQL 2016 última versión

Con MS SQL 2016 introducen una nueva función: SPLIT_STRING () para analizar múltiples valores.

Esto puede resolver su problema fácilmente.

Para la versión anterior de MS SQL

Si está utilizando una versión anterior, siga este paso:

Primero haz una función:

 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

Después de hacer esto, simplemente pase su cadena a esta función con separador.

Espero que esto te sea útil. :-)


-1

Use esto para crear "crear tabla de tipos". ejemplo simple para el usuario

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.