【问题标题】:running mongo queries against data in memory针对内存中的数据运行 mongo 查询
【发布时间】:2018-09-04 22:56:14
【问题描述】:

我有一个 mongodb 集合,我需要每小时运行许多计数操作(每个操作都有不同的查询)。当我第一次设置它时,集合很小,这些计数操作在大约一分钟内运行,这是可以接受的。现在它们大约需要 55 分钟,因此它们几乎可以连续运行。

与每个计数操作相关的查询相当复杂,我认为没有办法让它们全部使用索引运行(即作为 COUNT_SCAN 操作)。

我想出的唯一可行的解​​决方案是:

  • 每小时运行一次完整的集合扫描,将每个文档拉出数据库
  • 一旦每个文档都在内存中,就自己对它运行所有计数操作

如果没有我的解决方案,服务器每小时会运行几十次完整的集合扫描。使用我的解决方案,服务器只运行一个。这将我带到了一个奇怪的地方,我需要自己处理复杂的查询并重新实现它们,这样我就可以每小时计算出自己的计数。

所以我的问题是 mongo 驱动程序(在我的情况下是 pymongo,但我总体上很好奇)在解释查询文档但在本地针对内存中的数据而不是针对 mongodb 服务器上的数据运行它们时是否有任何支持。

最初这感觉像是一个奇怪的请求,但实际上在很多地方这种方法可能会在我的特定用例中大大减轻数据库的负载。所以我想知道它是否会不时出现在其他生产部署中。

【问题讨论】:

  • 我不这么认为。 MongoDB 将所有内容都视为“数据”,无论是在内存中还是其他地方。随着数据的增长,定期收集扫描只会越来越慢。你能举一些所涉及的查询的例子吗?使用预聚合报告模式 (docs.mongodb.com/ecosystem/use-cases/…) 怎么样?请注意,虽然链接的实现仅提及 MMAPv1,但该模式同样适用于 WiredTiger。
  • 有很多不同的查询,但一个例子是一个字段可能会丢失、为空或有值。我想计算它丢失、空或具有一个特定值的实例(但从计数中省略所有其他值)。我认为我不能用索引计算查询。
  • 预聚合报告模式可能有助于决定如何存储我生成的计数,但无论如何这只是一小部分数据。我真的只需要弄清楚如何以有效的方式执行计数操作。
  • 1) 你使用索引吗? 2)您能否提供可以重现此问题的数据和查询的简化示例?我之前也遇到过类似的问题,但是查询效率不是很高。在我简化查询并添加索引后,一切都变得更快了。
  • @wowkin2 我确实使用索引,但我的问题不在于如何让查询运行得更快。我是说有效地运行查询是不可能的,我正在询问一种在本地(数据库之外)运行查询的方法。

标签: python mongodb


【解决方案1】:

MongoDB In-Memory 存储引擎

如果您想使用 MongoDB 语法仅在 RAM 中使用复杂查询来处理数据,您可以将 MongoDB 配置为使用 In-Memory only storage engine,从而完全避免磁盘 I/O。
对我来说,拥有复杂查询和最佳性能的能力是最好的选择。

Python 内存数据库:

您可以使用以下方法之一:

  • PyDbLite - 一个快速、纯 Python、无类型、内存中的数据库引擎,使用 Python 语法而不是 SQL 来管理数据
  • TinyDB - 如果您需要一个简单的数据库,其 API 干净,无需大量配置即可工作,TinyDB 可能是您的正确选择。但不是一个快速的解决方案,有few other disadvantages

它们应该允许直接在 RAM 中处理数据,但我不确定这是否比上一个选项更好。

自己的自定义解决方案(例如用 Python 编写)

某些服务仅在应用程序级别处理 RAM 中的数据。如果您的解决方案并不复杂并且查询很简单 - 这没关系。但是由于某些时候查询变得更加复杂并且代码需要一些抽象级别(对于高级 CRUD),就像以前的数据库一样。

最后一个解决方案可以有最好的性能,但需要更多的时间来开发和支持。

【讨论】:

  • 我的问题不是让 mongo 解释查询 - 我需要使用代码在本地解释查询。因此,与其将查询文档传递给将其传递给服务器的 pymongo,我需要直接将查询文档与 Python 中的文档进行匹配。
  • 你可以直接使用 Python 来做,但在这种情况下它与 pymongo 或 MongoDB 无关。
  • @nonagon,添加了 Python 内存数据库示例,您可以在项目中调查和使用,甚至可以自己开发。但是所有这些选项都有不同的优点和缺点:)
  • 很抱歉给您带来了困惑。我有一个大型 mongodb 生产部署,99% 的时间我都像其他人一样发出查询。我有一个特殊的用例,从一个大型集合中获取所有文档并在本地处理它们确实很有意义。所以我嫁给了 mongo 查询文档语法,但是对于这个特殊的用例,我需要在 Python 代码中手动解释这些查询文档。所以我的问题不是关于切换数据库,而是关于在 Python 代码中手动解释 mongo 查询文档。
  • 我用我目前用来解决这个问题的代码添加了一个答案。我认为这可能有助于澄清我在寻找什么。从头开始编写代码感觉很奇怪,因为我认为我的用例实际上并没有那么奇怪,并且可能有一个库可以帮助完成这类事情。
【解决方案2】:

当你使用python时,你有没有考虑过Pandas?,你基本上可以尝试将你的JSON数据转换为pandas数据框并随意查询,你可以实现计数、分组、聚合等。请看一下文档。在下面添加一个小示例以帮助您建立联系。希望这会有所帮助。

例如:

