【问题标题】:Query performance issue for large nested data in mongodbmongodb中大型嵌套数据的查询性能问题
【发布时间】:2018-09-19 21:16:19
【问题描述】:

我正在尝试从名为 'tasks' 的大型数据集查询结果,该数据集包含 187297 个文档,这些文档嵌套在另一个名为 'workers' 的数据集中 em>,这又嵌套在一个名为 'production_units' 的集合中。

production_units -> 工人 -> 任务

(顺便说一句,这是 production_units 的简化版本):

[{
    "_id": ObjectId("5aca27b926974863ed9f01ab"),
    "name": "Z",
    "workers": [{
        "name": "X Y",
        "worker_number": 655,
        "employed": false,
        "_id": ObjectId("5aca27bd26974863ed9f0425"),
        "tasks": [{
            "_id": ObjectId("5ac9f6c2e1a668d6d39c1fd1"),
            "inbound_order_number": 3296,
            "task_number": 90,
            "minutes_elapsed": 120,
            "date": "2004-11-30",
            "start": 1101823200,
            "pieces_actual": 160,
            "pause_from": 1101812400,
            "pause_to": 1101814200
        }]
    }]
}]

为了实现这一点,我使用了以下聚合命令:

db.production_units.aggregate([{
    '$project': {
        'workers': '$workers'
    }
}, {
    '$unwind': '$workers'
}, {
    '$project': {
        'tasks': '$workers.tasks',
        'worker_number': '$workers.worker_number'
    }
}, {
    '$unwind': '$tasks'
}, {
    '$project': {
        'task_number': '$tasks.task_number',
        'pieces_actual': '$tasks.pieces_actual',
        'minutes_elapsed': '$tasks.minutes_elapsed',
        'worker_number': 1,
        'start': '$tasks.start',
        'inbound_order_number': '$tasks.inbound_order_number',
        'pause_from': '$tasks.pause_from',
        'date': '$tasks.date',
        '_id': '$tasks._id',
        'pause_to': '$tasks.pause_to'
    }
}, {
    '$match': {
        'start': {
            '$exists': true
        }
    }
}, {
    '$group': {
        'entries_count': {
            '$sum': 1
        },
        '_id': null,
        'entries': {
            '$push': '$$ROOT'
        }
    }
}, {
    '$project': {
        'entries_count': 1,
        '_id': 0,
        'entries': 1
    }
}, {
    '$unwind': '$entries'
}, {
    '$project': {
        'task_number': '$entries.task_number',
        'pieces_actual': '$entries.pieces_actual',
        'minutes_elapsed': '$entries.minutes_elapsed',
        'worker_number': '$entries.worker_number',
        'start': '$entries.start',
        'inbound_order_number': '$entries.inbound_order_number',
        'pause_from': '$entries.pause_from',
        'date': '$entries.date',
        'entries_count': 1,
        '_id': '$entries._id',
        'pause_to': '$entries.pause_to'
    }
}, {
    '$sort': {
        'start': 1
    }
}, {
    '$skip': 187290
}, {
    '$limit': 10
}], {
    allowDiskUse: true
})

而返回的文件是:

{ "entries_count" : 187297, "task_number" : 100, "pieces_actual" : 68, "minutes_elapsed" : 102, "worker_number" : 411, "start" : 1594118400, "inbound_order_number" : 8569, "pause_from" : 1594119600, "date" : "2020-07-07", "_id" : ObjectId("5ac9f6d3e1a668d6d3a06351"), "pause_to" : 1594119600 } { "entries_count" : 187297, "task_number" : 130, "pieces_actual" : 20, "minutes_elapsed" : 30, "worker_number" : 549, "start" : 1596531600, "inbound_order_number" : 7683, "pause_from" : 1596538800, "date" : "2020-08-04", "_id" : ObjectId("5ac9f6cde1a668d6d39f1b26"), "pause_to" : 1596538800 } { "entries_count" : 187297, "task_number" : 210, "pieces_actual" : 84, "minutes_elapsed" : 180, "worker_number" : 734, "start" : 1601276400, "inbound_order_number" : 8330, "pause_from" : 1601290800, "date" : "2020-09-28", "_id" : ObjectId("5ac9f6d0e1a668d6d39fd677"), "pause_to" : 1601290800 } { "entries_count" : 187297, "task_number" : 20, "pieces_actual" : 64, "minutes_elapsed" : 90, "worker_number" : 114, "start" : 1601800200, "inbound_order_number" : 7690, "pause_from" : 1601809200, "date" : "2020-10-04", "_id" : ObjectId("5ac9f6cee1a668d6d39f3032"), "pause_to" : 1601811900 } { "entries_count" : 187297, "task_number" : 140, "pieces_actual" : 70, "minutes_elapsed" : 84, "worker_number" : 49, "start" : 1603721640, "inbound_order_number" : 4592, "pause_from" : 1603710000, "date" : "2020-10-26", "_id" : ObjectId("5ac9f6c8e1a668d6d39df664"), "pause_to" : 1603712700 } { "entries_count" : 187297, "task_number" : 80, "pieces_actual" : 20, "minutes_elapsed" : 30, "worker_number" : 277, "start" : 1796628600, "inbound_order_number" : 4655, "pause_from" : 1796641200, "date" : "2026-12-07", "_id" : ObjectId("5ac9f6c8e1a668d6d39e1fc0"), "pause_to" : 1796643900 } { "entries_count" : 187297, "task_number" : 40, "pieces_actual" : 79, "minutes_elapsed" : 123, "worker_number" : 96, "start" : 3802247580, "inbound_order_number" : 4592, "pause_from" : 3802244400, "date" : "2090-06-27", "_id" : ObjectId("5ac9f6c8e1a668d6d39de218"), "pause_to" : 3802244400 }

但是,查询需要几秒钟才能显示结果,而不是几毫秒。这是分析器返回的结果:

 db.system.profile.findOne().millis 3216

(更新)

即使是以下简化的计数查询也可以在 312 毫秒内执行,而不是几时:

db.production_units.aggregate([{
        "$unwind": "$workers"
    }, {
        "$unwind": "$workers.tasks"
    },
    {
        "$count": "entries_count"
    }
])

这是explain() 为上述查询返回的内容:

{
    "stages" : [
        {
            "$cursor" : {
                "query" : {

                },
                "fields" : {
                    "workers" : 1,
                    "_id" : 0
                },
                "queryPlanner" : {
                    "plannerVersion" : 1,
                    "namespace" : "my_db.production_units",
                    "indexFilterSet" : false,
                    "parsedQuery" : {

                    },
                    "winningPlan" : {
                        "stage" : "COLLSCAN",
                        "direction" : "forward"
                    },
                    "rejectedPlans" : [ ]
                },
                "executionStats" : {
                    "executionSuccess" : true,
                    "nReturned" : 28,
                    "executionTimeMillis" : 13,
                    "totalKeysExamined" : 0,
                    "totalDocsExamined" : 28,
                    "executionStages" : {
                        "stage" : "COLLSCAN",
                        "nReturned" : 28,
                        "executionTimeMillisEstimate" : 0,
                        "works" : 30,
                        "advanced" : 28,
                        "needTime" : 1,
                        "needYield" : 0,
                        "saveState" : 1,
                        "restoreState" : 1,
                        "isEOF" : 1,
                        "invalidates" : 0,
                        "direction" : "forward",
                        "docsExamined" : 28
                    },
                    "allPlansExecution" : [ ]
                }
            }
        },
        {
            "$unwind" : {
                "path" : "$workers"
            }
        },
        {
            "$unwind" : {
                "path" : "$workers.tasks"
            }
        },
        {
            "$group" : {
                "_id" : {
                    "$const" : null
                },
                "entries_count" : {
                    "$sum" : {
                        "$const" : 1
                    }
                }
            }
        },
        {
            "$project" : {
                "_id" : false,
                "entries_count" : true
            }
        }
    ],
    "ok" : 1
}

我不是经验丰富的 DBA,所以我不知道我在聚合管道中究竟缺少什么,以解决我面临的性能问题。我也调查了这个问题并进行了研究,但没有找到任何解决方案。

