☰
openGauss数据库实验九讲:从gsql连接到JDBC可复现路径
2026/11/4 3:07:28 网站建设 项目流程

简介:这份郑州大学《数据库系统原理实验》报告书面向计算机科学与技术、软件工程等专业学生,围绕数据库课程实验提供从理论到实践的完整记录,可用于实验预习、过程对照与报告撰写参考。压缩包内仅含1个docx文档,大小约3.23MB,正文按实验一至实验九组织,覆盖DBMS认识、数据库表与索引创建、交互式SQL、视图、完整性控制、安全性控制、事务与并发控制、备份恢复及JDBC连接数据库等模块。文档不仅列出实验目的、内容与学时安排,还记录了OpenGauss、gsql等工具的操作过程、问题处理与结果分析,便于读者理解关键步骤和排错思路。目前已有1527人学习,适合需要完成数据库实验、梳理SQL操作与复盘课程重点的学习者参考。

1. 从一份实验报告书说起:九次实验的骨架与可复现路径

数据库实验最容易翻车的地方,从来不是 SQL 写不出来,而是换了台机器照着报告敲,第一步就卡在 gsql 连不上、omm 用户切不过去、表空间目录权限不对。这份《数据库系统原理实验》报告书的价值不在于能交作业,而在于它把九个实验串成了一条能从头跑到尾的主线:认识 DBMS 与 gsql 连接、建库建表建索引、交互式 SQL、视图、完整性控制、安全性控制、事务与并发、备份恢复,最后用 JDBC 把程序接进来。它适合三类人:要按 openGauss 完成课程实验并留下可复现记录的学生,手里只有 MySQL 经验想摸清 openGauss 差异的开发者,以及需要一套现成教学用 DDL 脚本当脚手架的人。下面按这条主线拆,重点放在每一步的顺序、参数和会卡住人的地方。

2. openGauss 环境落地:三种装法、omm 用户与 gsql 元命令速查

环境这一章看着最没技术含量,实际上九个实验里有七个的失败都追溯到这里。装法选错、用户切错、端口记错,后面所有 SQL 都白写。

2.1 三种安装形态的取舍边界

报告书里列了三种路子:在云主机镜像上直接部署、用虚拟机一键安装包、先装 openEuler 再手动装 openGauss。这三种不是难度递进关系,而是适用场景完全不同,选错了会在无关的地方耗掉半天。

安装方式大致耗时前置准备适合的场景常见绊脚点
云主机镜像部署十几分钟一台可远程登录的主机只想尽快跑通 SQL,不折腾系统端口没放行,本地连不上 26000
虚拟机一键安装半小时到一小时虚拟机软件、安装镜像要反复重装、要截图留档内存给太小,实例起不来
先装系统再装库一到两小时系统镜像、安装包、依赖想顺带练一遍部署流程依赖缺失、目录属主不对

我的建议很直接:如果目标是完成这九个实验,选虚拟机一键安装。原因不在于它简单,而在于它把安装用户、数据目录、环境变量一次性配好了,你不需要在GAUSSHOME、LD_LIBRARY_PATH、PGDATA这几个变量上纠缠。等你把九个实验都跑完,再回头做一次手动安装,那时候你才知道每个变量是干什么的,顺序反过来只会让人放弃。

云主机那条路要额外注意一点:数据库端口默认是 26000,不是 5432。很多人按 PostgreSQL 的习惯去连 5432,然后怀疑服务没起来。实际上gs_om -t status显示 Normal,只是你连错了门。

2.2 su - omm 与 gs_om -t start 的启动顺序

数据库装完之后,操作系统上会多出一个专用的安装用户,报告书里写的是 omm。这一步的顺序是:先切用户,再看状态,再启动,最后连接。顺序错了会得到一堆"命令不存在"。

# 切到数据库安装用户,减号不能省,它决定是否加载完整登录环境 su - omm # 先看实例状态,确认是 Normal 还是 Down,别上来就 start gs_om -t status --detail # 启动数据库服务,stop 停库,restart 重启 gs_om -t start # 启动后确认监听端口,26000 是默认主节点端口 ss -lntp | grep 26000

su - omm里的那个减号是整个流程里最容易被忽略的字符。带减号表示以登录方式切换,会读取 omm 用户的 shell 配置文件,把GAUSSHOME、LD_LIBRARY_PATH、PATH全部加载进来;不带减号只是切换身份,环境变量还是原来那个用户的,于是gs_om、gsql全都提示 command not found。我见过不止一次有人在这上面怀疑安装失败,重装了三次。

