
下記のようなシステムでパフォーマンスが良さげなSQLite を使用予定ですが、もっと速いものが無いか確認のため他のデータベースのパフォーマンスを計測してみました。SQL 利用前提ですが、NoSQL が圧倒的な性能を出す場合は検討する必要があるので KVS も確認しました。
| データベース名 | タイプ | 形態 | 評判 | 計測についての備考 |
|---|---|---|---|---|
| SQLite | RDB | 組み込み ※2 | おもちゃ、Android標準 | JDBC操作 ※1 |
| H2 | RDB | 組み込み ※2 | 組み込み最速 | JDBC操作 ※1 |
| Derby | RDB | 組み込み ※2 | Java標準で付属 | JDBC操作 ※1 |
| EHCache | KVS | 組み込み ※2 | OSCacheと2大巨頭 | 1万件ごとにディスク書き込み設定 |
| Redis(Jedis) | KVS | サーバー ※4 | 爆速 | JedisAPI 操作 |
| Mongodb | KVS | サーバー ※4 | 高パフォーマンス | Mongodb 標準API 操作 ※3 |
| Cassandra | KVS | サーバー | 大手導入事例多数、廃止も多数 | JDBC操作 (AutoCommit のみ可能) |
| MySQL(MyISAM) | RDB | サーバー | サーバ型非トランザクションRDB最速 | JDBC操作 ※1 |
| MySQL(InnoDB) | RDB | サーバー | サーバ型トランザクションRDB最速 | JDBC操作 ※1 |
| PostgreSQL | RDB | サーバー | MySQLと異なりGPLじゃない | JDBC操作 ※1 |
※1JDBC 操作に関して INSERT は 1 万件ごとにコミット、SELECT は主キー指定で 1 件ずつ全件取得
※2 組み込みモード (ローカルファイル永続化) のみ計測、インメモリモードやサーバーモードは未計測
※3 全 INSERT 後の ensureIndex によるインデックス作成時間が計測結果に含まれる
※4 遅延書き込み (非同期書き込み)*1
環境 CPUCore2 Duo2GH 2GHz*2、メモリ 4GB、Windows XP 32bit*3、HDD (TOSHIBA MK8052GSX)*4、しょぼめのノートパソコン
データ 主キー:数値、値:文字列、レコード長:約 200 byte
スレッド数 1

データベースの処理数に対する処理時間は、上記結果のMySQL に見られるような線形的増加 (リニア、直線的) に増加するパターンと、Cassandra のように指数関数的 (雪ダルマ式) に増大するパターンがあります。また、線形的増加から指数関数的増加に移行する臨界点や動作不能になる臨界点がある場合が多いです。1 万件での処理時間はこうだから 1 億件の予想処理時間はその 1 万倍というような情報をたまに見かけますが、件数が増大するほど線形的増加ではないほうが多いと思います。

SQLite が INSERT、SELECT とも予想以上に他を圧倒し高速でした。計測結果に示すとおり 1 億件程度のデータ (データファイルサイズは 1.8 GB でした) なら大丈夫そうです。H2 のサイトにH2 が最速とするパフォーマンス比較 (H2) がありますがSQLite はトランザクションがテストされていないという理由で比較されていません。ちなみに実際にはSQLite はトランザクションをサポートしていて、テストに関してもSQLite 本体コード 6万7000行 に対しテストコードは 4567万8000行 (publickey) だそうです。古い情報ですが、SQLiteを使うべき10の理由と5つのデメリット (CAP-LAB テクニカル) が色々参考になります。
SSD で試してみたところ、1 億件 INSERT 647秒、1 億回 SELECT 383秒 でした。さすがに速い。SSD 環境は CPU i5 2.4GH、メモリ 8GB、Windows 7 64bit*5、SSD (TOSHIBA THNSNC128GMMJ)*6、これもノートパソコンです。

EHCache は 1 万件取得で最速ですが、件数が増えると組み込みRDB のSQLite や H2 より遅いのは良い?にしても、クライアント・サーバー型 KVS の Mongodb や Redis より総合的に少し遅いのは予想外でした。overflowToDisk の設定にもよると思いますが、知らずに H2 や Mongodb のキャッシュとして EHCache を使ってまいそうです。すべてディスクに永続化するように設定していたのですが、100万件テストでは登録したデータを取得しようとしたときに欠落している場合があるため計測しませんでした。ところでロゴを見て気づいたのですが EHCache が回文になっているのを今初めて知りました。

