数据库设计与开发 - 实验3 数据库物理设计

Position Zero. This is 天堂真矢。頂きにキラめく星は、一つ。

实验任务

  • 创建数据分区表
  • 体会主键、外键约束
    • 练习更新、删除主表数据(针对主键属性且子表中可能有参照外键数据)
    • 练习更新、删除主表数据(针对非主键属性)
    • 练习先删除子表数据,再删除主表数据
    • 使用子查询方式更新、删除数据
      • 以上均自拟题目
  • 体会索引
    • 使用 EXPLAIN 查询计划
  • 权限管理
    • 以不同身份用户登录数据库建立表
      • 表名一样
    • 以不同身份用户查询自己与其他用户建立的表
    • 定义授权方案并进行验证

实验过程

创建数据分区表

分区表是指将一个大表按照某种规则分解成多个更小的、更容易管理的部分。每个部分称为一个分区,从逻辑上看是一个完整的表,但物理上这些数据分布在不同的表空间中。

分区表的优势包括:

  • 提升查询性能:通过分区消除,可以只扫描必要的分区,减少数据扫描量
  • 更易维护:可以独立管理各个分区,便于数据的备份和恢复
  • 改善可用性:分区故障只影响单个分区,不影响整个表的使用
  • 均衡I/O:数据分散存储,可以降低I/O争用

常见分区方式有:

  1. 范围分区:根据分区键的值范围将数据分配到不同分区
  2. 列表分区:根据分区键的离散值列表进行分区
  3. 哈希分区:使用哈希函数将数据均匀分布到各个分区
  4. 复合分区:组合使用多种分区方式

本次实验中, 我们将按和之前一样的格式创建 xs 表, 但按学生的入学年份进行范围分区。例如学生的学号为 20230101,则其入学年份为 2023

字段名字段含义字段类型字段长度NULL备注
xm姓名字符8
xh学号字符10PK
ydh所属学院代号字符2FK
bj班级字符8
chrq出生日期日期
xb性别字符2

为防止与之前的学生管理系统冲突, 我们将创建一个新的 Schema partition_test,并在其中创建分区表。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
CREATE SCHEMA IF NOT EXISTS partition_test;
CREATE TABLE IF NOT EXISTS partition_test.xs (
  xm VARCHAR(8) NOT NULL,
  xh VARCHAR(10) NOT NULL PRIMARY KEY,
  ydh CHAR(2) NOT NULL,
  bj VARCHAR(8) NULL,
  chrq DATE NULL,
  xb CHAR(2) NULL
) PARTITION BY RANGE (xh) (
  PARTITION p_old VALUES LESS THAN ('20200000'),
  PARTITION p_2020 VALUES LESS THAN ('20210000'),
  PARTITION p_2021 VALUES LESS THAN ('20220000'),
  PARTITION p_2022 VALUES LESS THAN ('20230000'),
  PARTITION p_2023 VALUES LESS THAN ('20240000'),
  PARTITION p_2024 VALUES LESS THAN ('20250000')
);
在Cloudbeaver中连接数据库并创建Schema与分区表
在Cloudbeaver中连接数据库并创建Schema与分区表

创建成功之后, 向 xs 表中插入数据。

1
2
3
4
5
INSERT INTO partition_test.xs (xm, xh, ydh, bj, chrq, xb) VALUES
('张三', '20230101', '01', '软件工程', '2003-01-01', '男'),
('李四', '20220102', '02', '计算机科学', '2002-02-02', '女'),
('王五', '20210103', '03', '网络工程', '2001-03-03', '男'),
('赵六', '20200104', '04', '信息管理', '2000-04-04', '女');

在 Data Studio 左侧的树形结构中, 可以看到 partition_test Schema 下的 xs 表已经有了六个分区。查看分区中的数据, 可以看到数据的储存符合我们的设计预期, 入学年份为 2023 年的学生 张三 被储存在了 p_2023 分区中.

