Могу ли я улучшить производительность этого запроса CSV-импорта? - PullRequest
2 голосов
/ 31 марта 2020

В настоящее время я пытаюсь импортировать CSV-файл размером 20 ГБ (примерно 64 миллиона строк, 58 столбцов) в базу данных ms sql.

Сначала я попытался сделать это с помощью SSIS, но это было так медленно Я решил попробовать вместо этого использовать Powershell и нашел хороший запрос здесь:

Высокопроизводительный импорт CSV

Запрос очень быстрый, мне удается вставить примерно 1 миллион строк в минуту. Однако , мне нужно иметь возможность обрабатывать разделители, заключенные в кавычки, например: Столбец1, «Автомобиль, самолет, лодка», Столбец3

Я сделал это с помощью регулярных выражений по рекомендации авторов: переключение:

 $null = $datatable.Rows.Add($line.Split($csvdelimiter))

на:

 $null = $datatable.Rows.Add($([regex]::Split($line, $csvSplit, $regexOptions)))

Полный запрос:

# Database variables
$sqlserver = "server"
$database = "database"
$table = "tablename"

# CSV variables
$csvfile = "filepath"
$csvdelimiter = ","
$firstRowColumnNames = $true
$fieldsEnclosedInQuotes = $true

# Handling of regex for comma problem
if ($fieldsEnclosedInQuotes) {
    $csvSplit = "($csvdelimiter)"
    $csvsplit += '(?=(?:[^"]|"[^"]*")*$)'
} else { $csvsplit = $csvdelimiter }



$regexOptions = [System.Text.RegularExpressions.RegexOptions]::ExplicitCapture


################### No need to modify anything below ###################
Write-Host "Script started..."
$elapsed = [System.Diagnostics.Stopwatch]::StartNew() 
[void][Reflection.Assembly]::LoadWithPartialName("System.Data")
[void][Reflection.Assembly]::LoadWithPartialName("System.Data.SqlClient")

# 50k worked fastest and kept memory usage to a minimum
$batchsize = 50000

# Build the sqlbulkcopy connection, and set the timeout to infinite
$connectionstring = "Data Source=$sqlserver;Integrated Security=true;Initial Catalog=$database;"
$bulkcopy = New-Object Data.SqlClient.SqlBulkCopy($connectionstring, [System.Data.SqlClient.SqlBulkCopyOptions]::TableLock)
$bulkcopy.DestinationTableName = $table
$bulkcopy.bulkcopyTimeout = 0
$bulkcopy.batchsize = $batchsize

# Create the datatable, and autogenerate the columns.
$datatable = New-Object System.Data.DataTable

# Open the text file from disk
$reader = New-Object System.IO.StreamReader($csvfile)
$firstline = (Get-Content $csvfile -First 1)
$columns = [regex]::Split($firstline, $csvSplit, $regexOptions)


if ($firstRowColumnNames -eq $true) { $null = $reader.readLine() }

foreach ($column in $columns) { 

    $null = $datatable.Columns.Add()
}

# Read in the data, line by line
while (($line = $reader.ReadLine()) -ne $null)  {
    $null = $datatable.Rows.Add($([regex]::Split($line, $csvSplit, $regexOptions)))
    $i++; if (($i % $batchsize) -eq 0) { 
        $bulkcopy.WriteToServer($datatable) 
        Write-Host "$i rows have been inserted in $($elapsed.Elapsed.ToString())."
        $datatable.Clear() 
    } 
} 

 # add in all the remaining rows since the last clear
 if($datatable.rows.count -gt 0) {
 $bulkcopy.writetoserver($datatable)
 $datatable.clear()
 }

# Clean Up
$reader.Close(); $reader.Dispose()
$bulkcopy.Close(); $bulkcopy.Dispose()
$datatable.Dispose()

Write-Host "Script complete. $i rows have been inserted into the database."
Write-Host "Total Elapsed Time: $($elapsed.Elapsed.ToString())"
# Sometimes the Garbage Collector takes too long to clear the huge datatable.
 [System.GC]::Collect()
pause

Это занимает гораздо больше времени с регулярное выражение:

24 секунды на 50 000 тыс. строк (с обработкой разделителей, встроенных в квоты)

2 секунды на 50 000 тыс. строк (без обработка)

