目次


Cloudant 内でのクエリーを最適化する

Comments

最近のアプリケーションには、従来にも増してデータの柔軟性が必要です。ここ数年にわたり、従来型のリレーショナル・データベースに欠けている部分を補う各種の NoSQL データベースが登場しています。非構造化データと半構造化データに関するアプリケーション要件をサポートするには、NoSQL データベースのより柔軟なデータ・モデルのほうが優れています。一種の NoSQL DB 製品としての IBM Cloudant では、Web アプリケーションとモバイル・アプリケーションを対象に、完全に管理されたデータベース・サービスを提供し、高度なインデックス作成手法、カスタム・ビュー、全文検索、そしてリアルタイム・クエリーなどの豊富な機能を使用できるようになっています。この記事では、皆さんが Cloudant NoSQL DB を運用する際にクエリーを最適化できるよう、さまざまな視点から見た最適化手法と最適化に関する経験を紹介します。この記事に記載する情報は、適用する各シナリオに最も適したクエリーについて理解を深め、アプリケーションに提供するデータ・クエリー・サービスの効率を最大限に高める上で役立つはずです。

Cloudant の紹介

Cloudant は、Apache CouchDB の商用リリースであり、Web アプリケーションとモバイル・アプリケーションを対象とした IBM の完全な運用・保守・データ管理プラットフォームです。Cloudant はクラウド上に構築された NoSQL データベースであることから、急増する Web およびモバイル・アプリケーションに理想的なプラットフォームとなっています。Cloudant を使用すると、クラウドの可用性、回復力、スケーラビリティーを利用して、アプリケーションをさらに高度なレベルに拡張できると同時に、データの可用性、耐久性、移動性が高まるように最適化できます。Cloudant はその特長である強力なインデックス作成機能や、複数のデータ・センターやデバイスにわたってネットワーク・エッジまでデータをプッシュする機能により、アクセスの高速化とフォールト・トレランスの向上を実現します。ユーザーは、いつでも、どこでもデータにアクセスできます。

他のデータベース製品と比べ、Cloudant は以下の点で優れています。

  • コピーの配布と複製をサポートします。データベースに対する読み取り/書き込みアクセスをクラウド内の各ノードに分散することにより、Cloudant はクラスターの規模を問わずに稼働し、データのセキュリティーと一貫性を確保します。
  • レプリカの間では継続的にデータが同期されます。Cloudant では複数のレプリカの同期およびリアルタイムの自動同期をサポートしています。1 つのコピーに含まれるデータを継続的に更新し、その更新を関連する他のコピーに自動的に同期させることができます。
  • クエリーを最適化するために、インデックスやビューをはじめとするさまざまな手法を使用できます。ビューのサポートに関しては、Cloudant はユーザー定義のビューもサポートします。Cloudant で使用する言語は JavaScript です。
  • JSON ドキュメントの分析とウェアハウジングに使用できます。従来型のデータ・ウェアハウジングとビジネス・インテリジェンス・アナリティクスに対応できるよう、Cloudant は IBM Db2 Warehouse との統合をサポートしています。オンラインでのレポート作成やビジネス・インテリジェンス・アナリティクスでも、IBM Db2 Warehouse との統合をサポートします。
  • ロケーション・ベースのデータを GeoJSON 形式で保管できます。

インデックスによる Cloudant クエリーの最適化

クエリーを高速化するために、データベース内では頻繁に使われるデータとそれらのデータに関連するクエリーにインデックスが付けられます。Cloudant でサポートしているインデックスには、以下の 2 つのタイプがあります。

  • "type": "text"
  • "type": "json"

この 2 つのインデックス・タイプには、用途と使用法に関して大きな違いがあります。