在Data Studio中查看分区表
在Data Studio中查看分区表

体会主键、外键约束

在每次练习之前, 都先将数据库中的所有数据恢复到实验二中插入数据之后的状态, 以免影响实验结果.

练习更新、删除主表数据(针对主键属性且子表中可能有参照外键数据)

更新数据

将学生表中学号为 20230108 的学生的学号改为 20230111,并更新所有表中与这个学生相关的记录.

如果我们直接使用下面的 SQL 语句进行更新的话, 会导致外键约束错误, 因为在成绩表中有一条记录通过外键约束引用了学生表中的学号 20230108.

1
UPDATE xs SET xh='20230111' WHERE xh='20230108';
更新主键属性时的外键约束错误
更新主键属性时的外键约束错误

在这种情况下, 如果要修改主表的主键属性, 我们需要先删除子表中引用了这个主键的记录, 然后再进行更新操作. 在更新之后, 我们需要重新插入之前删除的记录. 这种操作显然是比较繁琐, 且容易出错的, 因为我们需要手动删除和插入记录; 一旦我们在删除记录后忘记将记录添加回数据表, 就会导致数据的丢失.

为了避免这种情况, 我们可以使用 ON UPDATE CASCADE 选项来定义外键约束, 这样在更新主表的主键属性时, 子表中引用了这个主键的记录会自动更新.

用下面的 SQL 语句, 为数据表创建所需的外键约束.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
ALTER TABLE xs DROP CONSTRAINT IF EXISTS xs_ydh_fkey;
ALTER TABLE xs ADD CONSTRAINT xs_ydh_fkey
  FOREIGN KEY (ydh) REFERENCES xyb(ydh)
  ON UPDATE CASCADE ON DELETE CASCADE;

ALTER TABLE js DROP CONSTRAINT IF EXISTS js_ydh_fkey;
ALTER TABLE js ADD CONSTRAINT js_ydh_fkey
  FOREIGN KEY (ydh) REFERENCES xyb(ydh)
  ON UPDATE CASCADE ON DELETE CASCADE;

ALTER TABLE xk DROP CONSTRAINT IF EXISTS xk_xh_fkey;
ALTER TABLE xk ADD CONSTRAINT xk_xh_fkey
  FOREIGN KEY (xh) REFERENCES xs(xh)
  ON UPDATE CASCADE ON DELETE CASCADE;

ALTER TABLE xk DROP CONSTRAINT IF EXISTS xk_kcbh_fkey_1;
ALTER TABLE xk ADD CONSTRAINT xk_kcbh_fkey_1
  FOREIGN KEY (kcbh,jsbh) REFERENCES sk(kcbh,bh)
  ON UPDATE CASCADE ON DELETE CASCADE;

成功创建外键约束后, 我们可以直接使用下面的 SQL 语句进行更新操作.

1
UPDATE xs SET xh='20230111' WHERE xh='20230108';

此时, 子表中引用了这个主键的记录会自动更新, 不需要手动删除和插入记录.

更新主键属性时的外键约束成功
更新主键属性时的外键约束成功

删除数据

现在将学生表中学号为 20230111 的学生删除. 由于我们已经为外键约束定义了 ON DELETE CASCADE 选项, 所以在删除主表的主键属性时, 子表中引用了这个主键的记录会自动删除.

所以我们可以直接使用下面的 SQL 语句进行删除操作.

1
DELETE FROM xs WHERE xh='20230111';
删除带有外键约束的主键属性成功
删除带有外键约束的主键属性成功

我们可以通过下面的 SQL 语句确定学号为 20230111 的学生以及其选课记录已经被删除.

1
2
SELECT * FROM xs WHERE xh='20230111';
SELECT * FROM xk WHERE xh='20230111';

可以看见, 查询的结果为空, 说明学号为 20230111 的学生已经被删除, 其选课记录也因为外键约束而被删除.

外键层联删除成功
外键层联删除成功

练习更新、删除主表数据(针对非主键属性)

