Amazon Web Services ブログ

新しい Amazon DocumentDB の集計、配列、インデックス作成機能

Amazon DocumentDB (MongoDB 互換) は、MongoDB のワークロードをサポートする高速でスケーラブル、かつ可用性に優れた完全マネージド型のドキュメント データベース サービスです。今日、使用したものと同じ MongoDB アプリケーションコード、ドライバー、およびツールを使用して、Amazon DocumentDB でワークロードを実行、管理、および拡張できます。これにより、基となるインフラストラクチャの管理を気にせず、パフォーマンス、スケーラビリティ、および可用性を向上させることができます。

今日、Amazon DocumentDB は、新しい集計パイプライン演算子とステージをサポートするようになりました。これにより、ドキュメントで強力な集計を作成することができます。新しい機能には、集計文字列演算子 ($concat, $substr, $substrBytes, $substrCP, $strcasecmp)、配列集計演算子 ($size)、集計グループ アキュムレータ演算子 ($push)、および集計ステージ ($redact$indexStats) が含まれます。さらに、Amazon DocumentDB は、配列内の要素を更新する位置配列演算子 ($[]$[<identifier>]) およびインデックスを選択する hint() をサポートするようになりました。

このブログ記事では、一般的なユースケースを示してこれらの新機能のいくつかを紹介します。これにより、Amazon DocumentDB で大規模のアプリケーションを構築および管理する機能を使い始めることができます。

Amazon DocumentDB の使用の開始

Amazon DocumentDB を使い始めるには、Amazon DocumentDB 入門ガイドAWS CloudFormation のクイック スタートをご覧ください。準備ができたら、現在 MongoDB で使用しているものと同じアプリケーションコード、ドライバー、およびツールを使って、Amazon DocumentDB での開発を開始できます。

新機能

Amazon DocumentDB クラスターを作成して接続したら、次の例をよく理解して拡張することができます。

集計演算子とステージ

姓、名、位置などの文字列をドキュメントに保存するのが一般的です。アプリケーションでデータ処理をより簡単かつ高速にするために、2 つの文字列を結合するなどの操作をデータベース レベルまでプッシュダウンすることができます。Amazon DocumentDB に追加された新しい文字列集計演算子を使用すると、特定のユースケースに合わせて文字列を操作できます。あなたが人事アプリケーションを構築していて、組織の人々のために多数のドキュメントを含むコレクションを Amazon DocumentDB で保持していると想像してください。

入力

この例の各ドキュメントは 1 人用で、名、姓、生年月日、および各自の勤務地を表す会社固有の識別子が含まれています。

db.people.insertMany([
{ "_id":1, "first_name":"Jane", "last_name":"Doe", "DOB":"2/1/1999", "Desk": "MSP102-MN"},
{ "_id":2, "first_name":"John", "last_name":"Doe", "DOB":"12/21/1992", "Desk": "DSM301-IA"},
{ "_id":3, "first_name":"Steve", "last_name":"Smith", "DOB":"3/21/1981", "Desk":"MKE233-WI"}])

$concat

$concat 集計演算子は、ドキュメント内で複数の文字列を連結 (または組み合わせ) して、アプリケーションに返すことができる単一の文字列を生成します。これにより、アプリケーションで行われる作業が軽減されます。次の例では、ユーザーの姓と名を連結して、各ユーザーのフルネームを生成します。

クエリ:

db.people.aggregate(
[
 { $project: { full_name: { $concat: [ "$first_name", " ", "$last_name"] } } }
])

結果:

{ "_id" : 1, "full_name" : "Jane Doe" }
{ "_id" : 2, "full_name" : "John Doe" }
{ "_id" : 3, "full_name" : "Steve Smith" }

$concat 集計演算子を使用すると、個人のフルネームをアプリケーションに返すことで、アプリケーション レベルのロジックを必要とせずに使用できるようになります。

$substrCP

$substrCP 演算子は、文字列の一部 (部分文字列) を返します。この場合、目的の部分文字列は UTF-8 コードポイント (CP) の範囲として指定されます。$substr 演算子は部分文字列を文字の範囲として定義するのに役立ち、$substrBytes 演算子は部分文字列をバイトの範囲として定義するのに対して、$substrCP は与えられた文字列要素の文字数またはバイト数がわからない場合に部分文字列の定義に役立ちます。Unicode 文字列では一般的です。“è” の使用がこのポイントを強調するため、例として文字列 “caffè latte” を使用します。

