パフォーマンス比較

BraidDB, MySQL, PostgreSQLのパフォーマンス比較結果です。
全てのテストケースにおいてBraidDBが高速に動作しています。

テストケース 入力行数 出力行数 BraidDB MySQL PostgreSQL
Sort & Insert 100 万行 100 万行 3,701 ms 3,965 ms 9,216 ms
1,000 万行 1,000 万行 65,691 ms 114,130 ms 72,629 ms
1 億行 1 億行 1,003,712 ms 2,315,656 ms 2,384,929 ms
10 億行 10 億行 9,769,004 ms 36,259,928 ms 41,455,775 ms
Group & Insert 100 万行 1 万行 867 ms 1,434 ms 4,991 ms
1,000 万行 1 万行 8,453 ms 14,552 ms 69,703 ms
1 億行 1 万行 84,836 ms 144,346 ms 1,359,495 ms
10 億行 1 万行 890,073 ms 1,435,636 ms 21,959,678 ms

※時間は同様のテストを10回計測したものの平均値です。

Sort & Insert

Group & Insert

Sort & Insertテスト

入力データを全てのカラムをソート条件としてソートし、結果をディスク(MySQL, PostgreSQLの場合はテーブル)へ出力するテストです。
入力データ行数と出力データ行数は必ず等しくなります。

Group & Insertテスト

入力データを4つのカラムをキーとして集計し、結果をディスク(MySQL, PostgreSQLの場合はテーブル)へ出力するテストです。
4つのカラムは各々0~9の10種類の値をとるため、出力データは必ず1万行(=104)になります。

グループイメージ

テストの詳細について

テストを行った環境と具体的なテストの内容について説明します。

一般的によく利用されているOracle, Microsoft SQL Server, DB2のライセンスには、パフォーマンス比較結果を公開してはならないという条項が存在します。そこで、上記の条項が存在しない、MySQLをパフォーマンス比較に用いることにしました。

上記のライセンス条項が存在する商用データベースのパフォーマンスはお客様自身の手で確認する必要がありますが、一般的にMySQLより高速である場合は滅多にありません。

テスト環境

BraidDBとMySQLとのパフォーマンス比較を目的としているため、単一サーバ環境を利用しました。
CPUには4つのCPUコアが実装されていますが、全てのテストにおいて1コアだけを利用しています。これは、BraidDBは複数コアを利用したクエリ処理が可能ですが、MySQLとPostgreSQLが1つのクエリに対して複数のコアを利用できないため、同じ条件でテストする必要があったからです。

共通 CPU Intel(R) Core(TM)2 Quad CPU Q6600 @ 2.40GHz stepping 0b
(※1CPUコアのみ利用)
Memory 3GB
HDD 1 SATA 1.5TB 7200rpm (ext3) @ mount on /sdb
HDD 2 SATA 1.5TB 7200rpm (ext3) @ mount on /sdc
OS CentOS release 5.4 / x86_64 (Redhat Enterprise Linux 5.4互換)
Java java version “1.6.0_20″
Java(TM) SE Runtime Environment (build 1.6.0_20-b02)
Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01, mixed mode)
BraidDB version BraidDB 20100508
outputdir /sdb/srv
java.io.tmpdir /sdc
MySQL version mysql-server-5.0.77-4.el5_4.2
sort_buffer_size 64 MB
datadir /sdc/srv/lib/mysql
tmpdir /sdb/tmp
PostgreSQL version postgresql-server-8.1.18-2.el5_4.1
work_mem 64 MB
effective_cache_size 1,200 MB
PGDATA /sdc/srv/lib/postgresql
TABLESPACE /sdb/postgresql

BraidDB テスト内容

各々のテストケース毎に以下のクエリを実行しました。
クエリ結果をJavaコードを用いてディスクへ出力するため、クエリ中に出力命令はあらわれません。
そして、Javaコードをカスタマイズすることにより、パフォーマンスを落とすこと無く以下のような事後処理が可能です。

  • クエリでは表現しにくい複雑なデータ変換を行う
  • CSVファイルとして出力する
  • 他のデータベースへデータを出力する
実行クエリ
Sort & Insert (100,000万行)

each文を使って、各々10行ずつデータを格納している9個のテーブル a, b, c, d, e, f, g, h, i の直積を得ることによって、100,000万行(=109)の入力データを生成しています。そして、order文を使って生成した入力データを全てのカラムをソート条件としてソートしています。

each
  {a in integer32, b in integer64, c in real32, d in real64, e in text, f in integer32, g in integer64, h in real32, i in real64,}
order
  (+a, -b, +c, -d, +e, -f, +g, -h, +i)
Group & Insert (100,000万行)

each文を使って、各々10行ずつデータを格納している9個のテーブル a, b, c, d, e, f, g, h, i の直積を得ることによって、100,000万行(=109)の入力データを生成しています。そして、group文を使って生成した入力データを4つのカラム a, b, c, d をキーとして集計しています。

each
  {a in integer32, b in integer64, c in real32, d in real64, e in text, f in integer32, g in integer64, h in real32, i in real64,}