更新数据

重置数据表后, 将学生表中学号为 20230108 的学生的姓名改为 长崎素世.

1
2
UPDATE xs SET xm='长崎素世' WHERE xh='20230108';
SELECT * FROM xs WHERE xh='20230108';

可以看见, 在更新数据之后, 查询的结果显示, 学号为 20230108 的学生的姓名已经被修改为 长崎素世.

更新非主键属性成功
更新非主键属性成功

删除数据

要删除学号为 20230108 的学生, 由于我们没有为外键约束定义 ON DELETE CASCADE 选项, 所以仍需要先按上面的方法创建外键约束, 这样在删除主表的记录时, 子表中的相关记录会自动删除.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
ALTER TABLE xs DROP CONSTRAINT IF EXISTS xs_ydh_fkey;
ALTER TABLE xs ADD CONSTRAINT xs_ydh_fkey
  FOREIGN KEY (ydh) REFERENCES xyb(ydh)
  ON UPDATE CASCADE ON DELETE CASCADE;

ALTER TABLE js DROP CONSTRAINT IF EXISTS js_ydh_fkey;
ALTER TABLE js ADD CONSTRAINT js_ydh_fkey
  FOREIGN KEY (ydh) REFERENCES xyb(ydh)
  ON UPDATE CASCADE ON DELETE CASCADE;

ALTER TABLE xk DROP CONSTRAINT IF EXISTS xk_xh_fkey;
ALTER TABLE xk ADD CONSTRAINT xk_xh_fkey
  FOREIGN KEY (xh) REFERENCES xs(xh)
  ON UPDATE CASCADE ON DELETE CASCADE;

ALTER TABLE xk DROP CONSTRAINT IF EXISTS xk_kcbh_fkey_1;
ALTER TABLE xk ADD CONSTRAINT xk_kcbh_fkey_1
  FOREIGN KEY (kcbh,jsbh) REFERENCES sk(kcbh,bh)
  ON UPDATE CASCADE ON DELETE CASCADE;

创建外键约束后, 我们可以直接使用下面的 SQL 语句进行删除操作.

1
DELETE FROM xs WHERE xh='20230108';
删除非主键属性成功
删除非主键属性成功

练习先删除子表数据,再删除主表数据

重置数据库之后, 删除土木工程学院 (代号为05) 的相关信息,并把属于土木工程学院的同学,老师,授课信息以及同学的选课信息删除。

这个操作无法直接使用 ON DELETE CASCADE 选项来实现, 我们必须按照 依赖关系的逆序 删除数据,即从 最底层的引用表 开始删除,逐步向上删除高层表。

1
2
3
4
5
6
DELETE xk WHERE xk.jsbh IN (SELECT jsbh FROM js WHERE ydh = '05');
DELETE xk WHERE xk.xh IN (SELECT xh FROM xs WHERE ydh = '05');
DELETE sk WHERE sk.bh IN (SELECT jsbh FROM js WHERE ydh = '05');
DELETE js WHERE ydh = '05';
DELETE xs WHERE ydh = '05';
DELETE xyb WHERE ydh = '05';
成功按顺序删除子表格后删除主表格
成功按顺序删除子表格后删除主表格

使用子查询方式更新、删除数据

更新数据

重置数据表后, 找出没有任何一门成绩小于80的同学,将他们的班级信息更新为“全优良”。

我们可以使用子查询的方式完成这个操作, SQL语句如下:

1
2
3
4
5
6
UPDATE xs SET bj='全优良' WHERE xh IN (
  SELECT xh FROM xs WHERE xh NOT IN (
    SELECT xh FROM xk WHERE cj < 80
  )
);
SELECT * FROM xs WHERE bj='全优良';

可以看见, 在更新数据之后查询的结果显示, 没有任何一门成绩小于80的同学的班级都为 全优良.

使用子查询更新数据成功
使用子查询更新数据成功

删除数据

找出任意一门选课成绩小于75的同学,将他们的信息删除。

