[译]MongoDB数据建模介绍

翻出一篇很早以前翻译的文章。

数据建模介绍(Data Modeling Introduction)

官方文档:data models

不像SQL数据库一样,MongoDB中的数据有着灵活的模式。MongDB collection中的数据不强制document的数据结构。这种灵活性有助于将document映射到实体或者对象。每个document可以匹配描绘实体的相应数据字段,即使数据可能会发生变化。然而在实际使用中,同一个collection中的documents共享类似的数据结构。

数据建模的关键挑战是平衡应用需求,数据库引擎的性能特性和数据检索模式。在设计数据模型的时候,总是要考虑应用对数据的使用方式(如查询、更新、处理数据等)以及数据本身的固有结构。

Document Structure

为MongoDB应用程序设计数据模型的关键是围绕document数据结构和应用程序如何表示数据之间的关系。有两个工具(两种方法)表示数据之间的关系:referencesembedded document

References

References通过在一个document中存储链接或对其他document的引用来来表示数据之间的关系。应用程序可以解析这些应用来访问相关数据。这些是规范化的数据模型(Normalized Data Models)。

[译]MongoDB数据建模介绍_第1张图片

Embedded Data

Embedded Documents通过将相关的数据存储在同一个document中来捕获数据的相关性。MongoDB的document可以实现将一个document结构体嵌入到另一个document的一个域或数组中。这种非规范化的数据模型(denormalized data models)允许应用程序在单次数据库操作中访问、更新相关的数据。

[译]MongoDB数据建模介绍_第2张图片

详细内容参考Embedded Data Models

写操作的原子性(Atomicity of Write Operations)

MongoDB中,写操作只在document级是原子的,没有单个的写操作可以原子的覆盖多个documents或collections。非规范化数据模型中,embedded data将一个实体需要呈现的相关数据绑定在同一个document中,这有助于写的原子操作,因为一次写操作可以插入或更新实体的所有数据。规范化的数据模型将数据拆散到多个数据collections中,需要多个不是原子集合的写操作才能实现实体数据的完全插入和更新。

然而,促进原子写的模式可能限制应用程序使用数据的方式,或者限制修改应用程序的方式。Atomicity Considerations文档描述了设计schema时平衡灵活性和原子性的挑战。

数据增长(Data Growth)

当更新一个documents,如插入数据等,document占用的空间会增加。

对于MMAPv1存储引擎,如果document占用的空间大小超过了为document分配的空间,MongoDB会将这个document重定位到磁盘上。当使用MMAPv1存储引擎时,对数据增长的考虑将影响使用规范化还是非规范话的决定。参考Document Growth Considerations

数据使用和性能(Data Use and Performance)

当设计数据模型的时候,需要充分考虑应用程序怎么使用你的数据库。例如,如果你的应用程序只使用最近插入的documents,考虑使用Capped Collections。或者,你的应用程序主要是对一个collections的读操作,则可以增加一些通用查询的indexes来提高性能。

参考Operational Factors and Data Models 获取更多有关会影响数据模型设计的考虑。

数据建模概念 (Data Modeling Concepts)

考虑MongoDB中数据建模的以下几个方面:

  • Data Model Design:介绍在决定你的数据模型时有哪些不同的策略你可以选择,以及这些策略的优点和缺点。
  • Operational Factors and Data Models:当你设计你的数据模型的时候,你需要注意的一些详细特征,比如生命周期管理、索引、水平扩展性以及数据增长等。

Data Model Design

有效的数据模型应该是能够支持应用需求的。你的document的数据结构的关键是考虑到底使用Embedded还是References。

Embedded Data Models

如前面基本概念中提到的,MongoDB中Embedded Data Models将所有相关的数据都存储在一个Document中。结果就是应用程序只需要发起很少的查询、更新请求就可以完成一个普通的操作。

一般来说,在下面的情况下使用Embedded Data Models:

  • 你的实体之间存在包含关系。参考Model One-to-One Relationships with Embedded Documents
  • 你的实体之间存储一对多(one-to-many)的关系。在这种关系中,“多”或者“子document”总是同“一”或者“父document”一同出现,或是它的内容。参考[Model One-to-Many Relationships with Embedded Documents](Model One-to-Many Relationships with Embedded Documents)

一般情况下,嵌入式提供更好的读取性能,同时也提供一次数据库操作就能够请求和遍历相应数据的能力。嵌入式数据模型也提供了对相应数据更新的原子写操作的能力。

但是,在document中嵌入相关的数据将造成文档创建后不断增长的情况。使用MMAPv1存储引擎时,document尺寸增长将影响写性能,并且会导致数据碎片。

