基本概念和作用
什么是关系型数据库?
关系型数据库是信息的集合,它以预定义的关系组织数据,数据存储在一个或多个由列和行构成的表(或"关系")中,用户可以轻松查看和理解不同数据结构之间的关系。关系是不同表之间的逻辑连接,根据这些表之间的交互建立。
我们可以将关系型数据库视为一个电子表格文件集合,用来管理和关联数据。在关系型数据库模型中,每个"电子表格"都是一个存储信息的表,表示为列(特性)和行(记录或元组)。
特性(列)指定了数据类型,并且每条记录(或每行)都包含该特定数据类型的值。关系型数据库中的所有表都有一个称为主键 的特性(该特性是行的唯一标识符),并且每行都可以用于使用外键(对另一个现有表的主键的引用)创建不同表之间的关系。
以下关系型数据库模型的实际运用方式:
假设有一个客户 表和一个订单表。

客户表包含有关客户的数据:
- 客户 ID(主键)
- 客户名称
- 帐单邮寄地址
- 送货地址
在客户表中,客户 ID 是唯一标识关系型数据库中客户的主键。客户 ID 各不相同。
订单表包含有关订单的交易信息:
- 订单 ID(主键)
- 客户 ID(外键)
- 订单日期
- 发货日期
- 订单状态
在这里,标识特定订单的主键是订单 ID。我们就可以使用外键来关联客户表中的客户 ID,从而将客户与订单关联起来。比如说:可以生成一份有关在特定日期下单的所有客户的报告,或者找出上个月的订单送达日期有所推迟的客户。
由于数据以预定义关系的形式组织,因此可以声明性地查询数据。声明式查询是一种定义要从系统中提取哪些内容的方法,无需指明系统应如何计算结果。这是关系型系统不同于其他系统的核心。
数据表的结构和组成要素
- 表名:每个数据表都有一个唯一的名称,用于标识该表。
- 列(字段):数据表是由一列列的数据字段组成的,每个字段代表表中的一个属性。每个字段都有一个名称和数据类型,用于存储相应的数据。
- 行(记录):数据表中的每一行称为记录,表示一个实例或实体。每一行由多个字段组成,每个字段对应一个属性的值。
- 主键(Primary Key):主键是一列或一组列,可以唯一标识表中的每一行。它具有唯一性和非空性的特点,用于保证数据的完整性和数据行的唯一性。
- 外键(Foreign Key):外键是一个表中的列,引用另一个表的主键。它用于建立表与表之间的关联关系,实现数据的关联查询和数据完整性的维护。
- 索引(Index):索引是一种数据结构,用于提高查询效率。它是基于表中一个或多个列的值创建的,可以加速数据的查找和访问。
- 约束(Constraint):约束是用来保证数据的完整性和一致性的规则。常见的约束包括主键约束、唯一约束、非空约束、默认值约束等。
这些组成要素共同构成了关系型数据库中数据表的结构,通过合理设计和使用这些要素,可以实现数据的存储、查询和管理等功能。
主键(Primary Key)
是用来唯一标识关系表中的每一条记录的一列或一组列。主键具有唯一性和非空性的特点,用于保证数据的完整性和数据行的唯一性。主键的作用包括:
- 唯一标识数据行:主键可以确保每一条记录具有唯一的标识,避免数据冗余和重复。
- 维护数据完整性:主键可以保证每一条数据行都有一个唯一的标识,防止数据的丢失或混乱。
- 建立表与表之间的关联关系:主键可以作为外键的参照对象,用于建立表与表之间的关联关系。
主键的使用方法如下:
- 在创建表时定义主键:在创建表的时候,可以通过在列定义后面加上 PRIMARY KEY 关键字来指定主键。例如:
sql
`CREATE TABLE 表名
(
列1 数据类型 PRIMARY KEY,
列2 数据类型,
...
);`
修改表结构添加主键:如果表已经创建了,也可以通过 ALTER TABLE 语句来添加主键。例如:
ALTER TABLE 表名 ADD PRIMARY KEY (列名);
外键(Foreign Key)
是一个表中的列,引用另一个表的主键。外键用于建立表与表之间的关联关系,实现数据的关联查询和数据完整性的维护。外键的作用包括:
- 建立表与表之间的关联:通过外键,可以将两个或多个表关联起来,实现数据在不同表之间的关联查询和操作。
- 维护数据的一致性:外键可以保证数据在关联表之间的一致性,防止数据的丢失或混乱。
外键的使用方法如下:
- 在创建表时定义外键:在创建表的时候,可以通过在列定义后面加上 FOREIGN KEY 关键字来指定外键,并指定参照的表和列。例如:
sql
`CREATE TABLE 表名
(
列1 数据类型,
列2 数据类型,
...
FOREIGN KEY (列1) REFERENCES 参照表名(参照列名)
);`
- 修改表结构添加外键:如果表已经创建了,也可以通过 ALTER TABLE 语句来添加外键。例如:
sql
`ALTER TABLE 表名 ADD FOREIGN KEY (列名) REFERENCES 参照表名(参照列名);`
索引(INDEX )
在关系数据库中,如果有上万甚至上亿条记录,在查找记录的时候,想要获得非常快的速度,就需要使用索引。
索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。
对于student表:

如果要经常根据score
列进行查询,就可以对score
列创建索引:
sql
ALTER TABLE students
ADD INDEX idx_score (score);
使用ADD INDEX idx_score (score)
就创建了一个名称为idx_score
,使用列score
的索引。索引名称是任意的,索引如果有多列,可以在括号里依次写上,例如:
sql
ALTER TABLE students
ADD INDEX idx_name_score (name, score);/cod
索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。反过来,如果记录的列存在大量相同的值,例如gender
列,大约一半的记录值是M
,另一半是F
,因此,对该列创建索引就没有意义。
可以对一张表创建多个索引。索引的优点是提高了查询效率,缺点是在插入、更新和删除记录时,需要同时修改索引,因此,索引越多,插入、更新和删除记录的速度就越慢。
对于主键,关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的,因为主键会保证绝对唯一。
唯一约束(Unique Constraint)
是用于保证表中某个列的值是唯一的约束条件。唯一约束的作用包括:
- 唯一性约束:唯一约束可以保证表中某个列的值是唯一的,防止数据的冗余和重复。
- 数据完整性:唯一约束可以保证列的值不能为空,保证数据的完整性。
唯一约束的使用方法如下:
- 在创建表时定义唯一约束:在创建表的时候,可以通过在列定义后面加上 UNIQUE 关键字来指定唯一约束。例如:
sql
`CREATE TABLE 表名
(
列1 数据类型 UNIQUE,
列2 数据类型,
...
);`
- 修改表结构添加唯一约束:如果表已经创建了,也可以通过 ALTER TABLE 语句来添加唯一约束。例如:
sql
`ALTER TABLE 表名 ADD UNIQUE (列名);`
常见产品
MySQL、Oracle、SQL Server和PostgreSQL都是流行的关系型数据库管理系统(RDBMS),它们都有自己的特点和适用场景。
-
MySQL:
- MySQL是一个开源的关系型数据库管理系统,以其高性能、可靠性和易用性而闻名。
- 它适用于Web应用程序、小型到中型规模的企业应用和数据仓库等场景。
- MySQL支持事务处理和ACID(原子性、一致性、隔离性和持久性)特性,具有高度可扩展性和可定制性。
-
Oracle:
- Oracle是一个功能强大且广泛使用的商业级关系型数据库管理系统。
- 它适用于大型企业级应用程序和复杂的数据处理任务,具有出色的性能、可扩展性和可靠性。
- Oracle支持高级特性,如复杂的查询优化、分区表、并行处理和数据压缩等。
-
SQL Server:
- SQL Server是由微软开发和维护的关系型数据库管理系统。
- 它适用于Windows平台,广泛应用于企业级应用、Web应用和中小型数据库环境。
- SQL Server具有良好的集成性,与其他微软产品(如.NET框架)集成紧密,并提供了丰富的企业级功能,如可伸缩性、高可用性和安全性。
-
PostgreSQL:
- PostgreSQL是一个开源的关系型数据库管理系统,以其稳定性、可靠性和高级特性而著名。
- 它适用于各种规模的应用程序,从个人项目到大型企业级应用。
- PostgreSQL支持复杂的查询和高级特性,如事务处理、并发控制、外键约束、触发器和存储过程等。
对比:
- 开源性质:MySQL和PostgreSQL是开源的,可以免费使用和定制。而Oracle和SQL Server是商业软件,需要购买许可证。
- 社区支持和生态系统:MySQL和PostgreSQL拥有庞大的开源社区,提供了大量的文档和支持资源。Oracle和SQL Server则有更多的商业支持和专业服务。
- 性能和可靠性:Oracle和SQL Server在处理大规模企业级应用和复杂查询时表现优秀。MySQL和PostgreSQL则在中小型应用和Web应用方面具有出色的性能和可靠性。
- 功能和特性:Oracle和SQL Server提供了广泛的企业级特性和高级功能。MySQL和PostgreSQL也提供了许多功能,如事务处理、触发器和存储过程等,但在某些高级特性方面可能有所限制。
- 数据库生态系统:Oracle、SQL Server和MySQL都有大量的第三方工具和数据库驱动程序可供选择。PostgreSQL的生态系统相对较小,但也有一些常用工具和驱动程序可用。
总结来说,选择适合的数据库管理系统取决于应用需求、性能要求、可靠性需求、数据量和预算等因素。每个数据库系统都有自己的优势和特点,应根据具体情况进行评估和选择。
基本使用
查询数据
基本查询
sql
SELECT * FROM <表名>
条件查询
SELECT语句可以通过WHERE
条件来设定查询条件,查询结果是满足查询条件的记录。例如,要指定条件"分数在80分或以上的学生",写成WHERE
条件就是SELECT * FROM students WHERE score >= 80
。
其中,WHERE
关键字后面的score >= 80
就是条件。score
是列名,该列存储了学生的成绩,因此,score >= 80
就筛选出了指定条件的记录:
sql
SELECT * FROM test.students WHERE score >= 80;
投影查询
如果我们只希望返回某些列的数据,而不是所有列的数据,我们可以用SELECT 列1, 列2, 列3 FROM ...
,让结果集仅包含指定列。这种操作称为投影查询。
例如,从students
表中返回id
、score
和name
这三列:
sql
SELECT id, score, name FROM test.students;
排序
在实际业务中,我们使用SELECT查询时,查询结果集通常需要根据时间、按照id
等进行排序。如果我们要根据其他条件排序怎么办,可以加上ORDER BY
子句。例如按照成绩从低到高进行排序:
vbnet
SELECT id, name, gender, score FROM test.students ORDER BY score;
分页查询
使用SELECT查询时,如果结果集数据量很大,比如几万行数据,放在一个页面显示的话数据量太大,需要分页显示,每次显示100条。
例如:
要实现分页功能,实际上就是从结果集中显示第1100条记录作为第1页,显示第101200条记录作为第2页,以此类推。
因此,分页实际上就是从结果集中"截取"出第M~N条记录。这个查询可以通过LIMIT <N-M> OFFSET <M>
子句实现。
我们把student分页,每页3条记录。要获取第1页的记录,可以使用LIMIT 3 OFFSET 0
:
sql
SELECT id, name, gender, score
FROM test.students
ORDER BY score DESC
LIMIT 3 OFFSET 0;
上述查询LIMIT 3 OFFSET 0
表示,对结果集从0号记录开始,最多取3条。注意SQL记录集的索引从0开始。
可见,分页查询的关键在于,首先要确定每页需要显示的结果数量pageSize
(这里是3),然后根据当前页的索引pageIndex
(从1开始),确定LIMIT
和OFFSET
应该设定的值:
LIMIT
总是设定为pageSize
;OFFSET
计算公式为pageSize * (pageIndex - 1)
。
这样就能正确查询出第N页的记录集。
聚合查询
如果我们要统计一张表的数据量,例如,想查询students
表一共有多少条记录,难道必须用SELECT * FROM students
查出来然后再数一数有多少行吗?
这个方法当然可以,但是比较低效。对于统计总数、平均数这类常见的计算,SQL提供了专门的聚合函数,使用聚合函数进行查询,就是聚合查询,它可以快速获得结果。
仍然以查询students
表一共有多少条记录为例,我们可以使用SQL内置的COUNT()
函数查询:
sql
SELECT COUNT(*) FROM students;
COUNT(*)
表示查询所有列的行数,要注意聚合的计算结果虽然是一个数字,但查询的结果仍然是一个二维表,只是这个二维表只有一行一列,并且列名是COUNT(*)
。
通常,使用聚合查询时,我们应该给列名设置一个别名,便于处理结果:
sql
SELECT COUNT(*) num FROM students;
除了COUNT外,SQL还提供了如下聚合函数:
函数 | 说明 |
---|---|
SUM | 计算某一列的合计值,该列必须为数值类型 |
AVG | 计算某一列的平均值,该列必须为数值类型 |
MAX | 计算某一列的最大值 |
MIN | 计算某一列的最小值 |
分组聚合
如果我们要统计一班的学生数量,我们知道,可以用SELECT COUNT(*) num FROM students WHERE class_id = 1;
。如果要继续统计二班、三班的学生数量,难道必须不断修改WHERE
条件来执行SELECT
语句吗?
对于聚合查询,SQL还提供了"分组聚合"GROUP BY 的功能。我们观察下面的聚合查询:
按照class_id分组:
sql
SELECT class_id, COUNT(*) num FROM students GROUP BY class_id;
多表查询
SELECT查询不但可以从一张表查询数据,还可以从多张表同时查询数据。查询多张表的语法是:SELECT * FROM <表1> <表2>
。
例如,同时从students
表和classes
表的"乘积",即查询数据,可以这么写:结果集的列数是students
表和classes
表的列数之和,行数是students
表和classes
表的行数之积。
sql
SELECT * FROM students, classes;
这种多表查询又称笛卡尔查询,使用笛卡尔查询时要非常小心,由于结果集是目标表的行数乘积,对两个各自有100行记录的表进行笛卡尔查询将返回1万条记录,对两个各自有1万行记录的表进行笛卡尔查询将返回1亿条记录。
连接查询
连接查询是另一种类型的多表查询。连接查询对多个表进行JOIN运算,简单地说,就是先确定一个主表作为结果集,然后,把其他表的行有选择性地"连接"在主表结果集上。
例如,我们想要选出students
表的所有学生信息,可以用一条简单的SELECT语句完成:
sql
SELECT s.id, s.name, s.class_id, s.gender, s.score FROM students s;
但是,假设我们希望结果集同时包含所在班级的名称,上面的结果集只有class_id
列,缺少对应班级的name
列。
现在问题来了,存放班级名称的name
列存储在classes
表中,只有根据students
表的class_id
,找到classes
表对应的行,再取出name
列,就可以获得班级名称。
这时,连接查询就派上了用场。我们使用内连接------INNER JOIN来实现:
sql
SELECT s.id, s.name, s.class_id, c.name class_name, s.gender, s.score
FROM students s
INNER JOIN classes c
ON s.class_id = c.id;
与之对应的还有FULL OUTER JOIN,RIGHT OUTER JOIN,LEFT OUTER JOIN,以及FULL OUTER JOIN,他们之间的区别:
我们把tableA看作左表,把tableB看成右表,那么INNER JOIN是选出两张表都存在的记录:
LEFT OUTER JOIN是选出左表存在的记录:
RIGHT OUTER JOIN是选出右表存在的记录:
FULL OUTER JOIN则是选出左右表都存在的记录:
增、删、改数据
而对于增、删、改,对应的SQL语句分别是:
- INSERT:插入新记录;
- UPDATE:更新已有记录;
- DELETE:删除已有记录。
性能优化
数据库设计优化
范式化
为什么需要范式?
在关系数据库设计中,遵循范式可以减少冗余数据,避免数据不一致和更新异常,提高数据库的可维护性和可靠性。
比如下面这张表,没有考虑规范化设计,将员工姓名、部门、职位等信息全部存储到了同一个表中:

那这样就可能会出现一些数据的异常情况:
-
数据冗余
- 电话里面,会有重复的座机号
-
插入异常
- 比如新成立了一个部门,但是还没有员工,那如果以员工为管理对象的话,就会出现插入异常
-
更新异常
- 比如只有一个员工的部门,如果这个员工转岗或离职,修改它的部门后,之前的部门就没有了
-
删除异常
- 比如财务部的i昂贵个员工都离职了,删除之后,财务部门也会跟着删除掉了,也会有问题
因此我们需要更加规范化的设计数据库。
什么是范式?
范式是数据库设计中的一组规范,用于规范化数据库结构,它通过定义数据库表的结构和关系来减少数据冗余、避免数据异常,并优化数据库的性能。

常见的范式有以下几种:
第一范式(1NF) :确保每个数据项都是原子性的,即每个字段只包含一个值。同时表中需要有一个主键,这样可以消除数据重复和数据冗余
下面表中部门名称和姓名构成了一个复合主键,但是电话列,是可以拆分的,不符合第一范式:
我们需要将其拆分

第二范式(2NF) :首先需要满足第一范式,非主键字段完全依赖于主键,不能只依赖主键的一部分,如果有部分依赖,则将其移动到单独的表。
现在表中的【部门地址】,是只依赖【部门名称】的,也就是部份依赖主键,所以不符合第二范式:
我们就需要把它拆分出来,组成一个新的表,然后用部门编号和工号分别去做主键:

第三范式(3NF) :首先需要满足第二范式,确保表中的字段只依赖于主键或其他非主键字段。如果存在传递依赖,则将其移动到单独的表中,以避免数据冗余。
现在表中的【最低月薪】和【最高月薪】会依赖非主键【职位】,不符合第三范式,需要单独拆分出来:
此外,还有更高级的范式,如BCNF(Boyce-Codd范式)和第四范式(4NF),用于解决数据依赖和多值依赖等更复杂的问题。
BCNF
满足第三范式,且主键之间不能存在依赖关系
以下表格中,主表的主键是公寓分区和公寓房间,不存在依赖关系,符合BCNF
第四范式
4NF它建立在BCNF的基础上,要求关系模式R中的每个非平凡多值依赖都需要满足以下两个条件:
- R中不存在任何非平凡的函数依赖X→A,其中X是R的候选键,A是R的非主属性。
- R中的每个非平凡多值依赖X→Y都满足以下条件:对于X的每个可能的取值,Y只有一个可能的取值。
非主键之间相对独立,某一非主键不能出现多值的情况,不然会导致另一非主键的冗余
表中丁有两个手机,这种情况就是冗余的,需要把手机号拆出来,就完成了4NF的规范:
范式通过定义数据库表的结构和关系来减少数据冗余、避免数据异常,并优化数据库的性能。设计规范化的数据库结构,能确保数据存储的一致性、完整性和可靠性。
范式的缺点:
- 查询数据性能降低(需要多个表关联才能查询)
反范式化
反范式化是相对范式化而言的,反范式化是为了性能和读取效率而适当地违反范式要求。
它允许存在少量冗余,也就是用空间来换取时间
例如:
创建一个范式化的结构user和order
sql
-- 创建用户表
CREATE TABLE users (
user_id INT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(50)
);
-- 创建订单表
CREATE TABLE orders (
order_id INT PRIMARY KEY,
user_id INT,
order_date DATE,
total_amount DECIMAL(10, 2),
FOREIGN KEY (user_id) REFERENCES users(user_id)
);
创建一个反范式的结构denormalized_data
sql
-- 创建反范式化表
CREATE TABLE denormalized_data (
user_id INT PRIMARY KEY,
username VARCHAR(50),
email VARCHAR(50),
order_id INT,
order_date DATE,
total_amount DECIMAL(10, 2)
);
查询:
sql
-- 查询用户的所有订单(范式化)
SELECT u.user_id, u.username, o.order_id, o.order_date, o.total_amount
FROM users u
JOIN orders o ON u.user_id = o.user_id;

sql
-- 查询用户的所有订单(反范式化)
SELECT user_id, username, order_id, order_date, total_amount
FROM denormalized_data;