最速のSQLite と比較すると登録は 6 倍かかり、取得は 20 倍かかりますが、クライアント・サーバー型としては最も登録性能に優れています。ただし、分散環境ではデータがときどき消える、マルチコアでスケールしない (InfoQ)、などに注意する必要があります。もちろん、これらの問題は将来解消されるかもしれません。

NoSQL が終焉したと言われて久しいですが、Google のインフラ基盤やFacebookの HBase (Hadoop) で HBase (Hadoop データベース)*7 がバリバリ使われていて、Mixi では永続化機能付きmemcached とも言えるTokyoTyrant が使用され、DeNA ではMySQL に HandlerSocket を組み込み NoSQL を実現しています。必要な場面があるので今後も NoSQL や KVS が無くなることはなく、逆にRDB も衰退することはありません。Twitter は今でもMySQL +memcached ですし、GAE ではMySQL も用意されています。また、NoSQL に対してSQL が遅いというアンチテーゼとして、Postgres の設計者による分散インメモリRDB の VoltDB が登場しています。
業務システムから見るとSQL はそのままでスケールアウトしたいという要望に単純な NoSQL や KVS は合致しません。元々RDB がスケールアウトによりリニアにスケールしない問題や耐障害性を解決するためにGoogle のBigTable などにインスパイアされ登場したのが分散 KVS ですが、現在はRDB に NoSQL や KVS の要素が取り込まれています。例えば、MySQL 5.6 ではmemcached や HandlerSoclet などの概念が取り込まれSQL なしで高速アクセス可能になっています。またMySQL Cluster やOracle Exadata は分散 KVS と同じようにリニアにスケールアウト可能になっており、Postgres も 32 コア CPU 対応などスケールアップ可能になっています。
計測結果はあくまでも私が必要な環境と条件の計測結果であり、id:matsumoto_r さんがおっしゃられているとおりデータベースそのものの優劣を示しているものではありません。並行アクセスしたらとか、設定変えたらとか、たくさんご意見いただきましたが、もちろん、負荷条件を変えたりそれぞれ環境に合わせてチューニングすれば結果は変わると思います。今回の想定システムでは並行アクセスはほぼ発生せず意味がないのでマルチスレッドでの計測はせず、シングルスレッドでのデータ永続化の基本性能を確認するために実施しました。並行処理が得意(前提)とされるデータベースをシングル構成で計測に追加したのは差を確認するためです。並行性能の情報はたくさん公開されていると思いますが、ご自分の環境に合う方法で計測していただければと思います。
なお、ソフトウェアやドライバはすべて最新安定版、設定はデフォルトです。ただし、全データのディスク同期が前提の計測であるため、EHCache はインメモリではなくディスク永続化モードです。ちなみにSQLite はもう少し触ってみたところ Beta 版にすると 20% 高速化、非同期モードにするとさらに 10% 高速化しました。
私は臆病者なので良い結果を出さないとOracle に怒られる気がするからです。OTN ライセンスに下記の条項が含まれています。
オラクルの事前承諾なく、プログラムのベンチマークテストの結果を開示すること。
OTN開発者ライセンス
蛇足ですが、DB2 は設定を完璧にして最新パッチをあてて最高性能を出さないと怒られるかもしれません。
(A)ベンチマーク・テストで使用した方法 (例えば、ハードウェアおよびソフトウェアのセットアップ、導入手順および構成ファイル) を公開し、
SLA - L-JWOG-6K4JSQ
(B) 「プログラム」に対するIBM またはIBM 製品を提供する第三者 (以下「第三者」といいます。) から提供される最新の適用可能な更新、パッチ、修正が適用された所定の稼働環境で 「プログラム」を実行してベンチマーク・テストを行い
(C) 「プログラム」の資料ならびにIBM がサポートする「プログラム」用の Web サイトで提供されているすべてのパフォーマンス・チューニングおよび最良の方法に従うこと
今回の計測対象のメインとなるJDBC のソースはこちらになります。INSERT のあとの SELECT なのでキャッシュ云々の話もありますが、それも含めて各データソースに対して同じ操作をしています。SQLite のみ Class.forName しているのはドライバがサービスプロバイダーフレームワークに対応していないためです。(bitbucket リポジトリの最新ソースでは 2012/09 対応)
package test;import java.sql.Connection;import java.sql.DriverManager;import java.sql.PreparedStatement;import java.sql.ResultSet;import java.sql.Statement;import org.junit.After;import org.junit.Test;publicclass JdbcTest {@Testpublicvoid sqlite()throws Exception { Class.forName("org.sqlite.JDBC"); con = DriverManager.getConnection("jdbc:sqlite:test.sqlite3"); Statement st = con.createStatement(); executeUpdate(st,"drop table if exists person"); executeUpdate(st,"create table person (id integer primary key, name string)"); executeQuery(); }@Testpublicvoid h2()throws Exception { con = DriverManager.getConnection("jdbc:h2:testh2","sa",""); Statement st = con.createStatement(); executeUpdate(st,"drop table if exists person"); executeUpdate(st,"create table person (id integer primary key, name varchar)"); executeQuery(); }@Testpublicvoid derby()throws Exception { con = DriverManager.getConnection("jdbc:derby:derby;create=true"); Statement st = con.createStatement(); executeUpdate(st,"drop table person"); executeUpdate(st,"create table person (id int primary key, name varchar(200))"); executeQuery(); }@Testpublicvoid mysql_myisam()throws Exception { mysql("MyISAM"); }@Testpublicvoid mysql_innodb()throws Exception { mysql("InnoDB"); }privatevoid mysql(String engine)throws Exception { con = DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root",""); Statement st = con.createStatement(); executeUpdate(st,"drop table if exists person"); executeUpdate(st,"create table person (id integer primary key, name varchar(200)) engine = " + engine); executeQuery(con.getMetaData().getDatabaseProductName() +"(" + engine +")"); }@Testpublicvoid postgres()throws Exception { con = DriverManager.getConnection("jdbc:postgresql:postgres","postgres","postgres"); Statement st = con.createStatement(); executeUpdate(st,"drop table if exists person"); executeUpdate(st,"create table person (id integer primary key, name varchar)"); executeQuery(); }@Testpublicvoid cassandra()throws Exception { con = DriverManager.getConnection("jdbc:cassandra://localhost:9160/test"); Statement st = con.createStatement(); executeUpdate(st,"drop table person"); executeUpdate(st,"create table person (id int primary key, name text)"); executeQuery(); }// 共通メンバー -------------------------------------------private Connection con;privatestaticfinalint COUNT =10000 *10;privatestaticfinal String DATA ="12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890";@Afterpublicvoid after()throws Exception {if (con !=null) { con.close(); } }privatevoid executeUpdate(Statement st, String sql) {try { st.executeUpdate(sql); }catch (Exception e) { System.out.println(e.toString()); } }privatevoid executeQuery()throws Exception { executeQuery(con.getMetaData().getDatabaseProductName()); }privatevoid executeQuery(String databaseName)throws Exception {boolean isCassandra = databaseName.contains("Cassandra");boolean isAutoCommit = isCassandra; System.out.printf("%-14s", databaseName);if (!isAutoCommit) { con.setAutoCommit(false); }long insertStart = System.currentTimeMillis(); PreparedStatement insertPs = con.prepareStatement("insert into person (id, name) values(?, '" + DATA +"')");for (int i =0; i < COUNT; i++) { insertPs.setInt(1, i); insertPs.executeUpdate();if (!isAutoCommit && i %10000 ==0) { con.commit(); } }if (!isAutoCommit) { con.commit(); }double insertSec = (double) (System.currentTimeMillis() - insertStart) /1000;long selectStart = System.currentTimeMillis(); PreparedStatement selectPs = con.prepareStatement("select * from person where id = ?");for (int i =0; i < COUNT; i++) { selectPs.setInt(1, i); selectPs.executeQuery().next(); }double selectSec = (double) (System.currentTimeMillis() - selectStart) /1000; String countSql ="select count(1) from person";if (isCassandra) { countSql +=" limit 100000000"; } ResultSet rs = con.createStatement().executeQuery(countSql); rs.next(); logProcessTime(rs.getInt(1), insertSec, selectSec); }privatevoid logProcessTime(long count,double insertSec,double selectSec) { System.out.printf("%4d万件 ", count /10000); System.out.printf("%7.2f秒 ", insertSec); System.out.printf("%7.2f秒", selectSec); System.out.println(); }}
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。