Cómo pasar una matriz a un procedimiento almacenado de SQL Server

Respuestas:

437

SQL Server 2008 (o más reciente)

Primero, en su base de datos, cree los siguientes dos objetos:

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

Ahora en su código C #:

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

Si está utilizando SQL Server 2005, todavía recomendaría una función dividida sobre XML. Primero, crea una función:

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

Ahora su procedimiento almacenado puede ser:

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

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

Y en su código C # solo tiene que pasar la lista como '1,2,3,12'...


Encuentro que el método de pasar a través de parámetros con valores de tabla simplifica el mantenimiento de una solución que lo utiliza y, a menudo, ha aumentado el rendimiento en comparación con otras implementaciones, incluidas la división de cadenas y XML.

Las entradas están claramente definidas (nadie tiene que adivinar si el delimitador es una coma o un punto y coma) y no tenemos dependencias de otras funciones de procesamiento que no sean obvias sin inspeccionar el código del procedimiento almacenado.

En comparación con las soluciones que implican un esquema XML definido por el usuario en lugar de UDT, esto implica un número similar de pasos, pero en mi experiencia es un código mucho más simple de administrar, mantener y leer.

En muchas soluciones, es posible que solo necesite uno o algunos de estos UDT (tipos definidos por el usuario) que reutiliza para muchos procedimientos almacenados. Al igual que con este ejemplo, el requisito común es pasar por una lista de punteros de ID, el nombre de la función describe qué contexto deben representar esos Ids, el nombre del tipo debe ser genérico.

Aaron Bertrand
fuente
3
Me gusta la idea del parámetro de la tabla, nunca pensé en eso, saludos. Por lo que vale, el delimitador necesita pasar a la llamada de función SplitInts ().
Drammy
¿Cómo puedo usar el parámetro de tabla si solo tengo acceso a una cadena separada por comas
Bdwain
@bdwain frustraría el propósito: tendrías que usar una función de división para dividirlo en filas para ponerlo en el TVP. Divídalo en su código de aplicación.
Aaron Bertrand
1
@AaronBertrand gracias por la respuesta, en realidad lo descubrí. Tengo que usar un sub seleccionar entre los corchetes: SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds).
JaKXz
3
@ th1rdey3 Son implícitamente opcionales. stackoverflow.com/a/18926590/61305
Aaron Bertrand
44

Según mi experiencia, al crear una expresión delimitada a partir de los Id. De empleado, hay una solución difícil y agradable para este problema. Sólo se debe crear una expresión de cadena como ';123;434;365;'en los cuales 123, 434y 365algunas employeeIDs. Al llamar al siguiente procedimiento y pasarle esta expresión, puede obtener los registros que desee. Fácilmente puede unir la "otra tabla" en esta consulta. Esta solución es adecuada en todas las versiones del servidor SQL. Además, en comparación con el uso de tabla variable o tabla temporal, es una solución muy rápida y optimizada.

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
fuente
¡Agradable! ¡Realmente me gusta este enfoque donde estoy filtrando en claves int! +1
MDV2000
@ MDV2000 gracias :) En las claves de cadena, esto también tiene un buen rendimiento debido a que no arroja columnas int a varchar ...
Hamed Nazaktabar
Llego tarde al juego, ¡pero esto es muy inteligente! Funciona muy bien para mi problema.
user441058
¡Esto es increíble! Definitivamente voy a usar esto, gracias
Omar Ruder
26

Use un parámetro con valores de tabla para su procedimiento almacenado.

Cuando lo pase desde C #, agregará el parámetro con el tipo de datos de SqlDb.Structured.

Ver aquí: http://msdn.microsoft.com/en-us/library/bb675163.aspx

Ejemplo:

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

Debe pasarlo como un parámetro XML.

Editar: código rápido de mi proyecto para darle una idea:

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)

