Pymongo 慢,聚合日期

Pymongo slow with aggregate on date

也许这是我的 igno运行ce 显示,但是当我的时间范围很小时我有一个查询出现得很快,但是一旦我 运行 一个不同日期的查询查询很快就停止了。看起来好像在日期(或时间戳)字段上匹配,即使它的索引不是很有效 - 或者我只是做错了。

数据格式如下:

alarm_data = {
  "alarm_global_id": int,
  "alarm_severity": int,
  "alarm_date": float,
  "created": float,
  "new_status": bool,
  "exp_day_status": False,
  "exp_week_status": False,
  "exp_month_status": False,
  "exp_months_status": False,
  "time_in_alarm": float,
}

我有以下索引:

db.events.create_index("alarm_global_id", name="alarm_global_id")
db.events.create_index([("new_status", ASCENDING)], name="new")
db.events.create_index([("alarm_date",DESCENDING), ("exp_day_status",DESCENDING)], name="exp_day")
db.events.create_index([("alarm_date",DESCENDING), ("exp_week_status",DESCENDING)], name="exp_week")
db.events.create_index([("alarm_date",DESCENDING), ("exp_month_status",DESCENDING)], name="exp_month")
db.events.create_index([("alarm_date",DESCENDING), ("exp_months_status",DESCENDING)], name="exp_months")

alarm_date 字段是一个时间戳 - 所以实际上是一个浮点数。也许真正的 datetime 对象排序更好?

无论如何,我们的想法是使用它作为一种计算长期聚合的方法~每分钟一次左右,而不执行巨大的完整收集扫描。方法是将所有new数据作为增量,所有超过过期时间的数据作为负数,它们的总和是最后n秒的变化总和.处理新数据的方法是:

db.events.aggregate([
  { "$match": {"new_status":True}},
  { "$group": {"_id": "$alarm_global_id", "time":{"$sum":"$time_in_alarm"}, "count": {"$sum":1} } }
])

然后我们将 new_status 设置为 false,这样下次就找不到了。

为了计算天数,我们简单地匹配那些 alarm_date 小于 now - expiry:

的非新的
db.events.aggregate([
  { "$match": {"exp_day_status":False, "alarm_date":{"$lt":time.time()-(60*60*24)}} },
  { "$group": {"_id": "$alarm_global_id", "time":{"$sum":"$time_in_alarm"}, "count": {"$sum":1} } }
])

同样,完成后我们将 exp_day_status 更新为 True,以指示文档不需要再次包含在计算中。周、月和月版本重复相同的过程,只是更新了到期时间。

当我 运行 正在编写一个包含 60 documents/s 的测试并且 运行ges 被设置为 10 秒、30 秒、60 秒和 120 秒(而不是日、周、月,月值),计算和更新整个事情非常快~20-30ms - 即使当集合达到 3.5M 文档时,集合大小的计算速度也没有明显下降。 但是,一旦我将时间更改为 1 分钟、5 分钟、10 分钟和 20 分钟,事情就崩溃了。有趣的是,至少对我来说,每次到期时间的计算时间都很小,直到它超过阈值,然后计算变得非常慢。

每个阶段的计算时间如下:

  DB SIZE:  5237
  --------------------
  calculation times (ms):
  new:      3.04
  day:      3.41
  week:     1.00
  month:    1.00
  months:   0.96
  update:   13.05
  total:    24.05


  
  DB SIZE:  28590
  --------------------
  calculation times (ms):
  new:      4.00
  day:      46.02
  week:     39.00
  month:    39.00
  months:   39.01
  update:   203.00
  total:    370.03

**这里注意,日-月的所有计算结果都是0——没有新的结果,也没有符合条件的文档,所以为什么这么慢。然而,如果我有 10 秒、20 秒、30 秒和 60 秒的时间,它保持快速?

