【问题标题】:How to pass an array into a SQL Server stored procedure如何将数组传递到 SQL Server 存储过程
【发布时间】:2012-06-21 13:47:14
【问题描述】:

如何将数组传递给 SQL Server 存储过程?

例如,我有一个员工列表。我想将此列表用作表格并将其与另一个表格连接。但是员工列表应该作为 C# 的参数传递。

【问题讨论】:

标签: c# sql-server tsql stored-procedures


【解决方案1】:

SQL Server 2008(或更新版本)

首先,在您的数据库中,创建以下两个对象:

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

现在在您的 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

如果您使用的是 SQL Server 2005,我仍然建议使用 XML 上的拆分函数。首先,创建一个函数:

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

现在您的存储过程可以是:

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

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

在您的 C# 代码中,您只需将列表传递为 '1,2,3,12'...


我发现传递表值参数的方法简化了使用它的解决方案的可维护性,并且与其他实现(包括 XML 和字符串拆分)相比,通常具有更高的性能。

输入是明确定义的(没有人必须猜测分隔符是逗号还是分号),并且我们不依赖于其他处理函数,这些函数在不检查存储过程的代码的情况下并不明显。

与涉及用户定义的 XML 模式而不是 UDT 的解决方案相比,这涉及相似数量的步骤,但根据我的经验,管理、维护和阅读的代码要简单得多。

在许多解决方案中,您可能只需要这些 UDT(用户定义的类型)中的一个或几个,这些 UDT(用户定义的类型)可以重复用于许多存储过程。与这个例子一样,常见的要求是传递一个 ID 指针列表,函数名称描述了这些 Id 应该代表什么上下文,类型名称应该是通用的。

【讨论】:

  • 我喜欢表格参数的想法——从来没有想过——干杯。值得将分隔符传递给 SplitInts() 函数调用。
  • 如果只能访问逗号分隔的字符串,如何使用 table 参数
  • @bdwain 会破坏目的 - 您必须使用拆分功能将其分成几行以放入 TVP。在您的应用程序代码中分解它。
  • @AaronBertrand 感谢您的回复,我实际上只是想通了。我必须在括号中使用子选择:SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds)
  • @th1rdey3 它们是隐式可选的。 stackoverflow.com/a/18926590/61305
【解决方案2】:

根据我的经验,通过从employeeIDs 创建一个分隔表达式,这个问题有一个棘手而好的解决方案。您应该只创建一个类似';123;434;365;' 的字符串表达式,其中123434365 是一些员工ID。通过调用以下过程并将此表达式传递给它,您可以获取所需的记录。您可以轻松地将“另一个表”加入此查询。此解决方案适用于所有版本的 SQL Server。此外,与使用表变量或临时表相比,它是非常快速和优化的解决方案。

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

【讨论】:

  • 不错!我真的很喜欢这种在 int 键上进行过滤的方法! +1
  • @MDV2000 谢谢 :) 在字符串键上,这也有很好的性能,因为没有将 int 列转换为 varchar ...
  • 我来晚了,但这很聪明!非常适合我的问题。
  • 这太棒了!我一定会用这个,谢谢
  • 这不是一个好的解决方案,因为 Like % % 会导致对表进行扫描
【解决方案3】:

为您的存储过程使用表值参数。

当您从 C# 传入时,您将添加数据类型为 SqlDb.Structured 的参数。

请看这里:http://msdn.microsoft.com/en-us/library/bb675163.aspx

例子:

// 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();
}