版本3.0.0的MongoDB考虑到document growth和最小化数据碎片的可能性,使用Power of 2 Sized Allocations作为MMAPv1默认的分配策略。将来,MongoDB的document的size必须比BSON document的最大size小。对于bulk binary data,考虑GridFS

Normalized Data Models

一般情况下,如下情形使用规范化的数据模型:

  • 当嵌入式方式会导致数据重复,但是又不能提供最后的读取性能来弥补数据重复造成的影响。
  • 需要表示更复杂的多对多的关系。
  • 需要对大型分层数据集建模。

引用比嵌入式提供了更高的灵活性。但是,客户端应用必须发起后续访问请求来访问引用。换句话说就是需要对服务器发起多轮请求。

Operational Factors and Data Models

对使用MongoDB的应用数据建模不仅仅由数据本身决定,还依赖于MongoDB的特性。例如,不同的数据模型可能会给应用更高的查询效率,更高的插入、更新操作的吞吐量,或者更有效的将行为分布到sharded的集群中。

这些因素是在应用程序之外产生的操作和寻址要求,但是会影响应用程序操作MongoDB的性能。当开发数据模型时,结合下面的注意事项分析你应用程序的所有读写操作。

Document Growth

向document内队列中插入新的内容,或对document增加新的字段都会造成document占用空间的增加。如前面提到的,如果使用的是MMAPv1存储引擎,document size的增加需要在设计数据模型时被考虑到。如果document的size超出了分配的空间,MongoDB将会把这个document移到磁盘上。MongoDB v3.0.0默认使用Power of 2 Sized Allocation最小化了可能发生的重新分配,以及允许有效的重新使用释放出来的记录空间。

所以当使用MMAPv1时,如果你的应用程序需要频繁的更新,造成document占用空间快速增长很快就超出了当前power of 2 allocation限制,你可能需要重构你的数据模型改用references.

你可能还会采用预分配的策略来明确的避免document growth。参考Pre-Aggregated Reports Use Case

Atomicity

MongoDB中原子操作是document级的。一次操作不能针对多个documents。会修改多个documents的操作一次也只能针对一个document操作。确保你的应用程序将有原子依赖的数据存储在同一个document中。如果应用能够容忍对两片数据进行非原子操作的更新,则可以将这两片数据存储在不同的documents中。

将相关数据嵌入在同一个document中的数据模型有助于原子操作。对于引用方式的数据模型,应用程序需要分别发起读和写操作来检索和修改相应的数据片。

Sharding

MongoDB使用sharding来提供水平扩展。集群支持大规模数据集的部署、以及高吞吐量的操作。Sharding允许用户分割同一个数据库中的collection,并跨越多个示例或shards分发这些collection的documents。在分片集合中分发数据和应用流量,MongoDB使用shard key。选择合适的shard key对性能有重要影响,并且可以启动或阻止查询隔离和增加写能力。

仔细考虑要用作shard key的域非常重要。

Indexes

对普通查询,使用index可以提高查询性能。为经常查询的字段和返回排序结果的所有操作构建索引。MongoDB会自动为_id字段创建一个唯一的索引。

在创建索引时,考虑索引的以下行为:

  • 每一个索引至少需要8KB的数据空间。
  • 添加索引对写操作的性能有一些负面影响。对于具有高写读比(write-to-read ratio)的collections,索引的代价非常高,因为每次插入操作都需要更新索引。
  • 对于高读写比(read-to-write ratio)的collections,创建额外的索引非常有用。index不会对未索引的读操作产生影响。
  • 创建后,每个索引都会消耗磁盘和内存空间。这种用法是有意义的,并且需要追踪容量规范,尤其是对工作集大小的关注。

参考indexing Strategies, Analyze Query Performance, database profiler.

Large Number of Collections

在某些情况下,你可能选择将相关信息存储在多个collections中,而不是单个collection中。

考虑一个示例collection——logs用于存储各种环境和应用的log文档。logs collection包含如下格式的documents:

{ log: "dev", ts: ..., info: ... }
{ log: "debug", ts: ..., info: ...}

如果documents总量比较小,可以直接在collection中根据type对documents进行分组。但是对于logs,考虑维护不同的log collections,例如logs_devlogs_debuglogs_dev只包含开发环境相关的documents。

通常,拥有大量的collections不会有明显的性能损失,也不会导致非常好性能。但是,不同的collections对于高吞吐量的批处理非常重要。