同样这需要为外键约束定义 ON DELETE CASCADE 选项, 这样在删除主表的记录时, 子表中的相关记录会自动删除.

添加外键约束后, 我们可以直接使用下面的 SQL 语句进行删除操作.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DELETE FROM xs WHERE xh IN (
  SELECT xh FROM xs WHERE xh IN (
    SELECT xh FROM xk WHERE cj < 75
  )
);
-- 删除后检查
SELECT * FROM xs WHERE xh IN (
  SELECT xh FROM xs WHERE xh IN (
    SELECT xh FROM xk WHERE cj < 75
  )
);

可以看见, 在删除数据之后查询的结果显示, 任意一门选课成绩小于75的同学的信息已经被删除.

使用子查询删除数据成功
使用子查询删除数据成功

体会索引

查询计划 EXPLAIN

重置数据库之后, 利用查询计划 (EXPLAIN) 对查询语句进行分析。

1
2
3
4
5
EXPLAIN SELECT * FROM xs WHERE xh IN (
  SELECT xh FROM xs WHERE xh IN (
    SELECT xh FROM xk WHERE cj < 75
  )
);

在图中,我们可以看到每一步运行的时间,从而可以判断哪一步是最需要优化的步骤。

查询计划分析
查询计划分析

接下来,我们观察去掉所有约束和主键索引后查询效率发生的变化.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ALTER TABLE js DROP CONSTRAINT IF EXISTS js_pkey CASCADE;
ALTER TABLE js DROP CONSTRAINT IF EXISTS idx_js_ydh CASCADE;
ALTER TABLE kc DROP CONSTRAINT IF EXISTS kc_pkey CASCADE;
ALTER TABLE kc DROP CONSTRAINT IF EXISTS idx_kc_kc CASCADE;
ALTER TABLE kc DROP CONSTRAINT IF EXISTS idx_kc_xf CASCADE;
ALTER TABLE sk DROP CONSTRAINT IF EXISTS sk_pkey CASCADE;
ALTER TABLE xk DROP CONSTRAINT IF EXISTS xk_pkey CASCADE;
ALTER TABLE xk DROP CONSTRAINT IF EXISTS idx_xk_cj CASCADE;
ALTER TABLE xk DROP CONSTRAINT IF EXISTS idx_xk_jsbh CASCADE;
ALTER TABLE xk DROP CONSTRAINT IF EXISTS idx_xk_kcbh CASCADE;
ALTER TABLE xs DROP CONSTRAINT IF EXISTS xs_pkey CASCADE;
ALTER TABLE xs DROP CONSTRAINT IF EXISTS idx_xs_ydh CASCADE;
ALTER TABLE xyb DROP CONSTRAINT IF EXISTS xyb_pkey CASCADE;
查询计划分析 (没有索引)
查询计划分析 (没有索引)

可知建立索引与不建立索引,查询效率确实会发生一定差异。索引提高数据查找的效率,当数据量非常大时将会变得非常显著。如果建立适当的索引,数据库查询效率将会大幅度提高。

我们建立两张测试表, 一张有索引, 而一张无索引, 向两张表中插入大量数据, 用于测试大量数据下的性能差异.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
DROP TABLE IF EXISTS test_table_with_idx;
DROP TABLE IF EXISTS test_table_without_idx;
CREATE TABLE test_table_with_idx(
  id INTEGER PRIMARY KEY,
  x DECIMAL(5,4)
);
CREATE TABLE test_table_without_idx(
  id INTEGER,
  x DECIMAL(5,4)
);
DROP INDEX IF EXISTS t_index;
CREATE INDEX t_index ON test_table_with_idx(x);
DO $$
  BEGIN
    FOR cnt IN 1..100000 LOOP
      INSERT INTO test_table_with_idx(id,x) VALUES (cnt,RANDOM());
      INSERT INTO test_table_without_idx(id,x) VALUES (cnt,RANDOM());
    END LOOP;
  END