gs_om -t status --detail的--detail会多打印每个实例的数据目录和端口,排错时比只看一个 Normal 有用得多。gs_om -t start如果卡在启动中,先去看数据目录下的日志,常见原因是磁盘满或者上次非正常关机留下的残留进程,gs_om -t stop之后再 start 一次通常能解决。

2.3 gsql 连接串参数逐项拆解

gsql 是 openGauss 自带的命令行客户端,报告书里那条连接命令是gsql -d postgres -p 26000 -r。参数不多,但每个都有实际作用,乱填会连到别的库或者根本连不上。

# 连接默认库 postgres,端口 26000,开启行编辑 gsql -d postgres -p 26000 -r # 指定用户和主机连接(跨机或非 omm 用户时用) gsql -d db_school -p 26000 -h 127.0.0.1 -U u_student -W # 连接成功后先确认自己在哪个库、哪个用户下 SELECT current_database(), current_user;
参数含义使用建议
-d目标数据库名首次连 postgres,后续换成自己建的库
-p端口号默认 26000,不是 5432
-r开启 readline 行编辑强烈建议加,否则上下键翻历史不能用
-h主机地址本机可省,跨机必须写
-U数据库用户名不写则用当前操作系统用户
-W交互式输入密码别在命令行直接跟明文密码

-W后面不要直接跟密码。命令行里的明文密码会进 shell 历史,也会出现在ps的进程列表里,这是最常见的安全习惯问题。正确做法是让 gsql 交互式提示输入,或者在用户家目录下配置密码文件由客户端自动读取。

另一个细节:-d postgres里的 postgres 是安装完成后默认生成的数据库,它同时也是初始管理员的用户名,两个东西同名,初学时很容易混淆。连上去之后用\c db_school切换到自己建的库,而不是重新敲一遍 gsql,这样会话里的变量和设置能保留。

2.4 元命令与系统表:\l、\dt、\d+ 与 pg_database 的对照

报告书里花了不少篇幅讲反斜杠开头的元命令,这是 gsql 的交互式快捷方式,本质上是帮你查系统表。把它们和等价的 SQL 对上号,排错时才不会只会敲元命令。

-- 元命令 \l 的等价查询:列出所有数据库 SELECT datname FROM pg_database; -- 元命令 \dt 的等价查询:列出当前 schema 下的表 SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'public'; -- 元命令 \di 的等价查询:列出索引及其定义 SELECT indexname, indexdef FROM pg_indexes WHERE schemaname = 'public'; -- 元命令 \dn 的等价查询:列出所有 schema SELECT nspname FROM pg_namespace WHERE nspname NOT LIKE 'pg_%'; -- 元命令 \db 的等价查询:列出表空间 SELECT spcname FROM pg_tablespace;

这几条 SQL 的意义在于,元命令只能在交互式会话里用,而 SQL 可以写进脚本、可以配-c参数一次性执行、可以在 JDBC 里跑。写自动化检查脚本时,用系统表查询比调元命令靠谱。

\d+ 表名是使用频率最高的一个,它输出列名、类型、是否可空、默认值、存储方式、分布列等一整套信息,比information_schema.columns直观。看索引用\di,看某张表的完整定义用\d+ 表名,两个配合基本能覆盖建表阶段的所有核对需求。

还有两个概念要在这里分清:schema 和用户不是一回事。schema 是一个命名空间,用户是登录身份,一个用户可以拥有多个 schema,一个 schema 也可以被多个用户以不同权限访问。报告书里那句"一个数据库用户所拥有的数据库对象构成 schema"说得通俗,但在 openGauss 里 schema 并不从属于用户,它是独立对象,只是创建时默认归属创建者。

3. 建库建表建索引:表空间、schema 与六张教学表的 DDL 顺序

这一章是整份报告书里最能直接抄作业的部分,也是顺序最容易搞错的部分。表空间、数据库、schema、表、索引,这五层是有严格先后依赖的。

3.1 表空间与数据库:先有目录还是先有库

创建表空间的语句长这样,但它的前提条件是目录已经存在并且属主正确。这一点报告书里只写了"后边是空目录"一句带过,实际执行时这是第一个会报错的地方。

