☰
关系数据库与NoSQL操作实战:CRUD、事务与性能对比解析
2026/10/9 9:44:15 网站建设 项目流程

简介:实验四配套的完整实验报告与操作讲解文档,面向正在学习大数据技术原理、需要对比 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 状态,再分别试连一次。关系库这边可以用 psql:

PGPASSWORD=lab123 psql -h 127.0.0.1 -p 5432 -U lab -d labdb -c "select 1;"

文档库这边可以用 mongosh:

mongosh "mongodb://lab:lab123@127.0.0.1:27017/labdb?authSource=admin" --quiet --eval "db.runCommand({ping:1})"

看到两边都返回成功,环境就算通了。这里常踩的一个坑是 MongoDB 初始用户指向的是 admin 库而不是业务库,连接串里必须带authSource=admin,不然会一直报认证失败。

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: 'zhangsan@example.com', created_at: new Date() }, { name: '李四', email: 'lisi@example.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,每一条都是一次完整的后端往返和 fsync;NoSQL 的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 2>&1 | grep Time done

\timing on是 psql 自带计时开关,输出格式是毫秒数,五轮跑完自己算中位数。文档库那边可以用 mongosh 里的Date.now()包一层来计时。这样读出来的性能数字才具备可比性。实验结论往往会落在:数据量不大时两者没有数量级差距,真正拉开差距的是数据模型是否适配你的访问模式,而不是数据库引擎本身。

5. 双库对比最常见的 5 个坑:现象、原因和修复

5.1 模型设计层面的两个坑

第一个坑出现在文档库的「无模式」上。现象是同一个集合里,有的文档字段叫customerId,有的叫customer_id,有的 amount 存数字,有的存字符串。写入时一切正常,一跑聚合统计就报类型错误或查不到数据。原因很简单:文档库默认不对字段名和类型做强校验,灵活性的代价是约束上移。解决方法是应用层加校验逻辑,或使用文档库自带的 JSON Schema 校验,在集合创建时就把字段类型钉死。

第二个坑是文档模型嵌套过度。现象是订单里的商品明细被嵌在数组里,刚开始查询特别方便,一条文档就是完整业务上下文。后来要按月份统计商品销量,脚本越写越重,甚至要把整个数组拉回应用层循环累计。原因是你把反范式用在了不适合多维统计的业务上。解决方法是保持核心集合平铺,或者提前建一个汇总集合,用定时任务维护统计值,而不是事后从嵌套里扒数据。

5.2 执行层面的两个坑

第三个坑是批量参数没调好,性能结论直接被带偏。现象是同一个人测试,NoSQL 写入比关系库快 10 倍,换个人复现却快不到 2 倍。原因多半是关系库这边逐条 INSERT,NoSQL 那边用了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 也好,关系库也罢,操作比较的目的不是为了证明谁更好,而是让你在动手前就看清各自要付的成本。这个实验做完,你得到的不是一张跑分表,而一份属于自己的选型判断力。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询