[面试] 数据库

178 阅读11分钟

04 数据库

索引

1. 数据库索引

# 作用
  - 根据部分字段的值, 为数据生成一组排好序的数据结构;
  - 通过查找该数据结构, 可以帮助快速找到指定数据的所有字段;
# 种类
  - 全文索引, 只有 MyISAM 引擎支持;
  - 哈希索引, 存储内容是混乱的, 不支持范围查找;
  - B+ 树索引, 主流存储结构, 索引和数据分开存放;
  
  - 唯一索引, 索引字段的值唯一
  - 复合索引, 索引包含多个字段
  
  - 聚集索引, 一种存储方式, 即索引和数据保存在一个文件中(InnoDB)
  - 非聚集索引, 索引和数据保存在不同文件(MyISAM)
# 使用场景
  - 查询操作较多;
  - 插入, 删除, 修改操作较少 (创建和维护索引需要消耗计算资源);

2. B+树

# 数据存储的数据结构
  - 数据库底层使用 B+ 树实现;
# B+ 树
  - 非叶子节点: 只存索引和指针, 不存储数据, 索引来自下游节点的最小索引(冗余);
  - 叶子节点: 存储完整的索引和数据, 之间通过链表结构互相连接, 可以实现升序遍历, 范围查找;
  - 索引和真实数据分开存储, 扩大数据库允许的规模;
  - 在内存中, 只加载非叶子节点(冗余索引和指针);
  - 在磁盘中, 存储叶子节点(索引和数据);
# 查找过程
  - 根据查询语句判断所查字段是否有索引;
  - 在索引节点中用二分查找, 获得下游指针;
  - 通过指针找到下游节点, 直至叶子节点;
  - 在叶子节点中继续用二分查找, 获得结果;
# 其他数据结构
  - 二叉查找树: 
    * 每个节点存一个数据(和索引); 
    * 数据有序插入时不平衡, 效率低;
  - 二叉平衡树(红黑树) : 
    * 每个节点存一个数据(和索引), 子树不平衡时, 进行调整; 
    * 当数据过多时, 树的深度会很高, 效率低;
  - B 树: 
    * 每个节点存多个数据(和索引), 树的深度降低;
    * 要加载数据到内存中, 限制了数据库的规模;
  - Hash 表: 
    * 存储内容是混乱的, 不支持范围查找;
    * 如: 获得 > n 的数据;

3. 联合索引

# 联合索引
  - 将几个字段合并在一起, 作为数据的索引;
  - 在插入 B+ 树时, 是根据字段数据依次比较, 然后插入;
# 最左前缀法则
  - 要求在查询语句中, 指定条件应该安照联合索引的顺序从左到右依次给出;
  - 诺只给出索引中间的一个字段, 查询过程中无法使用索引结构;

数据库引擎

1. MyISAM 和 InnoDB 存储结构

# 存储方式不同 - 聚集和非聚集
  - MyISAM
    * 非聚集: 索引文件和数据文件分开存储;
    * frm 文件: 存储表结构 
    * MYI 文件: 存储索引和数据地址
    * MYD 文件: 存储数据
  - InnoDB
    * 聚集: 索引文件和数据文件存储在一起;
    * frm 文件: 存储表结构
    * ibd 文件: 存储索引和数据

2. MyISAM 和 InnoDB 区别

# 特点
  - INNODB : 当前默认, 安全性高, 支持事务处理, 多表多用户操作
  - MYISAM : 早些年使用, 节约空间, 速度较快
MYISAMINNODB
事务支持不支持支持
数据行锁定不支持 (表锁定)支持 (行锁定)
外键约束不支持支持
全文索引支持不支持
表空间大小较小较大, 约两倍
物理文件*.frm 文件
ibd 文件
* frm 文件
* .MYD 文件
* .MYI 文件

事务

1. 数据库事务

# 事务 ACID 属性
  - 原子性: 事务提交的一系列操作是不可分割的整体, 要么完全执行, 要么完全不执行, 不能只执行一部分;
           (银行一方转钱, 一方收钱必须一起完成)
  - 一致性: 事务完成前后, 逻辑状态保持一致;
           (银行发生转账后, 钱的总量不变)
  - 隔离性: 当一个事务在进行时, 其他事务不会对其进行干扰;
  - 持久性: 在事务进行前后, 数据库内的数据是持久化的, 不会因为外界(断电等)因素造成丢失;

