MongoDB
 sql >> Cơ Sở Dữ Liệu >  >> NoSQL >> MongoDB

Tổng hợp Mongodb $ group, giới hạn độ dài của mảng

Hiện đại

Từ MongoDB 3.6, có một cách tiếp cận "mới" cho việc này bằng cách sử dụng $lookup để thực hiện "tự nối" theo cách giống như cách xử lý con trỏ ban đầu được trình bày bên dưới.

Vì trong bản phát hành này, bạn có thể chỉ định một "pipeline" đối số cho $lookup làm nguồn cho "tham gia", điều này về cơ bản có nghĩa là bạn có thể sử dụng $match$limit để thu thập và "giới hạn" các mục nhập cho mảng:

db.messages.aggregate([
  { "$group": { "_id": "$conversation_ID" } },
  { "$lookup": {
    "from": "messages",
    "let": { "conversation": "$_id" },
    "pipeline": [
      { "$match": { "$expr": { "$eq": [ "$conversation_ID", "$$conversation" ] } }},
      { "$limit": 10 },
      { "$project": { "_id": 1 } }
    ],
    "as": "msgs"
  }}
])

Bạn có thể tùy chọn thêm phép chiếu bổ sung sau khi $lookup để làm cho các mục của mảng chỉ đơn giản là các giá trị thay vì các tài liệu có _id nhưng kết quả cơ bản là có bằng cách làm như trên.

Vẫn còn tồn tại SERVER-9277 thực sự yêu cầu một "giới hạn để đẩy" trực tiếp, nhưng sử dụng $lookup theo cách này là một giải pháp thay thế khả thi trong thời gian tạm thời.

LƯU Ý :Ngoài ra còn có $slice được giới thiệu sau khi viết câu trả lời ban đầu và được đề cập bởi "vấn đề JIRA nổi bật" trong nội dung gốc. Mặc dù bạn có thể nhận được cùng một kết quả với các tập kết quả nhỏ, nhưng nó vẫn liên quan đến việc "đẩy mọi thứ" vào mảng và sau đó giới hạn đầu ra cuối cùng của mảng ở độ dài mong muốn.

Vì vậy, đó là sự khác biệt chính và lý do tại sao nó thường không thực tế đối với $slice cho kết quả lớn. Nhưng tất nhiên có thể được sử dụng luân phiên trong những trường hợp cần thiết.

Có một số chi tiết khác về giá trị nhóm mongodb theo nhiều trường về cách sử dụng thay thế.

Bản gốc

Như đã nói trước đó, đây không phải là không thể nhưng chắc chắn là một vấn đề khủng khiếp.

Trên thực tế, nếu mối quan tâm chính của bạn là các mảng kết quả của bạn sẽ đặc biệt lớn, thì cách tốt nhất của bạn là gửi cho mỗi "talk_ID" riêng biệt dưới dạng một truy vấn riêng lẻ và sau đó kết hợp các kết quả của bạn. Trong cú pháp rất MongoDB 2.6, có thể cần một số điều chỉnh tùy thuộc vào việc triển khai ngôn ngữ của bạn thực sự là gì:

var results = [];
db.messages.aggregate([
    { "$group": {
        "_id": "$conversation_ID"
    }}
]).forEach(function(doc) {
    db.messages.aggregate([
        { "$match": { "conversation_ID": doc._id } },
        { "$limit": 10 },
        { "$group": {
            "_id": "$conversation_ID",
            "msgs": { "$push": "$_id" }
        }}
    ]).forEach(function(res) {
        results.push( res );
    });
});

Nhưng tất cả phụ thuộc vào việc đó có phải là điều bạn đang cố gắng tránh hay không. Vì vậy, câu trả lời thực sự:

Vấn đề đầu tiên ở đây là không có chức năng "giới hạn" số lượng mục được "đẩy" vào một mảng. Đó chắc chắn là thứ chúng tôi muốn, nhưng chức năng này hiện không tồn tại.

Vấn đề thứ hai là ngay cả khi đẩy tất cả các mục vào một mảng, bạn không thể sử dụng $slice , hoặc bất kỳ toán tử nào tương tự trong đường dẫn tổng hợp. Vì vậy, không có cách nào hiện nay để chỉ nhận được kết quả "top 10" từ một mảng đã tạo bằng một thao tác đơn giản.