Я что-то не так делаю? Является ли регулярное выражение правильным путем к этому? Могу ли я улучшить производительность запросов каким-либо образом или эта производительность потеряла то, что я должен принять?

Обновление: добавлен полностью запрос

Ответы [ 2 ]

2 голосов
/ 31 марта 2020

Для больших CSV я бы использовал Microsoft.VisualBasi c .FileIO.TextFieldParser . Весь синтаксический анализ (расширенный, см. Пример) выполняется там эффективно.

Не беспокойтесь о «VisualBasi c», это часть. NET. Сборка должна быть добавлена ​​явно и все.

Вот пример с некоторыми комментариями

    # temp data
    Set-Content z.csv @'
column1,column2,column3
"data,
""1a""",data2a,data3a


data1b,  data2b  ,data3b
'@

    Add-Type -AssemblyName Microsoft.VisualBasic

    $reader = New-Object Microsoft.VisualBasic.FileIO.TextFieldParser $PSScriptRoot\z.csv #! full path
    $reader.SetDelimiters(',') # default is none
    $reader.TrimWhiteSpace = $false # default is true

    while(!$reader.EndOfData) {
        $reader.LineNumber #! it counts not empty lines
        $reader.ReadFields() | %{ "data: '$_'" }
    }
    $reader.Close()

    Remove-Item z.csv
0 голосов
/ 16 апреля 2020

Я недавно реализовал CSV-парсер для. NET, который, как я утверждаю, (в настоящее время) самый быстрый , доступный в виде пакета nuget: Sylvan .Data.Csv .

Это очень легко можно использовать в сочетании с SqlBulkImport:

using var csvText = GetData(); // Gets a TextReader over a large-ish CSV dataset

DbDataReader dataReader = CsvDataReader.Create(csvText);

var csb = 
    new SqlConnectionStringBuilder { 
        DataSource = @"(LocalDb)\MSSqlLocalDb", 
        InitialCatalog = "Test", 
        IntegratedSecurity = true 
    };
using var conn = new SqlConnection(csb.ConnectionString);
conn.Open();
// HACK: (see notes)
dataReader = new EmptyAsNullDataReader(dataReader);

var bcp = new SqlBulkCopy(conn);
bcp.BulkCopyTimeout = 0;
bcp.DestinationTableName = "Feature";
bcp.BatchSize = 50000;
bcp.WriteToServer(dataReader);

Этот код смог импортировать 2,2 миллиона записей данных через ~ 12 секунд на моем компьютере.

Относительно "HACK": я принял решение, что схема по умолчанию для столбцов CsvDataReader будет строкой, не допускающей значения NULL. Это означает, что пустые поля будут читаться как пустая строка "" вместо null. Это спорно ли это был правильный выбор. К сожалению, SqlBulkCopy не позволяет импортировать (принудительно) пустые строки в нестроковые (int, float, date) столбцы. Есть два способа справиться с этим.

Первый, который немного хакерский, - это использовать адаптер , который превратит пустые строки в ноль.

Второй вариант - предоставить CsvDataReader явную схему построения. Предполагая, что к целевой таблице в SQL Server применена правильная схема, это также можно сделать относительно легко:

// a schema provider that uses an existing schema.
sealed class Schema : ICsvSchemaProvider
{
    readonly ReadOnlyCollection<DbColumn> schema;
    public Schema(ReadOnlyCollection<DbColumn> schema)
    {
        this.schema = schema;
    }
        public DbColumn GetColumn(string name, int ordinal)
    {
        return schema[ordinal];
    }
}

SqlConnection conn = ...;
var cmd = conn.CreateCommand();

// select an empty record set to get the table schema.
cmd.CommandText = "select top 0 * from [MyTargetTable]";
var reader = cmd.ExecuteReader();
var tableSchema = reader.GetColumnSchema();
reader.Close();
var csvSchema = new Schema(tableSchema);

var options = new CsvDataReaderOptions { Schema = csvSchema };
// the dataReader will now expose a strongly-typed schema which SqlBulkImport
// will be able to properly consume.
var dataReader = CsvDataReader.Create(csvText, options);


Полный образец можно увидеть в исходном репозитории . Который использует это sql определение таблицы в качестве места назначения импорта.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...