推荐几个有关范式的教学视频:
-
范式判断
-
将R分解为BCNF
-
将R分解为3NF
关系代数
概述: 是数据库系统中的一种查询语言,用于对关系模型中的数据进行操作。它提供了一组操作,可以从关系(通常是表格)中派生出新的关系,关系代数是关系型数据库查询的基础之一,能够帮助DBMS(数据库管理系统)执行查询请求
基本操作
选择(Selection, σ):
选择操作从关系中筛选出满足特定条件的元组(记录)
-
符号: σ 条件 ( R ) σ_{条件}(R) σ条件(R)
-
作用:从关系R中选择所有满足给定条件的元组
-
例子:假设有一个学生表,包含学生的姓名、年龄、成绩等信息,若要选择年龄大于20岁的学生,可以写成:
σ a g e > 20 σ_{age}>20 σage>20
投影(Projection, π):
投影操作用于选择关系中的特性属性(列)
-
符号: π 属性列表 ( R ) π_{属性列表}(R) π属性列表(R)
-
作用:从关系R中选择特定的属性列
-
例子:如果我们只对学生表中的姓名和成绩感兴趣,可以写成:
π n a m e , s c o r e ( 学生 ) π_{name, score}(学生) πname,score(学生)
这将返回一个新的关系,包含学生表中的姓名和成绩列
联接(Join, ⨝):
联接操作用于将两个关系中的元组结合起来,形成一个新的关系。常见的联接类型包括:
-
内联接(Inner Join):返回两个关系中所有匹配的元组
-
外联接(Outer Join):返回所有匹配的元组以及那些没有匹配的元组(根据不同的外联接类型,如左外联接、右外联接、全外联接,结果会有所不同)
-
符号:R ⨝ S
-
例子:假设有两个学生表(包含学生的姓名和学号)和选课(包含学生的学号和课程名称),要找出所有学生及其所选课程的信息:
学生 ⋈ 选课 学生⋈选课 学生⋈选课
这表示将学生表和选课表通过学号进行联接,返回包含学生姓名、学号、课程等信息的关系
并(Union, ∪):
并操作用于合并两个关系中的元组,要求两个关系具有相同的属性集合
-
符号:R ∪ S
-
作用:返回两个关系中所有不同的元组(对行数据去重)
-
例子:如果有两个关系学生A和学生B,分别表示两个班级的学生信息,要求得到所有学生的集合:
学生 A ∪ 学生 B 学生A ∪ 学生B 学生A∪学生B
这表示将两个关系的元组合并,去除重复的学生信息
差(Difference, -):
差操作用于返回存在于第一个关系中但不在第二个关系中的元组
-
符号:R - S
-
作用:返回关系R中存在但不在关系S中存在的元组
-
例子:如果学生A表示某班的学生,学生B表示已参加某考试的学生,那么:
学生 A − 学生 B 学生A - 学生B 学生A−学生B
这表示所有未参加考试的学生
笛卡尔积(Cartesian Product, ×):
笛卡尔积操作将两个关系中的所有元组进行组合,生成一个新的关系,新的关系中的每个元组由两个关系中的元组拼接而成
-
符号:R × S
-
作用:返回两个关系中所有元组的组合
-
例子:假设有学生表和课程表,执行笛卡尔积会返回每个学生与每个课程的组合:
学生 × 课程 学生 × 课程 学生×课程
如果学生表有3个学生,课程表有2门课程,结果将有6个元组
重命名(Rename, ρ):
重命名操作用于给关系或其属性命名:
-
符号: ρ 新名字 ( R ) ρ_{新名字}(R) ρ新名字(R)
-
作用:给关系R重新命名
-
例子:可以给一个关系重新命名,如:
ρ 新学生 ( 学生 ) ρ_{新学生}(学生) ρ新学生(学生)
这表示将学生表重命名为新学生
复合操作
选择和投影的组合:
假设要查询所有成绩大于80分的学生的姓名和成绩:
π
n
a
m
e
,
s
c
o
r
e
(
σ
s
c
o
r
e
>
80
(
学生
)
)
π_{name, score}(σ_{score>80}(学生))
πname,score(σscore>80(学生))
联接与其他操作的结合:
假设要查询所有选了”数据库系统“课程的学生的姓名和成绩,可以使用联接与头因操作:
π
n
a
m
e
,
s
c
o
r
e
(
学生⨝
σ
c
o
u
r
s
e
=
"
数据库系统
"
(
选课
)
)
π_{name, score}(学生 ⨝ σ_{course="数据库系统"}(选课))
πname,score(学生⨝σcourse="数据库系统"(选课))
SQL
概述: SQL(结构化查询语言)是用于与关系型数据库管理系统(RDBMS)进行交互的标准语言。主要用于操作数据库中的表,每个表由列和行组成。
入门SQL
数据查询(SELECT)
SELECT是SQL中最常用的语句,用于从一个或多个表中检索数据
查询所有列:
SELECT * FROM table_name;
- *:表示所有列
- table_name:要查询的表名称
查询特定列:
SELECT column1, column2 FROM table_name;
- 只选择column1和column2这两列的数据
使用条件查询(WHERE):
SELECT * FORM table_name WHERE condition;
-
WHERE子句用来指定查询的条件
-
例如,查询所有年龄大于20岁的学生:
SELECT * FROM students WHERE age > 20;
排序查询结果(ORDER BY):
SELECT * FROM table_name ORDER BY column_name [ASC | DESC];
-
ORDER BY用来按照某一列对查询结果进行排序
-
默认是升序ASC,可以使用DESC表示降序
-
例如,按年龄升序排列学生:
SELECT * FROM students ORDER BY age ASC;
限制查询结果(LIMIT):
SELECT * FROM table_name LIMIT n;
-
LIMIT用于限制返回的记录数
-
例如,查询前5条记录:
SELECT * FROM students LIMIT 5;
数据插入(INSERT)
插入单条记录:
INSERT INTO table_name (column1, column2) VALUES (value1, value2);
-
例如向students表插入一条记录:
INSERT INTO students (name, age, score) VALUES ('John', 22, 90);
插入多条记录:
INSERT INTO table_name (column1, column2) VALUES (value1, value2), (value3, value4);
-
例如:
INSERT INTO students (name, age, score) VALUES ('Alice', 20, 88), ('Bob', 21, 91);
数据更新(UPDATE)
用于更新表中已存在的记录
UPDATE table_name SET column1 = value1, column2 = value2 WHERE condition;
-
例如:更新学生John的分数:
UPDATE students SET score = 95 WHERE name = 'John';
-
注意:WHERE子句非常重要,缺少WHERE会更新表中的所有记录
数据删除(DELETE)
用于删除表中的记录:
DELETE FROM table_name WHERE condition;
-
例如,删除年龄小于18岁的学生:
DELETE FROM students WHERE age < 18;
-
如果没有WHERE子句,所有记录都会被删除
创建数据库和表
创建数据库:
CREATE DATABASE database_name;
-
例如,创建一个名为school的数据库:
CREATE DATABASE school;
创建表:
CREATE TABLE table_name {
column1 datatype,
column2 datatype,
...
};
-
例如,创建一个学生表students:
CREATE TABLE students ( id INT PRIMARY KEY, name VARCHAR(50), age INT, score FLOAT )
修改表结构
ALTER用于修改表结构,如添加、删除列等
添加列:
ALTER TABLE table_name ADD column_name datatype;
-
例如,向student表添加 email列:
ALTER TABLE students ADD email VARCHAR(100);
删除列:
ALTER TABLE table_name DROP COLUMN column_name;
-
例如,删除students表中的email列:
ALTER TABLE students DROP COLUMN email;
删除数据库和表
DROP用于删除数据库中的表或数据库
删除表:
DROP TABLE table_name;
-
例如,删除students表:
DROP TABLE students;
删除数据库:
DROP DATABASE database_name;
-
例如,删除数据库school:
DROP DATABASE school;
基本的SQL函数
-
聚合函数:
-
COUNT():计算记录数
-
SUM():计算列的总和
-
AVG():计算列的平均值
-
MAX():获取列的最大值
-
MIN():获取列的最小值
-
例如,查询所有学生的平均成绩:
SELECT AVG(score) FROM students;
-
-
字符串函数:
-
CONCAT():将多个字符串连接起来
-
UPPER():将字符串转换为大写
-
LOWER():将字符串转换为小写
-
例如,将学生姓名转换为小写
SELECT LOWER(name) FROM students;
-
多表查询
概述: 也称联接查询,指的是在同一查询中, 涉及两个或多个表的数据操作。多表查询通常用于获取存储在不同表中的相关数据,通过某种条件将它们关联起来,通过多表查询,可以实现更加复杂和灵活的数据提取需求。
内联接(INNER JOIN)
概述: 是最常见的多表查询类型,它返回的是两个表中满足联接条件的所有记录,如果某一表中没有匹配的记录,则该记录不会出现在查询结果中
示例:
假设有两个表students表和courses表,分别包含以下内容:
- students:student_id、name
- courses:course_id、student_id、course_name
要查询每个学生和他们所选的课程信息,可以使用INNER JOIN:
SELECT students.name, courses.course_name
FROM students
INNER JOIN courses
ON student.student_id = courses.student_id;
解释:这条查询通过students.student_id和courses.student_id继续匹配,只返回那些在students 和courses表中有对应记录的学生及课程
外联接(OUTER JOIN)
概述: 与内联接的不同之处在于,外联接返回的是所有匹配记录和不匹配的记录)可以进一步细分为三种类型:左外联接(LEFT JOIN)、右外联接(RIGHT JOIN)、全外联接(FULL OUTER JOIN)
左外联接(LEFT JOIN):
左外联接返回左表(查询中的第一个表)中的所有记录,以及右表(查询中的第二个表)中与左表记录匹配的记录。如果右表没有匹配的记录,结果中的对应部分将显示为NULL
示例:假设想要查询所有学生的信息,以及他们是否选修了某些课程。如果没有选课的学生也需要显示,可以使用左外联接:
SELECT students.name, courses.course_name
FROM students
LEFT JOIN courses
ON students.student_id = courses.student_id;
解释:如果某个学生没有选课,那么该学生的course_name将会显示为NULL,但仍会显示学生的name
右外联接(RIGHT JOIN):
右外联接返回右表(查询中的第二个表)中的所有记录,以及左表(查询的第一个表)中与右表匹配的记录。如果左表没有匹配的记录,结果中对应部分将显示为NULL
SELECT students.name, courses.course_name
FROM students
RIGHT JOIN courses
ON students.student_id = courses.student_id;
- 解释:此查询返回所有课程及选修这些课程的学生,如果某个课程没有学生选修,则name列显示为NULL
全外联接(FULL OUTER JOIN):
全外联接返回左表和右表中的所有记录。如果某一边没有匹配的记录,另一边的列会显示为NULL
SELECT students.name, courses.course_name
FROM students
FULL OUTER JOIN courses
ON students.studetn_id = courses.student_id;
- 解释:这条查询返回所有学生和所有课程,如果学生没有选课或课程没有学生选修,则结果中相应部分会显示为NULL
笛卡尔积(CARTESIAN PRODUCT)
概述: 笛卡尔积返回两个表中每个记录的组合,笛卡尔积并不基于任何联接条件,因此结果集可能非常庞大
示例:
假设有两个表:students表(包含3个学生)和courses表(包含2门课程),笛卡尔积的查询语句如下:
SELECT students.name, courses.course_name
FROM students, courses;
解释:这条查询会返回所有学生和所有课程的组合,结果为 3 * 2 = 6 条记录,即每个学生都将与每门课程组合成一条记录。
自联接(SELF JOIN)
概述: 自联接是一种特殊的联接,它用于将同一张表与自身进行联接,通常用于查询表中相互关联的数据。
示例:
假设employees表存储了员工信息,包括employee_id和manager_id,通过自联接查询员工及其经理的信息:
SELECT e1.name AS employee_name, e2.name AS manager_name
FROM employees e1
LEFT JOIN employees e2
ON e1.manager_id = e2.employee_id;
解释:这套查询将employees表与自身进行联接,e1代表员工,e2代表经理,通过manager_id和employee_id进行匹配,返回每个员工及其对应经理的姓名
联合(UNION)
概述: 联合是将两个或多个SELECT语句的结果合并成一个结果集。UNION会自动去重,如果需要保留重复数据,可以使用UNION ALL
示例:
假设有两个表students_2023和students_2024,分别存储2023年和2024年入学的学生信息,要查询所有学生的姓名,可以使用UNION:
SELECT name FROM students_2023
UNION
SELECT name FROM students_2024;
解释:这条查询会返回两个表中的所有学生姓名,并且去除重复的姓名。
中级SQL
子查询(Subquery)
概述: 子查询是嵌套在其他查询语句中的查询。它可以出现在SELECT、FROM、WHERE等子句中,通常用于过滤数据、计算聚合值或作为连接条件
标量子查询:
返回单一值,可以用在SELECT、WHERE或HAVING子句中
例如,查询每个学生的姓名及其选修课程的数量:
SELECT name,
(SELECT COUNT(*) FROM courses WHERE courses.student_id = students.student_id) AS course_count
FROM students;
解释:这里的子查询返回每个学生选修课程的数量,并将其作为列course_count展示
多行子查询:
返回多行数据,常用于IN或EXISTS中
SELECT name
FROM students
WHERE student_id IN (SELECT student_id FROM courses WHERE course_name = 'Math');
解释:该查询返回所有选修Math课程的学生姓名。子查询返回所有选修该课程的学生ID,然后在主查询中通过IN进行匹配
多列子查询:
返回多列数据,常用于WHERE子句中
SELECT name
FROM students
WHERE (student_id, age) IN (SELECT student_id, age FROM students WHERE score > 90);
解释:该查询返回所有年龄和分数大于90分的学生姓名
联接进阶引用
自然联接(NATURAL JOIN):
根据两个表中具有相同列名的列自动进行联接,他会自动选择相同列名并按这些列进行联接
SELECT *
FROM employees
NATURAL JOIN departments;
解释:NATURAL JOIN会自动识别两个表中相同的列名进行联接。这个操作等效于执行INNER JOIN和选择相同列的条件。
复合联接条件(Using Clause):
使用USING关键字来指定多个列作为联接条件:
SELECT e.name, d.department_name
FROM employees e
JOIN departments d USING (department_id);
解释:USING用于指定两个表中需要联接的列,并且这些列必须具有相同的名字。在这种情况下,联接的条件是deparment_id
聚合函数与分组(GROUP BY)
GROUP BY用于将查询结果分组,并对每个组执行聚合函数(如COUNT、SUM、AVG、MIN、MAX)
GROUP BY 和 HAVING:
HAVING用于过滤分组后的结果,类似于WHERE,但是作用于聚合后的数据
SELECT department_id, AVG(salary)
FROM employees
GROUP BY department_id
HAVING AVG(saraly) > 50000;
解释:这条查询会计算每个部分的平均工资,并仅返回平均工资大于50000的部门
GROUP BY多列:
你可以按多个列进行分组,这样可以获得更细致的统计结果
SELECT department_id, job_title, COUNT(*)
FROM employees
GROUP BY department_id, job_title;
解释:该查询返回每个部门、职位的员工数量
窗口函数(Window Functions)
概述: 是一类特殊的聚合函数,它可以在查询结果集中为每一行计算一个值,而不需要像GROUP BY那样分组。常用的窗口函数包括ROW_NUMBER、RANK、DENSE_RANK、NTILE等。
ROW_NUMBER():
为查询结果集中的每一行分配一个唯一的序号,通常用于分页查询或排名
SELECT name, salary, ROW_NUMBER() OVER (ORDER BY salary DESC) AS rank
FROM employees;
解释:该查询返回员工的姓名、工资,并根据工资降序排列,每一行都有一个唯一的排名(rank)
RANK()和DENSE_RANK():
RANK()和DENSE_RANK()用于生成排名,区别在于RANK()会跳过排名(例如,若有两个并列第一名,则第三名跳过),而DENSE_RANK()则不会跳过排名
SELECT name, salary, RANK() OVER (ORDER BY salary DESC) AS rank
FROM employees;
解释:这条查询为每个员工计算排名,工资高的员工排名靠前。如果有员工的工资相同,他们的排名也会相同。
事务控制
概述: 事务是指一组作为单一工作单元执行的SQL语句。在事务中,所有的操作要么都成功(COMMIT),要么都失败并回滚(ROLLBACK)
事务的基本操作:
- BEGIN TRANSACTION:开始一个事务
- COMMIT:提交事务,保存所有的修改
- ROLLBACK:回滚事务,撤销事务中所有的修改
BEGIN TRANSACTION;
UPDATE employees SET salary = saraly * 1.1 WHERE department_id = 1;
-- 如果没有问题,可以提交事务
COMMIT;
-- 如果出错了,回滚事务
ROLLBACK;
视图(VIEW)
概述: 视图是一个虚拟的表,它是一个包含SQL查询的命令查询。视图可以简化复杂查询的使用,提高查询的可读性和复用性
创建视图:
CREATE VIEW top_earning_employees AS
SELECT name, salary
FROM employees
WHERE salary > 100000;
这条语句创建了一个视图,该视图返回所有年薪超过10w的员工信息
查询视图:
SELECT * FROM top_earning_employees;
查询视图和查询普通表类似
删除视图:
DROP VIEW top_earning_employees;
索引(Index)
概述: 是提高查询性能的关键工具,它允许数据库更快速地查找数据,对于经常用作查询条件的列,可以创建索引
创建索引:
CREATE INDEX idx_employee_name ON employees (name);
该索引加速了基于name列的查询
删除索引:
DROP INDEX idx_employee_name;
高级SQL
复杂查询与高级子查询
相关子查询(Correlated Subquery):
查询条件依赖于外部查询的数据,在执行时,每一行外部查询的记录都将触发一次子查询。
SELECT name
FROM employees e
WHERE salary > (SELECT AVG(salary) FROM employees WHERE department_id = e.department_id);
解释:查询薪水高于所在部门平均薪水的员工。子查询是依赖于外部查询的每一行员工的 department_id 来计算该部门的平均薪水。
多层子查询(Nested Subquery):
是嵌套的子查询,通常用于执行更复杂的计算或过滤
SELECT name
FROM employees
WHERE department_id = (SELECT department_id
FROM departments
WHERE department_name = 'Sales'
);
解释:这条查询首先使用内层子查询查找名为 “Sales” 的部门的 department_id,然后在外层查询中使用这个 department_id`来查找所有属于该部门的员工。
存储过程和触发器
概述: 它们可以封装复杂的逻辑,自动化任务或响应特定的事件。
存储过程(Stored Procedure):
存储过程是一组预编译的SQL语句,可以在数据库中保存并反复调用
CREATE PROCEDURE raise_salary(IN dept_id INT, IN percentage DECIMAL)
BEGIN
UPDATE employees
SET salary = salary * (1 + percentage)
WHERE department_id = dept_id;
END;
解释:这个存储过程根据部门ID和百分比增加该部门员工的薪水
触发器(Trigger):
触发器是由特定事件(如插入、更新、删除)自动触发的SQL代码
CREATE TRIGGER salary_update
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
IF NEW.salary > 200000 THEN
INSERT INTO salary_updates (employee_id, old_salary, new_salary)
VALUES (NEW.employee_id, OLD.salary, NEW.salary);
END IF;
END;
解释:这个触发器会在员工的薪水更新后执行,若薪水超过200000,则将更改记录到salary_updates表中
实体-关系模型
概述: ER模型是一种用于描述和设计数据库结构的概念模型。主要由实体、属性、关系三种基本元素组成, 通过这些元素,我们能够直观地表示现实世界中数据的结构和它们之间的相互关系。
实体(Entity)
概述: 是现实世界可以独立存在并能够被明确描述的对象,通常表示一个表格(关系)的实例。
实体集(Entity Set): 由相同类型的实体组成的集合,例如所有的学生或员工可以构成一个实体集
示例:
- 学生:一个学生实体可能包括学生的姓名、学号、年龄等信息
- 图书:一本书的实体可能包括书名、作者、ISBN编号等信息
属性(Attribute)
概述: 是描述实体特征的数据项,它表示实体所拥有的某个具体特征或性质。属性通常用于详细描述实体,并且与实体的实例一一对应
属性的分类:
- 简单属性(Simple Attribute):不能再进一步分解的属性。例如,”学生姓名“是简单属性
- 复合属性(Composite Attribute):由多个简单属性组成的属性。例如,地址可能由”街道“、“城市”、“邮政编码”等组成
- 多值属性(Multivalued Attribute):一个实体可以有多个值的属性。例如,学生的“电话号码”可能有多个
- 派生属性(Derived Attribute):可以从其他属性中计算得出的属性。例如,学生的“年龄”可以从出生日期计算得出
示例:
- 学生实体的属性:学号、姓名、出生日期、性别、地址
- 图书实体的属性:书名、作者、出版日期、ISBN
关系(Relationship)
概述: 表示两个或多个实体之间的联系。关系通过实体之间的联系和约束表示为一个抽象的“关系”,来说明这些实体如何交互
分类:
- 一对一关系(One-to-One, 1:1):一个实体集中的每个实体只与另一个实体集中的一个实体相关联。例如一个学生只能有一个身份证,反之一个身份证只能对应一个学生
- 一对多关系(One-to-Many, 1:N):一个实体集中的每个实体可以与另一个实体集中的多个实体相关联,但后者的每个实体只能与前者的一个实体相关联。例如,一个部门可以有多个员工,但每个员工只属于一个部门
- 多对多关系(Many-to-Many, M:N):一个实体集中的每个实体可以与另一个实体集中的多个实体相关联。例如,学生和课程之间是多对多关系,一个学生可以选多门课程,一门课程可以有多个学生
ER图
概述: 是ER模型的图形化表示,通过图形化的方式描述实体、属性和关系,帮助设计人员更直观地理解和设计数据库。
ER图的基本符号:
- 实体:用矩形表示
- 属性:用椭圆表示,并通过线连接到相应的实体
- 关系:用菱形表示,连接参与关系的实体
- 主键属性:用双椭圆表示
- 多值属性:用双椭圆表示
约束: 用来限制数据的合法性和有效性。常见的约束有:
- 基数约束(Cardinality Constraint):描述实体之间的关系数量限制。例如,学生与课程的关系是多对多的,这意味着每个学生可以选多门课程,反之,每门课程也可以被多个学生选修
- 参与约束(Participation Constraint):描述一个实体是否必须参与某个关系。它分为全参与和部分参与:
- 全参与:表示实体必须参与某个关系
- 部分参与:表示实体可以参与,也可以不参与某个关系
扩展:
- 弱实体(Weak Entity):表示没有足够属性来唯一标识的实体。通常,弱实体依赖于一个或多个强实体来标识。例如,订单项(OrderItem)依赖于订单(Order)来唯一标识
- 继承(Inheritance):表示实体集之间的继承关系,通常是在面向对象的数据库设计中使用。例如,一个员工实体可以是全职员工和兼职员工的父类,后者继承了前者的属性
数据库设计
目标:
- 提供高效的数据存取
- 保证数据的完整性和一致性
- 降低数据冗余
- 使数据库易于扩展和维护
设计过程
需求分析: 明确数据库需要存储什么样的数据,这些数据之间的关系是什么,如何使用这些数据
概念设计(Conceptual Design): 从业务需求出发,抽象出一个全面的数据类型,并使用ER模型来描述数据和它们之间的关系
- 实体集:定义要存储的主要数据对象
- 属性:定义每个实体的特性
- 关系:定义实体之间的联系
逻辑设计(Logical Design): 将概念设计转换为具体的数据库模型,通常是关系模型。逻辑设计的目标是确定如何将实体-关系模型(ER模型)转换为数据库的表结构。
步骤:
- 实体转换为表格:每个实体集通常转换为一个数据库表(关系)。表的每一行代表一个实体实例,表的每一列代表实体的属性
- 关系转换为外键:实体之间的关系(如一对多、一对一、多对多)需要用外键(Foreign Key)来表示。例如,一个学生选修课程的关系可以用学生表的外键来表示学生与课程之间的关系。
- 规范化(Normalization):规范化是关系模型中一种用于减少冗余数据和防止数据差异的过程。通过分解大表,确保表结构的高效性和一致性
规范化:目的是减少数据冗余和数据异常,规范化有多个级别,常见的有:
- 第一范式(1NF):要求每个字段的值都是原子性的,即不可再分的
- 第二范式(2NF):在1NF的基础上,要求表中的每个非主属性都完全依赖于主键
- 第三范式(3NF):在2NF的基础上,要求表中的非主属性不依赖于其他非主属性
- BCNF:是3NF的进一步强化,要求每个决定因素都是候选键
在实际的设计中,有时为了优化查询性能,可以适当进行反规范化,即引入冗余数据来提升查询效率
物理设计(Physical Design): 是将逻辑模型转换为实际的数据库实现。它关注如何优化数据存储和访问性能
- 数据存储结构:确定数据在硬盘上的存储方式。选择适当的数据存储结构(例如B树、哈希表)和索引结构,以提高查询和更新的效率
- 索引(Index):索引是加速查询操作的一种数据结构,它通过为表中的一列或多列创建索引,来提高查询速度。常见的索引类型有:
- 单列索引:针对表中的单一列创建索引
- 复合索引:针对表中的多个列创建索引
- 唯一索引:确保某列的数据值是唯一的
- 全文索引:用于文本数据的全文搜索
- 存储优化:选择合适的存储介质和存储方式(例如,表空间、分区、压缩等)来优化数据库性能
- 数据分区(Partitioning):将大型表分割成较小的部分,以提高查询性能和数据库维护的效率,常见的分区方法有:
- 范围分区(Range Partitioning):根据数据范围将数据分区
- 哈希分区(Hash Partitioning):根据数据值的哈希值进行分区
- 列表分区(List Partitioning):根据预定义的值将数据分区
数据库实施: 涉及将设计好的数据库模型实现到实际的数据库管理系统中
- 使用SQL语句创建数据库结构
- 输入数据并进行出测试
- 根据需求调整数据库结构和索引
数据库测试与优化:
- 查询优化:通过分析查询执行计划、索引涉及、表结构调整等来优化查询性能
- 数据完整性和一致性检查:确保数据遵循业务规则,并且在各种操作下保持一致性
- 事务管理和并发控制:确保多个事务在并发操作时不会引发数据冲突或不一致
关键概念
超键、候选键、主键与外键:
- 超键(Superkey):是一个属性集,能够唯一标识关系模式中的每一元组,可以包含冗余属性
- 候选键(Candidate Key):是最小的超键,不能删除任何属性,否则无法我i一标识元组
- 主键:从候选键选出一个作为主键,表中的唯一标识符,用于唯一标识每一行数据,每个表只能有一个主键
- 外键:表中的字段,它引用另一个表的主键,表示两个表之间的关联。外键用于维护表之间的关系完整性
数据完整性约束(Data Integrity Constraints): 用来确保数据库中数据的准确性和可靠性的规则:
- 实体完整性(Entity Integrity):确保每个表都有唯一主键
- 参照完整性(Referential Integrity):确保外键字段的值在相关表中存在
- 域完整性(Domain Integrity):确保字段的数据值符合预定义的域
- 用户定义完整性(User-Defined Integrity):由用户自定义的规则或约束,通常用于表示业务逻辑
事务(Transaction): 是数据库操作的一个基本单位,由一组SQL语句组成,执行这些语句要保证原子性、一致性、隔离性和持久性(ACID属性)。在设计时,需要考虑事务的并发控制和数据一致性
检索
存储
数据库存储结构
行(Tuple):
数据库中的一行数据称为行(在RDBMS中也叫元组或记录)。一行数据是按列存储的,每一列对应一个属性(字段)。行存储在页中,每个页可以存储多行
页(Page):
数据库的基本存储单元是页(也叫块block),一个页通常由一块固定大小的内存或磁盘空间组成(常见大小为4KB、8KB、16KB等)。数据表、索引、以及其他数据库对象中的数据都是在页中存储的
- 页头:包含一些元数据,例如页的标识符、页的类型(数据页、索引页等)、页的状态(已使用、空闲等)
- 数据区:实际存储数据的区域。每个页可以存储多行数据记录
文件(File):
多个页可以组成一个文件。数据库表、索引等对象通常由对应的文件,这些文件会存储数据记录、索引信息或其他数据库对象
文件组织方式
概述: 指在物理存储介质上如何组织数据库文件以及如何高效地存储、访问这些文件。文件组织的不同方式影响到数据检索的速度和效率。
堆组织(Heap Organization):
是最简单的文件组织方式,其中数据行是无序地插入到文件中的。每当有新纪录插入时,数据将直接追加到文件的末尾。
特点:
- 简单、插入操作开销小
- 查询效率低
- 适用于数据量小或不常查询的场景
顺序组织(Sequential Organization):
是将数据行按照某个列的值进行排序,并且数据在文件中按照顺序存储。
特点:
- 查询效率较高,特别是范围查询和按顺序检索
- 插入操作较慢,因为要保证数据顺序
- 删除操作可能导致空间碎片,需要定期整理
- 适用于查询中有大量范围检索或顺序扫描的场景
哈希组织(Hash Organization):
通过哈希函数将数据映射到一个预定大小的桶中。对于某些特定字段(如主键或其他唯一标识符),可以通过哈希算法来定位数据的位置
特点:
- 查询效率高,特别是对于精确查找
- 不支持范围查询,因为哈希是无序的
- 插入和删除操作通常较为高效
- 遇到哈希冲突时,系统需要采取一定的冲突解决策略(如链式法、开放地址法)
B+树组织(B+ Tree Organization):
是一种多路平衡树,广泛应用于数据库索引的文件组织中。所有数据都存储在叶子节点中,而内部节点仅存储索引信息。B+树的叶子节点通过链表连接,支持高效的范围查询
特点:
- 支持高效的查询、插入、删除操作,尤其适合于范围查询
- 适合处理大量数据,具有较好的平衡性,查询性能优良
- 支持动态更新,数据量增大时也能保持平衡
B树组织(B Tree Organization):
是一种多路平衡树,类似于B+树,不同之处在于,B树的内部节点也存储实际的数据记录
特点:
- 支持高效的查找、插入和删除操作
- 不适合范围查询,因为数据存储在内部节点中
- 在某些情况下,由于数据存储在内部节点,B树的内存效率较差
索引
概述: 是提高数据库查询性能的关键技术,可以快速定位数据的位置
聚焦索引(Clustered Index):
将数据表的物理存储顺序与索引顺序一致,表中的数据行按照索引的顺序存储。在一个表中,只能有一个聚焦索引
特点:
- 查询性能高,尤其是范围查询
- 插入和删除操作可能导致索引的重排
非聚焦索引(Non-Clustered Index):
是指索引结构与数据表的存储顺序无关。索引记录中保存了数据表中记录的物理地址或主键值
特点:
- 支持多个索引
- 查询速度较快,但比聚焦索引稍慢
- 插入和删除较快,因为数据的物理顺序不受影响
唯一索引(Unique Index):
确保某一列(或多列)中的值唯一,它在保证数据完整性的同时,也提高了查询速度
特点:
- 适用于需要确保唯一性的字段,如主键、唯一约束等
- 提高查询效率,同时避免重复数据
全文索引(Full-text Index):
用于全文检索,特别适用于大规模文本数据的检索,它可以建立一个词汇表,映射每个单词的位置
特点:
- 对于文本数据,尤其是内容查询非常高效
- 不适合结构化数据,通常用于文章、评论等文本内容的搜索
存储优化
数据压缩: 通过减少存储空间来提高存储效率
- 无损压缩:保证压缩后的数据可以完全恢复
- 有损压缩:通过丢弃一些冗余数据来减少存储空间
分区和分片:
分区是将表的数据按某些标准(如日期、地理位置等)分割到多个物理文件中
分片是将数据库的不同部分(如不同的数据表或数据库)分散存储在不同服务器上
缓存:
DBMS会使用缓存(内存中的缓存或磁盘缓存)来加速对频繁访问数据的读取。通过使用高速缓存,可以显著减少磁盘IO操作,提高查询性能
查询处理
概述: 负责将用户的查询请求转换为数据库能够执行的具体操作,并在尽可能高效的方式下执行查询。
查询解析
概述: 将用户输入的查询转换为数据库能够理解的内部表示形式的过程。该过程由查询解析器(Parser)来执行。
词法分析和语法分析:
- 词法分析(Lexical Analysis):将输入的SQL查询字符串分解成一系列的tokens,例如关键字、表名、列名、操作符等
- 语法解析(Syntax Analysis):根据SQL的语法规则,将tokens组织成一个语法树或解析树。这一阶段会检查SQL查询的语法是否正确,并生成一个树形结构,表示SQL查询的结构化信息
生成查询树:
是一个抽象语法树(AST),表示查询的逻辑结构。
查询优化
概述: 为查询生成一个高效的执行计划
查询执行计划:
Query Execution Plan 是数据库系统执行查询时所采用的具体步骤序列。执行计划包含了如何访问数据、如何连接不同表、如何执行操作(如排序、过滤等)等信息
常见的执行计划:
- 选择(Selection):从表中筛选出符合条件的行
- 投影(Projection):选择需要显示的列
- 连接(Join):将多个表的数据根据连接条件组合起来
- 排序(Sort):对数据进行排序操作
- 聚合(Aggregation):进行数据的汇总、分组等操作
查询优化的目标:
最优执行计划通常具有以下特点:
- 最小的IO成本:尽可能少地访问磁盘
- 最小的计算成本:尽可能少地进行计算,避免不必要的操作
- 最小的内存使用:尽量减少内存的使用量,避免内存溢出
查询优化策略:
- 选择谓词推送(Predicate Pushdown):将查询中的过滤条件(WHERE 子句)尽早地应用于数据,减少不必要的行的读取
- 投影谓词推动(Projection Pushdown):将查询中的投影条件(SELECT 子句)尽早地应用于数据,减少传递不必要的列
- 连接顺序优化(Join Order Optimization):确定连接多个表的最优顺序,不同的连接顺序可能会大幅度影响查询效率
- 索引使用:查询优化器可能会选择合适的索引来加速数据检索过程。通过使用索引,可以避免全表扫描,直接定位到需要的行。
- 视图合并(View Merging):对于复杂查询,优化器可以考虑合并视图或子查询,避免不必要的中间结果。
代价估算:
查询优化器会根据数据统计信息(如表的大小、列的基数、索引的选择性等)来估算执行不同计划的代价。通常,代价会以某种形式的成本度量(如 CPU 时间、磁盘 I/O、内存使用量等)来表示。优化器会选择代价最小的执行计划。
启发式优化与成本基优化:
- 启发式优化:利用经验规则和启发式方法来生成和优化执行计划。启发式优化不考虑所有可能的执行计划,但通过一些简单规则来尽量提升性能。
- 成本基优化:通过详细计算不同执行计划的代价,并选择最优的执行计划。成本基优化通常需要对每个操作的代价进行估算,并选择代价最小的方案。
联接实现
嵌套循环联接(Nested Loop Join, NLJ)
概述: 是一种常用的数据库联接算法,特别适用于没有索引或者联接的列没有排序的情况
主要步骤:
- 外层循环:选择一个表作为外层表,通常是较小的表,在外层循环中,逐行扫描外层表的每一行记录
- 内层循环:对于外层表中的每一行记录,扫描另一个表(内层表)的每一行记录
- 比较:对于外层表和内层表的每一对记录,执行连接条件的比较。
伪代码:
for each row r1 in Table1:
for each row r2 in Table2:
if r1 and r2 satisfy the join condition:
ADD (r1, r2) to the result set
块嵌套循环联接(Block Nested Loop Join, BNLJ)
概述: 是NLJ的改进算法,旨在减少内存的消耗并提高效率
主要步骤:
- 分块:首先将关系R和S按照块大小B进行分块,确保每次扫描一个数据库可以完全加载到内存中
- 外层循环:从外部关系R中取出一个数据块
- 内存循环:
- 对于每一个外部快,扫描内部关系S中的每个数据块
- 对内外两个块中的每对元组进行连接操作,找出符合联接条件的元组
性能优化:
- 内存利用:BNLJ利用内存缓存多个块,减少了磁盘IO的频繁访问
- 减少IO操作:BNLJ通过加载数据块而不是单独的元组,显著减少了磁盘访问的次数
事务恢复
概述: 确保数据库出现故障时能够恢复到一致状态
事务
事务恢复的目标 :
- 持久性(Durability):即使发生故障,已提交的事务结果也不会丢失
- 原子性(Atomicity):事务要么全部成功,要么完全不做更改。如果事务在执行过程中出现故障,数据库需要回滚(undo)事务操作,恢复到事务执行前的状态
- 一致性(Consistency):事务应将数据库从一个一致的状态转换到另一个一致的状态。即使发生故障,事务执行后的状态应满足数据库的约束条件
- 隔离性(Isolation):事务的执行不应受到其他事务的影响。恢复过程中,要保证正在执行的事务互不干扰
故障类型:
- 系统崩溃:如操作系统崩溃、硬件故障、突然断电等
- 媒体故障:如磁盘损坏,数据文件丢失
- 事务故障:事务在执行过程中由于逻辑错误、死锁等问题被中止或回滚
- 存储故障:如数据库日志损坏,导致恢复时缺少关键数据
事务日志(WAL):
日志的作用:记录了数据库操作的详细信息,包括每个事务的开始、提交和混滚操作,数据修改前后的值等。
日志格式:
- 写操作日志:记录每个修改操作的详细信息,包括修改前的数据和修改后的数据
- 事务开始日志:记录了事务的启动,包括事务ID
- 事务提交日志:记录事务的提交,表示该事务的修改已持久化
- 事务回滚日志:记录事务的回滚,撤销该事务的所有修改
恢复的基本过程:
- 分析阶段:恢复系统会扫描日志文件,找出所有已经提交的事务,未提交的事务以及在故障发生前已执行的操作。
- 重做阶段(Redo):系统会重新执行日志中已经提交的事务,确保所有已提交的事务操作已经持久化
- 回滚阶段(Undo):系统会撤销所有未完成的事务(包括崩溃前已开始但未提交的事务),恢复它们开始前的状态。
并发控制
概述: 是指在多个事务并行执行时,确保它们能够互补干扰且不会破坏数据库的一致性和完整性的机制。
问题
丢失更新(Lost Update):
如果两个事务都读取同一数据并进行更新,且更新操作没有同步,就可能导致其中一个事务的更新被另一个事务的更新覆盖,造成数据丢失
脏读(Dirty Read):
是指事务A读取了事务B修改但未提交的数据。如果事务B回滚,事务A就会读到无效数据。
不可重复读(Non-repeatable Read):
发生在同一事务内多次读取相同数据时,由于其他事务修改了数据,导致读取到不同的值
举例:
- 事务T1读取帐户余额为100
- 事务T2修改帐户余额为120并提交
- 事务T1再次读取帐户余额是,得到120,而第一次读取时是100
幻读(Phantom Read):
指的是同一事务内多次查询数据时,由于其他事务插入、删除或修改了数据,导致查询结果发生变化
举例:
- 事务T1查询符合条件的帐户列表,结果是10个帐户
- 事务T2插入了一个新帐户并提交
- 事务T1再次查询时,发现结果是11个帐户
目标
主要目标时保障DBMS中的事务隔离性
事务的隔离性通常有4个级别:
-
读未提交(Read Uncommited):事务可以读取未提交事务的数据,容易出现脏读问题
-
读已提交(Read Commited):事务只能读取已提交事务的数据,但可能会出现不可重复读
-
可重复读(Repeatable Read):事务在执行期间多次读取同一数据时,保证读取结果不变,防止不可重复读,但可能出现幻读
-
串行化(Serializable):事务按照顺序串行执行,完全避免幻读、不可重复读和脏读,但性能最差
技术
锁机制:
锁的种类:
- 共享锁(Shared Lock, S-Lock):允许多个事务同时读取数据,但不允许修改。多个事务可以持有共享锁,但不能对数据进行修改
- 排他锁(Exclusive Lock, X-Lock):事务在修改数据时需要获得排他锁,排他锁会阻止其他事务读取或修改相同的数据
- 意向锁(Intent Lock):指示事务打算在某个粒度的数据上获得锁
- 悲观锁与乐观锁:悲观锁假设会发生冲突,因此通过加锁来避免;乐观锁假设冲突概率较低,通过版本控制或数据验证等方式来避免冲突
锁的管理:
- 两阶段锁协议(Twp-Phase Locking, 2PL):为了避免死锁,数据库系统通常要求事务在执行期间按一定顺序获取锁,并在事务结束前不释放任何锁,但是还是不能完全避免死锁
- 死锁:当两个或多个事务在持有锁的情况下等待对方释放锁时,便发生了死锁,解决死锁的方法包括:
- 死锁检测:系统监控锁的状态,并通过算法发现死锁
- 死锁避免:通过资源分配策略(如银行家算法)避免死锁的发生
- 死锁恢复:当检测到死锁时,回滚某个事务以释放锁
时间戳排序:
通过为每个事务分配一个唯一的时间戳来控制事务的顺序,每个事务在执行时必须遵循一个全局的时间戳顺序,数据库根据事务的时间戳来判断对其他事务的影响
- 时间戳排序的核心:如果事务T1时间戳早于事务T2,则事务T1必须先执行,T2必须等待
- 时间戳排序方法通过比较事务的读/写时间戳来确保不发生冲突。
多版本并发控制(MVCC):
MVCC通过为数据创建多个版本来避免锁的竞争。每当一个事务对数据进行修改时,数据库会为该数据创建一个新版本,而不会直接覆盖旧版本。其他事务仍然可以访问旧版本的数据,这样避免了对数据的阻塞。
- 事务视图:MVCC通过维护每个事务可见的数据版本来实现并发控制
- 快照隔离(Snapshot Isolation):在MVCC中,事务看到的是一个数据快照,保证事务在执行期间的数据一致性
乐观并发控制(Optimistic Concurrency Control, OCC):
假设事务之间不会发生冲突,因此事务执行时无需锁定数据,事务在执行结束后,通过检查数据是否被其他事务修改来决定是否提交。如果发生冲突,事务会回滚并重新执行