非关系型数据库(NoSQL)

时间复杂度为O(n).
在大数据集上通过 跳过(skip) + 限制返回数(limit) 进行分页的效率不高,
因为每次查询时都skip都需要对偏移量进行计数.
由于NoSQL数据库在查找单个对象时效率极高, 因此出现了一种基于主键的分页方式.
这种方式优于skip的原因在于它可以直接找到具体的对象, 然后从这个对象开始分页, 而不需要计数.
时间复杂度为O(1).
无法按页数分页, 因此不可能从"第1页"跳转至"第x页".
部分索引允许在建立索引时先用一个查询表达式过滤掉不符合条件的文档,
这使得部分索引能够只对用户需要的文档建立索引, 从而降低索引需要的存储空间.
在文档上创建schema_version字段, 该字段的数字代表了文档使用的模式(schema).
注意: 变更集将无法同步主文档的模式变化, 因此变更集很可能是没有模式的.
可以使用JavaScript Object Notation (JSON) Patch来作为差异补丁的规范.
创建文档副本.
  • 一个集合保存最新版本, ObjectID是维持不变的.
    它的表现和没有版本控制的文档是一样的.
  • 一个集合保存所有旧版本(称为history集合).
    用origin字段引用它原始的ObjectID.
  • 大多数查询都是在最新版本上进行的, 因此要尽可能保证最新版本的查询效率.
  • 数据库支持多文档事务, 或者不需要事务.
使用一个集合进行文档版本控制.
  • 对历史版本的查询是普遍的.
  • 由于使用单个集合, 会导致集合内的索引量不必要的增长.
  • 数据库支持多文档事务, 或者不需要事务.
直接在文档里保存不同版本的文档.
  • 单集合模式无法满足需求(通常是因为索引非常昂贵).
  • 一次查询全部版本是普遍的.
  • 数据库不支持多文档事务.
  • 单文档有16MB的大小限制
  • 过大的单文档可能导致一些性能问题
