Иногда наступает такая ситуация, когда откуда-нибудь приходит огромаднейший CSV-файл, в котором несколько миллионов (это не преувеличение) строк, и надо его содержимое внести в некоторую таблицу нашей БД.

ОС:Windows 7 SP1 x64
Язык:C# / .NET 4
IDE:Visual Studio
БД:Microsoft SQL Server

Такой файл даже ничем не открыть (кроме Excel, который, впрочем, сможет показать только первый миллион записей).

Но и ладно, нам открывать его не надо, а чтение и вставку записей в БД будет делать функция, которую мы напишем.

Сразу суть: есть специальный класс по массовой вставке записей в таблицу БД - SqlBulkCopy. Но сложность в том, что если прочитать сразу всё содержимое CSV-файла (который может достигать размера в несколько гигабайт) и попытаться это вставить в БД, то очень легко оперативки не хватит и всё записнет.

Выходом будет использовать некоторый буфер:

  1. В буфер считывается порция CSV-файла;
  2. Прочитанное разбирается и вставляется в БД;
  3. Буфер очищается и всё по новой уже со следующей порцией.

Допустим, наша таблица назначения состоит из трёх полей (колонок):

  1. Первичный ключ, он нас не интересует, генерируется автоинкрементом;
  2. Поле first;
  3. Поле second.

И допустим, файл CSV имеет такой вид:

whore,howmuch
Arlington,300
Fox,900
Goodwin,400
Lawrence,800
...

Первая строка - “шапка таблицы”. Как видим, названия колонок отличаются от наших, потому автобиндинг использовать не будем - далее в коде он закомментирован, а имена колонок прописываются руками.

Вот моё решение.

/// <summary>
/// Чтение CSV-файла и сохранение записей в таблицу БД
/// </summary>
/// <returns>
/// 0 - всё прошло без ошибок
/// 1 - не удалось сохранить записи в БД
/// 2 - ещё что-то не удалось
/// </returns>
int readCSVandSave2DB() // в параметры можно вынести размер буфера и путь до файла
{
    // задаём размер буфера, пусть будет 9000 (строк CSV-файла)
    int csvBufferSize = 9000;
    // указываем путь до CSV-файла
    string path2file = @"C:\temp\hugefile.csv";
    // сюда он будет читаться
    DataTable csvData = new DataTable();
    try // пробуем читать
    {
        // для TextFieldParser надо подключить библиотеку Microsoft.VisualBasic
        // но он вовсе не обязателен, CSV в DataTable можете разбирать как хотите
        using (TextFieldParser csvReader = new TextFieldParser(path2file))
        {
            csvReader.SetDelimiters(new string[] { "," });
            csvReader.HasFieldsEnclosedInQuotes = true;
            string[] colFields = csvReader.ReadFields();
            //foreach (string column in colFields) // автобиндинг
            //{
                // названия колонок в таблице
                DataColumn datecolumnSer = new DataColumn("first");
                datecolumnSer.AllowDBNull = true;
                csvData.Columns.Add(datecolumnSer);
                DataColumn datecolumnNum = new DataColumn("second");
                datecolumnNum.AllowDBNull = true;
                csvData.Columns.Add(datecolumnNum);
            //}
            int buffer_wannabe = 0;
            while (!csvReader.EndOfData)
            {
                string[] fieldData = csvReader.ReadFields();
                csvData.Rows.Add(fieldData);

                buffer_wannabe++;
                // начитали 9000 строк, заносим их в базу и сбрасываем буфер на 0
                if (buffer_wannabe == csvBufferSize)
                {
                    buffer_wannabe = 0;

                    // функция вставки записей описана ниже
                    if (!insertCurrentBunchOfRecs(csvData)) return 1;

                    csvData.Rows.Clear();
                }
            }
            // в последнем чтении в буфере меньше 9000 записей, их тоже надо занести
            if (buffer_wannabe != 0)
            {
                if (!insertCurrentBunchOfRecs(csvData)) return 1;
                csvData.Rows.Clear();
            }
        }
    }
    catch { return 2; }
    
    // всё огонь
    return 0;
}

/// <summary>
/// Сохранение новых записей во временную таблицу в БД
/// </summary>
/// <param name="csvData">строки из CSV</param>
/// <returns></returns>
static private bool insertCurrentBunchOfRecs(DataTable csvData)
{
    try
    {
        using (SqlConnection dbConnection = new SqlConnection("ПОДКЛЮЧЕНИЕКВАШЕЙБД"))
        {
            dbConnection.Open();
            // вся соль вот в этом классе - SqlBulkCopy - он делает всю магию
            using (SqlBulkCopy s = new SqlBulkCopy(dbConnection))
            {
                // если таблица в какой-то схеме, то указать это
                s.DestinationTableName = "ИМЯСХЕМЫ.ИМЯТАБЛИЦЫ";
                foreach (var column in csvData.Columns)
                {
                    s.ColumnMappings.Add(column.ToString(), column.ToString());
                }
                s.WriteToServer(csvData);
            }
        }
    }
    catch { return false; }
    
    return true;
}

Некоторые фрагменты кода позаимствовал где-то в интернетах, как обычно.

Через три месяца статья получила некоторое продолжение в виде полноценного приложения для разрезания исходного огромного CSV файла на более мелкие, с которыми можно работать не имея гигабайтов оперативки.