Tìm hiểu các MongoDB aggregation operator hay dùng
mongodb
19
White

Nguyễn Tuấn Anh viết ngày 23/12/2019

1. Aggregation overview

Aggregation là một framework tổng hợp dữ liệu của MongoDB. Aggregation được xây dựng dựa trên mô hình xử lý dữ liệu dưới dạng pipeline. Aggregation pipeline bao gồm nhiều stage. Trong mỗi stage, chúng ta sử dụng một aggregation operator để biến đổi dữ liệu của các input document. Các output document của stage phía trước sẽ là input document của stage ngay sau. Các aggregation operator có thể được sử dụng nhiều lần trong pipeline, ngoại trừ $out, $merge, và $geoNear.

Điểm mạnh của aggregation framework là:

  • Xử lý nhanh và mạnh mẽ với lượng ít băng thông.
  • Giải quyết được các yêu cầu phức tạp.
  • Có thể làm việc với dữ liệu lớn.

MongoDB cung cấp phương thức db.collection.aggregate() để chạy aggregation pipeline.

Cú pháp:

db.userCollection.aggregate( [ { <stage 1> }, { <stage 2> }, ..., { <stage N> } ], { <options> } );

Trong đó:

  • Các stage được đặt trong một array theo thứ tự thực hiện trước sau.
  • Các option là tùy chọn, không nhất thiết phải có.

...

Để minh họa cho các operator, mình sẽ sử dụng 2 collection là orderscustomers.

Collection orders:

/* 1 */
{
    "_id" : ObjectId("5dfed89743e6fed50628907c"),
    "cust_id" : "A123",
    "products" : [ 
        "apple", 
        "lemon"
    ],
    "amount" : 500,
    "status" : "completed"
}

/* 2 */
{
    "_id" : ObjectId("5dfed8a643e6fed50628908a"),
    "cust_id" : "B456",
    "products" : [ 
        "lemon"
    ],
    "amount" : 100,
    "status" : "processing"
}

/* 3 */
{
    "_id" : ObjectId("5dfed8b043e6fed50628908f"),
    "cust_id" : "B456",
    "products" : [ 
        "apple", 
        "orange"
    ],
    "amount" : 300,
    "status" : "completed"
}

/* 4 */
{
    "_id" : ObjectId("5dfed8b943e6fed506289094"),
    "cust_id" : "C789",
    "products" : [ 
        "apple", 
        "lemon", 
        "orange"
    ],
    "amount" : 800,
    "status" : "completed"
}

/* 5 */
{
    "_id" : ObjectId("5dfed8c343e6fed506289097"),
    "cust_id" : "A123",
    "products" : [ 
        "apple"
    ],
    "amount" : 250,
    "status" : "completed"
}

Collection customers:

/* 1 */
{
    "_id": "A123",
    "name": "Alice"
}

/* 2 */
{
    "_id": "B456",
    "name": "Bob"
}

/* 3 */
{
    "_id": "C789",
    "name": "Carol"
}

2. $match

$match được dùng để lọc các document theo một điều kiện nào đó. $match tương tự như WHEREHAVING trong SQL.

Cú pháp:

{ $match: { <query> } }

Cú pháp query của $match y hệt cú pháp của read operation query (tương tự như find()).

Ví dụ:

Lọc các order của customer có ID là A123:

db.orders.aggregate([
    { 
        $match: { 
            cust_id: "A123" 
        } 
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : ObjectId("5dfed89743e6fed50628907c"),
    "cust_id" : "A123",
    "products" : [ 
        "apple", 
        "lemon"
    ],
    "amount" : 500,
    "status" : "completed"
}

/* 2 */
{
    "_id" : ObjectId("5dfed8c343e6fed506289097"),
    "cust_id" : "A123",
    "products" : [ 
        "apple"
    ],
    "amount" : 250,
    "status" : "completed"
}

3. $project

$project được dùng để chỉ định các field sẽ xuất hiện trong output document. Đó có thể là các field đã tồn tại trong input document, hoặc cũng có thể là các field được tính toán mới. $project tương tự như SELECT trong SQL.

Cú pháp:

{ $project: { <specification(s)> } }

Trong đó:

specification có thể có các dạng sau:

  • _id: <0 or false>: field _id sẽ không xuất hiện trong output document (mặc định _id luôn xuất hiện trong output document).
  • <field X>: <1 or true>: field X sẽ xuất hiện trong output document.
  • <field X>: <expression>: field X sẽ được tính toán dựa trên một expression nào đó.

Ví dụ:

db.orders.aggregate([
    { 
        $project: { 
            _id: 0,
            cust_id: 1,
            new_amount: { $add: ["$amount", 100] }
        } 
    }
])

=> Kết quả:

/* 1 */
{
    "cust_id" : "A123",
    "new_amount" : 600.0
}

/* 2 */
{
    "cust_id" : "B456",
    "new_amount" : 200.0
}

/* 3 */
{
    "cust_id" : "B456",
    "new_amount" : 400.0
}

/* 4 */
{
    "cust_id" : "C789",
    "new_amount" : 900.0
}

/* 5 */
{
    "cust_id" : "A123",
    "new_amount" : 350.0
}

4. $count

$count mới xuất hiện trong MongoDB version 3.4. $count trả về thêm trong output một field X chứa tổng số input document.

Cú pháp:

{ $count: <string> }

Trong đó:

<string> là tên của field X, phải khác rỗng, không được bắt đầu bằng ký tự $ và không được bao gồm ký tự ..

Ví dụ:

db.orders.aggregate([
    { 
        $count: "total"
    }
])

=> Kết quả:

/* 1 */
{
    "total" : 5
}

5. $limit$skip

$limit được dùng để giới hạn số lượng output document. $skip được dùng để chỉ định số lượng document sẽ bị bỏ qua trong output (tính từ document đầu tiên). $limit$skip tương tự như LIMITOFFSET trong SQL.

Cú pháp:

{ $limit: <positive integer> }
{ $skip: <positive integer> }

Ví dụ:

Lấy order thứ 3 và thứ 4:

db.orders.aggregate([
    { 
        $skip: 2
    },
    {
        $limit: 2
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : ObjectId("5dfed8b043e6fed50628908f"),
    "cust_id" : "B456",
    "products" : [ 
        "apple", 
        "orange"
    ],
    "amount" : 300,
    "status" : "completed"
}

/* 2 */
{
    "_id" : ObjectId("5dfed8b943e6fed506289094"),
    "cust_id" : "C789",
    "products" : [ 
        "apple", 
        "lemon", 
        "orange"
    ],
    "amount" : 800,
    "status" : "completed"
}

6. $sort

$sort được dùng để sắp xếp các document trong output theo một tiêu chí nào đó. $sort tương tự như ORDER BY trong SQL.

Cú pháp:

{ $sort: { <field1>: <sort order>, <field2>: <sort order> ... } }

Trong đó, <sort order> có thể có các giá trị sau:

  • 1: sắp xếp theo thứ tự tăng dần
  • -1: sắp xếp theo thứ tự giảm dần

Ví dụ:

Sắp xếp các order theo thứ tự giảm dần của amount:

db.orders.aggregate([
    { 
        $sort: { amount: -1 }
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : ObjectId("5dfed8b943e6fed506289094"),
    "cust_id" : "C789",
    "products" : [ 
        "apple", 
        "lemon", 
        "orange"
    ],
    "amount" : 800,
    "status" : "completed"
}

/* 2 */
{
    "_id" : ObjectId("5dfed89743e6fed50628907c"),
    "cust_id" : "A123",
    "products" : [ 
        "apple", 
        "lemon"
    ],
    "amount" : 500,
    "status" : "completed"
}

/* 3 */
{
    "_id" : ObjectId("5dfed8b043e6fed50628908f"),
    "cust_id" : "B456",
    "products" : [ 
        "apple", 
        "orange"
    ],
    "amount" : 300,
    "status" : "completed"
}

/* 4 */
{
    "_id" : ObjectId("5dfed8c343e6fed506289097"),
    "cust_id" : "A123",
    "products" : [ 
        "apple"
    ],
    "amount" : 250,
    "status" : "completed"
}

/* 5 */
{
    "_id" : ObjectId("5dfed8a643e6fed50628908a"),
    "cust_id" : "B456",
    "products" : [ 
        "lemon"
    ],
    "amount" : 100,
    "status" : "processing"
}

7. $group

$group được dùng để gom nhóm các input document theo expression _id. Mỗi nhóm tương ứng với một output document. Trong $group, chúng ta có thể sử dụng các accumulator expression như $sum, $avg, $max, $min, ...

$group tương tự như GROUP BY trong SQL.

Cú pháp:

{
  $group:
    {
      _id: <expression>, // Group By Expression
      <field1>: { <accumulator1> : <expression1> },
      ...
    }
 }

Nếu _id được set bằng null, MongoDB sẽ query tất cả các input document.

Ví dụ:

Gom nhóm các order theo cust_id, đồng thời tính tổng amount của từng cust_id:

db.orders.aggregate([
    { 
        $group: {
            _id: "$cust_id",
            total: { $sum: "$amount" }
        }
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : "B456",
    "total" : 400
}

/* 2 */
{
    "_id" : "C789",
    "total" : 800
}

/* 3 */
{
    "_id" : "A123",
    "total" : 750
}

8. $unwind

$unwind được dùng để phân tách giá trị của một array field trong các input document. Nếu như array field của một input document có N phần tử thì trong output sẽ có N document.

Cú pháp:

{ $unwind: <field path> }

Ví dụ:

Mình sẽ thử áp dụng $unwind với array field products để xem kết quả nó sẽ như thế nào:

db.orders.aggregate([
    { 
        $unwind: "$products"
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : ObjectId("5dfed89743e6fed50628907c"),
    "cust_id" : "A123",
    "products" : "apple",
    "amount" : 500,
    "status" : "completed"
}

/* 2 */
{
    "_id" : ObjectId("5dfed89743e6fed50628907c"),
    "cust_id" : "A123",
    "products" : "lemon",
    "amount" : 500,
    "status" : "completed"
}

/* 3 */
{
    "_id" : ObjectId("5dfed8a643e6fed50628908a"),
    "cust_id" : "B456",
    "products" : "lemon",
    "amount" : 100,
    "status" : "processing"
}

/* 4 */
{
    "_id" : ObjectId("5dfed8b043e6fed50628908f"),
    "cust_id" : "B456",
    "products" : "apple",
    "amount" : 300,
    "status" : "completed"
}

/* 5 */
{
    "_id" : ObjectId("5dfed8b043e6fed50628908f"),
    "cust_id" : "B456",
    "products" : "orange",
    "amount" : 300,
    "status" : "completed"
}

/* 6 */
{
    "_id" : ObjectId("5dfed8b943e6fed506289094"),
    "cust_id" : "C789",
    "products" : "apple",
    "amount" : 800,
    "status" : "completed"
}

/* 7 */
{
    "_id" : ObjectId("5dfed8b943e6fed506289094"),
    "cust_id" : "C789",
    "products" : "lemon",
    "amount" : 800,
    "status" : "completed"
}

/* 8 */
{
    "_id" : ObjectId("5dfed8b943e6fed506289094"),
    "cust_id" : "C789",
    "products" : "orange",
    "amount" : 800,
    "status" : "completed"
}

/* 9 */
{
    "_id" : ObjectId("5dfed8c343e6fed506289097"),
    "cust_id" : "A123",
    "products" : "apple",
    "amount" : 250,
    "status" : "completed"
}

9. $lookup

$lookup cho phép chúng ta thực hiện một phép left outer join giữa hai collection trong cùng một database. Với mỗi input document, $lookup sẽ thêm một array field chứa các document matching của collection được join.

Cú pháp:

{
   $lookup:
     {
       from: <collection to join>,
       localField: <field from the input documents>,
       foreignField: <field from the documents of the "from" collection>,
       as: <output array field>
     }
}

Trong đó:

  • from collection không thể bị shard.
  • as có thể có tên bất kỳ, nhưng nếu một field nào đó trong document đã có tên như vậy thì giá trị của field đó sẽ bị ghi đè.

$lookup tương đương với đoạn SQL sau:

SELECT *, <output array field>
FROM collection
WHERE <output array field> IN (SELECT *
                               FROM <collection to join>
                               WHERE <foreignField>= <collection.localField>);

Ví dụ:

Mình sẽ join orderscustomers để bổ sung thêm thông tin của customer trong từng order:

db.orders.aggregate([
    {
        $lookup: {
            from: "customers",
            localField: "cust_id",
            foreignField: "_id",
            as: "customer"
        }
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : ObjectId("5dfed89743e6fed50628907c"),
    "cust_id" : "A123",
    "products" : [ 
        "apple", 
        "lemon"
    ],
    "amount" : 500,
    "status" : "completed",
    "customer" : [ 
        {
            "_id" : "A123",
            "name" : "Alice"
        }
    ]
}

/* 2 */
{
    "_id" : ObjectId("5dfed8a643e6fed50628908a"),
    "cust_id" : "B456",
    "products" : [ 
        "lemon"
    ],
    "amount" : 100,
    "status" : "processing",
    "customer" : []
}

/* 3 */
{
    "_id" : ObjectId("5dfed8b043e6fed50628908f"),
    "cust_id" : "B456",
    "products" : [ 
        "apple", 
        "orange"
    ],
    "amount" : 300,
    "status" : "completed",
    "customer" : []
}

/* 4 */
{
    "_id" : ObjectId("5dfed8b943e6fed506289094"),
    "cust_id" : "C789",
    "products" : [ 
        "apple", 
        "lemon", 
        "orange"
    ],
    "amount" : 800,
    "status" : "completed",
    "customer" : [ 
        {
            "_id" : "C789",
            "name" : "Carol"
        }
    ]
}

/* 5 */
{
    "_id" : ObjectId("5dfed8c343e6fed506289097"),
    "cust_id" : "A123",
    "products" : [ 
        "apple"
    ],
    "amount" : 250,
    "status" : "completed",
    "customer" : [ 
        {
            "_id" : "A123",
            "name" : "Alice"
        }
    ]
}

10. Kết hợp các aggregation operator

Trong phần này, chúng ta sẽ kết hợp các aggregation operator trong một pipeline. Mình sẽ minh họa thông qua 2 ví dụ.

Ví dụ 1

Yêu cầu:

  1. Lọc tất cả các order có statuscompleted.
  2. Gom nhóm các order có được ở bước 1 theo cust_id và tính tổng amount.
  3. Sắp xếp theo tổng amount giảm dần.
db.orders.aggregate([
    { 
        $match: { 
            status: "completed" 
        } 
    },
    { 
        $group: {
            _id: "$cust_id",
            total: { $sum: "$amount" } 
        } 
    },
    { 
        $sort: { "total": -1 }
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : "C789",
    "total" : 800
}

/* 2 */
{
    "_id" : "A123",
    "total" : 750
}

/* 3 */
{
    "_id" : "B456",
    "total" : 300
}

Ví dụ 2

Tiếp tục ví dụ 1, chúng ta sẽ bổ sung thêm customer name cho các output document. Các bạn lưu ý chúng ta sẽ chỉ bổ sung customer name mà thôi, còn customer ID thì chúng ta đã lưu trong _id rồi.

db.orders.aggregate([
    { 
        $match: { 
            status: "completed" 
        } 
    },
    { 
        $group: {
            _id: "$cust_id",
            total: { $sum: "$amount" } 
        } 
    },
    { 
        $sort: { "total": -1 }
    },
    {
        $lookup: {
            from: "customers",
            localField: "_id",
            foreignField: "_id",
            as: "customer"
        }
    },
    {
        $project: {
            total: 1,
            cust_name: "$customer.name"
        }
    },
    {
        $unwind: "$cust_name"
    }
])

=> Kết quả:

/* 1 */
{
    "_id" : "C789",
    "total" : 800,
    "cust_name" : "Carol"
}

/* 2 */
{
    "_id" : "A123",
    "total" : 750,
    "cust_name" : "Alice"
}

/* 3 */
{
    "_id" : "B456",
    "total" : 300,
    "cust_name" : "Bob"
}
Bình luận


White
{{ comment.user.name }}
Bỏ hay Hay
{{comment.like_count}}
Male avatar
{{ comment_error }}
Hủy
   

Hiển thị thử

Chỉnh sửa

White

Nguyễn Tuấn Anh

30 bài viết.
259 người follow
Kipalog
{{userFollowed ? 'Following' : 'Follow'}}
Cùng một tác giả
White
58 42
MyContact là một ứng dụng mà mình thường viết mỗi khi học một ngôn ngữ hay công nghệ mới. MyContact chỉ là một ứng dụng CRUD đơn giản, cho phép ngư...
Nguyễn Tuấn Anh viết hơn 3 năm trước
58 42
White
40 17
Hướng dẫn lập trình Spring Security Trong bài viết lần này, mình sẽ giúp các bạn bước đầu tìm hiểu (Link) thông qua xây dựng các chức năng: Đăng ...
Nguyễn Tuấn Anh viết hơn 3 năm trước
40 17
White
22 0
Trước đây khi mới học Spring, mình thường nhảy thẳng lên tìm hiểu các project như Spring MVC hay Spring Boot để viết ứng dụng, thỉnh thoảng mới ngó...
Nguyễn Tuấn Anh viết gần 2 năm trước
22 0
Bài viết liên quan
White
45 7
Giới thiệu MongoDB là một giải pháp nosql database. Data được lưu ở dạng các bson document. Hỗ trợ vertical scaling và horizontal scaling, dynamic...
manhdung viết hơn 5 năm trước
45 7
White
20 10
Quá trình lột xác ngoạn mục của một hệ thống cổ lỗ sĩ khi được thiết kế cẩn thận: 1 usecase thành công của việc áp dụng triệt để các phương pháp xử...
Minh Monmen viết 3 tháng trước
20 10
White
6 0
Khoảng 5, 7 năm nay NOSQL là đề tài rất nóng bỏng. SQL phổ biến thì chỉ có khoảng vài ba cái như PostgreSQL, MySQL, MS SQL, Oracle nói chung na na ...
Ngoc Dao viết hơn 4 năm trước
6 0
{{like_count}}

kipalog

{{ comment_count }}

bình luận

{{liked ? "Đã kipalog" : "Kipalog"}}


White
{{userFollowed ? 'Following' : 'Follow'}}
30 bài viết.
259 người follow

 Đầu mục bài viết

Vẫn còn nữa! x

Kipalog vẫn còn rất nhiều bài viết hay và chủ đề thú vị chờ bạn khám phá!