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


Answers:


437

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(用户定义类型)重新用于许多存储过程。与该示例一样,通常的要求是传递ID指针列表,函数名称描述那些Id应该表示的上下文,类型名称应该是通用的。


3
我喜欢表参数的想法-从来没有想过-欢呼。分隔符需要将其传递给SplitInts()函数调用。
德拉米

如果只能访问以逗号分隔的字符串,我该如何使用table参数
bdwain

@bdwain会达到目的-您必须使用split函数将其分成几行以放入TVP。在您的应用程序代码中进行细分。
亚伦·伯特兰

1
@AaronBertrand感谢您的答复,我实际上只是想通了。我必须在方括号中使用子选择:SELECT [colA] FROM [MyTable] WHERE [Id] IN (SELECT [Id] FROM @ListOfIds)
JaKXz 2014年

3
@ th1rdey3是隐式可选的。stackoverflow.com/a/18926590/61305
亚伦·贝特朗

44

根据我的经验,通过从employeeID创建定界表达式,此问题有一个棘手且不错的解决方案。您应该只创建一个字符串表达式,例如';123;434;365;'in-which 123434并且365是一些employeeID。通过调用以下过程并将此表达式传递给它,您可以获取所需的记录。您可以轻松地将“另一个表”联接到该查询中。该解决方案适用于所有版本的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 '16

@ MDV2000谢谢:)在字符串键上,由于没有将int列强制转换为varchar,所以它也具有良好的性能……
Hamed Nazaktabar

我玩游戏迟到了,但这很聪明!适用于我的问题。
user441058

这真太了不起了!我一定会用这个,谢谢
奥马尔·鲁德

26

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

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

17

您需要将其作为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模式以维护数据完整性,但您不必这样做。我建议您使用它,因此这里有一个快速代码,以确保您总是得到长整型的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 2012年

@ganders:反之亦然。
abatishchev 2014年

14

上下文始终很重要,例如数组的大小复杂性。对于中小型列表,尽管应作一些说明,但此处发布的几个答案也不错。

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

综上所述,如果您正在使用的数据很大或不是很大但仍在不断增长,则IEnumerableTVP方法是最佳选择,因为它会将数据流传输到SQL Server(如该DataTable方法),但不会要求在内存中进行任何重复的收集(与其他方法不同)。我在此答案中张贴了SQL和C#代码的示例:

将字典传递给存储过程T-SQL


6

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

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

以下链接可能对您有帮助

将集合传递给存储过程


5

我一直在搜索所有示例以及如何将任何数组传递给sql server的答案,而无需麻烦地创建新的Table类型,直到找到此linK,以下是我将其应用于我的项目的方式:

-以下代码将获取一个Array作为参数并将该--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 

希望你喜欢它


2
@zaitsman:最干净并不意味着最好或最合适。人们常常放弃灵活性和/或“适当的”复杂性和/或性能以获得“干净的”代码。这里的答案是“可以”,但仅适用于小型数据集。如果传入的数组@s是CSV,则将其简单拆分(即INSERT INTO ... SELECT FROM SplitFunction)会更快。转换为XML的速度比CLR慢,而基于属性的XML则要快得多。这是一个简单的列表,但是传入XML或TVP也可以处理复杂的数组。不知道避免一次简单的一次性操作会获得什么CREATE TYPE ... AS TABLE
所罗门·鲁兹基

5

这将为您提供帮助。:)遵循以下步骤,

  1. 打开查询设计器
  2. 复制并粘贴以下代码,它将创建将字符串转换为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. 执行此SP使用exec sp_DeleteId '1,2,3,12'这是您要删除的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列表
Saboor Awan

2

我花了很长时间才弄清楚这一点,以防万一有人需要...

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

我试图让他的功能在“(1,2,3)中的where”子句中工作,并且没有直接的运气。因此,我首先创建了一个临时表,然后进行了内部联接而不是“ where in”。这是我的示例用法,在我的情况下,我想获取不包含某些成分的食谱列表:

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)

一般来说,最好不要将JOIN联接到表变量,而应联接到临时表。默认情况下,表变量似乎只显示一行,尽管周围存在一两个窍门(请参阅@AaronBertrand的出色而详尽的文章:sqlperformance.com/2014/06/t-sql-queries/…)。
所罗门·鲁兹基2014年

1

正如上面其他人提到的,执行此操作的一种方法是将数组转换为字符串,然后在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#)都具有“联接”功能,可用于从数组创建字符串。

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

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


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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.