简介郑州大学计算机与人工智能学院《数据库系统原理》课程的实验报告文档围绕DBMS系统认识与万里数据库GreatSQL的安装、配置、启动和停止展开并覆盖虚拟机CentOS环境准备、SELinux与防火墙关闭、依赖包安装、二进制包部署及服务注册等实验环节。文档以1个docx文件交付压缩包约3.69MB包含实验目的、实验内容、类型与学时分配、主要仪器材料、实验过程、结果分析及报告撰写规范配有操作截图、图题编号和表格格式示例便于对照课程要求完成记录与复盘。已有509人学习适合需要梳理数据库实验流程、补全报告结构或排查GreatSQL环境配置问题的学生参考。其价值在于给出从环境搭建到结果分析的完整实验范式帮助读者理解数据库逻辑组件与物理组件、熟悉GreatSQL管理工具并借鉴问题记录与分析方法提升实验报告质量。1. 先跑起来再看原理数据库原理实验报告的第一课为什么是装库翻任何一份数据库原理实验报告最先出现的往往不是 ER 图、也不是范式分解而是一长串yum install、systemctl start和改配置文件的命令。很多人把这部分当成打杂觉得真正的原理在后面几张表里。恰恰相反装库这一段是整个数据库课程里信息密度最高的部分它在几十分钟内把进程模型、权限边界、磁盘布局、元数据组织这四件事一次性摊在你面前。你之所以觉得它像打杂是因为没人告诉你每一步对应的是 DBMS 的哪一块机制。把这块补上后面写 SQL 时看到的就不再是语法而是一棵 B 树在页上的分布、一次约束检查的时机、一份元数据字典的落盘位置。2. 从 systemctl start 到客户端握手一个数据库服务是怎么活过来的2.1 单元文件、守护进程与 mysql 用户的权限边界安装完二进制包之后做的第一件“有技术含量”的事是把greatsql.service写进/lib/systemd/system/然后systemctl daemon-reload。这条命令做的事不是“刷新缓存”而是让 systemd 重新扫描单元目录、把新文件解析成内存里的 unit 对象。没有它systemctl start greatsql会直接告诉你找不到这个服务。单元文件里真正决定行为的是这几行[Unit] DescriptionGreatSQL Server Afternetwork.target [Service] Usermysql Groupmysql Typenotify ExecStart/usr/local/GreatSQL-8.0.32-25/bin/mysqld --defaults-file/etc/my.cnf LimitNOFILE65535 Restarton-failure [Install] WantedBymulti-user.targetUsermysql意味着 mysqld 不是以 root 身份运行的它一启动就把自己的权限降到一个普通用户上。这就是为什么安装步骤里必须先groupadd mysql、useradd -g mysql mysql -d /dev/null -s /sbin/nologin再把/var/lib/mysql的属主改成 mysql。省掉这一步mysqld 能启动但初始化时会因为无法写数据目录而直接退出——报错信息通常是Cant create/write to file看起来像磁盘问题实质是权限问题。Typenotify是个容易被忽略的细节。它的含义是 mysqld 初始化完成、真正可以接受连接之后会通过sd_notify主动告诉 systemd“我准备好了”。如果是Typesimplesystemctl start在 fork 出进程的那一刻就返回成功此时数据库其实还在做崩溃恢复客户端连上去会收到连接被拒绝。systemctl status里那一行Active: active (running)的可信度完全取决于 Type 选得对不对。LimitNOFILE65535则是文件描述符上限。数据库的每一个连接、每一个打开的表空间文件都占用 fd默认的 1024 在高并发下很快耗尽表现为“连上了但一查就断”。这条配置属于典型的“平时看不见、出事就致命”。2.2 监听端口与连接线程客户端连上来之后服务起来之后验证手段是三件套systemctl status greatsql看进程状态、ps -ef | grep mysqld看进程树、ss -lntp | grep mysqld看监听端口。第三条最关键它证明 mysqld 已经在某个 TCP 端口上 accept 连接了默认 3306。systemctl status greatsql # 单元状态 最近日志 ps -ef | grep mysqld # 确认进程存在看启动参数 ss -lntp | grep mysqld # 确认端口处于 LISTEN 且绑定地址正确ss输出里那一列Local Address:Port要重点看。0.0.0.0:3306表示监听所有网卡127.0.0.1:3306表示只监听本机。这个差异由my.cnf里的bind-address决定。实验环境里经常出现“服务是 active 的、进程也在、就是连不上”八成是bind-address绑在了回环地址上而客户端从宿主机通过虚拟机 IP 访问。连接进来之后发生的事是理解 DBMS 架构的第一个分水岭。MySQL 系产品的模型是“每连接一线程”客户端握手成功服务端分配一个线程专门服务这条连接线程里依次走连接层认证、权限、服务层解析器、优化器、执行器、存储引擎层InnoDB最后落到文件系统。所谓“连接池组件”在这个架构里更多是逻辑上的分层描述真正省资源的高并发方案要靠线程池插件或中间件原生就是线程模型。这也解释了为什么连接数一多SHOW PROCESSLIST会列出一大排Sleep状态的线程——它们占着线程栈和内存等着下一条 SQL。2.3 SELinux 与防火墙两层访问控制为什么常常挡在 mysqld 前面安装步骤里最先执行的几条命令是关 SELinux 和关防火墙setenforce 0 # 临时切到 permissive重启失效 sed -i /^SELINUX/c SELINUXdisabled /etc/selinux/config # 改配置文件永久生效 systemctl stop firewalld systemctl disable firewalld # 停掉并取消开机自启两条命令对应两套完全不同的机制混在一起讲就会变成玄学。SELinux 是强制访问控制MAC它在传统的“属主/属组/其他”这套自主访问控制DAC之外再加一层基于安全上下文的判定。mysqld 进程跑起来带的是mysqld_t这个域数据目录带的是mysqld_db_t这个类型只有策略允许mysqld_t访问mysqld_db_t读写才放行。你把/var/lib/mysql的属主改成 mysql、权限给到 750DAC 这一层已经通过了但 MAC 那层如果策略没覆盖到自定义路径比如把数据目录挪到了/data/mysql照样被拒日志里会出现denied { write }这样的审计记录。setenforce 0把 SELinux 切到宽容模式只记录不拦截等于把这层判定旁路掉。防火墙是另一回事它管的是网络层。3306 端口没在 zone 里放行数据包根本到不了 mysqld 的 socketss看端口是 LISTEN 的客户端却一直超时。生产环境的正确做法是firewall-cmd --add-port3306/tcp --permanent加--reload把端口放开而不是把防火墙关掉。实验环境选择关掉是拿安全性换配置复杂度这个取舍要心里有数一旦这台机器暴露在公网关掉的两层防护就是两个洞。至于那些依赖包也不是随便列的。libaio-devel提供 Linux 原生异步 IOInnoDB 的预读和刷脏要靠它numactl让数据库能在 NUMA 架构上绑定内存节点避免跨节点访问带来的延迟抖动jemalloc替换 glibc 默认的 ptmalloc原因是数据库的内存分配模式是大量小对象、长生命周期、多线程并发ptmalloc 在这种场景下容易产生内存碎片和锁竞争。这些都是“装的时候不知道为什么、跑起来才知道少了不行”的典型。2.4 数据目录与初始化/var/lib/mysql 里躺着什么初始化数据目录的命令是mysqld --initialize-insecure --usermysql --datadir/var/lib/mysql。加-insecure意味着 root 账号生成空密码实验环境够用生产环境绝对不能用。执行完之后ls /var/lib/mysql会看到一堆文件每个都有明确职责文件/目录作用关键点ibdata1系统表空间存数据字典、双写缓冲、回滚段8.0 部分迁出mysql.ibd数据字典表空间表定义、账号、权限都在这undo_001/002回滚表空间MVCC 的旧版本链存在这里#innodb_redo/重做日志8.0.30 之后是目录内含多个固定大小文件binlog.000001二进制日志主从复制和时间点恢复的基础库名/*.ibd独立表空间每张表一个文件前提是innodb_file_per_tableONibdata1和#innodb_redo的关系是理解“为什么数据库能崩溃恢复”的入口修改数据页之前先把“我要改哪个页的哪个偏移、改成什么”写进 redo log日志落盘成功才算事务提交数据页可以慢慢刷。所以数据库的写性能瓶颈往往不在数据文件而在日志的fsync。3. 三级模式两层映像一条 CREATE TABLE 到底改了几层东西3.1 外模式、模式、内模式与两层映像教科书上讲三级模式很容易讲成三个抽象名词。落到实际系统里它们非常具体外模式是视图和权限能看到的列模式是CREATE TABLE定义的列名和类型内模式是这行数据在 16KB 页里怎么排、用哪个索引找。两层映像承担的是解耦职责。外模式/模式映像让视图在基表加列、改列名之后依然有效因为视图存的是查询定义而非结果模式/内模式映像让ALTER TABLE加索引、换行格式、挪表空间不用改任何一条 SQL因为存储布局对上层透明。这两层映像在实现上并不神秘——它们就是数据字典里的两组记录一组描述视图定义一组描述表与索引的物理属性。3.2 基本表、视图、触发器、存储过程谁有实体谁是记录这四类逻辑组件的区别本质是“有没有自己的物理存储”。基本表有实体CREATE TABLE会在数据目录下生成.ibd文件行数据真的落在页里。视图没有实体它只是数据字典里的一条查询定义查询视图时优化器会把视图定义展开、和外部查询合并这叫视图合并merge 算法如果视图里含聚合、UNION、LIMIT则会退化成物化临时表叫 temptable 算法。触发器也没有实体数据但它有可执行体定义存在数据字典里在BEFORE INSERT、AFTER UPDATE这类时机被引擎回调。存储过程同理它是一段预编译的 SQL 集合存在mysql.proc8.0 之后并入数据字典调用时才装载执行。这里有个常见误解以为触发器会自动帮你做很多事。实际上触发器的执行是在同一事务内、由存储引擎在行级操作前后同步调用的里面写的 SQL 一旦复杂就会拖慢每一次写入而且触发器里不能显式提交事务。它是“隐式耦合”的典型用之前要掂量。3.3 information_schema元数据自己也是一张表元数据最容易被当成黑匣子。其实在 MySQL 系里元数据被组织成information_schema这个虚拟库你可以像查普通表一样查它-- 查看当前库里所有基本表及其引擎、行数估算 SELECT TABLE_NAME, ENGINE, TABLE_ROWS, DATA_LENGTH FROM information_schema.TABLES WHERE TABLE_SCHEMA DB; -- 查看触发器TRIGGER_NAME、EVENT_MANIPULATION、ACTION_TIMING、ACTION_STATEMENT SELECT TRIGGER_NAME, EVENT_MANIPULATION, ACTION_TIMING, EVENT_OBJECT_TABLE FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA DB; -- 查看存储过程和函数用 ROUTINE_TYPE 区分 PROCEDURE / FUNCTION SELECT ROUTINE_NAME, ROUTINE_TYPE, CREATED FROM information_schema.ROUTINES WHERE ROUTINE_SCHEMA DB AND ROUTINE_TYPE PROCEDURE; -- 查看约束主键、外键、唯一约束都在 KEY_COLUMN_USAGE 里外键详情还要看 REFERENTIAL_CONSTRAINTS SELECT CONSTRAINT_NAME, TABLE_NAME, COLUMN_NAME, REFERENCED_TABLE_NAME FROM information_schema.KEY_COLUMN_USAGE WHERE TABLE_SCHEMA DB;这几条查询的价值不在于语法而在于它们揭示了一件事DBMS 把自己的“自我描述”也做成了关系表。这叫自描述self-describing是关系数据库能自我管理的根基。TABLE_ROWS那一列要特别注意InnoDB 返回的是采样估算值不是精确行数拿它做业务判断会翻车要精确就老老实实COUNT(*)。顺带纠正一个实验记录里高频出现的错误用SHOW FULL PROCESSLIST去“查看视图”。这条命令列出的是当前连接线程和它们正在执行的语句跟视图没有任何关系。看视图定义要用SHOW CREATE VIEW 视图名或者查information_schema.VIEWS。这类命令与概念的错配恰恰说明元数据这块不能靠猜。3.4 约束与外键参照完整性由谁负责检查约束分两类检查的位置完全不同。NOT NULL、CHECK、主键唯一性这一类是在行写入时由存储引擎或服务层当场判定的外键这种跨表约束则由 InnoDB 在行操作时做额外的加锁查询。外键能建成功需要同时满足几个条件缺一个就会报errno: 150这种毫无信息量的错误两张表必须是同一个存储引擎都是 InnoDB引用列和被引用列的数据类型要相似整型的符号位和字节数要一致字符串的字符集和排序规则必须完全相同被引用列上必须有索引主键或唯一索引都行且不能是前缀索引。-- 先建父表父表的被引用列上必须有索引 CREATE TABLE Departments ( Dno CHAR(4) PRIMARY KEY, Dname CHAR(20), Dheadno CHAR(7) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 再建子表外键列类型、字符集、排序规则必须与父表完全一致 CREATE TABLE Teachers ( Tno CHAR(7) PRIMARY KEY, Dno CHAR(4), CONSTRAINT fk_teacher_dept FOREIGN KEY (Dno) REFERENCES Departments (Dno) ON DELETE SET NULL ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;代码里ON DELETE SET NULL和ON UPDATE CASCADE是参照完整性的两种处理策略前者在父行删除时把子行的外键列置空要求该列可空后者在父键更新时级联改子行。这两种动作不是免费的级联更新会锁住子表相关行父表一次批量更新可能引发大面积锁等待。所以高性能场景常见的做法是把外键约束挪到应用层数据库只负责存——这是一个明确的设计取舍不是“外键没用”。CHECK约束在 MySQL 8.0.16 之后才真正生效之前的版本只解析不执行。这个版本差异在老资料里埋了很多坑。4. 数据页与 B 树为什么加了索引查询就少读几个页4.1 页、行格式与磁盘 IO 的最小单位InnoDB 读写磁盘的最小单位是页默认 16KB由innodb_page_size决定建库后不可改。一个区extent是 64 个页共 1MB。行格式默认DYNAMIC特点是变长字段完全溢出到页外页内只留 20 字节指针老版本COMPACT最多在页内留 768 字节前缀。为什么强调“页是 IO 单位”因为数据库的每一次读本质都是“把一页从磁盘搬进 Buffer Pool”。主键查一行如果这行所在的页不在内存里就要读一次盘读 1 行和读这一页上的 100 行代价完全一样。这解释了两个现象一是全表扫描在小表上并不比索引慢多少因为页数少二是SELECT *比SELECT 需要的列慢因为它可能把整行都拉回来甚至触发更多页的读取。4.2 聚簇索引与二级索引唯一索引和普通索引的差别InnoDB 的表本身就是一棵 B 树叫聚簇索引叶子节点直接存整行数据这棵树的键是主键。如果没有显式主键InnoDB 会选一个非空唯一索引代替都没有就生成一个 6 字节的隐藏ROW_ID。这就是为什么“每张表都该有主键”不是风格建议而是性能建议——没有主键的表所有二级索引都得靠隐藏列回表。二级索引是另一棵 B 树叶子节点存的是“索引列 主键值”。所以通过二级索引查非索引列要先在二级索引里找到主键再拿主键去聚簇索引里取整行这叫回表是两次 B 树查找。能省掉第二次查找的办法是覆盖索引查询涉及的列全在索引里EXPLAIN的Extra会显示Using index。-- 在 Students 的 Dno 上建普通索引 CREATE INDEX idx_student_dno ON Students (Dno); -- 这条查询只用到 Dno可以直接从二级索引返回无需回表 EXPLAIN SELECT Dno FROM Students WHERE Dno CS; -- Extra: Using index -- 这条要取 Sname必须回表 EXPLAIN SELECT Sname FROM Students WHERE Dno CS; -- Extra: NULL或 Using wheretype: ref一个常被忽略的差异是唯一索引和普通索引的写入代价。普通索引的插入可以走 change buffer要插入的索引页不在 Buffer Pool 里时先把这次修改记在 change buffer等页被读进来再合并省掉一次随机读。唯一索引不行因为必须读页确认“没有重复值”这次读躲不掉。所以纯从写性能看能确定业务唯一的场景用唯一索引只在查询需要时用普通索引而不是无脑全上唯一索引。4.3 执行计划一条 SQL 进来之后被改了两次SQL 进入服务层后先被解析器做成语法树然后经历两轮优化。第一轮是逻辑优化做的是等价变换把WHERE 11 AND a1化简成a1把外连接在被引用列非空时转成内连接把IN子查询改写成半连接或物化。第二轮是物理优化基于成本模型在多个执行路径里挑一个走哪个索引、要不要索引下推、 join 顺序怎么排。成本模型的输入是统计信息也就是索引的基数cardinality和页数这些数据由ANALYZE TABLE或自动采样维护。-- 看优化器最终选了哪条路 EXPLAIN FORMATTRADITIONAL SELECT s.Sname, c.Cname, sc.Grade FROM Students s JOIN SC sc ON s.Sno sc.Sno JOIN Courses c ON sc.Cno c.Cno WHERE sc.Grade 60;看EXPLAIN的输出要抓住四列type反映访问方式ref、range是可接受的ALL是全表扫描key是实际用的索引为空说明没走索引rows是估算扫描行数和实际差一个数量级就说明统计信息过期Extra里的Using index condition表示索引下推生效Using filesort表示排序没走索引。很多人调优调不动根子在于拿rows估算值当真实值来分析。4.4 char(9) 与 varchar(9)定长字段对索引的影响实验里学号定义为char(9)课程号char(5)看起来只是风格选择实际影响索引的字节数。在 utf8mb4 下CHAR(9)的索引键按最大字节数计算是 36 字节不管实际存了几个字符VARCHAR(9)在索引里按实际长度加长度前缀算。字段一多索引页能容纳的条目数就差出一截树高可能多一层。更隐蔽的是尾随空格语义。在PAD SPACE排序规则比如utf8mb4_general_ci下abc和abc 被判定相等char列的比较会自动忽略尾部空格而在NO PAD排序规则MySQL 8.0 默认的utf8mb4_0900_ai_ci下它们不相等。跨版本迁移时“明明数据一样却关联不上”或者“唯一索引突然报冲突”往往就是这个差异。5. 避坑排查装完能跑、跑着才出问题的五个坑5.1 服务显示 active客户端却连不上现象systemctl status greatsql显示active (running)ps也能看到 mysqld 进程但客户端连接报Cant connect to MySQL server或直接超时。原因按概率排一是bind-address绑定了127.0.0.1只监听回环二是 firewalld 没放行 3306三是 SELinux 拦了网络绑定日志里有name_bind审计四是客户端连的是宿主机 IP 而虚拟机网络是 NAT压根不通。解决先用ss -lntp | grep mysqld看监听地址确认是不是0.0.0.0再在本机mysql -h127.0.0.1 -uroot -p试一次本机通、外部不通就是网络层或防火墙最后查/var/log/audit/audit.log里有没有denied记录确认不是 SELinux。5.2 DROP TABLE 之后磁盘空间没回来现象删了几张大表df -h显示磁盘占用几乎没变数据目录里.ibd文件也还在。原因有两个层次。第一DROP TABLE删的是表空间文件但如果这个文件还被某个进程持有句柄比如备份进程、或者之前的连接没释放文件系统的 inode 不会回收。第二如果删的是行DELETE FROMInnoDB 只是把行标记删除、页留在表空间里复用空间不会还给操作系统。解决DROP TABLE后用lsof | grep deleted找占用句柄的进程DELETE大批量数据后用ALTER TABLE 表名 ENGINEInnoDB重建表空间回收空洞或者用OPTIMIZE TABLE。5.3 外键建不上只报 errno 150现象ALTER TABLE ... ADD FOREIGN KEY失败错误信息只有一行errno: 150看不出原因。原因外键要求父表被引用列有索引、两列数据类型兼容、字符集与排序规则完全一致、存储引擎都是 InnoDB。任何一条不满足都报同一个错误码。常见触发点是父表用utf8mb4_0900_ai_ci、子表用utf8mb4_general_ci或者一边是INT一边是INT UNSIGNED。解决SHOW CREATE TABLE把两张表都打出来逐字比对字符集和类型SHOW INDEX FROM 父表确认被引用列上有索引。改字符集用ALTER TABLE 子表 CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci。5.4 数字字面量插进 char 列前导零静悄悄没了现象教师号定义成char(7)插入写成INSERT INTO Teachers VALUES (05001, ...)查出来是5001长度对不上后面建外键或者关联查询时匹配失败。原因05001是不带引号的数字字面量MySQL 先把它当整数解析成 5001再隐式转换成字符串5001前导零在这一步就丢了。同理学号201705001插进char(12)会变成 9 个字符。整个过程不报错最多给个 warning。解决字符型字段的常量一律加引号写成05001建表时对齐长度排查时用SELECT LENGTH(Tno), Tno FROM Teachers看实际长度。5.5 日期写成 1999_12_16严格模式下直接失败现象INSERT INTO Students VALUES (..., 1999_12_16, ...)在 MySQL 8.0 默认配置下报Incorrect date value。原因8.0 默认开启STRICT_TRANS_TABLES非法日期直接报错而不是像老版本那样悄悄存成0000-00-00。分隔符必须是-或/下划线不认。解决日期统一写1999-12-16。如果确实需要宽松行为可以临时SET sql_mode 但这等于关掉了数据校验不推荐。顺带一提WHERE year(Birthday) BETWEEN 1997 AND 1999这种写法在列上套了函数索引会失效改成Birthday BETWEEN 1997-01-01 AND 1999-12-31才能走范围扫描。6. 设计取舍二进制包、最小安装与“先造轮子再拆轮子”的顺序实验环境选的是预编译二进制包加最小化安装而不是源码编译这个选择背后有明确权衡。源码编译能定制编译选项、针对特定 CPU 指令集优化、裁掉不用的存储引擎代价是编译时间长、依赖链复杂、出问题时排查面更大。教学场景真正需要的不是极致的性能而是一个能在半小时内跑起来、且行为可预期的数据库实例好把注意力放在机制观察上。如果编译就耗掉一次实验课那观察 SELinux 拦不拦 mysqld、观察 redo log 目录里有多少个文件这些更有价值的环节就没时间做了。同理关 SELinux 和防火墙也是取舍。生产环境的正确姿势是配策略、开端口而不是关服务实验环境关掉它们是为了把“访问被拒绝”这种与数据库原理无关的变量先排除掉等原理清楚之后再回头补安全配置。这个顺序很重要——先理解机制再讨论安全边界反过来容易变成背配置模板。还有一个容易忽视的取舍是最小化安装的镜像选择。glibc2.17这个后缀不是随便写的它决定了这个二进制包能在哪些发行版上直接跑。glibc 版本不匹配的典型症状是启动时报version GLIBC_2.xx not found跟数据库本身一点关系都没有。选包之前先ldd --version确认系统 glibc 版本比事后 debug 划算得多。带过很多次实验之后我形成了一个固定习惯任何一个新环境先跑systemctl status、ss -lntp、SHOW VARIABLES LIKE datadir、SELECT sql_mode这四条把服务状态、监听、数据落地位置、SQL 模式全部确认一遍再开始建库建表。这四条检查花不到两分钟能挡掉后面八成“明明照着步骤做却跑不通”的问题。那份实验报告里九组实验的完整命令、截图和参数记录正好覆盖了从装库到多表查询的这条链路需要对照着复现某个环节的时候可以直接取用。希望帮到你。本文还有配套的精品资源点击获取