但除非必要,一般不建议反范式化,因其代价高昂:
反范式化的缺点:
- 失去了数据完整性保障:打破范式,意味着之前通过范式化解决的更新、插入、删除异常问题又将重新冒出来,也就是说,冗余数据的一致性要靠 DBA 自己来保证,而不像索引视图等由 DBMS 来保证
- 牺牲了写入速度:由于反范式化引入了冗余数据,更新时要修改多处,但大多数场景都是读密集的,写入慢一点问题不大
- 浪费了存储空间:存储了不必要的冗余数据,自然会浪费一些存储空间,但空间换时间一般是可接受的(毕竟内存、硬盘等资源已经相对廉价了)
范式与反范式适用场景
范式化和反范式化是数据库设计中的两种不同的策略,用于优化数据存储和查询性能。适用范式化还是反范式化取决于具体的应用需求和查询模式。
范式化的适用场景:
- 数据一致性要求高:范式化可确保数据的一致性,通过将数据分解为更小的表,并使用外键关联这些表,确保每个表中的数据保持一致。
- 数据更新频繁:如果数据经常发生更新,范式化可以减少数据冗余,减少了更新时需要修改的数据量,提高了更新的效率。
- 存储空间相对较大:范式化将数据分解为多个表,避免了数据的冗余存储,能够节省存储空间。
- 数据要求灵活:范式化的设计可以更容易地处理不同类型的查询,并支持更复杂的查询操作。
反范式化的适用场景:
- 查询性能要求高:反范式化通过将相关的数据存储在一起,避免了多表关联查询,提高了查询性能。特别是对于复杂的查询和大数据量的查询,反范式化可以显著提升性能。
- 数据读取频繁:如果应用中的查询操作远远超过更新操作,反范式化可以减少查询时需要关联的表数量,提高查询速度。
- 存储空间相对较小:反范式化将数据冗余存储,避免了频繁的表关联操作,但会占用更多的存储空间。在存储空间相对较小的情况下,反范式化可以减少查询时的磁盘IO开销,提高查询性能。
实际开发中,建议根据业务特性来设计数据库。
需要注意的是,选择范式化还是反范式化并不是绝对的,取决于具体的应用需求和查询模式。在进行数据库设计时,需要权衡不同的因素,并根据实际情况做出决策。有时候也可以采用混合的策略,即将部分数据范式化,部分数据反范式化,以兼顾数据一致性和查询性能的需求。
数据库参数调优
数据库参数调优是提高数据库性能的一个重要手段。常见的包含以下几点:
查询缓存优化
如果数据库执行大量的重复查询,可以通过增加查询缓存来提高查询性能。
但是在mysql8.0.0之后官方移除了查询缓存。主要原因是:
Query Cache设计之初,MySQL希望可以利用查询缓存,提升查询的效率,在执行每一次SELECT的时候,MySQL都会首先经过 **Query Cache
**区域,检查查询是否可以命中缓存,如果命中,则直接返回结果集,相较于从硬盘(Disk)检索数据,直接从内存(RAM)中获取数据集无疑是极为高效的,可以大大的节省查询执行的时间。
但是存在以下问题:
1、Query Cache
对SQL语句的缓存基于字节级别,任意一点点的改动也无法命中。
2、Query Cache
的淘汰策略过于苛刻,任何对于表中数据的修改,都会使得缓存失效。在开发场景下比较不易达到;
3、查询请求没有命中Query Cache
时,MySQL会需要额外的性能开销去处理结果集,写入Query Cache
中,这个额外的开销是**13%**左右。
并发参数调优
在Mysql中,控制并发连接和线程的主要参数包括 max_connections、back_log、 thread_cache_size、table_open_cahce。
-
max_connections
: 允许连接到MySQL数据库的最大数量,默认值是 151。sqlSHOW VARIABLES LIKE '%max_connections%';
-
table_open_cache
:该参数用来控制所有SQL语句执行线程可打开表缓存的数量, 而在执行SQL语句时,每一个SQL执行线程至少要打 开 1 个表缓存。 -
thread_cache_size
: 可控制 MySQL 缓存客户服务线程的数量。 -
innodb_read_io_threads
** 和****innodb_write_io_threads
**:这两个参数分别控制InnoDB读和写I/O操作的线程数。
索引优化
没有索引
我们通过批量插入20万条数据在test_user表中,并查询数据
csharp
select * from test_user where phone='15784722262' and lan_id=414 and region_id=33;

耗时55毫秒

联合索引
建立联合索引phone,lan_id,region_id
sql
alter table test_user add index idx_phone_lan_region(phone,lan_id,region_id);
再次查询,耗时只剩下1毫秒

联合索引可以根据查询中的多个条件更精确地定位数据。如果查询涉及到多个列,联合索引可以比单列索引更精确地筛选出符合条件的数据,从而减少了搜索的数据量。
覆盖索引
当一个索引包含(或覆盖)所有在查询中需要从数据库表中检索的字段,那么这就是一个覆盖索引 。覆盖索引可以极大地提高查询性能。因为访问索引通常比访问原数据行要快得多,特别是当原数据行的大小比索引的大小要大得多或者分散在硬盘的不同位置时。因为索引的大小通常比整个表的大小要小,索引也是有序的,所以这样的查询会更快。
但是注意,并非所有查询都可以使用覆盖索引,只有当查询的列都被索引覆盖时,才能使用覆盖索引。
sql
select * from test_user where phone='18711539812' and lan_id=680 and region_id=77;
-- 覆盖索引
select phone, lan_id, region_id from test_user where phone='18711539812' and lan_id=680 and region_id=77;

通过比较,可以发现覆盖索引也优化了查询速度。从1.2ms提升到了0.4ms.

索引排序
在数据库中,索引不仅用于快速查找特定行,而且可以被用于对数据进行排序。当执行一个有排序需求的查询(例如包含 ORDER BY 子句的查询)时,如果排序的列有索引,数据库可以利用索引的有序性直接返回排序好的数据,而不需要进行额外的排序操作。
我们直接查询语句,耗时86ms
sql
SELECT create_time from test_user where create_time BETWEEN '2023-11-17 00:18:37' AND '2023-11-17 10:58:58' order by create_time
这是为create_time字段建立索引,利用B+树索引的天然有序性完成排序功能,无需把数据加载到内存进行排序。
建立索引:
sql
alter table test_user add index index_create_time(create_time);
在进行查询,耗时14ms。