当你使用的模型有大量的collections,需要考虑下面的行为:

  • 每一个collection都有确定的几KB的最小开销。

  • 每一个index,包括默认的_idindex都需要至少8KB的数据空间。

  • 对于每个database,单个namespace文件(例如.ns)存储了database的所有meta-data,每个index和collection在namespace文件中都有自己的条目。MongoDB对namespace文件的大小设置了限制。limits on the size of namespace files

  • MongoDB使用MMAPv1存储引擎时,对namespace的个数有限制(limits on the number of namespaces)。你可能想要知道当前namespace的数量,以便知道还可以容纳多少个额外的namespace。想要知道当前namespace的数量,可以通过下面的shell命令:

      db.system.namespaces.count()
    

    namespace的数量限制取决了namespace文件的大小。namespace文件的默认大小是16MB。如果需要改变namespace文件的大小,在启动server的时候使用选项--nssize 。对于已经存在的database,使用--nssize启动server后,在mongo shell中运行命令db.repairDatabase()

Collection Contains Large Number of Small Documents

如果你有一个collection有大量的小documents,处于性能方面的原因你应该考虑嵌入式的方式。如果你可以通过某种逻辑关系将这些小documents分组并且你经常通过这个分组检索这些documents,你可能可以考虑将这些documents卷起来(rolling-up)放到一个含有一个嵌入式数组的大document中。

将这些小文档“卷起来”组成逻辑分组意味着检索一个分组文档的查询涉及到的是顺序读取和很少的随机访问磁盘。另外将小文档“卷起来”放到一个大文档的公共字段中还有利于对这些字段的检索。这意味着对应index中将存在更少的公共字段的拷贝,和更少的关联键条目。

但是如果你经常只需要检索一个分组中的文档的子集,那么将它们“卷起来”放到一起可能并不能提供更好的性能。另外,如果小的文档表示的是数据的自然模型,那么你应该保持现状。

Storage Optimization for Small Documents

每个MongoDB文档都会有一些开销。通常情况下这些开销是微不足道的,但是如果你的所有文档都只有很少的字节这些开销就变得不可忽略不记了,比如你的documents只有一两个字段,则可能会出现这种情况。

对于优化这类集合的存储利用率,考虑如下建议和策略:

  • 显示的使用_id字段

    MongoDB会自动的为每一个document创建一个_id字段,并_id产生一个唯一的12bit的ObjectID。另外MongoDB总是为_id字段创建索引。对于较小的文档,这可能相对来来说占据了更大的空间。

    要优化对存储的使用,用户可以在向collection插入一个documents的时候显示的指定的_id字段的值。此策略允许应用程序在_id字段中存入一个值,如果这个值不存在_id字段中将会在文档的其他地方占用空间。

    你可以在_id字段中存入任何值,但是因为这个值将被作为collection中documents的primary key,它必须是唯一的。

  • 使用更短的字段名

    MongoDB在每一个document中存储所有的字段名。对于大多数documents,这代表文档本身只使用很少的存储空间。但是对于小的document,这意味着文档本身所占用的空间比例很大。考虑类似于拥有如下小文档的collection:

      { last_name : "Smith", best_score: 3.9 }
    

    如果你使用缩短的字段名,如将last_name改为lnamebest_score改为score。那么你就可以节省9Bytes的空间。

      { lname : "Smith", score : 3.9 }
    

注意:缩短字段名会降低表现力,并且不会对大文档和文档开销不是很重要的问题的情况提供明显的好处。较短的字段名也不会减少index占用的空间,因为index有预定义的结构。

一般情况下没有必要使用缩短的字段名。

  • Embedded Documents

    有些情况下,你可能希望将一个小文档嵌入到一个大的文档中,来节省每个文档的开销。

Data Lifecycle Management

数据建模是应该要考虑数据的生命周期管理。

collection的Time to Live or TTL feature将在一段时间后过期documents。如果你的应用程序只需要一些数据在有限的时间内存在于数据库中,可以考虑使用TTL功能。

另外,如果你的应用程序只使用最近插入的documents,考虑使用Capped Collections。Capped collections对插入的documents提供先进先出(FIFO)管理,并有效的支持基于插入顺序的插入和读取文档操作。

Data Model Examples and Patterns

Model Relationships Between Documents

Model One-to-One Relationships with Embedded Documents

考虑下面的示例映射patronaddress之间的关系。该示例显示了如果你需要在一个实体上下文中查看另一个实体数据时,使用嵌入式方式相对于引用方式的好处。在patronaddress数据是一对一关系中,address属于patron

在规范化数据模型中,address文档包含了一个对patron文档的引用.

{
   _id: "joe",
   name: "Joe Bookreader"
}

{
   patron_id: "joe",
   street: "123 Fake Street",
   city: "Faketon",
   state: "MA",
   zip: "12345"
}

