我有下面的代码在我的while循环,它是显着缓慢,有关于如何改进这一点的建议吗?
open IN, "<$FileDir/$file" || Err( "Failed to open $file at location: $FileDir" );
my $linenum = 0;
while ( $line = <IN> ) {
if ( $linenum == 0 ) {
Log(" This is header line : $line");
$linenum++;
} else {
$linenum++;
my $csv = Text::CSV_XS->new();
my $status = $csv->parse($line);
my @val = $csv->fields();
$index = 0;
Log("number of parameters for this file is: $sth->{NUM_OF_PARAMS}");
for ( $index = 0; $index <= $#val; $index++ ) {
if ( $index < $sth->{NUM_OF_PARAMS} ) {
$sth->bind_param( $index + 1, $val[$index] );
}
}
if ( $sth->execute() ) {
$ifa_dbh->commit();
} else {
Log("line $linenum insert failed");
$ifa_dbh->rollback();
exit(1);
}
}
}
到目前为止,最昂贵的操作是访问数据库服务器;这是一次网络旅行,每次需要数百毫秒左右的时间。
这些DB操作是插入的吗?如果是这样,则为具有多行的insert
语句构造一个字符串,而不是逐行插入,原则上该循环中有多少行就有多少行。然后运行那个事务。
测试并根据需要缩小,如果加起来有太多行。†
一些更容易看到的效率低下
-
不要在每次循环中都构造一个对象。在循环之前构建它一次,然后在循环中根据需要使用/重新填充。那么,这里就不需要
parse
+fields
了,而getline
也快了一点 -
不需要每次读取都使用
if
语句。首先读取一行数据,这就是标题。然后进入循环,没有if
s
总的来说,没有现在可能不需要的占位符,像
my $csv = Text::CSV_XS->new({ binary => 1, auto_diag => 1 });
# There's a $table earlier, with its @fields to populate
my $qry = "INSERT into $table (", join(',', @fields), ") VALUES ";
open my $IN, '<', "$FileDir/$file"
or Err( "Failed to open $file at location: $FileDir" );
my $header_arrayref = $csv->getline($IN);
Log( "This is header line : @$header_arrayref" );
my @sql_values;
while ( my $row = $csv->getline($IN) ) {
# Use as many elements in the row (@$row) as there are @fields
push @sql_values, '(' .
join(',', map { $dbh->quote($_) } @$row[0..$#fields]) . ')';
# May want to do more to sanitize input further
}
$qry .= join ', ', @sql_values;
# Now $qry is readye. It is
# INSERT into table_name (f1,f2,...) VALUES (v11,v12...), (v21,v22...),...
$dbh->do($qry) or die $DBI::errstr;
我还纠正了打开文件时的错误处理,因为问题中的||
在这种情况下绑定得太紧,并且有效地存在open IN, ( "<$FileDir/$file" || Err(...) )
。我们需要or
而不是||
。那么,三个参数的open
更好。看到perlopentut
如果您确实需要占位符,可能是因为您不能进行单个插入,但必须将其分解为多个,或者出于安全原因,那么您需要为要插入的每一行生成精确的?
-元组,然后为它们提供正确数量的值。
可以先组装数据,然后在此基础上构建?
元组
my $qry = "INSERT into $table (", join(',', @fields), ") VALUES ";
...
my @data;
while ( my $row = $csv->getline($IN) ) {
push @data, [ @$row[0..$#fields] ];
}
# Append the right number of (?,?...),... with the right number of ? in each
$qry .= join ', ', map { '(' . join(',', ('?')x@$_) . ')' } @data;
# Now $qry is ready to bind and execute
# INSERT into table_name (f1,f2,...) VALUES (?,?,...), (?,?,...), ...
$dbh->do($qry, undef, map { @$_ } @data) or die $DBI::errstr;
这可能会生成一个非常大的字符串,这可能会突破RDBMS或其他资源的限制。在这种情况下,将@data
分成更小的批次。‡
prepare
语句与(?,?,...)
的行值的正确数量的批,execute
在批次的循环。最后,另一种方法是使用数据库的工具直接从文件中加载数据。这将比通过DBI
快得多,甚至可能包括将输入的CSV处理成只包含所需数据的另一个CSV的需要。
由于您不需要输入CSV文件中的所有数据,因此首先像上面那样读取和处理该文件,然后只写入包含所需数据的文件(上面的@data
)。那么,有两种可能的方法
在PostgreSQL中使用
COPY
,在MySQL和Oracle(等)中使用LOAD DATA [LOCAL] INFILE
;或者,使用专用工具从RDBMS -
mysqlimport
(MySQL),SQL*Loader
/sqlldr
(Oracle)等导入/加载文件。我希望这是最快的方法
第二个选项也可以在程序之外完成,通过system
作为外部命令运行适当的工具(或者通过合适的库更好)。
†在一个应用程序中,我在初始insert
中放入了多达数百万行——该语句的字符串本身有几十MB——并且每天在单个语句中插入约100k行,到目前为止已经运行了几年。这是好的服务器上的postgresql
,当然还有ymmv.
‡一些RDBMS不支持像这里这样的多行(批处理)插入查询;尤其是甲骨文似乎没有。(我们最后被告知,这就是这里使用的数据库。)但在Oracle中还有其他方法可以做到这一点,请查看评论中的链接,并搜索更多。然后脚本将需要构造一个不同的查询,但操作原理是相同的。