用途という観点からすると、テキスト・タイプのインデックスは Cloudant ドキュメント自体の構造ではなく、ドキュメントに含まれる特定の内容を焦点としています。したがって、ユーザーがデータベース内のドキュメントの構造を十分に理解していない場合や、ドキュメントの構造が大幅に変動し、その形式も複雑な場合には、テキスト・インデックスが推奨される手段となります。対照的に、ドキュメントの構造に関する要件の高い JSON インデックスは、1 つ以上の特定のフィールドに基づいて作成されます。したがって、ユーザーがデータベース内のドキュメントの構造に詳しい場合は、JSON インデックスを作成するという方法を選べます。明示的に適切なフィールドを指定することで、指定されたフィールドが含まれる、Cloudant に対するクエリーのすべてを最適化できます。この観点からすると、JSON インデックスの概念は、従来型のリレーショナル・データベース内でのインデックスの概念と似ています。どちらも特定の列またはフィールドを対象に最適化されるためです。

使用法という観点から見ると、テキスト・インデックスは Cloudant のデータ検索インターフェース内でのみ使用できます。このインターフェースでサポートされる構文は、Apache Lucene の Query Parse Syntax です。対照的に、JSON インデックスを使用できるのは、Cloudant のデータ・クエリー・インターフェースに限られます。こちらのインターフェースでは、Cloudant のクエリー解析構文に従って JSON オブジェクトのクエリーを実行できます。どちらのタイプのインデックス作成インターフェースも、さまざまなカスタム・クエリーをサポートする強力なものとなっています。テキスト・タイプのインデックスでも、Cloudant ではユーザーが例えば条件付きの判断を使用して特定のフィールドを識別するといった方法で、インデックスを付けるデータの範囲を正確に定義できます。したがって、ほとんどのシナリオでは (ドキュメントの構造が比較的単純で明確な場合)、テキスト・タイプと JSON タイプのインデックスは互いに変換することができます。ただし、ドキュメントの構造が複雑であり、明確でない場合は、テキスト・インデックスとデータ検索インターフェースしか使用できません。

インデックスの作成速度という点では、同じ量のデータを処理する場合、テキスト・タイプは JSON タイプより速度が劣ります。なぜなら、テキスト・インデックスを作成する際に、Cloudant では指定されたデータ構造だけでなく、その内容も処理するためです。それとは対照的に、JSON インデックスは構造自体のみを扱います。場合によっては、テキスト・インデックスを使用してデータベース全体の全文検索を行うこともできます。そのためのコードは、以下のとおり単純です。

例 1. テキスト・インデックスを使用して全文検索を作成する

{
    "type": "text",
    "index": { }
}

さらに、高度な集約や地理空間ベースの計算などといった Cloudant の拡張機能の中には、JSON インデックスに基づくクエリー・インターフェース内でしか使用できないものもあります。したがって、この 2 つのインデックス・タイプのどちらを使用するかを選択する際は、既存のデータをどれだけ理解しているかが重要な鍵となります。

Cloudant のビューに基づくクエリーの最適化

従来型のリレーショナル・データベースでは、ビューを使用してデータのフィルタリング、アクセスの制御、クエリーの最適化を行います。Cloudant の場合、ビューは主にデータのフィルタリングとクエリーの最適化に使用され、データへのアクセス制御にはほとんど関与しません。このセクションでは、ビューとクエリー最適化について説明します。

ビューとインデックス

Cloudant ではビューが作成されると、そのビュー内のデータ用のインデックスが自動的に作成されます。したがって、作成されたビュー内に含まれるデータにインデックスを付ける必要はありません。ビューに関連付けられたインデックスは、ビュー・ベースのクエリーに対してのみ有効になります。この場合、前述の 2 つのインデックス・タイプより優先されます。

以下のいずれかのイベントが発生すると、インデックスの内容が増分方式で自動的に更新されます。

  • 新しいドキュメントがデータベースに追加された場合
  • 既存のドキュメントがデータベースから削除された場合
  • データベース内の既存のドキュメントが更新された場合

ビュー定義が含まれるデザイン・ドキュメントが更新されると、そのビューのインデックスは完全に更新されます。さらに、デザイン・ドキュメントが更新された場合、ビュー自体は更新されないとしても、そのデザイン・ドキュメントによって定義されているすべてのビューが完全に更新されます。データのサイズが大きいと、ビューの完全更新に時間がかかってデータベースのパフォーマンスが損なわれるため、こうした事態は常に回避しなければなりません。ビューを定義する際は、関係のないビューは別のデザイン・ドキュメントに分けて定義するようにしてください。

