[Database, Node.js] Node-oracledb v2 Query Methods and Fetch Tuning

原文はこちら。
https://blogs.oracle.com/opal/node-oracledb-v2-query-methods-and-fetch-tuning

Node.js node-oracledb v2 add-on for Oracle Databaseでは、より低レベルのデータ・アクセス・レイヤーを改訂して、スタンドアロン・プロジェクトODPI-Cとして仕立てました。これは、他のいくつかの言語APIによって再利用されています。
oracledb
https://www.npmjs.com/package/oracledb
Oracle Database Programming Interface for C (ODPI-C)
https://oracle.github.io/odpi/
ODPI-Cによって、node-oracledbの内部クエリ処理コードの一部を簡素化する理由と機会を手にしました。
要約すると、node-oracledbには、Oracle Databaseに対するクエリ実行する4つの方法があります。これらはバージョン1とバージョン2で同じです。
  • 直接フェッチ
    非ResultSetやqueryStream()を使わないデータ取得方法です。すべての行は1つの大きな配列で返され、maxRows(v2では無制限の配列サイズが許されます)に制限されています。
  • ResultSet getRow()
    すべての行が返されるまで各呼び出しで1つの行を戻します。
  • ResultSet getRows(numRows)
    すべての行が返されるまで各呼び出しの行のバッチを返します。
  • queryStream()
    すべての行が返されるまで行をストリームします。
node-oracledb v2での変更点は以下の通りです。
  • 無制限の行数をフェッチできるように拡張された直接フェッチをデフォルトにしました。 これはmaxRows=0の場合の挙動です。
  • 新しいプロパティfetchArraySizeprefetchRows(以前内部フェッチバッファリングとチューニングに使用していました)を置き換えました。デフォルトのサイズは100です。fetchArraySizeは、直接フェッチ、ResultSet getRow()およびqueryStream()に影響します。
  • getRows(numRows, ...) の内部フェッチバッファリングはnumRows値によってのみ調整できます。以前は、prefetchRowsも内部バッファリングに影響する可能性がありました。
  • queryStream()は、内部バッファのサイジングにfetchArraySizeを使用します。
  • パフォーマンスを向上させるためにJavaScriptでgetRow()が実装され、内部バッファサイズのチューニングのためにfetchArraySizeを使用します。
ここで説明したいv2の変更は、「直接フェッチ」における内部バッファリング方法です。

直接フェッチで無制限の行数をフェッチできるように、データをfetchArraySizeサイズのバッチでOracle Databaseから内部的にフェッチし、その後連結されてコールバックで戻される大きな結果配列を形成します。これゆえ、fetchArraySizeを使用してフェッチのパフォーマンスをチューニングできます。node-oracledb v1では、データベースからデータをフェッチする前に、maxRowsサイズの1つの大きな配列が割り当てられていました(node-oracledb v2では、本当に必要ならば、fetchArraySize=maxRowsmaxRows>0と設定して同じ挙動を実現できます)。
fetchArraySizeが100(デフォルト)と1000の場合に50000行をフェッチするという2つの異なるシナリオを見てみましょう。コードはこのエントリの最後にあります。
Direct fetch:        rows: 50000, batch size: 100, seconds: 4.054

Direct fetch:        rows: 50000, batch size: 1000, seconds: 1.643
この場合(ローカルデータベース使用時)fetchArraySizeを増やすとパフォーマンスが向上することがわかります。これにはさまざまな要因があるかもしれませんが、共通するのは、レコードのバッチを取得するための「ラウンドトリップ」が少ないため、ネットワークコストの削減とデータベースの負荷の削減です。各クエリと環境は異なるため、独自のチューニングが必要です。
直接フェッチにfetchArraySizeを使用する利点は次のとおりです。
  • データのバッチ処理とネットワーク転送のパフォーマンスを調整できます。
  • クエリ結果の行数が不明であるか、または実行ごとに変化する場合、使用メモリは徐々に増加します。(v1でのmaxRowsに基づく)1つの大きなメモリチャンクを、「最悪の場合」を考慮して多数の行を処理するために事前に割り当てる必要はありません。
直接フェッチには2つの欠点があります。
  • 結果を格納する1つの大きな配列が必要です。 これはv1とv2で同じです。
  • レコードのバッチを連結すると、最終結果の配列より多くのメモリーを使用する可能性があり、断片化を引き起こす可能性があります。