如果address数据需要频繁的同name一起被检索,在References模型中,你的应用需要发起查询请求来解析引用。更好的数据模型是将address嵌入到patron数据中,如下所示:

{
   _id: "joe",
   name: "Joe Bookreader",
   address: {
              street: "123 Fake Street",
              city: "Faketon",
              state: "MA",
              zip: "12345"
            }
}

使用这种嵌入式数据模型,你的应用程序只需一次查询请求就可以检索到所有的用户信息。

Model One-to-Many Relationships with Embedded Documents

接着上面的那个例子,如果一个patron有多个address,使用引用数据模型的示例如下:

{
   _id: "joe",
   name: "Joe Bookreader"
}

{
   patron_id: "joe",
   street: "123 Fake Street",
   city: "Faketon",
   state: "MA",
   zip: "12345"
}

{
   patron_id: "joe",
   street: "1 Some Other Street",
   city: "Boston",
   state: "MA",
   zip: "12345"
}

同上面一对一关系示例,现在一对多关系的情况下,如果你的应用程序需要平凡的获取用户地址,那么你应用程序需要发起的查询请求更多。改成嵌入式模型即可一次完成所有的信息检索:

{
   _id: "joe",
   name: "Joe Bookreader",
   addresses: [
                {
                  street: "123 Fake Street",
                  city: "Faketon",
                  state: "MA",
                  zip: "12345"
                },
                {
                  street: "1 Some Other Street",
                  city: "Boston",
                  state: "MA",
                  zip: "12345"
                }
              ]
 }

Model One-to-Many Relationships with Document References

考虑如下示例映射publisherbook之间的关系。这个示例展示了,References数据模型在避免publisher信息重复上比Embedding的优势。

publisher信息嵌入到book文档中将造成publisher信息的重复,如下所示:

{
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",
   publisher: {
              name: "O'Reilly Media",
              founded: 1980,
              location: "CA"
            }
}

{
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",
   publisher: {
              name: "O'Reilly Media",
              founded: 1980,
              location: "CA"
            }
}

如果要避免这种重复,使用Reference数据模型,将publisher信息从book collection中剥出来放到一个单独的collection中。

当使用References模型是,关系增长的方式决定了在哪里存储References。例如这个示例中,如果每个publisher对应的book数增长有限,则将对book的引用存储publisher documents中会比较有用。否则,如果一个publisher对应book数是无边界的,这种数据模型将造成可变的、不断增长的数组,如下面的示例所示:

{
   name: "O'Reilly Media",
   founded: 1980,
   location: "CA",
   books: [123456789, 234567890, ...]
}

{
    _id: 123456789,
    title: "MongoDB: The Definitive Guide",
    author: [ "Kristina Chodorow", "Mike Dirolf" ],
    published_date: ISODate("2010-09-24"),
    pages: 216,
    language: "English"
}

{
   _id: 234567890,
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English"
}

要避免这种可变的、不断增长的数组,可以将publisher的Reference存储在bookdocument中。

{
   _id: "oreilly",
   name: "O'Reilly Media",
   founded: 1980,
   location: "CA"
}

{
   _id: 123456789,
   title: "MongoDB: The Definitive Guide",
   author: [ "Kristina Chodorow", "Mike Dirolf" ],
   published_date: ISODate("2010-09-24"),
   pages: 216,
   language: "English",
   publisher_id: "oreilly"
}

{
   _id: 234567890,
   title: "50 Tips and Tricks for MongoDB Developer",
   author: "Kristina Chodorow",
   published_date: ISODate("2011-05-06"),
   pages: 68,
   language: "English",
   publisher_id: "oreilly"
}

Model Tree Structures

MongoDB允许使用各种形式的树形数据结构对大型层次结构和嵌套数据关系建模。考虑如下树形结构的数据,后面示例都将围绕此图结构的数据建模。

[译]MongoDB数据建模介绍_第3张图片

Model Tree Structures with Parent References

Parent Reference模式将每个树节点存储在一个document中。除了树节点外,document还会存父节点的ID。所以对于示例图中的树形结构Parent Reference模式的存储方式如下:

db.categories.insert( { _id: "MongoDB", parent: "Databases" } )
db.categories.insert( { _id: "dbm", parent: "Databases" } )
db.categories.insert( { _id: "Databases", parent: "Programming" } )
db.categories.insert( { _id: "Languages", parent: "Programming" } )
db.categories.insert( { _id: "Programming", parent: "Books" } )
db.categories.insert( { _id: "Books", parent: null } )
  • 检索节点父亲的查询快速且简单

      db.categories.findOne( { _id: "MongoDB" } ).parent
    
  • 你可以对parent字段创建索引,以快速的查询父亲节点

      db.categories.createIndex( { parent: 1 } )
    
  • 你可以通过parent字段查询它的直接子节点

      db.categories.find( { parent: "Databases" } )
    