group (a, b, c, d)
  {sum = sumInteger32(f), max = maximumInteger32(f), min = minimumInteger32(f), count = countInteger32(),}
スキーマとデータ
{
  Result == {a:Integer32, b:Integer64, c:Real32, d:Real64, e:Text, f:Integer32},
  integer32 = [0   , 3   , 4   , 7   , 8   , 9   , 6   , 5   , 2   , 1   ],
  integer64 = [0L  , 3L  , 4L  , 7L  , 8L  , 9L  , 6L  , 5L  , 2L  , 1L  ],
  real32    = [0.0 , 3.0 , 4.0 , 7.0 , 8.0 , 9.0 , 6.0 , 5.0 , 2.0 , 1.0 ],
  real64    = [0.0L, 3.0L, 4.0L, 7.0L, 8.0L, 9.0L, 6.0L, 5.0L, 2.0L, 1.0L],
  text      = ["0" , "3" , "4" , "7" , "8" , "9" , "6" , "5" , "2" , "1" ],
}
クエリを実行するためのJavaコード
public void doTest(String aSchemaText, String aQueryText) throws LexerException, ParserException {
  DDefaultValueEvaluator tEvaluator = DDefaultValueEvaluator.INSTANCE;
  EDefaultDecoder tDecoder = new EDefaultDecoder();
 
  // aSchemaTextを実行しスキーマをtContextへ代入
  ETerm tSchemaExpression = (ETerm) tDecoder.toTermNode(aSchemaText);
  DRowTerm tSchema = (DRowTerm) tEvaluator.evaluateValueForTerm(DInitialContext.INSTANCE, tSchemaExpression, new DWorkspace());
  DContext tContext = new DDefaultSelfRowTermValueContext(DInitialContext.INSTANCE, tSchema);
 
  // aQueryTextを実行し結果をtResultへ代入
  ETerm tQueryExpression = (ETerm) tDecoder.toTermNode(aQueryText);
  DCollectionTerm tResult = (DCollectionTerm) tEvaluator.evaluateValueForTerm(tContext, tQueryExpression, new DWorkspace());
 
  // tResultをディスクへ出力
  DEditableCollectionTerm tOutput = new DBinaryFileEditableCollectionTerm(new File("/sdb/srv/result"), tContext.getValueForType("Result"));
  tOutput.insertAllElements(tResult);
}

MySQL テスト内容

各々のテストケース毎に以下のクエリを実行しました。

実行クエリ
Sort & Insert (100,000万行)

select文のfrom句を使って、各々10行ずつデータを格納している9個のテーブル a, b, c, d, e, f, g, h, i の直積を得ることによって、100,000万行(=109)の入力データを生成しています。そして、order by句を使って生成した入力データを全てのカラムをソート条件としてソートしています。

INSERT INTO
 RESULT
SELECT
 a.col1 AS a, b.col1 AS b, c.col1 AS c, d.col1 AS d, e.col1 AS e, f.col1 AS f, g.col1 AS g, h.col1 AS h, i.col1 AS i
FROM
 integer32 a, integer64 b, real32 c, real64 d, text e, integer32 f, integer64 g, real32 h, real64 i
ORDER BY
 a ASC, b DESC, c ASC, d DESC, e ASC, f DESC, g ASC, h DESC, i ASC
Group & Insert (100,000万行)

select文のfrom句を使って、各々10行ずつデータを格納している9個のテーブル a, b, c, d, e, f, g, h, i の直積を得ることによって、100,000万行(=109)の入力データを生成しています。そして、group by句を使って生成した入力データを4つのカラム a, b, c, d をキーとして集計しています。

INSERT INTO
 RESULT
SELECT
 a.col1 AS a, b.col1 AS b, c.col1 AS c, d.col1 AS d, SUM(f.col1) AS SUM, MAX(f.col1) AS MAX, MIN(f.col1) AS MIN, COUNT(*) AS COUNT
FROM
 integer32 a, integer64 b, real32 c, real64 d, text e, integer32 f, integer64 g, real32 h, real64 i
GROUP BY
 a, b, c, d
スキーマとデータ
CREATE TABLE integer32 (`col1` INT         NOT NULL, PRIMARY KEY  (`col1`)) DEFAULT CHARSET=utf8;
CREATE TABLE integer64 (`col1` BIGINT      NOT NULL, PRIMARY KEY  (`col1`)) DEFAULT CHARSET=utf8;
CREATE TABLE real32    (`col1` FLOAT       NOT NULL, PRIMARY KEY  (`col1`)) DEFAULT CHARSET=utf8;
CREATE TABLE real64    (`col1` DOUBLE      NOT NULL, PRIMARY KEY  (`col1`)) DEFAULT CHARSET=utf8;
CREATE TABLE text      (`col1` VARCHAR(20) NOT NULL, PRIMARY KEY  (`col1`)) DEFAULT CHARSET=utf8;
 
