Как я могу вставить 10 миллионов записей в кратчайшие сроки?

У меня есть файл (в котором 10 миллионов записей), как показано ниже:

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

Итак, в основном я хочу вставить в базу данных 10 миллионов записей. поэтому я прочитал файл и загрузил его на 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();

но прошивка займет много времени. Как я могу вставить 10 миллионов записей в кратчайшие сроки с помощью C #?

Обновление 1:
Массовая ВСТАВКА:

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

     ROWTERMINATOR =' \n'
  );

Моя таблица выглядит как показано ниже:

DATAs
(
     DatasField VARCHAR(MAX)
)

но я получаю следующую ошибку:

Msg 4866, уровень 16, состояние 1, строка 1
Ошибка массовой загрузки. Столбец в файле данных слишком длинный для строки 1, столбца 1. Убедитесь, что терминатор поля и терминатор строки указаны правильно.

Msg 7399, уровень 16, состояние 1, строка 1
Поставщик OLE DB «BULK» для связанного сервера «(null)» сообщил об ошибке. Провайдер не предоставил никакой информации об ошибке.

Msg 7330, уровень 16, состояние 2, строка 1
Невозможно получить строку от поставщика OLE DB "BULK" для связанного сервера "(null)".

Ниже код работал:

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

person MD TAHMID HOSSAIN    schedule 10.09.2014    source источник
comment
Не можете ли вы импортировать это напрямую с помощью инструментов импорта SQL-сервера? Зачем использовать для этого C #?   -  person gjvdkamp    schedule 10.09.2014
comment
Э-э, взгляните на BULK INSERT, BCP, SqlBulkCopy и т. Д. Абсолютно наихудший способ сделать это - вставить 10 миллионов строк по одной в цикл.   -  person Aaron Bertrand    schedule 10.09.2014
comment
Вы пробовали BULK INSERT - msdn.microsoft.com/en-gb/library/ms188365 .aspx?   -  person Kami    schedule 10.09.2014
comment
использовать XML, я могу сделать это без включения BULK INSERT   -  person MethodMan    schedule 10.09.2014
comment
@AaronBertrand хуже, чем по одному из графического интерфейса? :)   -  person Daniel E.    schedule 10.09.2014
comment
Создайте пакет SSIS или класс SqlBulkCopy из пространства имен system.data.sqlclient.   -  person kidshaw    schedule 10.09.2014
comment
У вас нет разделителя на строку? Неужели одна очень длинная строка вставляется только в один столбец? Если да, то сделайте свою колонку, возможно, varchar(max). Какова ваша структура таблицы, в которую вы хотите вставить строки, и как они выглядят?   -  person Will Marcouiller    schedule 10.09.2014
comment
@WillMarcouiller разделитель на строку - это новая строка. моя таблица выглядит как таблица (data varchar (max)). line1 выглядит как что-то line2 выглядит как something2   -  person MD TAHMID HOSSAIN    schedule 10.09.2014
comment
@MDTAHMIDHOSSAIN Имейте в виду, что могут быть другие факторы, которые повлияют на скорость чтения / записи данных, например, если это старый SATA2 HD, и другие вещи.   -  person Prix    schedule 10.09.2014


Ответы (4)


Пожалуйста, не создавайте DataTable для загрузки с помощью BulkCopy. Это подходящее решение для небольших наборов данных, но нет абсолютно никаких причин загружать все 10 миллионов строк в память перед вызовом базы данных.

Лучше всего (за пределами BCP / BULK INSERT / OPENROWSET(BULK...)) передавать содержимое из файла в базу данных через табличный параметр (TVP). Используя TVP, вы можете открыть файл, прочитать строку и отправить строку до завершения, а затем закрыть файл. Этот метод занимает всего одну строку. Я написал статью Потоковая передача данных в SQL Server 2008 из приложения, в которой есть пример этого самого сценария.

Ниже приводится упрощенный обзор структуры. Я предполагаю ту же таблицу импорта и имя поля, как показано в вопросе выше.

Обязательные объекты базы данных:

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

