【问题标题】:Filter Named Keys by Date按日期过滤命名键
【发布时间】:2019-08-30 12:25:25
【问题描述】:

我正在尝试过滤日期之间的数据,但由于复杂性,我无法执行过滤。

请帮助我 mongo DB 聚合查询

我必须通过开始日期和结束日期,它应该返回

{
    col1: {
        12 - 02 - 2019: val1,
        14 - 02 - 2019: val3
    },
    col2: {
        12 - 02 - 2019: val1,
        14 - 02 - 2019: val3
    },
    col3: {
        12 - 02 - 2019: val1,
        14 - 02 - 2019: val3
    }
}

这是我的 MongoDB 文档--------

{
    _id: ObjectId('65656222dss5ds'),
    data: {
        col1: {
            '12-07-2012': 'value1',
            '13-07-2012': 'value2',
            '14-07-2012': 'value3',
            '15-07-2012': 'value5'
        },
        col2: {
            '12-07-2012': 'value1',
            '13-07-2012': 'value2',
            '14-07-2012': 'value3',
            '15-07-2012': 'value5'
        },
        col3: {
            '12-07-2012': 'value1',
            '13-07-2012': 'value2',
            '14-07-2012': 'value3',
            '15-07-2012': 'value5'
        }
    }
}

【问题讨论】:

  • 您尝试过什么?请考虑阅读How do I ask a good question? 并编辑您的问题。
  • 您有机会修复该数据设计吗?您将日期作为字符串作为键(lvals)。你不能轻易地对此进行查询,更不用说 agg。您的文档会像{date: ISODate("20121207"), value: "value1" } 这样得到更好的服务,然后您的查询就变得容易了。
  • 没有日期只是字符串格式

标签: mongodb aggregation-framework


【解决方案1】:

我像对待倒置混淆 C 挑战一样处理这个问题:给定具有挑战性的数据和所需的输出......如何?

c = db.foo.aggregate([
// Start the journey of turning lvals into rvals...                          
{$project: {x: {$objectToArray: "$$CURRENT.data"}}}

// ... and do it again!                                                      
,{$project: {QQ: {$map: {
                input: "$x",
                as: "z",
                in: {
                    vv: {$objectToArray: "$$z.v"},
                    colk: "$$z.k"
                }
            }}
    }}

// At this point we have no more lvals of interest, but we have too          
// many arrays.  Let's simplify and turn it into individual docs:            
,{$unwind: "$QQ"}

// At this point we have a bunch of docs where QQ.colk is the collection     
// key and QQ.vv is an array of (k,v) value pairs of (string date, value):    
//    {                                                                      
//      "_id" : 1,                                                         
//      "QQ" : {                                                           
//        "vv" : [                                                           
//          {"k" : "12-07-2012",  "v" : "value44"},                          
//          {"k" : "13-07-2012",  "v" : "value45"},                          
//          {"k" : "14-07-2012",  "v" : "value46"},                          
//          {"k" : "15-07-2012",  "v" : "value47"                            
//           ],                                                              
//        "colk" : "col3"                                                    
//        }                                                                  
//   }                                                                       
//       
// OK.  Now it is time to turn those DD-MM-YYYY strings into dates so we     
// can do a proper filter.  We do so by running the QQ.vv array through      
// the $map function and using $dateFromParts + $substr to make a date.      
// Note that we "reuse" projected field QQ (i.e. input was QQ and the        
// project is QQ, sort of like saying QQ = f(QQ) ) and just keep carrying    
// along colk:                                                               
,{$project: {QQ: {$map: {
                input: "$QQ.vv",
                as: "z",
                in: {
                    v: "$$z.v",
                    d: {$dateFromParts : {
                            "year":  {$toInt: {$substr: ["$$z.k",6,4]}},
                            "month": {$toInt: {$substr: ["$$z.k",3,2]}},
                            "day":   {$toInt: {$substr: ["$$z.k",0,2]}}
                        }}
                }
            }},
             colk: "$QQ.colk"
    }}


// We now have filterable dates in an array associated with colk.            
// Now we can filter!  I hardcode the dates here but it should be clear this is
// where variables would come into play:                                                  
,{$project: {QQ: {$filter: {
                input: "$QQ",
                as: "zz",
                cond: { $and: [
{$gt: [ "$$zz.d", new ISODate("20120713") ]},
{$lt: [ "$$zz.d", new ISODate("20120716") ]}
                               ]}
            }},
             colk: "$colk"
    }}



// Almost home!   Now: reconstitute the collection key (colk):               
,{$group: {_id: "$colk", members: {$push: "$QQ"} }}

   ]);