数据库就可以直接使用createTime的索引进行排序,而不需要对结果集进行额外的排序操作。这样可以大大提高查询的效率。
注意,索引排序并不总是最优的选择。在某些情况下,数据库可能会选择其他的执行计划,比如全表扫描或者使用其他索引。
索引使用原则
使用索引可以提高数据库查询的性能,但并不是所有的场景都适合使用索引。以下是一些索引使用的原则:
- 选择性原则 :如果一个字段的值大部分都是唯一的,那么对这个字段建立索引会有很高的查询效率。相反,如果一个字段的值大部分都相同,如性别字段,那么对这个字段建立索引效果就不明显。
- 频繁查询和更新的原则 :对于经常需要查询的字段,可以考虑添加索引 。但是,如果一个字段经常被修改,那么索引反而可能会降低性能,因为每次数据变动都需要更新索引。
- 查询条件和排序字段原则 :在常用的查询条件和排序字段上创建索引,可以提高查询速度和排序速度。
- 主键索引原则 :数据库表的主键字段上通常都会创建索引,这是因为主键字段的值是唯一的,且经常被用作查询条件。
- 覆盖索引原则 :如果一个查询可以通过索引获取全部需要的数据,那么这个查询就是一个覆盖查询。
- 避免过度索引:索引并不是越多越好,过多的索引会占用更多的磁盘空间,并且在插入和修改数据时需要更新索引,这会降低写操作的性能。
事务管理
什么是事务
常见的例子就是银行转账,A账户给B账户转账一个亿。在这种交易的过程中,有几个问题值得思考:
- 如何同时保证上述交易中,A账户总金额减少一个亿,B账户总金额增加一个亿?** 原子性**
- A账户如果同时在和C账户交易,如何让这两笔交易互不影响? 隔离性
- 如果交易完成时数据库突然崩溃,如何保证交易数据成功保存在数据库中? 持久性
- 如何在支持大量交易的同时,保证数据的合法性(没有钱凭空产生或消失) ? 一致性
如何保证交易的正常可靠,数据库就得解决以上的四个问题。这就是事务诞生的背景。其中,
- 原子性(Atomicity) :原子性指数据库事务是一个不可分割的操作单元,要么全部执行成功,要么全部回滚到初始状态。
- 一致性(Consistency) :一致性指数据库事务在开始和结束时,数据必须满足预定义的一致性约束。
- 隔离性(Isolation) :隔离性指数据库事务的执行是相互隔离的,事务之间的操作互不干扰。隔离性能够防止并发事务导致的数据冲突和不一致性。
- 持久性(Durability) :持久性指一旦事务提交,其对数据库的修改将永久保存,并且能够经受系统故障的考验。
如何保证ACID?
想要理解ACID的实现,先要理解WAL。
首先我们介绍一下WAL机制(Write-Ahead Logging),这是数据库系统的一个基本概念。用于确保数据持久性和一致性,以及故障时的恢复机制。 (先日志后操作)
WAL是数据库所有更改的顺序记录。使用WAL的系统中,所有的修改在提交之前都写入log文件,在通过log文件中记录的日志执行真正的操作。如果事务失败,WAL记录会被忽略,撤销修改。
下面我们可以以mysql为例,去了解一下mysql是如何实现ACID的。
mysql的日志有很多种,如二进制日志、错误日志、查询日志、慢查询日志等。此外InnoDB还提供了两种事务日志:redo log
(重做日志)和undo log
(回滚日志)。
原子性
所谓原子性就是一个事务是不可分割的,要么全部成功,要么全部回滚。
这里主要用到的就是undo log
。当事务对数据库进行修改的时候,会生成对应的undo log;如果事务执行失败或调用了rollback,InnoDB会根据undo log的内容做与之前相反的工作:对于每个插入,回滚时会执行删除;对于每个删除,回滚时会执行插入等
sql
START TRANSACTION;
UPDATE bank SET balance = balance - 500 WHERE account_id = 1;
-- 错误语句
delete from non_existent_table where id = 1;
UPDATE bank SET balance = balance + 500 WHERE account_id = 2;
COMMIT;
执行以上事务我们会发现,结果如下:

我们发现还是会进行第一条update的操作。并在第二句操作进行中断。
我们所说的mysql事务的原子性是指它具备这样的能力。成功时通过commit来完成事务,失败时通过rollback来撤销事务。而不意味着mysql在遇到失败时会自动回滚事务。
因此我们需要根据错误来处理回滚。
sql
START TRANSACTION;
UPDATE bank SET balance = balance - 500 WHERE account_id = 1;
rollback;
-- 错误语句
delete from non_existent_table where id = 1;
UPDATE bank SET balance = balance + 500 WHERE account_id = 2;
COMMIT;

持久性
持久性的主要实现依靠的是redo log
。首先我们来了解一些redo log出现的原因。
InnoDB作为MySQL的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘IO,效率会很低。为此,InnoDB提供了缓存 (Buffer Pool),Buffer Pool中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:当从数据库读取数据时,会首先从Buffer Pool中读取,如果Buffer Pool中没有,则从磁盘读取后放入Buffer Pool;当向数据库写入数据时,会首先写入Buffer Pool,Buffer Pool中修改的数据会定期 刷新到磁盘中(这一过程称为刷脏)。
如果MySQL宕机,而此时Buffer Pool中修改的数据还没有刷新到磁盘,导致数据的丢失怎么办?
**redo log
**被引入来解决这个问题。

