下記のようなシステムでパフォーマンスが良さげな SQLite を使用予定ですが、もっと速いものが無いか確認のため他のデータベースのパフォーマンスを計測してみました。SQL 利用前提ですが、NoSQL が圧倒的な性能を出す場合は検討する必要があるので KVS も確認しました。
- データ件数は 1 億件程度、JDBC SQL 利用可能
- INSERT、UPDATE はバッチ
- SELECT は主キーアクセス性能を重視
- 将来スケールアウトのための分散はありえるが、スタンドアロンで遅いのはだめ
データベースのパフォーマンス比較
計測したデータベース
データベース名 | タイプ | 形態 | 評判 | 計測についての備考 |
---|---|---|---|---|
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 | 爆速 | Jedis API 操作 |
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 |
※1 JDBC 操作に関して INSERT は 1 万件ごとにコミット、SELECT は主キー指定で 1 件ずつ全件取得
※2 組み込みモード (ローカルファイル永続化) のみ計測、インメモリモードやサーバーモードは未計測
※3 全 INSERT 後の ensureIndex によるインデックス作成時間が計測結果に含まれる
※4 遅延書き込み (非同期書き込み) *1
計測結果
環境 CPU Core2 Duo 2GH 2GHz*2、メモリ 4GB、Windows XP 32bit*3、HDD (TOSHIBA MK8052GSX)*4、しょぼめのノートパソコン
データ 主キー:数値、値:文字列、レコード長:約 200 byte
スレッド数 1
線形的増加と指数関数的増加、臨界点
データベースの処理数に対する処理時間は、上記結果の MySQL に見られるような線形的増加 (リニア、直線的) に増加するパターンと、Cassandra のように指数関数的 (雪ダルマ式) に増大するパターンがあります。また、線形的増加から指数関数的増加に移行する臨界点や動作不能になる臨界点がある場合が多いです。1 万件での処理時間はこうだから 1 億件の予想処理時間はその 1 万倍というような情報をたまに見かけますが、件数が増大するほど線形的増加ではないほうが多いと思います。
SQLite が予想以上に他を圧倒し高速
![f:id:cypher256:20121013205423g:image:w190:right f:id:cypher256:20121013205423g:image:w190:right](https://cdn-ak.f.st-hatena.com/images/fotolife/c/cypher256/20121013/20121013205423.gif)
SSD で試してみたところ、1億件 INSERT 647秒、1億回 SELECT 383秒 でした。さすがに速い。SSD 環境は CPU i5 2.4GH、メモリ 8GB、Windows 7 64bit*5、SSD (TOSHIBA THNSNC128GMMJ)*6、これもノートパソコンです。
件数が増えると EHCache が想定以上に遅い
EHCache は 1 万件取得で最速ですが、件数が増えると組み込み RDB の SQLite や H2 より遅いのは良い?にしても、クライアント・サーバー型 KVS の Mongodb や Redis より総合的に少し遅いのは予想外でした。overflowToDisk の設定にもよると思いますが、知らずに H2 や Mongodb のキャッシュとして EHCache を使ってまいそうです。すべてディスクに永続化するように設定していたのですが、100万件テストでは登録したデータを取得しようとしたときに欠落している場合があるため計測しませんでした。ところでロゴを見て気づいたのですが EHCache が回文になっているのを今初めて知りました。
Mongodb がクライアント・サーバー型としては登録性能に優れている
![f:id:cypher256:20121013205425p:image:w220:right f:id:cypher256:20121013205425p:image:w220:right](https://cdn-ak.f.st-hatena.com/images/fotolife/c/cypher256/20121013/20121013205425.png)
NoSQL、KVS が終焉
![f:id:cypher256:20121013205427p:image:w190:right f:id:cypher256:20121013205427p:image:w190:right](https://cdn-ak.f.st-hatena.com/images/fotolife/c/cypher256/20121013/20121013205427.png)
業務システムから見ると SQL はそのままでスケールアウトしたいという要望に単純な NoSQL や KVS は合致しません。元々 RDB がスケールアウトによりリニアにスケールしない問題や耐障害性を解決するために Google の BigTable などにインスパイアされ登場したのが分散 KVS ですが、現在は RDB に NoSQL や KVS の要素が取り込まれています。例えば、MySQL 5.6 では memcached や HandlerSoclet などの概念が取り込まれ SQL なしで高速アクセス可能になっています。また MySQL Cluster や Oracle Exadata は分散 KVS と同じようにリニアにスケールアウト可能になっており、Postgres も32コア CPU 対応などスケールアップ可能になっています。
追加 2012/10/19
計測結果についての補足
なお、ソフトウェアやドライバはすべて最新安定版、設定はデフォルトです。ただし、全データのディスク同期が前提の計測であるため、EHCache はインメモリではなくディスク永続化モードです。ちなみに SQLite はもう少し触ってみたところ Beta 版にすると 20% 高速化、非同期モードにするとさらに 10% 高速化しました。
計測になぜ Oracle が含まれてないの?
計測ソース*8
今回の計測対象のメインとなる 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; public class JdbcTest { @Test public void 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(); } @Test public void 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(); } @Test public void 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(); } @Test public void mysql_myisam() throws Exception { mysql("MyISAM"); } @Test public void mysql_innodb() throws Exception { mysql("InnoDB"); } private void 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 + ")"); } @Test public void 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(); } @Test public void 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; private static final int COUNT = 10000 * 10; private static final String DATA = "12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890"; @After public void after() throws Exception { if (con != null) { con.close(); } } private void executeUpdate(Statement st, String sql) { try { st.executeUpdate(sql); } catch (Exception e) { System.out.println(e.toString()); } } private void executeQuery() throws Exception { executeQuery(con.getMetaData().getDatabaseProductName()); } private void 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); } private void 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(); } }