次のように、$substrCP: { $substrCP: [ "caffè latte", 0, 5] } について考えてみましょう。“caffè” は文字列の始めから 5 つのコードポイント (すなわち 0) であるため、出力は “caffè” になります。同様のクエリ { $substrBytes: [ "caffè latte", 0, 5] } に対して $subtrBytes を使用した場合、“è” の格納に 2 バイトかかり、“è” の最初のバイトだけが返されるのでエラーになり、役に立ちません。したがって、同じ結果を得るために、次のクエリ { $substrBytes: [ "caffè latte", 0, 6] } を使用します。開発を単純化できるため、$substrCP$subtrBytes を使用することをお勧めします。文字列に文字を格納するのに必要なバイト数について考える必要はありません。

前の例から組織の人々という標本を用いて、従業員の居住地を抽出するために $substrCP を使う場合は、次のクエリを使用します。

クエリ:

db.people.aggregate(
  [
    {
      $project: {
          "state": { $substrCP: [ "$Desk", 7, 2] }
      }
    }
  ]
)

結果:

{ "_id" : 1, "state" : "MN" }
{ "_id" : 2, "state" : "IA" }
{ "_id" : 3, "state" : "WI" }

各従業員の勤務地におけるエンコードから、地域を表す 2 文字の略語は文字列の頭から 7 コードポイントで、長さは 2 コードポイントであることがわかります。つまり、 { $substrCP: [ "$Desk", 7, 2] } です。したがって、返される結果は文字列最後の 2 つのコードポイントです。従業員の勤務地にこのタイプのエンコードを使用している場合は、各コンポーネントについて固定長を維持するようにします。そうでなければ、書いてある通りクエリが中断されます。

$strcasecmp

同様に、$strcasecmp 演算子を使用して、2 つの文字列間で大文字と小文字の区別せずに比較を実行できます。つまり、{ $strcasecmp: [ string1, string2 ] } です。比較の出力は、string2 が string1 より大きい (1)、string1 と等しい (0)、または string1 より小さい (-1) かどうかを示します。

クエリ:

db.people.aggregate(
   [
     {
       $project:
          {
            item: 1,
            compare: { $strcasecmp: [ "$Desk", "mke233-wi" ] }
          }
      }
   ]
)

結果:

{ "_id" : 1, "compare" : 1 }
{ "_id" : 2, "compare" : -1 }
{ "_id" : 3, "compare" : 0 }

人々の標本から、“mke233-wi” と各従業員の勤務地を、大文字と小文字を区別しない $strcasecmp 演算子を使って比較すると、最後のエントリ (つまり、“_id”:3) は、“MKE233-WI” と等しくなります。

配列集計演算子 $size と集計グループのアキュムレータ演算子 $push について、ファンタジー ホッケーリーグのユーザー プロファイルを構築するシナリオを考えてみましょう。各ユーザーは、フォローしたいチームを選択できます。

入力:

db.profiles.insertMany([
{ "_id":1, "user":"hockeyFan01", "teams": ["sharks", "panthers", "stars"]},
{ "_id":2, "user":"puck22", "teams": ["ducks", "rangers"]},
{ "_id":3, "user":"pondHockey12", "teams": ["sharks", "panthers", "stars"]}
])

$size

$size 演算子を使用して、配列フィールド内の項目数を返すことができます。このシナリオでは、$size を使用して、各ユーザーがフォローしているチームの数を返します。

クエリ:

db.profiles.aggregate([
   {
      $project: {
         item: 1,
         "numberOfTeams": { $size: "$teams" }
      }
   }
])

結果:

{ "_id" : 1, "numberOfTeams" : 3 }
{ "_id" : 2, "numberOfTeams" : 2 }
{ "_id" : 3, "numberOfTeams" : 3 }

$push

$push 集計グループ アキュムレータ演算子を $unwind 集計ステージと $group 集計ステージで使用すると、チームとファンによるデータの再分類やピボット操作を実行できます。データを再分類すると、プロモーションメールで特定チームのファンをターゲットにしたいというシナリオに役立ちます。この場合、アプリケーションのロジックは簡単です。Sharks のプロモーションメールを“hockeyFan01” と “pondHockey12” と比較して、すべてのドキュメントおよび結果メールを 1 件ずつ処理するか、チームごとに標本のスキャンを行う必要があります。

クエリ:

db.profiles.aggregate( 
[ { $unwind: "$teams" },
 { $group : { _id : "$teams", "fans": { $push: "$user" } } }] 
)

結果:

{ "_id" : "rangers", "fans" : [ "puck22" ] }
{ "_id" : "ducks", "fans" : [ "puck22" ] }
{ "_id" : "sharks", "fans" : [ "hockeyFan01", "pondHockey12" ] }
{ "_id" : "stars", "fans" : [ "hockeyFan01", "pondHockey12" ] }
{ "_id" : "panthers", "fans" : [ "hockeyFan01", "pondHockey12" ] }

$redact

