【问题标题】:What is fastest structure to read in MongoDB: multiple documents or subdocuments?在 MongoDB 中读取最快的结构是什么:多个文档或子文档?
【发布时间】:2015-12-05 19:20:02
【问题描述】:

简介

我使用 Mongo 存储中等长度的金融时间序列,我可以通过 2 种方式阅读:

  • 检索 1 个系列的整个长度

  • 检索特定日期的 N 系列

为了方便第二种查询,我按年份对系列进行了切片。这减少了在特定日期查询大量序列时的数据负载(例如:如果我在特定日期查询1000个时间序列的值,则查询回每个历史的整个历史是不可行的,可以回溯40年 = 28k 每个)

问题

写入对时间不敏感。存储空间充足。 读取具有时间敏感性。为第一类和第二类快速读取归档数据的最佳选择是什么?

选项 A - 单独的文档

{_id:xxx, stock:IBM, year:2014, prices:[<daily prices for 2014>]}
{_id:xxx, stock:IBM, year:2015, prices:[<daily prices for 2015>]}

在选项 A 中,我将 find()yearstock 上使用复合索引

选项 B - 子文档

{
 _id:xxx,
 stock:IBM,
 2014:[<daily prices for 2014>],
 2015:[<daily prices for 2015>],
 }

在选项 B 中,我会在 stock 上的简单索引上使用 find(),并添加一个投影以仅返回我寻找的 year

选项 B.1 - 带有压缩内容的子文档

同上,但&lt;daily prices for 201x&gt; 是通过 jsoning 和 zlibbing 压缩的

选项 C - 包含每日数据的子文档

{
 _id:xxx,
 stock:IBM,
     0:<price for day 0 of 2014>,
     1:<price for day 1 of 2014>,
     ...
     n:<price for day n of 2015>,  //n can be as large as 10.000
 }

选项 D - 嵌套子文档

{
 _id:xxx,
 stock:IBM,
 2014:{
     0:<price for day 0>,
     1:<price for day 1>,
     ...
     }
 2015:{
     0:<price for day 0>,
     1:<price for day 1>,
     ...
     }

然后我必须应用像this 这样的查询方法。请注意,选项 D 可能会使读取上述第一种类型所需的数据增加一倍。

【问题讨论】:

    标签: mongodb pymongo


    【解决方案1】:

    目前的解决方案:

    我发现这种基于 选项 A 的方法对于上述两种读取都非常有效:

    cursor = mycollection.find({'year':{ '$in': years}, 'stocks':{ '$in': stocks }}).hint('year_1_ind_1')
    
    docs = [d for d in docs]
    

    【讨论】:

      【解决方案2】:

      嗯,我想我可以改进你的模型更容易:

      {
        _id: new ObjectId()
        key: "IBM",
        date: someISODate,
        price: somePrice,
        exchange: "NASDAQ"
      }
      db.stocks.createIndex({key:1, date:1, exchange:1})
      

      在此模型中,您拥有所需的所有信息:

      db.stocks.find({
        key: "IBM", 
        date: { 
          $gte: new ISODate("2014-01-01T00:00:00Z"),
          $lt: new ISODate("2015-01-01T00:00:00Z")
        }
      })
      

      例如,如果您想知道 2014 年 5 月 IBM 股票的平均价格,您可以使用聚合:

      db.stocks.aggregate([
        { $match: {
            key: "IBM",
            date:{
              $gte: new ISODate("2014-05-01T00:00:00Z"),
              $lt: new ISODate("2014-06-01T00:00:00Z")
            }
        },
        { $group: {
            _id: {
              stock: "$key",
              month: { $month:"$date"},
              year: { $year:"$date" }
            },
            avgPrice: {$avg: "$price" }
          }
        }
      ]}
      

      将导致返回的文档如下:

      {
        _id: {
          stock: "IBM",
          year: "2014",
          month: "5"
        },
        avgPrice: "8000.42"
      }
      

      您甚至可以很容易地预先计算每只股票和每个月的平均值

      db.stocks.aggregate([
        {
          $group: {
              _id: {
                stock: "$key",
                month: { $month: "$date" },
                year: { $year: "$date" }
              },
              averagePrice: {$avg:"$price"}
          }
        },
        { $out: "avgPerMonth" }
      ]}
      

      查找 IBM 在 2014 年 5 月的平均值现在变成了一个简单的查询:

      db.avgPerMonth.find({
         "_id":{
           "stock":"IBM",
           "month":"5",
           "year":"2014"
         }
      })
      

      等等。你真的想对股票使用聚合。例如:“IBM 股票在历史上哪一个月最贵?”

      很好,很简单,具有最佳的读写性能。 此外,您还可以为聚合查询保存多个 $unwind 语句(对于不太容易的任意键)。

      当然,我们有 key 的重复值的冗余,但我们规避了一些问题:

      1. BSON documents are limited to a size of 16MB,因此您的模型会施加理论限制。
      2. 当使用 MongoDB 的 mmapv1 存储引擎(这是唯一适用于 MongoDB 3.0 的存储引擎)时,扩展文档的大小可能会导致在数据文件中进行相当昂贵的文档迁移,因为文档是保证永远不会碎片化。
      3. 复杂的模型导致复杂的代码。复杂的代码更难维护。代码越难维护,所需的时间就越长。一项任务需要的时间越长,代码维护成本就越高(金钱和/或时间)。结论:复杂的模型比简单的模型更昂贵。

      编辑

      对于日期,您需要确保牢记不同的时区,并将它们标准化为 Zulu 时间在进行聚合以精确到日期时保持在交易所的时区内。

      【讨论】:

      • Markus,首先非常感谢您的清晰回答。其次:您提出“每天一份文档”模型,然后按{key:1, date:1, exchange:1} 对其进行索引,这对于专注于一只股票的查询很有用。如果我需要在一天内查询 100 只股票,我应该创建一个反向顺序的新索引{date:1, key:1},还是可以只使用find 使用$in 来获取 100 只股票的列表钥匙?再次强调,这里的关键是读取速度(写入对时间不敏感)
      • 进一步评论:添加了 follow-up question 以构建您的“每天一个文档”解决方案
      • 您的查询将是db.stocks.find({key:{$in: keysToFind}, day:{$gte: todayStart, $lt: tomorrowStart}})。因此,遵循建议索引的顺序。这样,仅使用索引来识别文档。如果您真的想拥有出色的读取性能并且愿意投资于 RAM,请在{key:1, date:1, price:1, exchange:1} 上创建一个索引,使每个query covered(将{_id:0, key:1, date:1, price:1, exchange:1} 设置为投影时)。
      • 档案的进一步说明:涵盖的查询不能用于上面的选项 A,因为该选项具有包含数组的字段,这显然是不允许的。这同样适用于原始帖子中包含数组的任何其他选项。
      • 请注意,这种“每天一个文档”的方法显然具有较大的索引大小。我有 40k 个时间序列,每个都有 20 年的数据。这意味着根据这篇 SO 帖子stackoverflow.com/questions/8607637/…,索引大小约为 5-10 gig,这会在一定程度上增加 RAM 和定价估算。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-26
      • 1970-01-01
      • 2016-02-01
      • 1970-01-01
      • 2016-07-24
      • 2015-08-10
      相关资源
      最近更新 更多