Код приложения C # для использования вышеуказанных объектов SQL приведен ниже. Обратите внимание, как вместо заполнения объекта (например, DataTable) и последующего выполнения хранимой процедуры в этом методе именно выполнение хранимой процедуры инициирует чтение содержимого файла. Входной параметр хранимой процедуры не является переменной; это возвращаемое значение метода GetFileContents. Этот метод вызывается, когда SqlCommand вызывает ExecuteNonQuery, который открывает файл, считывает строку и отправляет строку в SQL Server через конструкции IEnumerable<SqlDataRecord> и yield return, а затем закрывает файл. Хранимая процедура просто видит табличную переменную @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 # можно адаптировать для отправки в нескольких полях (пример, показанный в статье «Обработка данных ...», ссылка на которую приведена выше, проходит в 2-х полях).
  3. Вы также можете управлять значением каждой записи в операторе SELECT в файле proc.
  4. Вы также можете отфильтровать строки, используя условие WHERE в proc.
  5. Вы можете получить доступ к переменной таблицы TVP несколько раз; это ТОЛЬКО ЧТЕНИЕ, но не "только вперед".
  6. Advantages over SqlBulkCopy:
    1. SqlBulkCopy is INSERT-only whereas using a TVP allows the data to be used in any fashion: you can call MERGE; you can DELETE based on some condition; you can split the data into multiple tables; and so on.
    2. Поскольку TVP не поддерживает только INSERT, вам не нужна отдельная промежуточная таблица для выгрузки данных.
    3. Вы можете получить данные из базы данных, позвонив ExecuteReader вместо ExecuteNonQuery. Например, если в таблице импорта DATAs было поле IDENTITY, вы могли бы добавить предложение OUTPUT в INSERT для передачи обратно INSERTED.[ID] (при условии, что ID - это имя поля IDENTITY). Или вы можете передать результаты совершенно другого запроса, или и то, и другое, поскольку несколько наборов результатов могут быть отправлены и доступны через Reader.NextResult(). Получение информации из базы данных невозможно при использовании SqlBulkCopy, однако здесь есть несколько вопросов, касающихся S.O. людей, желающих сделать именно это (по крайней мере, в отношении недавно созданных IDENTITY значений).
    4. Для получения дополнительной информации о том, почему иногда это происходит быстрее для всего процесса, даже если получение данных с диска в SQL Server немного медленнее, см. Этот технический документ от группы консультирования клиентов SQL Server: Увеличение пропускной способности с помощью TVP
person Solomon Rutzky    schedule 10.09.2014
comment
Взгляните на мое предложение, оно более элегантное - person playful; 11.09.2014
comment
@CesarBoucas: ваше предложение определенно интересно и более эффективно, чем предварительная загрузка DataTable. Он действительно использует существующий SqlBulkCopy, что может быть приятным, но также может быть ограничивающим. Хотя ваш метод допускает преобразование / проверку в классе IDataReader, он не допускает никакого контроля в базе данных и не позволяет возвращать данные в вызывающий процесс (я просто добавил примечание об этом внизу). - person Solomon Rutzky; 11.09.2014
comment
@srutzky Попробуем сосредоточиться на объеме вопроса. Речь идет только о загрузке данных из файла в таблицу, в нем не упоминается преобразование / проверка / возврат данных и так далее. Будь проще ;) - person playful; 11.09.2014
comment
@CesarBoucas: Мое предложение и пример кода сосредоточены на поставленном вопросе. Я только указал в качестве дополнительных примечаний, что есть много дополнительных преимуществ и возможностей. Я не говорю, что ваше решение неуместно, просто оно едва ли более элегантно и что нужно от многого отказаться, чтобы получить это предельное увеличение элегантности, если оно вообще есть. Хотя, опять же, есть случаи (например, невозможность добавить объекты БД и / или необходимость замены процесса, который уже выгружается в таблицу импорта), когда ваше предложение является идеальным. - person Solomon Rutzky; 11.09.2014
comment
Спасибо @SolomonRutzky. Я также использовал ваш подход и несколько раз использовал полностью модные TVP-материалы. Эта работа идеально подходит для меня. - person cat_minhv0; 09.08.2018
comment
@SolomonRutzky Извините за очень поздний комментарий, но как ваш подход может быть реализован, если я использую Dapper? Я имею в виду, например, как передать IEnumerable ‹SqlDataRecord› в метод Dapper Execute? Спасибо. - person geeko; 21.04.2020
comment
@SolomonRutzky Кроме того, можно ли использовать ваш подход без хранимых процедур (т.е. напрямую передавать TVP на обычные запросы)? - person geeko; 21.04.2020
comment
Привет @geeko. Не уверен насчет Dapper, так как я им не пользуюсь. Но да, вы, безусловно, должны иметь возможность использовать TVP с обычным параметризованным SQL. В этом сценарии имя параметра должно иметь префикс @, и вам необходимо заполнить свойство SqlParameter.TypeName (при вызове хранимой процедуры это не требуется). - person Solomon Rutzky; 21.04.2020
comment
@SolomonRutzky Спасибо за ответ. Просто для подтверждения: использование TVP с обычными параметризованными запросами также приведет к потоковой передаче TVP, и запросы начнут выполняться и будут видеть TVP, как только будет передана первая запись? - person geeko; 21.04.2020
comment
@geeko Прошло много времени с тех пор, как я проводил тестирование, и сейчас у меня нет времени, но это действительно должно быть так же. Итак, в какой бы степени ни выполнялась процедура во время загрузки данных, то же самое должно быть верным для параметризованного SQL. Но технически я не уверен, выполняется ли запрос или процедура до передачи данных. - person Solomon Rutzky; 21.04.2020
comment
Можете ли вы уточнить с некоторыми изменениями приведенный выше код C # можно адаптировать для пакетной обработки данных? Разве потоковая передача TVP уже не пакетируется при отправке на сервер? Или вы имели в виду чтение партиями из входного файла? - person muusbolla; 16.03.2021
comment
@muusbolla Нет, TVP ничего не пакетирует; Я действительно имел в виду чтение из файла партиями. Код в моем ответе открывает файл, выгружает все его в сохраненную процедуру за один раз, а затем закрывает файл. Я давно не смотрел на это, но мне кажется, что я имел в виду шаблон, в котором вы: открываете файл вне GetFileContents(), выполняете процедуру в цикле (передавая fileHandle в GetFileContents(fileHandle, batchSize)) до File.EOF, затем закройте файл. Это позволяет совершать меньшие транзакции. Я постараюсь в ближайшее время опубликовать пример в своем блоге и ссылку на него здесь. - person Solomon Rutzky; 26.03.2021

