在 pymongo 中加速 $or 查询

Speeding up $or query in pymongo

我在 mongodb 中存储了 collection 的 18 亿条记录,其中每条记录如下所示:

{
    "_id" : ObjectId("54c1a013715faf2cc0047c77"),
    "service_type" : "JE",
    "receiver_id" : NumberLong("865438083645"),
    "time" : ISODate("2012-12-05T23:07:36Z"),
    "duration" : 24,
    "service_description" : "NQ",
    "receiver_cell_id" : null,
    "location_id" : "658_55525",
    "caller_id" : NumberLong("475035504705")
}

我需要获取 200 万特定用户的所有记录(我在文本文件中有感兴趣的用户 ID)并在将结果写入数据库之前对其进行处理。我在 receiver_id 和 caller_id 上有索引(每个都是单个索引的一部分)。

我现在的流程是这样的:

for user in list_of_2million_users:
    user_records = collection.find({ "$or" : [ { "caller_id": user }, { "receiver_id" : user } ] })
    for record in user_records:
        process(record)

但是,消耗user_records游标平均需要15秒(处理函数非常简单,运行ning时间低)。这将无法处理 200 万用户。有什么建议可以加快 $or 查询的速度吗?因为这似乎是最 time-consuming 的一步。

db.call_records.find({ "$or" : [ { "caller_id": 125091840205 }, { "receiver_id" : 125091840205 } ] }).explain()
{
    "clauses" : [
        {
            "cursor" : "BtreeCursor caller_id_1",
            "isMultiKey" : false,
            "n" : 401,
            "nscannedObjects" : 401,
            "nscanned" : 401,
            "scanAndOrder" : false,
            "indexOnly" : false,
            "nChunkSkips" : 0,
            "indexBounds" : {
                "caller_id" : [
                    [
                        125091840205,
                        125091840205
                    ]
                ]
            }
        },
        {
            "cursor" : "BtreeCursor receiver_id_1",
            "isMultiKey" : false,
            "n" : 383,
            "nscannedObjects" : 383,
            "nscanned" : 383,
            "scanAndOrder" : false,
            "indexOnly" : false,
            "nChunkSkips" : 0,
            "indexBounds" : {
                "receiver_id" : [
                    [
                        125091840205,
                        125091840205
                    ]
                ]
            }
        }
    ],
    "cursor" : "QueryOptimizerCursor",
    "n" : 784,
    "nscannedObjects" : 784,
    "nscanned" : 784,
    "nscannedObjectsAllPlans" : 784,
    "nscannedAllPlans" : 784,
    "scanAndOrder" : false,
    "nYields" : 753,
    "nChunkSkips" : 0,
    "millis" : 31057,
    "server" : "some_server:27017",
    "filterSet" : false
}

这是 collection 统计数据:

 db.call_records.stats()
{
    "ns" : "stc_cdrs.call_records",
    "count" : 1825338618,
    "size" : 438081268320,
    "avgObjSize" : 240,
    "storageSize" : 468641284752,
    "numExtents" : 239,
    "nindexes" : 3,
    "lastExtentSize" : 2146426864,
    "paddingFactor" : 1,
    "systemFlags" : 0,
    "userFlags" : 1,
    "totalIndexSize" : 165290709024,
    "indexSizes" : {
        "_id_" : 73450862016,
        "caller_id_1" : 45919923504,
        "receiver_id_1" : 45919923504
    },
    "ok" : 1
}

我 运行宁 Ubuntu 服务器有 125GB 内存。

请注意,我只会 运行 此分析一次(我不会定期进行)。

如果 caller_idreceiver_id 上的索引是单个复合索引,则此查询将执行 collection 扫描而不是索引扫描。确保它们都是单独索引的一部分,即:

db.user_records.ensureIndex({caller_id:1})
db.user_records.ensureIndex({receiver_id:1})

您可以在 mongo shell:

中确认您的查询正在执行索引扫描
db.user_records.find({'$or':[{caller_id:'example'},{receiver_id:'example'}]}).explain()

如果解释计划returns其游标类型为 BTreeCursor,则您正在使用索引扫描。如果显示 BasicCursor,则表示您正在进行 collection 扫描,这并不好。

了解每个索引的大小也很有趣。为了获得最佳查询性能,两个索引都应完全加载到 RAM 中。如果索引太大以至于只有一个(或两个都不!)适合 RAM,您将不得不从磁盘将它们分页以查找结果。如果它们太大而无法放入您的 RAM,您的选择就不会太多,基本上要么以某种方式拆分您的 collection 并 re-indexing,要么获得更多 RAM。你总是可以得到一个 AWS RAM-heavy 实例只是为了这个分析的目的,因为这是一个 one-off 的东西。

我不知道为什么你的方法这么慢。

但您可能想尝试这些替代方法:

  1. 一次对多个 ID 使用 $in。我不确定 mongodb 是否能很好地处理数百万个值,但如果不能,请对 ID 列表进行排序,然后将其分成几批。
  2. 在应用程序中进行集合扫描,并根据包含感兴趣 ID 的哈希集检查每个条目。一次性脚本的性能应该可以接受,特别是因为您对这么多 ID 感兴趣。

我不是 MongoDB 方面的专家,尽管我遇到了类似的问题并且以下解决方案帮助我解决了这个问题。希望对你也有帮助。

查询正在使用索引并扫描准确的文档,因此您的索引没有问题,但我建议您:

首先尝试查看命令的状态:mongostat --discover

参见 page faults & index miss 等参数。

您是否尝试过预热(先执行查询后的查询性能)?热身后的表现如何?如果与上一个相同,则可能是页面错误。

如果您打算 运行 将其作为分析,我认为预热数据库可能会对您有所帮助。