2017-11-18 11 views
1

IDを表す2つの数値列を持つ非常に大きなCSVファイル(〜10mil行)があります。要件は次のとおりです。最初のIDを指定すると、2番目のIDを非常に高速に返します。 私はCSVをマップ構造のように振る舞わせる必要があり、それはメモリ内になければなりません。 awk変数をシェルに公開する方法を見つけることができなかったので、bash連想配列の使用を考えました。大きなCSVファイルのbash連想配列への読み込み遅い/スティック

問題は、連想配列にcsvを読み込むと〜8milの行の後に非常に遅くなったり固まったりすることです。私は考えることができる減速の原因を排除しようとしてきました。ファイルの読み取り/ IO、関連性のあるarraylimitationsです。だから、I have a couple of functionsは連想配列にファイルを読み込みますが、それらはすべて同じ遅さの問題があります。

Here is the test data

  1. loadSplittedFilesViaMultipleArrays - >元のファイルが小さいファイル(1ミル行)に分割し、4つの連想配列(最大3つのミルレコード毎)を構築するためにループを読み取るしばらく使用した前提
  2. loadSingleFileViaReadarray - > readarrayを使用して元のファイルを一時配列に読み込み、連想配列を作成します。
  3. loadSingleFileViaWhileRead - >読み込みループを使用して連想配列を作成します

しかし、私はそれを把握していないようです。たぶんこのやり方は完全に間違っています...誰かがいくつか提案してもらえますか?

+0

bashはこのようなタスクに適していません。適切なプログラミング言語を使用する方が良いでしょう。 – janos

+0

あなたの問題は基本的です。間違ったツールを選択した... – HuStmpHrrr

+0

'sqlite3'または' mysql'がより適しています。 – dawg

答えて

2

このサイズの連想配列の場合、Bashは間違ったツールです。使用を検討して言語(PerlやPython、Rubyの、PHP、JS、などなど)を使用すると、通常のbashと一緒にインストールされsqlite3 SQLデータベースを使用することができバッシュのみ環境については

より適しています。 (ただしPOSIXではありません)

まず、csvファイルからデータベースを作成します。 (exp.dbは、この時点では存在してはいけません)があり、この(PerlやPython、Rubyの、GUIツール)を行うには多くの方法があるが、これはsqlite3のcommand line shellで対話的に行うのに十分に簡単です:

$ sqlite3 exp.db 
SQLite version 3.19.3 2017-06-27 16:48:08 
Enter ".help" for usage hints. 
sqlite> create table mapping (id integer primary key, n integer); 
sqlite> .separator "," 
sqlite> .import /tmp/mapping.csv mapping 
sqlite> .quit 

あるいは、パイプSQL文:

#!/bin/bash 

cd /tmp 

[[ -f exp.db ]] && rm exp.db # must be a new db as written 

echo 'create table mapping (id integer primary key, n integer); 
.separator "," 
.import mapping.csv mapping' | sqlite3 exp.db 

(注意:書かれたとして、exp.dbが存在していなければならないか、INSERT failed: UNIQUE constraint failed: mapping.idを取得するあなたはとてもデータベースexp.dbが更新ではなく、csvファイルで作成され、それを書くことができますが、あなたはおそらくしたいと思います。 Python、Perl、Tcl、Rubyなどの言語を使用してください)

いずれの場合も、最初の列を2番目の列にマッピングする索引付きデータベースが作成されます。インポートは、少しの間(198メガバイトの例で15〜20秒)かかりますが、それは、インポートcsvファイルから新しい永続的なデータベースを作成します。

$ ls -l exp.db 
-rw-r--r-- 1 dawg wheel 158105600 Nov 19 07:16 exp.db 

その後すばやくバッシュからその新しいデータベースを照会することができます

$ time sqlite3 exp.db 'select n from mapping where id=1350044575' 
1347465036 

real 0m0.004s 
user 0m0.001s 
sys  0m0.001s 

これは、古いiMacでは4ミリ秒かかります。

あなたのクエリのバッシュ変数を使用したい場合は、必要に応じて使用すると、クエリ文字列を連結または構築することができます。

$ q=1350044575 
$ sqlite3 exp.db 'select n from mapping where id='"$q" 
1347465036 

とDBが永続的であることから、あなただけにcsvファイルのファイルの時刻を比較することができますあなたはそれを再作成する必要があるかどうかをテストするためのDBファイル:

if [[ ! -f "$db_file" || "$csv_file" -nt "$db_file" ]]; then 
    [[ -f "$db_file" ]] && rm "$db_file" 
    echo "creating $db_file" 
    # create the db as above... 
else 
    echo "reusing $db_file"  
fi  
# query the db... 

より:

  1. sqlite tutorial
  2. sqlite home
+0

ニース!あなたのファイルが274MBのとき、なぜ 'exp.db'が151MBになるのか分かりませんか?私はMac上のバージョン3.19.3です。 –

+0

うわー!それは大きなファイルです。私の 'sql'は錆びにくいので、小さくて速いファイルを作る方法があるかもしれません。それは主に、私が乗ろうとしていたコンセプトでした。 – dawg

+0

OK - 私のメインマックで試して、ファイルは158メガバイトです。 CSVの例は198MBです。うーん。 Macとファイルの履歴によって異なります。最初のファイルにはトランザクション履歴が含まれている可能性があります。 – dawg

1

私は、CSVをハッシュに読み込んだ後、永遠にクライアントからのTCP経由で要求をルックアップする小さなPerlベースのTCPサーバーを作った。それはかなり自明です:

ので
#!/usr/bin/perl 
use strict; 
use warnings; 

################################################################################ 
# Load hash from CSV at startup 
################################################################################ 
open DATA, "mapping.csv"; 
my %hash; 
while(<DATA>) { 
    chomp $_; 
    my ($field1,$field2) = split /,/, $_; 
    if($field1 ne '') { 
     $hash{$field1} = $field2; 
    } 
} 
close DATA; 
print "Ready\n"; 

################################################################################ 
# Answer queries forever 
################################################################################ 
use IO::Socket::INET; 

# auto-flush on socket 
$| = 1; 
my $port=5000; 

# creating a listening socket 
my $socket = new IO::Socket::INET (
    LocalHost => '127.0.0.1', 
    LocalPort => $port, 
    Proto => 'tcp', 
    Listen => 5, 
    Reuse => 1 
); 
die "cannot create socket $!\n" unless $socket; 

while(1) 
{ 
    # waiting for a new client connection 
    my $client_socket = $socket->accept(); 

    my $data = ""; 
    $client_socket->recv($data, 1024); 

    my $key=$data; 
    chomp $key; 
    my $reply = "ERROR: Not found $key"; 
    if (defined $hash{$key}){ 
     $reply=$hash{$key}; 
    } 
    print "DEBUG: Received $key: Replying $reply\n"; 

    $client_socket->send($reply); 
    # notify client that response has been sent 
    shutdown($client_socket, 1); 
} 

、あなたがgo.plとして上記のコードを保存し、その後でそれを実行可能にします。

./go.pl & 

chmod +x go.pl 

、その後で、バックグラウンドでサーバを起動します

次に、クライアントとしてルックアップを実行する場合は、標準socatユーティリティ(

)を使用してlocalhost:5000にキーを送信します。迅速なベンチマークとして
socat - TCP:127.0.0.1:5000 <<< "1350772177" 
1347092335 

、それは8秒で千件の検索を行います。

START=$SECONDS; tail -1000 *csv | awk -F, '{print $1}' | 
    while read a; do echo $a | socat - TCP:127.0.0.1:5000 ; echo; done; echo $START,$SECONDS 

これはおそらく、ソケット接続とティアダウンのオーバーヘッドを減らすために、要求ごとにルックアップするために複数のキーを処理するためのわずかな変化によって高速化することができます。

+0

うーん...これは、OPの問題のために大過剰です。そうは思わない? – HuStmpHrrr

+0

@HuStmpHrrrいいえ、まったくありません - 私は8msがかなり良いと思います。ここには他の提案なしで9時間ありました... –

+0

@ MarkSetchellに感謝しますが、私はperlの使用を避けたいです(私は組織内に新しい言語を導入したくありません;)) – treaz

2

@HuStmpHrrrのコメントに触発されて、別の、おそらくより単純な選択肢について考えました。

あなたは1メガバイト(またはその他)にファイルを分割するパラレルサイズのチャンク、その後、並行してその結果チャンクのそれぞれを検索するために、すべてのCPUコアを使用しGNUを使用することができます。

parallel --pipepart -a mapping.csv --quote awk -F, -v k=1350044575 '$1==k{print $2;exit}' 
1347465036 

は下取ります私のiMac上で2番目とそれは最後のレコードだった。

関連する問題