【问题标题】:Perl performance is slow, file I/O issue or due to while loopPerl 性能缓慢,文件 I/O 问题或由于 while 循环
【发布时间】:2021-10-22 09:34:38
【问题描述】:

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

【问题讨论】:

  • 数据库操作将成为您的瓶颈。如果这些确实是“插入”,那么您可以批量插入,而不是为每个插入使用数据库事务。
  • 不要为循环的每次迭代创建一个新的Text::CSV_XS 对象。而是创建它并使用它的 getline 方法循环文件。
  • 请为您正在使用的 DBMS 添加标签。此外,您可以使用this 分析您的代码。它并不完美,但对于您显示的代码来说可能已经足够了。
  • 谁能举例说明如何进行批量插入,因为它是瓶颈并且性能仍然很慢。
  • @Jeg CSV 文件字段中的代码 bind_paramsNUM_OF_PARAMS。 CSV 文件的行之间的字段数可以更改吗?您是否总是像看起来那样绑定 CSV 文件的第一个 NUM_OF_PARAMS 字段?

标签: database performance perl while-loop file-read


【解决方案1】:

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

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

根据需要进行测试并按比例缩小,如果这增加了太多行。可以继续向插入语句的字符串添加行,直到确定的最大数量,插入,然后继续。

一些更容易看到的低效率

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

  • 每次读取都不需要if 语句。首先读取一行数据,这就是你的标题。 然后进入循环,不用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, ( "&lt;$FileDir/$file" || Err(...) )。我们需要or 而不是那里的||。然后,三个参数open 更好。见perlopentut

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

可以先组装数据,然后基于它构建?-tuples

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)。那么,有两种可能的方式

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

  • 使用专用工具从 RDBMS 导入/加载文件 - mysqlimport (MySQL)、SQL*Loader/sqlldr (Oracle) 等。我希望这是最快的方式

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


在一个应用程序中,我在最初的 insert 中汇总了多达数百万行——该语句的字符串本身高达数十 MB——并且保持到现在为止,每天在一条语句中插入约 100k 行。这是 postgresql 在好的服务器上,当然还有 ymmv。

一些 RDBMS 不支持像这里使用的那样的多行(批量)插入查询;特别是甲骨文似乎没有。 (最后我们被告知这里使用的数据库。)但是在Oracle中还有其他方法可以做到,请参见cmets中的链接,并搜索更多。那么脚本就需要构造一个不同的查询,但操作原理是一样的。

【讨论】:

  • 这里提出的三点是否需要具体的代码示例让我知道
  • 我相信Text::CSVgetline方法比使用parse更有效。
  • 感谢大家的回复。 Zdim,如果可以的话,你能提供代码示例吗?谢谢
  • @Jeg 是的,等我一分钟我会的
  • @Jeg 好像Oracle中sql的COPY只在表之间复制,而不是从文件中复制。但是有LOAD DATA INFILE filename INTO TABLE tablename FIELDS ( ... ) 或类似的(我不知道Oracle,只是查了一下)。因此,您可以按此处所示处理 CSV 并将其写出,然后LOAD 它。应该比逐行插入要快得多!这是blog.oracle 上的一些代码。也就是说,如果上面关于如何在 Oracle 上进行多行(批量)插入的链接由于某种原因无法正常工作。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2013-02-05
  • 1970-01-01
  • 2020-08-11
  • 1970-01-01
  • 2016-01-04
  • 2015-11-13
  • 1970-01-01
相关资源
最近更新 更多