@IT エンジニアライフのコメンテータ(だった)生島さんのコラムhttp://el.jibun.atmarkit.co.jp/g1sys/2010/05/post-2d1b.htmlのコメント欄に参加しました。
生島さんのコラムですが、過去に度々炎上してきましたが、炎上するたびに、
『SQLはオブジェクト指向言語の数十倍の効率』
という、この手の話が出てきます。この手の呪文は他にも幾つかあるのですが、これを出せば議論が終結するというある種の必殺技みたいに使われます。
が、それどころか、毎回、毎回、明確な結論が出ずにさらにコメント欄が荒れます。
私としては、本来はどうでもよい話なのですが、いきがかり上、私も思わず、
「私は、過去にSQLが遅いのでSQLを崩して、C言語でJOINをやらせて高速化しました。OO言語ではないですが、今だったらC++を使うでしょう(なぜってハッシュクラス があるから)。」
と発言しました。恐らく、多くの方は、
『いやいや、いくらなんでも、それはウソでしょう。』
とか、
『売り言葉に買い言葉でしょうが、それは良くないでしょう。』
とか、
『幾らC++が好きって言ったって、原理的にDBMS内で処理が閉じるSQLの方が速いでしょう。』
とか思われたことでしょう。
私も、そういうツッコミが来ることは重々承知していたのですが、現実に私は10年以上前になりますが、上記のような最適化を行ったことがありました。
以来、別にわざわざSQLを崩してCでJOINなんて事はしませんでしたが、逆にその後、さまざまなプロジェクトを通して、DBMSの動作をみる限り上記の最終手段は、まだ有効だなというのも実感としてあったのですが、あまりの共感の得られ無さにものすごい孤独感に襲われ、また生島さんの煽りも受け、このあたりで白黒はっきり付けたいと思います。
では、どのように白黒つけるのかですが、やはりベンチマークテストを行ってみるしかないかと思います。
つまり、生島さんが件のコラムのコメント欄に書かれた
TABLE_A a INNER JOIN TABLE_B b a.KEY = b.KEY
をもとにしたSQLをC++で書き実際に実行させてその実行時間をみてみましょう。
■実験の環境
<追記>この記事のコメント欄の指摘(サーバーとクライアントマシンが分かれていない)および環境が変わったので再現できない関係で、新しく環境を構築してテストをやり直しました。今回実験したテスト環境を示します。
■実験するSQLとプログラムの概要
以下のSQLを実行させ、カンマをセパレーターとして標準出力へ出力させます。
CSVファイルへの出力を想定したSQL、JOINの部分は生島さんがコメント欄で指摘したSQLそのものになっています。このSQLをもとにJOIN部分をC++でやらせてみます。
SELECT Price.CODE, RDATE, OPEN, CLOSE, NAME FROM Price INNER JOIN Company ON (Price.CODE = Company.CODE)
■実験1 素直にSQL側でJOINをさせたものを実行
以下のコードのとおり、実験するSQLをそのまま実行してみました。
#include <iostream>
#include <time.h>
#include "../kz_odbc.h"
using namespace std;
int main(void)
{
kz_odbc db("DSN=Trade",true);
kz_stmt stmt(&db);
time_t t = time(NULL);
// テーブルからデータの取得
stmt, "SELECT Price.CODE, RDATE, OPEN, CLOSE, NAME "
" FROM Price INNER JOIN Company ON (Price.CODE=Company.CODE)"
, endsql;
kz_string_array result = stmt.next();
int cnt = 0;
while ( !result.empty() ) {
cout << result[0] << "," << result[1] << ","
<< result[2] << "," << result[3] << ","
<< result[4] << "\n";
result = stmt.next();
cnt++;
}
cerr << "Execute time is " << time(0) - t << "sec." << endl;
cerr << "Record count is " << cnt << "." << endl;
return 0;
}
コードですが、Wordpressに合わせて編集してますので、変なところで改行が入っていますが御勘弁を。若干ですが、コードの説明を、
stmt, "SELECT ・・・・
とか
result.empty()
stmt.next()
の部分が私が作成したライブラリになります。といってもODBC APIを呼び出しているだけになります。そう特異なものでもないかと思います。
実行結果ですが、
Execute time is 131sec. Record count is 4671568.となりました。プログラムは標準出力に出力していますが、実行に際しては標準出力をファイルにリダイレクトしています。その方が実行速度は速くなります。
比較元のデータが無いので何とも言えませんが、1秒間に約3万5千件のデータがCSVファイルへ落とされているのでマシンスペックを考えますとまずまずでしょう。
ちなみに、実行ブランを確認しましたが、CompanyテーブルへのアクセスのTYPEはeq_refでユニークキーによるJOIN(最速のテーブルアクセス)が実行されていることを確認しました。
■実験2 C++側でネステッドループでJOINさせてみる
ループループといっていたものですが、いわゆるネステッドスープのことだと推測します。
#include <iostream>
#include <time.h>
#include "../kz_odbc.h"
using namespace std;
int main(void)
{
kz_odbc db("DSN=Trade",true);
kz_stmt stmt(&db);
time_t t = time(NULL);
// テーブルからデータの取得
stmt, "SELECT CODE,RDATE,OPEN,CLOSE FROM Price", endsql;
kz_string_array result = stmt.next();
int cnt = 0;
while ( !result.empty() ) {
// JOINの実行(ネステッドループ)
kz_stmt stmt2(&db);
stmt2, "SELECT NAME FROM Company WHERE CODE = ? "
, result[0].c_str(), endsql;
kz_string_array result2 = stmt2.next();
cout << result[0] << "," << result[1] << ","
<< result[2] << "," << result[3] << ","
<< result2[0] << "\n";
result = stmt.next();
cnt++;
}
cerr << "Execute time is " << time(0) - t << "sec." << endl;
cerr << "Record count is " << cnt << "." << endl;
return 0;
}
実行結果は以下のとおりです。Execute time is 1714sec. Record count is 4671568.
これはものすごく遅いですね。生島さんが、
『SQLにすると数十倍速くなる』
といっていたのは、実験1のコードと実験2のコードを比べて言っていたと思われます。
では、これ以上に速くさせる方法はないのでしょうか?
生島さんの言うとおり、OO言語はSQLと比べて何十倍も遅いのでしょうか?
■実験3 C++側でハッシュJOINさせてみる
件のコメント欄で生島さんが難しいとおっしゃっていた、ハッシュJOINですが、実は特段、難しいものではありません。以下のようにすっきりと実装できます。
ちなみにコード中に出てきますmapというのはバイナリサーチを行います。なので、正確にはハッシュJOINではありません。
C++でハッシュ検索を行うには、Boost等のライブラリを使う必要があります。
つまり今回のコードはある意味、最適化の余地を残しているのですが、ここではテストの再現性(環境設定)の手間を考えてmapを使います。
#include <iostream>
#include <time.h>
#include "../kz_odbc.h"
using namespace std;
int main(void)
{
kz_odbc db("DSN=Trade",true);
kz_stmt stmt(&db);
time_t t = time(NULL);
// マスターの取得・マップの作成
map< string, string> company;
stmt, "SELECT CODE, NAME FROM Company ", endsql;
kz_string_array result = stmt.next();
while ( !result.empty() ) {
company.insert( pair< string, string>( result[0], result[1]) );
result = stmt.next();
}
// テーブルからデータの取得
stmt, "SELECT CODE,RDATE,OPEN,CLOSE FROM Price ", endsql;
result = stmt.next();
int cnt = 0;
while ( !result.empty() ) {
cout << result[0] << "," << result[1] << ","
<< result[2] << "," << result[3] << ","
<< company[ result[0] ] << "\n";
result = stmt.next();
cnt++;
}
cerr << "Execute time is " << time(0) - t << "sec." << endl;
cerr << "Record count is " << cnt << "." << endl;
return 0;
}
結果ですが、以下のとおり、実験1のコードよりも早くなっております。
Execute time is 108sec. Record count is 4671568.
■結果
実行結果を再度以下に掲載します。実験1(SQL) | 131秒 |
実験2(C++側でネステッドループ) | 1714秒 |
実験3(C++側でハッシュ) | 108秒 |
明確に結果が出ているかと思います。こんなに単純なテストの結果からでも
「SQLをばらしてJOINをC++で行えば速くなる場合がある」
ということは理解していただけれるかと思います。
また、
『SQLはオブジェクト指向言語の数十倍の効率』
というのは、単純に
「OO言語側の最適化が不十分である可能性がある」
ということも言えるでしょう。
ただ、実験3では、高々十数%しか速くなっていません。
ということであれば、通常はやはり実験1のようなコードの方がトータル(開発効率と実行効率を考えると)としては良いと思われる。実験3のような事実はあくまでも知識としてしておきたいところです。
追記、コメント欄の議論を踏まえて再度記事をアップしました。
追記、コメント欄の指摘(ローカルマシンで動かしている)を受けまして再度環境を作成して実験しました。
追記、まとめ記事を作成しました。
2011-01-03 20:11:05
久々に昔のブックマークを漁ってたら、生島さんの@ITコラムが出てきたので辿ってきてみました。
半年も前のブログにコメントしてスミマセン…
予想した通りやっぱりWhere句が無かったw
でも、生島さんと同じ結論(実験1のようなコードの方がトータルとしては良い)になってますね。
実験3が速い理由はこんな感じでしょうか?
・Where句無しの全件出力(結合する前の時点で無駄データがほぼ取得されない)
・オプティマイザによる結合方法の判定自体が省かれる。(誤差?)
単位株数が2000株のものだけ抽出、とかになると、また話が変わってしまうわけです。
ご存知の通り、ハッシュJOIN等の機能が元々RDB側にあり、
これをCとかJavaとかの言語側で再開発するのは非効率です。(その分のドキュメントが要るため)
逆に、オプティマイザやRDBとのI/Fといったオーバーヘッドが足を引っ張ることもあります。(この記事の例)
後者がでかくなるのは、デカルト積でデータ増幅させる時のネットワーク負荷とかかな…
トータルでは後者は誤差だと思いますが、あのコラムは無駄な炎上とかするよりも
上記のトレードオフとかの議論に発展して欲しかった…