本番環境では、ビジネスのニーズにより、新しいビューを作成したりビュー全体を更新したりすることを避けられない場合があります。大量のデータがある場合は、データベース・リソースが処理時間を長い間占拠してシステム全体の速度を低下させないよう、Cloudant の本番環境内でビューを直接作成または更新することは避けなければなりません。

そのための 1 つの方法としては、テスト環境内に本番データベースのバックアップを作成し、継続的な複製を設定します。その上で、バックアップ・データベース内でビューを作成または更新し、処理が完了するまで待ちます。その後、バックアップ・データベースを本番環境の Cloudant インスタンスに同期させる必要があります。アプリケーションに極めて短い応答時間が必要な場合は、ビューの増分式更新もデータベースのパフォーマンスに影響することがあります。なぜなら、Cloudant ではクエリーのパフォーマンスを向上させるためにビューの 3 つのコピーを保持し、これらのコピーの間で一貫性を維持しなければならないためです。実際のところ、Cloudant に用意されているパラメーター (stable、stale、update など) を使用すれば、最新ではなくても 1 つのフラグメントだけからデータを受信するかどうか、最新ではないデータを許容するかどうか、あるいはビューをリアルタイムで更新するかどうかを、ユーザーが決定できます。ユーザーは独自の要件に応じて、これら 3 つの選択肢の間で適切なパラメーターを選択しなければなりません。このリンク先の Cloudant の公式資料に関連情報が詳しく記載されているので、詳細についてはここでは説明しません。

map 関数

Cloudant でのビュー定義には主要な部分として、map 関数と reduce 関数の 2 つが含まれます。map 関数の入力はドキュメントです。map 関数内で、ドキュメントに含まれる各フィールドの値を読み取ることができます。関連するビジネス・ロジックを追加した後は、emit 関数を使用して、変換後のドキュメントを reduce 関数にフィードできます。ビューはデフォルトで関連するインデックスを作成し、リアルタイムでデータを更新することから、map 関数内に比較的複雑なデータ・レベルのビジネス・ロジックを配置することで、データ処理を効率化できます。

emit 関数は、map 関数の結果を出力するために使用します。したがって、ビューの作成と更新、そしてビュー・ベースのクエリーを高速化するためには、emit 関数を使用するときに、無益なフィールドを出力したりドキュメント全体を出力したりするのではなく、必要なフィールドだけを出力するようにすることが肝心です。Cloudant の公式資料で、map 関数について明確にわかりやすく説明されているので、詳細についてはここでは説明しません。

reduce 関数

Cloudant の公式資料での reduce 関数の説明はあまりにも簡潔で、実用的な例も補足的な説明も欠けています。そのため、初めて Cloudant を使用するユーザーが複雑な reduce 関数を実装するのは簡単ではありません。そこで、このセクションでは例を用いて、reduce 関数とその具体的な実装および基礎となる原則について説明します。

reduce 関数に渡される引数には、「キー」、「値」、「rereduce」の 3 つがあります。通常、reduce 関数では以下の 2 つのケースを処理する必要があります。

rereduce が false であるケース:

  • 「キー」は、[key, id] という形の配列を要素として持つ配列です。ここで、key は map 関数によって出力されたキー、id はそのキーの生成元となったドキュメントを識別します。
  • 「値」は、キーに含まれる要素に対して出力された値からなる配列です。

rereduce が true であるケース:

  • 「キー」は null になります。
  • 「値」は、以前の reduce 関数呼び出しによって返された値からなる配列です。

以下に具体例を記載します。この例では、データベース内に保管されているドキュメントが、システムのユーザーによって作成されたファイルであることを前提としています。各ファイルは、そのファイルに対応するフォルダー内に配置されます。したがって、各レコードには user_id、folder_id、file_id、そして説明と作成時刻のタイムスタンプなどの情報が含まれます。ここで目標とするのは、ビューの MapReduce 関数を使用して、特定の folder_id および user_id を指定するとすべての file_id が返されるクエリーを作成することです (Cloudant 内では、この機能を他の手段によって実現することもできます。以下の例は、MapReduce 関数を説明する目的でカスタマイズしたものです)。