$$;

然后对两张表分别进行大量查询, 观察查询效率的差异.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
DO $$
  BEGIN
    FOR cnt IN 1..100000 LOOP
      PERFORM * FROM test_table_with_idx WHERE x = 0.5;
    END LOOP;
  END
$$;

DO $$
  BEGIN
    FOR cnt IN 1..100000 LOOP
      PERFORM * FROM test_table_without_idx WHERE x = 0.5;
    END LOOP;
  END
$$;

实验表明, 有索引的情况下100000次查询用时仅需2秒, 而没有索引的情况下用时超过了30分钟. 可以看出, 有索引的查询效率明显高于没有索引的查询效率. 这说明了索引在数据库查询中的重要性, 在数据量较大的情况下, 索引可以显著提高查询效率.

有索引的查询
有索引的查询
没有索引的查询
没有索引的查询

使用 EXPLAIN 语句可以查看查询计划.

1
2
EXPLAIN SELECT * FROM test_table_with_idx WHERE x = 0.5;
EXPLAIN SELECT * FROM test_table_without_idx WHERE x = 0.5;

#figure( grid(columns: 2, gutter: 1em, image(“assets/explain-with-idx.png”), image(“assets/explain-without-idx.png”), ), caption: “查询计划分析 (有索引, 没有索引)”, )

可以看到, 有索引时, 查询采用 Bitmap Heap Scan, 而没有索引时, 查询采用 Seq Scan. Bitmap Heap Scan 是一种高效的查询方式, 它会先在索引中查找符合条件的记录, 然后再在数据表中查找对应的记录. 而 Seq Scan 则是顺序扫描整个数据表, 效率较低. 这也进一步证明了索引在查询中的重要性.

权限管理

以不同身份用户登录数据库建立表

使用 SSH 连接到服务器,切换到 omm 用户, 使用 gsql 命令连接到数据库。连接到数据库后,使用 CREATE USER 命令创建一个新用户。

1
CREATE USER starlight PASSWORD 'PositionZER0@123';

创建成功后, 使用 \q 命令退出数据库。使用新创建的用户登录数据库。

1
gsql -d postgres -U starlight -W PositionZER0@123
登录用户
登录用户

使用与实验二中一样的语句创建学院表 xyb.

1
2
3
4
CREATE TABLE IF NOT EXISTS xyb ( -- 创建学院表,存储学院基础信息
    ydh CHAR(2) PRIMARY KEY NOT NULL,  -- 学院代号,主键(不允许空,固定长度2字符)
    ymc CHAR(30) NOT NULL             -- 学院名称(不允许空,固定长度30字符)
);

执行之后, 使用 \dt 命令查看表格信息, 可以看到表格创建成功.

\dt 命令的结果可以发现, 尽管表名相同, 但是表格的 Schema 不同. 用户 starlight 建立的 xyb 表格在 starlight 这一 Schema 下. 这说明了不同用户创建的表格是相互独立的, 互不影响.

创建学院表
创建学院表

xyb 表格中插入不同数据, 便于后续实验中进行区分.

以不同身份用户查询自己与其他用户建立的表

以用户 starlight 登录数据库, 使用 \dt 命令查看表格信息. 可以看到, 该用户只能看到自己创建的表格, 不能看到其他用户创建的表格.

使用 SELECT 语句查询用户 starlight 创建的 xyb 表格.

1
SELECT * FROM xyb;
查询自己创建的表格
查询自己创建的表格

若需要查询用户 cloudb 创建的 xyb 表格, 如果直接使用 SELECT 语句, 会提示权限拒绝. 这是因为用户 starlight 没有权限访问其他用户创建的表格.

1
SELECT * FROM cloudb.xyb;
查询其他用户创建的表格提示权限拒绝
查询其他用户创建的表格提示权限拒绝

若要给予用户 starlight 访问用户 cloudb 创建的 xyb 表格的权限, 需要使用 GRANT 命令. 以用户 omm 登录数据库, 使用 GRANT 命令给予用户 starlight Schema和对应表格的访问权限.

