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

#1-表值参数。我可以定义仅带ID字段的表类型,并发送包含ID的表以进行更新。

建议#2-proc正文中带有OPENXML()的XML参数(varchar)。 />
建议#3-列表解析。如果可能的话,我宁愿避免这种情况,因为它似乎笨拙且容易出错。

其中任何偏好还是我错过的任何想法?

评论

您如何获得大量的ID?

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

#1 楼

关于此问题的最好的文章是Erland Sommarskog撰写的:


SQL Server 2008中的数组和列表-使用表值参数
SQL Server 2005及更高版本中的数组和列表-当TVP不能剪切时
在SQL Server 2000及更早版本中的数组和列表

他涵盖了所有选项并且解释得很好。答案,但是Erland在Arrays上的文章就像Joe Celko在树和其他SQL对待方面的书:)

#2 楼

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

这种方法的优点是:


make一个存储过程调用,所有数据都作为1个参数传入。
表输入是经过结构化和强类型化的
无需字符串构建/解析或处理XML
就可以轻松地使用表输入进行过滤,联接或任何

但是,请注意:如果通过ADO.NET或ODBC调用使用TVP的存储过程,并使用SQL Server Profiler查看活动,则会注意到SQL Server收到几个INSERT语句加载TVP,TVP中的每一行都需要一个,然后调用该过程。这是设计使然。每次调用该过程时,都需要编译这批INSERT,这构成了小的开销。但是,即使有这么多开销,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”。

–大面
16-11-29在8:09



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

–尼克·查玛斯(Nick Chammas)
16年11月29日在16:25

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

–EAmez
19年1月18日在9:17

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

–尼克·查玛斯(Nick Chammas)
19年1月19日在18:02

#3 楼

在Erland Sommarskog的权威文章“ SQL Server中的数组和列表”中讨论了整个主题。请选择要选择的版本。

摘要,对于SQL Server 2008之前的版本,TVP胜过其余版本


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

无论如何,这篇文章还是值得一读的,以了解其他技术和思考。

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

#4 楼

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


#5 楼

在向您介绍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.兰伯特
2011年1月14日20:02

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

– jcolebrand♦
2011年1月14日20:05

#6 楼

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

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

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

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

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

#7 楼

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

还请注意:我正在为服务编写一些代码,而另一个类中有很多预定义的代码位,但是我将其编写为控制台应用程序是为了我可以调试它,所以我从控制台应用程序中删除了所有这些内容。请原谅我的编码风格(例如硬编码的连接字符串),因为它有点像“丢掉一个”。我想展示如何使用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>,因为我在数据库中将其定义为表,并在其应用程序中将其定义为List<T>

#8 楼

我可以选择提案#1或作为替代方案,创建一个只保存已处理ID的草稿表。在处理过程中插入到该表中,然后完成操作,调用类似于以下内容的过程:表,所以它应该很快。您还可以使用ADO.net或使用的任何数据适配器来批量处理插入内容。

#9 楼

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

在标记所指定的sql-server-2008上下文中,E。Sommarskog的Arrays and Lists在SQL Server 2008中还有另一篇很棒的文章。顺便说一句,我在玛丽亚(Marian)所指的文章中找到了它。答案。

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


简介
背景
表值参数在T-SQL中
从ADO .NET中传递表值参数

使用列表
使用数据表
使用DataReader
最后的注释
/>

使用其他API中的表值参数

ADO
LINQ和实体框架
JDBC
PHP
Perl
如果您的API不支持TVP


性能注意事项
服务器端
客户端-侧面
是否具有主键?


确认和反馈
修订历史记录

除了那里提到的技术外,我有一种感觉在某些情况下,批量复制和ulk插件值得在一般情况下提及。

#10 楼


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

对于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


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

希望对您有所帮助。 :-)

#11 楼

您可以像这样输入一串用逗号分隔的列表:

-- Here is the String Array you want to convert to a Table
declare @StringArray varchar(max)
set @StringArray = 'First item,Second item,Third item';

-- Here is the table which is going to contain the rows of each item in the String array
declare @@mytable table (EachItem varchar(50))

-- Just create a select statement appending UNION ALL to each one of the item in the array
set @StringArray = 'select ''' + replace(@StringArray, ',', ''' union all select ''') + ''''
-- Push the data into your table
insert into @@mytable exec (@StringArray)

-- You now have the data in an an array inside a table that you can join to other objects
select * from @@mytable


#12 楼

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

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