例 2. MapReduce の例 - map 関数

function (doc) {
  if((doc.obj_type === "file")
      && doc.file_id && doc.folder_id){
        var user_id = doc.user_id;
        if(user_id){
          emit([user_id, doc.folder_id], doc.file_id);
        }   
	}     
}

例 2 は、map 関数の実装です。このコードによって、各ドキュメントをキーと値のペアに変換します。ここで、キーはuser_id と folder_id からなるリスト、値は file_id です。

例 3. MapReduce の例 - reduce 関数

function (keys, values, rereduce) {
  if (rereduce) {
    var Rresult = "";
    for(var j in values){
      var sub_value = values[j];
      var sa = values[j].split(",");
      for(var m in sa){
        if (Rresult.indexOf(sa[m])<0){
          if(Rresult == ""){
          Rresult =  sa[m];
        }else{
          Rresult = Rresult + "," + sa[m];
        }
        }
      }
    }
    return Rresult;
  } else {
    var result ="";
    for(var i in values){
      if(result.indexOf(values[i])<0){
        if(result == ""){
          result =  values[i];
        }else{
          result = result + "," + values[i];
        }
      }
    }
    return result;
  }
}

例 3 は、reduce 関数の実装です。このコードは rereduce が true のケース、false のケースのそれぞれを処理します。図 1 に、詳しい実装プロセスを示しています。ステップ 1 でプロシージャーを実行します。この場合、rereduce は false であるため、図 1 の層 L0 にあるデータを処理します。層 L0 にあるデータは map 関数の出力であり、この場合は reduce 関数の入力でもあります。rereduce が false の場合、層 L0 の出力が層 L1 の入力データになります。このプロセスでは、同じキーを共有する値が、コンマを区切り文字として 1 つの文字列に連結されます。例えば、層 L1 の左端にあるキーと値のペアでは、値が「File1, File2, File3」のようになります。続いて、rereduce が true に等しいケースのコードが繰り返し実装されます。つまり、層 L1 で同じキーを持つ入力文字列をコンマで分割し、重複する文字列を削除してから、コンマで区切って連結します。新しい文字列は層 L2 に送信されます。層 L2 に複数の親ノードがある場合は、このプロセスを繰り返します。

図 1. reduce 関数プロシージャー

前述の MapReduce 関数のビジネス・ロジックに基づき、Cloudant は関連するデータを前処理してから map および reduce 関数の結果を Cloudant インデックスの B ツリーに送信します。後でこのビューに基づくクエリーを実行すると、関連するデータが素早く返されます。ビュー・ベースのクエリーを使用する場合に頻繁に用いられるパラメーターは、group と group_level の 2 つです。

ビュー・ベースのクエリーでの group と group_level

Cloudant では、ビュー・ベースのクエリーにいくつものパラメーターをオプションで使用できるようになっています。例えば、開始および終了キーの値のパラメーター、複数のキー値を同時に渡すためのパラメーター、reduce 関数を実行するかどうかを指定するパラメーター、レコードを制限したり一部のレコードをスキップしたりするためのパラメーターがあります。こうしたパラメーターの中で、最も強力ではあるものの、エラーの原因となりがちなのは、group と group_level の 2 つです。

ビュー・ベースのクエリーでパラメーターが使用されていない場合、reduce 関数がビュー内で定義されているとしたら、クエリー結果が reduce 関数の最終出力になります。前のセクションの例で、パラメーターを指定せずにビュー・ベースのクエリーをそのまま実行すると、図 4 に示すような結果になります。

例 4. パラメーターを使用しない場合のビュー・ベースのクエリー結果

Method: GET /$DATABASE/_design/$DDOC/_view/$VIEW-NAME
Request: None
Response:
{
    "rows": [
        {
            "key": null,
            "value": "F1,F2,F3,F4,F5"
        }
    ]
}

