文档数据库比 RDBMS 快吗?实操体验告诉你

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
云原生多模数据库 Lindorm,多引擎 多规格 0-4节点
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: MongoDB基础原理介绍

关于作者:

image.png

John Page 是一名文档数据库资深人士,在为情报界构建全栈文档数据库技术 18 年后加入了 MongoDB。


我效力于全球领先的文档数据库公司 MongoDB

Email Header_3.png

文档数据库和关系数据库有很多相似之处,如强类型数据、ACID 事务、富查询、更新和聚合功能,以及索引和 B 树等。文档模型数据库与关系数据库之间的真正不同之处体现在于,它能够在存储和数据建模层一些表格是“碎片”内容嵌入其他表中。这就有点像 RDBMS 中按照索引组织的表格,但对于特定的工作负载,它提供了更大的优化范围。此外,它还能够非常轻松从 Java、C# 和其他现代语言保存对象。


我认为 MongoDB 非常适合电子商务或物流等高吞吐量联机事务处理 (OLTP) 工作负载,既可在其中读写数据,也可将其看作信息源。我还认为(下面将详细说明),对于给定读取/写入/更新工作负载,不论是在单位成本执行的事务数量,还是在每开发小时所开发的功能数量,MongoDB 的效率远远超过 RDBMS。


我没把握的是 MongoDB 的效率比 RDBMS 具体高多少(平心而论,我也没有可复验的证据来验证我的观点)。因此,我花了些时间测量相关值。本文简短介绍了我执行的测试及结果,以及可从何处取得代码来亲自测试。在比较时,我尽力避免参照另一个数据库对一个数据库进行专家式过度调优,因为大部分数据库性能测试都是如此操作。我非常熟悉 MongoDB,但是我会尽量克制自己过度“卖弄”。另一方面,我的确尽我所能地兼顾 PostgreSQL 和 MySQL。我比较熟悉 MySQL。我还使用了这三款数据库的托管版本,因为我希望能够使用由运行这些服务的专家所安装和配置的版本。我的目标是能够在公平的环境中执行公正的测试。当然还得请大家根据我在下方记录的全部数据、测试和调优选项,判断我是否做到客观公平。

我评定了哪些工作负载

我选择模拟英国政府的汽车检测系统。公众可以查询汽车最近的检验以及汽车修理厂,在车辆年检通过或未通过时输入数据。我选择这个的原因是英国政府发布了实际数据、数据的关系架构和查询,以及数据格式指南。


代码测试的内容包括:检索汽车的最新测试详情、添加新汽车、未能通过汽车测试,以及修改现有结果以更正行驶里程。不同测试所用的比率也不同。我加载了 2021 年的全部数据,刚刚超过 4000 万个测试结果。

数据库和客户端托管选择

我的测试全部都在主要云提供商环境中完成。我使用的是与数据库位于同一个区域的托管 Unix 服务器,为各案例执行测试应用程序。我使用 MongoDB Atlas 来执行 MongoDB 副本集,也使用了该云提供商的托管 MySQL 和 PostgreSQL 产品组合。我尽量保留相同的实例规格,为保证透明度,我选择同时显示规格和每小时定价。

代码

我用的代码是多线程 Java。我将 MongoDB Java 驱动程序用于 Atlas,将 JDBC 用于 MySQL 和 Postgres。值得注意的是,MongoDB 只需要 20% 的代码行就能够读取和写入数据库,因为不需要在一个事务中执行多次插入就能够保留和检索对象,也不需要从多行重建对象。

可检索对象并将其从 MongoDB 转换为 JSON 的代码如下。

public String getMOTResultInJSON(String identifier) {
      long identifierLong;
      try {
          identifierLong = Long.valueOf(identifier);
          Bson byIdQuery = Filters.eq("vehicleid", identifierLong);
          testObj = testresults.find(byIdQuery).limit(1).first();
          if (testObj != null) {
              return testObj.toJson();
          }
      } catch (Exception e) {
          logger.error(e.getLocalizedMessage());
      }
      return "{ }"; // Not found
  }

相对于适用于 RDBMS 的代码,适合只有一个嵌套层的对象。

