在Perl数据结构中保留大量数据是否可以

时间:2017-01-11 17:57:21

标签: perl

我从客户端收到一些CSV。这些CSV的平均大小为20 MB。

格式为:

Cutomer1,Product1,cat1,many,other,info
Cutomer1,Product2,cat1,many,other,info
Cutomer1,Product2,cat2,many,other,info
Cutomer1,Product3,cat1,many,other,info
Cutomer1,Product3,cat7,many,other,info
Cutomer2,Product5,cat1,many,other,info
Cutomer2,Product5,cat1,many,other,info
Cutomer2,Product5,cat4,many,other,info
Cutomer3,Product7,cat,many,other,info

我目前的做法: 我将所有这些记录临时存储在一个表中,然后在表中查询:

where customer='customer1' and product='product1'
where customer='customer1' and product='product2'
where customer='customer2' and product='product1'

问题:在数据库中插入然后选择花费太多时间。很多事情正在发生,处理一个CSV需要10-12分钟。我目前正在使用SQLite,它非常快。但是如果我完全删除插入和选择,我想我会节省更多时间。

我想知道是否可以将这个完整的CSV存储在一些复杂的perl数据结构中?

机器通常有500MB +空闲RAM。

1 个答案:

答案 0 :(得分:2)

如果您显示的查询是您想要执行的唯一一种查询,那么这很简单。

my $orders; # I guess
while (my $row = <DATA> ) {
    chomp $row;
    my @fields = split /,/, $row;

    push @{ $orders->{$fields[0]}->{$fields[1]} } \@fields; # or as a hashref, but that's larger
}

print join "\n", @{ $orders->{Cutomer1}->{Product1}->[0] }; # typo in cuStomer

__DATA__
Cutomer1,Product1,cat1,many,other,info
Cutomer1,Product2,cat1,many,other,info
Cutomer1,Product2,cat2,many,other,info
Cutomer1,Product3,cat1,many,other,info
Cutomer1,Product3,cat7,many,other,info
Cutomer2,Product5,cat1,many,other,info
Cutomer2,Product5,cat1,many,other,info
Cutomer2,Product5,cat4,many,other,info
Cutomer3,Product7,cat,many,other,info

您只需构建一个深度为几级的哈希引用的索引。第一级有客户。它包含另一个hashref,它具有与此索引匹配的行列表。然后你可以决定你是否只想把整个东西作为一个数组引用,或者如果你想在那里放一个带有键的哈希引用。我使用数组引用,因为它消耗更少的内存。

稍后您可以轻松查询。我把上面包括在内。这是输出。

Cutomer1
Product1
cat1
many
other
info

如果您不想记住索引但必须编写许多不同的查询,您可以创建代表magic numbers的变量(甚至常量)。

use constant {
    CUSTOMER => 0,
    PRODUCT  => 1,
    CATEGORY => 2,
    MANY     => 3,
    OTHER    => 4,
    INFO     => 5,
};

# build $orders ...

my $res = $orders->{Cutomer1}->{Product2}->[0];

print "Category: " . $res->[CATEGORY];

输出结果为:

Category: cat2

要订购结果,您可以使用Perl&#39; sort。如果您需要按两列排序,那么SO上有答案可以解释如何执行此操作。

for my $res ( 
    sort { $a->[OTHER] cmp $b->[OTHER] } 
    @{ $orders->{Customer2}->{Product1} } 
) {
    # do stuff with $res ...
}

但是,您只能按照客户和产品进行搜索。

如果有多种类型的查询,这会变得很昂贵。如果您还要按类别对它们进行分组,则每次查看一次时都要对所有这些进行迭代,或者构建第二个索引。这样做比等待几秒钟更难,所以你可能不想这样做。

  

我想知道是否可以将这个完整的CSV存储在一些复杂的perl数据结构中?

绝对是出于这个目的。 20兆字节不是很多。

我已经使用此代码创建了一个20004881字节和447848行的测试文件,但这并不完美,但可以完成工作。

use strict;
use warnings;
use feature 'say';
use File::stat;

open my $fh, '>', 'test.csv' or die $!;
while ( stat('test.csv')->size < 20_000_000 ) {
    my $customer = 'Customer' . int rand 10_000;
    my $product  = 'Product' . int rand 500;
    my $category = 'cat' . int rand 7;
    say $fh join ',', $customer, $product, $category, qw(many other info);
}