このように、コンマで連結された一連の値の文字列がクエリー結果となります。結果で返されるキーが null となっている理由は、最後のステップで実行される reduce 関数は、rereduce が true であるケースを処理しているためです。ただし、ここで目的としているのは user_id と folder_id でグループ化されたすべての file_id を取得することなので、この結果では目的を果たせません。パラメーター「group」は、デフォルトでは false に設定されます。このパラメーターをクエリー内に含めて値を true に指定すると、図 5 のような結果になります。

例 5. パラメーター group を使用した場合のビュー・ベースのクエリー結果

Method: GET /$DATABASE/_design/$DDOC/_view/$VIEW-NAME?group=true
Request: None
Response:
{
    "rows": [
        {
            "key": ["U1","Fo1"],
            "value": "F1,F2,F3,F4,F5"
        },
        {
            "key": ["U2","Fo1"],
            "value": "F1,F2,F3,F4"
        },
        {
            "key": ["U2","Fo2"],
            "value": "F1,F3,F4"
        }
	]
}

このように、結果は map 関数のキー出力別にグループ化されます。reduce 関数は、これらのグループのそれぞれに含まれる値を処理することになります。その場合、user_id と folder_id を組み合わせた値をキーとして指定して渡すと、クエリーからは指定したキーに対応する値だけが返されます。そうなれば、前述のニーズをほぼ満たすことになります。

group_level パラメーターを使用するには、2 つの前提条件があります。1 つは group パラメーターが true であること、もう 1 つはキーが複合キーであることです。いわゆる複合キーとは、1 つの値になるのではなく、配列になる必要があるキーのことを意味します。前の例で使用しているキーは、user_id と folder_id からなる複合キーです。上述の 2 つの前提が両方とも満たされていれば、group_level を、1 からキー配列の長さまでの範囲の整数として指定できます。group_level の値を n にすると、reduce 関数はキー配列の最初の n 個の要素のみを使用して値をグループ化します。例 6 では前の例に基づき、group_level を 1 に設定したクエリーを行っています。

例 6. パラメーター group_level を 1 に設定した場合のビュー・ベースのクエリー結果

Method: GET /$DATABASE/_design/$DDOC/_view/$VIEW-NAME?group=true&group_level=1
Request: None
Response:
{
    "rows": [
        {
            "key": ["U1"],
            "value": "F1,F2,F3,F4,F5"
        },
        {
            "key": ["U2"],
            "value": "F1,F2,F3,F4"
        }
	]
}

group_level を 1 に設定しているため、結果で返されるキー配列には複合キーの最初の要素、つまり user_1 だけが含まれています。この場合、共通の user_id を共有する値は、folder_id とは関係なくすべてグループ化されることになります。

group_level を 2 に設定すると、クエリーの結果は例 5 の結果と同じになります。つまり、group パラメーターが true として指定されている場合、group_level のデフォルト値はキー配列の長さになるということです。

そこで、folder_id キーのみを使用して値をグループ化するにはどうすればよいのかという疑問が浮かんでくることでしょう。現在のところ、このような割り当ては、Cloudant のクエリー・インターフェースではサポートされていません。folder_id のみを使用して値をグループ化するための代替手段としては、このビューの map 関数を変更するか新しいビューを追加して、folder_id パラメーターを複合キーの左端に配置するという方法があります。

ビューを使用して効率的にドキュメント参照を操作する

NoSQL データベース内のドキュメントの間でさまざまな参照を設定することは推奨されません。それよりも、重複する情報という形でデータを参照先ドキュメント内に保管することをお勧めします。けれども、複雑なビジネス・ロジックにはどうしてもドキュメントの参照が必要になります。ドキュメント内に ID のようなフィールドがある場合、通常は、その ID を使用して参照先ドキュメント内のデータを取得するためのクエリーを追加しなければなりません。