1 分钟一过去,一天的计算就会增加到 ~30 毫秒。当我们超过一周的时间段时,也会发生同样的情况——计算时间上升到 60 毫秒。如果我们尝试达到一个小时,那么它会非常大,因此每秒执行一次最终会花费超过一秒的时间。对我来说有趣的是,计算星期几的时间非常快(比如 1-2 毫秒),直到那个时间结束,然后它突然扫描整个集合或其他东西——如果它能准确地减少读取的数字,那肯定会将速度提高到性能良好的程度。

我确实理解这样的想法,即随着集合变大,查询时间会变长,但我可能天真地假设,如果我只返回 50 个结果进行严格查询,它不会增加到很多秒 30 分钟的结果,因为它应该快速拒绝任何比过期时间更新的内容,并且在过期字段上没有适当的布尔值,从而大大加快查询速度。

如果这完全是此设置的预期行为,请告诉我,我只是对任何系统要求太多来执行此任务。

更新 这是聚合的解释方法的输出:

{'explainVersion': '1',
 'stages': [{'$cursor': {'queryPlanner': {'namespace': 'events.events',
     'indexFilterSet': False,
     'parsedQuery': {'$and': [{'exp_week_status': {'$eq': False}},
       {'alarm_date': {'$lt': 1636715435.6099443}}]},
     'queryHash': '6B9D5528',
     'planCacheKey': '21EBBA73',
     'maxIndexedOrSolutionsReached': False,
     'maxIndexedAndSolutionsReached': False,
     'maxScansToExplodeReached': False,
     'winningPlan': {'stage': 'PROJECTION_SIMPLE',
      'transformBy': {'alarm_global_id': 1, 'time_in_alarm': 1, '_id': 0},
      'inputStage': {'stage': 'FETCH',
       'filter': {'exp_week_status': {'$eq': False}},
       'inputStage': {'stage': 'IXSCAN',
        'keyPattern': {'alarm_date': 1, 'exp_day_status': -1},
        'indexName': 'exp_day',
        'isMultiKey': False,
        'multiKeyPaths': {'alarm_date': [], 'exp_day_status': []},
        'isUnique': False,
        'isSparse': False,
        'isPartial': False,
        'indexVersion': 2,
        'direction': 'forward',
        'indexBounds': {'alarm_date': ['[-inf.0, 1636715435.609944)'],
         'exp_day_status': ['[MaxKey, MinKey]']}}}},
     'rejectedPlans': [{'stage': 'PROJECTION_SIMPLE',
       'transformBy': {'alarm_global_id': 1, 'time_in_alarm': 1, '_id': 0},
       'inputStage': {'stage': 'FETCH',
        'inputStage': {'stage': 'IXSCAN',
         'keyPattern': {'alarm_date': 1, 'exp_week_status': -1},
         'indexName': 'exp_week',
         'isMultiKey': False,
         'multiKeyPaths': {'alarm_date': [], 'exp_week_status': []},
         'isUnique': False,
         'isSparse': False,
         'isPartial': False,
         'indexVersion': 2,
         'direction': 'forward',
         'indexBounds': {'alarm_date': ['[-inf.0, 1636715435.609944)'],
          'exp_week_status': ['[false, false]']}}}},
      {'stage': 'PROJECTION_SIMPLE',
       'transformBy': {'alarm_global_id': 1, 'time_in_alarm': 1, '_id': 0},
       'inputStage': {'stage': 'FETCH',
        'filter': {'exp_week_status': {'$eq': False}},
        'inputStage': {'stage': 'IXSCAN',
         'keyPattern': {'alarm_date': 1, 'exp_month_status': -1},
         'indexName': 'exp_month',
         'isMultiKey': False,
         'multiKeyPaths': {'alarm_date': [], 'exp_month_status': []},
         'isUnique': False,
         'isSparse': False,
         'isPartial': False,
         'indexVersion': 2,
         'direction': 'forward',
         'indexBounds': {'alarm_date': ['[-inf.0, 1636715435.609944)'],
          'exp_month_status': ['[MaxKey, MinKey]']}}}},
      {'stage': 'PROJECTION_SIMPLE',
       'transformBy': {'alarm_global_id': 1, 'time_in_alarm': 1, '_id': 0},
       'inputStage': {'stage': 'FETCH',
        'filter': {'exp_week_status': {'$eq': False}},
        'inputStage': {'stage': 'IXSCAN',
         'keyPattern': {'alarm_date': 1, 'exp_months_status': -1},
         'indexName': 'exp_months',
         'isMultiKey': False,
         'multiKeyPaths': {'alarm_date': [], 'exp_months_status': []},
         'isUnique': False,
         'isSparse': False,
         'isPartial': False,
         'indexVersion': 2,
         'direction': 'forward',
         'indexBounds': {'alarm_date': ['[-inf.0, 1636715435.609944)'],
          'exp_months_status': ['[MaxKey, MinKey]']}}}}]}}},
  {'$group': {'_id': '$alarm_global_id',
    'time': {'$sum': '$time_in_alarm'},
    'count': {'$sum': {'$const': 1}}}}],
 'serverInfo': {'host': 'PC0V4SFH',
  'port': 27017,
  'version': '5.0.3',
  'gitVersion': '657fea5a61a74d7a79df7aff8e4bcf0bc742b748'},
 'serverParameters': {'internalQueryFacetBufferSizeBytes': 104857600,
  'internalQueryFacetMaxOutputDocSizeBytes': 104857600,
  'internalLookupStageIntermediateDocumentMaxSizeBytes': 104857600,
  'internalDocumentSourceGroupMaxMemoryBytes': 104857600,
  'internalQueryMaxBlockingSortMemoryUsageBytes': 104857600,
  'internalQueryProhibitBlockingMergeOnMongoS': 0,
  'internalQueryMaxAddToSetBytes': 104857600,
  'internalDocumentSourceSetWindowFieldsMaxMemoryBytes': 104857600},
 'command': {'aggregate': 'events',
  'pipeline': [{'$match': {'alarm_date': {'$lt': 1636715435.6099443},
     'exp_week_status': False}},
   {'$group': {'_id': '$alarm_global_id',
     'time': {'$sum': '$time_in_alarm'},
     'count': {'$sum': 1}}}],
  'explain': True,
  'lsid': {'id': UUID('9fbae31f-dbc5-4b52-85f2-5bc7eba82bfc')},
  '$db': 'events',
  '$readPreference': {'mode': 'primaryPreferred'}},
 'ok': 1.0}

所以这个解决方案对我来说很奇怪,这一定是有原因的,但是复合索引中字段的顺序很重要。 新索引有 $lt date 第二个和 status 第一个:

db.events.create_index([("exp_day_status",DESCENDING), ("alarm_date",DESCENDING)], name="exp_day")], name="exp_day")

只需更改顺序,使 exp_day_status 排在第一位,alarm_date 排在第二位,就完全改变了一切。超过 120 万个文档现在将近 6 个小时,计算时间自我们开始以来没有变化(除了每个部分随着年龄的增长而略有增加):


  DB SIZE:  1244217
  --------------------
  FIELD    TIME(ms)    COUNT
  new:      2.01        65
  30mins:   1.05        64
  1hr:      1.46        64
  2hr:      0.37        63
  months:   1.01        0
  update:   14.03       256
  total:    19.93
  --------------------
  avg:      29.27

后续问题是是否有更好的方法来构建整个流程。我们目前 运行 获取聚合方法,然后执行 update_many(),它执行相同的查询。新的问题是,是否先执行基本的 find(),然后使用该结果执行 update_one()findOneAndUpdate(),使用第一个找到的 _ids 会更快?还是有一种我在 mongo 中没有遇到的方法可以直接将 find many 的输出通过管道传输到更新?