Parent Reference模式对树结构提供了简单的存储方案,但是如果要检索子树则需要多次查询。

Model Tree Structures with Child References

Child References模式同样为每一个树节点存储一个document,另外每一个节点document中还会存一个数组记录这个节点子节点的ID。示例图中树形结构Child References模式的存储方式如下:

db.categories.insert( { _id: "MongoDB", children: [] } )
db.categories.insert( { _id: "dbm", children: [] } )
db.categories.insert( { _id: "Databases", children: [ "MongoDB", "dbm" ] } )
db.categories.insert( { _id: "Languages", children: [] } )
db.categories.insert( { _id: "Programming", children: [ "Databases", "Languages" ] } )
db.categories.insert( { _id: "Books", children: [ "Programming" ] } )
  • 检索一个节点的直接孩子节点的查询快速且简单。

      db.categories.findOne( { _id: "Databases" } ).children
    
  • 你可以为children字段创建所以来实现快速检索子节点。

      db.categories.createIndex( { children: 1 } )
    
  • 你可以通过child字段中的一个节点查询他的父亲节点,同样也可以查到他的兄弟节点。

      db.categories.find( { children: "MongoDB" } )
    

Child References模式为树形结构的存储提供了一个很好的方案,但是不太适合需要对子树进行检索的方案。这种模式还可以用于对图的存储提供解决方案,图中,一个节点往往有多个子节点。

Model Tree Structures with an Array of Ancestors

Array of Ancestors模式同样将每个节点存储在一个单独的document中。另外每个节点document中还有一个数组用于存储节点的祖先ID或路径。除了祖先外,另外还有一个字段存储节点直接父亲节点的ID引用,示例图的Arrar of Ancestors模式存储方式如下:

db.categories.insert( { _id: "MongoDB", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
db.categories.insert( { _id: "dbm", ancestors: [ "Books", "Programming", "Databases" ], parent: "Databases" } )
db.categories.insert( { _id: "Databases", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
db.categories.insert( { _id: "Languages", ancestors: [ "Books", "Programming" ], parent: "Programming" } )
db.categories.insert( { _id: "Programming", ancestors: [ "Books" ], parent: "Books" } )
db.categories.insert( { _id: "Books", ancestors: [ ], parent: null } )
  • 检索节点祖先或路径的查询快速且简单。

      db.categories.findOne( { _id: "MongoDB" } ).ancestors
    
  • 可以为ancestors字段创建索引,以快速的查询祖先节点。

      db.categories.createIndex( { ancestors: 1 } )
    
  • 你可以通过ancestors字段查找一个节点的所有后裔。

      db.categories.find( { ancestors: "Programming" } )
    

Array of Ancestors模式可以通过对ancestors字段创建索引来提供一个快速且有效的查找后裔和祖先节点的方案。这使得这种模式能够适用于需要的子树进行检索的情况。

Array of Ancestors模式比即将要介绍的Materialized Paths稍慢,但是使用起来更简单。

Model Tree Structures with Materialized Paths

Materialized Paths模式也为每个节点存储一个document。每个document除了存储节点信息还,还有一个额外的字段以string的形式存储节点的所有祖先或path。虽然Materialized Paths模式需要额外的步骤处理string或正则表达式,这种模式对于处理路径,尤其是通过部分路径寻找节点提供了更加灵活的方式。

对于前面示例图中的树形结构,Materialized Paths模式的存储方式如下:

db.categories.insert( { _id: "Books", path: null } )
db.categories.insert( { _id: "Programming", path: ",Books," } )
db.categories.insert( { _id: "Databases", path: ",Books,Programming," } )
db.categories.insert( { _id: "Languages", path: ",Books,Programming," } )
db.categories.insert( { _id: "MongoDB", path: ",Books,Programming,Databases," } )
db.categories.insert( { _id: "dbm", path: ",Books,Programming,Databases," } )
  • 你可以通过检索整棵树来查询,通过path字段排序

      db.categories.find().sort( { path: 1 } )
    
  • 你可以使用正则表达来式来查找某一个节点的所有后代,如programming:

      db.categories.find( { path: /,Programming,/ } )
    
  • 同样,你可以检索根节点的所有后代:

      db.categories.find( { path: /^,Books,/ } )
    
  • Path字段创建索引:

      db.categories.createIndex( { path: 1 } )
    

至于index对性能的改进则取决于查询方式:

  • 对于检索一个从节点开始的路径的子树,如/^,Books,/ or /^,Books,Programming,/,则对path创建的索引将对查询性能有显著提高。
  • 对于不是从根节点开始的路径的子树的查询,如/,Databases,/,或者类似的子树查询。这种查询的节点可能在索引字符串的中间,查询必须检查整个索引。对于这种情况,索引可能会对查询性能有所改善,但是这取决于index相对于整个collection是否足够的小。

Model Tree Structures with Nested Sets

Nested Set模式将树的每个节点标识为树的往返遍历中的途径点。应用程序将访问树中的每个节点两次,第一次在初始行程期间,第二次在返回行程期间。Nested Set模式为每个节点存储一个document,另外还在document中存储节点的父亲,和在left字段中存储初始行程的途径,和在right字段中存储返回行程的途经信息。

本章开始示例图的Nested Set模式图式和数据库表示如下图所示:

[译]MongoDB数据建模介绍_第4张图片

db.categories.insert( { _id: "Books", parent: 0, left: 1, right: 12 } )
db.categories.insert( { _id: "Programming", parent: "Books", left: 2, right: 11 } )
db.categories.insert( { _id: "Languages", parent: "Programming", left: 3, right: 4 } )
db.categories.insert( { _id: "Databases", parent: "Programming", left: 5, right: 10 } )
db.categories.insert( { _id: "MongoDB", parent: "Databases", left: 6, right: 7 } )
db.categories.insert( { _id: "dbm", parent: "Databases", left: 8, right: 9 } )

你可以通过查询检索一个节点的后代:

var databaseCategory = db.categories.findOne( { _id: "Databases" } );
db.categories.find( { left: { $gt: databaseCategory.left }, right: { $lt: databaseCategory.right } } );

Nested Set模式对于检索子树提供了快速、高效的方案,但是如果要修改树的数据结构则是低效的。所以这种结构最好应用于不需要修改的静态树。

Model Specific Application Contexts

Model Data for Atomic Operations

MongoDB写操作,如db.collection.update(), db.collection.findAndModify(), db.collection.remove()的原子操作是在document层的。对于需要一起update的字段,需要将他们嵌入在同一个document中以确保能对他们进行原子更新。

示例:

{
    _id: 123456789,
    title: "MongoDB: The Definitive Guide",
    author: [ "Kristina Chodorow", "Mike Dirolf" ],
    published_date: ISODate("2010-09-24"),
    pages: 216,
    language: "English",
    publisher_id: "oreilly",
    available: 3,
    checkout: [ { by: "joe", date: ISODate("2012-10-15") } ]
}

Update

db.books.update (
   { _id: 123456789, available: { $gt: 0 } },
   {
     $inc: { available: -1 },
     $push: { checkout: { by: "abc", date: new Date() } }
   }
)

return

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

Model Data to Support Keyword Search

关键字搜索不同于文本搜索和全文本搜索,并且不提供词干(stemming)和其他文本处理功能。参考Limitations of Keyword Indexes了解更多信息。

如果应用程序需要查询包含存储文本的字段的内容,可以执行完全匹配,也可以使用$regex通过正则表达式匹配。但是对于文本的某些操作,这些方法并不能满足应用程序的要求。

这种模式描述了一种使用MongoDB支持关键字搜索的方法,以支持应用程序搜索功能,该功能使用与文本字段存储在同一个documents中的关键字数组。结合多键索引(multi-key index),这种模式能够支持应用程序的关键字索引。

模式

为你的documents增加结构体以支持关键字查询,在documents中创建一个数组字段,并将关键字以string类型存储在数组中。然后你就可以对这个数据创建一个multi-key索引,并且通过从数组中选择值创建查询。

示例:

{ title : "Moby-Dick" ,
  author : "Herman Melville" ,
  published : 1851 ,
  ISBN : 0451526996 ,
  topics : [ "whaling" , "allegory" , "revenge" , "American" ,
    "novel" , "nautical" , "voyage" , "Cape Cod" ]
}

topic字段创建一个multi-key索引:

db.volumes.createIndex( { topics: 1 } )

multi-key索引分别为topic数组中的每一个关键字创建一个索引入口。例如这里一个入口是给whaling的,还有另外一个入口是给allegory的。

然后接可以基于关键字进行查询了,例如:

db.volumes.findOne( { topics : "voyage" }, { title: 1 } )

注意一个有大量元素的数组,会在插入时产生较大的索引成本

关键字索引的限制

MongoDB通过特定的数据模型和multi-key index支持关键字查询,但是,这些关键字索引在以下情况下不足以与全文本方式相比:

  • 词干(stemming)。MongoDB中的关键字不能解析root或相关词的关键字。
  • 同义词(Synonyms)。基于关键字的搜索功能必须在应用层支持同义词或相关的查询。
  • 排行(ranking)。本文描述的关键字查找不提供给结果增加权重的方法。
  • 异步索引(Asynchronous Indexing)。MongoDB创建索引是同步的,这意味着关键字索引的索引总是最新的,并且可以实时操作。但是异步批量索引对于某些种类的内容和工作负载可能更有效。

Model Monetary Data

处理货币数据的应用程序通常需要捕获货币小数部分单位的能力,并且需要在执行算术运算时模拟精确的小数舍入。被很多现代系统使用的基于二进制的浮点运算(例如float、double)不能很好的表示精确的十进制小数,并且需要一定程度的近似,这使得它并不适合于货币算术。这个约束是货币数据建模时的一个重要考虑。

在MongoDB中通过numericnon-numeric模型可以有多种方法对货币数据进行建模。

Numeric Model

当你需要查询数据库获取精确的、数学有效的匹配,或者需要执行服务端的算术运算(如$inc, $mul),或者聚合框架算术(aggregation framework arithmetic)时,那么numeric model是合适的。

如下两种方法遵循numeric model

  • Using the Decimal BSON Type:这是一种基于十进制的浮点格式,能够提供精确的精度。MongoDB 3.4及以后的版本支持。
  • Using a Scale Factor:将货币数据通过乘以10比例因子的乘方转换为64位整数(长BSON类型)

十进制BSON type使用IEEE 754 decimal 128基于十进制的浮点数字格式。不像基于二进制的浮点数据格式(例如,double bson type),decimal 128不会对十进制数进行近似,并且能够提供货币数据所需的精确度。

在MongoDB shell中,decimal values使用NumberDecimal()构造器分配和查询。如下示例向gasprices collection中添加了一个包含gas price的document。NumberDecimal

db.gasprices.insert{ "_id" : 1, "date" : ISODate(), "price" : NumberDecimal("2.099"), "station" : "Quikstop", "grade" : "regular" }

如下查询则匹配上面的document:

db.gasprices.find( { price: NumberDecimal("2.099") } )
Convert Values to Decimal

collection中的数据可以通过执行一次性转换(one-time transformation)或修改应用程序逻辑在访问记录时转换来转换为decimal类型。

  • 一次性collection转换

    可以通过遍历collection中的所有documents来对collection进行转换,将documents中的货币数据转为decimal类型,再写会到collection中。

强烈建议在document中为decimal值增加一个新的字段,并在新的字段通过验证后再删除原来的字段。

警告:
请确保在隔离的测试环境中测试十进制转换。一旦在MongoDB3.4或之后的版本中创建或修改了数据文件,这些文件将不再与之前的版本兼容,并且不支持对包含decimal的文件的降级。

Scale Factor Transformation

考虑如下collection,它使用scale factor方法将货币数据保存为表示分数的64位整数:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong("1999") },
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong("3999") },
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong("2999") },
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong("2495") },
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong("8000") }

