【问题标题】:MongoDB: does document size affect query performance?MongoDB:文档大小会影响查询性能吗?
【发布时间】:2014-07-13 07:48:48
【问题描述】:

假设一个手机游戏由一个 MongoDB 数据库支持,该数据库包含一个包含数百万个文档的 User 集合。

现在假设有几十个属性必须与用户相关联 - 例如一组_idFriend 文档、他们的用户名、照片、_id 值数组Game 文档、last_login 日期、游戏内货币计数等等等。

我担心的是,在数以百万计的用户文档上创建和更新大型、不断增长的数组是否会给每个用户文档增加任何“权重”,和/或降低整个系统的速度。

我们可能永远不会超过每个文档 16mb,但我们可以肯定地说,如果我们直接存储这些不断增长的列表,我们的文档将大 10-20 倍。

问题:这甚至是 MongoDB 中的一个问题吗?如果使用投影和索引等正确管理查询,文档大小是否重要?我们是否应该积极修剪文档大小,例如引用外部列表与直接嵌入 _id 值列表?

换句话说:如果我想要用户的last_login 值,如果我的User 文档为100kb 与5mb,那么仅投射/选择last_login 字段的查询是否会有所不同?

或者:如果我想查找具有特定 last_login 值的所有用户,文档大小会影响这种查询吗?

