1400万行のデータをRubyで扱うと...?(C/C++でリベンジ編)

1400万行のデータをRubyで扱うと...?で玉砕したのでここはC/C++で書いてみることにしました。C/C++の場合、使い捨ても業腹なので簡単なベースクラスを作成し派生クラスで対応することにします。ベースクラスでは今回必要なものだけを実装し、再利用するようなことがあれば逐次追加していくつもりです。(サンプルソースからはエラー処理を端折っています。)

static char* server_args[] = {
  "MySQLBase.cpp","--datadir=/usr/local/mysql/var","--key_buffer_size=32M"
};
static char* server_groups[] = {
  "embedded","server","this_program_SERVER",(char *)NULL 
};
class CMySQLBase {
protected:
  MYSQL*  m_db;
  MYSQL_RES* m_res;
public:
  CMySQLBase(void);
  virtual ~CMySQLBase(void);
  virtual int run(int argc,char* argv[]) = 0;
protected:
  int   init(void); // 初期化処理
  int   end(void);   // 終了処理
  bool  conn(const char* dbname); // コネクト処理
  bool  query(const char* query); // クエリ発行
  void  get_result(void); //結果の取得
  MYSQL_ROW get_row(void); //行の取得
  unsigned int get_field(void); //フィールド数の取得
};

このとき、それぞれのメソッドの実装は以下のとおり。

int CMySQLBase::init(void) {
  int r = mysql_library_init(
    sizeof(server_args)/sizeof(char *),server_args,server_groups);
  if (!r) m_db = mysql_init(NULL);
  return r;
}
int CMySQLBase::end(void){
  mysql_close(m_db);
  mysql_library_end();
}
bool CMySQLBase::conn(const char* dbname){
  return mysql_real_connect(
    m_db,NULL,"mysql","xxx",dbname,0,NULL,0) ? true : false;
}
bool CMySQLBase::query(const char* query){
  return mysql_query(m_db,query) == 0 ? true : false;
}
void CMySQLBase::get_result(void){
  m_res = mysql_store_result(m_db);
}
MYSQL_ROW CMySQLBase::get_row(void){
  return mysql_fetch_row(m_res);
}
unsigned int CMySQLBase::get_field(void){
  return mysql_num_fields(m_res);
}

このベースクラスから今回処理用の派生クラスを作成します。派生クラスのコンストラクタでベースクラスのinit()/conn()をデストラクタでend()を呼び出します。後は、呼び出し元からget_record()でレコードを取得します。

class foo : public CMySQLBase {
public:
  foo(void){id=category=price=0;init();conn('db1');}; 
  virtual ~foo(void){end();};
  bool get_record(const char* id);
  int get_category(void);
  int get_price(void);
private:
  int category;
  int price;
};
===
bool foo::get_recorde(const char* id){
  string q = "select category,price from foo where id = ";
  q.append(id);
  if (!query(q.c_str())) return false;
  get_result();
  MYSQL_ROW  row = get_row();
  category = atol(row[0]);
  price = atol(row[1]);
  return true;
}
int foo::get_category (void){
  return category;
}
int foo::get_price(void){
  return price;
}

これでようやく準備完了。あとはCSVを読み込んでまわす処理を作成します。ccsvクラスはプログラミング作法のサンプルを使用したCSV処理クラス(昔から愛用しています)

ifstream in;
ofstream out;

in.open(argv[1],ios::in);
ccsv csv(in);
foo _foo;
string line;
string id;
char   buff[1024];

while(csv.getline(line)){
  if (id.compare(csv.getfield(0)) != 0){
    id = csv.getfield(0);
    _foo.get_record(id.c_str());
  }
  memset(buff,0x00,sizeof(buff));
  sprintf(buff,"%s,%d,%s,%s,%d,%s\n",
    id.c_str(),
    _foo.get_category(),
    csv.getfield(1).c_str(),
    csv.getfield(3).c_str(),
    _foo.get_price(),
    csv.getfield(2).c_str());
  out << buff;
}