通过使用$multiply操作符将price乘以NumberDecimal("0.01")可以将long值转换为格式正确的十进制小数。下面的aggregation pipeline将转换后的结果分配给一个名为priceDec的新字段。

db.clothes.aggregate(
  [
    { $match: { price: { $type: "long" }, priceDec: { $exists: 0 } } },
    {
      $addFields: {
        priceDec: {
          $multiply: [ "$price", NumberDecimal( "0.01" ) ]
        }
      }
    }
  ]
).forEach( ( function( doc ) {
  db.clothes.save( doc );
} ) )

聚合的结果可以通过db.clothes.find()查询进行验证:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberLong(1999), "priceDec" : NumberDecimal("19.99") }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberLong(3999), "priceDec" : NumberDecimal("39.99") }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberLong(2999), "priceDec" : NumberDecimal("29.99") }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberLong(2495), "priceDec" : NumberDecimal("24.95") }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberLong(8000), "priceDec" : NumberDecimal("80.00") }

如果你不想增加一个新的字段来存储十进制值,也可以直接覆盖原始字段。如下的update()方法首先检查long price是否存在,然后将long转换为十进制值,然后再存回到price中。

db.clothes.update(
  { price: { $type: "long" } },
  { $mul: { price: NumberDecimal( "0.01" ) } },
  { multi: 1 }
)