【问题讨论】:

    标签: mongodb mongodb-query


    【解决方案1】:

    首先,您应该花点时间阅读一下 MongoDB 如何参考填充因子和 powerof2sizes 分配来存储文档:

    http://docs.mongodb.org/manual/core/storage/ http://docs.mongodb.org/manual/reference/command/collStats/#collStats.paddingFactor

    简单地说,MongoDB 在存储原始文档时会尝试分配一些额外的空间以允许增长。 Powerof2sizes 分配成为 2.6 版中的默认方法,它将以 2 的幂增加文档大小。

    总体而言,如果所有更新都符合原始大小分配,性能会更好。原因是如果他们不这样做,则需要将整个文档移动到具有足够空间的其他地方,从而导致更多的读取和写入,并实际上使您的存储碎片化。

    如果您的文档的大小真的要增长 10 倍到 20 倍,这可能意味着每个文档需要多次移动,这取决于您的插入、更新和读取频率,这可能会导致问题。如果是这种情况,您可以考虑以下几种方法:

    1) 在初始插入时分配足够的空间来覆盖大部分(比如说 90%)正常文档的生命周期增长。虽然一开始这在空间使用方面效率低下,但随着文档的增长,效率会随着时间的推移而提高,而不会降低性能。实际上,您将提前支付存储费用,您最终会在以后使用这些存储空间,以便随着时间的推移获得良好的性能。

    2) 创建“溢出”文档 - 假设应用了典型的 80-20 规则,并且 80%​​ 的文档将适合特定大小。分配该数量并添加一个溢出集合,如果他们有超过 100 个朋友或 100 个游戏文档,您的文档可以指向该集合。溢出字段指向这个新集合中的一个文档,如果溢出字段存在,您的应用程序只会在新集合中查找。允许 80% 的用户进行正常的文档处理,并避免在 80% 的不需要的用户文档上浪费大量存储空间,代价是增加应用程序的复杂性。

    在任何一种情况下,我都会考虑通过构建适当的索引来使用覆盖查询:

    覆盖查询是这样的查询:

    all the fields in the query are part of an index, and
    all the fields returned in the results are in the same index.
    

    因为索引“覆盖”了查询,MongoDB 可以同时匹配查询 条件并仅使用索引返回结果; MongoDB 确实 不需要看文档,只需要索引,就可以完成 查询。

    仅查询索引可以比查询文档快得多 指数之外。索引键通常小于 他们编目的文档,索引通常在 RAM 或 在磁盘上按顺序定位。

    在此处了解有关该方法的更多信息:http://docs.mongodb.org/manual/tutorial/create-indexes-to-support-queries/

    【讨论】:

    • 因此,换句话说,文档的大小对查询的性能没有显着影响 - 但对于更新操作等,它非常重要(以您注意到的方式)。对吗?
    • 我不会走那么远:) - 从您的示例中不清楚您的文档有多大。较大的文档可能会导致性能降低,但它是大小和整体用例的函数。通过频繁更新导致文档大小显着增长确实会导致性能影响和存储使用效率低下。如果您有大型文档,但在任何给定时间的工作字段集有限,则涵盖的查询可能会产生很大的不同。您可以查看预读设置以优化大型文档检索docs.mongodb.org/manual/administration/production-notes
    • 您的文档对我来说听起来不是很大 - 请记住,限制是 16 MB,除非我遗漏了一些内容,否则您将远低于 16 KB,因此您不必担心文档尺寸。大多数人不太担心文档大小。通过嵌入的 _id 值对外部集合的引用显着增加了查询成本(两个而不是一个),因此您只在必要时才这样做。综上所述,我能给你的关于 MongoDB 的最好建议就是对它进行基准测试。每个用例都略有不同。
    • 这个答案本身是正确的,但并没有真正回答这个特定的问题。我想知道为什么这是一个公认的答案。
    • 另外,我不同意@JohnPetrone 的建议,即预先分配更大尺寸的文档。 2 的幂算法本质上是一种以指数方式增加文档大小的算法!除非每次写入数据时数据都翻倍,否则不会发生空间重新分配。这就是规模呈指数增长的美妙之处。因此,假设您的数据大小从 1 KB 增长到 1MB(1024 次),mongo 调整文档大小的最大次数是 log(1024) = 10。
    【解决方案2】:

    改写这个问题的一种方法是说,如果文档分别为 16mb 和 16kb,那么 100 万个文档查询是否需要更长的时间?

    如果我错了,请纠正我,根据我自己的经验,文档越小,查询越快。

    我对 500k 文档和 25k 文档进行了查询,而 25k 的查询明显更快 - 快了几毫秒到 1-3 秒不等。在生产中,时间差大约是 2 到 10 倍。

    文档大小发挥作用的一个方面是查询排序,在这种情况下,文档大小将影响查询本身是否运行。我已经多次达到这个限制,试图对少至 2k 的文档进行排序。

    这里有一些解决方案的更多参考: https://docs.mongodb.org/manual/reference/limits/#operations https://docs.mongodb.org/manual/reference/operator/aggregation/sort/#sort-memory-limit

    归根结底,受苦的还是最终用户。

    当我尝试修复导致性能异常缓慢的大型查询时。我通常会发现自己使用数据子集创建一个新集合,并使用大量查询条件以及排序和限制。

    希望这会有所帮助!

    【讨论】:

      【解决方案3】:

      只是想分享一下我在 MongoDB 中处理大型文档时的经验……别这样!

      我们犯了一个错误,即允许用户在文档中包含以 base64 编码的文件(通常是图像和屏幕截图)。我们最终收集了大约 50 万份文档,每个文档的大小从 2 Mb 到 10 Mb 不等。

      在这个集合中做一个简单的聚合会导致集群崩溃!

      在 MongoDB 中,聚合查询可能非常繁重,尤其是对于像这样的大型文档。聚合中的索引只能在某些情况下使用,因为我们需要$group,所以没有使用索引,MongoDB 将不得不扫描所有文档。

      在具有较小文档的集合中执行完全相同的查询非常快,并且资源消耗不是很高。

      因此,在 MongoDB 中查询大型文档会对性能产生很大影响,尤其是聚合。

      此外,如果您知道文档在创建后会继续增长(例如,例如在给定实体(文档)中包含日志事件),请考虑为这些子项创建一个集合,因为大小也可能成为一个问题未来。

      布鲁诺。

      【讨论】:

        【解决方案4】:

        简短回答:是的。

        长答案:它将如何影响查询取决于许多因素,例如查询的性质、可用内存和索引大小。

        你能做的最好的就是测试。

        下面的代码将生成两个名为 smallDocuments 和 bigDocuments 的集合,每个集合有 1024 个文档,不同之处仅在于包含大字符串和 _id 的字段“c”。 bigDocuments 集合大约有 2GB,因此请小心运行它。

        const numberOfDocuments = 1024;
        
        // 2MB string x 1024 ~ 2GB collection
        const bigString = 'a'.repeat(2 * 1024 * 1024);
        
        // generate and insert documents in two collections: shortDocuments and
        // largeDocuments;
        for (let i = 0; i < numberOfDocuments; i++) {
          let doc = {};
          // field a: integer between 0 and 10, equal in both collections;
          doc.a = ~~(Math.random() * 10);
        
          // field b: single character between a to j, equal in both collections;
          doc.b = String.fromCharCode(97 + ~~(Math.random() * 10));
        
          //insert in smallDocuments collection
          db.smallDocuments.insert(doc);
        
          // field c: big string, present only in bigDocuments collection;
          doc.c = bigString;
        
          //insert in bigDocuments collection
          db.bigDocuments.insert(doc);
        }
        

        您可以将此代码放在一个文件中(例如 create-test-data.js)并直接在 mongoshell 中运行它,输入以下命令:

        mongo testDb &lt; create-test-data.js

        这需要一段时间。之后,您可以执行一些测试查询,例如:

        const numbersToQuery = [];
        
        // generate 100 random numbers to query documents using field 'a':
        for (let i = 0; i < 100; i++) {
          numbersToQuery.push(~~(Math.random() * 10));
        }
        
        const smallStart = Date.now();
        numbersToQuery.forEach(number => {
          // query using inequality conditions: slower than equality
          const docs = db.smallDocuments
            .find({ a: { $ne: number } }, { a: 1, b: 1 })
            .toArray();
        });
        print('Small:' + (Date.now() - smallStart) + ' ms');
        
        const bigStart = Date.now();
        numbersToQuery.forEach(number => {
          // repeat the same queries in the bigDocuments collection; note that the big field 'c'
          // is ommited in the projection
          const docs = db.bigDocuments
            .find({ a: { $ne: number } }, { a: 1, b: 1 })
            .toArray();
        });
        print('Big: ' + (Date.now() - bigStart) + ' ms');
        

        在这里我得到了以下结果:

        无索引:

        Small: 1976 ms
        Big: 19835 ms
        

        在两个集合中索引字段“a”后,使用.createIndex({ a: 1 })

        Small: 2258 ms
        Big: 4761 ms
        

        这表明对大文档的查询速度较慢。使用索引,bigDocuments 的结果时间比 smallDocuments 大 100% 以上。

        我的建议是:

        1. 在查询中使用相等条件 (https://docs.mongodb.com/manual/core/query-optimization/index.html#query-selectivity);
        2. 使用覆盖查询 (https://docs.mongodb.com/manual/core/query-optimization/index.html#covered-query);
        3. 使用适合内存的索引 (https://docs.mongodb.com/manual/tutorial/ensure-indexes-fit-ram/);
        4. 保持文档小;
        5. 如果您需要使用文本索引的短语查询,请确保整个集合都适合内存https://docs.mongodb.com/manual/core/index-text/#storage-requirements-and-performance-costs,最后一个项目符号);
        6. 生成测试数据并进行测试查询,模拟您的应用用例;如果需要,使用随机字符串生成器。

        我在大文档中使用 MongoDB 进行文本查询时遇到问题:Autocomplete and text search memory issues in apostrophe-cms: need ideas

        这里有一些我编写的用于在 ApostropheCMS 中生成示例数据的代码,以及一些测试结果:https://github.com/souzabrs/misc/tree/master/big-pieces

        这更像是一个数据库设计问题,而不是 MongoDB 内部问题。我认为 MongoDB 就是这样设计的。但是,在其文档中提供更明显的解释会大有帮助。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2016-03-04
          • 1970-01-01
          • 1970-01-01
          • 2021-11-11
          • 2011-04-09
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多