基本的な免責事項から始めましょう。問題に答える内容の本体は、検索ですでに回答されています。 Double Nested Array MongoDB 。そして「記録のために」ダブル トリプルにも適用されます またはQuadrupal または任意 基本的に同じ原則としてのネストのレベル常に 。
答えのもう1つの重要なポイントは、アレイをネストしないです。 、その回答でも説明されているように(そして私はこれを多く繰り返しました 回数)、あなたが「考える」理由は何でも 「ネスト」が必要です 実際には、あなたがそれを理解するような利点はありません。実際、「ネスト」 本当に人生をはるかに難しくしているだけです。
ネストされた問題
「リレーショナル」モデルからのデータ構造の変換の主な誤解は、ほぼ常に「ネストされた配列レベルを追加する」として解釈されます。 関連するすべてのモデルに対して。ここで提示しているのは、この誤解の例外ではありません。「正規化」されているように見える 各サブ配列に、その親に関連するアイテムが含まれるようにします。
MongoDBは「ドキュメント」ベースのデータベースであるため、これを実行したり、実際に基本的に必要なデータ構造コンテンツを実行したりすることができます。ただし、これは、そのような形式のデータが操作しやすい、または実際の目的で実際に実用的であることを意味するものではありません。
デモンストレーションのために、スキーマに実際のデータを入力してみましょう。
{
"_id": 1,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "A",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
]
},
{
"third_item": "B",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
]
}
]
},
{
"second_item": "A",
"third_level": [
{
"third_item": "B",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
}
]
}
]
},
{
"first_item": "A",
"second_level": [
{
"second_item": "B",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
}
]
}
]
}
]
},
{
"_id": 2,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "A",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
]
}
]
}
]
},
{
"_id": 3,
"first_level": [
{
"first_item": "A",
"second_level": [
{
"second_item": "B",
"third_level": [
{
"third_item": "A",
"forth_level": [
{
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
]
}
]
}
]
}
これは、質問の構造とは「少し」異なりますが、デモンストレーションの目的で、確認する必要のあるものが含まれています。主に、サブ配列を持つアイテムを含むドキュメント内の配列があり、サブ配列内のアイテムなどがあります。 「正規化」 もちろん、これは「アイテムタイプ」または実際に持っているものとしての各「レベル」の識別子によるものです。
コアの問題は、これらのネストされた配列内からデータの「一部」が必要なことです。MongoDBは実際には「ドキュメント」を返したいだけです。つまり、一致する「サブ」に到達するために何らかの操作を行う必要があります。アイテム」。
「正しく」の問題でも これらすべての「下位基準」に一致するドキュメントを選択するには、 $ elemMatch
配列要素の各レベルで条件の正しい組み合わせを取得するため。 "ドット表記"
をそのまま使用することはできません。 これらの $ elemMatch
がない場合
正確な「組み合わせ」を取得せず、任意ので条件が真であるドキュメントを取得するだけのステートメント 配列要素。
実際には「配列の内容を除外する」 それは実際には追加の違いの一部です:
db.collection.aggregate([
{ "$match": {
"first_level": {
"$elemMatch": {
"first_item": "A",
"second_level": {
"$elemMatch": {
"second_item": "A",
"third_level": {
"$elemMatch": {
"third_item": "A",
"forth_level": {
"$elemMatch": {
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}
}
}
}
}
}
}
}
}},
{ "$addFields": {
"first_level": {
"$filter": {
"input": {
"$map": {
"input": "$first_level",
"in": {
"first_item": "$$this.first_item",
"second_level": {
"$filter": {
"input": {
"$map": {
"input": "$$this.second_level",
"in": {
"second_item": "$$this.second_item",
"third_level": {
"$filter": {
"input": {
"$map": {
"input": "$$this.third_level",
"in": {
"third_item": "$$this.third_item",
"forth_level": {
"$filter": {
"input": "$$this.forth_level",
"cond": {
"$and": [
{ "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
{ "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.third_item", "A" ] },
{ "$gt": [ { "$size": "$$this.forth_level" }, 0 ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.second_item", "A" ] },
{ "$gt": [ { "$size": "$$this.third_level" }, 0 ] }
]
}
}
}
}
}
},
"cond": {
"$and": [
{ "$eq": [ "$$this.first_item", "A" ] },
{ "$gt": [ { "$size": "$$this.second_level" }, 0 ] }
]
}
}
}
}},
{ "$unwind": "$first_level" },
{ "$unwind": "$first_level.second_level" },
{ "$unwind": "$first_level.second_level.third_level" },
{ "$unwind": "$first_level.second_level.third_level.forth_level" },
{ "$group": {
"_id": {
"date": "$first_level.second_level.third_level.forth_level.sales_date",
"price": "$first_level.second_level.third_level.forth_level.price",
},
"quantity_sold": {
"$avg": "$first_level.second_level.third_level.forth_level.quantity"
}
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quanity_sold": "$quantity_sold"
}
},
"quanity_sold": { "$avg": "$quantity_sold" }
}}
])
これは、「乱雑」および「関与」として最もよく説明されます。 $ elemMatchを使用したドキュメント選択の最初のクエリは、
一口以上ですが、その後に $filterがあります。
および $ map
すべての配列レベルの処理。前述のように、これは実際にレベルがいくつあってもパターンです。
または、 $ unwind
を実行することもできます。
および $ match
アレイを適切にフィルタリングする代わりに組み合わせますが、これによりに追加のオーバーヘッドが発生します。 $ unwind
不要なコンテンツが削除される前に、MongoDBの最新リリースでは、一般的に $ filter
最初にアレイから。
ここでの最後の場所は、 $ group
>
実際に配列内にある要素によって、<を実行する必要があります。 code> $ unwind
とにかくこの前のアレイの各レベル。
実際の「グループ化」は、通常、 sales_date
を使用すると簡単です。 およびprice
最初ののプロパティ 蓄積し、その後のステージを $ push コード>
別の価格
秒として各日付内の平均を累積する値 蓄積。
注 :日付の実際の処理は、実際の使用では、保存する粒度によって大きく異なる場合があります。このサンプルでは、日付はすべて、各「日」の開始にすでに丸められています。実際に実際の「日時」値を累積する必要がある場合は、おそらく次のような構成が必要になります。
{ "$group": {
"_id": {
"date": {
"$dateFromParts": {
"year": { "$year": "$first_level.second_level.third_level.forth_level.sales_date" },
"month": { "$month": "$first_level.second_level.third_level.forth_level.sales_date" },
"day": { "$dayOfMonth": "$first_level.second_level.third_level.forth_level.sales_date" }
}
}.
"price": "$first_level.second_level.third_level.forth_level.price"
}
...
}}
$ dateFromParts
を使用する
およびその他の
非正規化の開始
上記の「混乱」から明らかなことは、ネストされた配列での作業は必ずしも簡単ではないということです。このような構造は、MongoDB 3.6より前のリリースでは一般にアトミックに更新することさえできませんでした。また、それらを更新したり、基本的に配列全体を置き換えたりしたことがなくても、クエリを実行するのは簡単ではありません。これが表示されているものです。
しなければならない場所 親ドキュメント内に配列コンテンツがある場合は、通常、「フラット化」することをお勧めします。 および「非正規化」 そのような構造。これはリレーショナル思考に反しているように見えるかもしれませんが、パフォーマンス上の理由から、実際にはそのようなデータを処理するための最良の方法です:
{
"_id": 1,
"data": [
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
]
},
{
"_id": 2,
"data": [
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
},
{
"_id": 3,
"data": [
{
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
]
}
これはすべて最初に表示されたものと同じデータですが、ネストではありません 実際には、すべてを各親ドキュメント内の単一のフラット化された配列に配置します。確かにこれは複製を意味します さまざまなデータポイントの違いがありますが、クエリの複雑さとパフォーマンスの違いは自明です。
db.collection.aggregate([
{ "$match": {
"data": {
"$elemMatch": {
"first_item": "A",
"second_item": "A",
"third_item": "A",
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}
}
}},
{ "$addFields": {
"data": {
"$filter": {
"input": "$data",
"cond": {
"$and": [
{ "$eq": [ "$$this.first_item", "A" ] },
{ "$eq": [ "$$this.second_item", "A" ] },
{ "$eq": [ "$$this.third_item", "A" ] },
{ "$gte": [ "$$this.sales_date", new Date("2018-11-01") ] },
{ "$lt": [ "$$this.sales_date", new Date("2018-12-01") ] }
]
}
}
}
}},
{ "$unwind": "$data" },
{ "$group": {
"_id": {
"date": "$data.sales_date",
"price": "$data.price",
},
"quantity_sold": { "$avg": "$data.quantity" }
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quantity_sold": "$quantity_sold"
}
},
"quantity_sold": { "$avg": "$quantity_sold" }
}}
])
これらをネストする代わりに、 $ elemMatch
同様に、 $ filter
を呼び出します。
式、すべてがはるかに明確で読みやすく、処理が非常に簡単です。クエリで使用される配列内の要素のキーに実際にインデックスを付けることさえできるという別の利点があります。それはネストされたの制約でした MongoDBがそのような
「配列コンテンツフィルタリング」以降のすべて その後、 "data.sales_date"
のようなパス名だけを除いて、まったく同じままです。 長く曲がりくねった"first_level.second_level.third_level.forth_level.sales_date"
とは対照的に 前の構造から。
埋め込まない場合
最後に、もう1つの大きな誤解は、すべての関係 配列内の埋め込みとして変換する必要があります。これは実際にはMongoDBの意図ではなく、「結合」ではなくデータの単一の取得を行うことを意味する場合にのみ、同じドキュメント内の「関連する」データを配列に保持することを目的としていました。
ここでの古典的な「注文/詳細」モデルは、通常、現代の世界で、顧客の住所、注文合計などの詳細を、の詳細と同じ「画面」内に表示する「注文」の「ヘッダー」を表示する場合に適用されます。 「注文」のさまざまな広告申込情報。
RDBMSの開始にさかのぼると、典型的な80文字×25行の画面では、1つの画面にそのような「ヘッダー」情報が表示され、購入したすべての詳細行は別の画面に表示されていました。したがって、当然、それらを別々のテーブルに格納するためのある程度の常識がありました。世界がそのような「画面」でより詳細に移動するにつれて、通常は全体、または少なくとも「ヘッダー」とそのような「順序」の最初の非常に多くの行を見たいと思うでしょう。
したがって、MongoDBは関連データを含む「ドキュメント」を一度に返すため、この種の配置を配列に入れることが理にかなっているのはなぜですか。個別にレンダリングされた画面を個別にリクエストする必要はなく、データはすでに「事前結合」されているため、そのようなデータに「結合」する必要もありません。
必要かどうかを検討してください-別名「完全に」非正規化
したがって、ほとんどの場合、そのような配列内のほとんどのデータを実際に処理することに関心がないことがほとんどわかっている場合は、通常、すべてを1つのコレクションにまとめ、別のプロパティを追加する方が理にかなっています。そのような「参加」が時折必要とされる場合、「親」を識別するために:
{
"_id": 1,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 2,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-01"),
"quantity": 1
},
{
"_id": 3,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-02"),
"quantity": 1
},
{
"_id": 4,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 5,
"parent_id": 1,
"first_item": "A",
"second_item": "A",
"third_item": "B",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 6,
"parent_id": 1,
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 7,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 2,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 8,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-10-31"),
"quantity": 1
},
{
"_id": 9,
"parent_id": 2,
"first_item": "A",
"second_item": "A",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
},
{
"_id": 10,
"parent_id": 3,
"first_item": "A",
"second_item": "B",
"third_item": "A",
"price": 1,
"sales_date": new Date("2018-11-03"),
"quantity": 1
}
繰り返しますが、これは同じデータですが、今回は完全に別のドキュメントで、別の目的で実際に必要になる可能性がある場合に備えて、親への参照が含まれています。ここでの集計はすべて親データとはまったく関係がないことに注意してください。また、別のコレクションに保存するだけで、パフォーマンスの向上と複雑さの解消がどこでもたらされるかも明らかです。
db.collection.aggregate([
{ "$match": {
"first_item": "A",
"second_item": "A",
"third_item": "A",
"sales_date": {
"$gte": new Date("2018-11-01"),
"$lt": new Date("2018-12-01")
}
}},
{ "$group": {
"_id": {
"date": "$sales_date",
"price": "$price"
},
"quantity_sold": { "$avg": "$quantity" }
}},
{ "$group": {
"_id": "$_id.date",
"prices": {
"$push": {
"price": "$_id.price",
"quantity_sold": "$quantity_sold"
}
},
"quantity_sold": { "$avg": "$quantity_sold" }
}}
])
すべてがすでにドキュメントであるため、「配列をフィルタリングする」必要はありません。 または他の複雑さのいずれかを持っています。一致するドキュメントを選択して結果を集計するだけで、これまでとまったく同じ2つの最終ステップが実行されます。
最終結果を得るだけの目的で、これは上記のいずれの方法よりもはるかに優れたパフォーマンスを発揮します。問題のクエリは実際には「詳細」データのみに関係しているため、最善のアクションは、常に最高のパフォーマンス上の利点を提供するため、詳細を親から完全に分離することです。
そして、ここでの全体的なポイントは、アプリケーションの残りの部分の実際のアクセスパターンが決してないということです。 配列の内容全体を返す必要があります。そうすれば、とにかく埋め込まれるべきではなかったでしょう。どうやらほとんどの「書き込み」操作は、同様に関連する親に触れる必要がないはずです。これは、これが機能するかどうかを決定するもう1つの要因です。
結論
一般的なメッセージは、原則として、配列をネストしてはならないということです。せいぜい、関連する親ドキュメント内に部分的に非正規化されたデータを含む「単一の」配列を保持する必要があります。残りのアクセスパターンでは、親と子がまったく連携して使用されない場合は、データを実際に分離する必要があります。
「大きな」変化は、データの正規化が実際に優れていると考えるすべての理由が、そのような組み込みドキュメントシステムの敵であることが判明したことです。 「結合」を回避することは常に良いことですが、「結合された」データの外観を持つ複雑なネストされた構造を作成しても、実際にはうまくいきません。
正規化であるとあなたが「考える」ことに対処するためのコストは、通常、最終的なストレージ内の複製および非正規化データの追加のストレージとメンテナンスを超えることになります。
上記のすべてのフォームが同じ結果セットを返すことにも注意してください。簡潔にするためのサンプルデータには単一のアイテムのみが含まれている、または多くても複数の価格ポイントがある場合、「平均」は依然として 1
であるという点で、かなり派生的です。 それがとにかくすべての値が何であるかだからです。しかし、これを説明する内容はすでに非常に長いので、実際には「例として」だけです:
{
"_id" : ISODate("2018-11-01T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}
{
"_id" : ISODate("2018-11-02T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}
{
"_id" : ISODate("2018-11-03T00:00:00Z"),
"prices" : [
{
"price" : 1,
"quantity_sold" : 1
},
{
"price" : 2,
"quantity_sold" : 1
}
],
"quantity_sold" : 1
}