-- 表空间指向的目录必须事先存在,且属主为数据库安装用户 CREATE TABLESPACE tpcds_local RELATIVE LOCATION 'tablespace/tablespace_1'; -- 建库时挂到刚建的表空间上,不指定则用默认表空间 CREATE DATABASE db_sc WITH TABLESPACE = tpcds_local; -- 库改名,报告书里从 db_sc 改成 db_school 的那一步 ALTER DATABASE db_sc RENAME TO db_school;

RELATIVE LOCATION后面的路径是相对于实例数据目录的,不是相对于当前工作目录。这几个字看漏了,你就会在$GAUSSHOME下面到处找那个 tablespace 文件夹。手懒的话可以直接用LOCATION写绝对路径,语义更直白,出错时也更容易定位。

目录没建就执行 CREATE TABLESPACE,报错信息通常指向路径不存在或无权限;目录建了但属主是 root,报错会变成权限不足。两种情况都要在 omm 用户下先mkdir -p再chmod,或者干脆用 omm 身份建目录。这个坑在实验二里出现频率极高,因为大部分人是在 root 下顺手把目录建了。

ALTER DATABASE ... RENAME TO要注意一点:改名时不能有其他会话连着这个库。如果有连接残留,会提示数据库正在被访问。做法是先SELECT datname, count(*) FROM pg_stat_activity GROUP BY 1看一眼,把空闲连接清掉再改。

3.2 schema、用户、角色三者的权限边界

报告书在实验一里用了一段仓库管理员做比喻解释用户和角色,这个比喻是对的,但落到 SQL 上还要补一层 schema 的关系。

-- 建 schema,归属当前用户 CREATE SCHEMA book_eg; -- 在指定 schema 下建表,表名必须带 schema 前缀或先 SET search_path CREATE TABLE book_eg.teachers_demo ( tno CHAR(20) PRIMARY KEY, tname CHAR(20) NOT NULL ); -- 建用户(会自动创建一个同名 schema) CREATE USER u_student PASSWORD 'Abc@123456'; -- 建角色,角色是权限集合,不用于登录 CREATE ROLE r_readonly; -- 把角色授予用户,用户即拥有该角色的全部权限 GRANT r_readonly TO u_student; -- 级联删除 schema 及其下所有对象,慎用 DROP SCHEMA book_eg CASCADE;

CREATE USER和CREATE ROLE的区别在于前者默认带 LOGIN 属性,后者默认不带。openGauss 里创建用户时会顺带建一个同名 schema,这个行为很多人不知道,结果发现自己建完用户,\dn里多出来一个 schema,还以为是谁动过库。

密码是另一个高频报错点。openGauss 对密码有复杂度要求:长度、大小写、数字、特殊字符至少满足若干项,且不能与用户名相同。报错信息里会明确写出不满足哪一条,照着改就行,别反复试同一个弱密码。

DROP SCHEMA ... CASCADE这条要单独说。CASCADE 会把它下面所有表、视图、函数一起删掉,不弹任何确认。报告书里用它演示级联删除没问题,但在真实库里执行前,先用\dt book_eg.*确认一下里面到底有什么,这个动作花不了十秒。

3.3 六张教学表的建表语句与约束落位

教师、院系、学生、课程、选课、教学六张表之间存在循环引用:教师表里的院系号指向院系表,院系表里的系主任工号又指回教师表。直接按业务含义顺序建,无论先建哪张都会因外键找不到被引用列而失败。