【讨论】:

    【解决方案4】:

    您需要将其作为 XML 参数传递。

    编辑:我的项目中的快速代码给你一个想法:

    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)
    

    然后您可以使用临时表加入您的表。 我们将 arrayOfUlong 定义为内置 XML 模式以维护数据完整性,但您不必这样做。我建议使用它,因此这里有一个快速代码,可确保您始终获得带有 long 的 XML。

    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
    

    【讨论】:

    • 我认为当有很多行时使用表变量是个坏主意?使用临时 (#table) 表不是更好的性能吗?
    • @ganders:反之亦然。
    【解决方案5】:

    上下文总是很重要的,比如数组的sizecomplexity。对于中小型列表,这里发布的几个答案都很好,但应该做出一些澄清:

    • 对于拆分分隔列表,基于 SQLCLR 的拆分器是最快的。如果你想自己写,有很多例子,或者你可以下载免费的SQL# CLR 函数库(我写的,但是 String_Split 函数和许多其他函数是完全免费的)。
    • 拆分基于 XML 的数组 可以很快,但您需要使用基于属性的 XML,而不是基于元素的 XML(这是此处答案中显示的唯一类型,尽管 @AaronBertrand 的 XML示例是最好的,因为他的代码使用了text() XML 函数。有关使用 XML 拆分列表的更多信息(即性能分析),请查看 Phil Factor 的"Using XML to pass lists as parameters in SQL Server"
    • 使用 TVP 非常棒(假设您至少使用 SQL Server 2008 或更高版本),因为数据会流式传输到 proc 并显示为预解析和强类型的表变量。然而,在大多数情况下,将所有数据存储在DataTable 中意味着复制内存中的数据,因为它是从原始集合中复制的。因此,使用DataTable 传入 TVP 的方法不适用于较大的数据集(即不能很好地扩展)。
    • 与简单的 Int 或 String 分隔列表不同,XML 可以处理多个一维数组,就像 TVP 一样。但也与DataTable TVP 方法一样,XML 不能很好地扩展,因为它需要将内存中的数据大小增加一倍以上,因为它需要额外考虑 XML 文档的开销。

    综上所述,如果您使用的数据很大或不是很大但一直在增长,那么IEnumerable TVP 方法是最佳选择,因为它将数据流式传输到 SQL Server(如@ 987654329@ 方法),但不需要在内存中复制任何集合(与任何其他方法不同)。我在这个答案中发布了一个 SQL 和 C# 代码示例:

    Pass Dictionary to Stored Procedure T-SQL

    【讨论】:

      【解决方案6】:

      正如上面其他人所指出的,一种方法是将数组转换为字符串,然后在 SQL Server 中拆分字符串。

      从 SQL Server 2016 开始,有一种内置的方法来拆分字符串,称为

      STRING_SPLIT()

      它返回一组可以插入到临时表(或真实表)中的行。

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

      会产生:

      价值 ----- 123 456 789 246 22 33 44 55 66

      如果你想变得更漂亮:

      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
      

      会给你与上面相同的结果(除了列名是“thenumber”),但已排序。您可以像使用任何其他表一样使用表变量,因此您可以根据需要轻松地将其与数据库中的其他表连接起来。

      请注意,您的 SQL Server 安装必须在兼容级别 130 或更高级别才能识别 STRING_SPLIT() 函数。您可以使用以下查询检查您的兼容性级别:

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

      大多数语言(包括 C#)都有一个“join”函数,可用于从数组创建字符串。

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

      然后将sqlparam 作为参数传递给上面的存储过程。

      【讨论】:

        【解决方案7】:

        sql server 不支持数组,但是有几种方法可以将集合传递给存储过程。

        1. 通过使用数据表
        2. 通过使用 XML。尝试将您的集合转换为 xml 格式,然后将其作为输入传递给存储过程

        以下链接可能对您有所帮助

        passing collection to a stored procedure

        【讨论】:

          【解决方案8】:

          这将对您有所帮助。 :) 按照接下来的步骤,

          1. 打开查询编辑器

          2. 复制粘贴下面的代码,它将创建将String转换为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. 创建以下存储过程

             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. 使用exec sp_DeleteId '1,2,3,12' 执行此SP,这是您要删除的一串Id,

          5. 您可以在 C# 中将数组转换为字符串,并将其作为存储过程参数传递,如下所示,

            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 ;
            

          这将删除单个存储过程调用中的多行。一切顺利。

          【讨论】:

          • 我已经使用了这个逗号分隔的解析功能,它适用于小数据集,如果你检查它的执行计划,它会导致大数据集出现问题,你必须有多个 csv 列表在存储过程中
          【解决方案9】:

          我一直在搜索有关如何将任何数组传递给 sql server 而无需创建新表类型的所有示例和答案,直到我找到了这个linK,以下是我如何将它应用到我的项目中:

          --以下代码将获取一个数组作为参数并插入该数组的值 --array 到另一个表中

          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 
          

          希望你喜欢它

          【讨论】:

          • @zaitsman:清洁并不意味着最好或最合适。人们经常放弃灵活性和/或“适当的”复杂性和/或性能来获得“干净”的代码。这里的答案是“好的”,但仅适用于小型数据集。如果传入数组 @s 是 CSV,那么简单地拆分它会更快(即 INSERT INTO...SELECT FROM SplitFunction)。转换为 XML 比 CLR 慢,而基于属性的 XML 无论如何要快得多。这是一个简单的列表,但传入 XML 或 TVP 也可以处理复杂的数组。不知道避免使用简单的一次性CREATE TYPE ... AS TABLE 会获得什么。
          【解决方案10】:

          从 SQL Server 2016 开始,您可以简单地使用拆分字符串

          例子:

          WHERE (@LocationId IS NULL OR Id IN (SELECT items from Split_String(@LocationId, ',')))
          

          【讨论】:

            【解决方案11】:

            我花了很长时间才弄明白,所以以防万一有人需要...

            这是基于 Aaron 回答中的 SQL 2005 方法,并使用他的 SplitInts 函数(我刚刚删除了 delim 参数,因为我总是使用逗号)。我正在使用 SQL 2008,但我想要一些适用于类型化数据集(XSD、TableAdapters)的东西,我知道字符串参数适用于这些。

            我试图让他的函数在“where in (1,2,3)”类型的子句中工作,但没有运气直接的方式。所以我先创建了一个临时表,然后做了一个内部连接而不是“在哪里”。这是我的示例用法,在我的情况下,我想获取不包含某些成分的食谱列表:

            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)
            

            【讨论】:

            • 一般来说,最好不要加入表变量,而是加入临时表。默认情况下,表变量似乎只有一行,尽管有一两个技巧(查看@AaronBertrand 的优秀而详细的文章:sqlperformance.com/2014/06/t-sql-queries/…)。
            【解决方案12】:
            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
            

            【讨论】:

              【解决方案13】:

              从 SQL Server 2016 开始,您可以将列表作为 NVARCHAR() 引入并使用 OPENJSON

              DECLARE @EmployeeList nvarchar(500) = '[1,2,15]'
              
              
              SELECT * 
              FROM Employees
              WHERE ID IN (SELECT VALUE FROM OPENJSON(@EmployeeList ))
              

              【讨论】:

                猜你喜欢
                • 2014-08-29
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2017-03-30
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多