我错过了什么?

【问题讨论】:

  • 您是否尝试将 $match 放在聚合的开头?它将减少通过不同阶段的文档数量
  • 有点难以理解您想要通过查询实现什么,您能否添加您的集合的示例文档以及预期结果?
  • @felix 查询将每个工作人员嵌套的任务文档连接起来,然后返回最后 10 个文档(以适应分页)。发布的文件是否足够,或者我需要提供更多信息?
  • @Nicolas 对不起,但我不明白你的意思。你的意思是一个空的$match(即$match: {})?
  • 我的意思是,你可以尝试在第一时间输入 `$match: {"workers.tasks.start": {$exists: true}} 来过滤你需要的文档

标签: mongodb aggregation-framework query-performance


【解决方案1】:

如果没有查询的explain(),就不可能确定查询的瓶颈是什么。但是,这里有一些关于如何改进此查询的建议


在管道末端使用单个 $project 阶段

查询包含 5 个 $project 阶段,而实际上只需要一个。这会增加很多开销,尤其是在应用于大量文档时。 相反,使用点表示法来查询嵌套字段,例如:

{ "$unwind": "$workers.tasks" }

尽早致电$match

$match 允许删除部分文档,因此请尽早添加,以便在较少数量的文档上应用进一步的聚合阶段

$project之前调用skip$limit

由于查询只返回 10 个文档,因此无需在 180000 个其他文档上应用 $project 阶段

正确索引用于排序的字段

这很可能是瓶颈。确保字段 workers.tasks.start 已编入索引(有关详细信息,请参阅 MongoDB ensureIndex()

不计算查询中返回的文档的 nb

代替 $group/$unwind 阶段来计算匹配文档,同时运行另一个查询以仅计算匹配文档的数量


主查询现在看起来像:

db.collection.aggregate([{
        "$unwind": "$workers"
    }, {
        "$unwind": "$workers.tasks"
    }, {
        "$match": {
            "workers.tasks.start": {
                "$ne": null
            }
        }
    },
    {
        "$sort": {
            "workers.tasks.start": 1
        }
    }, {
        "$skip": 0
    }, {
        "$limit": 10
    },
    {
        "$project": {
            "task_number": "$workers.tasks.task_number",
            "pieces_actual": "$workers.tasks.pieces_actual",
            "minutes_elapsed": "$workers.tasks.minutes_elapsed",
            "worker_number": "$workers.worker_number",
            "start": "$workers.tasks.start",
            "inbound_order_number": "$workers.tasks.inbound_order_number",
            "pause_from": "$workers.tasks.pause_from",
            "date": "$workers.tasks.date",
            "_id": "$workers.tasks._id",
            "pause_to": "$workers.tasks.pause_to"
        }
    }
])

你可以在这里试试:mongoplayground.net/p/yua7qspo2Jj

计数查询将是

db.collection.aggregate([{
        "$unwind": "$workers"
    }, {
        "$unwind": "$workers.tasks"
    }, {
        "$match": {
            "workers.tasks.start": {
                "$ne": null
            }
        }
    },
    {
        "$count": "entries_count"
    }
])

计数查询看起来像

【讨论】:

  • 感谢您的出色回答。但是在测试了你上面提到的查询之后,我真的不知道为什么计数查询需要 312 毫秒,而主查询只需要 26 毫秒。导致 $count 开销的原因是什么?
  • @Kais 这很难说...你可能想看看查询的explain(),它会告诉你 mongodb 在哪里花费时间
  • 我更新了我的问题以提供explain() 的结果。
  • @Ashish 目前在 json 模式下是不可能的,但您可以在 mgodatagen 模式下实现这一点(例如,mongoplayground.net/p/yIEueXLsH6R)。如果您发现任何错误,请在此处报告:github.com/feliixx/mongoplayground/issues
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2022-01-02
  • 2013-09-09
  • 1970-01-01
  • 2018-03-25
  • 2017-07-16
  • 1970-01-01
  • 2018-03-06
相关资源
最近更新 更多