import pandas as pd
from pandas.io.json import json_normalize
data = {
"data_points":[
    {"a":1,"b":3,"c":2},
    {"a":3,"b":2,"c":1},
    {"a":5,"b":4,"d":3}
   ]
}
# convert json to data frame
df = json_normalize(data["data_points"])

上面的 Pandas 数据框视图。

现在您可以尝试对它们执行求和、计数等操作。

例子:

# sum of column `a`
df['a'].sum()

output: 9

# sum of column `c` that has null values.
df['c'].sum()

output: 3.0

# count of column `c` that has null values.
df['c'].count()

output: 2

【讨论】:

  • 感谢您的建议,但我的问题实际上是关于如何针对本地存储在内存中的文档解释 mongo 查询
  • 同意,但只是想一想为什么要使用 Mongo 类型查询来查询这些数据?最后它只是一个 JSON 数据,您可以使用其他方式来完成相同的工作。像这样的东西,或者可能还有其他东西。我认为如果您编写自定义解决方案,其性能可能与其他解决方案不同。
  • 就我而言,我对 mongo 查询做了很多事情——查询服务器、修改查询、再次查询服务器等等,所以我想保留所有关于 mongo 查询的内容为了一致性
【解决方案3】:

这是我目前用来解决这个问题的代码。我有足够的测试来针对它运行以使其符合我的用例,但它可能不是 100% 正确的。我当然不会处理所有可能的查询文档。

def check_doc_against_mongo_query(doc, query):
    """Return whether the given doc would be returned by the given query.

    Initially this might seem like work the db should be doing, but consider a use case where we
    need to run many complex queries regularly to count matches. If each query results in a full-
    collection scan, it is often faster to run a single scan fetching the entire collection into
    memory, then run all of the matches locally.

    We don't support mongo's full query syntax here, so we'll need to add support as the need
    arises."""

    # Run our check recursively
    return _match_query(doc, query)


def _match_query(doc, query):
    """Return whether the given doc matches the given query."""

    # We don't expect a null query
    assert query is not None

    # Check each top-level field for a match, we AND them together, so return on mismatch
    for k, v in query.items():
        # Check for AND/OR operators
        if k == Mongo.AND:
            if not all(_match_query(doc, x) for x in v):
                return False
        elif k == Mongo.OR:
            if not any(_match_query(doc, x) for x in v):
                return False
        elif k == Mongo.COMMENT:
            # Ignore comments
            pass
        else:
            # Now grab the doc's value and match it against the given query value
            doc_v = nested_dict_get(doc, k)
            if not _match_doc_and_query_value(doc_v, v):
                return False

    # All top-level fields matched so return match
    return True


def _match_doc_and_query_value(doc_v, query_v):
    """Return whether the given doc and query values match."""

    cmps = []  # we AND these together below, trailing bool for negation

    # Check for operators
    if isinstance(query_v, Mapping):
        # To handle 'in' we use a tuple, otherwise we use an operator and a value
        for k, v in query_v.items():
            if k == Mongo.IN:
                cmps.append((operator.eq, tuple(v), False))
            elif k == Mongo.NIN:
                cmps.append((operator.eq, tuple(v), True))
            else:
                op = {Mongo.EQ: operator.eq, Mongo.GT: operator.gt, Mongo.GTE: operator.ge,
                      Mongo.LT: operator.lt, Mongo.LTE: operator.le, Mongo.NE: operator.ne}[
                          k]
                cmps.append((op, v, False))
    else:
        # We expect a simple value here, perform an equality check
        cmps.append((operator.eq, query_v, False))

    # Now perform each comparison
    return all(_invert(_match_cmp(op, doc_v, v), invert) for op, v, invert in cmps)


def _invert(result, invert):
    """Invert the given result if necessary."""

    return not result if invert else result


def _match_cmp(op, doc_v, v):
    """Return whether the given values match with the given comparison operator.

    If v is a tuple then we require op to match with any element.

    We take care to handle comparisons with null the same way mongo does, i.e. only null ==/<=/>=
    null returns true, all other comps with null return false. See:
    https://stackoverflow.com/questions/29835829/mongodb-comparison-operators-with-null
    for details.

    As an important special case of null comparisons, ne null matches any non-null value.
    """

    if doc_v is None and v is None:
        return op in (operator.eq, operator.ge, operator.le)
    elif op is operator.ne and v is None:
        return doc_v is not None
    elif v is None:
        return False
    elif isinstance(v, tuple):
        return any(op(doc_v, x) for x in v)
    else:
        return op(doc_v, v)

【讨论】:

  • 我不想接受这个答案,因为我认为必须有更好的方法。我认为这个非最佳答案将有助于澄清我的问题。
【解决方案4】:

也许您可以尝试另一种方法? 我的意思是,MongoDB 在计数方面的表现非常糟糕,总体而言,它的集合很大。

我在上一家公司遇到了一个非常相似的问题,我们所做的是创建一些“计数器”对象,并在您对数据执行的每次更新中更新它们。 这样一来,您就完全避免了计数。

文档将类似于:

{
query1count: 12,
query2count: 512312,
query3count: 6
}

如果 query1count 与查询相关:“userId = 13 的所有文档”,那么在您的 python 层中,您可以在创建/更新文档之前检查 userId = 13,如果是,则增加所需的计数器。

这确实会给您的代码增加很多额外的复杂性,但计数器的读取将在 O(1) 中执行。

当然,并不是所有的查询都那么简单,但使用这种方法可以大大减少执行时间。

【讨论】:

  • 同意理论上我可以按照您的描述逐步执行计数。就我而言,我认为在本地解释查询会更容易维护。
猜你喜欢
  • 2015-08-13
  • 1970-01-01
  • 2011-11-02
  • 2012-07-27
  • 2019-07-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-11-03
相关资源
最近更新 更多