IT

어레이의 모든 요소가 조건과 일치하는지 확인합니다.

itgroup 2023. 5. 28. 20:35
반응형

어레이의 모든 요소가 조건과 일치하는지 확인합니다.

문서 모음이 있습니다.

date: Date
users: [
  { user: 1, group: 1 }
  { user: 5, group: 2 }
]

date: Date
users: [
  { user: 1, group: 1 }
  { user: 3, group: 2 }
]

사용자 배열에 있는 모든 사용자 ID가 다른 배열인 [1, 5, 7]에 있는 모든 문서를 찾기 위해 이 컬렉션에 대해 쿼리하려고 합니다.이 예제에서는 첫 번째 문서만 일치합니다.

제가 찾은 가장 좋은 해결책은 다음과 같습니다.

$where: function() { 
  var ids = [1, 5, 7];
  return this.users.every(function(u) { 
    return ids.indexOf(u.user) !== -1;
  });
}

유감스럽게도 이는 성능을 저하시키는 것으로 보입니다. $where 문서에는 다음과 같은 내용이 나와 있습니다.

$where는 JavaScript를 평가하며 인덱스를 활용할 수 없습니다.

이 쿼리를 개선하려면 어떻게 해야 합니까?

원하는 쿼리는 다음과 같습니다.

db.collection.find({"users":{"$not":{"$elemMatch":{"user":{$nin:[1,5,7]}}}}})

목록 1,5,7 이외의 요소가 없는 모든 문서를 찾으라는 메시지입니다.

더 나은 방법은 모르겠지만, 이것에 접근하는 몇 가지 다른 방법이 있으며, 사용 가능한 MongoDB 버전에 따라 다릅니다.

이것이 당신의 의도인지 아닌지는 잘 모르겠지만, 논리가 구현될 때 샘플 배열 내에 포함되어야 하는 해당 문서 배열 내의 요소와 일치하기 때문에 표시된 쿼리는 첫 번째 문서 예제와 일치합니다.

따라서 문서에 이러한 모든 요소가 포함되도록 하려면 운영자가 선택해야 합니다.

db.collection.find({ "users.user": { "$all": [ 1, 5, 7 ] } })

하지만 당신의 논리가 실제로 의도된 것이라는 가정 하에 작업을 한다면 적어도 제안에 따르면, 당신은 평가된 자바스크립트에서 당신의 ** 조건의 대상이 되는 문서가 줄어들도록 운영자와 결합함으로써 그 결과들을 "필터링"할 수 있습니다.

db.collection.find({
    "users.user": { "$in": [ 1, 5, 7 ] },
    "$where": function() { 
        var ids = [1, 5, 7];
        return this.users.every(function(u) { 
            return ids.indexOf(u.user) !== -1;
        });
    }
})

그리고 실제 스캔한 문서의 배열 요소 수에 일치하는 문서의 요소 수를 곱하지만 추가 필터를 사용하지 않는 것보다 더 낫습니다.

또는 실제 어레이 조건에 따라 연산자와 함께 사용되는 연산자의 논리적 추상화를 고려할 수도 있습니다.

db.collection.find({
    "$or": [
        { "users.user": { "$all": [ 1, 5, 7 ] } },
        { "users.user": { "$all": [ 1, 5 ] } },
        { "users.user": { "$all": [ 1, 7 ] } },
        { "users": { "$size": 1 }, "users.user": 1 },
        { "users": { "$size": 1 }, "users.user": 5 },
        { "users": { "$size": 1 }, "users.user": 7 }
    ]
})

따라서 이는 일치 조건의 가능한 모든 순열의 세대이지만, 사용 가능한 설치 버전에 따라 성능이 달라질 수 있습니다.

참고: 이 경우 완전히 실패합니다. 완전히 다른 작업을 수행하고 실제로 논리적인 결과를 낳기 때문입니다.


MongoDB 2.6 이상에서는 수집된 문서의 수에 따라 마일리지가 가장 효율적일 수 있습니다.

