【问题标题】:How to fast reduce a datatable according to existing entries in a DB如何根据数据库中的现有条目快速减少数据表
【发布时间】:2016-01-18 19:02:42
【问题描述】:

我目前正在尝试将数据表批量插入到数据库中。它工作得很好而且很快。唯一的问题出现 如果数据库中已有任何行(重复键)。

为了解决这个问题,我修改了我的程序,以便我首先检查每个新条目是否已经存在于数据库中。 这是.......慢(在目前的情况下,我没有很多条目,但后来我需要检查超过 200k 的条目,并且需要检查几次)。 因此,我需要像现在一样让它更快(如果可能的话)。

数据表的结构是这样的:

DataTable transactionTable.Columns.Add("DeviceId", typeof(Int32));
transactionTable.Columns.Add("LogDate", typeof(DateTime));
transactionTable.Columns.Add("LogType", typeof(Int32));
transactionTable.Columns.Add("LogText", typeof(String));

transactionTable.PrimaryKey = new DataColumn[3] {
    transactionTable.Columns[0],
    transactionTable.Columns[1],
    transactionTable.Columns[2]
};

到目前为止,我有以下内容:

DataTable insertTable = transactionTable.Copy();
insertTable.Clear();
using (SqlConnection sqlcon = new SqlConnection(this.GetConnString()))
{
    sqlcon.Open();
    foreach (var entry in transactionTable.AsEnumerable())
    {
        using (SqlCommand sqlCom = sqlCon.CreateCommand())
        {
            sqlCom.Parameters.Clear();
            sqlCom.CommandText = "SELECT 1 FROM myTable WHERE"
                    + " DeviceId = @DeviceId AND LogDate = @LogDate"
                    + " AND LogType = @LogType"
            sqlCom.Parameters.AddWithValue("@DeviceId", entry.Field<Int32>("DeviceId"));
            sqlCom.Parameters.AddWithValue("@LogDate", entry.Field<DateTime>("LogDate"));
            sqlCom.Parameters.AddWithValue("@LogType", entry.Field<Int32>("LogType"));

            using (SqlDataREader myRead = sqlCon.ExecuteReader()
            {
                myRead.Read();

                if (myRead.HasRows == false)
                {
                    insertTable.Rows.Add(entry.ItemArray);
                }
            }

        }
    }
}

// And afterwards the bulkinsert which I think is out of scope for the question itself 
// (I use the insertTable there)

现在我的问题是:有什么方法可以更快地做到这一点,以免出现密钥违规问题?

【问题讨论】:

  • 如果您的数据库在另一个系统/服务器上,那么减少迭代中对数据库的调用次数可能会帮助您提高慢速网络连接的性能,即一次性从数据库中获取相关记录并进行比较断开的数据
  • @harman 因为它的日志只有一个日期和一个类型,我不能说这个类型是在此之后出现的,......在相关数据方面没有什么可做的。因此可悲的是,在我看到的那个方向上什么都没有
  • @JamesBrierley sql 合并是什么意思? (我的问题是我有一个数据表,我通过批量复制将其放入数据库中,如果遇到重复的键,批量复制会自动失败。所以不确定 sql 合并语句在那里有什么帮助?)
  • 我在想你可以使用类似于stackoverflow.com/a/9649040/5111146 的解决方案,但我自己对 sql merge 不太了解

标签: c# sql sql-server datatable sql-server-2012


【解决方案1】:

在这种情况下,我会使用一些临时表。以下是一些步骤:

  1. 批量插入临时表(使用SqlBulkCopy
  2. 使用带有左连接的存储过程插入基表以消除现有行
  3. 截断临时表

因此,您需要删除代码中的 foreach 语句,添加用于插入基表的存储过程,添加用于截断的存储过程。或者您可以将最后两个步骤合二为一。

【讨论】:

    【解决方案2】:

    我有类似的设置。

    我正在使用带有Table-Valued 参数和MERGE 语句的存储过程。另请参阅Table-Valued Parameters,例如如何在 .NET 中使用它们。

    我会将问题的焦点从简单的批量插入转移到将一批行合并到包含现有数据的表中。

    目标表

    CREATE TABLE [dbo].[MyTable](
        [DeviceId] [int] NOT NULL,
        [LogDate] [datetime] NOT NULL,
        [LogType] [int] NOT NULL,
        [LogText] [nvarchar](50) NOT NULL,
    CONSTRAINT [PK_MyTable] PRIMARY KEY CLUSTERED 
    (
        [DeviceId] ASC,
        [LogDate] ASC,
        [LogType] ASC
    ))
    

    创建用户定义的表类型

    CREATE TYPE [dbo].[MyTableType] AS TABLE(
        [DeviceId] [int] NOT NULL,
        [LogDate] [datetime] NOT NULL,
        [LogType] [int] NOT NULL,
        [LogText] [nvarchar](50) NOT NULL,
        PRIMARY KEY CLUSTERED 
    (
        [DeviceId] ASC,
        [LogDate] ASC,
        [LogType] ASC
    ))
    

    测试并衡量为TYPE 指定PRIMARY KEY 是否会使整个过程更快或更慢。

    带有 TVP 的存储过程

    CREATE PROCEDURE [dbo].[MergeMyTable]
        @ParamRows dbo.MyTableType READONLY
    AS
    BEGIN
        -- SET NOCOUNT ON added to prevent extra result sets from
        -- interfering with SELECT statements.
        SET NOCOUNT ON;
    
        BEGIN TRANSACTION;
        BEGIN TRY
    
            MERGE INTO dbo.MyTable as Dest
            USING
            (
                SELECT
                    TT.[DeviceId],
                    TT.[LogDate],
                    TT.[LogType],
                    TT.[LogText]
                FROM
                    @ParamRows AS TT
            ) AS Src
            ON 
                (Dest.[DeviceId] = Src.[DeviceId]) AND
                (Dest.[LogDate]  = Src.[LogDate]) AND
                (Dest.[LogType]  = Src.[LogType])
            WHEN MATCHED THEN 
            UPDATE SET 
                Dest.[LogText] = Src.[LogText]
            WHEN NOT MATCHED BY TARGET THEN 
            INSERT
                ([DeviceId]
                ,[LogDate]
                ,[LogType]
                ,[LogText])
            VALUES
                (Src.[DeviceId],
                Src.[LogDate],
                Src.[LogType],
                Src.[LogText]);
    
            COMMIT TRANSACTION;
        END TRY
        BEGIN CATCH
            ROLLBACK TRANSACTION;
        END CATCH;
    
    END
    

    调用这个存储过程,传递给它一批要合并的行。测试和测量性能如何随批次大小而变化。尝试 1K、10K、100K 行的批处理。

    如果您不想用新值更新现有行,请删除MERGEWHEN MATCHED THEN 部分,它会更快。

    【讨论】:

    • 我对这种方法与 SqlBulkCopy 的性能非常感兴趣。也适用于较少的行,作为应用程序多行插入/更新的可能标准方式。尤其是在可能有触发器的表上。
    • 对于 C# 的使用,我是否更正了我将批量复制到用户定义的表中,然后使用上述过程将结果合并到普通表中?
    • @Thomas,没有。这里没有批量复制。请参阅 MSDN 中的示例如何使用 TVP 调用此类存储过程。此链接在答案中:msdn.microsoft.com/en-us/library/bb675163(v=vs.110).aspx
    • 啊,所以我可以使用普通的 sqlcommand 来执行该过程并从数据表中一次性填充整个内容。不知道(一直认为 sqlbulkcopy 是唯一的方法)。 tnx
    • TVP 数据使用与 SqlBulkCopy 相同的内部插入批量技术传递到 SQL Server。不同之处在于目标是一个由 SQL Server 管理的内部临时表,它公开 TVP 数据。此外,将 TVP 作为IEnumerable&lt;SqlDataRecord&gt; 传递可以流式传输 TVP 数据,而无需一次将整个源数据集加载到内存中。
    【解决方案3】:

    您可以在IGNORE_DUP_KEY 设置为ON 的情况下删除并重新创建索引。像这样的:

    ALTER TABLE datatable
    ADD CONSTRAINT PK_datatable 
    PRIMARY KEY CLUSTERED (DeviceId,LogDate,LogType,LogText) 
    WITH (IGNORE_DUP_KEY = ON)
    

    此选项的作用是在尝试对索引进行任何重复插入时报告具有不同严重性和消息的重复键错误。它不允许输入重复项,但会继续插入所有不重复的记录,并且仅在发现并忽略重复项时发出警告消息。

    更多信息请访问此链接:Creating Unique Indexes

    【讨论】:

    • +1。我完全忘记了这个选项。使用此选项,简单的批量插入可能比MERGE 工作得更快。服务器仍然必须做与MERGE 几乎相同的事情,但这些内部操作可能会更加优化。值得尝试和衡量时间。
    • 另外,您可以消除对临时表的需要...您可以直接批量复制到目标表中。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-09
    • 1970-01-01
    • 2015-11-30
    • 2018-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多