当数据修改时,除了修改Buffer Pool中的数据,还会在redo log记录这次操作;当事务提交时,会调用fsync接口对redo log进行刷盘。如果MySQL宕机,重启时可以读取redo log中的数据,对数据库进行恢复。redo log采用的是WAL,所有修改先写入日志,再更新到Buffer Pool,保证了数据不会因MySQL宕机而丢失,从而满足了持久性要求。
隔离性
隔离性主要侧重于事务之间的相互隔离,并发执行,不相互影响。隔离性主要通过锁机制和**多版本并发控制(MVCC)**来实现。首先我们了解一下隔离级别。
隔离级别
在并发的情况下,读操作可能存在以下三类问题。
- 脏读,事务读物的数据为另一个事务未提交的数据。以账户举例
时间 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 将用户A的余额从0改为100 | |
T3 | 查询用户A的余额为100(脏读) | |
T4 | 提交事务 |
- 不可重复度:事务在先后两次读取的同一个数据不一致。以账户举例
时间 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 查询用户A余额为100 | |
T3 | 修改用户A的余额从100到200 | |
T4 | 提交事务 | |
T5 | 查询用户A的余额为200(不可重复读) |
- 幻读:事务按某个条件先后两次查询数据库,查询结果的条数不同。以账户举例
时间 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 查询用户id在0-3之间的用户余额:id为1的用户余额为100 | |
T3 | 插入新用户id为2的用户余额为200 | |
T4 | 提交事务 | |
T5 | 查询用户id在0-3之间的用户余额:id为1的用户余额为100;id为2的用户余额为200(幻读) |
针对以上存在的问题。SQL中定义了4种隔离级别。一般来说隔离级别越低,系统开销越小,支持的并发越高,隔离性越差。在大多数数据库系统中,默认的隔离级别是读已提交(如Oracle)或可重复读(如 mysql)

我们可以通过一下命令查看数据库的默认隔离级别以及相应修改隔离级别。
sql
SHOW VARIABLES LIKE 'transaction_isolation';
SET SESSION TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE];

下面我们可以通过一个demo取测试一下可重复度的隔离级别对不可重复度
问题的修缮。
事务A:
ini
start transaction;
select * from bank where account_id= 1;
事务B:
ini
start transaction;
update bank set balance = 9999 where account_id = 1;
commit;
事务A重新读取发现账户余额未变。

如果我们将mysql的隔离级别降到Read Commited
,会发现并发读取的不可重复读
问题会再次出现。

锁机制
对于两个事务写操作之间的隔离主要是通过锁机制实现的。要求同一时刻只能有一个事务对数据进行写操作。
原理:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。
事务A:
ini
start transaction;
update bank set balance = 9999 where account_id = 1;

其中lock_type为RECORD,代表锁为行锁(记录锁);lock_mode为X,代表排它锁(写锁)。
此时事务B进行操作时,要等待事务A释放。

