翻出一篇很早以前翻译的文章。
官方文档:data models
不像SQL数据库一样,MongoDB中的数据有着灵活的模式。MongDB collection中的数据不强制document的数据结构。这种灵活性有助于将document映射到实体或者对象。每个document可以匹配描绘实体的相应数据字段,即使数据可能会发生变化。然而在实际使用中,同一个collection中的documents共享类似的数据结构。
数据建模的关键挑战是平衡应用需求,数据库引擎的性能特性和数据检索模式。在设计数据模型的时候,总是要考虑应用对数据的使用方式(如查询、更新、处理数据等)以及数据本身的固有结构。
为MongoDB应用程序设计数据模型的关键是围绕document数据结构和应用程序如何表示数据之间的关系。有两个工具(两种方法)表示数据之间的关系:references和embedded document
References通过在一个document中存储链接或对其他document的引用来来表示数据之间的关系。应用程序可以解析这些应用来访问相关数据。这些是规范化的数据模型(Normalized Data Models)。
Embedded Documents通过将相关的数据存储在同一个document中来捕获数据的相关性。MongoDB的document可以实现将一个document结构体嵌入到另一个document的一个域或数组中。这种非规范化的数据模型(denormalized data models)允许应用程序在单次数据库操作中访问、更新相关的数据。
详细内容参考Embedded Data Models
MongoDB中,写操作只在document级是原子的,没有单个的写操作可以原子的覆盖多个documents或collections。非规范化数据模型中,embedded data将一个实体需要呈现的相关数据绑定在同一个document中,这有助于写的原子操作,因为一次写操作可以插入或更新实体的所有数据。规范化的数据模型将数据拆散到多个数据collections中,需要多个不是原子集合的写操作才能实现实体数据的完全插入和更新。
然而,促进原子写的模式可能限制应用程序使用数据的方式,或者限制修改应用程序的方式。Atomicity Considerations文档描述了设计schema时平衡灵活性和原子性的挑战。
当更新一个documents,如插入数据等,document占用的空间会增加。
对于MMAPv1
存储引擎,如果document占用的空间大小超过了为document分配的空间,MongoDB会将这个document重定位到磁盘上。当使用MMAPv1
存储引擎时,对数据增长的考虑将影响使用规范化还是非规范话的决定。参考Document Growth Considerations
当设计数据模型的时候,需要充分考虑应用程序怎么使用你的数据库。例如,如果你的应用程序只使用最近插入的documents,考虑使用Capped Collections。或者,你的应用程序主要是对一个collections的读操作,则可以增加一些通用查询的indexes来提高性能。
参考Operational Factors and Data Models 获取更多有关会影响数据模型设计的考虑。
考虑MongoDB中数据建模的以下几个方面:
有效的数据模型应该是能够支持应用需求的。你的document的数据结构的关键是考虑到底使用Embedded还是References。
如前面基本概念中提到的,MongoDB中Embedded Data Models将所有相关的数据都存储在一个Document中。结果就是应用程序只需要发起很少的查询、更新请求就可以完成一个普通的操作。
一般来说,在下面的情况下使用Embedded Data Models
:
包含
关系。参考Model One-to-One 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
一般情况下,如下情形使用规范化的数据模型:
引用比嵌入式提供了更高的灵活性。但是,客户端应用必须发起后续访问请求来访问引用。换句话说就是需要对服务器发起多轮请求。
对使用MongoDB的应用数据建模不仅仅由数据本身决定,还依赖于MongoDB的特性。例如,不同的数据模型可能会给应用更高的查询效率,更高的插入、更新操作的吞吐量,或者更有效的将行为分布到sharded的集群中。
这些因素是在应用程序之外产生的操作和寻址要求,但是会影响应用程序操作MongoDB的性能。当开发数据模型时,结合下面的注意事项分析你应用程序的所有读写操作。
向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
MongoDB中原子操作是document级的。一次操作不能针对多个documents。会修改多个documents的操作一次也只能针对一个document操作。确保你的应用程序将有原子依赖的数据存储在同一个document中。如果应用能够容忍对两片数据进行非原子操作的更新,则可以将这两片数据存储在不同的documents中。
将相关数据嵌入在同一个document中的数据模型有助于原子操作。对于引用方式的数据模型,应用程序需要分别发起读和写操作来检索和修改相应的数据片。
MongoDB使用sharding来提供水平扩展。集群支持大规模数据集的部署、以及高吞吐量的操作。Sharding允许用户分割同一个数据库中的collection,并跨越多个示例或shards分发这些collection的documents。在分片集合中分发数据和应用流量,MongoDB使用shard key。选择合适的shard key对性能有重要影响,并且可以启动或阻止查询隔离和增加写能力。
仔细考虑要用作shard key的域非常重要。
对普通查询,使用index可以提高查询性能。为经常查询的字段和返回排序结果的所有操作构建索引。MongoDB会自动为_id
字段创建一个唯一的索引。
在创建索引时,考虑索引的以下行为:
参考indexing Strategies, Analyze Query Performance, database profiler.
在某些情况下,你可能选择将相关信息存储在多个collections中,而不是单个collection中。
考虑一个示例collection——logs用于存储各种环境和应用的log文档。logs collection包含如下格式的documents:
{ log: "dev", ts: ..., info: ... }
{ log: "debug", ts: ..., info: ...}
如果documents总量比较小,可以直接在collection中根据type
对documents进行分组。但是对于logs,考虑维护不同的log collections,例如logs_dev
和logs_debug
,logs_dev
只包含开发环境相关的documents。
通常,拥有大量的collections不会有明显的性能损失,也不会导致非常好性能。但是,不同的collections对于高吞吐量的批处理非常重要。
当你使用的模型有大量的collections,需要考虑下面的行为:
每一个collection都有确定的几KB的最小开销。
每一个index,包括默认的_id
index都需要至少8KB的数据空间。
对于每个database,单个namespace文件(例如
)存储了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有大量的小documents,处于性能方面的原因你应该考虑嵌入式的方式。如果你可以通过某种逻辑关系将这些小documents分组并且你经常通过这个分组检索这些documents,你可能可以考虑将这些documents卷起来(rolling-up)放到一个含有一个嵌入式数组的大document中。
将这些小文档“卷起来”组成逻辑分组意味着检索一个分组文档的查询涉及到的是顺序读取和很少的随机访问磁盘。另外将小文档“卷起来”放到一个大文档的公共字段中还有利于对这些字段的检索。这意味着对应index中将存在更少的公共字段的拷贝,和更少的关联键条目。
但是如果你经常只需要检索一个分组中的文档的子集,那么将它们“卷起来”放到一起可能并不能提供更好的性能。另外,如果小的文档表示的是数据的自然模型,那么你应该保持现状。
每个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
改为lname
,best_score
改为score
。那么你就可以节省9Bytes的空间。
{ lname : "Smith", score : 3.9 }
注意:缩短字段名会降低表现力,并且不会对大文档和文档开销不是很重要的问题的情况提供明显的好处。较短的字段名也不会减少index占用的空间,因为index有预定义的结构。
一般情况下没有必要使用缩短的字段名。
Embedded Documents
有些情况下,你可能希望将一个小文档嵌入到一个大的文档中,来节省每个文档的开销。
数据建模是应该要考虑数据的生命周期管理。
collection的Time to Live or TTL feature将在一段时间后过期documents。如果你的应用程序只需要一些数据在有限的时间内存在于数据库中,可以考虑使用TTL功能。
另外,如果你的应用程序只使用最近插入的documents,考虑使用Capped Collections。Capped collections对插入的documents提供先进先出(FIFO)管理,并有效的支持基于插入顺序的插入和读取文档操作。
考虑下面的示例映射patron
和address
之间的关系。该示例显示了如果你需要在一个实体上下文中查看另一个实体数据时,使用嵌入式方式相对于引用方式的好处。在patron
与address
数据是一对一关系中,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"
}
}
使用这种嵌入式数据模型,你的应用程序只需一次查询请求就可以检索到所有的用户信息。
接着上面的那个例子,如果一个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"
}
]
}
考虑如下示例映射publisher
和book
之间的关系。这个示例展示了,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存储在book
document中。
{
_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"
}
MongoDB允许使用各种形式的树形数据结构对大型层次结构和嵌套数据关系建模。考虑如下树形结构的数据,后面示例都将围绕此图结构的数据建模。
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
模式对树结构提供了简单的存储方案,但是如果要检索子树则需要多次查询。
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
模式为树形结构的存储提供了一个很好的方案,但是不太适合需要对子树进行检索的方案。这种模式还可以用于对图的存储提供解决方案,图中,一个节点往往有多个子节点。
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
稍慢,但是使用起来更简单。
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是否足够的小。Nested Set
模式将树的每个节点标识为树的往返遍历中的途径点。应用程序将访问树中的每个节点两次,第一次在初始行程期间,第二次在返回行程期间。Nested Set
模式为每个节点存储一个document,另外还在document中存储节点的父亲,和在left
字段中存储初始行程的途径,和在right
字段中存储返回行程的途经信息。
本章开始示例图的Nested Set
模式图式和数据库表示如下图所示:
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
模式对于检索子树提供了快速、高效的方案,但是如果要修改树的数据结构则是低效的。所以这种结构最好应用于不需要修改的静态树。
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 })
关键字搜索不同于文本搜索和全文本搜索,并且不提供词干(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
支持关键字查询,但是,这些关键字索引在以下情况下不足以与全文本方式相比:
root
或相关词的关键字。处理货币数据的应用程序通常需要捕获货币小数部分单位的能力,并且需要在执行算术运算时模拟精确的小数舍入。被很多现代系统使用的基于二进制的浮点运算(例如float、double)不能很好的表示精确的十进制小数,并且需要一定程度的近似,这使得它并不适合于货币算术。这个约束是货币数据建模时的一个重要考虑。
在MongoDB中通过numeric
和non-numeric
模型可以有多种方法对货币数据进行建模。
当你需要查询数据库获取精确的、数学有效的匹配,或者需要执行服务端的算术运算(如$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") } )
collection中的数据可以通过执行一次性转换(one-time transformation)或修改应用程序逻辑在访问记录时转换来转换为decimal
类型。
一次性collection转换
可以通过遍历collection中的所有documents来对collection进行转换,将documents中的货币数据转为decimal
类型,再写会到collection中。
强烈建议在document中为
decimal
值增加一个新的字段,并在新的字段通过验证后再删除原来的字段。警告:
请确保在隔离的测试环境中测试十进制转换。一旦在MongoDB3.4或之后的版本中创建或修改了数据文件,这些文件将不再与之前的版本兼容,并且不支持对包含decimal
的文件的降级。
考虑如下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") }
考虑如下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") }
还可以在应用程序逻辑内执行向十进制数据的转换。在这种情况下,修改应用程序逻辑,在访问记录时执行转换。典型的应用程序逻辑如下:
测试新字段是否存在,并且新字段是十进制类型;
如果新十进制字段不存在:
注:如果你使用MongoDB 3.4或更高的版本。使用
decimal
类型对货币数据进行建模是比使用Scale Factor
更好的方法。
使用Scale Factor
对货币数据建模:
例如,如下示例将9.99美元扩大1000倍达到0.1分的精度。
{ price: 9990, currency: "USD" }
这种方法对给定的货币值做如下假设:
使用这种模型时,应用程序必须在使用执行对值的适当缩放时保证一致。
如果没有必要在服务器端对货币数据执行算术运算,或者在服务器端的近似已经足够,则对货币数据使用non-numeric
模型建模可能是合适的。
如下方法遵循non-numeric
模型:
Using two fields for the monetary value
:一个字段将精确的货币数据以non-numeric
的string存储,另一个字段存储基于二进制的浮点近似值(double BSON type)。non-numeric
数据类型,例如BinData
或string
如下示例使用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
表示,然后基于确切的货币数据过滤返回的文档。
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 ) );