1
2
GRANT USAGE ON SCHEMA cloudb TO starlight;
GRANT SELECT ON TABLE cloudb.xyb TO starlight;

执行成功后, 以用户 starlight 登录数据库, 使用 SELECT 语句查询用户 cloudb 创建的 xyb 表格.

授权后成功查询其他用户创建的表格
授权后成功查询其他用户创建的表格

定义授权方案并进行验证

例如需要给予用户 starlight 对用户 cloudb 创建的 xyb 表格的访问权限, 对 xs 表格的访问与修改权限, 对 xk 表无访问权限. 这时可以使用 GRANT 命令给予用户 starlight 对应的权限.

1
2
3
GRANT USAGE ON SCHEMA cloudb TO starlight;
GRANT SELECT ON TABLE cloudb.xyb TO starlight;
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE cloudb.xs TO starlight;

执行成功后, 以用户 starlight 登录数据库, 使用 SELECT 语句可顺利查询用户 cloudb 创建的 xyb 表格; 使用 INSERT, UPDATE, DELETE 语句可顺利修改到用户 cloudb 创建的 xs 表格; 使用 SELECT 语句查询用户 cloudb 创建的 xk 表格时, 提示权限拒绝.

1
2
3
SELECT * FROM cloudb.xyb;
INSERT INTO cloudb.xs (xm, xh, ydh, bj, chrq, xb) VALUES ('巴珠绪', '20230111', '01', '计算机科学与技术', '2003-01-01', '女');
SELECT * FROM cloudb.xk;
成功设置权限
成功设置权限

实验结论

通过本次数据库物理设计实验,我得出了以下几点结论:

  1. 数据分区表:分区表能有效地将大量数据分散存储,提高查询和管理效率。根据实验观察,按范围分区是处理时序数据(如按年份划分的学生信息)的有效策略。

  2. 主键与外键约束

    • 修改或删除被外键引用的主键数据需要特别小心。如果没有设置合适的级联操作,数据库会拒绝执行违反引用完整性的操作。
    • 使用 ON UPDATE CASCADEON DELETE CASCADE 选项可以自动维护引用完整性,避免手动处理依赖关系的繁琐步骤。
    • 当涉及复杂的关系结构时,必须按照依赖关系的逆序进行删除操作。
  3. 索引的重要性

    • 实验证明索引对查询性能的影响是巨大的。在含有10万条记录的表上,有索引的查询比无索引查询快约900倍。
    • 通过 EXPLAIN 命令分析,可以看到有索引时数据库使用 Bitmap Heap Scan 等高效的查询方式,而无索引时只能使用 Sequential Scan。
    • 索引的效果随数据量增加而更加显著,对大规模数据库系统尤为重要。
  4. 子查询在数据操作中的应用:使用子查询可以实现复杂的数据筛选和操作,提高SQL语句的表达能力和灵活性。

实验体会

本次实验让我深入理解了数据库物理设计的重要性和复杂性。以下是我的主要体会:

  1. 数据库性能优化的关键:物理设计直接影响数据库的性能。合理的索引设计、表分区策略和约束定义能显著提升查询效率,尤其在大规模数据环境下。

  2. 权衡与取舍:数据库设计涉及多方面的权衡。例如,添加索引提高查询性能,但会占用存储空间并可能降低写入性能;CASCADE操作简化了数据管理,但可能导致意外的大规模数据删除。

  3. 实验与验证的重要性:理论知识需要通过实验验证和巩固。例如,索引对性能的影响虽然在理论上清晰,但通过实际测量两种情况下的查询时间,我对索引的重要性有了更直观的认识。

  4. 数据完整性与业务逻辑:数据库约束(如外键)不仅是技术实现,也反映了业务规则和数据完整性要求。合理设计约束可以防止错误数据的产生。

发表于 2025-04-07
JS
Arrow Up