
简介实验四配套的完整实验报告与操作讲解文档面向正在学习大数据技术原理、需要对比 MySQL 与 HBase、Redis、MongoDB 四种数据库的本科生或自学者。内容紧密围绕第6章实验目的展开先梳理了关系型数据库与 NoSQL 数据库在数据模型、事务机制、水平扩展方式上的核心差异再完整呈现 MySQL 的建表、插入、查询、更新等 Shell 操作以及 MySQL 的 JDBC 客户端编程示例并扩展到 HBase、Redis、MongoDB 的常用 Shell 命令与 Java API 使用要点。报告附有实际运行截图代码注释清晰每一步都便于对照练习和复核结果。包体为1个 docx 文件大小约1.54MB实验环境说明、操作步骤、完整代码与截图集中在同一文档中打开即可按章节复用。该资源已有3500余人学习下载适合作为课程实验报告模板、数据库操作复习资料或期末实践参考。1. 实验四到底在比什么两类数据库的操作差异不是「谁快」而是「怎么用」很多人看到「实验四NoSQL和关系数据库的操作比较」这个标题第一反应是想知道谁更快。真把这个实验做一遍你反而会发现一个反直觉的结论在小数据量下关系数据库不一定比 NoSQL 慢在中等数据量下NoSQL 的优势也不在「快」而在「改起来省事」和「横向扩展顺滑」。这个实验的真正价值是逼着你把同一份业务数据分别用两种模型写一遍从建表、插入、查询、更新到事务逐个操作去感知差异而不是背一句「NoSQL 天生快」的口号。适合正在做存储选型的开发者和运维也适合想把课本结论亲手验证一遍的学生。2. 搭一套可重复的双库环境从数据建模到准备同一份测试数据2.1 为什么拿「传统关系库」和「文档型 NoSQL」做对比关系数据库的代表是 SQL 二维表 ACID 事务它把一致性放在第一位而 NoSQL 有键值、文档、列族、图四类形态其中文档型与业务模型最贴近操作起来最容易看出和 SQL 的思维差异。我一般会选开源的 PostgreSQL 作为关系库代表选 MongoDB 风格作为文档库代表。前者满足标准 SQL事务能力强后者用 BSON 文档支持嵌套结构和动态字段。对比实验并不需要真的在两套生产环境上跑一个容器环境就够。如果你的团队更熟悉 MySQL结论也基本适用因为这次实验考察的不是产品细节而是「表模型 vs 文档模型」的操作方式差异。2.2 用容器把实验环境装起来Compose 文件与连接检查常见做法是用 Docker Compose 一次性拉起两个服务避免在本机装两套依赖。下面这个 Compose 文件把关系库和文档库都暴露到本机端口数据目录挂到本地方便重置重跑。# docker-compose.yml services: postgres: image: postgres:16 container_name: lab_pg environment: POSTGRES_USER: lab POSTGRES_PASSWORD: lab123 POSTGRES_DB: labdb ports: - 5432:5432 volumes: - ./pgdata:/var/lib/postgresql/data mongodb: image: mongo:7 container_name: lab_mongo environment: MONGO_INITDB_ROOT_USERNAME: lab MONGO_INITDB_ROOT_PASSWORD: lab123 ports: - 27017:27017 volumes: - ./mongodata:/data/db这个配置的关键点有三个端口映射决定了你后续用哪个端口连接数据卷必须挂载否则容器重启后数据就没了认证参数要提前定好后面连接串都要用它。启动后先别急着写代码用docker ps确认两个容器都是 healthy 状态再分别试连一次。关系库这边可以用 psqlPGPASSWORDlab123 psql -h 127.0.0.1 -p 5432 -U lab -d labdb -c select 1;文档库这边可以用 mongoshmongosh mongodb://lab:lab123127.0.0.1:27017/labdb?authSourceadmin --quiet --eval db.runCommand({ping:1})看到两边都返回成功环境就算通了。这里常踩的一个坑是 MongoDB 初始用户指向的是 admin 库而不是业务库连接串里必须带authSourceadmin不然会一直报认证失败。2.3 准备一份逻辑等价的数据客户与订单为了让操作对比有意义数据必须尽量等价。我用一张客户表和一张订单表作为关系库样例订单表外键指向客户表在文档库里拆成 customers 和 orders 两个集合orders 里存 customerId 来对应。这种对齐方式牺牲了一部分文档模型嵌套优势但换来的是两张模型的可比性实验结论不会被「模型设计不同」干扰。-- 关系库初始化 CREATE TABLE customers ( id SERIAL PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE, created_at TIMESTAMP DEFAULT now() ); CREATE TABLE orders ( id SERIAL PRIMARY KEY, customer_id INT NOT NULL REFERENCES customers(id), amount DECIMAL(10,2) NOT NULL, status TEXT NOT NULL DEFAULT pending, created_at TIMESTAMP DEFAULT now() );对应的文档库初始化如下// 文档库初始化 db.customers.insertMany([ { name: 张三, email: zhangsanexample.com, created_at: new Date() }, { name: 李四, email: lisiexample.com, created_at: new Date() } ]); db.orders.insertMany([ { customer_id: 1, amount: 199.00, status: pending, created_at: new Date() }, { customer_id: 2, amount: 59.90, status: paid, created_at: new Date() } ]);注意两个细节关系库的表结构把字段类型定死写错类型直接报错文档库插进customer_id: 1和customer_id: 1都不会报错但这会给后续关联查询埋雷。这正是实验想让你体会的差异之一——约束被移到了应用层灵活性提升了脏数据风险也上来了。3. 同一份业务两种 CRUD 写法的核心差别3.1 插入与更新约束、无模式和 upsert插入操作的观感差异是最直观的。关系库写 INSERT 时必须匹配表结构字段名写错、类型不匹配都会立刻被拒。文档库则像往 JSON 里塞东西多一个字段、少一个字段都行。这种自由在快速迭代时很舒服但实验建议你顺手做一次「类型错误插入」感受一下双方的反应。-- 关系库更新订单状态 UPDATE orders SET status paid WHERE id 1; -- 关系库带子查询的关联更新 UPDATE orders SET status cancelled WHERE customer_id IN (SELECT id FROM customers WHERE name 张三);文档库的更新语法是updateOne和updateMany配合$set修改指定字段和 SQL 的 SET 逻辑类似。它多了一个 SQL 没有的选项叫 upsert查不到就插入查得到就更新这在同步场景里很好用。// 文档库更新一条订单 db.orders.updateOne( { _id: ObjectId(...) }, { $set: { status: paid } } ); // 文档库upsert 写法 db.orders.updateOne( { order_no: NO-1001 }, { $set: { amount: 299.00, status: pending } }, { upsert: true } );参数说明$set只更新指定字段不会覆盖整个文档upsert: true是文档库特有的原子操作避免了「先查再插」两步逻辑。而关系库里实现同样效果需要INSERT ... ON CONFLICT DO UPDATE或手动判断写起来更啰嗦但意图更直白。3.2 查询语法SELECT 与链式 find 的思维差异查询是两类数据库写法差异最大的地方。SQL 是声明式语言你描述「要什么」数据库帮你规划执行路径文档库的查询是链式方法调用更像在代码里逐步组装条件。下面看等值、范围、排序和分页的对照。-- 关系库订单分页查询 SELECT id, customer_id, amount, status FROM orders WHERE amount 50 ORDER BY created_at DESC LIMIT 10 OFFSET 20;// 文档库相同查询 db.orders.find( { amount: { $gte: 50 } }, { _id: 0, customer_id: 1, amount: 1, status: 1 } ).sort({ created_at: -1 }).skip(20).limit(10);两个写法有四个对应关系WHERE对应第一个find参数投影对应第二个find参数ORDER BY对应.sort()LIMIT/OFFSET对应.limit().skip()。但有一个差异值得注意——SQL 的排序字段必须存在于已知列里写错直接报错文档库的.sort()可以排任意字段即使某条文档没这个字段也会被特殊排序。对刚上手的人来说这既是方便也是坑。3.3 删除与清库delete、truncate 和 drop 的区别删除操作的层级差异在双库对比中经常被忽略。关系库有 DELETE删行、TRUNCATE清空表但保留结构、DROP连表一起删文档库对应的是deleteMany删文档、deleteMany({})清空集合、drop()删集合。实验里建议你把三档操作都跑一遍观察性能和语义差异。-- 关系库全表清空保留表结构速度快 TRUNCATE TABLE orders RESTART IDENTITY;// 文档库清空集合等效于 TRUNCATE db.orders.deleteMany({}); // 文档库删除集合本身等效于 DROP TABLE db.orders.drop();容易踩坑的是 RESTART IDENTITY 这串参数。关系库里 TRUNCATE 默认不重置自增 ID业务里如果你清空数据后希望 ID 从 1 重新开始必须加这句。很多人上线前清测试数据没加结果线上第一单 ID 变成了几百对账脚本当场看懵。3.4 事务与原子性ACID 与文档级原子性实验做到事务这里才算触及本质。关系库的事务是全局的多行、多表更新要么全成功要么全失败传统 NoSQL 的单文档更新天然原子但跨文档操作并非默认支持。文档库现代版本支持多文档事务但前提是部署为副本集单节点实例跑事务是空话。-- 关系库事务扣库存 建订单放在一起 BEGIN; INSERT INTO orders (customer_id, amount, status) VALUES (1, 88.00, pending); UPDATE products SET stock stock - 1 WHERE id 100; COMMIT;// 文档库事务示意 const session db.getMongo().startSession(); session.startTransaction(); try { session.getDatabase(labdb).orders.insertOne( { customer_id: 1, amount: 88.00, status: pending }, { session } ); session.getDatabase(labdb).products.updateOne( { _id: 100 }, { $inc: { stock: -1 } }, { session } ); session.commitTransaction(); } catch (e) { session.abortTransaction(); }参数说明SQL 事务靠 BEGIN/COMMIT 包住隔离级别由数据库参数控制文档库事务需要显式开启 session所有操作都带 session 参数提交时用commitTransaction。注意实验里很容易出现「单机 NoSQL 跑事务报错」的情况这不是写法问题而是部署模式问题。这个区别足以解释生产选型里很多争论不是 NoSQL 不支持事务而是它的事务能力和部署形态深度绑定。4. 批量写入与查询性能和索引到底差在哪4.1 批量写入的差距从哪来合并提交与 ordered 参数很多网上对比帖说 NoSQL 写入比关系库快一个数量级但实验里这个结论经常翻车。差距主要来自三个地方网络往返次数、日志刷盘策略、索引维护量。关系库如果你一条条 INSERT每一条都是一次完整的后端往返和 fsyncNoSQL 的insertMany是单次请求发一批数据自然快。把关系库也改成批量提交后差距就小很多。-- 关系库批量插入的正确姿势 INSERT INTO orders (customer_id, amount, status) VALUES (1, 10.00, pending), (2, 20.00, pending), (3, 30.00, pending);// 文档库批量插入 db.orders.insertMany([ { customer_id: 1, amount: 10.00, status: pending }, { customer_id: 2, amount: 20.00, status: pending }, { customer_id: 3, amount: 30.00, status: pending } ], { ordered: false });参数说明ordered: false让 MongoDB 在遇到某条失败时继续处理后续文档而不是整体中止批量导入场景下能明显提升吞吐。关系库没有这个参数它靠的是把多条 INSERT 合并进一条语句。实验时建议用 1 万条数据去压记录三种写法的耗时关系库单条循环、关系库批量、NoSQL insertMany。你会发现真正的瓶颈往往不在数据库内核而在你代码里写了多少次往返。4.2 索引和查询计划谁在偷偷做全表扫描查询性能对比如果不建索引结论是没有意义的。关系库和文档库都支持二级索引和复合索引但查看执行计划的方式不同。关系库用 EXPLAIN ANALYZE文档库用.explain(executionStats)。-- 关系库创建索引并查看执行计划 CREATE INDEX idx_orders_customer_status ON orders(customer_id, status); EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id 1 AND status paid;// 文档库创建复合索引 db.orders.createIndex({ customer_id: 1, status: 1 }); db.orders.find({ customer_id: 1, status: paid }).explain(executionStats);参数说明关系库里复合索引的字段顺序非常讲究(customer_id, status)可以支撑「按客户查订单」和「按客户状态查订单」但撑不起单纯按 status 的查询。文档库的索引方向 1 表示升序-1 表示降序范围查询和排序的场景需要关注方向选择。实验里一个值得记录的数据点是建索引前两个库在小数据量下都是毫秒级看不出差别数据量加到百万行后没索引的查询从几毫秒变成几秒加了索引又回到几十毫秒。这个对比比任何理论都直观。4.3 性能结论怎么读才不冤枉预热与中位数实验性能对比最容易犯的错是拿第一次运行的数据说事。数据库有页缓存第一次查询把数据读进内存第二次直接走缓存两者的耗时能差几十倍。我在实验里一般会让测试脚本先跑两轮预热再正式计时取多轮结果的中位数而不是平均值因为平均值容易被偶发抖动带偏。for i in 1 2 3 4 5; do psql -h 127.0.0.1 -U lab -d labdb -c \timing on -f query_test.sql 21 | grep Time done\timing on是 psql 自带计时开关输出格式是毫秒数五轮跑完自己算中位数。文档库那边可以用 mongosh 里的Date.now()包一层来计时。这样读出来的性能数字才具备可比性。实验结论往往会落在数据量不大时两者没有数量级差距真正拉开差距的是数据模型是否适配你的访问模式而不是数据库引擎本身。5. 双库对比最常见的 5 个坑现象、原因和修复5.1 模型设计层面的两个坑第一个坑出现在文档库的「无模式」上。现象是同一个集合里有的文档字段叫customerId有的叫customer_id有的 amount 存数字有的存字符串。写入时一切正常一跑聚合统计就报类型错误或查不到数据。原因很简单文档库默认不对字段名和类型做强校验灵活性的代价是约束上移。解决方法是应用层加校验逻辑或使用文档库自带的 JSON Schema 校验在集合创建时就把字段类型钉死。第二个坑是文档模型嵌套过度。现象是订单里的商品明细被嵌在数组里刚开始查询特别方便一条文档就是完整业务上下文。后来要按月份统计商品销量脚本越写越重甚至要把整个数组拉回应用层循环累计。原因是你把反范式用在了不适合多维统计的业务上。解决方法是保持核心集合平铺或者提前建一个汇总集合用定时任务维护统计值而不是事后从嵌套里扒数据。5.2 执行层面的两个坑第三个坑是批量参数没调好性能结论直接被带偏。现象是同一个人测试NoSQL 写入比关系库快 10 倍换个人复现却快不到 2 倍。原因多半是关系库这边逐条 INSERTNoSQL 那边用了insertMany两边根本没站在同一起跑线。解决方法是关系库也合并成批量提交并确认 NoSQL 的 writeConcern 设置一致。默认的 writeConcern 是「写入主节点即返回」如果调成复制到多数节点才返回耗时立刻上涨这不是引擎变慢是持久性换取的结果。第四个坑出现在分页上。现象是用limit skip翻页前几十页很快翻到几千页后一次查询慢得离谱。原因是 skip 的实现是丢掉前面所有文档再返回越往后扫描的成本越高关系库和文档库都有类似问题。解决方法是改用「基于条件的翻页」记住上一页最后一条记录的 ID 或时间下一页用WHERE id 上页最后id LIMIT 20或{ _id: { $gt: lastId } }。这个优化在大数据量场景下效果是数量级的。5.3 实验方法层面的坑第五个坑是没预热就下结论。现象是同一个查询第一次要 800ms第二次变成 15ms于是误以为数据库出了问题。原因是操作系统的页缓存把数据文件读进内存了后续查询直接命中缓存。解决方法是记录预热前后的差异正式对比前先跑两轮同样查询等到耗时稳定再取样。这类坑最容易让人产生「性能是玄学」的错觉其实是把冷热缓存混在了一个报告里。6. 把操作比较做成选择依据三轮热压验证法与其对着跑分的数字纠结不如把实验扩展成一个可复现的验证流程。我现在的习惯是跑「三轮热压」第一轮做数据预热让缓存填充第二轮正式记录各项操作的耗时第三轮用更高的并发量大压一把取第二轮和第三轮的中位数作为参考。这个流程不用任何监控平台脚本就能完成但足以回答「我的业务到底更适合哪边」这个问题。验证时要关注的指标不是 QPS 峰值而是三件事写入操作的 p99 延迟、查询在真实数据集上的耗时稳定性、以及跨文档事务是否能满足你的业务语义。把这些记在下表里选型说服力远大于一句「我们用了 XXX」。业务场景建议选择理由强事务、多表关联统计关系库ACID 与 SQL 聚合能力成熟高频单文档写入、字段多变文档型 NoSQL无模式 单文档原子性大规模分片扩展NoSQL原生分片机制更顺滑复杂图关系查询图数据库关系库表达多跳关系很吃力我以前带过一个模块A 同学图省事把一个订单状态机埋进文档库的嵌套数组里前两个月确实痛快。后来要做对账汇总他对着聚合管道写了整个下午最后还是回炉成平铺的集合加一张汇总表。那次之后我定了一个习惯动工前把数据模型画出来把核心查询列出来在两种模型里各写一遍伪代码谁写起来自然就用谁。NoSQL 也好关系库也罢操作比较的目的不是为了证明谁更好而是让你在动手前就看清各自要付的成本。这个实验做完你得到的不是一张跑分表而一份属于自己的选型判断力。希望帮到你。本文还有配套的精品资源点击获取