通过对InnoDB不同锁类型的特性分析,可以利用锁解决脏读、不可重复读、幻读:
- 排它锁解决脏读 :事务A在对数据修改加排它锁,但直到事务 commit 时才释放锁。同时进行的事务B希望读取同一行数据时,会被事务A的排它锁堵塞,所以解决了脏读的问题。
- 共享锁解决不可重复读:事务A读取ID=1这一行数据,然后为ID=1添加共享锁,事务B同时希望update ID=1,此时获取写锁失败,因此在事务A执行完之前,没有其他任何事务可以对ID=1这一行做修改,因此解决了重复读的问题
- 临键锁解决幻读
虽然共享锁和排它锁解决了事务隔离的并发问题,但锁会导致大量的堵塞,性能下降。某些时候会造成死锁,因此需要更高效的方式实现事务的隔离级别,也就是 MVCC 多版本并发控制。
MVCC
MVCC(Multi-Version Concurrency Control),即多版本的并发控制协议。他的主要特点是:在同一个时刻,不同的事务读取到的数据可能是不同的。举例如下:
时间 | 事务A | 事务B | 事务C |
T1 | 开始事务 | 开始事务 | 开始事务 |
T2 | 查询id为1的用户余额为100 | ||
T3 | 修改id为1的用户余额为200 | ||
T4 | 提交事务 | ||
T5 | 查询id为1的余额为100 | 查询id为1的用户余额为200 |
MVCC最大的优点就是读不加锁,因此读写不冲突,并发性能较好。要实现MVCC需要让多个版本的数据共存。即id为1的用户的余额不版本值不同,但是都可共存。主要的实现原理如下:
-
每一行的数据中包含了隐藏列,隐藏列中包含了数据的
事务id
(trx_id)以及执行undo log的指针
(roll_pointer)等信息。 -
而每条
undo log
也会指向更早版本的undo log
,从而形成了一条版本链。 -
通过版本链,MySQL可以将数据恢复到指定版本;但是具体要恢复到哪个版本,则需要根据ReadView来确定。
-
所谓ReadView,是指事务在某一时刻给整个事务系统(trx_sys)打快照,之后再进行读操作时,会将读取到的数据中的事务id与trx_sys快照比较,从而判断数据对该ReadView是否可见。其中ReadView包含以下几个重要属性
- rx_ids : 当前系统中那些活跃(未提交)的读写事务ID , 它数据结构为一个List。(
重点注意
:这里的trx_ids中的活跃事务,不包括当前事务自己和已提交的事务) - low_limit_id: 目前出现过的最大的事务ID+1,即下一个将被分配的事务ID。
- up_limit_id: 活跃事务列表trx_ids中最小的事务ID。
- creator_trx_id: 表示生成该 ReadView 的事务的事务id。
- rx_ids : 当前系统中那些活跃(未提交)的读写事务ID , 它数据结构为一个List。(
可见性判断:

下面我们以可重复读的隔离级别为例来解释一下MVCC怎么实现对脏读、不可重复读以及幻读等问题的解决。
- 脏读
时间 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 将用户A的余额从0改为100 | |
T3 | 查询用户A的余额为0(避免脏读) | |
T4 | 提交事务 |
事务A在T3时刻之前,生成ReadView,此时事务B处于活跃状态且没有提交。因此事务B的id应该存在ReadView的trx_ids
中,是不可见的,事务A根据undo log的指针会找到上一版本的数据,得到的账户余额是0而非100。
- 不可重复读
时间 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 查询用户A余额为100 | |
T3 | 修改用户A的余额从100到200 | |
T4 | 提交事务 | |
T5 | 查询用户A的余额为100(避免不可重复读) |
事务A在T2读余额之前生成ReadView,此时如果事务B开始还没提交,B事务的id也会存在trx_ids
中;如果事务B还没开始,此时事务A的id(+1)大于等于low_limit_id,因此事务B的修改对于ReadView都是不可见的。此时读取的余额为100。
在T5时刻再次读取的时候,会根据T2时刻生成的ReadView进行判断。因此事务B的修改还是不可见的。会根据undo log查询上一版本的值。
- 幻读
时间 | 事务A | 事务B |
T1 | 开始事务 | 开始事务 |
T2 | 查询用户id在0-3之间的用户余额:id为1的用户余额为100 | |
T3 | 插入新用户id为2的用户余额为200 | |
T4 | 提交事务 | |
T5 | 查询用户id在0-3之间的用户余额:id为1的用户余额为100;(避免幻读) |
当事务A在T2时刻读取0<id<3的用户余额前,会生成ReadView。此时事务B分两种情况讨论,一种是如图中所示,事务已经开始但没有提交,此时其事务id在ReadView的trx_ids中;一种是事务B还没有开始,此时其事务id大于等于ReadView的low_limit_id。无论是哪种情况,根据前面介绍的规则,事务B的修改对ReadView都不可见。
当事务A在T5时刻再次读取0<id<3的用户余额时,会根据T2时刻生成的ReadView对数据的可见性进行判断,从而判断出事务B的修改不可见。因此对于新插入的数据,事务A根据其指针指向的undo log查询上一版本的数据,发现该数据并不存在,从而避免了幻读。
一致性
一致性确保数据库在事务之前和事务之后保持状态一致。
- 数据的完整性: 实体完整性、列完整性(如字段的类型、大小、长度要符合要求)、外键约束等
- 业务的一致性:例如在银行转账时,不管事务成功还是失败,双方钱的总额不变。
其实数据一致性是通过事务的原子性、持久性和隔离性来保证的:
备份和恢复
在开发和管理应用时,数据备份和回复时十分重要的。
数据库备份是将数据库的数据和结构复制到另一个位置或存储介质的过程。数据库备份的重要性在于:
- 数据恢复:当数据库发生故障、数据损坏或人为错误时,备份可以用于恢复数据到之前的状态。
- 灾难恢复:在面临灾难性事件(如硬件故障、自然灾害)时,备份可以用于恢复整个数据库,确保业务的连续性。
- 测试和开发:备份可以用于创建测试和开发环境,使开发人员能够在真实数据的副本上进行安全的实验和开发工作。
全备份与恢复
全备份是备份数据库中的所有数据。这种备份最简单,也最为彻底,但需要的存储空间和备份时间都是最大的。
备份实现:备份demo数据库
css
-- 备份数据库
mysqldump -u root -p demo > demo.sql
-- 备份具体的表
mysqldump -u root -p demo bank > bank.sql
恢复实现:
shell
-- demo1为新的数据库名称
mysql -uroot -p demo1 <demo.sql
mysql> use demo;
mysql> source bank.sql;

值得注意的是:
mysqldump可以备份数据库的结构和数据但是无法备份用户、权限 等信息。同时mysqldump备份的过程中是锁表的,会导致对应的表无法对外服务。
增量备份与恢复
增量备份是只备份自上次完全备份或增量备份以来发生更改的部分数据。增量备份可以减少备份所需的时间和存储空间。增量备份主要依赖的是bin-log日志。
- 首先检查数据库的日志功能是否开启
sql
show variables like '%log_bin%';
- 如果没开启,配置my.ini文件,并重启服务
c
log_bin = /log/mysql-bin.log

- 查看当前记录日志的文件
ini
show master status;

- 记录当前日志对应的sql文件
matlab
mysqlbinlog "C:/ProgramData/MySQL/MySQL Server 8.0/Data/HUWENLI-P-bin.000015" > log2.sql
-
修改数据库中记录
-
删除记录
-
根据之前的日志生成的sql进行增量恢复。
cssmysql -u root -p < log2.sql
-
结语
关系型数据库是信息化管理系统的基础与核心,对于数据的存储、处理和管理起着至关重要的作用。随着云计算的普及,越来越多的关系型数据库开始向云原生模式转变。越来越多的数据库也开始支持多种数据模型,比如关系模型、文档模型、图模型等。这种多模型数据库可以更好地满足复杂的数据处理需求。同时Nosql、NewSql等新性数据库也开始出现,具体使用哪种数据库,取决于应用需求。