步骤操作原因
1建 Teachers,暂不加 Dno 外键打破循环依赖
2建 Departments,带 Dheadno 外键此时 Teachers 已存在
3ALTER Teachers 补 Dno 外键回填引用关系
4建 Students、Courses单向依赖 Departments
5建 SC、Teaches依赖前面所有表
-- 第一步:教师表先建出来,Dno 只留列不加外键 CREATE TABLE Teachers ( Tno CHAR(20) PRIMARY KEY, Tname CHAR(20) NOT NULL, Sex CHAR(20) CHECK (Sex IN ('男', '女')), Birthday DATE, Title CHAR(20), Dno CHAR(20) ); -- 第二步:院系表带外键引用教师表的主键 CREATE TABLE Departments ( Dno CHAR(20) PRIMARY KEY, Dname CHAR(20), Dheadno CHAR(20), CONSTRAINT fk_dept_head FOREIGN KEY (Dheadno) REFERENCES Teachers(Tno) ); -- 第三步:回填教师表的外键,打破循环 ALTER TABLE Teachers ADD CONSTRAINT fk_teacher_dept FOREIGN KEY (Dno) REFERENCES Departments(Dno); -- 第四步:学生与课程表 CREATE TABLE Students ( Sno CHAR(20) PRIMARY KEY, Sname CHAR(20) NOT NULL, Sex CHAR(20) CHECK (Sex IN ('男', '女')), Birthday DATE, Enrollyear CHAR(20), Speciality CHAR(20), Dno CHAR(20), CONSTRAINT fk_stu_dept FOREIGN KEY (Dno) REFERENCES Departments(Dno) ); CREATE TABLE Courses ( Cno CHAR(20) PRIMARY KEY, Cname CHAR(20) NOT NULL, Period SMALLINT, Credit SMALLINT ); -- 第五步:选课表与教学表,组合主键更贴合业务 CREATE TABLE SC ( Sno CHAR(20), Cno CHAR(20), Grade SMALLINT CHECK (Grade BETWEEN 0 AND 100), PRIMARY KEY (Sno, Cno), FOREIGN KEY (Sno) REFERENCES Students(Sno), FOREIGN KEY (Cno) REFERENCES Courses(Cno) ); CREATE TABLE Teaches ( Tno CHAR(20), Cno CHAR(20), Term CHAR(20), PRIMARY KEY (Tno, Cno, Term), FOREIGN KEY (Tno) REFERENCES Teachers(Tno), FOREIGN KEY (Cno) REFERENCES Courses(Cno) );

这段脚本的核心是外键的依赖顺序,不是语法本身。第二步和第三步的拆分就是处理循环引用的标准手法:先建一张不带闭环外键的表,把闭环的另一半建出来,再 ALTER 回去补上。任何多表互相引用的场景都适用这个套路,不限于教学表。

CHECK (Sex IN ('男', '女'))比报告书里的CHECK (Sex='男' OR Sex='女')更短,语义等价。带空值的列要注意三值逻辑:CHECK 约束在结果为 NULL 时视为通过,所以如果 Sex 允许为空,插 NULL 不会被拦。要真正强制非空,得额外加 NOT NULL。

SC 表的主键我用的是(Sno, Cno)组合主键,而不是加一个自增 id。这是教学场景下的合理选择:一个学生一门课只选一次,组合主键天然去重。真实业务里要考虑重修、多次选课,那就得引入学期字段,把主键扩成三列。选哪种取决于你的业务假设,写报告时把假设写清楚比写对更重要。

列级约束和表级约束的区别,报告书里用了一大段解释,落到实操上只有一条判断标准:这个约束是否涉及多个列。涉及多列(比如组合主键、复合外键、跨列 CHECK)必须写表级;只涉及单列,写列级更紧凑。性能上没有区别,纯粹是可读性问题。

3.4 索引创建、自动索引与 \di 验证

建索引这一步,很多人建完就完事,从不验证到底生效没有。验证方法很简单,一条 SQL 加一个元命令。

-- 在学生表的院系号上建普通索引 CREATE INDEX student_dept ON Students(Dno); -- 查看当前 schema 下所有索引 -- \di -- 用系统表确认索引是否真的建上了 SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'students'; -- 看某条查询是否走索引,重点看 Seq Scan 还是 Index Scan EXPLAIN SELECT * FROM Students WHERE Dno = 'D01';

主键和唯一约束会自动创建索引,所以\di里看到的索引数量一定比手建的多,这是正常现象。判断某个索引有没有必要存在,看两点:这一列是否经常出现在 WHERE 或 JOIN 条件里,以及这一列的取值重复度高不高。在性别这种只有两个值的列上建索引,优化器大概率直接忽略,走了还不如不走。

EXPLAIN输出的Seq Scan表示全表扫描,Index Scan表示走索引。教学表数据量只有几十行,优化器很可能认为全表扫描更快,这是正常的,不代表索引失效。想验证索引效果,塞几千行数据进去再看执行计划,报告里如果写了"索引提升了查询效率",最好附上这个对比。

删索引的语句是DROP INDEX 索引名,注意不需要带表名。同一张表上建太多索引会拖慢插入和更新,因为每次写操作都要维护索引结构。教学场景无所谓,真实场景下每加一个索引都要能说出它服务于哪条查询。