//Query From https://data.dft.gov.uk/anonymised-mot-test/MOT_user_guide_v4.docx
private final String getlatestByVehicleSQL = "select " +
"tr.*, " +
"ft.FUEL_TYPE, " +
"tt.TESTTYPE AS TYPENAME, " +
"to2.RESULT, " +
"ti.*, " +
"fl.*, " +           "tid.MINORITEM,tid.RFRDESC,tid.RFRLOCMARKER,tid.RFRINSPMANDESC,tid.RFRADVISORYTEXT,tid.TSTITMSETSECID, " +
"b.ITEMNAME AS LEVEL1, " +
"c.ITEMNAME AS LEVEL2, " +
"d.ITEMNAME AS LEVEL3, " +
"e.ITEMNAME AS LEVEL4, " +
"f.ITEMNAME AS LEVEL5  " +
"from TESTRESULT tr " +
"LEFT JOIN TESTITEM ti  on ti.TESTID = tr.TESTID " +
"LEFT JOIN FUEL_TYPES ft on ft.TYPECODE = tr.FUELTYPE " +
"LEFT JOIN TEST_TYPES tt on tt.TYPECODE  = tr.TESTTYPE " +
 "LEFT JOIN TEST_OUTCOME to2 on to2.RESULTCODE  = tr.TESTRESULT " +
"LEFT JOIN FAILURE_LOCATION fl on ti.LOCATIONID = fl.FAILURELOCATIONID " +
"LEFT JOIN TESTITEM_DETAIL AS tid ON ti.RFRID = tid.RFRID AND tid.TESTCLASSID = tr.TESTCLASSID " +
"LEFT JOIN TESTITEM_GROUP AS b ON tid.TSTITMID = b.TSTITMID AND tid.TESTCLASSID = b.TESTCLASSID " +
"LEFT JOIN TESTITEM_GROUP AS c ON b.PARENTID = c.TSTITMID AND b.TESTCLASSID = c.TESTCLASSID " +
"LEFT JOIN TESTITEM_GROUP AS d ON c.PARENTID = d.TSTITMID AND c.TESTCLASSID = d.TESTCLASSID " +
"LEFT JOIN TESTITEM_GROUP AS e ON d.PARENTID = e.TSTITMID AND d.TESTCLASSID = e.TESTCLASSID " +
"LEFT JOIN TESTITEM_GROUP AS f ON e.PARENTID = f.TSTITMID AND e.TESTCLASSID = f.TESTCLASSID " +
"WHERE  tr.TESTID = (SELECT TESTID FROM TESTRESULT WHERE VEHICLEID=? LIMIT 1)";
public String getMOTResultInJSON(String identifier) {
   long identifierLong;
   jsonObj = new JSONObject();
   // Check we aren't a new thread - if we are we need a new conneciton.
   try {
       identifierLong = Long.valueOf(identifier);
       //Pick a prepared statement from out list of readers randomly
       PreparedStatement getTestStmt = readConnections.get(ThreadLocalRandom.current().nextInt(0, readConnections.size()));
       getTestStmt.setLong(1, identifierLong);
       ResultSet testResult = getTestStmt.executeQuery();
       ResultSetMetaData metaData = testResult.getMetaData();
   // Create JSON from a set of Rows
       String[] topFieldNames = { "TESTID", "VEHICLEID", "TESTTYPE", "TESTRESULT", "TESTDATE", "TESTCLASSID", "TYPENAME","TESTMILEAGE", "POSTCODEREGION", "MAKE", "MODEL", "COLOUR", "FUELTYPE", "FUEL_TYPE", "CYLCPCTY","FIRSTUSEDATE","RESULT" };
   String[] itemFieldNames = { "RFRID", "RFRTYPE", "DMARK", "LOCATIONID", "LAT", "LONGITUDINAL", "VERTICAL","MINORITEM", "RFRDESC","RFRLOCMARKER",               "RFRINSPMANDESC", "RFRADVISORYTEXT", "LEVEL1", "LEVEL2", "LEVEL3", "LEVEL4", "LEVEL5" };
       boolean firstRow = true;
       JSONArray itemsJSON = new JSONArray();
       while (testResult.next()) {
;
           JSONObject itemJSON = new JSONObject();
           for (int col = 1; col <= metaData.getColumnCount(); col++) {
               String label = metaData.getColumnLabel(col);
               if (firstRow && Arrays.asList(topFieldNames)
                                     .contains(label.toUpperCase())) {
                   Object val = testResult.getObject(col);
                   jsonObj.put(label.toLowerCase(), val);
               }
               // All Rows add to the Items array - this is a simple JSON structure
               // Wiith just one top level array of objects
               if (Arrays.asList(itemFieldNames).contains(label.toUpperCase())) {
                   Object val = testResult.getObject(col);
                   itemJSON.put(label.toLowerCase(), val);
               }
           }
           /* If our item isnt blank add it to the items JSONArray */
           if (itemJSON.optInt("rfrid", -1) != -1) {
               itemsJSON.put(itemJSON);
           }
           firstRow = false;
       }
       jsonObj.put("testitems", itemsJSON);
       testResult.close();
       return jsonObj.toString();
   } catch (Exception e) {
       e.printStackTrace();
       logger.error(e.toString());
 }
   return jsonObj.toString();
}

