【问题标题】:How to write data to a file for loading into MySQL database?如何将数据写入文件以加载到 MySQL 数据库中?
【发布时间】:2014-01-08 05:51:40
【问题描述】:

在将解析的数据插入 MySQL 数据库时,我遇到了某种 I/O 瓶颈。因此,我不想解析文件并将每条记录单独插入数据库,而是将记录写入文件(如 CSV),然后我可以手动将文件加载到数据库中。

如何使用 Perl 来解决这个问题?下面是当前将数据插入数据库的代码,但我想将数据写入文件,以使以后尽可能轻松地将数据插入 MySQL 的格式。

#INSERT INTO ANNOUNCE TABLE
foreach my $au (@ANNOUNCED) {

    my $val=$au->[0];
    my $IP = $prefix->ip();
    my $subnetmask = $prefix->mask();


    $Announce_update->execute($IP,$subnetmask,$UpdateKey);

}

【问题讨论】:

    标签: mysql perl


    【解决方案1】:

    为了从外部文件加载数据,MySQL 提供了LOAD DATA INFILE 命令。 LOAD DATA INFILE 在输入文件格式方面相当灵活,允许您指定分隔符、EOL 字符、是否引用字段等。它与 Text::CSV 配合使用非常好,您可以使用它来输出分隔文件包含您的数据。

    首先,将数据写入文件:

    use Text::CSV;
    
    my $csv = Text::CSV->new({ eol => "\n" }) or die Text::CSV->error_diag();
    
    my $infile = "/path/to/file";
    open my $fh, ">", $infile or die $!;
    
    for my $i (0..$#ANNOUNCED)
    {
       # Don't end last line with '\n' or we'll get a garbage row when we load
       # to the database
       $csv->eol(undef) if $i == $#ANNOUNCED;
    
       # Generate the data to insert for this row
    
       # Write to file
       $csv->print($fh, [ $IP, $subnetmask, $UpdateKey ]);
    }
    
    # Close file handle to flush the buffer
    close $fh;
    

    请注意,写入文件后必须关闭文件句柄,否则 MySQL 可能无法获取所有数据。

    接下来,加载文件:

    my $query = "LOAD DATA LOCAL INFILE '$infile' INTO TABLE table
                 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'
                 LINES TERMINATED BY '\\n'";
    
    eval {
       # $dbh is an already open database handle
       my $rows_inserted = $dbh->do($query);
    };
    die $@ if $@;
    

    LOCAL 关键字影响文件是否位于 MySQL 服务器上。如果您要加载到位于 localhost 的数据库,则可以省略 LOCAL 关键字,这听起来可能违反直觉(使用 LOCAL,客户端将文件的副本发送到服务器的临时目录和服务器从那里读取它,因此它是服务器本地的)。如果没有 LOCAL 关键字,文件必须对所有人可读(即在 *nix 上至少有 0644 的权限,因为您还必须写入它)。 MySQL 还会根据是否使用LOCAL 在不同的位置查找相对路径;详情请见documentation

    上述查询假设您的表只有三列。如果它有更多,您需要指定要插入数据的列,按照 CSV 中出现的顺序,例如:

    my $query = "LOAD DATA LOCAL INFILE '$infile' INTO TABLE table
                 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '\"'
                 LINES TERMINATED BY '\\n'
                 (ip, subnetmask, updatekey)";
    

    复合插入

    LOAD DATA INFILE 如果您一次插入大量行,是迄今为止最快的方法。然而,对于较小的插入,写入和读取临时文件的额外 I/O 开销(特别是如果您使用 LOCAL 选项)使其比使用像这样的复合插入要慢:

    # Inserts 3 rows at once
    INSERT INTO table VALUES (foo, foo, foo), (bar, bar, bar), (baz, baz, baz)
    

    LOAD DATA INFILE 的行数比复合插入快。对于我的数据库和应用程序,我做了一些分析,发现这个数字大约为 100,但几乎可以肯定它对你来说是不同的。我写了一个函数,根据要插入的数据行数选择最有效的方法:

    sub insert_rows {
        my $data = shift; # Reference to an AoA
    
        my $num_rows = $#{ $data };
    
        if ($num_rows < 100) {
            # Generate compound insert statement
        }
        else {
            # LOAD DATA INFILE
        }
    }
    

    请注意,max_allowed_packet 的默认值在 MySQL 5.7 中仅为 1MB。如果您的复合插入语句超过此值,您将收到Packet too large 错误并且插入将失败。您可以将其最大调整为 1GB,但到那时您可能已经达到了LOAD DATA INFILE 更有效的阈值。

    【讨论】:

    • 非常感谢您非常详细的解释和编辑我的问题所花费的时间。有些事情我不明白,例如您在回答中对我的脚本所做的更改。我将上传我的整个脚本供您查看,稍后可能会删除。我也不明白这一行:对于我的 $i (0..$#ANNOUNCED)。
    • 因此,由于我有 10000 个要解析的文件,所以我正在做的是在特定文件中的记录(块)之后将数据插入数据库。仅1个文件完成解析和插入大约需要5分钟。但后来我想,我不会插入数据库,而是将解析后的数据写入 csv 或其他东西,然后按照你的建议执行 LOAD DATA INFILE。
    • @iseesharp 我只是省略了设置$IP$subnetmask$UpdateKey 的步骤,以避免混淆答案。您显然需要添加这些步骤才能获得您的价值观。
    • @iseesharp for my $i (0..$#ANNOUNCED) 循环遍历 @ANNOUNCED 数组,将变量 $i 设置为当前数组索引。要访问循环内的当前元素,请使用$ANNOUNCED[$i]。这是必要的,因为您不希望文件中的最后一行以 \n 结尾,否则 MySQL 将加载垃圾行。
    【解决方案2】:

    此时使用Text::CSV 可能会让您的生活更轻松。如果您的系统上还没有该模块,请安装该模块,然后将此行添加到您的脚本中:

    use Text::CSV ;
    

    注释掉代码中的数据库插入位:

     # $dbh->prepare("SET FOREIGN_KEY_CHECKS = 0;")->execute();
     # $Announce_update->execute($IP,$subnetmask,$UpdateKey);
    

    然后尝试抓取你想要的数据:

     my @csvdata = ($IP,$subnetmask,$UpdateKey); # assuming data is in these vars 
                                                  # and they aren't references used 
                                                  # by $Announce_update somehow
    

    然后将其写入某个地方的 CSV 文件(摘自文档),您将“手动”加载这些文件(我假设您的意思是使用 MySQL 控制台或 CLI 工具)。

      my $csv = Text::CSV->new ( { binary => 1 } )  
      open $fh, ">:encoding(utf8)", "csvdata.csv" or die ;
      $csv->print ($fh, $_) for @csvdata;
    

    希望这会有所帮助 - 虽然手头有点短。即使它确实有效,我也不确定你这样做是否会让你的生活更轻松;-)

    【讨论】:

    • 当然,如果您在另一个文件中已经拥有数据,那么从那里获取它可能会更快/更容易......尽管我明白你为什么想要在调查瓶颈时保持工作流程基本相同。为什么不为真正的问题寻求帮助呢?瓶颈可能出在插入/更新脚本本身吗?
    • 感谢您的回复,我目前正在按照您的建议调查 Text::CSV 并遵循 ThisSuitIsBlackNot 在他的回答中所说的内容,尽管我完全理解。
    猜你喜欢
    • 1970-01-01
    • 2016-04-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-14
    相关资源
    最近更新 更多