以下是该文件的摘录:

$ head -n 20 test.csv
Customer2339,Product176,cat0,many,other,info
Customer2611,Product330,cat2,many,other,info
Customer1346,Product422,cat4,many,other,info
Customer1586,Product109,cat5,many,other,info
Customer1891,Product96,cat5,many,other,info
Customer5338,Product34,cat6,many,other,info
Customer4325,Product467,cat6,many,other,info
Customer4192,Product239,cat0,many,other,info
Customer6179,Product373,cat2,many,other,info
Customer5180,Product302,cat3,many,other,info
Customer8613,Product218,cat1,many,other,info
Customer5196,Product71,cat5,many,other,info
Customer1663,Product393,cat4,many,other,info
Customer6578,Product336,cat0,many,other,info
Customer7616,Product136,cat4,many,other,info
Customer8804,Product279,cat5,many,other,info
Customer5731,Product339,cat6,many,other,info
Customer6865,Product317,cat2,many,other,info
Customer3278,Product137,cat5,many,other,info
Customer582,Product263,cat6,many,other,info

现在让我们使用这个输入文件运行上面的程序,查看内存消耗和数据结构大小的一些统计信息。

use strict;
use warnings;
use Devel::Size 'total_size';

use constant {
    CUSTOMER => 0,
    PRODUCT  => 1,
    CATEGORY => 2,
    MANY     => 3,
    OTHER    => 4,
    INFO     => 5,
};

open my $fh, '<', 'test.csv' or die $!;

my $orders;
while ( my $row = <$fh> ) {
    chomp $row;
    my @fields = split /,/, $row;

    $orders->{ $fields[0] }->{ $fields[1] } = \@fields;
}

say 'total size of $orders: ' . total_size($orders);

这是:

total size of $orders: 185470864

因此该变量消耗185兆字节。这远远超过20MB的CSV,但我们有一个易于搜索的索引。使用htop我发现实际过程消耗287MB。我的机器有16G的内存,所以我不在乎。使用大约3.6秒,运行该程序的速度相当快,但我有一台SSD是一台新的CORE i7机器。

但是如果你有500MB的空余,它就不会占用你的全部记忆。可能一种SQLite方法会占用更少的内存,但是你必须将这种速度与SQLite方法的速度进行比较,以确定哪一种更适合。

我使用方法described in this answer将文件读入SQLite数据库 1 。我需要先在文件中添加一个标题行,但这很简单。

$ sqlite3 test.db
SQLite version 3.11.0 2016-02-15 17:29:24
Enter ".help" for usage hints.
sqlite> .mode csv test
sqlite> .import test.csv test

由于我无法正确衡量这一点,因此我们说感觉就像大约2秒钟。然后我为特定查询添加了一个索引。

sqlite> CREATE INDEX foo ON test ( customer, product );

这感觉好像花了一秒钟。现在我可以查询。

sqlite> SELECT * FROM test WHERE customer='Customer23' AND product='Product1';
Customer23,Product1,cat2,many,other,info

结果瞬间出现(这不科学!)。由于我们没有测量从Perl数据结构中检索的时间长短,我们无法对它们进行比较,但感觉这一切都需要大约相同的时间。

但是,SQLite文件大小仅为38839296,大约为39MB。它比CSV文件大,但不是很多。看起来sqlite3进程只消耗了大约30kB的内存,考虑到索引,我觉得这很奇怪。

总之,SQLite似乎更方便,占用的内存更少。在Perl中执行此操作没有任何问题,它可能是相同的速度,但使用SQL进行此类查询感觉更自然,所以我会选择这个。

如果我可能如此大胆,我会假设你在SQLite中没有设置索引,这使得它需要更长的时间。我们这里的行数并不多,即使对于SQLite也是如此。正确地指出它是件小事。

如果您实际上并不知道索引的作用,请考虑一下电话簿。它具有页面两侧的首字母索引。找到John Doe,你抓住D,然后以某种方式看。现在想象没有这样的事情。你需要随意地捅更多。然后尝试找到电话号码为123-555-1234的家伙。如果没有索引,这就是您的数据库所做的事情。

1)如果你想编写脚本,你也可以将命令管道或读入sqlite3实用程序来创建数据库,然后使用Perl的DBI进行查询。例如,sqlite3 foo.db <<<'.tables\ .tables'(反斜杠\表示文字换行符)会打印两次表格列表,因此这样的导入也会起作用。