Nhưng bạn thực sự có thể tạo ra một tập hợp các hoạt động để "cắt" một cách hiệu quả các ranh giới nhóm của bạn. Nó khá liên quan, và ví dụ ở đây tôi sẽ giảm các phần tử mảng "cắt" xuống chỉ "sáu". Lý do chính ở đây là để chứng minh quy trình và chỉ ra cách thực hiện điều này mà không bị phá hủy với các mảng không chứa tổng số bạn muốn "cắt".

Đưa ra một mẫu tài liệu:

{ "_id" : 1, "conversation_ID" : 123 }
{ "_id" : 2, "conversation_ID" : 123 }
{ "_id" : 3, "conversation_ID" : 123 }
{ "_id" : 4, "conversation_ID" : 123 }
{ "_id" : 5, "conversation_ID" : 123 }
{ "_id" : 6, "conversation_ID" : 123 }
{ "_id" : 7, "conversation_ID" : 123 }
{ "_id" : 8, "conversation_ID" : 123 }
{ "_id" : 9, "conversation_ID" : 123 }
{ "_id" : 10, "conversation_ID" : 123 }
{ "_id" : 11, "conversation_ID" : 123 }
{ "_id" : 12, "conversation_ID" : 456 }
{ "_id" : 13, "conversation_ID" : 456 }
{ "_id" : 14, "conversation_ID" : 456 }
{ "_id" : 15, "conversation_ID" : 456 }
{ "_id" : 16, "conversation_ID" : 456 }

Bạn có thể thấy rằng khi nhóm theo các điều kiện của bạn, bạn sẽ nhận được một mảng có mười phần tử và một mảng khác có "năm". Những gì bạn muốn làm ở đây là giảm cả hai xuống "sáu" trên cùng mà không "phá hủy" mảng sẽ chỉ khớp với "năm" phần tử.

Và truy vấn sau:

db.messages.aggregate([
    { "$group": {
        "_id": "$conversation_ID",
        "first": { "$first": "$_id" },
        "msgs": { "$push": "$_id" },
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "seen": { "$eq": [ "$first", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "seen": { "$eq": [ "$second", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "seen": { "$eq": [ "$third", "$msgs" ] },
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "forth": 1,
        "seen": { "$eq": [ "$forth", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$forth" },
        "fifth": { "$first": "$msgs" }
    }},
    { "$unwind": "$msgs" },
    { "$project": {
        "msgs": 1,
        "first": 1,
        "second": 1,
        "third": 1,
        "forth": 1,
        "fifth": 1,
        "seen": { "$eq": [ "$fifth", "$msgs" ] }
    }},
    { "$sort": { "seen": 1 }},
    { "$group": {
        "_id": "$_id",
        "msgs": { 
            "$push": {
               "$cond": [ { "$not": "$seen" }, "$msgs", false ]
            }
        },
        "first": { "$first": "$first" },
        "second": { "$first": "$second" },
        "third": { "$first": "$third" },
        "forth": { "$first": "$forth" },
        "fifth": { "$first": "$fifth" },
        "sixth": { "$first": "$msgs" },
    }},
    { "$project": {
         "first": 1,
         "second": 1,
         "third": 1,
         "forth": 1,
         "fifth": 1,
         "sixth": 1,
         "pos": { "$const": [ 1,2,3,4,5,6 ] }
    }},
    { "$unwind": "$pos" },
    { "$group": {
        "_id": "$_id",
        "msgs": {
            "$push": {
                "$cond": [
                    { "$eq": [ "$pos", 1 ] },
                    "$first",
                    { "$cond": [
                        { "$eq": [ "$pos", 2 ] },
                        "$second",
                        { "$cond": [
                            { "$eq": [ "$pos", 3 ] },
                            "$third",
                            { "$cond": [
                                { "$eq": [ "$pos", 4 ] },
                                "$forth",
                                { "$cond": [
                                    { "$eq": [ "$pos", 5 ] },
                                    "$fifth",
                                    { "$cond": [
                                        { "$eq": [ "$pos", 6 ] },
                                        "$sixth",
                                        false
                                    ]}
                                ]}
                            ]}
                        ]}
                    ]}
                ]
            }
        }
    }},
    { "$unwind": "$msgs" },
    { "$match": { "msgs": { "$ne": false } }},
    { "$group": {
        "_id": "$_id",
        "msgs": { "$push": "$msgs" }
    }}
])

Bạn nhận được kết quả hàng đầu trong mảng, tối đa sáu mục nhập:

{ "_id" : 123, "msgs" : [ 1, 2, 3, 4, 5, 6 ] }
{ "_id" : 456, "msgs" : [ 12, 13, 14, 15 ] }

Như bạn có thể thấy ở đây, rất nhiều điều thú vị.

Sau khi bạn đã nhóm ban đầu, về cơ bản, bạn muốn "bật" $first giá trị của ngăn xếp cho các kết quả mảng. Để làm cho quá trình này được đơn giản hóa một chút, chúng tôi thực sự làm điều này trong hoạt động ban đầu. Vì vậy, quá trình trở thành:

  • $unwind mảng
  • So sánh với các giá trị đã thấy với $eq trận đấu bình đẳng
  • $sort kết quả thành "float" false các giá trị không nhìn thấy ở trên cùng (điều này vẫn giữ nguyên thứ tự)
  • $group quay lại lần nữa và "bật" $first giá trị không nhìn thấy là thành viên tiếp theo trên ngăn xếp. Ngoài ra, điều này sử dụng $cond toán tử để thay thế các giá trị "đã thấy" trong ngăn xếp mảng bằng false để giúp đánh giá.

Hành động cuối cùng với $cond ở đó để đảm bảo rằng các lần lặp lại trong tương lai không chỉ cộng lại giá trị cuối cùng của mảng khi số lượng "lát cắt" lớn hơn các thành viên của mảng.

Toàn bộ quá trình đó cần được lặp lại cho bao nhiêu mục bạn muốn "cắt". Vì chúng tôi đã tìm thấy mục "đầu tiên" trong nhóm ban đầu, điều đó có nghĩa là n-1 lặp lại cho kết quả lát mong muốn.

Các bước cuối cùng thực sự chỉ là một minh họa tùy chọn về việc chuyển đổi mọi thứ trở lại thành mảng để có kết quả như được hiển thị cuối cùng. Vì vậy, thực sự chỉ cần đẩy các mục có điều kiện hoặc false trở lại theo vị trí phù hợp của chúng và cuối cùng "lọc" ra tất cả false giá trị để các mảng cuối có "sáu" và "năm" thành viên tương ứng.

Vì vậy, không có toán tử tiêu chuẩn nào để đáp ứng điều này, và bạn không thể chỉ "giới hạn" việc đẩy ở 5 hoặc 10 hoặc bất kỳ mục nào trong mảng. Nhưng nếu bạn thực sự phải làm điều đó, thì đây là cách tiếp cận tốt nhất của bạn.

Bạn có thể tiếp cận điều này với mapReduce và từ bỏ khung tổng hợp cùng nhau. Cách tiếp cận mà tôi sẽ thực hiện (trong giới hạn hợp lý) sẽ là có một bản đồ băm trong bộ nhớ trên máy chủ một cách hiệu quả và tích lũy các mảng vào đó, trong khi sử dụng JavaScript slice để "giới hạn" kết quả:

db.messages.mapReduce(
    function () {

        if ( !stash.hasOwnProperty(this.conversation_ID) ) {
            stash[this.conversation_ID] = [];
        }

        if ( stash[this.conversation_ID.length < maxLen ) {
            stash[this.conversation_ID].push( this._id );
            emit( this.conversation_ID, 1 );
        }

    },
    function(key,values) {
        return 1;   // really just want to keep the keys
    },
    { 
        "scope": { "stash": {}, "maxLen": 10 },
        "finalize": function(key,value) {
            return { "msgs": stash[key] };                
        },
        "out": { "inline": 1 }
    }
)

Vì vậy, về cơ bản chỉ xây dựng đối tượng "trong bộ nhớ" khớp với các "khóa" được phát ra với một mảng không bao giờ vượt quá kích thước tối đa mà bạn muốn tìm nạp từ kết quả của mình. Ngoài ra, điều này thậm chí không bận tâm đến việc "phát ra" vật phẩm khi đáp ứng được ngăn xếp tối đa.

Phần giảm thực sự không làm gì khác ngoài cơ bản chỉ giảm thành "khóa" và một giá trị duy nhất. Vì vậy, đề phòng trường hợp trình giảm thiểu của chúng tôi không được gọi, điều này sẽ đúng nếu chỉ có 1 giá trị tồn tại cho một khóa, thì hàm finalize sẽ xử lý ánh xạ các khóa "stash" đến đầu ra cuối cùng.

Hiệu quả của điều này khác nhau tùy thuộc vào kích thước của đầu ra và đánh giá JavaScript chắc chắn không nhanh, nhưng có thể nhanh hơn xử lý các mảng lớn trong một đường dẫn.

Bỏ phiếu cho các vấn đề JIRA để thực sự có toán tử "lát cắt" hoặc thậm chí là "giới hạn" trên "$ push" và "$ addToSet", cả hai đều sẽ hữu ích. Cá nhân tôi hy vọng rằng ít nhất một số sửa đổi có thể được thực hiện đối với $map toán tử để hiển thị giá trị "chỉ số hiện tại" khi xử lý. Điều đó sẽ cho phép "cắt" và các hoạt động khác một cách hiệu quả.

Thực sự bạn sẽ muốn viết mã này để "tạo" tất cả các lần lặp bắt buộc. Nếu câu trả lời ở đây nhận được đủ tình yêu và / hoặc thời gian khác mà tôi đang chờ xử lý, thì tôi có thể thêm một số mã để minh họa cách thực hiện điều này. Nó đã là một phản hồi khá dài.

Mã để tạo đường dẫn:

var key = "$conversation_ID";
var val = "$_id";
var maxLen = 10;

var stack = [];
var pipe = [];
var fproj = { "$project": { "pos": { "$const": []  } } };

for ( var x = 1; x <= maxLen; x++ ) {

    fproj["$project"][""+x] = 1;
    fproj["$project"]["pos"]["$const"].push( x );

    var rec = {
        "$cond": [ { "$eq": [ "$pos", x ] }, "$"+x ]
    };
    if ( stack.length == 0 ) {
        rec["$cond"].push( false );
    } else {
        lval = stack.pop();
        rec["$cond"].push( lval );
    }

    stack.push( rec );

    if ( x == 1) {
        pipe.push({ "$group": {
           "_id": key,
           "1": { "$first": val },
           "msgs": { "$push": val }
        }});
    } else {
        pipe.push({ "$unwind": "$msgs" });
        var proj = {
            "$project": {
                "msgs": 1
            }
        };
        
        proj["$project"]["seen"] = { "$eq": [ "$"+(x-1), "$msgs" ] };
       
        var grp = {
            "$group": {
                "_id": "$_id",
                "msgs": {
                    "$push": {
                        "$cond": [ { "$not": "$seen" }, "$msgs", false ]
                    }
                }
            }
        };

        for ( n=x; n >= 1; n-- ) {
            if ( n != x ) 
                proj["$project"][""+n] = 1;
            grp["$group"][""+n] = ( n == x ) ? { "$first": "$msgs" } : { "$first": "$"+n };
        }

        pipe.push( proj );
        pipe.push({ "$sort": { "seen": 1 } });
        pipe.push(grp);
    }
}

pipe.push(fproj);
pipe.push({ "$unwind": "$pos" });
pipe.push({
    "$group": {
        "_id": "$_id",
        "msgs": { "$push": stack[0] }
    }
});
pipe.push({ "$unwind": "$msgs" });
pipe.push({ "$match": { "msgs": { "$ne": false } }});
pipe.push({
    "$group": {
        "_id": "$_id",
        "msgs": { "$push": "$msgs" }
    }
}); 

Điều đó xây dựng phương pháp lặp lại cơ bản lên đến maxLen với các bước từ $unwind tới $group . Cũng được nhúng trong đó có các chi tiết về các phép chiếu cuối cùng được yêu cầu và câu lệnh điều kiện "lồng nhau". Cuối cùng về cơ bản là cách tiếp cận được thực hiện cho câu hỏi này:

$ Trong điều khoản của MongoDB có đảm bảo đơn đặt hàng không?



  1. Redis
  2.   
  3. MongoDB
  4.   
  5. Memcached
  6.   
  7. HBase
  8.   
  9. CouchDB
  1. Tại sao nên học MongoDB - 10 lý do nên học MongoDB cho năm 2022

  2. mongoengine - Truy vấn trên ListField của EmbeddedDocumentField

  3. Cách thay thế tài liệu hiện có khi nhập tệp vào MongoDB

  4. Mongo sắp xếp theo một điều kiện được tính toán

  5. Các tài liệu phụ Mongoose so với lược đồ lồng nhau