将数组参数传递给存储过程


53

我有一个过程,可以捕获一堆记录(1000条记录)并对其进行操作,完成后,我需要将大量记录标记为已处理。我可以用大量的ID来表明这一点。我试图避免使用“循环更新”模式,因此我想找到一种更有效的方法来将此ID包发送到MS SQL Server 2008存储的proc中。

提案1-表格值参数。我可以定义一个带有ID字段的表类型,然后发送一个包含ID的表进行更新。

提案2-proc正文中带有OPENXML()的XML参数(varchar)。

提案3-列表解析。如果可能的话,我宁愿避免这种情况,因为它似乎笨拙且容易出错。

其中有什么偏好,或者我错过了任何想法?


您如何获得大量的ID?
拉里·科尔曼

我通过另一个存储的过程将它们与“有效载荷”数据一起拉低。不过,我不需要更新所有数据-只需更新某些记录上的标志即可。
D. Lambert

Answers:



23

关于StackOverflow的讨论很多,涵盖了很多方法。对于SQL Server 2008+,我更喜欢使用表值参数。从本质上讲,这是SQL Server对您的问题的解决方案-将值列表传递给存储过程。

这种方法的优点是:

  • 进行一次存储过程调用,并将所有数据作为1参数传入
  • 表输入是结构化的和强类型的
  • 无需字符串构建/解析或处理XML
  • 可以轻松地使用表输入进行过滤,联接或其他操作

但是,请注意:如果通过ADO.NET或ODBC调用使用INSERTTVP 的存储过程,并通过SQL Server Profiler查看活动,则会注意到SQL Server收到多个语句以加载TVP,每行一个在TVP中,然后调用该过程。这是设计使然INSERT每次调用该过程时都需要编译这批s,这构成了很小的开销。但是,即使有这些开销,TVP 在大多数用例的性能和可用性方面仍会淘汰其他方法。

如果您想了解更多信息,Erland Sommarskog 完全了解表值参数的工作方式,并提供了一些示例。

这是我炮制的另一个示例:

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

运行此命令时,出现错误:消息2715,级别16,状态3,过程tvp_test,行4 [Batch Start Line 4]列,参数或变量#2:找不到数据类型id_list。参数或变量“ @customer_list”的数据类型无效。消息1087,级别16,状态1,过程tvp_test,第13行[批处理开始第4行]必须声明表变量“ @customer_list”。
达米安

@Damian- CREATE TYPE开头的语句是否成功运行?您正在运行哪个版本的SQL Server?
Nick Chammas