すべてのクエリメソッドのタイミングを見てみましょう。これは1回の実行です。 スクリプトを実行するたびに変動が予想されました。「バッチサイズ」の数値は、getRows(numRows)呼び出し時はnumRows、それ以外のfetchメソッドの場合はfetchArraySizeです。
Direct fetch:        rows: 50000, batch size: 100, seconds: 4.054
ResultSet getRow():  rows: 50000, batch size: 100, seconds: 1.625
ResultSet getRows(): rows: 50000, batch size: 100, seconds: 1.586
queryStream():       rows: 50000, batch size: 100, seconds: 1.691

Direct fetch:        rows: 50000, batch size: 1000, seconds: 1.643
ResultSet getRow():  rows: 50000, batch size: 1000, seconds: 1.471
ResultSet getRows(): rows: 50000, batch size: 1000, seconds: 1.327
queryStream():       rows: 50000, batch size: 1000, seconds: 1.415
ResultSetとqueryStream()メソッドは、一度にすべての行をメモリに格納する必要がないため、メモリ管理が少なくて済みます。明らかに最初の結果が外れ値です。メモリの小さなまとまりを連結するというメモリ管理の結果大きくなっています。これを少し改善するためにnode-oracledbでできることについていくつかアイデアがありますが、これらのアイデアは将来のプロジェクトで調査すべきものであり、最終的にすべての行を同時にメモリに保持しなければならないという同じメモリに保持しなければならないという最初の欠点を解決できるものではありません。
結論として、ResultSetを使用するか、多数の行を扱う場合にはストリーミングを使うことが推奨されます。これは私たちがv1でお知らせしたものと同じです。
少数の行の場合、さまざまなクエリメソッドはほとんど同じように機能します。 タイミングは非常に短いので、以下に示す実行結果の違いをノイズとして扱うことができます。ここで各クエリは1行だけを返しました。
Direct fetch:        rows: 1, batch size: 100, seconds: 0.011
ResultSet getRow():  rows: 1, batch size: 100, seconds: 0.012
ResultSet getRows(): rows: 1, batch size: 100, seconds: 0.013
queryStream():       rows: 1, batch size: 100, seconds: 0.013

Direct fetch:        rows: 1, batch size: 1, seconds: 0.012
ResultSet getRow():  rows: 1, batch size: 1, seconds: 0.012
ResultSet getRows(): rows: 1, batch size: 1, seconds: 0.013
queryStream():       rows: 1, batch size: 1, seconds: 0.013
あまりないかもしれませんが、特に、行数がわかっている場合(1行など)、少数の行をクエリする場合は、小さなfetchArraySizeまたはnumRowsを使用することをお勧めします。これにより、node-oracledb、Oracleクライアント・ライブラリ、およびデータベース全体に割り振られる必要のあるメモリ量が少なくて済みます。

References

node-oracledbのドキュメントは以下にあります。
node-oracledb 2.0 Documentation for the Oracle Database Node.js Add-on
https://github.com/oracle/node-oracledb/blob/master/doc/api.md
If you are currently using node-oracledb v1を現在お使いの場合、移行のためのドキュメントに関心をお持ちかもしれません。
Migrating from node-oracledb 1.13 to node-oracledb 2.0
https://github.com/oracle/node-oracledb/blob/master/doc/api.md#migratev1v2

Code

以下は筆者が使用した基本的なテストスクリプトがあります。ResultSetのコードは、以下のv1のサンプルに由来しています。
An Overview of Result Sets in the Node.js Driver
https://jsao.io/2015/07/an-overview-of-result-sets-in-the-nodejs-driver/
config.jsファイルは最後にあります。dbconfig.jsファイルは、以下のURLにある例と同じです。
node-oracledb/examples/dbconfig.js
https://github.com/oracle/node-oracledb/blob/master/examples/dbconfig.js
タイミングには、DB内の文の実行が含まれますが、これはnode-oracledbによって制御されません。直接フェッチの場合、クエリ実行コストとfetchArraySizeが制御するデータフェッチコストを区別する方法はJavaScriptにはありません。

Direct Fetch

// direct fetch

var oracledb = require('oracledb');
var dbConfig = require('./dbconfig.js');
var config = require('./config.js');