Luego puede usar la tabla temporal para unirse a sus tablas. Definimos arrayOfUlong como un esquema XML incorporado para mantener la integridad de los datos, pero no tiene que hacerlo. Recomiendo usarlo, así que aquí hay un código rápido para asegurarse de que siempre obtenga un XML con longs.

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
fuente
¿Pensé que era una mala idea usar variables de tabla cuando hay muchas filas? ¿No es mejor rendimiento usar una tabla temporal (#table) en su lugar?
Gaders
@ganders: Yo diría viceversa.
abatishchev
14

El contexto siempre es importante, como el tamaño y la complejidad de la matriz. Para listas pequeñas a medianas, varias de las respuestas publicadas aquí están bien, aunque se deben hacer algunas aclaraciones:

  • Para dividir una lista delimitada, un divisor basado en SQLCLR es el más rápido. Existen numerosos ejemplos si desea escribir el suyo, o simplemente puede descargar la biblioteca SQL # gratuita de funciones CLR (que escribí, pero la función String_Split, y muchas otras, son completamente gratuitas).
  • La división de matrices basadas en XML puede ser rápida, pero debe usar XML basado en atributos, no XML basado en elementos (que es el único tipo que se muestra en las respuestas aquí, aunque el ejemplo XML de @ AaronBertrand es el mejor ya que su código está usando el código text()Función XML: para obtener más información (es decir, análisis de rendimiento) sobre el uso de XML para dividir listas, consulte "Uso de XML para pasar listas como parámetros en SQL Server" de Phil Factor.
  • El uso de TVP es excelente (suponiendo que esté utilizando al menos SQL Server 2008 o más reciente) ya que los datos se transmiten al proceso y se muestran previamente analizados y fuertemente tipados como una variable de tabla. SIN EMBARGO, en la mayoría de los casos, almacenar todos los datos DataTablesignifica duplicar los datos en la memoria a medida que se copian de la colección original. Por lo tanto, el uso del DataTablemétodo de pasar TVP no funciona bien para conjuntos de datos más grandes (es decir, no escala bien).
  • XML, a diferencia de las simples listas delimitadas de Ints o Strings, puede manejar más de una matriz unidimensional, al igual que los TVP. Pero también, al igual que el DataTablemétodo TVP, XML no se escala bien, ya que duplica el tamaño de datos en la memoria, ya que también debe tener en cuenta la sobrecarga del documento XML.

Dicho todo esto, SI los datos que está utilizando son grandes o aún no son muy grandes pero están creciendo constantemente, entonces el IEnumerablemétodo TVP es la mejor opción ya que transmite los datos a SQL Server (como el DataTablemétodo), PERO no requieren cualquier duplicación de la colección en la memoria (a diferencia de cualquiera de los otros métodos). Publiqué un ejemplo del código SQL y C # en esta respuesta:

Pasar diccionario al procedimiento almacenado T-SQL

Solomon Rutzky
fuente
6

No hay soporte para la matriz en el servidor sql, pero hay varias formas en que puede pasar la colección a un proceso almacenado.

  1. Mediante el uso de datatable
  2. Mediante el uso de XML. Intente convertir su colección en un formato xml y luego páselo como entrada a un procedimiento almacenado

El siguiente enlace puede ayudarlo

pasar la colección a un procedimiento almacenado

Praveen
fuente
5

He estado buscando a través de todos los ejemplos y respuestas sobre cómo pasar cualquier matriz al servidor sql sin la molestia de crear un nuevo tipo de tabla, hasta que encontré este enlace , a continuación es cómo lo apliqué a mi proyecto:

El siguiente código obtendrá una matriz como parámetro e insertará los valores de esa matriz en otra tabla

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 

Espero que lo disfrutes

Adán
fuente
2
@zaitsman: MÁS LIMPIO no significa mejor o más apropiado. A menudo se renuncia a la flexibilidad y / o la complejidad y / o el rendimiento "apropiado" para obtener un código "limpio". Esta respuesta aquí es "ok" pero solo para pequeños conjuntos de datos. Si la matriz entrante @ses CSV, entonces sería más rápido simplemente dividir eso (es decir, INSERTAR EN ... SELECCIONAR DESDE SplitFunction). La conversión a XML es más lenta que CLR, y el XML basado en atributos es mucho más rápido de todos modos. Y esta es una lista simple pero pasar XML o TVP también puede manejar arreglos complejos. No estoy seguro de lo que se gana evitando un simple, una vez CREATE TYPE ... AS TABLE.
Solomon Rutzky
5

Esto te ayudara. :) Sigue los siguientes pasos,

  1. Abra el Diseñador de consultas
  2. Copiar Pegar el siguiente código tal como está, creará la Función que convierte la Cadena a Int

    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. Cree el siguiente procedimiento almacenado

     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. Ejecute este SP. Usando exec sp_DeleteId '1,2,3,12'esto hay una cadena de Id que desea eliminar,

  5. Convierte su matriz en una cadena en C # y la pasa como un parámetro de Procedimiento almacenado

    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 ;

Esto eliminará varias filas, todo lo mejor

Charan Ghate
fuente
He usado esta función de análisis separado por comas, funcionaría para un conjunto de datos pequeño, si verifica su plan de ejecución, causará problemas en el conjunto de datos grande y donde debe tener una lista de múltiples CSV en el procedimiento almacenado
Saboor Awan
2

Me tomó mucho tiempo resolver esto, así que en caso de que alguien lo necesite ...

Esto se basa en el método SQL 2005 en la respuesta de Aaron, y usando su función SplitInts (acabo de eliminar el parámetro delim ya que siempre usaré comas). Estoy usando SQL 2008 pero quería algo que funcione con conjuntos de datos escritos (XSD, TableAdapters) y sé que los parámetros de cadena funcionan con ellos.

Estaba tratando de hacer que su función funcionara en una cláusula de tipo "dónde en (1,2,3)", y no tuve suerte en el camino directo. Así que creé una tabla temporal primero, y luego hice una unión interna en lugar del "en dónde". Aquí está mi ejemplo de uso, en mi caso quería obtener una lista de recetas que no contienen ciertos ingredientes:

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
fuente
En términos generales, es mejor no UNIRSE a una variable de tabla, sino a una tabla temporal. Las variables de tabla, por defecto, solo parecen tener una fila, aunque hay un truco o dos alrededor de eso (consulte el excelente y detallado artículo de @ AaronBertrand: sqlperformance.com/2014/06/t-sql-queries/… ).
Solomon Rutzky
1

Como otros han señalado anteriormente, una forma de hacerlo es convertir su matriz en una cadena y luego dividirla dentro de SQL Server.

A partir de SQL Server 2016, hay una forma integrada de dividir cadenas llamada

STRING_SPLIT ()

Devuelve un conjunto de filas que puede insertar en su tabla temporal (o tabla real).

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

produciría:

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

Si quieres ponerte más elegante:

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

le daría los mismos resultados que anteriormente (excepto que el nombre de la columna es "thenumber"), pero ordenado. Puede usar la variable de tabla como cualquier otra tabla, por lo que puede unirla fácilmente con otras tablas en la base de datos si lo desea.

Tenga en cuenta que su instalación de SQL Server debe estar en el nivel de compatibilidad 130 o superior para STRING_SPLIT()que se reconozca la función. Puede verificar su nivel de compatibilidad con la siguiente consulta:

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

La mayoría de los lenguajes (incluido C #) tienen una función de "unión" que puede usar para crear una cadena a partir de una matriz.

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

Luego pasa sqlparamcomo parámetro al procedimiento almacenado anterior.

Patrick Chu
fuente
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
fuente