数据库知识点
数据库面试
事务
事务的概念
事务(Transaction)是一个操作序列,不可分割的工作单位,以begin/start transaction
开始,以 rollback/commit 结束。
Rollback在事务提交成功后不会修改数据库的内容。但对create drop select无效(没有意义)。值得注意的是,事务回滚rollback后,自增id依然自增,因为innodb的auto_increament的计数器记录的当前值是保存在内存中的,并不是存在于磁盘上,当mysql server处于运行的时候,这个计数值只会随着insert增长,不会随着delete而减少。而当mysql server启动时,当我们需要去查询auto_increment计数值时,mysql便会自动执行:SELECT MAX(id) FROM 表名 FOR UPDATE;语句来获得当前auto_increment列的最大值,然后将这个值放到auto_increment计数器中。所以就算Rollback MySQL的auto_increament计数器也不会作负运算。
commit会修改数据库内容。
1 | # 1. 事务的隐式定义 |
事务的四个特性(ACID)
A原子性:事务在逻辑上是不可分割的操作单元。要么所有操作提交成功,要么全部失败回滚。
C一致性:所有的事务对一个数据的读取结果都是相同的。
I隔离性:事务所做的修改在提交前,对其他事务是不可见的。(并发执行的事务之间不能相互影响)
D持久性:一旦事务提交成功,对数据的修改是永久性的。
并发一致性的问题
丢失修改(脏写):一个事务对数据进行了修改,在事务提交之前,另一个事务对同一个数据进行了修改,覆盖了之前的修改。
脏读:多个事务操作时。一个事务A读取了被另一个事务B修改、但未提交的数据,然后用这个数据做了业务处理后B突然回滚了这个数据。
不可重复读:在同一个事务中(还没提交),某查询操作在一个时间读取一行数据和之后一个时间读取该行数据,发现数据已经发生修改。
幻读:当事务在查询多次执行时,由于其他事务在这个数据范围内执行了插入操作,会导致每次返回不同的结果集。(数据条数不同)
产生并发不一致问题的主要原因是破坏了事务的隔离性,解决方法是通过并发控制(事务隔离机制、MVCC 多版本隔离机制、锁机制)来保证隔离性。
数据库的四种隔离级别
- 未提交读:事务中的修改,即使没有提交,对其它事务也是可见的。
Serializable(序列化)
Oracle 默认的事务隔离级别是提交读,在此级别下,通过多版本的控制解决了幻读和不可
重复读。Mysql 显然没有,但可以通过在数据库中设置乐观锁字段或使用悲观锁来控制数
据一致性。
乐观锁和悲观锁
悲观锁
认为数据随时会被修改,因此每次读取数据之前都会上锁,防止其它事务读取或修
改数据;应用于数据更新比较频繁的场景。
悲观锁的实现方式:select … for update
悲观锁的拓展内容
- 需要注意的是,在事务中,只有SELECT … FOR UPDATE 或LOCK IN SHARE MODE 同一笔数据时会等待其它事务结束后才执行,一般SELECT … 则不受此影响。拿上面的实例来说,当我执行select status from t_goods where id=1 for update;后。我在另外的事务中如果再次执行select status from t_goods where id=1 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果我是在第二个事务中执行select status from t_goods where id=1;则能正常查询出数据,不会受第一个事务的影响。
MySQL select…for update的Row Lock与Table Lock
上面我们提到,使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住)。
https://www.cnblogs.com/wxgblogs/p/6849064.html
常见的悲观锁类型
排它锁 / X 锁:事务对数据加上 X 锁时,只允许此事务读取和修改此数据,其他事务不能对该数据加任何锁(select…for update)
共享锁 / S 锁:加了 S 锁后,该事务只能对数据进行读取而不能修改,其他事务只能加 S 锁,不能加 X 锁(select…lock in share mode)
意向锁:(表级锁)
一个事务在获得某个数据行对象的 S 锁之前,必须先获得整个表的 IS 锁或更强的锁;
一个事务在获得某个数据行对象的 X 锁之前,必须先获得整个表的 IX 锁;
IS/IX 锁之间都是兼容的
好处:如果一个事务想要对整个表加 X 锁,就需要先检测是否有其它事务对该表或者该表中的某一行加了锁,这种检测非常耗时。有了意向锁之后,只需要检测整个表是否存在 IX/IS/X/S锁就行了。
乐观锁
操作数据时不会上锁,但是更新时会判断在此期间有没有别的事务更新这个数据,若被更新过,则失败重试;适用于读多写少的场景。
乐观锁的实现方式:加一个版本号或者时间戳字段,每次数据更新时同时更新这个字段;先读取想要更新的字段或者所有字段,更新的时候比较一下,只有字段没有变化才进行更新。
三级封锁协议
一级封锁协议:事务在修改数据之前必须先对其加 X 锁,直到事务结束才释放。可以解决丢失修改问题(两个事务不能同时对一个数据加 X 锁,避免了修改被覆盖);
二级封锁协议:在一级的基础上,事务在读取数据之前必须先加 S 锁,读完后释放。可以
解决脏读问题(如果已经有事务在修改数据,就意味着已经加了 X 锁,此时想要读取数据的
事务并不能加 S 锁,也就无法进行读取,避免了读取脏数据);
三级封锁协议:在二级的基础上,事务在读取数据之前必须先加 S 锁,直到事务结束才能
释放。可以解决不可重复读问题(避免了在事务结束前其它事务对数据加 X 锁进行修改,保
证了事务期间数据不会被其它事务更新)
MVCC(多版本并发控制)
MVCC 在每行记录后面都保存有两个隐藏的列,用来存储创建版本号和删除版本号。
创建版本号:创建一个数据行时的事务版本号(事务版本号:事务开始时的系统版本号;
系统版本号:每开始一个新的事务,系统版本号就会自动递增);删除版本号:删除操作时
的事务版本号;
各种操作:
插入操作时,记录创建版本号;
删除操作时,记录删除版本号;
更新操作时,先记录删除版本号,再新增一行记录创建版本号;
查询操作时,要符合以下条件才能被查询出来:删除版本号未定义或大于当前事务版本号(删除操作是在当前事务启动之后做的);创建版本号小于或等于当前事务版本号(创建操作是事务完成或者在事务启动之前完成)通过版本号减少了锁的争用,提高了系统性能;可以实现提交读和可重复读两种隔离级别,
未提交读无需使用 MVCC
两段锁协议
是指所有的事务必须分两个阶段对数据项加锁和解锁。即事务分两个阶段
,第一个阶段是获得封锁
。事务可以获得任何数据项上的任何类型的锁,但是不能释放;第二阶段是释放封锁
,事务可以释放任何数据项上的任何类型的锁,但不能申请。
第一阶段是获得封锁的阶段,称为扩展阶段
:其实也就是该阶段可以进入加锁操作,在对任何数据进行读操作之前要申请获得S锁
,在进行写操作之前要申请并获得X锁
,加锁不成功,则事务进入等待状态
,直到加锁成功才继续执行。就是加锁后就不能解锁
了。
第二阶段是释放封锁的阶段,称为收缩阶段
:当事务释放一个封锁后,事务进入封锁阶段,在该阶段只能进行解锁而不能再进行加锁操作
。
数据库三大范式
第一范式(1NF,Normal Form)
属性不应该是可分的。
如果数据库表中的所有字段值都是不可分解的原子值,就说明该数据库表满足了第一范式。第一范式的合理遵循需要根据系统的实际需求来定。
eg:学生(学号,姓名,性别,出生年月日),如果业务需要最后一列再分成(出生年,出生月,出生日),它就不是一范式了。
第二范式(2NF)
每个非主属性完全依赖于主属性集(候选键集)
对记录的**唯一性
,要求记录有唯一标识,即实体的唯一性,即不存在部分依赖**;
B 完全依赖于 A,就是说 A 中的所有属性唯一决定 B,属性少了就不能唯一决定,属性多了
则有冗余(叫部分依赖不叫完全依赖)。
第二范式需要确保数据库表中的每一列都和主键相关,而不能只与主键的某一部分相关(主
要针对联合主键而言)。也就是说在一个数据库表中,一个表中只能保存一种数据,不可以
把多种数据保存在同一张数据库表中。
主属性集/候选码集:某一组属性能够唯一确定其它的属性(主键就是从候选键集中选
的一个键),而其子集不能,这样的属性组中的属性就是主属性;不在候选码集中的属
性成为非主属性;
eg:
表:学号、课程号、姓名、学分;
这个表明显说明了两个事务:学生信息, 课程信息;由于非主键字段必须依赖主键,这里学分依赖课程号,姓名依赖与学号,所以不符合二范式。
正确做法:
学生:Student
(学号, 姓名);
课程:Course
(课程号, 学分);
选课关系:StudentCourse
(学号, 课程号, 成绩)。
第三范式(3NF)
在 2NF 的基础上,非主属性不传递依赖于主属性。
第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。
传递依赖:如果 C 依赖于 B,B 依赖于 A,那么 C 传递依赖于 A;
eg:
表: 学号, 姓名, 年龄, 学院名称, 学院电话
因为存在依赖传递: (学号) → (学生)→(所在学院) → (学院电话) 。这样学院电话可能就会成为重复的冗余信息。
正确做法:
学生:(学号, 姓名, 年龄, 所在学院);
学院:(学院, 电话)。
反范式化
一般说来,数据库只需满足第三范式(3NF
)就行了。
没有冗余的数据库设计可以做到。但是,没有冗余的数据库未必是最好的数据库,有时为了提高运行效率,就必须降低范式标准,适当保留冗余数据。具体做法是:在概念数据模型设计时遵守第三范式,降低范式标准的工作放到物理数据模型设计时考虑。降低范式就是增加字段,允许冗余,**达到以空间换时间的目的
**。
〖例〗:如订单表,“金额”这个字段的存在,表明该表的设计不满足第三范式,因为“金额”可以由“单价”乘以“数量”得到,说明“金额”是冗余字段。但是,增加“金额”这个冗余字段,可以提高查询统计的速度,这就是以空间换时间的作法。
UNION和UNION ALL
Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序;
Union All:对两个结果集进行并集操作,包括重复行,不进行排序;
https://www.cnblogs.com/canger/p/9760217.html数据库详细操作
存储过程(procedure)
存储过程是事先经过编译并存储在数据库中的一段 SQL 语句的集合。想要实现相应的功能时,只需要调用这个存储过程就行了(类似于函数,输入具有输出参数)。
存储过程和函数的区别
- 函数有1个返回值,而存储过程是通过参数返回的,可以有多个或者没有
- 函数一般情况下是用来计算并返回一个计算结果;存储过程一般是用来完成特定的数据操作(比如修改、插入数据库表或执行某些DDL语句等等)
eg
in参数例子
1 | DELIMITER // # 声明//为分割符 |
执行结果
1 | 1. mysql > SET @p_in=1; |
以上可以看出,p_in
虽然在存储过程中被修改,但并不影响@p_in
的值
out参数例子
1 | DELIMITER // |
执行结果
1 | 1. mysql > SET @p_out=1; |
INOUT参数例子
1 | DELIMITER // |
执行结果
1 | 1. mysql > SET @p_inout=1; |
存储过程的优缺点
优点
- 预先编译,而不需要每次运行时编译,提高了数据库执行效率;
封装了一系列操作,对于一些数据交互比较多的操作,相比于单独执行 SQL 语句,可以减少网络通信量;
具有可复用性,减少了数据库开发的工作量;
安全性高,可以让没有权限的用户通过存储过程间接操作数据库;
更易于维护
缺点
可移植性差,存储过程将应用程序绑定到了数据库上;(本来应该由程序做的事情交给数据库去做)
开发调试复杂,没有好的 IDE;
修改复杂,需要重新编译,有时还需要更新程序中的代码以更新调用
小型程序完全无用。对于小型web应用来说,它的使用价值就更小了,反而会拖累开发进度。
like:模糊查询
%:表示任意 0 个或多个字符
like:模糊查询
_:代表单个字符
like不区分大小写,如果需要区分加入BINARY
‘%’通配符可以到匹配任意字符,但是不能匹配 NULL。也就是说 “%”匹配不到 tb_students_info 数据表中值为 NULL 的记录。
如果查询内容中包含通配符,可以使用“\”转义符
触发器
触发器(TRIGGER)是由事件(比如INSERT/UPDATE/DELETE)来触发运行的操作(不能被直接调用,不能接收参数)。在数据库里以独立的对象存储,用于保证数据完整性(比如可以检验或转换数据)。
索引失效的情况?
以“%”开头的 LIKE 语句;
OR 语句前后没有同时使用索引;
数据类型出现隐式转化(如 varchar 不加单引号的话可能会自动转换为 int 型);
对于组合索引,必须满足 最左匹配原则/最左前缀原则 (最左优先,eg:多列索引 col1、col2 和 col3,则 索引生效的情形包括 col1 或 col1,col2 或 col1,col2,col3);
如果 MySQL 估计全表扫描比索引快,则不使用索引(比如非常小的表)
在索引字段上使用not,<>,!=。不等于操作符是永远不会用到索引的,因此对它的处理只会产生全表扫描。 优化方法: key<>0 改为 key>0 or key<0。
在哪些地方适合创建索引?
主键和外键(连接两张表的列)上
某列经常作为最大最小值;
经常被查询的字段;
经常出现在 ORDER BY/GROUP BY/DISDINCT 后面的字段
创建索引时需要注意什么?
只应建立在小字段上,而不要对大文本或图片建立索引(一页存储的数据越多一次 IO 操作获取的数据越大效率越高);
建立索引的字段应该非空,在 MySQL 中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。应该用 0、一个特殊的值或者一个空串代替 NULL;
选择数据密度大(唯一值占总数的百分比很大)的字段作索引
索引的分类
一般mysql主键上的就是聚集索引
⚠️聚集索引比起非聚集索引的优缺点
区别
- 聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个
- 聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续
- 聚集索引:物理存储按照索引排序;聚集索引是一种索引组织形式,索引的键值逻辑顺序决定了表数据行的物理存储顺序。
- 非聚集索引:物理存储不按照索引排序;非聚集索引就是普通索引了,仅仅只是对数据列创建相应的索引,不影响整个表的物理存储顺序。
- 索引是通过二叉树的数据结构来描述的,我们可以这么理解聚集索引:索引的叶节点就是数据节点。而非聚集索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。
- 聚集索引插入数据时速度要慢(时间花费在“物理存储的排序”上,也就是首先要找到位置然后插入),查询数据比非聚集数据的速度快。
动作描述 | 使用聚集索引 | 使用非聚集索引 |
---|---|---|
列经常被分组排序 | 应 | 应 |
返回某范围内的数据 | 应 | 不应 |
一个或极少不同值 | 不应 | 不应 |
小数目的不同值 | 应 | 不应 |
大数目的不同值 | 不应 | 应 |
频繁更新的列 | 不应 | 应 |
外键列 | 应 | 应 |
主键列 | 应 | 应 |
频繁修改索引列 | 不应 | 应 |
稠密索引和稀疏索引
稠密索引
数据库中的每个搜索键值都有一个索引记录。这样可以加快搜索速度,但需要更多空间来存储索引记录本身。索引记录包含搜索键值和指向磁盘上实际记录的指针。
稀疏索引
在稀疏索引中,不会为每个搜索关键字创建索引记录。要搜索记录,我们首先按索引记录进行操作,然后到达数据的实际位置。如果索引里没有,那么系统将开始顺序搜索,直到找到所需的数据为止。
- 只有当关系按搜索码排列顺序存储时才能使用稀疏索引,换句话说也就是只有索引是聚集索引时才能使用稀疏索引。
关于组合索引
索引的底层是一颗 B+树,那么组合索引当然还是一颗 B+树,只不过组合索引的健值数量不是一个,而是多个。构建一颗 B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建 B+树。
索引的优缺点,什么时候使用索引,什么时候不能使用索引
索引最大的好处是提高查询速度,缺点是更新数据时效率低,因为要同时更新索引。
对数据进行频繁查询进建立索引,如果要频繁更改数据不建议使用索引。
索引的原理
二叉查找树:左节点的值小于根节点,右节点的值大于根节点。但有可能形成一条链。
红黑树:特殊的二叉查找树,每个节点有红黑两种颜色,根节点和叶子结点都是黑色。如果一个节点是红色的,则它的子节点必须是黑色的。从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。
平衡二叉树(AVL-tree):所有子树高度之差的绝对值不超过1的二叉查找树,有效避免了成链的问题。
- B树:多叉的平衡树,节点大小也是左小右大,并且从左到右递增。假设$$N$$($N>=2$)叉的B树,非叶子结点的字节点数$>1且<=N$ 枝节点的关键字数量大于等于$ceil(m/2)-1$个且小于等于$M-1$个
数据库索引的实现原理(B+树)
B+树:B+树是B树的一个升级版,相对于B树来说B+树更充分的利用了节点的空间,让查询速度更加稳定,其速度完全接近于二分法查找。B+树的非叶子节点只存索引,所以同样大小的磁盘页可以容纳更多的节点元素,所以数据量相同的情况下,B+树更矮胖,所以IO次数更少。
B+树只有叶子节点才会存真正的数据,所以每次查找一定会找到叶子节点,但B树有可能只要找到中间节点就结束了,所以B+树的查找性能比B树更加稳定。
B树的范围查询需要繁琐的中序遍历,但B+树的叶子节点是用链表相连的,所以只需要遍历链表即可。B+树自带排序功能,范围查询简单。
列举几种表连接方式
- 内连接(Inner Join):仅将两个表中满足连接条件的行组合起来作为结果集
- 自然连接:显示属性相同的列,去除重复的属性
- 等值连接:所有属性相同的列都显示,包括重复的
- 外连接(Outer Join)
- 左连接:左边表的所有数据(整个表)都有显示出来,右边的表数据只显示共同有的那部分,没有对应的部分补NULL;
- 右连接:和左连接相反;
- 全外连接(Full Outer Join):查询出左表和右表所有数据,但是去除两表的重复数据
- 交叉连接(Cross Join):返回两表的笛卡尔积(对于所含数据分别为m、n的表,返回m*n的结果)
什么是视图?什么是游标?
视图:从数据库的基本表中通过查询选取出来的数据组成的
虚拟表
(数据库中存放视图的定义)。可以对其进行增/删/改/查等操作。视图是对若干张基本表的引用,一张虚表,查询语句执行的结果,不存储具体的数据(基本表数据发生了改变,视图也会跟着改变);可以跟基本表一样,进行增删改查操作(ps:增删改操作有条件限制);如连表查询产生的视图无法进行,对视图的增删改会影响原表的数据。好处:
- 通过只给用户访问视图的权限,保证数据的安全性;
- 简化复杂的SQL操作,隐藏数据的复杂性(比如复杂的连接);
游标(Cursor):用于定位在查询返回的结果集的特定行,以对特定行进行操作。使用游标可以方便地对结果集进行移动遍历,根据需要滚动或对浏览/修改任意行中的数据。主要用于交互式应用。
关于having
where、聚合函数、having 在from后面的执行顺序:where>聚合函数(sum,min,max,avg,count)>having
若须引入聚合函数来对group by 结果进行过滤 则只能用having。( 是先执行聚合函数还是先过滤 然后比对我上面列出的执行顺序 一看便知)
HAVING语句通常与GROUP BY语句联合使用,用来过滤由GROUP BY语句返回的记录集。
HAVING语句的存在弥补了WHERE关键字不能与聚合函数联合使用的不足。
语法:
1 | SELECT column1, column2, … column_n, aggregate_function (expression) |
如果想查询平均分高于80分的学生记录可以这样写:
1 | SELECT id, COUNT(course) as numcourse, AVG(score) as avgscore |
在这里,如果用WHERE代替HAVING就会出错。
注意事项 :
1、where 后不能跟聚合函数,因为where执行顺序大于聚合函数。
2、where 子句的作用是在对查询结果进行分组前,将不符合where条件的行去掉,即在分组之前过滤数据,条件中不能包含聚组函数,使用where条件显示特定的行。
3、having 子句的作用是筛选满足条件的组,即在分组之后过滤数据,条件中经常包含聚组函数,使用having 条件显示特定的组,也可以使用多个分组标准进行分组。
SQL执行顺序
(1)from
(2) join
(3) on
(4) where
(5)group by
(6) avg,sum…. (组函数)
(7)with rollup/cube(rollup和cube的区别:若group by x,y,rollup会算每个x的total和总的total,cube则会算出每个x的total,每个y的total和总的total,如果group by中有n列,rollup会计算n种组合,那cube就会计算$2^{n}-1$种组合。详细说明)
(8)having
(9) select
(10) distinct
(11) order by
(12) LIMIT/OFFSET
每一步生成一个虚拟表然后将这个虚拟表交给下一步去操作。(order by生成的是游标,所以order by子句的查询不能用于表达式)
MySQL 的两种存储引擎 InnoDB 和 MyISAM 的区别
InnoDB 支持事务,可以进行 Commit 和 Rollback;
MyISAM 只支持表级锁,而 InnoDB 还支持行级锁,提高了并发操作的性能;
InnoDB 支持外键;
MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢;
MyISAM 支持压缩表和空间数据索引,InnoDB 需要更多的内存和存储;
InnoDB 支持热备份
索引的区别
MyISAM
- 不管是主键索引、唯一键索引或者普通索引,其索引都属于稀疏索引
InnoDB
- 若一个主键被定义,该主键则作为密集索引
- 若没有主键被定义,该表的第一个唯一非空索引则作为密集索引
- 若不满足以上条件,innodb内部会生成一个隐藏主键(密集索引)
- 非主键索引(稀疏索引)存储相关键位和它对应的主键值,包含两次查找
热备份和冷备份
热备份:在数据库运行的情况下备份的方法。优点:可按表或用户备份,备份时数据库仍可使用,可恢复至任一时间点。但是不能出错
冷备份:数据库正常关闭后,将关键性文件复制到另一位置的备份方式。优点:操作简单快速,恢复简单
应用场景
- MyISAM 管理非事务表。它提供高速存储和检索(MyISAM 强调的是性能,每次查询具有原子性,其执行速度比 InnoDB 更快),以及全文搜索能力。如果表比较小,或者是只读数据(有大量的 SELECT),还是可以使用 MyISAM;
- InnoDB 支持事务,并发情况下有很好的性能,基本可以替代 MyISAM
奇技淫巧
ifnull函数
1 | IFNULL(expression, alt_value) |
如果第一个参数的表达式 expression 为 NULL,则返回第二个参数的备用值。
LIMIT 和OFFSET
使用limit 可以实现分页比如
limit 0,5 是从1到5条,
limit 5,3 是从,6到8条,
使用limit offset 时
limit 5 offset 0 从 1 到5 条
limit 5 offset 5 从 6 到10 条