如何在最短的时间内插入1000万条记录?

时间:2014-09-10 16:08:43

标签: c# sql-server import bulkinsert table-valued-parameters

我有一个文件(有1000万条记录),如下所示:

    line1
    line2
    line3
    line4
   .......
    ......
    10 million lines

所以基本上我想在数据库中插入1000万条记录。 所以我读了文件并将其上传到SQL Server。

C#代码

System.IO.StreamReader file = 
    new System.IO.StreamReader(@"c:\test.txt");
while((line = file.ReadLine()) != null)
{
    // insertion code goes here
    //DAL.ExecuteSql("insert into table1 values("+line+")");
}

file.Close();

但插入需要很长时间。 如何使用C#在最短的时间内插入1000万条记录?

更新1:
批量插入:

BULK INSERT DBNAME.dbo.DATAs
FROM 'F:\dt10000000\dt10000000.txt'
WITH
(

     ROWTERMINATOR =' \n'
  );

我的表格如下:

DATAs
(
     DatasField VARCHAR(MAX)
)

但我收到以下错误:

  

Msg 4866,Level 16,State 1,Line 1
  批量加载失败。第1行第1列的数据文件中的列太长。验证是否正确指定了字段终止符和行终止符。

     

Msg 7399,Level 16,State 1,Line 1
  OLE DB提供程序" BULK"对于链接服务器"(null)"报告错误。提供商没有提供有关错误的任何信息。

     

Msg 7330,Level 16,State 2,Line 1
  无法从OLE DB提供程序中获取行" BULK"对于链接服务器"(null)"。

以下代码有效:

BULK INSERT DBNAME.dbo.DATAs
FROM 'F:\dt10000000\dt10000000.txt'
WITH
(
    FIELDTERMINATOR = '\t',
    ROWTERMINATOR = '\n'
);

4 个答案:

答案 0 :(得分:41)

创建DataTable以通过BulkCopy加载。对于较小的数据集,这是一个很好的解决方案,但在调用数据库之前,绝对没有理由将所有1000万行加载到内存中。

您最好的选择(在BCP / BULK INSERT / OPENROWSET(BULK...)之外)是通过表值参数(TVP)将文件中的内容流式传输到数据库中。通过使用TVP,您可以打开文件,阅读一行&发送一行直到完成,然后关闭该文件。此方法的内存占用量仅为一行。我写了一篇文章Streaming Data Into SQL Server 2008 From an Application,其中有一个例子就是这个场景。

结构的简单概述如下。我假设相同的导入表和字段名称,如上面的问题所示。

必需的数据库对象:

-- First: You need a User-Defined Table Type
CREATE TYPE ImportStructure AS TABLE (Field VARCHAR(MAX));
GO

-- Second: Use the UDTT as an input param to an import proc.
--         Hence "Tabled-Valued Parameter" (TVP)
CREATE PROCEDURE dbo.ImportData (
   @ImportTable    dbo.ImportStructure READONLY
)
AS
SET NOCOUNT ON;

-- maybe clear out the table first?
TRUNCATE TABLE dbo.DATAs;

INSERT INTO dbo.DATAs (DatasField)
    SELECT  Field
    FROM    @ImportTable;

GO

使用上述SQL对象的C#app代码如下。请注意如何填充对象(例如DataTable)然后执行存储过程,而在此方法中,执行存储过程以启动文件内容的读取。存储过程的输入参数不是变量;它是方法GetFileContents的返回值。当SqlCommand调用ExecuteNonQuery(打开文件)读取一行并通过IEnumerable<SqlDataRecord>yield return构造将行发送到SQL Server时调用该方法,然后关闭文件。存储过程只看到一个表变量@ImportTable,一旦数据开始过来就可以访问(注意:数据确实在tempdb中持续很短的时间,即使不是全部内容)。

using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.IO;
using Microsoft.SqlServer.Server;

