$lookup 中的其他连接条件严重降低了性能(使用管道)

Posted

技术标签:

【中文标题】$lookup 中的其他连接条件严重降低了性能(使用管道)【英文标题】:Terribly degraded performance with other join conditions in $lookup (using pipeline) 【发布时间】:2020-08-20 04:13:58 【问题描述】:

因此,在一些代码审查期间,我决定通过改进一个聚合来提高现有查询性能,如下所示:

    .aggregate([
        //difference starts here
        
            "$lookup": 
                "from": "sessions",
                "localField": "_id",
                "foreignField": "_client",
                "as": "sessions"
            
        ,
        
            $unwind: "$sessions"
        ,
        
            $match: 
                "sessions.deleted_at": null
            
        ,
        //difference ends here
        
            $project: 
                name: client_name_concater,
                email: '$email',
                phone: '$phone',
                address: addressConcater,
                updated_at: '$updated_at',
            
        
    ]);

到这里:

    .aggregate([
    //difference starts here
    
        $lookup: 
            from: 'sessions',
            let: 
                id: "$_id"
            ,
            pipeline: [
                
                    $match: 
                        $expr: 
                            $and:
                                [
                                    
                                        $eq: ["$_client", "$$id"]
                                    , 
                                    $eq: ["$deleted_at", null]
                                ,
                                ]
                        
                    
                
            ],
            as: 'sessions'
        
    ,
    
        $match: 
            "sessions": $ne: []
        
    ,
    //difference ends here
        
            $project: 
                name: client_name_concater,
                email: '$email',
                phone: '$phone',
                address: addressConcater,
                updated_at: '$updated_at',
            
        
    ]);

我认为第二个选项应该更好,因为我们少了一个阶段,但是相反的性能差异很大,第一个查询平均运行约 40 毫秒,另一个在 3.5 - 5 秒之间, 100 倍以上。另一个集合(会话)大约有 120 个文档,而这个大约有 152 个,但是,即使由于数据大小可以接受,为什么这两个之间的差异,不是基本相同的东西,我们只是添加管道中的连接条件与连接的其他主要条件。我错过了什么吗?

其中包含的一些函数或变量大多是静态的或不应该影响 $lookup 部分的串联。

谢谢

编辑:

为版本 1 添加了查询计划:


        "stages": [
            
                "$cursor": 
                    "query": 
                        "$and": [
                            
                                "deleted_at": null
                            ,
                            
                        ]
                    ,
                    "fields": 
                        "email": 1,
                        "phone": 1,
                        "updated_at": 1,
                        "_id": 1
                    ,
                    "queryPlanner": 
                        "plannerVersion": 1,
                        "namespace": "test.clients",
                        "indexFilterSet": false,
                        "parsedQuery": 
                            "deleted_at": 
                                "$eq": null
                            
                        ,
                        "winningPlan": 
                            "stage": "COLLSCAN",
                            "filter": 
                                "deleted_at": 
                                    "$eq": null
                                
                            ,
                            "direction": "forward"
                        ,
                        "rejectedPlans": []
                    
                
            ,
            
                "$lookup": 
                    "from": "sessions",
                    "as": "sessions",
                    "localField": "_id",
                    "foreignField": "_client",
                    "unwinding": 
                        "preserveNullAndEmptyArrays": false
                    
                
            ,
            
                "$project": 
                    "_id": true,
                    "email": "$email",
                    "phone": "$phone",
                    "updated_at": "$updated_at"
                
            
        ],
        "ok": 1
    