MongoDB是基于硬盘的功能最丰富的NoSQL数据库, 因此它在NoSQL选型中往往会成为最佳选择.
MongoDB为文档生成ObjectId, 尽管在同一秒内生成的ObjectId可能无法保证插入顺序的正确性,
但总体上而言, ObjectId可以被用于排序和比较.
MongoDB在 分布式部署的情况下 支持多文档ACID事务, 开启事务后会影响数据库的性能.
MongoDB支持在更新和插入文档时对文档进行JSON Schema验证和自定义函数验证.
由于支持部分更新, MongoDB非常适合处理无模式的文档,为一切更新都是在应用程序里完成的.
这些旧API的存在极具误导性.
例如 countcountDocuments, 前者是旧API, 后者是新API,
前者返回的值是估计值, 后者才能返回准确值.
这导致需要 $group 操作的查询全部都需要遍历全表, 导致查询变得不可接受.
尽管MongoDB提供了很多操作符, 并且具有流水线式的外观,
但这些操作符相比SQL仍然显得难以使用, 无法重现一些SQL查询.
MongoDB的WiredTiger引擎在默认情况下会使用256MB或50%-1GB中的较大者作为缓存.
https://www.mongodb.com/docs/manual/reference/program/mongod/#std-option-mongod.--wiredTigerCacheSizeGB
在启用日志记录的情况下, MongoDB可以通过文件系统快照备份.
CouchDB的一个优点是它的资源占用量很低, 比MongoDB要低得多.
CouchDB官方文档建议用户自己生成UUID.
这是因为在创建文档时, 如果依赖于CouchDB自动生成的UUID,
在POST请求出现网络故障时, 可能导致插入两个同样的文档.
例如, 第一个请求在回程时出现网络故障, 所以客户端认为文档没有创建, 发出第二个请求,
但实际上CouchDB已经创建了第一个请求的文档, 只是UUID没能成功返回,
这就导致第一篇文档对客户端"失踪"了.
CouchDB通过在更新时附带文档的修订(rev)与当前数据库里的属性是否吻合来确定更新成功与否.
这是一种乐观并发策略, 因此更新可能会失败, 客户端需要重新查询该id的最新修订号, 重新发起更新.
CouchDB不提供直接查询id以外的文档查询功能,
因此任何高级查询都需要通过CouchDB里内置的视图功能来实现.
每次更新数据库都会导致视图更新.
存储在CouchDB里的视图在内部被称为设计文档(design document), 存储在专门的区域.
然而, MongoDB也提供MapReduce功能, 并且它还支持更好理解的流水线式聚合查询.
CouchDB的视图是通过JavaScript实现的,
本质上是在将一个键值对数据库通过JavaScript代码转换为一种倒排索引,
索引将以 [字段1, 字段2...] -> 文档id 的形式创建映射(对不需要的文档可以不映射).
CouchDB在查询时可以一并获取文档id指向的文档数据.
视图可以使用多个值作为键名, 这使得多参数查询成为可能.
在查询时, 可以用键名控制查询范围和对结果集排序.
和MongoDB一样, CouchDB更适合基于id的分页, 而不是基于页码的分页.
Reduce相当于CouchDB的聚合查询, 它通过JavaScript将Map的结果集计算为单个返回值.
CouchDB并不会在获取所有的数据集之后才运行Reduce的, 这显然会导致不可接受的内存消耗.
用户编写reduce函数时需要注意每次运行后的返回值必须是简化的值, 否则将随着reduce的多次运行增加内存消耗量.
CouchDB没有开箱即用的部分更新, 虽然可以通过设计文档中的update functions实现类似的功能,
但比MongoDB要繁琐得多.
如果需要部分更新, 则 建议使用MongoDB或SQLite3, CouchDB在各种意义上都更像是一种键值对数据库.
为了简化视图查询, CouchDB引入了类似MongoDB的查询方式, 可以通过 $eq 这样的运算符进行查询.
在进行查询时, 需要使用为数据库添加查询字段的索引, 否则效率会很低.
然而, CouchDB只提供了有限数量的Mongo运算符(例如缺乏 =$add=), 以至于仍然需要依赖视图来实现很多查询.
Fauxton是CouchDB自带的Web UI, 提供复制数据库, 添加设计文档等便捷功能.
  • 文档很差, 历经多次迭代, 一些文档是缺失的, 以至于需要对Fauxton进行逆向工程来得到请求格式.
  • 没有原子化的批量删除文档的方法.
  • 删除是以标记实现的, 需要调用purge才能真正删除文档.
  • 官方JavaScript客户端couchdb-nano(v10)设计得很差:
    • 愚蠢的封装(例如head方法).
    • 缺少设计文档相关的API.
    • 愚蠢的错误处理.
      PouchDB(v7.2.2)的API绝对好于它, 但也有自己的问题:
    • 没有提供purge方法(https://github.com/pouchdb/pouchdb/issues/802).
    • TypeScript存在缺陷(bulkDocs没有正确处理接口).
    • 愚蠢的错误处理.
    • 主要是为前端设计的.
  • API设计得很低效率, 例如不能在查询的同时执行批量删除, 而必须获得查询结果, 然后再执行删除.
  • 绝对性能较差, 由多方面因素导致:
    • 使用HTTP作为通讯协议
    • CouchDB使用的数据结构相当于在文件系统上另外建立了一个文件系统
一种内存映射数据库, 支持事务.
尽管它主要的应用场景是读取而不是写入, 但实际上写入也比其他数据库快.
Node.js和Deno的LMDB绑定: https://github.com/kriszyp/lmdb-js
在一个案例中, 相同数据的情况下, 比sqlite3的数据库大13%.
lmdb的key被限制在1978个字节以内, 这在一些情况下会非常容易超出限制.
LevelDB是一个键值对数据库(支持Put, Get, Delete操作), 像SQLite一样使用单个文件.
虽然缺乏第一方Node.js支持, 但第三方支持很好.
LevelDB没有内建的事务支持.
https://github.com/Level/level-ttl
该库会为带有TTL的项目创建以下额外的项目:
  • 基于原始key创建的带有过期时间戳的key(expiryKey) => 项目的原始key
    这个项目是实现TTL过期查询的关键.
  • 基于原始key创建的key(prefixKey) => 过期时间戳
    这个项目的存在是为了能够主动取消项目的TTL, 如果没有这个项目, 就不知道该项目当前的expiryKey.
该库在内部使用setInterval定期查询数据库清理过期的项目.
查询过期项目时利用了LevelDB存储的key是有序的这一特性,
通过设置createReadStream的 ltegte 参数就能查询到过期的 expiryKey.
存在竞争条件:
https://github.com/Level/level-ttl/issues/68
在2020年检查时, 上个版本发布于2018年.
支持ACID.
文档在相当程度上缺失.
缺乏第一方Node.js支持.
由于开源许可证变更为GNU AGPL,
Berkeley DB在一些Linux发行版被淘汰(例如Debian), 转为使用LMDB.
基于LevelDB的一种键值对数据库, 在大于内存的数据集上性能表现优于LevelDB.
使用MongoDB API的一种嵌入式文档数据库.
在内存中有完整的数据库副本, 因此不适用于大容量场景.
于2019年被MongoDB收购的嵌入式数据库Realm,
似乎已经不再提供免费版本.
https://github.com/ideawu/ssdb
与Redis兼容的高性能持久化KVDB数据库, 构建在LevelDB之上.