db.problem.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Just keeping the "user" element value
    { "$group": {
        "_id": "$_id",
        "users": { "$push": "$users.user" }
    }},

    // Compare to see if all elements are a member of the desired match
    { "$project": {
        "match": { "$setEquals": [
            { "$setIntersection": [ "$users", [ 1, 5, 7 ] ] },
            "$users"
        ]}
    }},

    // Filter out any documents that did not match
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

따라서 이 접근 방식은 내용을 비교하기 위해 새로 도입된 집합 연산자를 사용합니다. 물론 비교하려면 배열을 재구성해야 합니다.

지적된 바와 같이, 위의 결합된 연산자와 동일한 연산자를 단일 연산자에서 수행하는 직접 연산자가 있습니다.

db.collection.aggregate([
    { "$match": { 
        "users.user": { "$in": [ 1,5,7 ] } 
    }},
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},
    { "$unwind": "$users" },
    { "$group": {
        "_id": "$_id",
        "users": { "$push": "$users.user" }
    }},
    { "$project": {
        "match": { "$setIsSubset": [ "$users", [ 1, 5, 7 ] ] }
    }},
    { "$match": { "match": true } },
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

또는 MongoDB 2.6의 연산자를 활용하면서 다른 접근 방식을 사용할 수도 있습니다.

db.collection.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    // and a note of it's current size
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
        "size": { "$size": "$users" }
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Filter array contents that do not match
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Count the array elements that did match
    { "$group": {
        "_id": "$_id",
        "size": { "$first": "$size" },
        "count": { "$sum": 1 }
    }},

    // Compare the original size to the matched count
    { "$project": { 
        "match": { "$eq": [ "$size", "$count" ] } 
    }},

    // Filter out documents that were not the same
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

물론 2.6 이전 버전에서는 조금 더 길게 감긴 했지만, 다음 중 어느 것을 여전히 수행할 수 있습니다.

db.collection.aggregate([
    // Match documents that "could" meet the conditions
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Keep your original document and a copy of the array
    { "$project": {
        "_id": {
            "_id": "$_id",
            "date": "$date",
            "users": "$users"
        },
        "users": 1,
    }},

    // Unwind the array copy
    { "$unwind": "$users" },

    // Group it back to get it's original size
    { "$group": { 
        "_id": "$_id",
        "users": { "$push": "$users" },
        "size": { "$sum": 1 }
    }},

    // Unwind the array copy again
    { "$unwind": "$users" },

    // Filter array contents that do not match
    { "$match": { 
        "users.user": { "$in": [ 1, 5, 7 ] } 
    }},

    // Count the array elements that did match
    { "$group": {
        "_id": "$_id",
        "size": { "$first": "$size" },
        "count": { "$sum": 1 }
    }},

    // Compare the original size to the matched count
    { "$project": { 
        "match": { "$eq": [ "$size", "$count" ] } 
    }},

    // Filter out documents that were not the same
    { "$match": { "match": true } },

    // Return the original document form
    { "$project": {
        "_id": "$_id._id",
        "date": "$_id.date",
        "users": "$_id.users"
    }}
])

일반적으로 다양한 방법을 사용할 수 있습니다. 사용해 보고 무엇이 가장 효과적인지 확인하십시오.아마도 기존 양식과의 단순한 조합이 가장 좋을 것입니다.그러나 모든 경우 선택할 수 있는 인덱스가 있는지 확인합니다.

db.collection.ensureIndex({ "users.user": 1 })

여기 있는 모든 예처럼 어떤 식으로든 액세스할 수 있는 한 최상의 성능을 제공합니다.


평결

저는 이것에 흥미를 느껴서 결국 어떤 것이 최고의 성능을 가졌는지 보기 위해 테스트 케이스를 고안했습니다.먼저 테스트 데이터를 생성합니다.

var batch = [];
for ( var n = 1; n <= 10000; n++ ) {
    var elements = Math.floor(Math.random(10)*10)+1;

    var obj = { date: new Date(), users: [] };
    for ( var x = 0; x < elements; x++ ) {
        var user = Math.floor(Math.random(10)*10)+1,
            group = Math.floor(Math.random(10)*10)+1;

        obj.users.push({ user: user, group: group });
    }

    batch.push( obj );

    if ( n % 500 == 0 ) {
        db.problem.insert( batch );
        batch = [];
    }

} 

1개의 랜덤 배열이 있는 컬렉션에 10000개의 문서가 있습니다.무작위 값을 1.0으로 보유한 길이 10, 일치하는 문서 수 430개(일치에서 7749개 감소)에 도달했으며 다음과 같은 결과(평균)

  • 조항이 있는 자바스크립트: 420ms
  • 집계: 395ms
  • 그룹 어레이 수가 650ms인 Aggregate
  • 두 개의 집합 연산자가 있는 집계: 275ms
  • 애그리게이트: 250ms

샘플 전체에서 마지막 두 개를 제외한 모든 것이 약 100ms 더 빠른 피크 편차를 보였고, 마지막 두 개 모두 220ms의 응답을 나타냈습니다.가장 큰 변화는 JavaScript 쿼리에서 발생했으며 결과도 100ms 더 느리게 표시되었습니다.

하지만 여기서 중요한 점은 VM 아래에 있는 노트북의 하드웨어에 대한 것입니다. 하지만 아이디어를 제공합니다.

따라서 집합 연산자가 포함된 Aggregate, 특히 MongoDB 2.6.1 버전은 단일 연산자로서 얻을 수 있는 약간의 추가적인 이점을 통해 성능 면에서 확실히 승리합니다.

(2.4 호환 방법으로 표시된 바와 같이) 이 프로세스에서 가장 큰 비용이 문(평균 100ms 이상)이 될 것이라는 점을 감안할 때 특히 흥미롭습니다. 따라서 선택 항목의 평균은 약 32ms이며 나머지 파이프라인 단계는 평균 100ms 미만으로 실행됩니다.따라서 통합과 JavaScript 성능에 대한 상대적인 개념을 제공합니다.

저는 엄격한 평등보다는 객체 비교를 통해 Asya의 솔루션을 구현하기 위해 하루의 상당 부분을 보냈습니다.그래서 저는 여기서 공유하기로 했습니다.

userIds에서 전체 사용자로 질문을 확장했다고 가정해 보겠습니다.모든 항목이 있는 모든 문서를 찾으려는 경우users어레이가 다른 사용자 어레이에 있습니다.[{user: 1, group: 3}, {user: 2, group: 5},...]

이것은 작동하지 않습니다.db.collection.find({"users":{"$not":{"$elemMatch":{"$nin":[{user: 1, group: 3},{user: 2, group: 5},...]}}}}})$nin은 엄격한 평등에만 효과가 있기 때문입니다.그래서 우리는 객체 배열에 대해 "배열에 없음"을 표현하는 다른 방법을 찾아야 합니다.그리고 사용하기$where쿼리 속도가 너무 느려집니다.

솔루션:

db.collection.find({
 "users": {
   "$not": {
     "$elemMatch": {
       // if all of the OR-blocks are true, element is not in array
       "$and": [{
         // each OR-block == true if element != that user
         "$or": [
           "user": { "ne": 1 },
           "group": { "ne": 3 }
         ]
       }, {
         "$or": [
           "user": { "ne": 2 },
           "group": { "ne": 5 }
         ]
       }, {
         // more users...
       }]
     }
   }
 }
})

논리를 정리하려면: $elemMatch는 배열에 없는 사용자가 있는 모든 문서와 일치합니다.따라서 $not은 배열에 모든 사용자가 있는 모든 문서와 일치하지 않습니다.

언급URL : https://stackoverflow.com/questions/23595023/check-if-every-element-in-array-matches-condition

반응형