参照先ドキュメントに対するクエリーを効率化するために、Cloudant のビューでは 1 つのクエリーだけを使用して、元のドキュメントと参照先ドキュメントを同時に返すことができるようになっています。map 関数内で、値の最終出力に「_id」フィールドが含まれており、そのフィールドに対応するドキュメント id の値が指定されています。したがって、ビューに対するクエリーを実行するときに、include_docs パラメーターが true に指定されている限り、指定された ID に対応するドキュメントが取得されて返されることになります。一例を説明しましょう。例 7 にデータベース内の 3 つのデータを記載します。

例 7. ドキュメント参照クエリーに対してビューを最適化する例でのソース・データ

{"_id":"2sof3204234u","node_name":"Node 1","parent":"3sldfjsla"}
{"_id":"5ladsfjldd","node_name":"Node 2","parent":"3sldfjsla"}
{"_id":"3sldfjsla","node_name":"Node 3","parent":""}

Node 1 と Node 2 は、それぞれの親として Node 3 の ID を参照します。各ノードに対するクエリーを実行する際に該当する親を検出するには、まず、新しいビューを作成する必要があります。例 8 に、その新しいビューの map 関数を記載します。

例 8. ドキュメント間の参照を処理するために使用する map 関数

function(doc) {
    if (doc.parent) {
        emit(doc.node_name, { "_id": doc.parent });
    }
}

このビューの reduce 関数はブランクのままにできます。map 関数での最終出力の値は、1 つの ID 属性だけが含まれるオブジェクトです。そのオブジェクトの親の属性値、つまり親の ID を渡します。このビューに基づいてデータのクエリーを実行すると、例 9 に示す結果になります。

例 9. ドキュメント間の参照を処理するビューを使用した場合のクエリー結果

Method: GET /$DATABASE/_design/$DDOC/_view/$VIEW-NAME?include_docs=true
Request: None
Response:
{  
   "total_rows":3,
   "offset":0,
   "rows":[  
      {  
         "id":"2sof3204234u",
         "key":"Node 1",
         "value":{  
            "_id":"3sldfjsla"
         },
         "doc":{  
            "_id":"3sldfjsla",
            "node_name":"Node 3",
            "parent":""
         }
      },
      {  
         "id":"5ladsfjldd",
         "key":"Node 2",
         "value":{  
            "_id":"3sldfjsla"
         },
         "doc":{  
            "_id":"3sldfjsla",
            "node_name":"Node 3",
            "parent":""
         }
      },
      {  
         "id":"3sldfjsla",
         "key":"Node 3",
         "value":{  
            "_id":""
         },
         "doc":null
      }
   ]
}

上記のクエリーによって返された結果では、「doc」フィールドに参照先ドキュメントの内容が格納されています。Node 1 と Node 2 は両方とも Node 3 の ID を参照しています。ビューを使用することにより、Node 1 と Node 2 は Node 3 のドキュメント・データが含まれた状態で返されます。

だたし、このような処理にはドキュメントの形式に関する特定の要件が伴います。その 1 つとして、参照元と参照先のドキュメントは同じ、あるいは同様の構造になっていなければなりません。構造が異なると、参照先ドキュメントがフィルターで除外されて、返される結果に含めることができなくなります。例 10 では、例 7 のソース・データを変更し、Node 3 から parent フィールドを削除しています。

例 10. ドキュメント参照クエリーに対してビューを最適化する例でのソース・データ (変更バージョン)

{"_id":"2sof3204234u","node_name":"Node 1","parent":"3sldfjsla"}
{"_id":"5ladsfjldd","node_name":"Node 2","parent":"3sldfjsla"}
{"_id":"3sldfjsla","node_name":"Node 3"}

map 関数の定義は例 8 での定義と変わっていません。Node 3 には parent フィールドが含まれていないため、このデータを処理すると、Node 3 は map 関数によって除外され、他のノードを出力する際に参照エラーが発生します。最終的な結果を例 11 に示します。

例 11. ドキュメント間の参照を処理するビューを使用した場合のクエリー結果 (変更後)