$redact 集計ステージは、ドキュメント内で選択されたフィールドに基づいてドキュメントのコンテンツを含めたり除外したりするシナリオに役立ちます。この集計ステージの利用方法をよりよく理解するために、患者記録システムを構築し、特定のコードへのアクセス権を持つ医師のみが患者記録を閲覧できるようにしたい場合について話してみましょう。医師がコードにアクセスできない場合、患者の記録はクエリから返されたドキュメントから削除されます。

入力:

db.patient.insertMany([
{ "_id":1,
  "code": "ICU",
  "patient":"John Doe",
  "DOB": "9/30/1987",
  "Hospital": "First Hill"
},
{ "_id":2,
  "code": "Reg",
  "patient":"Jane Doe",
  "DOB": "3/27/1989",
  "Hospital": "Cherry Hill"
},
{ "_id":3,
  "code": "Spec",
  "patient":"Steve Smith",
  "DOB": "1/8/1997",
  "Hospital": "Pill Hill"
}
])

クエリ:

db.patient.aggregate(
   [
     { $redact: {
        $cond: {
           if: { $eq: ["Reg", "$code"]},
           then: "$$DESCEND",
           else: "$$PRUNE"
         }
       }
     }
   ]
);

結果:

{
	"_id" : 2,
	"code" : "Reg",
	"patient" : "Jane Doe",
	"DOB" : "3/27/1989",
	"Hospital" : "Cherry Hill"
}

位置配列演算子

新しく追加された位置配列演算子を理解するために、航空会社のリワードプログラムの一環として Amazon DocumentDB を使用してフライトマイルを保存するユースケースを考えてみましょう。

入力:

db.miles.insertMany([
{ "_id" : 1, "member_since" : ISODate("1987-01-01T00:00:00Z"), "credit_card" : false, "flight_miles" : [ 1205, 2560, 880 ]},
{ "_id" : 2, "member_since" : ISODate("1982-01-01T00:00:00Z"), "credit_card" : true, "flight_miles" : [ 1205, 2560, 890, 2780]},
{ "_id" : 3, "member_since" : ISODate("1999-01-01T00:00:00Z"), "credit_card" : true, "flight_miles" : [ 1205, 880]}])

注: Amazon DocumentDB では、insertMany() はアトミック操作なので、コマンド全体が成功するか失敗するかのどちらかになります。ここで一貫性を持つと、挿入を介して廊下で障害が発生してからデータベースに書き込まれたドキュメントを調整することについて、心配する必要がないので便利です。

$[]

位置配列演算子 $[] を使用して、配列フィールドの各要素に対して演算を実行できます。たとえば、ある航空会社が、特定年度から顧客のフライトマイルを配列によって記録しているとします。顧客が航空会社にクレジットカードを登録すると、その年にすでに獲得したマイルに対して 100% のボーナスが付与されます。

クエリ:

db.miles.update(
   { credit_card: {$eq: true}},
   { $mul: { "flight_miles.$[]": NumberInt(2) } },
   { multi: true }
)

db.miles.find()

結果:

{ "_id" : 1, "member_since" : ISODate("1987-01-01T00:00:00Z"), "credit_card" : false, "flight_miles" : [ 1205, 2560, 880 ] }
{ "_id" : 2, "member_since" : ISODate("1982-01-01T00:00:00Z"), "credit_card" : true, "flight_miles" : [ 2410, 5120, 1780, 5560 ] }
{ "_id" : 3, "member_since" : ISODate("1999-01-01T00:00:00Z"), "credit_card" : true, "flight_miles" : [ 2410, 1760 ] }

クレジットカード会員の顧客のフライトマイルが 2 倍になったことを確認できます。 

$[<identifier>]

1980 年以来、航空会社を利用してきた最高顧客のために、2,000 マイル未満のフライトマイルをすべて 2,000 マイルに切り上げる追加プロモーションを実行することができます。Amazon DocumentDB では、$[<identifier>] 位置配列演算子を使用してそのクエリを実行できます。

クエリ:

db.miles.update(
	{member_since: {$lte: ISODate("1990-01-01T00:00:00Z")}},   
	{$set: {"flight_miles.$[element]": 2000}},   
	{multi: true, arrayFilters: [{"element": {$lt:2000}}]}
)

db.miles.find()

 結果:

{ "_id" : 3, "member_since" : ISODate("1999-01-01T00:00:00Z"), "credit_card" : true, "flight_miles" : [ 2410, 1760 ] }
{ "_id" : 1, "member_since" : ISODate("1987-01-01T00:00:00Z"), "credit_card" : false, "flight_miles" : [ 2000, 2560, 2000 ] }
{ "_id" : 2, "member_since" : ISODate("1982-01-01T00:00:00Z"), "credit_card" : true, "flight_miles" : [ 2410, 5120, 2000, 5560 ] }