4. 交互式 SQL 与视图:从多表连接到一个可更新视图的判断

SQL 这一章覆盖实验三和实验四,表面上是最熟悉的内容,但 openGauss 在类型转换和视图可更新性上的行为和 MySQL 有差异,照搬经验会踩坑。

4.1 单表查询到三表连接的写法收敛

查询需求一旦从单表跨到多表,写法就有多种。以"查某位学生选修的课程名和成绩"为例,内连接是最直观的一种。

-- 三表内连接:学生 → 选课 → 课程 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 s.Sno = 'S001' ORDER BY sc.Grade DESC; -- 等价写法:用 EXISTS 表达"存在选课记录" SELECT c.Cname FROM Courses c WHERE EXISTS ( SELECT 1 FROM SC sc WHERE sc.Cno = c.Cno AND sc.Sno = 'S001' );

两种写法的结果集不完全一样:第一种会把学生姓名和成绩一起带出来,一行对应一条选课记录;第二种只输出课程名,去掉了重复。选择哪种取决于你要的列,不是哪种"更高级"。

内连接会丢掉没有选课的学生。如果需求是"列出所有学生及其选课数,没选课的显示 0",就得换成 LEFT JOIN 配合 COALESCE。

-- 左连接保留没有选课记录的学生,NULL 转 0 SELECT s.Sno, s.Sname, COALESCE(COUNT(sc.Cno), 0) AS course_cnt FROM Students s LEFT JOIN SC sc ON s.Sno = sc.Sno GROUP BY s.Sno, s.Sname ORDER BY course_cnt DESC;

GROUP BY后面必须列出所有非聚合的输出列,这是标准 SQL 的要求,openGauss 严格执行。MySQL 在宽松模式下允许省略,从 MySQL 迁过来的写法会直接报错。COALESCE用来把 NULL 转成 0,不用它的话,没选课的学生那一行会显示 NULL 而不是 0,报告里的统计表就对不上。

4.2 聚合与分组:GROUP BY 与 HAVING 的边界

WHERE 和 HAVING 的分工是个老问题,判断标准只有一条:过滤发生在分组前还是分组后。分组前能确定的用 WHERE,依赖聚合结果的用 HAVING。

-- 统计每门课的选课人数与平均分,只保留人数超过 2 的课 SELECT c.Cno, c.Cname, COUNT(sc.Sno) AS stu_cnt, ROUND(AVG(sc.Grade), 2) AS avg_grade FROM Courses c JOIN SC sc ON c.Cno = sc.Cno WHERE sc.Grade IS NOT NULL -- 分组前过滤:去掉没有成绩的记录 GROUP BY c.Cno, c.Cname HAVING COUNT(sc.Sno) > 2 -- 分组后过滤:按人数筛 ORDER BY avg_grade DESC;

WHERE sc.Grade IS NOT NULL在分组前就干掉了空成绩,避免AVG把 NULL 算进去影响分母。HAVING COUNT(sc.Sno) > 2依赖聚合结果,只能放在 HAVING。把 HAVING 的条件挪到 WHERE 会直接报错,因为 WHERE 执行时 COUNT 还没有值。

ROUND(AVG(...), 2)里的第二个参数是保留位数。openGauss 的 AVG 返回的是数值类型,不加 ROUND 会输出一长串小数,放进报告表格里很难看。这类细节不影响正确性,但影响可读性。

还有一个容易被忽略的点:COUNT(*) 和 COUNT(列名) 结果可能不同。前者数行数,后者跳过 NULL。统计选课人数时如果用 COUNT(sc.Grade),没成绩的记录不会被计入,得到的数字比实际选课人数少。写统计 SQL 时想清楚要数的是"记录数"还是"非空值数"。

4.3 视图创建、查询改写与可更新性判断

视图在报告书里是实验四的全部内容。创建视图本身没有难度,难的是判断一个视图能不能 UPDATE。判断标准不是靠猜,是有明确规则的。

-- 建一个学生成绩视图,隐藏出生日期等无关列 CREATE VIEW v_student_score AS SELECT s.Sno, 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; -- 查看视图定义,确认列来源 -- \d+ v_student_score -- 尝试更新视图,多表连接的视图通常会失败 UPDATE v_student_score SET Grade = 90 WHERE Sno = 'S001';