此时,每个文档都有一个对应于唯一集合键(col1、col2、col3)的_id。 members 是一个数组数组。 OP对日期重叠没有说太多 等等,但没关系。希望你从这里开始:

{                                                                            
  "_id" : "col1",                                                            
  "members" : [                                                              
    [                                                                        
      {                                                                      
        "v" : "value3",                                                      
        "d" : ISODate("2012-07-14T00:00:00Z")                                
      },                                                                     
      {                                                                      
        "v" : "value5",                                                      
        "d" : ISODate("2012-07-15T00:00:00Z")                                
      }                                                                      
    ],                                                                       
    [                                                                        
      {                                                                      
        "v" : "value22",                                                     
        "d" : ISODate("2012-07-14T00:00:00Z")                                
      },                                                                     
      {                                                                      
        "v" : "value23",                                                     
        "d" : ISODate("2012-07-15T00:00:00Z")                                
      }                                                                      
    ]                                                                        
  ]                                                                          
}                                                                          ```

【讨论】:

  • 做得很好。您确实应该避免在可能是一个的地方连续创建多个执行相同操作的管道阶段(例如 $project )。你也可以使用$toDate,它实际上可以识别dd-mm-YYYY。而且您实际上错过了关于使用输入对象创建查询的部分问题。不完全是 OP 要求的,但很好的尝试
  • 在多个管道上为真!有时我会使用“更大”的函数来逐步显示正在发生的事情,因为 MQL 的新手有时无法关注 $filters$maps$maps 等。我认为 OP 说他需要传入 start 和结束日期,而不是从输入文档中获取它......?但最后:您在下面的综合回答指出了这一点:我们希望在设计中使用值,而不是键。
【解决方案2】:

如前所述,您的真正问题与数据的结构有关。虽然通过命名键组织和访问事物通常被宣传为客户端代码中数据访问的最佳模式,但确切的相反通常适用于数据库,而 MongoDB也不例外。

数据库基本上希望 values 执行搜索而不是 keys,因此您实际上需要通过运行时强制更改所有数据em>keys 到 values 中,以便按照您想要的方式对其进行过滤。

也就是说,这里有一个显示方法的清单:

const { MongoClient } = require('mongodb');

const url = 'mongodb://localhost:27017';
const opts = { useNewUrlParser: true, useUnifiedTopology: true };

// Basic logging helper
const log = data => console.log(JSON.stringify(data, undefined, 2));

// Sample document
const data = {
  data: {
    col1: {
      '12-07-2012': 'value1',
      '13-07-2012': 'value2',
      '14-07-2012': 'value3',
      '15-07-2012': 'value5'
    },
    col2: {
      '12-07-2012': 'value1',
      '13-07-2012': 'value2',
      '14-07-2012': 'value3',
      '15-07-2012': 'value5'
    },
    col3: {
      '12-07-2012': 'value1',
      '13-07-2012': 'value2',
      '14-07-2012': 'value3',
      '15-07-2012': 'value5'
    }
  }
};

// Sample input conditions
const input = {
  col1: {
    '12-07-2012': 'value1',   // clearly pairs of "from" and "to"
    '14-07-2012': 'value3'
  },
  col2: {
    '12-07-2012': 'value1',
    '14-07-2012': 'value3'
  },
  col3: {
    '12-07-2012': 'value1',
    '14-07-2012': 'value3'
  }
};

// Helper for converting strings to valid ISO dates
const toDate = dateStr => new Date(dateStr.split("-").reverse().join("-"));

//  Helper for the $filter arguments for $or
const makeCond = input => Object.entries(input)
  // get key and value pairs of object and make an array per 'key'
  .map(([k,v]) =>
    ({
      // Reduce the v objects as key value pairs into a single array
      '$and': Object.entries(v).reduce((o, [k,v], i) =>
        [
          ...o,     // spread the reduced array

          // Add and spread these new array elements
          ...[
            // Use $gte or $lte depending on current index
            { [(i == 0) ? '$gte' : '$lte']: [ '$$this.date', toDate(k) ] },
            { [(i == 0) ? '$gte' : '$lte']: [ '$$this.value', v ] }
          ]
        ],
        // The initial array for reduce
        [{ '$eq': [ '$$this.col', k ] }])
    })
  );

const makeOrCondition = input => Object.entries(input)
  .map(([col,v]) =>
    ({
      col,
      date: Object.keys(v).reduce((o,k,i) =>
        ({ ...o, [(i == 0) ? '$gte' : '$lte']: toDate(k) }), {}),
      value: Object.values(v).reduce((o,v,i) =>
        ({ ...o, [(i == 0) ? '$gte': '$lte']: v }), {})
    })
  );

(async function() {

  let client;

  try {
    client = await MongoClient.connect(url, opts);

    let db = client.db('test');

    await db.collection('example').deleteMany({});
    await db.collection('example').insertOne(data);

    // Debug the makeCond
    //log(makeCond(input));

    // Covert objects to arrays of arrays
    const mapObjects = {
      '$map': {
        'input': { '$objectToArray': '$data' },
        'in': {
          '$let': {
            'vars': { 'col': '$$this.k' },
            'in': {
              '$map': {
                'input': { '$objectToArray': '$$this.v' },
                'in': {
                  'col': '$$col',
                  'date': { '$toDate': '$$this.k' },
                  'value': '$$this.v'
                }
              }
            }
          }
        }
      }
    };

    // Flatten arrays of arrays to single array
    const joinArrays = {
      '$reduce': {
        'input': mapObjects,
        'initialValue': [],
        'in': { '$concatArrays': [ '$$value', '$$this' ] }
      }
    };

    // Apply the filter to the array elements
    const filterArray = {
      '$filter': {
        'input': joinArrays,
        'cond': { '$or': makeCond(input) }
      }
    };

    // Basically an inline version of $group
    const grouper = {
      '$reduce': {
        'input': filterArray,
        'initialValue': [],
        'in': {
          '$let': {
            'vars': { 'current': '$$this' },
            'in': {
              '$concatArrays': [
                //  Filter reduce output from the matching col
                { '$filter': {
                  'input': '$$value',
                  'cond': { '$ne': [ '$$current.col', '$$this.k' ] }
                }},
                // Conditionally join to:
                { '$cond': {
                  'if': {
                    '$ne': [
                      { '$indexOfArray': [
                        '$$value.k', '$$this.col'
                      ]},
                      -1
                    ]
                  },
                  // Concat the inner array where matched
                  'then': [{
                    'k': '$$this.col',
                    'v': {
                      '$concatArrays': [
                        { '$arrayElemAt': [
                          '$$value.v',
                          { '$indexOfArray': ['$$value.k', '$$this.col'] }
                        ]},
                        [{ 'k': '$$this.date', 'v': '$$this.value' }]
                      ]
                    }
                  }],
                  // Create the inner array where not matched
                  'else': [{
                    'k': '$$this.col',
                    'v': [{
                      'k': '$$this.date',
                      'v': '$$this.value'
                    }]
                  }]
                }}
              ]
            }
          }
        }
      }
    };

    const pipeline = [
      { '$match': {
        '$expr': { '$gt': [{ '$size': filterArray }, 0] }
      }},
      { '$project': {
        'data': {
          '$arrayToObject': {
            '$map': {
              'input': grouper,
              'in': {
                // reformat
                'k': '$$this.k',
                'v': {
                  '$arrayToObject': {
                    '$map': {
                      'input': '$$this.v',
                      'in': {
                        'k': {
                          '$dateToString': {
                            'date': '$$this.k',
                            'format': '%d-%m-%Y'
                          }
                        },
                        'v': '$$this.v'
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }}
    ];

    log(pipeline);

    let result = await db.collection('example').aggregate(pipeline).toArray();
    log(result);

    // Create example2

    await db.collection('example').aggregate([
      { '$project': { 'data': joinArrays } },
      { '$out': 'example2' }
    ]).toArray();


    /*
     * Simple $elemMatch and $filter usage when already an array
     *
     */
    let result2 = await db.collection('example2').aggregate([
      { '$match': {
        'data': {
          '$elemMatch': {
            '$or': makeOrCondition(input)
          }
        }
      }},
      { '$project': {
        'data': {
          '$filter': {
            'input': '$data',
            'cond': { '$or': makeCond(input) }
          }
        }
      }}
    ]).toArray();

    log(result2);

    // Create example3
    await db.collection('example2').aggregate([
      { '$unwind': '$data' },
      { '$replaceRoot': { 'newRoot': '$data' } },
      { '$out': 'example3' }
    ]).toArray();

    /*
     * Really simple when the elements are discreet documents
     * in their own collection
     */

    let result3 = await db.collection('example3').find({
      '$or': makeOrCondition(input)
    }).toArray();

    log(result3);

  } catch (e) {
    console.error(e);
  } finally {
    if (client)
      client.close();
  }

})()

示例 1

这是首先执行的主要aggregate() 列表,基本上就是您所要求的。你会看到这个输出产生了不满足input文档中提供的条件的键的期望删除:

  {
    "_id": "5d6a7ac8736dce1c76d9d3e8",
    "data": {
      "col1": {
        "12-07-2012": "value1",
        "13-07-2012": "value2",
        "14-07-2012": "value3"
      },
      "col2": {
        "12-07-2012": "value1",
        "13-07-2012": "value2",
        "14-07-2012": "value3"
      },
      "col3": {
        "12-07-2012": "value1",
        "13-07-2012": "value2",
        "14-07-2012": "value3"
      }
    }
  }

基本上完成的方法是使用$filter 运算符从数组 中删除不满足条件的元素。但为了做到这一点,您需要应用 $objectToArray 以便将 keys 转换为具有 kv 属性的对象,其中包含 values和每个属性的价值。注意部分:

// Covert objects to arrays of arrays
const mapObjects = {
  '$map': {
    'input': { '$objectToArray': '$data' },
    'in': {
      '$let': {
        'vars': { 'col': '$$this.k' },
        'in': {
          '$map': {
            'input': { '$objectToArray': '$$this.v' },
            'in': {
              'col': '$$col',
              'date': { '$toDate': '$$this.k' },
              'value': '$$this.v'
            }
          }
        }
      }
    }
  }
};

它也使用$map 来处理元素并将内部 对象映射kv 属性的数组中。还要注意$toDate,它足够聪明,可以识别字符串的dd-mm-yyy 格式并转换为BSON 日期进行比较。

另外需要注意的是$reduce 的用法,以便展平由嵌套结构(显示为joinArrays)生成的数组数组 和实际$filter 条件:

// Apply the filter to the array elements
const filterArray = {
  '$filter': {
    'input': joinArrays,
    'cond': { '$or': makeCond(input) }
  }
};

这里的makeCond() 实际上是为了将问题中的input 样本(为了匹配提供的数据而更正)转换为实际表达式,以便在cond 的参数中使用@987654327 @。您可以在程序输出中查看生成的管道,看看它的实际外观,但那是执行实际过滤的部分。

您还可以注意到,此处使用的实际 pipeline 只需要两个管道阶段,$match 用于仅选择返回仍然匹配这些条件的键的文档,以及执行实际工作的 $project在返回结果之前从文档中删除不满足条件的键。

另请注意,$map$reduce 的其他部分嵌套在 $filter 表达式中,当然,整个表达式在两个管道阶段都得到重用。

在实际的$project 中,我们以不同的方式使用$reduce,以便将数据分组重新组合在一起,以准备好与原始文档一样的预期输出形式。这可以交替使用单独的 $unwind$group 阶段来完成,但这样做远没有那么有效,即使它可能更容易阅读。

其他需要注意的是$indexOfArray$arrayElemAt 的用法,它们有助于分组全部在$cond 中处理if/then/else 逻辑。这是另一个带有内部数组连接的缩减数组,因此这里也使用了$concatArrays

最后,为了返回带有命名键的原始对象形式,BSON 日期值需要转换为字符串才能对键名有效。 $dateToString 运算符接受带有"%d-%m-%Y" 格式字符串的format 参数恢复为原始格式。

可能需要一段时间才能深入了解,但这些是链接,代码中有 cmets。会有更多解释,但 Stack Overflow 只允许有限的答案空间,这接近了这个限制。阅读并运行示例代码以了解详细信息以及参考主要方法的引用链接。

示例 2

代码基本上是为了表明示例中提供的主要“提升” 以及对问题的直接回答基本上都是关于将文档内容转换为数组,因此“日期”和其他条件可以从内容中过滤出来。

“示例 2”代码的重点是表明,当您将 data 属性构造为一个 数组 开始时,查询操作会变得更加简单和高效。

当您打算进行过滤或任何其他查询操作而是在 values 上而不是在 keys 上,就像在开场白中提到的那样。

示例 3

这基本上是为了证明,如果您唯一关心的是在文档中处理 data 的内容,那么实际上将这些条目作为谨慎的文档分离到它们自己的集合中,这将是最简单的查询形式,并且迄今为止最有效的,因为根本不需要计算任何东西,并且可以在整个过程中使用索引。

作为谨慎的文档,该过程实际上是仅查询,根本不需要aggregate() 处理。这使它快速

结论

虽然大多数事情可能使用聚合框架,但它仍然不是总是推荐的解决方案。这也应该证明设计在考虑如何实际使用您的数据时的重要性。

因此,简而言之,如果您想以一种有意义的方式“查询”任何内容,而又不会引入不必要的开销(这会破坏应用程序性能并大大增加代码维护的复杂性),那么请使用 values 而不是keys 来识别那些有意义的数据点,而不是以这种方式使用。

【讨论】:

  • 解释得很好。谢谢。我有一个查询,在您输入的列中,但列是动态的,我只能传递开始日期和结束日期。你能帮我在哪里进行更改
猜你喜欢
  • 1970-01-01
  • 2015-05-12
  • 1970-01-01
  • 2018-09-08
  • 2019-03-01
  • 2016-01-10
  • 2018-11-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多