2. 事务异常情况

# 回滚丢失(第一类丢失)
  - 因为其他事务的回滚造成当前事务更新数据的丢失, 如:
    * 原始数据10, 事务A, B均读取到10;
    * 事务A更新到11并提交;
    * 事务B更新失败, 将原数据回滚到10;
    * 导致事务A的更新无效;
# 覆盖丢失(第二类丢失)
  - 因为两个事务先后提交, 后提交的事务数据覆盖了先提交的数据, 如:
    * 原始数据10, 事务A, B均读取到10;
    * 事务A更新到11并提交;
    * 事务B更新到12, 将数据11更新到12;
    * 导致事务A的更新无效;
# 脏读
  - 事务读取到了其他事务未提交前更新的中间数据, 该中间数据后续又因为事务失败而回滚, 造成了脏数据被读取, 如:
    * 原始数据10, 事务A读取到10;
    * 事务A更新到11, 但未提交;
    * 事务B读取到11, 将数据11更新到12;
    * 事务A失败, 原始数据回滚到10;
    * 事务B依然提交12, 导致最终结果包含事务A本应回滚掉的脏数据;
# 不可重复读
  - 同一事务先后读取同一条数据, 但是获得的数据值不一样, 如:
    * 原始数据10, 事务A, B读取到10;
    * 事务B将数据更新到11;
    * 事务A再次读取数据, 获得11, 与原来的10不一样;
# 幻读
  - 同一事务先后更新和读取整个表, 但是发现插入新的数据, 如:
    * 原始表有10条数据, 事务A更新所有数据为0;
    * 事务B将5条新的数据插入到表中;
    * 事务A再次读取数据, 发现15条数据, 其中5条数据没有归0;

3. 事务隔离级别

# 隔离级别
  - 数据库为了防止出现事务异常而设置的机制;
  - 默认隔离级别: Oracle(读已提交), MySql(可重复读)
# 读未提交
  - 最低的事务隔离级别;
  - 允许其他事务在未提交的情况下读;
  - 所有事务异常都不能防止;
# 读已提交
  - 当一个事务要读取数据时, 必须保证数据是已经被提交了;
  - 可以避免回滚丢失, 脏读;
# 可重复读
  - 当一个事务开始后, 先后读取的数据保持一致, 不受其他事务影响;
  - 可以避免不可重复读和覆盖丢失;
# 串行化
  - 最严格的事务隔离级别, 要求所有事务都串行化执行, 不能并发执行;
  - 避免所有事务异常, 但是牺牲了数据库并发访问的性能;

其他

1. 三大范式

# 第一范式 (1NF): 
  - 要求数据库表的每一字段都是不可分割的原子项;
  (如, 家庭信息(人口数,地址) -> 家庭人口, 家庭地址;)
# 第二范式(2NF):
  - 满足1NF;
  - 非主键字段必须完全依赖于主键字段;
  - [ 这里的主键字段一般指联合主键, 数据库表中的每一列都必须和联合主键完全相关,而不能只与联合主键的一部分相关]
  (如满减活动的订单主键一般由 '订单号' + '产品号' 组成, 对于 '产品价格' 或 '产品折扣' 都是和主键完全相关的, 
   但 '订单生成时间', '产品产地' 等信息只与主键一部分相关, 应该拆出两个表)
# 第三范式(3NF)
  - 满足2NF;
  - 任何非主[属性]不依赖于其它非主属性(在2NF基础上消除传递依赖)
  - [ 第三范式需要确保数据表中的每一列数据都和主键直接相关,而不能间接相关。]
  (如, '学号' 和 '班主任工号', '班主任性别', '班主任年龄' 等都是完全依赖的, 但是它们的关系是间接的;
   '班主任性别', '班主任年龄' 直接依赖的是 '班主任工号', 然后才依赖到 '学号', 应该将间接依赖的属性拆分出来)

2. 联接查询

# 左联接 (left join)
  - 返回左表查询字段的所有数据, 以及右表查询字段中满足相等关系的数据;
# 右联接 (right join)
  - 返回右表查询字段的所有数据, 以及左表查询字段中满足相等关系的数据;