对于版本 2:


        "stages": [
            
                "$cursor": 
                    "query": 
                        "deleted_at": null
                    ,
                    "fields": 
                        "email": 1,
                        "phone": 1,
                        "sessions": 1,
                        "updated_at": 1,
                        "_id": 1
                    ,
                    "queryPlanner": 
                        "plannerVersion": 1,
                        "namespace": "test.clients",
                        "indexFilterSet": false,
                        "parsedQuery": 
                            "deleted_at": 
                                "$eq": null
                            
                        ,
                        "winningPlan": 
                            "stage": "COLLSCAN",
                            "filter": 
                                "deleted_at": 
                                    "$eq": null
                                
                            ,
                            "direction": "forward"
                        ,
                        "rejectedPlans": []
                    
                
            ,
            
                "$lookup": 
                    "from": "sessions",
                    "as": "sessions",
                    "let": 
                        "id": "$_id"
                    ,
                    "pipeline": [
                        
                            "$match": 
                                "$expr": 
                                    "$and": [
                                        
                                            "$eq": [
                                                "$_client",
                                                "$$id"
                                            ]
                                        ,
                                        
                                            "$eq": [
                                                "$deleted_at",
                                                null
                                            ]
                                        
                                    ]
                                
                            
                        
                    ]
                
            ,
            
                "$match": 
                    "sessions": 
                        "$not": 
                            "$eq": []
                        
                    
                
            ,
            
                "$project": 
                    "_id": true,
                    "email": "$email",
                    "phone": "$phone",
                    "updated_at": "$updated_at"
                
            
        ],
        "ok": 1
    

需要注意的是,joined sessions 集合具有非常大的数据(一些导入的数据)的某些属性,所以我认为它可能会在某种程度上影响由于这些数据而导致的查询大小?但是为什么这两个 $lookup 版本之间存在差异。

【问题讨论】:

每个版本的查询计划是什么? @Oleg 谢谢,在帖子中添加了它们。还删除了连接函数、限制、偏移等,以使解释的输出更短。不过有一件事,在取消限制后,管道版本跳到了 10 多秒,这是什么鬼。 @Ncifra,我很好奇,你的 MongoDB 版本是多少? @TheeSritabtim 4.0.9 【参考方案1】:

第二个版本为加入集合中的每个文档添加了一个聚合管道执行

The documentation 说:

指定要在连接集合上运行的管道。管道从连接的集合中确定生成的文档。要返回所有文档,请指定一个空管道 []。

管道是针对集合中的每个文档执行的,而不是针对每个匹配的文档。

根据集合的大小(文档数量和文档大小),这可能需要相当长的时间。

取消限制后,流水线版本跳到10多秒

有道理 - 由于移除限制而导致的所有其他文档也必须为它们执行聚合管道。

聚合管道的每个文档执行可能没有尽可能优化。例如,如果为每个文档设置和拆除管道,则 that 中的开销很容易比 $match 条件中的开销更大。

使用其中一种有什么情况吗?

为每个加入的文档执行一个聚合管道提供了额外的灵活性。如果您需要这种灵活性,那么执行管道可能是有意义的,尽管无论如何都需要考虑性能。如果不这样做,明智的做法是使用性能更高的方法。

【讨论】:

所以就扫描了多少文档而言,它有点像 no_documents_coll1*no_documents_coll2?不过我不明白,这是 MongoDB 的问题,还是我这边的错误使用,因为对我来说,这有点“遵循使用文档”。看起来查找内部的管道是使用它时不太明显的东西的语法糖。 似乎已经有一些事情了:jira.mongodb.org/browse/SERVER-41171 “管道内部查找是语法糖”是什么意思?您要求数据库执行额外的工作。您可以推断该工作的结果等同于另一个操作的结果,或者(过于简单化)根本不执行该工作。但是您仍然要求数据库执行该工作,并且如果它以一种直接的方式实现,即使该工作在这种特殊情况下不会影响结果,它也会执行该工作。 我的意思是即使它“看起来”像你做的操作更少,因为你在主聚合管道的一个阶段内(没有 $unwind 等额外的阶段),你正在做额外的工作,这就是为什么我说句法糖部分,这让你看起来好像在做一些不是真正发生的事情,比如 await/async 让你看起来像是在做同步动作,即使你还在异步。 不要失去这个问题的主要目的:使用其中一个或另一个时是否有任何情况?还考虑到 JIRA 问题,我假设第二个版本已经降级,所以现在没有理由转向它,即使代码更少。

以上是关于$lookup 中的其他连接条件严重降低了性能(使用管道)的主要内容,如果未能解决你的问题,请参考以下文章

INNER JOIN 条件中的列顺序严重影响性能

在 mongodb 中使用 $lookup 指定多个连接条件

将$ lookup与条件连接一起使用

Rails/MySQL:使用 LEFT JOINS 的 Group/Distinct 使查询时间加倍/性能降低

Apache新严重缺陷,远程桌面易受黑客攻击

根据 MongoDB 中的值进行条件 $lookup 吗?