Method: GET /$DATABASE/_design/$DDOC/_view/$VIEW-NAME?include_docs=true
Request: None
Response:
{  
   "total_rows":2,
   "offset":0,
   "rows":[  
      {  
         "id":"2sof3204234u",
         "key":"Node 1",
         "value":{  
            "_id":"3sldfjsla"
         },
         "doc":null
      },
      {  
         "id":"5ladsfjldd",
         "key":"Node 2",
         "value":{  
            "_id":"3sldfjsla"
         },
         "doc":null
      }
   ]
}

このセクションでは主にビュー内のインデックスと reduce 関数について詳しく説明しました。ここに記載した例と Cloudant の公式 Web サイト上にあるデータを照らし合わせると、複雑なビューを作成する方法をより深く理解できるはずです。

Cloudant のクエリーに関するその他の最適化

リクエストと再試行タイムアウトの値を調整するための設定

Cloudant はクラウド・ベースのサービスであるため、一般に、Cloudant インスタンスが稼働するノードは他のインスタンスと共有されます。したがって、同じノード内のデータベース間でのリソースの競合や相互作用は避けられません。例えば、ノードのインスタンスの 1 つが短時間の間でもノードのリソースを大量に消費すると、同じノードの他のインスタンスに影響が及び、データのリクエストに対するレスポンスが返ってくるまでに、かなりの時間がかかります。

同じノード内の他のインスタンスが瞬間的に大量のリソースを消費したために自分自身のインスタンスでリクエストの遅延が発生したり処理が失敗したりしないようにするには、Cloudant のリクエストのタイムアウト・メカニズムと再試行メカニズムを有効にする必要があります。リクエストに対して時間内にレスポンスを返せないことが判明したら、タイムアウト・メカニズムにより、リクエストをタイムアウトにしなければなりません。その後、再試行メカニズムを使用してリクエストを再送します。リクエストのタイムアウトと再試行回数の値は、アプリケーション・システムと Cloudant 環境との間の接続を複数回調整した後、特定のパフォーマンス要件に応じて設定する必要があります。

Cloudant で提供しているソフトウェア開発キットでは、リクエストのタイムアウトと再試行はデフォルトで無効にされています。したがって、開発者が明示的にこれらの設定を指定しなければなりません。例 12 に、NodeJS を例としてリクエスト・タイムアウトを設定する方法を示します。

例 12. NodeJS 内で Cloudant のリクエスト・タイムアウトを設定する例

var Cloudant = require('cloudant');
var cloudant = Cloudant({account:me, 
password:password,
requestDefaults: { "timeout": 5000 }
});

上記の例では、Cloudant の初期化関数内で、requestDefaults プロパティーを作成し、タイムアウトを 5000 ミリ秒として指定しています。この関数が初期化されると、Cloudant データベースとの接続が確立されます。この接続を介して送信されるすべてのリクエストには、タイムアウトとして 5 秒が設定されることになります。

Cloudant の初期化関数では、retry というプラグインを渡すことができます。このプラグインでもタイムアウトと再試行回数を設定できますが、これらの設定は上記の概念とは異なります。Cloudant DBaaS の料金設定の詳細の 1 つによって、特定の料金で実行できる 1 秒あたりの同時リクエストの最大数が指定されています。この最大数を上回るクエリーは拒否されて、HTTP 429 メッセージが返されます。retry プラグインでは、指定の条件下でのみ再試行回数とタイムアウトを設定できます。デフォルトでは、最大数を上回るクエリーは 3 回再試行され、500 ミリ秒でタイムアウトになります。

Cloudant のビューと結合演算

従来型のリレーショナル・データベースでは、2 つのテーブルが外部キーによって関連付けられている場合、結合演算を使用した 1 つのクエリーによって両方のテーブルから情報を取得できます。一方、Cloudant では、タイプが異なる 2 つのドキュメントのデータが関連付けられている場合、通常は、両方のドキュメントからデータを取得するには 2 つの HTTP クエリー・リクエストを使用しなければなりません。

例 13. Cloudant のビューと結合演算のサンプル・データ