INSERT INTO integer32 VALUES (0);
INSERT INTO integer32 VALUES (1);
INSERT INTO integer32 VALUES (2);
INSERT INTO integer32 VALUES (3);
INSERT INTO integer32 VALUES (4);
INSERT INTO integer32 VALUES (5);
INSERT INTO integer32 VALUES (6);
INSERT INTO integer32 VALUES (7);
INSERT INTO integer32 VALUES (8);
INSERT INTO integer32 VALUES (9);
 
INSERT INTO integer64 VALUES (0);
INSERT INTO integer64 VALUES (1);
INSERT INTO integer64 VALUES (2);
INSERT INTO integer64 VALUES (3);
INSERT INTO integer64 VALUES (4);
INSERT INTO integer64 VALUES (5);
INSERT INTO integer64 VALUES (6);
INSERT INTO integer64 VALUES (7);
INSERT INTO integer64 VALUES (8);
INSERT INTO integer64 VALUES (9);
 
INSERT INTO real32 VALUES (0);
INSERT INTO real32 VALUES (3);
INSERT INTO real32 VALUES (4);
INSERT INTO real32 VALUES (7);
INSERT INTO real32 VALUES (8);
INSERT INTO real32 VALUES (9);
INSERT INTO real32 VALUES (6);
INSERT INTO real32 VALUES (5);
INSERT INTO real32 VALUES (2);
INSERT INTO real32 VALUES (1);
 
INSERT INTO real64 VALUES (0);
INSERT INTO real64 VALUES (3);
INSERT INTO real64 VALUES (4);
INSERT INTO real64 VALUES (7);
INSERT INTO real64 VALUES (8);
INSERT INTO real64 VALUES (9);
INSERT INTO real64 VALUES (6);
INSERT INTO real64 VALUES (5);
INSERT INTO real64 VALUES (2);
INSERT INTO real64 VALUES (1);
 
INSERT INTO text VALUES ('0');
INSERT INTO text VALUES ('3');
INSERT INTO text VALUES ('4');
INSERT INTO text VALUES ('7');
INSERT INTO text VALUES ('8');
INSERT INTO text VALUES ('9');
INSERT INTO text VALUES ('6');
INSERT INTO text VALUES ('5');
INSERT INTO text VALUES ('2');
INSERT INTO text VALUES ('1');
クエリを実行するためのJavaコード
public void doTest(String aSchemaText, String aQueryText) throws SQLException, ClassNotFoundException {
  // DBへコネクションを確立
  Class.forName("com.mysql.jdbc.Driver");
  Connection tConnection = DriverManager.getConnection("jdbc:mysql://localhost/performance?useUnicode=true&characterEncoding=utf8", "root", "root");
  try {
    Statement tStatement = tConnection.createStatement();
    try {
      // スキーマを作成
      tStatement.execute(aSchemaText);
 
      // クエリを実行
      tStatement.executeUpdate(aQueryText);
    } finally {
      tStatement.close();
    }
  } finally {
    tConnection.close();
  }
}

上記のJavaコードのとおり、クエリ結果はinsert into文を用いてディスクへ出力されるため、クエリ結果を事後処理することができません。
仮に、以下のようなJavaコードを用いてクエリ結果を事後処理する場合、クエリ結果が100万件程度であれば1GBのヒープサイズで処理可能です。

38
39
40
41
42
43
44
45
46
47
48
49
50
51
ResultSet tResultSet = tStatement.executeQuery(tQueryString);
try {
  while (tResultSet.next()) {
    tDataOutput.writeInt(tResultSet.getInt(1)); // a
    tDataOutput.writeLong(tResultSet.getLong(2)); // b
    tDataOutput.writeFloat(tResultSet.getFloat(3)); // c
    tDataOutput.writeDouble(tResultSet.getDouble(4)); // d
    tDataOutput.writeChars(tResultSet.getString(5)); // e
    tDataOutput.writeDouble(tResultSet.getInt(6)); // f
    tCount++;
  }
} finally {
  tResultSet.close();
}

しかし、クエリ結果が1,000万件となるような場合では10GB以上のヒープサイズが必要になるため、下記のようなOutOfMemoryErrorが発生します。

[Full GC 943443K->1902K(1004928K), 0.2341340 secs]
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
        at com.mysql.jdbc.MysqlIO.nextRowFast(MysqlIO.java:1633)
        at com.mysql.jdbc.MysqlIO.nextRow(MysqlIO.java:1410)
        at com.mysql.jdbc.MysqlIO.readSingleRowSet(MysqlIO.java:2887)
        at com.mysql.jdbc.MysqlIO.getResultSet(MysqlIO.java:477)
        at com.mysql.jdbc.MysqlIO.readResultsForQueryOrUpdate(MysqlIO.java:2582)
        at com.mysql.jdbc.MysqlIO.readAllResults(MysqlIO.java:1758)
        at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2172)
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2690)
        at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2619)
        at com.mysql.jdbc.StatementImpl.executeQuery(StatementImpl.java:1465)
        at jp.kurusugawa.braid.performance.data.MysqlDataInsert10MTest.main(MysqlDataInsert10MTest.java:38)

従って、MySQLでは大量のクエリ結果に対するJavaコードでの事後処理はほぼ不可能です。

参考URL


ホーム | RSS | 採用情報 | 会社情報