# 内联接 (inner join)
  - 只返回两表查询字段中满足相等关系的数据;

3. 外键

# 什么是外键
  - 将另一个表的主键字段关联到本表中的非主键字段, 如; 学生表(主键:学号, 外键:班主任工号) 教师表(主键:工号);
  - 包含外键的表, 被称为'主表'; 被关联的表, 被称为'从表';
  - 级联操作, 当修改某个表时, 另一个表也会改变;
  - 删除时, 必须先删除'从表', 再删除'主表';
# 作用
  - 保持一致性;
  - 减少表容量;
# 最佳实践
  - 生产上, 不建议在工作中使用物理外键, 一切外键概念在应用层解决;
  - 原因: DELETE 和 UPDATE 操作时需要考虑外键约束, 导致开发测试数据不方便; 

4. MVCC 机制

# 多版本并发控制
  - 用于解决不可重复读和一部分幻读;
  - 每个事务事务开始时会对已提交的数据行创建快照;
  - 在事务执行过程中, 事务都对该快照进行读取, 所以并发的事务之间对数据的读取和修改都是隔离的;
  - 每条数据都有创建版本和删除版本, 事务的读取, 更新, 删除和插入都需要根据版本号来修改:
    * 事务读取时, 读取 删除版本 > 当前事务版本, 创建版本 < 当前事务版本 的数据;
    * 事务插入时, 修改 创建版本 = 当前事务版本;
    * 事务删除时, 修改 删除版本 = 当前事务版本
    * 事务更新, 是删除和插入的组合;
# 快照读
  - MVCC 只保证了事务在 select 操作时, 可以并发访问数据, 避免所有问题(包括幻读);
  - 但是, 其他并发操作或非快照读取数据时, 依然有问题(幻读);

SQL 查询排查

1. 获得待优化的SQL语句

# 1. 开启慢查询日志, 设定合适的慢查询阈值
  SET GLOBAL slow_query_log=1;
  SET GLOBAL long_query_time=10;
# 2. 运行数据库, 慢查询被记录到日志文件中
# 3. 使用 mysqldumpslow 慢查询分析工具, 找到查询时间最长/访问次数最多的语句

2. SQL优化方式

# 服务器硬件优化 (磁盘->固态硬盘)
# 服务器系统优化 (windows->Linux)
# SQL本身优化
  - 用连接查询来代替子查询, 可以提高查询效率
    * 子查询将内层查询的结果作为外层查询的输入, 在这过程中, Mysql 需要创建临时表来保存子查询的结果, 并在外层查询结束后销毁临时表, 所以比较耗时;
    * 连接查询是在内存中完成多个表的连接, 并不需要创建临时表, 所以比较高效;
# 反范式化设计优化
  - 数据库的范式化设计
    * 通过拆表的方式, 尽可能地减少了原始数据表中的冗余;
    * 这种方式对数据的增加, 删除, 修改是有益的, 但对查询是没有益处的, 因为这样往往需要联表查询;
  - 反范式化设计
    * 反范式化设计就是允许数据表有适当的冗余, 把冗余字段添加到主表上, 以合并为一个表;
    * 这样用单表查询取代多表查询, 提高了数据读取的效率, 用空间换时间;
# 索引优化
  - 判断是否用到索引
    * 用 explain 分析 SQL 语句
    * 判断是否用到索引 (key非空?)
    * 判断是否完全用到所有索引字段 (计算 key_len==所有索引字段长度?)
  - 策略1: 尽量全值匹配
  - 策略2: 最佳左前缀原则 (查询从索引的最左前列开始并且不跳过索引中的列)
  - 策略3: 不在索引列上做任何操作 (操作会导致索引失效而转向全表扫描)
  - 策略4: 范围条件放最后 (有范围查询会导致后面的索引列全部失效)
  - 策略5: 尽量使用覆盖索引(只访问索引的查询,减少select *)
  - 策略6: 不等于要慎用 (在使用不等于时, 无法使用索引会导致全表扫描)
  - 策略7: 在字段为not null的情况下,使用is null 或 is not null 会导致索引失效
  - 策略8: like+(最前)通配符, 导致索引失效, 变成全表扫描的操作
  - 策略9: 字符串不加单引号索引失效
  - 策略10: OR改union效率高
  
  - 覆盖索引可以避免部分索引失效的问题