{
	"id":"file1",
	"name":"how to learn JS",
	"user_id":"user1",
	"doc_type":"file"
},
{
	"id":"file2",
	"name":"Thinking in Java",
	"user_id":"user1",
	"doc_type":"file"
},
{
	"id":"user1",
	"name":"John",
	"doc_type":"user"
}

例 13 に示されているデータの場合、ユーザーの情報とそれに関連付けられているファイルの情報を見つけるには、一般に、ユーザー情報に対するクエリーとファイル情報に対するクエリーの 2 つが必要になります。一方、Cloudant のビューを使用することによって、この目標を 1 つのクエリーだけで達成できます。例 14 に、そのためのビューの定義を示します。このビューに必要なのは map 関数だけです。

例 14. 結合演算を処理するために使用するビュー

function(doc){
	if(doc.type=="user"){
		emit([doc.id,0],doc)
	}else if(doc.type=="file"){
		emit(doc.user_id,1),doc
	}
}

この関数では、複合キーを使用しています。複合キーの最初の要素はユーザーの ID です。2 番目の要素は状況によって異なり、2 とおりあります。まず、ドキュメントのタイプが user の場合は、2 番目の要素が 0 となります。タイプが file の場合は、2 番目の要素が 1 となり、複合キーの対応する値として該当するドキュメントの情報が格納されます。

デフォルトでは、Cloudant のビュー・ベースのクエリー結果はキーを基準にソートされます。具体的には、["abc", 2] は ["abc"] と ["abc", 1] の後になる一方で、["abc", 2, "xyz"] より前に配置されます。このルールを前提とすれば、特定のユーザーのユーザー情報とファイル情報を 1 つのクエリーによって取得することができます。それには、startkey と endkey を指定します。例えば、ID が「abc」に設定されたユーザーのユーザー情報とファイル情報を取得するには、startkey を ["abc"]、endkey を ["abc", 2] として指定します。

HTTP 永続接続または接続プールを使用する

Cloudant で提供しているソフトウェア開発キットでは、クライアントと Cloudant 間の接続を確立する際のアプリケーション・レベルのプロトコルは HTTP です。クライアント・アプリケーションから Cloudant へのリクエストは、短時間で大量の数に上ることがあります。HTTP 永続接続を有効にすると、クライアントと Cloudant の間で接続が再利用されるため、リクエストが実行されるごとに接続を確立するプロセスがなくなり、それによってクエリーがある程度まで高速化されます。例 15 に、Node.js で HTTP 永続接続を有効化する方法を示します。まず、npm を使用して agentkeepalive モジュールをインストールする必要があります。

例 15. Node.js での HTTP 永続接続の有効化

// create custom HTTPS agent
var HttpsAgent = require('agentkeepalive').HttpsAgent;
var myagent = new HttpsAgent({
  maxSockets: 50, 
  maxKeepAliveRequests: 0,
  maxKeepAliveTime: 30000
});

// Setup Cloudant connection
var cloudant = require('cloudant')({ 
  url: 'https://myusername:mypassword@myaccount.cloudant.com', 
  "requestDefaults": { "agent" : myagent }
});

注意する点として、クライアントによっては (Java の Apache Http Client など)、HTTP 永続接続を有効にするために HTTP 接続プールを使用しますが、その役割は同等です。

まとめ

代表的なドキュメント・データベースおよび NoSQL データベースとして、Cloudant はその使用方法、特にデータ・クエリーと最適化という点で、従来型のリレーショナル・データベースとは異なります。この記事で説明した最適化方法はあらゆる状況を網羅しているわけではなく、特定のデータおよび使用ケースによって組み合わせて使用する必要があります。データの量が増え、複雑化するにつれ、Cloudant のクエリーの効率性に関する要件が高くなるだけでなく、Cloudant クエリーを最適化する方法も増えてきます。Cloudant でのクエリーを効率化できるよう、このトピックについて他の読者とも話し合うことをお勧めします。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=ビジネス・アナリティクス
ArticleID=1064118
ArticleTitle=Cloudant 内でのクエリーを最適化する
publish-date=12272018