上面这条 UPDATE 大概率报错,原因是视图涉及三张表连接,数据库无法确定要改哪张基表。可更新视图的成立条件大致是:只来自一张表、不含聚合函数、不含 DISTINCT、不含 GROUP BY 或 HAVING、不含 UNION、不含子查询里的聚合。满足这些条件的视图叫简单视图,可以当成表来增删改;否则就是只读视图。

需要可更新时,把视图收敛到单表:

-- 单表视图,只暴露必要列,具备可更新性 CREATE VIEW v_student_basic AS SELECT Sno, Sname, Sex, Dno FROM Students WHERE Dno IS NOT NULL; -- 这条可以正常执行,会作用到 Students 基表 UPDATE v_student_basic SET Sname = '张三' WHERE Sno = 'S001';

带WHERE条件的单表视图依然可更新,只是更新后的行如果不再满足视图条件,会从视图里"消失"。这不算 bug,是视图的语义决定的。报告里如果要做这个实验,建议把这个现象记录下来,比单纯贴一句"更新成功"有价值得多。

4.4 用视图做列级抽象与权限收口

视图的第二个用途是权限控制。数据库的 GRANT 是表级和列级的,但列级授权写起来繁琐,用视图包一层更清爽。

-- 面向查询角色的视图,屏蔽出生日期和入学年份 CREATE VIEW v_student_public AS SELECT Sno, Sname, Speciality, Dno FROM Students; -- 只授予视图的查询权限,不授予基表权限 GRANT SELECT ON v_student_public TO r_readonly; -- 验证:以只读角色连接后,只能看到视图里的列 SELECT * FROM v_student_public LIMIT 5;

这个做法的关键在于:被授权者拿不到基表的 SELECT 权限,只能通过视图访问,于是列级隔离就实现了。要注意视图的属主必须有基表权限,否则视图建出来查不了。视图默认以属主权限执行(类似 SECURITY DEFINER 的效果),这一点和某些数据库的 definer/invoker 选项不同,跨用户使用前先确认属性。

视图还有一层价值是隔离表结构变更。基表加列、拆表的时候,只要视图的输出列保持不变,上层查询就不用改。教学场景体会不到这个好处,但这是视图在工程里最常被使用的理由。

5. 完整性、安全性、事务与备份:四类控制实验的排查清单

实验五到实验八这四类控制,单独看每条语句都很短,组合起来才见真章。这一章按"怎么做 + 为什么"推进,最后把五条实际踩过的坑整理成现象、原因、解决三段式。

5.1 约束追加:ALTER TABLE 与外键的先后顺序

建表时没加约束,后期补加是常态。补加约束的语句不复杂,复杂的是表里已经有数据的时候。

-- 给已有表补加外键,前提是被引用列必须是主键或唯一键 ALTER TABLE Students ADD CONSTRAINT fk_stu_dept FOREIGN KEY (Dno) REFERENCES Departments(Dno); -- 补加 CHECK 约束,注意已有数据必须全部满足,否则整条语句失败 ALTER TABLE SC ADD CONSTRAINT ck_grade CHECK (Grade BETWEEN 0 AND 100); -- 删除约束 ALTER TABLE SC DROP CONSTRAINT ck_grade; -- 外键的删除行为可以单独指定 ALTER TABLE SC DROP CONSTRAINT sc_sno_fkey, ADD CONSTRAINT sc_sno_fkey FOREIGN KEY (Sno) REFERENCES Students(Sno) ON DELETE CASCADE;

补加外键时最常见的报错是被引用列没有唯一约束。外键的语义是"我的值必须出现在对方的唯一记录里",对方列如果不唯一,这个语义就成立不了。报错信息里会明确写出找不到匹配的唯一约束,看到这句就去检查对方表的主键定义。

CHECK 约束补加失败,八成是表里已有脏数据。比如成绩列里存了一个 120,加BETWEEN 0 AND 100的约束就会整条语句回滚。解决办法是先查出来改掉:SELECT * FROM SC WHERE Grade NOT BETWEEN 0 AND 100;,改完再加约束。这个顺序不能反,ALTER TABLE 是原子的,一条不满足就全部不生效。

ON DELETE CASCADE是把双刃剑。删学生时自动删掉他的选课记录,业务上合理;但如果 SC 表还被别的表引用,级联会一路传下去。教学库里无所谓,生产库里加 CASCADE 之前,先用SELECT把会被删掉的行数数出来。