1980 年代から航空会社を利用してきた 2 人の顧客については、flight_miles の “1205”、”880″、および “1780” が 2000 マイルのしきい値を下回っていたため、 追加プロモーションの一環として、結果が 2000 マイルに設定されました。

インデックス作成

新しく追加されたインデックス作成機能を使用して、インデックスの選択と使用を制御および管理することができます。使用されていないインデックスがある場合は、それらを使用するか削除するかによって、データベースはもちろん、最終的にはアプリケーションのパフォーマンスを向上させることができます。

hint()

Amazon DocumentDB で、クエリ プロセッサはパフォーマンスを最大化するクエリプランを評価して選択します。場合によっては、クエリ プロセッサに特定のインデックスを使用させることができます。それには、hint() 演算子を使用できます。

入力:

Amazon DocumentDB に食料品店の在庫を保存するユースケースを考えてみましょう。以下の食料品標本には、商品名、カテゴリ、価格、数量が含まれています。

db.grocery.insertMany([
{ "_id" : 1, "product":"orange", "category":"fruit", "price":0.89, "qty":354 },
{ "_id" : 2, "product" : "apple", "category": "fruit", "price":1.29, "qty":876},
{ "_id" : 3, "product" : "tomato", "category": "apparel", "price":0.29, "qty":39}
])	

db.grocery.createIndex( { product: 1 } )

入力クエリの結果として、食料品標本と商品フィールドのインデックスを作成します。hint() を使用して、クエリ プロセッサが商品フィールドで作成したインデックスを使用することができます。

クエリ:

db.grocery.find({"product":"orange"}).hint( { product: 1 } )

結果:

{
	"_id" : 1,
	"product" : "orange",
	"category" : "fruit",
	"price" : 0.89,
	"qty" : 354
}

商品フィールドのインデックスが、explain() メソッドで実際に使用されたことを検証できます。このメソッドは、クエリプラン、またはクエリ プロセッサが食料品標本のクエリを実行した方法に関する情報を提供します。

クエリ:

db.grocery.find({"product":"orange"}).hint({"product":1}).explain()

結果:

db.grocery.find({"product":"orange"}).hint({"product":1}).explain()
{
        "queryPlanner" : {
                "plannerVersion" : 1,
                "namespace" : "test.grocery",
                "winningPlan" : {
                        "stage" : "SUBSCAN",
                        "inputStage" : {
                                "stage" : "IXSCAN",
                                "indexName" : "product_1",
                                "direction" : "forward"
                        }
                }
        },
        "serverInfo" : {
                "host" : "blog-host",
                "port" : 27017,
                "version" : "3.6.0"
        },
        "ok" : 1
}

explain() の出力から、クエリ結果を生成したクエリプラン (つまり、ステージ) が、 “indexName”:”product_1” を使用したインデックス スキャン (すなわち “IXSCAN”) であり、したがってインデックスが食料品標本のドキュメントをクエリするために使用されたことを検証できます。

$indexStats

前述された食料品の例に続いて、インデックスに対していくつかクエリを実行すると、$indexStats 集計演算子を使用してこれらのインデックスで操作の使用方法を理解することができます。インデックスはクエリのパフォーマンスを向上させる場合に役立ちますが、データを挿入するときにデータベースに追加作業を作成します。データベースのクエリ時にインデックスの使用頻度を理解しておくと、インデックスが必要かどうかについて十分な情報を得たうえでの決定を行えます。

クエリ:

db.grocery.aggregate( [ { $indexStats: { } } ] )

結果:

{ "name" : "_id_", "key" : { "_id" : 1 }, "host" : "blog-host.com:27017", "accesses" : { "ops" : NumberLong(3), "since" : ISODate("2019-02-19T23:27:10.214Z") } }
{ "name" : "product_1", "key" : { "product" : 1 }, "host" : "blog-host.com:27017", "accesses" : { "ops" : NumberLong(7), "since" : ISODate("2019-02-19T23:27:18.042Z") } }

この結果から、2019 年 2 月 19 日に作成されてから、またはサーバーが最後に再起動されてから、インデックス “_id_” が 3 回クエリされたことがわかります。同様に、インデックス “product_1”は、同じ日に作成されてから 7 回クエリされました。本番データベースの場合は、$indexStats を使用して、インデックスの使用状況を時間とともにベースライン化およびモニターできます。

まとめ

この Amazon DocumentDB リリースでは、大規模なアプリケーションの構築と管理を可能にする新機能を追加しました。Amazon DocumentDB の詳細については、Amazon DocumentDB のホームページおよび入門ガイドを参照してください。

このブログ記事に質問やご意見がある場合は、本ページのコメント欄に記入してください。


著者について

Joseph Idziorek は、アマゾン ウェブ サービスのプリンシパルプロダクトマネージャーです。