これで、万端。果たして結果は19分!なんとか使い物になりそうです。ふうぅ~。それにしても疲れました。今更ながらRubyの楽ちんさが身に沁みました。

この記事のトラックバックURL:

http://hippos-lab.com/blog/trackback/94

Comments

担当の方、きっと過去に大きなトラウマがあるんですよね。とにかくdelete/dropという言葉に過剰反応するんですよ。安全性という意味では、参照権限しかあげないよ!ってのが一番ですがこの時代、時間もお金と等価ですからね。まあ、データを日々抜き出す大変さにネをあげてなんとかしてくれぇーって言ってくるのがいつものパタンですからちょっとイジワルにその日を待ってますよ、フフフ。

ちなみに申請は却下、でした。

なるほど、業務ですから権限の管理はきつくするのは必要ですけど、やりにくそうですね。

> 今思いついたのですがテンポラリテーブルじゃなくてパーマネントのテーブルを申請
デイリータスクとの事ですので、パーマネントでも良さそうですよね。

> というかこれだけ大量のデータを毎日抜き出してくるっていうところから改めて欲しいものですね。
というか、当て込むCSVを作る時に必要なCSVも一緒に生成できるんじゃないですかね、きっと。その提案をしていった方が皆ハッピーになれるんじゃ。(伺っている状況ですと提案が通るとは思えませんが(^^;)

utaさん、おめでとうございます。本年もよろしくお願いします。

そうですね、utaさんの解が最速かもしれません。データベースに関連する処理はデータベースにお願いしたほうが間違いがなくてたぶん早いと思います。一度、データベースに突っ込む方法というのは提案はしていたんですけどCreate権限を貰えませんでしたぁ。ここのお客さん(といっても社内の人間なんですけど)もうカタくて...というかこれだけ大量のデータを毎日抜き出してくるっていうところから改めて欲しいものですね。

う、今思いついたのですがテンポラリテーブルじゃなくてパーマネントのテーブルを申請しておいてinsert/truncateというテがありましたね。適当なテーブル名と用途で申請してみようかなぁ。うまく通ればメッケもんです。

明けましておめでとうございます。utaです。
本年も宜しくお願い致します。

やはり処理速度の点ではCとは比べ物になりませんねぇ。
私も、Rubricksを少しでも高速化しようと頑張ってはいますが、
新機能(≒負荷)追加の方が早くていたちごっこの様相を呈しています。

さて、本件に関してですがRubyからは離れますが、
mysqlに全部やらせてしまうという考え方もあるのではないかなと思います。


# vi /tmp/test1.csv
=================================================================
1,name1,am1,2007-01-01
2,name2,am2,2007-01-01
3,name3,am3,2007-01-01
4,name4,am4,2007-01-01
=================================================================

# mysql
mysql> create database test;
mysql> use test
mysql> create table foo (
-> id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
-> category VARCHAR(32),
-> name VARCHAR(32),
-> price INTEGER,
-> value VARCHAR(32),
-> primary key(id)
-> ) type=InnoDB;

mysql> insert into foo values(1,'cat1','name1',500,'dummy');
mysql> insert into foo values(2,'cat2','name2',2500,'dummy');
mysql> insert into foo values(3,'cat1','name3',800,'dummy');
mysql> insert into foo values(4,'cat2','name4',2100,'dummy');

mysql> create table bar (
-> id INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
-> name VARCHAR(32),
-> amount VARCHAR(32),
-> date DATE,
-> primary key(id)
-> ) type=InnoDB;

mysql> load data local infile '/tmp/test1.csv' into table bar fields terminated by ',';

mysql> select foo.id, foo.category, bar.name, bar.amount, foo.price, bar.date
-> from foo, bar where foo.id = bar.id
-> into outfile '/tmp/test2.csv' fields terminated by ',';

こんな感じで。
CSVファイルの空文字列やNULLの指定方法と、
プログラム内でエラーチェックはできない点に注意すれば
他の言語を噛ますよりも恐らく高速なのではないかなと思います。