5.2 授权与角色:GRANT 的作用域与回收

安全性控制实验的核心是 GRANT 和 REVOKE。语法简单,作用域容易搞混。

-- 建只读角色与读写角色 CREATE ROLE r_readonly; CREATE ROLE r_readwrite; -- 表级授权:查询与插入 GRANT SELECT ON Students TO r_readonly; GRANT SELECT, INSERT, UPDATE ON SC TO r_readwrite; -- 把角色授予具体用户,用户继承角色的全部权限 GRANT r_readonly TO u_student; -- 回收权限,回收角色用 REVOKE 角色名 REVOKE INSERT ON SC FROM r_readwrite; -- 查看某个用户最终拥有的权限 SELECT grantee, table_name, privilege_type FROM information_schema.table_privileges WHERE grantee = 'u_student';

GRANT 权限 ON 表 TO 角色和GRANT 角色 TO 用户是两条完全不同的语句,前者授对象权限,后者授角色成员关系。初学者经常把两者混着写,然后困惑为什么权限没生效。记法很简单:ON 后面跟表就是对象权限,TO 后面跟用户就是角色分配。

权限回收之后,已存在的连接不会立刻生效,需要重新连接或者执行SET ROLE刷新。这个行为和很多数据库一致,但在实验里容易被当成"REVOKE 没起作用"。验证时最好退出重连一次再查。

还有一个细节:information_schema.table_privileges只显示直接授予的权限,通过角色继承来的权限要查role_table_grants。查权限时两个视图对照着看,才不会漏。

5.3 事务与并发:两条会话验证隔离与锁等待

事务和并发控制这一节,光看语句看不出效果,必须开两个会话对着做。这是整份报告书里唯一必须双窗口操作的实验。

-- 会话 A:开事务,改一行但不提交 BEGIN; UPDATE SC SET Grade = 95 WHERE Sno = 'S001' AND Cno = 'C001'; -- 此处先不 COMMIT,保持事务打开 -- 会话 B:改同一行,观察是否阻塞 BEGIN; UPDATE SC SET Grade = 60 WHERE Sno = 'S001' AND Cno = 'C001'; -- 此处会一直等待,直到会话 A 提交或回滚 -- 会话 A:提交 COMMIT; -- 会话 B 随即返回,改为 60 生效 COMMIT;

默认隔离级别是 READ COMMITTED,会话 B 会一直等会话 A 释放行锁。如果不想无限等,可以在会话 B 里先设一个超时:

-- 超过 5 秒拿不到锁就报错返回,避免会话挂死 SET lock_timeout = '5s';

排查锁等待要看两个视图。pg_stat_activity看有哪些会话在跑、状态是什么,pg_locks看锁的持有和等待关系。state列显示active且wait_event不为空,基本就是卡在等锁。找到阻塞的会话后,可以用pg_terminate_backend(pid)干掉,但这是最后手段,先确认对方是不是有未提交的事务。

死锁在实验里不容易复现,因为需要两个会话以相反顺序锁两行。构造方法:A 锁第一行,B 锁第二行,然后 A 去锁第二行,B 去锁第一行,数据库检测到环路会主动回滚其中一个,报错信息里会写明死锁。这个实验做一次,比看十页教材印象深。

5.4 备份与恢复:gs_dump 的逻辑备份与恢复顺序

备份恢复实验用的是 gs_dump,这是逻辑备份工具,导出的是 SQL 文本或自定义格式,不是物理文件拷贝。

# 切到安装用户执行,输出目录必须 omm 可写 su - omm # 导出整个库为纯 SQL 文本 gs_dump -p 26000 -d db_school -f /home/omm/backup/db_school.sql -F p # 导出为自定义格式,支持并行恢复和选择性还原 gs_dump -p 26000 -d db_school -f /home/omm/backup/db_school.dmp -F c # 恢复:纯 SQL 用 gsql 直接灌 gsql -p 26000 -d db_school_new -f /home/omm/backup/db_school.sql # 恢复:自定义格式用 gs_restore gs_restore -p 26000 -d db_school_new /home/omm/backup/db_school.dmp

-F p是 plain,输出纯文本 SQL;-F c是 custom,输出压缩的自定义格式。前者能直接用编辑器打开看内容,适合教学和排查;后者支持-t只还原某张表,适合真实场景。选哪种取决于你要不要挑着恢复。