private static IEnumerable<SqlDataRecord> GetFileContents()
{
   SqlMetaData[] _TvpSchema = new SqlMetaData[] {
      new SqlMetaData("Field", SqlDbType.VarChar, SqlMetaData.Max)
   };
   SqlDataRecord _DataRecord = new SqlDataRecord(_TvpSchema);
   StreamReader _FileReader = null;

   try
   {
      _FileReader = new StreamReader("{filePath}");

      // read a row, send a row
      while (!_FileReader.EndOfStream)
      {
         // You shouldn't need to call "_DataRecord = new SqlDataRecord" as
         // SQL Server already received the row when "yield return" was called.
         // Unlike BCP and BULK INSERT, you have the option here to create a string
         // call ReadLine() into the string, do manipulation(s) / validation(s) on
         // the string, then pass that string into SetString() or discard if invalid.
         _DataRecord.SetString(0, _FileReader.ReadLine());
         yield return _DataRecord;
      }
   }
   finally
   {
      _FileReader.Close();
   }
}

上面的GetFileContents方法用作存储过程的输入参数值,如下所示:

public static void test()
{
   SqlConnection _Connection = new SqlConnection("{connection string}");
   SqlCommand _Command = new SqlCommand("ImportData", _Connection);
   _Command.CommandType = CommandType.StoredProcedure;

   SqlParameter _TVParam = new SqlParameter();
   _TVParam.ParameterName = "@ImportTable";
   _TVParam.TypeName = "dbo.ImportStructure";
   _TVParam.SqlDbType = SqlDbType.Structured;
   _TVParam.Value = GetFileContents(); // return value of the method is streamed data
   _Command.Parameters.Add(_TVParam);

   try
   {
      _Connection.Open();

      _Command.ExecuteNonQuery();
   }
   finally
   {
      _Connection.Close();
   }

   return;
}

附加说明:

  1. 通过一些修改,可以调整上述C#代码以批量处理数据。
  2. 经过微小修改后,上述C#代码可以适用于多个字段的发送(&#34; Steaming Data ...&#34;上面链接的文章在2个字段中传递)。
  3. 您还可以操作proc中的SELECT语句中每条记录的值。
  4. 您还可以使用过程中的WHERE条件过滤掉行。
  5. 您可以多次访问TVP表变量;它是READONLY但不是&#34;仅转发&#34;。
  6. 优于SqlBulkCopy的优势:
    1. SqlBulkCopy仅限INSERT,而使用TVP可以以任何方式使用数据:您可以调用MERGE;你可以根据某些条件DELETE;您可以将数据拆分为多个表;等等。
    2. 由于TVP不仅仅是INSERT,您不需要单独的临时表来将数据转储到。
    3. 您可以通过调用ExecuteReader而不是ExecuteNonQuery从数据库中恢复数据。例如,如果IDENTITY导入表中有DATAs字段,您可以向OUTPUT添加INSERT子句以传回INSERTED.[ID](假设IDIDENTITY字段的名称。或者您可以传回完全不同的查询结果,或者两者都可以,因为可以通过Reader.NextResult()发送和访问多个结果集。使用SqlBulkCopy时无法从数据库中获取信息,但S.O.上有几个问题。想要做到这一点的人(至少关于新创建的IDENTITY值)。
    4. 有关为什么整个过程有时更快的更多信息,即使从磁盘获取数据到SQL Server的速度稍慢,请参阅SQL Server客户咨询团队的白皮书:Maximizing Throughput with TVP

答案 1 :(得分:6)

在C#中,最好的解决方案是让SqlBulkCopy读取文件。为此,您需要将IDataReader直接传递给SqlBulkCopy.WriteToServer方法。以下是一个示例:http://www.codeproject.com/Articles/228332/IDataReader-implementation-plus-SqlBulkCopy

答案 2 :(得分:3)

最好的方法是在第一个解决方案和第二个解决方案之间进行混合, 创建DataTable并在循环中向其添加行,然后使用BulkCopy进行上传 通过一个连接use this for help in bulk copy

连接到数据库

另外一件事要注意,批量复制几乎是一种非常敏感的操作 每一个错误都会使副本无效,例如,如果你将dataTable中的列名称声明为“text”,而在数据库中将其“Text”声明为“Text”则会产生异常,祝你好运。

答案 3 :(得分:-1)

如果要在最短的时间内插入1000万条记录,直接使用SQL查询进行测试,则应使用此查询

 CREATE TABLE TestData(ID INT IDENTITY (1,1), CreatedDate DATETIME)
 GO

 INSERT INTO TestData(CreatedDate) SELECT GetDate()
 GO 10000000