oracledb.getConnection(
  dbConfig,
  function(err, connection) {
    if (err) throw err;

    var rowsProcessed = 0;
    var startTime = Date.now();

    connection.execute(
      'select * from all_objects where rownum <= :mr',
      [ config.maxRows ],
      { fetchArraySize: config.batchSize },
      function(err, results) {
        if (err) throw err;

        rowsProcessed = results.rows.length;

        // do work on the rows here

        var t = ((Date.now() - startTime)/1000);
        console.log('Direct fetch:        rows: ' + rowsProcessed +
                    ', batch size: ' + config.batchSize + ', seconds: ' + t);
         
        connection.release(function(err) {
          if (err) console.error(err.message);
        });
      });
  });

ResultSet getRow()

// ResultSet getRow()

var oracledb = require('oracledb');
var dbConfig = require('./dbconfig.js');
var config = require('./config.js');

oracledb.getConnection(
  dbConfig,
  function(err, connection) {
    if (err) throw err;

    var rowsProcessed = 0;
    var startTime = Date.now();

    connection.execute(
      'select * from all_objects where rownum <= :mr',
      [ config.maxRows ],
      {
        resultSet: true,
        fetchArraySize: config.batchSize
      },
      function(err, results) {
        if (err) throw err;

        function processResultSet() {
          results.resultSet.getRow(function(err, row) {
            if (err) throw err;
            if (row) {
              rowsProcessed++;

              // do work on the row here

              processResultSet(); // try to get another row from the result set
              return; // exit recursive function prior to closing result set
            }

            var t = ((Date.now() - startTime)/1000);
            console.log('ResultSet getRow():  rows: ' + rowsProcessed +
                        ', batch size: ' + config.batchSize + ', seconds: ' + t);
             
            results.resultSet.close(function(err) {
              if (err) console.error(err.message);

              connection.release(function(err) {
                if (err) console.error(err.message);
              });
            });
          });
        }

        processResultSet();
      }
    );
  }
);

ResultSet getRows()

// ResultSet getRows()

var oracledb = require('oracledb');
var dbConfig = require('./dbconfig.js');
var config = require('./config.js');

oracledb.getConnection(
  dbConfig,
  function(err, connection) {
    if (err) throw err;

    var rowsProcessed = 0;
    var startTime = Date.now();

    oracledb.fetchArraySize = 1;
    connection.execute(
      'select * from all_objects where rownum <= :mr',
      [ config.maxRows ],
      { resultSet: true },
      function(err, results) {
        var rowsProcessed = 0;

        if (err) throw err;

        function processResultSet() {
          results.resultSet.getRows(config.batchSize, function(err, rows) {
            if (err) throw err;

            if (rows.length) {
              rows.forEach(function(row) {
                rowsProcessed++;

                // do work on the row here
              });

              processResultSet(); // try to get more rows from the result set

              return; // exit recursive function prior to closing result set
            }

            var t = ((Date.now() - startTime)/1000);
            console.log('ResultSet getRows(): rows: ' + rowsProcessed +
                        ', batch size: ' + config.batchSize + ', seconds: ' + t);
             
            results.resultSet.close(function(err) {
              if (err) console.error(err.message);

              connection.release(function(err) {
                if (err) console.error(err.message);
              });
            });
          });
        }
        processResultSet();
      });
  });

queryStream()

// queryStream()

var oracledb = require('oracledb');
var dbConfig = require('./dbconfig.js');
var config = require('./config.js');

oracledb.getConnection(
  dbConfig,
  function(err, connection) {
    if (err) throw err;

    var rowsProcessed = 0;
    var startTime = Date.now();

    var stream = connection.queryStream(
      'select * from all_objects where rownum <= :mr',
      [ config.maxRows ],
      { fetchArraySize: config.batchSize }
    );

    stream.on('data', function (data) {
      // do work on the row here
      rowsProcessed++;
    });

    stream.on('end', function () {
      var t = ((Date.now() - startTime)/1000);
      console.log('queryStream():       rows: ' + rowsProcessed +
                  ', batch size: ' + config.batchSize + ', seconds: ' + t);
       
      connection.close(
        function(err) {
          if (err) { console.error(err.message); }
        });
    });
  }); 

The Configuration File

// config.js

var maxRows;      // number of rows to query
var batchSize;    // batch size for fetching rows

maxRows = 50000;
batchSize = 1000

exports.maxRows = maxRows;
exports.batchSize = batchSize; 

0 件のコメント:

コメントを投稿