恢复的目标库必须事先存在,gs_dump 不会帮你建库。这是新手最容易卡住的一步:拿着备份文件执行恢复,报错说数据库不存在,然后怀疑备份坏了。正确顺序是先CREATE DATABASE db_school_new;,再执行恢复。

输出目录的属主问题在实验八里几乎必现。用 root 建了/home/omm/backup,切到 omm 后写不进去,报权限拒绝。解决就是chown omm:omm /home/omm/backup,一行命令的事,但不知道的话会绕很久。

5.5 五条踩坑记录:现象、原因、解决

下面五条是这类实验里复现率最高的失败,按现象、原因、解决写清楚,遇到时对号入座即可。

第一条:建外键报"there is no unique constraint matching given keys"。现象是 ALTER TABLE 加外键时直接失败,提示找不到匹配的唯一约束。原因是被引用的列上既没有主键也没有唯一索引,外键的语义要求对方唯一。解决办法是给被引用列补上 PRIMARY KEY 或 UNIQUE 约束,或者改引用对方已有主键的另一列。

第二条:ALTER TABLE 加 CHECK 约束报数据违规。现象是语句失败,提示已有行不满足新约束。原因是表里存在历史脏数据,比如成绩超范围、性别写成"未知"。解决办法是先查出违规行,清洗或修正后再执行 ALTER,顺序不能反。

第三条:新建用户连接报认证失败。现象是 gsql 用新用户连库,提示 password authentication failed。原因通常是两处:密码不满足复杂度要求(长度、字符类型、不能与用户名相同),或者连接来源没有被放行。解决办法是重建用户并用合规密码,同时确认服务端的访问控制配置允许该来源连接。

第四条:两个会话卡住不动,界面无响应。现象是会话 B 的 UPDATE 一直不返回,也不报错。原因是会话 A 持有行锁且未提交事务。解决办法是在会话 B 设lock_timeout,再去pg_locks里找到阻塞的 pid,让会话 A 提交或回滚。

第五条:gs_dump 备份文件为空或权限拒绝。现象是导出命令执行完,目标文件是 0 字节或者干脆报错。原因是输出目录属主不是 omm,或者磁盘写满。解决办法是先chown再重跑,跑完用ls -lh确认文件大小,别假设成功。

6. JDBC 打通最后一公里:驱动、连接串与一次可复现的验证

实验九是把前面八个实验的成果接进程序,很多人到这一步才发现前面建的库连不上,问题往往出在驱动和连接串上。openGauss 的 JDBC 驱动类名是org.opengauss.Driver,不是 PostgreSQL 那个org.postgresql.Driver,用错类名会直接抛 ClassNotFound。驱动包从 openGauss 的官方发布渠道下载,加进工程依赖即可。

// 连接串关键参数:协议头、主机、端口、库名 String url = "jdbc:opengauss://127.0.0.1:26000/db_school"; String user = "u_student"; String pass = "Abc@123456"; try (Connection conn = DriverManager.getConnection(url, user, pass); PreparedStatement ps = conn.prepareStatement( "SELECT Sname, Grade FROM SC JOIN Students USING (Sno) WHERE Cno = ?")) { ps.setString(1, "C001"); // 参数化,别拼字符串 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { System.out.println(rs.getString(1) + " -> " + rs.getInt(2)); } } }
连接串参数作用常见误设
jdbc:opengauss://协议前缀写成jdbc:postgresql://导致驱动不认
端口26000服务端口沿用 5432 连不上
?currentSchema=指定默认 schema不设则用 search_path,找不到表
?connectTimeout=连接超时毫秒数不设则跨机时长时间挂起

PreparedStatement加?占位符这两件事一起说:它不只是防注入,还能让数据库复用执行计划。字符串拼接的 SQL 每次文本都不同,数据库要重新解析,参数化之后同样的语句结构可以命中缓存。

我最早做这个实验时,把连接串写成jdbc:postgresql://,本地跑通了,因为工程里恰好还留着 PostgreSQL 驱动,结果换到只装了 openGauss 驱动的机器上就崩了,排查了半天才发现是协议头的问题。从那以后我每次搭 JDBC 环境,都强制先单独跑一个只做SELECT 1的最小验证,确认驱动加载、连接串、账号密码三件事全通,再往里加业务 SQL。希望帮到你。

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

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

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

立即咨询