GridDB v5.5新機能: SQLバッチ

GridDB v5.5のリリースに伴い、結合を使用したSQL検索のパフォーマンス、パーティションド・インデックスの最適化、SQLのバッチ更新が更新されました。これらの新機能はGridDBサーバのパフォーマンスを向上させるためのものでありますが、特にバッチ更新は、GridDBコンテナに対してSQLによる更新を一括して提供する機能であり、GridDBアプリケーションにおけるいくつかの最適化のための明確な道筋を開くものです。本記事では、簡単なベンチマークのレンズを通して、SQL更新をバッチで実行する方法を紹介し、これらのコマンドをバッチで実行することで、どの程度速くなるか(あるいは速くならないか!)を確認します。そして最後に、一般的にGridDBではフードの最適化によってより高速に動作するNoSQL APIとの比較も行います。また、その他の機能についても軽く説明します。

はじめに (前提条件)

gitリポジトリはこちらからcloneできます: GitHub

$ git clone -b sql_batch https://github.com/griddbnet/Blogs.git

そして、mvn installを実行し、出来上がった.jarファイルを次のように実行します: java -jar target/SqlBatch.jar`。

あるいは、このシンプルなプロジェクトをdockerで実行することもできます。

$ docker build -t sql-batch .
$ docker run sql-batch --network=host 

注意:上記のコマンドは、既にホストマシン上でGridDBサーバ(v5.5にアップデート済み)をデフォルトの設定で実行していることを前提としています。DockerコンテナでGridDBを実行したい場合は、このプロジェクトの実行方法を調整する必要があります。

その後、気軽にGridDB CLIに立ち寄って、操作結果も確認してみてください。

SQL コストベース最適化

v5.5リリース以前は、SQL結合を必要とするSQLクエリは、そのクエリを作成するユーザ/開発者のみが最適化する必要がありました。例えば、クエリを作成する場合、クエリを作成する順序は、検索結果を得るために結合されるテーブルの順序になります。しかし、v5.5のリリースでは、コストベースの最適化という新しい設定が追加された(デフォルトではtrueに設定されているが、gs_cluster.jsonファイルでオフにすることができる)。注: この機能をオフにしたい場合は、クラスタ設定ファイルで /sql/costBasedJoin を false に設定する。コストベース結合をfalseに設定すると、代わりにルールベース結合と呼ばれるものが使用されます。

コストベースの方式は、すべての最適化を自動的に行うので、最適化についてすぐに知る必要はありません。従来のルールベースの方法では、最適化を開発者側で行う必要があり、開発者はテーブル結合を最適化する最善の方法を直接知る必要がありました。

コストベースの方法は、どのクエリの結果がより多くの行を持つかを常に把握し、それに応じて調整します。例として、次のようなクエリがあったとします:

FROM A, C, B WHERE A.x>=C.z AND C.z>=B.y AND B.y=A.x` とします。

書かれた順番(ルールベース)に結合するのではなく、AとBの関係が高いことを確認し、AとBを結合し、その結果をCと結合します。

最適化のもう一つの方法は、どのテーブルがよりフィルタリングされているか(つまり、結果の行数が少ないか)を見つけ、それらを最初に結合することです。別の例を挙げましょう:

FROM A, B, C WHERE A.x=C.z AND C.z=B.y AND A.x IN (1, 2) AND B.x IN (1, 2, 3)`.

ここで、テーブルAはより多くのフィルタリングを行い、より狭い結果になります。

ヒント

コストベースの最適化設定はデフォルトでオンに設定されていますが、クエリを最適化する別の方法があります。この設定をオンのままにしておくのではなく、オフにして hint システムを使用することもできます(注意:コストベースの最適化が true に設定されている場合でもヒントを使用することができます)。

ヒントシステムでは、クエリに特別なマーカーを設定することで、クエリの実行方法のヒントをシステムに与えることができます。そのためには、SELECT文の前か後にコメント(特別な予約文字 /*+ で表示されます)を設定する必要があります:

/*+
Leading(t3 t2 t1)
 */
SELECT *
  FROM t1, t2, t3
    ON t1.x = t2.y and t2.y = t3.z
  ORDER BY t1.x
  LIMIT 10;

この例では、ヒント Leading はテーブル結合の順番をクエリに伝えます。この他にも利用できるヒントはたくさんあります。

テーブルの分割に関連するインデックス結合の最適化

バージョン5.5以前では、テーブルのパーティションが多すぎる場合、インデックス結合が行われないことがありました。この更新により、テーブルに多くのパーティションや異なるタイプがある場合でも、インデックス結合を実行する計画は期待通りに実行されるようになりました。

SQL インサートのベンチマーク

それでは、SQLバッチ操作のベンチマークを始めましょう。

まず、SQL によるデータ行の挿入にどれくらいの時間がかかるかを確認するため、GridDB’s JDBC Connector を使用して、簡単な Java プログラムから SQL 呼び出しを行います。まず SQL 単発挿入(一度に 1 行)、次に SQL バッチ更新/挿入(一度に 1,000 行)、次に NoSQL(一度に 1 行)、最後に NoSQL MultiPut で 10,000 行の blob データを挿入します。順番としては、JDBC経由でGridDBサーバーに接続し、適切なテーブル/コンテナをドロップ/作成し、データ行を挿入し、最後に各操作にかかった時間を出力します。

JDBC経由でGridDBに接続します。

このパートでは、GridDB JDBC コネクタをインストールする必要がある。このプロジェクトでmavenを使用しているのであれば、依存関係としてリストされているはずだ(もちろん、GridDB Serverと一緒に):

 <dependency>
            <groupId>com.github.griddb</groupId>
            <artifactId>gridstore-jdbc</artifactId>
            <version>5.5.0</version>
        </dependency>

JDBCでは、適切なJDBC URLで接続を行う。以下は、Javaでそれを構築するコードです:

           String notificationMember = "127.0.0.1:20001";
            String clusterName = "myCluster";
            String databaseName = "public";
            String username = "admin";
            String password = "admin";
            String applicationName = "SampleJDBC";

            String encodeClusterName = URLEncoder.encode(clusterName, "UTF-8");
            String encodeDatabaseName = URLEncoder.encode(databaseName, "UTF-8");

            String jdbcUrl = "jdbc:gs://" + notificationMember + "/" + encodeClusterName + "/" + encodeDatabaseName;

            Properties prop = new Properties();
            prop.setProperty("user", username);
            prop.setProperty("password", password);
            prop.setProperty("applicationName", applicationName);

            Connection con = DriverManager.getConnection(jdbcUrl, prop);
            Statement stmt = con.createStatement();

ここでは、ローカルマシンにデフォルトのGridDBサーバーがインストールされているものとします。リモートサーバやdockerを使用している場合は、notificationMemberが異なるかもしれません。

これで stmt 変数が作成され、GridDB サーバに SQL 文を発行できるようになりました。

テーブルの作成

次に、テーブルを作成する。カラムはrowkey(id)とdata(datatype blob)の2つだけにします。

SQL文を作成しましょう。

           stmt.executeUpdate("DROP TABLE IF EXISTS SQL_Single_Input");
            stmt.executeUpdate("CREATE TABLE SQL_Single_Input ( id integer PRIMARY KEY, data blob )");
            PreparedStatement pstmt = con.prepareStatement("INSERT INTO SQL_Single_Input(id, data) VALUES(?, ?)");

            stmt.executeUpdate("DROP TABLE IF EXISTS SQL_Batch_Input");
            stmt.executeUpdate("CREATE TABLE SQL_Batch_Input ( id integer PRIMARY KEY, data blob )");
            PreparedStatement pstmtBatch = con.prepareStatement("INSERT INTO SQL_Batch_Input(id, data) VALUES(?, ?)");

            stmt.executeUpdate("DROP TABLE IF EXISTS SampleNoSQL_BlobData");
            stmt.executeUpdate("CREATE TABLE SampleNoSQL_BlobData ( id integer PRIMARY KEY, data blob )");

これらを実行すると、テーブルは一旦削除され、再度作成され、データが挿入されるのを待ちます。

データの挿入

ブロブデータは、すべての挿入において同じです。

           byte[] b = new byte[1000];
            new Random().nextBytes(b);
            SerialBlob serialBlob = new SerialBlob(b);

では、データを挿入してみましょう:

// SQL Single Update
            Long startTime = System.nanoTime();
            for (int i = 1; i <= 10000; i++) {
                pstmt.setInt(1, i);
                pstmt.setBlob(2, serialBlob);
                pstmt.executeUpdate();
            }
            Long endTime = System.nanoTime();
            Long duration = (endTime - startTime)/ 1000000; // milliseconds
            System.out.println("singular SQL took: " + Long.toString(duration) + " milliseconds");
            pstmt.close();

そして結果が出ました: 5462ミリ秒

バッチ更新がもっとうまくいくか試してみましょう。

// SQL Batch Update
            startTime = System.nanoTime();
            for (int i = 1; i <= 10000; i++) {
                pstmtBatch.setInt(1, i);
                pstmtBatch.setBlob(2, serialBlob);
                pstmtBatch.addBatch();

                if (i % 1000 == 0) {
                    @SuppressWarnings("unused")
                    int[] cnt = pstmtBatch.executeBatch();
                }
                
            }
            endTime = System.nanoTime();
            duration = (endTime - startTime)/ 1000000;
            System.out.println("add batch SQL took: " + Long.toString(duration) + " milliseconds");

このコードでは、1,000行のデータを10回挿入しています。その結果、add batch SQL took: 4116ミリ秒。これは約33%のスピードアップになります。開発者側から見て、いかに簡単に実装できるかを考えれば、悪ありません。

NoSQL でのデータ挿入

最後に、同じ基本的な操作をNoSQL側から試してみましょう。まず、GridDBサーバーと新しい接続を行う必要があります。

            // NoSQL.java
            Properties props = new Properties();
            props.setProperty("notificationMember", "127.0.0.1:10001");
            props.setProperty("clusterName", "myCluster");
            props.setProperty("user", "admin"); 
            props.setProperty("password", "admin");
            store = GridStoreFactory.getInstance().getGridStore(props);

ポート番号は20001から10001に変更され、接続を行うために必要な詳細情報が少し減りました。

接続が完了したら、データを作成し、行を挿入することができます。

    // NoSQL.java
    protected  void runSinglePut(SerialBlob blob) throws GSException, SerialException, SQLException {
        Collection<String, Row> col = store.getCollection("SampleNoSQL_BlobData");  
        col.setAutoCommit(true);
 
        for (int i = 1; i <= 10000; i++) {
            Row row;
            row = col.createRow();
            row.setInteger(0, i);
            row.setBlob(1, blob);
            col.put(row);
        }
    }

    //main()
    NoSQL noSQL = new NoSQL();

    startTime = System.nanoTime();
    noSQL.runSinglePut(serialBlob);
    endTime = System.nanoTime();
    duration = (endTime - startTime) / 1000000;
    System.out.println("NoSQL took: " + Long.toString(duration) + " milliseconds");

最後に、Javaでマルチアウトプットを試してみましょう:

   protected  void runMulti (SerialBlob blob) throws GSException, SerialException, SQLException {
        final Map<String, List<row>> rowListMap = new HashMap<String, List</row><row>>();
        Collection<String, Row> col = store.getCollection("MultiPutNoSQL_Blobdata");    
        col.setAutoCommit(true);

        List</row><row> rows = new ArrayList<>();
        for (int i = 1; i <= 10000; i++) {
            Row row = col.createRow();
            row.setInteger(0, i);
            row.setBlob(1, blob);
            rows.add(row);
        }
        rowListMap.put("MultiPutNoSQL_Blobdata", rows);
        store.multiPut(rowListMap);
    }</row>

そして結果は Multi Put took: 213ミリ秒`です。この結果は以前の試行よりはるかに速いですが、驚くべきことではありません。ここでは10,000行すべてを一度に挿入しており、NoSQLがすでにより高速なオペレーションを実行していることに加えて、全体的なスピードもはるかに高速になっています。

そして、ここにもすべての結果があります(結果を変えるために別の時間を実行しました):

singular SQL took: 5317 milliseconds
batch SQL took: 4353 milliseconds
NoSQL (Single Put) took: 941 milliseconds
NoSQL (multi Put) took: 213 milliseconds

結論

この記事では、GridDB の新しい SQL バッチ更新機能を紹介し、シングル SQL 入力、NoSQL シングル入力、NoSQL マルチ入力と直接比較しました。

ブログの内容について疑問や質問がある場合は Q&A サイトである Stack Overflow に質問を投稿しましょう。 GridDB 開発者やエンジニアから速やかな回答が得られるようにするためにも "griddb" タグをつけることをお忘れなく。 https://stackoverflow.com/questions/ask?tags=griddb

Leave a Reply

Your email address will not be published. Required fields are marked *