在SP代码中,此语句是内联`SELECT @ param1 AS param1;'。。目的是什么?您不使用use或param1,为什么将其作为参数放在SP标头中?
EAmez

@EAmez-这只是一个任意例子。重点@customer_list不是@param1。该示例仅说明您可以混合使用不同的参数类型。
Nick Chammas

21

整个主题是在讨论由厄兰Sommarskog明确的文章:“数组和列表在SQL Server”。选择您要选择的版本。

综上所述,对于 SQL Server 2008的台湾居民入境许可证哪里王牌休息

  • CSV,按您的喜好进行拆分(我通常使用数字表)
  • XML和解析(最好使用SQL Server 2005+)
  • 在客户端上创建一个临时表

无论如何,该文章值得阅读,以了解其他技术和思想。

编辑:对于其他地方的巨大列表的最新答案:将数组参数传递给存储过程


14

我知道我参加这个聚会迟到了,但是过去我遇到了这样的问题,必须发送多达10万个bigint数字,并且做了一些基准测试。我们最终以二进制格式将它们作为图像发送-最多传输10万个数字时,它比其他所有格式都快。

这是我的旧(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

以下代码将整数打包为二进制blob。我在这里反转字节顺序:

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

在向您介绍SO或在此处回答它之间,我感到很痛苦,因为这几乎是一个编程问题。但是因为我已经有了解决方案,所以我使用...我将其发布;)

这种方法的工作方式是,您将以逗号分隔的字符串(简单拆分,不执行CSV样式拆分)作为varchar(4000)馈入存储过程,然后将该列表馈入此函数并得到一个方便的表,一张只有varchars的表。

这样,您就可以仅发送要处理的id的值,然后可以进行简单的连接。

或者,您可以使用CLR DataTable进行某些操作并将其输入,但这需要更多的支持开销,并且每个人都可以理解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

好吧,我特别想避免使用逗号分隔的列表,这样我就不必写类似的东西,但是既然已经写好了,我想我必须把解决方案重新投入使用。;-)
D. Lambert

1
我说尝试和真实是最简单的。您可以在几秒钟的代码中用C#吐出一个逗号分隔的列表,并且可以将它快速放入该函数中(在将其放入存储过程之后),而您甚至不必考虑它。〜而且我知道您说过您不想使用函数,但是我认为这是最简单的方法(也许不是最有效的方法)
jcolebrand

5

我通常会收到从我们的应用程序发送的1000行和10000行的数据集,这些数据集将由各种SQL Server存储过程进行处理。

为了满足性能需求,我们使用TVP,但是您必须实现自己的dbDataReader抽象,以克服其默认处理模式下的一些性能问题。我不会讨论方法和原因,因为它们超出了此请求的范围。

我没有考虑过XML处理,因为我还没有发现XML实现仍然具有超过10,000个“行”的性能。

列表处理可以通过一维和二维计数(数字)表处理来处理。我们已经在各个领域成功使用了这些功能,但是当有数百个“行”时,管理良好的TVP的性能会更高。

与有关SQL Server处理的所有选择一样,您必须根据使用模型进行选择。


5

我终于有机会做一些TableValuedParameters并且它们工作得很好,所以我将粘贴一些完整的lotta代码,以显示我如何使用它们,以及一些当前代码中的示例:(注意:我们使用ADO 。净)

另请注意:我正在为服务编写一些代码,并且在另一个类中有很多预定义的代码位,但是我将其编写为控制台应用程序以便可以对其进行调试,因此我将所有内容从控制台应用程序。请原谅我的编码风格(如硬编码的连接字符串),因为它有点像“丢掉一个”。我想展示如何使用a List<customObject>并将其轻松地作为表推送到数据库中,以便在存储过程中使用。下面的C#和TSQL代码:

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

另外,如果您愿意(对于遇到此问题的所有读者),我也会对我的编码风格提出建设性批评,但请保持建设性;)...如果您真的想要我,请在此处的聊天室中找到我。希望借助这段代码,可以看到他们如何使用List<Current>db,因为我将其定义为db中的表和List<T>他们的应用程序中的a。


3

我可以选择提案#1或作为替代方案,创建一个只保存已处理ID的草稿表。在处理期间插入到该表中,然后在完成后调用类似于以下内容的proc:

BEGIN TRAN

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

TRUNCATE TABLE processedIds

COMMIT TRAN

您将执行许多插入操作,但是它们将插入到一张小桌子上,因此它应该很快。您也可以使用ADO.net或您使用的任何数据适配器批量处理插入内容。


2

问题标题包括将数据从应用程序传输到存储过程的任务。该部分不包括在问题正文中,但让我也尝试回答这个问题。

在标记所指定的sql-server-2008上下文中,还有E. Sommarskog的《SQL Server 2008中的数组和列表》一书。顺便说一句,我在他的答案中提到的玛丽安文章中找到了它。

我不只是给出链接,而是引用其内容列表:

  • 介绍
  • 背景
  • T-SQL中的表值参数
  • 从ADO .NET传递表值参数
    • 使用清单
    • 使用数据表
    • 使用DataReader
    • 结束语
  • 使用其他API中的表值参数
    • ODBC
    • OLE DB
    • 阿多
    • LINQ和实体框架
    • JDBC
    • 的PHP
    • 佩尔
    • 如果您的API不支持TVP,该怎么办
  • 性能考量
    • 服务器端
    • 客户端
    • 主键与否?
  • 致谢和反馈
  • 修订记录

除了那里提到的技术之外,我觉得在某些情况下,应该提及批量复制和批量插入,以使其与一般情况相适应。


1

将数组参数传递给存储过程

对于MS SQL 2016最新版本

使用MS SQL 2016,他们引入了一个新函数:SPLIT_STRING()来解析多个值。

这样可以轻松解决您的问题。

对于MS SQL旧版本

如果您使用的是旧版本,请执行以下步骤:

首先使一个功能:

 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

完成此操作后,只需使用分隔符将字符串传递给此函数即可。

希望对您有帮助。:-)


-1

使用它来创建“创建类型表”。用户的简单示例

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