Perl 性能缓慢,文件 I/O 问题或由于 while 循环

Perl performance is slow, file I/O issue or due to while loop

我的 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);
        }
    }
}

到目前为止,最昂贵的操作是访问数据库服务器;这是一次网络旅行,每次大约数百毫秒。

这些数据库操作是插入的吗?如果是这样,则不是逐行插入,而是为具有多行的 insert 语句构造一个字符串,原则上在该循环中有多少行。然后运行那笔交易。

如果行数加起来太多,请根据需要进行测试和缩减。可以继续向插入语句的字符串添加行,直到确定的最大数量,插入,然后继续。

一些更容易发现的低效问题

  • 不要每次循环都构造一个object。在循环之前构建一次,然后根据需要在循环中构建 use/repopulate。那么,这里就不用parse+fields了,而getline也快一点

  • 不需要每次读取都使用 if 语句。先读一行数据,就是你的header。 然后进入循环,不用ifs

总而言之,没有现在可能不需要的占位符,比如

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 而不是 ||。然后,three-argument open 更好。参见 perlopentut

如果你确实需要占位符,也许是因为你不能有一个单一的插入,但它必须分成许多或出于安全原因,那么你需要为每个生成准确的 ?-tuples要插入的行,然后为它们提供正确数量的值。

可以先 assemble 数据,然后基于它构建 ? 元组

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 具有正确数量 (?,?,...) row-values 的语句用于批次,并且 execute 在批次循环中。

最后,另一种方法是使用数据库的工具直接从文件加载数据以达到特定目的。这将比通过 DBI 要快得多,甚至可能包括将输入的 CSV 处理成另一个只有所需数据的 CSV。

由于您不需要输入 CSV 文件中的所有数据,请先按上述方式读取和处理文件,然后写出仅包含所需数据的文件(@data 以上)。那么,有两种可能的方法

  • 为此使用 SQL 命令 – COPY 在 PostgreSQL,LOAD DATA [LOCAL] INFILE 在 MySQL 和 Oracle(等);或者,

  • 对 RDBMS 中的 importing/loading 文件使用专用工具 – mysqlimport (MySQL), SQL*Loader/sqlldr ( Oracle) 等。我希望这是最快的方式

这些选项中的第二个也可以通过程序完成,方法是 运行通过 system(或者更好的是通过合适的库)将适当的工具作为外部命令。


在一个应用程序中,我在最初的 insert 中汇总了多达数百万行 - 该语句的字符串本身是高十位MB——并且这使得 运行 ~100k 行每天插入到一个语句中,到现在已经有几年了。这是 postgresql 在好的服务器上,当然还有 ymmv。

一些 RDBMS 不支持像这里使用的 multi-row(批处理)插入查询;特别是甲骨文似乎没有。 (最后才知道这里用的是这个数据库。)不过Oracle中还有其他的方法,请看评论中的链接,搜索更多。然后脚本将需要构造不同的查询,但操作原理是相同的。