В C # лучшее решение - позволить SqlBulkCopy читать файл. Для этого вам нужно передать IDataReader прямо SqlBulkCopy.WriteToServer методу. Вот пример: http://www.codeproject.com/Articles/228332/IDataReader-implementation-plus-SqlBulkCopy

person playful    schedule 11.09.2014
comment
Это определенно хорошее решение. Но лучшее - это то, чего кто-то пытается достичь. Использование SqlBulkCopy ограничивает то, что можно сделать, поскольку оно позволяет только вставлять данные в таблицу. С другой стороны, TVP позволяет делать с данными все, что угодно, до того, как они будут вставлены, после того, как они будут вставлены, объединены вместо вставки, дополнительные операции, которые необходимо выполнить (например, сначала усечь целевую таблицу) и т. Д. Также, как я только что добавленный в конец раздела дополнительных примечаний, TVP позволяет передавать результаты обратно в приложение, тогда как через SqlBulkCopy это невозможно. - person Solomon Rutzky; 11.09.2014
comment
@srutzky Предполагая, что возник вопрос, как вставить 10 миллионов записей в кратчайшие сроки [C #] ?. Представленное мной решение - это решение, которое лучше всего соответствует поставленному вопросу, другими словами, это лучшее решение для предложенного вопроса. Потому что, как вы сказали: SqlBulkCopy позволяет вставлять данные только в таблицу - и это как раз вопрос. - person playful; 11.09.2014
comment
Я понимаю, что вы говорите, но лучшее еще предстоит определить ОП. Возможно, что объем вопроса указан неверно из-за того, что ОП не знает, что то, что я описываю, возможно. Я много раз сталкивался с этим, когда кто-то формулировал вопрос слишком узко, но не знал об этом в то время. Загрузка данных самым быстрым способом может на самом деле быть более медленным общим решением в зависимости от того, что делается после того, как данные есть. Кроме того, то, что я предложил, в лучшем случае лишь немного сложнее, чем то, что вы предлагаете, но при этом обладает большой гибкостью. - person Solomon Rutzky; 11.09.2014

лучший способ - это сочетание вашего 1-го решения и 2-го, создать DataTable и в цикле добавить к нему строки, а затем использовать BulkCopy для загрузки в БД в одном соединении используйте это для помощи при массовом копировании

еще одна вещь, на которую следует обратить внимание, массовое копирование - очень чувствительная операция, почти каждая ошибка приведет к аннулированию копии, например, если вы объявите имя столбца в таблице данных как «текст», а в БД его «Текст», это вызовет исключение. , удачи.

person Liran    schedule 10.09.2014
comment
К вашему сведению, это решение подходит для гораздо небольших наборов данных. Проблема заключается в высокой стоимости памяти при увеличении объема данных. В случае 10 миллионов строк, если предположить, что 10 символов в строке - это 100 МБ данных в памяти, верно? Ну, это зависит от того, как данные хранятся в памяти. Этот случай, по-видимому, представляет собой только строки, состоящие из 2 байтов на символ, поскольку строки .Net - это UCS-2. Следовательно, файл с 10 миллионами строк занимает 200 МБ памяти в DataTable, прежде чем первая строка может быть отправлена ​​в базу данных. - person Solomon Rutzky; 11.09.2014

Если вы хотите вставить 10 миллионов записей в кратчайшие сроки для прямого использования SQL-запроса в целях тестирования, вы должны использовать этот запрос.

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

 INSERT INTO TestData(CreatedDate) SELECT GetDate()
 GO 10000000
person Rakesh Prajapati    schedule 19.06.2018