聚合结果:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : NumberDecimal("19.99") }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : NumberDecimal("39.99") }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : NumberDecimal("29.99") }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : NumberDecimal("24.95") }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : NumberDecimal("80.00") }

Non-Numeric Transformation

考虑如下collection,它使用non-numeric模型将货币数据的具体值以string类型精确表示:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99" }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99" }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99" }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95" }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00" }

如下方法首先检查price是否存在并且是string类型,然后将string值转换为十进制值存储到priceDec字段中:

db.clothes.find( { $and : [ { price: { $exists: true } }, { price: { $type: "string" } } ] } ).forEach( function( doc ) {
  doc.priceDec = NumberDecimal( doc.price );
  db.clothes.save( doc );
} );

转换结果:

{ "_id" : 1, "description" : "T-Shirt", "size" : "M", "price" : "19.99", "priceDec" : NumberDecimal("19.99") }
{ "_id" : 2, "description" : "Jeans", "size" : "36", "price" : "39.99", "priceDec" : NumberDecimal("39.99") }
{ "_id" : 3, "description" : "Shorts", "size" : "32", "price" : "29.99", "priceDec" : NumberDecimal("29.99") }
{ "_id" : 4, "description" : "Cool T-Shirt", "size" : "L", "price" : "24.95", "priceDec" : NumberDecimal("24.95") }
{ "_id" : 5, "description" : "Designer Jeans", "size" : "30", "price" : "80.00", "priceDec" : NumberDecimal("80.00") }

Application Logic Transformation

还可以在应用程序逻辑内执行向十进制数据的转换。在这种情况下,修改应用程序逻辑,在访问记录时执行转换。典型的应用程序逻辑如下:

  • 测试新字段是否存在,并且新字段是十进制类型;

  • 如果新十进制字段不存在:

    • 通过正确的转换旧字段来创建它;
    • 删除旧字段;
    • 保留转换记录;

Using a Scale Factor

注:如果你使用MongoDB 3.4或更高的版本。使用decimal类型对货币数据进行建模是比使用Scale Factor更好的方法。

使用Scale Factor对货币数据建模:

  • 确定货币数据所需要的最大精度。例如,你的应用程序可能需要货币数据的精度是美元的0.1分。
  • 将货币数据乘以10的幂转换为整型数据,确保你的应用程序需要的精度是整数的最低有效位。例如,如果你需要的经度是0.1分,则需要乘以1000.
  • 存储转换后的数据。

例如,如下示例将9.99美元扩大1000倍达到0.1分的精度。

{ price: 9990, currency: "USD" }

这种方法对给定的货币值做如下假设:

  • 比例因子对于同一种货币是相同的。
  • 比例因子是货币固定的、已知的属性。应用程序可以通过货币确定比例因子。

使用这种模型时,应用程序必须在使用执行对值的适当缩放时保证一致。

Non-Numeric Model

如果没有必要在服务器端对货币数据执行算术运算,或者在服务器端的近似已经足够,则对货币数据使用non-numeric模型建模可能是合适的。

如下方法遵循non-numeric模型:

  • Using two fields for the monetary value:一个字段将精确的货币数据以non-numeric的string存储,另一个字段存储基于二进制的浮点近似值(double BSON type)。
Using two fields for the Monetary value
  1. 在一个字段中将精确的货币数据编码为non-numeric数据类型,例如BinDatastring
  2. 在第二个字段中存储精确值得双精度浮点近似值。

如下示例使用non-numeric模型表示9.99 USD的price,和0.25 USD的fee:

{
  price: { display: "9.99", approx: 9.9900000000000002, currency: "USD" },
  fee: { display: "0.25", approx: 0.2499999999999999, currency: "USD" }
}

需要注意的是(with some care):应用程序可以使用数据近似对字段执行范围和排序呢查询。但是,使用approximation字段进行查询或排序操作要求应用程序执行客户端的post-processing以解码精确数据的non-numeric表示,然后基于确切的货币数据过滤返回的文档。

Model Time Data

MongoDB默认情况下stores times in UTC,并且会将任何本地时间表示转换成这种形式。

必须操作或者报告一些未经修改的本地时间值的应用程序可以存储一些与UTC时间戳并列的时区,并且在应用程序逻辑中计算原始的本地时间。

示例,你可以在MongoDB shell中存储当前日期以及UTC的当前客户端offset:

var now = new Date();
db.data.save( { date: now,
                offset: now.getTimezoneOffset() } );

你也可以通过应用已经保存的offset重新构造本地原始时间:

var record = db.data.findOne();
var localNow = new Date( record.date.getTime() -  ( record.offset * 60000 ) );

你可能感兴趣的:(Mongo)