Finding two elements in an array of documents that appear in a given order

a 夏天 提交于 2019-12-25 00:59:05

问题


Let's assume I have the following document structure in database:

{ name: String,
  subDocs: [{
    index: Number,
    value: Number
  }]
}

So that a typical document looks like:

{ name: 'Document 1',
  subDocs: [ 
    { index: 0, value: 30 },
    { index: 1, value: 40 },
    { index: 2, value: 10 },
    { index: 3, value: 20 },
    { index: 4, value: 700 },
    { index: 5, value: 40 }
  ]
}

Now, I would like to find all documents which contain subDocs with values A = 10 and B = 40 BUT the occurrence of the items in the array must meet the following requirement A.index < B.index. So basically the item with the value of A must appear earlier in the collection than B. So the object above meets the requirement while this one not, because the values don't appear in the order:

{ name: 'Document 2',
  subDocs: [ 
    { index: 0, value: 40 },
    { index: 1, value: 70 },
    { index: 2, value: 10 },
    { index: 3, value: 20 },
    { index: 4, value: 700 }
  ]
}

Can we achieve it with Mongoose yet not sacrificing performance of such a query?


回答1:


If you want that sort of constraint in the query, then you basically have two options depending on what your MongoDB version supports:

MongoDB 3.6

You would preferably use $expr "in addition" to any normal query conditions to actually select valid documents:

var A = 10, B = 40;

Model.find({
  "subDocs.value": { "$all": [A, B] },
  "$expr": {
    "$lt": [
      { "$arrayElemAt": [
        "$subDocs.index",
        { "$indexOfArray": [ "$subDocs.value", A ]}
      ]},
      { "$arrayElemAt": [
        "$subDocs.index",
        { "$indexOfArray": [ "$subDocs.value", B ]}  
      ]}
    ]
  }
})

Or matching the "last" occurrence:

Model.find({
  "subDocs.value": { "$all": [A, B] },
  "$expr": {
    "$lt": [
      { "$arrayElemAt": [
        "$subDocs.index",
        { "$subtract": [
          { "$subtract": [{ "$size": "$subDocs.value" }, 1 ] },
          { "$indexOfArray": [ { "$reverseArray": "$subDocs.value" }, A ] }
        ]}
      ]},
      { "$arrayElemAt": [
        "$subDocs.index",
        { "$subtract": [
          { "$subtract": [{ "$size": "$subDocs.value" }, 1 ] },
          { "$indexOfArray": [ { "$reverseArray": "$subDocs.value" }, B ] }
        ]}
      ]}
    ]
  }
})

Earlier Versions

Same thing, but without native operators you need to use the JavaScript evaluation of $where:

var A = 10, B = 40;

Model.find({
  "subDocs.value": { "$all": [A, B] },
  "$where": `this.subDocs.find( e => e.value === ${A}).index
      < this.subDocs.find( e => e.value === ${B}).index`
})

Or matching the "last" occurrence:

Model.find({
  "subDocs.value": { "$all": [10,40] },
  "$where": `let arr = this.subDocs.reverse();
      return arr.find( e => e.value === ${A}).index
        > arr.find( e => e.value === ${B}).index`
})

If you needed that in an aggregation pipeline, then you would use $redact and similar logic to the first example instead:

var A = 10, B = 40;

Model.aggregate([
  { "$match": { "subDocs.value": { "$all": [A, B] } } },
  { "$redact": {
    "$cond": {
      "if": {
        "$lt": [
          { "$arrayElemAt": [
            "$subDocs.index",
            { "$indexOfArray": [ "$subDocs.value", A ]}
          ]},
          { "$arrayElemAt": [
            "$subDocs.index",
            { "$indexOfArray": [ "$subDocs.value", B ]}  
          ]}
        ]
      },
      "then": "$$KEEP",
      "else": "$$PRUNE"
    }
  }}
])

Suffice to say that the "comparison logic" is not actually native to "query operator expressions" themselves, so the only part that "optimally" can be applied to an index is using the $all query operator in all cases. The essential remaining logic actually applies "after" that main expression is evaluated and "in addition to" in order that no results are returned other that those meeting the expression with either $expr or $where.

The basic logic of each is essentially to extract the value of the "index" property from the "first" array member that actually matches the respective value in the "value" property. Where this is "less than", then the condition is true and this satisfies the document being returned.

So note that either "calculated evaluation" matches the efficiency of query operators, and without being used "in combination" of other query operator conditions which are able to access an "index", then a "full collection scan" will be initiated.

But the overall result is certainly more efficient than returning all matching items to the first query condition and then rejecting them on the cursor "after" returning from the database.


See also documentation for $arrayElemAt, $indexOfArray, $lt and Array.find() for JavaScript



来源:https://stackoverflow.com/questions/49972032/finding-two-elements-in-an-array-of-documents-that-appear-in-a-given-order

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!