可以访问我的 GitHub 存储库中的代码。该存储库提供下载及清理数据所需的全部说明。 (它里面有几处错误,例如奇数重复键。它还缺失显式 NULL 值,因此 PostgreSQL 所报告的 CSV 中缺失值是字符串。)将数据加载到 PostgreSQL 或 MySQL 中,再将其转换为对象,最后再从 PostgreSQL 或 MySQL 加载到 MongoDB 中。由于测试用具拥有可从 RDBMS 创建对象的代码,因此,将数据加载到 MongoDB 的最简单方式是从 MySQL 将其读取为对象,再将这些对象写入 Atlas。

结果

执行第一个测试时,针对 Atlas 和云提供商使用了推荐的最小“生产”设置。生产指的是包括适用于灾难恢复 (DR) 和读取扩展之副本的最低等级,依赖的是专用计算,而不是可突发计算,以确保性能可预测。MongoDB Atlas 拥有 3 个数据库实例并搭载 2 个 vCPU 核心和 8GB RAM,而 MySQL 和 Postgres 则拥有 2 个实例并搭载 2 个 vCPU 核心和 16GB RAM。数据是一样的。

对于读取/插入/更新工作负载,要求执行之线程的比率为 85:15:5;而只读工作负载的比率为 100:0:0。总体来看,在小型服务器上,我使用了 100 个线程;而通过大型数据库服务器完成测试时使用了 300 个。


我测试了两种情况,即,只从primary/写入程序实例(单一服务器)读取但由其他实例提供 DR/HA 容错功能;以及,将读取分发到secondary/读取程序实例(包括读取副本)。在小型数据库设置中,MongoDB 拥有两个次要节点(允许的最小值),而 MySQL 和 Postgres 只拥有一个节点(允许的最小值)。MongoDB Atlas 三节点配置的每小时总价仍然稍低于云提供商的双节点解决方案。

混合读取/写入/更新工作负载 - 单一服务器(小型)

** IOPS 定价的重要提示:对于 RDBMS,磁盘读取和写入收费为每 100 万次 I/O 操作 0.22 美元。这几个测试下来,可以得出使用计算实例的成本每小时可增加 50% 到 250% 不等。MongoDB Atlas 定价包括所有磁盘成本。

只读工作负载 - 单一服务器(小型)

混合读取/写入/更新工作负载 - 包括读取副本(小型)

只读工作负载 - 包括读取副本(小型)

执行第二个测试时,针对 Atlas 和云提供商使用了较大“生产”设置。该设置包括适用于 DR 和读取扩展的 3 个副本,以及足够 RAM 来确保 RDBMS 能够让工作集保留在缓存中。RDBMS 和 MongoDB Atlas 都拥有 3 个数据库实例,以及 4 个 CPU 和 32GB RAM。数据与上次测试一样。比率相同的情况下,线程的总数量从 100 个增加到 300 个。

混合读取/写入/更新工作负载 - 单一服务器(大型)

只读工作负载 - 单一服务器(大型)

混合读取/写入/更新工作负载 - 包括副本(大型)

只读工作负载 - 包括副本(大型)

注意事项与结论

结果的差异大到令人咋舌。看起来,MySQL 处理大量并行线程的能力尤其低下。背后原因很可能是服务器调整问题。添加写入和更新后,与只读相比,MySQL 和 MongoDB 的每线程读取量都相对下降。


在读取方面,MongoDB Atlas 比 PostgreSQL 快 50–100%,比 MySQL 快得多。这个结果证实了我的预判。此外,每笔事务也实惠许多,要知道全部 I/O 成本都已纳入 Atlas 中,而云提供商 RDBMS 则产生了额外、非常大额 IOPS 成本。


MongoDB 会压缩磁盘上的数据,但是在缓存中不会经过压缩。也就是说,与 RDBMS 不同,Atlas 永远无法保留 RAM 格式的 45 GB 数据,且其速度与 I/O 息息相关。我没有使用 $lookup(相当于 MongoDB 的 LEFT JOIN),因此总体并不复杂。文档包含来自小型域表格的 200+ 个字符串说明,主要介绍了失败项。在这种情况下,最好是 $lookup 查询中的这些说明,而不是将其存储在数据中。此时,一股脑嵌入所有内容没用,了解如何充分利用文档模型才能带来丰厚回报。我可以扩展测试来演示这个效果,因为让数据缓存应当能够进一步优化速度。


PostgreSQL 和 MongoDB 在新数据插入速率上的表现相当,MongoDB 快 5–10%,但有一个例外,因此很可能需要重新测试或进一步调查。在 MongoDB 中更新操作比 PostgreSQL 慢二到四倍,但仍比 MySQL 快。这个结果在意料之内,因为我们在大得多的文档中更改单一值,因此在写出更改方面,MongoDB 的 I/O 比 PostgreSQL 多。前面提到使用 $lookup 的建议能够将文档大小从平均 1260 字节减少到平均 895 字节,显著提高读取和写入的速度。

仍有疑问?试试复刻我的代码后再执行几个测试。真实的 OLTP 会输入的内容包括多张表格、复杂的更新和并行,而不是对随机数据的键/值检索,后者的推出往往是为了比较数据库模型。立即注册 阿里云版MongoDB进行免费试用。


立即加入阿里云MongoDB开发者训练营,带你5天快速入门全球最受欢迎的 NoSQL数据库!或扫码加入钉群,与MongoDB专家一对一沟通,了解更多阿里云MongoDB产品与方案,市场活动及线上培训等内容。

image.png

相关实践学习
MongoDB数据库入门
MongoDB数据库入门实验。
快速掌握 MongoDB 数据库
本课程主要讲解MongoDB数据库的基本知识,包括MongoDB数据库的安装、配置、服务的启动、数据的CRUD操作函数使用、MongoDB索引的使用(唯一索引、地理索引、过期索引、全文索引等)、MapReduce操作实现、用户管理、Java对MongoDB的操作支持(基于2.x驱动与3.x驱动的完全讲解)。 通过学习此课程,读者将具备MongoDB数据库的开发能力,并且能够使用MongoDB进行项目开发。 &nbsp; 相关的阿里云产品:云数据库 MongoDB版 云数据库MongoDB版支持ReplicaSet和Sharding两种部署架构,具备安全审计,时间点备份等多项企业能力。在互联网、物联网、游戏、金融等领域被广泛采用。 云数据库MongoDB版(ApsaraDB for MongoDB)完全兼容MongoDB协议,基于飞天分布式系统和高可靠存储引擎,提供多节点高可用架构、弹性扩容、容灾、备份回滚、性能优化等解决方案。 产品详情: https://www.aliyun.com/product/mongodb
相关文章
|
8天前
|
弹性计算 关系型数据库 数据库
自建数据库迁移到云数据库实操
本课程详细介绍了自建数据库迁移到阿里云RDS的实操步骤。主要内容包括:创建实例资源、安全设置、配置自建的MySQL数据库、数据库的迁移、从自建数据库切换到RDS以及清理资源。通过这些步骤,学员可以掌握如何将自建数据库安全、高效地迁移到云端,并确保应用的正常运行。
68 26
|
6月前
|
运维 Oracle 关系型数据库
screw生成数据库文档
screw生成数据库文档
96 12
|
2月前
|
存储 SQL JSON
介绍一下RDBMS和NoSQL数据库之间的区别
【10月更文挑战第21天】介绍一下RDBMS和NoSQL数据库之间的区别
139 2
|
5月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue+uniapp的《数据库原理及应用》课程平台的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue+uniapp的《数据库原理及应用》课程平台的详细设计和实现(源码+lw+部署文档+讲解等)
|
4月前
|
SQL Oracle 关系型数据库
.NET 开源快捷的数据库文档查询和生成工具
【8月更文挑战第1天】推荐几款.NET开源数据库文档工具:1. DBDocumentor,支持多类型数据库,快速生成详尽文档;2. SqlDoc,界面简洁,自定义内容与格式;3. DBInfo,强大查询功能,支持多种导出格式。这些工具有效提升文档管理效率与质量。
|
6月前
|
SQL XML Java
后端数据库开发JDBC编程Mybatis之用基于XML文件的方式映射SQL语句实操
后端数据库开发JDBC编程Mybatis之用基于XML文件的方式映射SQL语句实操
79 3
|
6月前
|
关系型数据库 MySQL 数据库
MySQL数据库开发之多表查询数据准备及案例实操
MySQL数据库开发之多表查询数据准备及案例实操
52 1
|
6月前
|
JavaScript Java 测试技术
基于SpringBoot+Vue的数据库课程在线教学的详细设计和实现(源码+lw+部署文档+讲解等)
基于SpringBoot+Vue的数据库课程在线教学的详细设计和实现(源码+lw+部署文档+讲解等)
64 2
|
6月前
|
存储 JSON NoSQL
【文档数据库】ES和MongoDB的对比
【文档数据库】ES和MongoDB的对比
419 1
|
7月前
|
JavaScript Java 关系型数据库
废物回收机构|基于SprinBoot+vue的地方废物回收机构管理系统(源码+数据库+文档)
废物回收机构|基于SprinBoot+vue的地方废物回收机构管理系统(源码+数据库+文档)
117 18