黑马程序员mysql优化面试(mysql优化 面试)

发布时间:2022-11-09

本文目录一览:

  1. 「春招系列」MySQL面试核心25问(附答案)
  2. 面试中常问:mysql数据库做哪些优化也提高mysql性能
  3. 什么是Mysql优化?

「春招系列」MySQL面试核心25问(附答案)

篇幅所限本文只写了MySQL25题,像其他的Redis,SSM框架,算法,计网等技术栈的面试题后面会持续更新,个人整理的1000余道面试八股文会放在文末给大家白嫖,最近有面试需要刷题的同学可以直接翻到文末领取。 如果表使用自增主键,那么每次插入新的记录,记录就会顺序添加到当前索引节点的后续位置,当一页写满,就会自动开辟一个新的页。如果使用非自增主键(比如身份证号或学号等),由于每次插入主键的值近似于随机,因此每次新纪录都要被插到现有索引页的中间某个位置,频繁的移动、分页操作造成了大量的碎片,得到了不够紧凑的索引结构,后续不得不通过OPTIMIZE TABLE(optimize table)来重建表并优化填充页面。 Server层按顺序执行SQL的步骤为: 简单概括: 可以分为服务层和存储引擎层两部分,其中:

  • 服务层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖MySQL的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。
  • 存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。现在最常用的存储引擎是InnoDB,它从MySQL 5.5.5版本开始成为了默认的存储引擎。 Drop、Delete、Truncate都表示删除,但是三者有一些差别:
  • Delete 用来删除表的全部或者一部分数据行,执行Delete之后,用户需要提交(commmit)或者回滚(rollback)来执行删除或者撤销删除,会触发这个表上所有的delete触发器。
  • Truncate 删除表中的所有数据,这个操作不能回滚,也不会触发这个表上的触发器,TRUNCATE比Delete更快,占用的空间更小。
  • Drop 命令从数据库中删除表,所有的数据行,索引和权限也会被删除,所有的DML触发器也不会被触发,这个命令也不能回滚。 因此,在不再需要一张表的时候,用Drop;在想删除部分数据行时候,用Delete;在保留表而删除所有数据的时候用Truncate。 隔离级别:脏读、不可重复读、幻影读
  • READ-UNCOMMITTED(未提交读)
  • READ-COMMITTED(提交读)
  • REPEATABLE-READ(重复读)
  • SERIALIZABLE(可串行化读) MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读) 这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 REPEATABLE-READ(可重读)事务隔离级别下使用的是 Next-Key Lock 算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重读)已经可以完全保证事务的隔离性要求,即达到了 SQL标准的SERIALIZABLE(可串行化)隔离级别。 因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是READ-COMMITTED(读取提交内容),但是你要知道的是InnoDB 存储引擎默认使用 REPEATABLE-READ(可重读)并不会有任何性能损失。 InnoDB 存储引擎在分布式事务的情况下一般会用到SERIALIZABLE(可串行化)隔离级别。 主要原因:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。 文件与数据库都是需要较大的存储,也就是说,它们都不可能全部存储在内存中,故需要存储到磁盘上。而所谓索引,则为了数据的快速定位与查找,那么索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数,因此B+树相比B树更为合适。数据库系统巧妙利用了局部性原理与磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次I/O就可以完全载入,而红黑树这种结构,高度明显要深的多,并且由于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性。 最重要的是,B+树还有一个最大的好处:方便扫库。 B树必须用中序遍历的方法按序扫库,而B+树直接从叶子结点挨个扫一遍就完了,B+树支持range-query非常方便,而B树不支持,这是数据库选用B+树的最主要原因。 B+树查找效率更加稳定,B树有可能在中间节点找到数据,稳定性不够。 B+tree的磁盘读写代价更低:B+tree的内部结点并没有指向关键字具体信息的指针,因此其内部结点相对B树更小。如果把所有同一内部结点的关键字存放在同一块盘中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多,相对来说IO读写次数也就降低了。 B+tree的查询效率更加稳定:由于内部结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引,所以,任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。 视图是一种虚拟的表,通常是有一个表或者多个表的行或列的子集,具有和物理表相同的功能。游标是对查询出来的结果集作为一个单元来有效的处理。一般不使用游标,但是需要逐条处理数据的时候,游标显得十分重要。 而在 MySQL 中,恢复机制是通过回滚日志(undo log)实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后在对数据库中的对应行进行写入。当事务已经被提交之后,就无法再次回滚了。 回滚日志作用:
  1. 能够在发生错误或者用户执行 ROLLBACK 时提供回滚相关的信息。
  2. 在整个系统发生崩溃、数据库进程直接被杀死后,当用户再次启动数据库进程时,还能够立刻通过查询回滚日志将之前未完成的事务进行回滚,这也就需要回滚日志必须先于数据持久化到磁盘上,是我们需要先写日志后写数据库的主要原因。

InnoDB vs MyISAM

总结 数据库并发会带来脏读、幻读、丢弃更改、不可重复读这四个常见问题,其中:

  • 脏读:在第一个修改事务和读取事务进行的时候,读取事务读到的数据为100,这是修改之后的数据,但是之后该事务满足一致性等特性而做了回滚操作,那么读取事务得到的结果就是脏数据了。
  • 幻读:一般是T1在某个范围内进行修改操作(增加或者删除),而T2读取该范围导致读到的数据是修改之间的了,强调范围。
  • 丢弃修改:两个写事务T1 T2同时对A=0进行递增操作,结果T2覆盖T1,导致最终结果是1而不是2,事务被覆盖。
  • 不可重复读:T2 读取一个数据,然后T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 第一个事务首先读取var变量为50,接着准备更新为100的时,并未提交,第二个事务已经读取var为100,此时第一个事务做了回滚。最终第二个事务读取的var和数据库的var不一样。 T1 读取某个范围的数据,T2 在这个范围内插入新的数据,T1 再次读取这个范围的数据,此时读取的结果和第一次读取的结果不同。 T1 和 T2 两个事务都对一个数据进行修改,T1 先修改,T2 随后修改,T2 的修改覆盖了 T1 的修改。例如:事务1读取某表中的数据A=50,事务2也读取A=50,事务1修改A=A+50,事务2也修改A=A+50,最终结果A=100,事务1的修改被丢失。 T2 读取一个数据,T1 对该数据做了修改。如果 T2 再次读取这个数据,此时读取的结果和第一次读取的结果不同。

悲观锁 vs 乐观锁

  • 悲观锁:先获取锁,再进行业务操作,一般就是利用类似 SELECT … FOR UPDATE 这样的语句,对数据加锁,避免其他事务意外修改数据。当数据库执行SELECT … FOR UPDATE时会获取被select中的数据行的行锁,select for update获取的行锁会在当前事务结束时自动释放,因此必须在事务中使用。
  • 乐观锁:先进行业务操作,只在最后实际更新数据时进行检查数据是否被更新过。Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。

分库与分表

分库与分表的目的在于,减小数据库的单库单表负担,提高查询性能,缩短查询时间。 通过分表,可以减少数据库的单表负担,将压力分散到不同的表上,同时因为不同的表上的数据量少了,起到提高查询性能,缩短查询时间的作用,此外,可以很大的缓解表锁的问题。分表策略可以归纳为垂直拆分和水平拆分:

  • 水平分表:取模分表就属于随机分表,而时间维度分表则属于连续分表。如何设计好垂直拆分,我的建议:将不常用的字段单独拆分到另外一张扩展表。将大文本的字段单独拆分到另外一张扩展表,将不经常修改的字段放在同一张表中,将经常改变的字段放在另一张表中。对于海量用户场景,可以考虑取模分表,数据相对比较均匀,不容易出现热点和并发访问的瓶颈。
  • 库内分表:仅仅是解决了单表数据过大的问题,但并没有把单表的数据分散到不同的物理机上,因此并不能减轻 MySQL 服务器的压力,仍然存在同一个物理机上的资源竞争和瓶颈,包括 CPU、内存、磁盘 IO、网络带宽等。

分库与分表带来的分布式困境与应对之策

  • 数据迁移与扩容问题:一般做法是通过程序先读出数据,然后按照指定的分表策略再将数据写入到各个分表中。
  • 分页与排序问题:需要在不同的分表中将数据进行排序并返回,并将不同分表返回的结果集进行汇总和再次排序,最后再返回给用户。 不可重复读的重点是修改,幻读的重点在于新增或者删除。 视图是虚拟的表,与包含数据的表不一样,视图只包含使用时动态检索数据的查询;不包含任何列或数据。使用视图可以简化复杂的 sql 操作,隐藏具体的细节,保护数据;视图创建后,可以使用与表相同的方式利用它们。 视图不能被索引,也不能有关联的触发器或默认值,如果视图本身内有order by 则对视图再次order by将被覆盖。 创建视图:create view xxx as xxxx 对于某些视图比如未使用联结子查询分组聚集函数Distinct Union等,是可以对其更新的,对视图的更新将对基表进行更新;但是视图主要用于简化检索,保护数据,并不用于更新,而且大部分视图都不可以更新。 B+tree的磁盘读写代价更低,B+tree的查询效率更加稳定 数据库索引采用B+树而不是B树的主要原因:B+树只要遍历叶子节点就可以实现整棵树的遍历,而且在数据库中基于范围的查询是非常频繁的,而B树只能中序遍历所有节点,效率太低。

B+树的特点

在最频繁使用的、用以缩小查询范围的字段,需要排序的字段上建立索引。不宜:

  1. 对于查询中很少涉及的列或者重复值比较多的列。
  2. 对于一些特殊的数据类型,不宜建立索引,比如文本字段(text)等。 如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。 我们知道在InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次,这样就会比较慢。覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! 举例: 学号 | 姓名 | 性别 | 年龄 | 系别 | 专业 ---|---|---|---|---|--- 20020612 | 李辉 | 男 | 20 | 计算机 | 软件开发 20060613 | 张明 | 男 | 18 | 计算机 | 软件开发 20060614 | 王小玉 | 女 | 19 | 物理 | 力学 20060615 | 李淑华 | 女 | 17 | 生物 | 动物学 20060616 | 赵静 | 男 | 21 | 化学 | 食品化学 20060617 | 赵静 | 女 | 20 | 生物 | 植物学 主键为候选键的子集,候选键为超键的子集,而外键的确定是相对于主键的。

面试中常问:mysql数据库做哪些优化也提高mysql性能

在开始演示之前,我们先介绍下两个概念。 概念一,数据的可选择性基数,也就是常说的cardinality值。 查询优化器在生成各种执行计划之前,得先从统计信息中取得相关数据,这样才能估算每步操作所涉及到的记录数,而这个相关数据就是cardinality。简单来说,就是每个值在每个字段中的唯一值分布状态。 比如表t1有100行记录,其中一列为f1。f1中唯一值的个数可以是100个,也可以是1个,当然也可以是1到100之间的任何一个数字。这里唯一值越的多少,就是这个列的可选择基数。 那看到这里我们就明白了,为什么要在基数高的字段上建立索引,而基数低的的字段建立索引反而没有全表扫描来的快。当然这个只是一方面,至于更深入的探讨就不在我这篇探讨的范围了。 概念二,关于HINT的使用。 这里我来说下HINT是什么,在什么时候用。 HINT简单来说就是在某些特定的场景下人工协助MySQL优化器的工作,使她生成最优的执行计划。一般来说,优化器的执行计划都是最优化的,不过在某些特定场景下,执行计划可能不是最优化。 比如:表t1经过大量的频繁更新操作,(UPDATE,DELETE,INSERT),cardinality已经很不准确了,这时候刚好执行了一条SQL,那么有可能这条SQL的执行计划就不是最优的。为什么说有可能呢? 来看下具体演示 譬如,以下两条SQL, A:

select * from t1 where f1 = 20;

B:

select * from t1 where f1 = 30;

如果f1的值刚好频繁更新的值为30,并且没有达到MySQL自动更新cardinality值的临界值或者说用户设置了手动更新又或者用户减少了sample page等等,那么对这两条语句来说,可能不准确的就是B了。 这里顺带说下,MySQL提供了自动更新和手动更新表cardinality值的方法,因篇幅有限,需要的可以查阅手册。 那回到正题上,MySQL 8.0 带来了几个HINT,我今天就举个index_merge的例子。 示例表结构:

mysql> desc t1;
+------------+--------------+------+-----+---------+----------------+
| Field      | Type         | Null | Key | Default | Extra          |
+------------+--------------+------+-----+---------+----------------+
| id         | int(11)      | NO   | PRI | NULL    | auto_increment |
| rank1      | int(11)      | YES  | MUL | NULL    |                |
| rank2      | int(11)      | YES  | MUL | NULL    |                |
| log_time   | datetime     | YES  | MUL | NULL    |                |
| prefix_uid | varchar(100) | YES  |     | NULL    |                |
| desc1      | text         | YES  |     | NULL    |                |
| rank3      | int(11)      | YES  | MUL | NULL    |                |
+------------+--------------+------+-----+---------+----------------+
7 rows in set (0.00 sec)

表记录数:

mysql> select count(*) from t1;
+----------+
| count(*) |
+----------+
|    32768 |
+----------+
1 row in set (0.01 sec)

这里我们两条经典的SQL: SQL C:

select * from t1 where rank1 = 1 or rank2 = 2 or rank3 = 2;

SQL D:

select * from t1 where rank1 =100 and rank2 =100 and rank3 =100;

表t1实际上在rank1,rank2,rank3三列上分别有一个二级索引。 那我们来看SQL C的查询计划。 显然,没有用到任何索引,扫描的行数为32034,cost为3243.65。

mysql> explain format=json select * from t1 where rank1 =1 or rank2 = 2 or rank3 = 2\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "3243.65"
    },
    "table": {
      "table_name": "t1",
      "access_type": "ALL",
      "possible_keys": [
        "idx_rank1",
        "idx_rank2",
        "idx_rank3"
      ],
      "rows_examined_per_scan": 32034,
      "rows_produced_per_join": 115,
      "filtered": "0.36",
      "cost_info": {
        "read_cost": "3232.07",
        "eval_cost": "11.58",
        "prefix_cost": "3243.65",
        "data_read_per_join": "49K"
      },
      "used_columns": [
        "id",
        "rank1",
        "rank2",
        "log_time",
        "prefix_uid",
        "desc1",
        "rank3"
      ],
      "attached_condition": "((`ytt`.`t1`.`rank1` = 1) or (`ytt`.`t1`.`rank2` = 2) or (`ytt`.`t1`.`rank3` = 2))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

我们加上hint给相同的查询,再次看看查询计划。 这个时候用到了index_merge,union了三个列。扫描的行数为1103,cost为441.09,明显比之前的快了好几倍。

mysql> explain format=json select /*+ index_merge(t1) */ * from t1 where rank1 =1 or rank2 = 2 or rank3 = 2\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "441.09"
    },
    "table": {
      "table_name": "t1",
      "access_type": "index_merge",
      "possible_keys": [
        "idx_rank1",
        "idx_rank2",
        "idx_rank3"
      ],
      "key": "union(idx_rank1,idx_rank2,idx_rank3)",
      "key_length": "5,5,5",
      "rows_examined_per_scan": 1103,
      "rows_produced_per_join": 1103,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "330.79",
        "eval_cost": "110.30",
        "prefix_cost": "441.09",
        "data_read_per_join": "473K"
      },
      "used_columns": [
        "id",
        "rank1",
        "rank2",
        "log_time",
        "prefix_uid",
        "desc1",
        "rank3"
      ],
      "attached_condition": "((`ytt`.`t1`.`rank1` = 1) or (`ytt`.`t1`.`rank2` = 2) or (`ytt`.`t1`.`rank3` = 2))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

我们再看下SQL D的计划: 不加HINT,

mysql> explain format=json select * from t1 where rank1 =100 and rank2 =100 and rank3 =100\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "534.34"
    },
    "table": {
      "table_name": "t1",
      "access_type": "ref",
      "possible_keys": [
        "idx_rank1",
        "idx_rank2",
        "idx_rank3"
      ],
      "key": "idx_rank1",
      "used_key_parts": [
        "rank1"
      ],
      "key_length": "5",
      "ref": [
        "const"
      ],
      "rows_examined_per_scan": 555,
      "rows_produced_per_join": 0,
      "filtered": "0.07",
      "cost_info": {
        "read_cost": "478.84",
        "eval_cost": "0.04",
        "prefix_cost": "534.34",
        "data_read_per_join": "176"
      },
      "used_columns": [
        "id",
        "rank1",
        "rank2",
        "log_time",
        "prefix_uid",
        "desc1",
        "rank3"
      ],
      "attached_condition": "((`ytt`.`t1`.`rank3` = 100) and (`ytt`.`t1`.`rank2` = 100))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

加了HINT,

mysql> explain format=json select /*+ index_merge(t1)*/ * from t1 where rank1 =100 and rank2 =100 and rank3 =100\G
*************************** 1. row ***************************
EXPLAIN: {
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "5.23"
    },
    "table": {
      "table_name": "t1",
      "access_type": "index_merge",
      "possible_keys": [
        "idx_rank1",
        "idx_rank2",
        "idx_rank3"
      ],
      "key": "intersect(idx_rank1,idx_rank2,idx_rank3)",
      "key_length": "5,5,5",
      "rows_examined_per_scan": 1,
      "rows_produced_per_join": 1,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "5.13",
        "eval_cost": "0.10",
        "prefix_cost": "5.23",
        "data_read_per_join": "440"
      },
      "used_columns": [
        "id",
        "rank1",
        "rank2",
        "log_time",
        "prefix_uid",
        "desc1",
        "rank3"
      ],
      "attached_condition": "((`ytt`.`t1`.`rank3` = 100) and (`ytt`.`t1`.`rank2` = 100) and (`ytt`.`t1`.`rank1` = 100))"
    }
  }
}
1 row in set, 1 warning (0.00 sec)

对比下以上两个,加了HINT的比不加HINT的cost小了100倍。 总结下,就是说表的cardinality值影响这张的查询计划,如果这个值没有正常更新的话,就需要手工加HINT了。相信MySQL未来的版本会带来更多的HINT。

什么是Mysql优化?

优化数据库的方法

  1. 选取最适用的字段属性 MySQL可以很好的支持大数据量的存取,但是一般说来,数据库中的表越小,在它上面执行的查询也就会越快。因此,在创建表的时候,为了获得更好的性能,我们可以将表中字段的宽度设得尽可能小。 例如,在定义邮政编码这个字段时,如果将其设置为CHAR(255),显然给数据库增加了不必要的空间,甚至使用VARCHAR这种类型也是多余的,因为CHAR(6)就可以很好的完成任务了。同样的,如果可以的话,我们应该使用MEDIUMINT而不是BIGINT来定义整型字段。 另外一个提高效率的方法是在可能的情况下,应该尽量把字段设置为NOT NULL,这样在将来执行查询的时候,数据库不用去比较NULL值。 对于某些文本字段,例如“省份”或者“性别”,我们可以将它们定义为ENUM类型。因为在MySQL中,ENUM类型被当作数值型数据来处理,而数值型数据被处理起来的速度要比文本类型快得多。这样,我们又可以提高数据库的性能。
  2. 使用连接(JOIN)来代替子查询(Sub-Queries) MySQL从4.1开始支持SQL的子查询。这个技术可以使用SELECT语句来创建一个单列的查询结果,然后把这个结果作为过滤条件用在另一个查询中。例如,我们要将客户基本信息表中没有任何订单的客户删除掉,就可以利用子查询先从销售信息表中将所有发出订单的客户ID取出来,然后将结果传递给主查询,如下所示:
    DELETE FROM customerinfo
    WHERE CustomerID NOT IN (SELECT CustomerID FROM salesinfo)
    
    使用子查询可以一次性的完成很多逻辑上需要多个步骤才能完成的SQL操作,同时也可以避免事务或者表锁死,并且写起来也很容易。但是,有些情况下,子查询可以被更有效率的连接(JOIN)替代。例如,假设我们要将所有没有订单记录的用户取出来,可以用下面这个查询完成:
    SELECT * FROM customerinfo
    WHERE CustomerID NOT IN (SELECT CustomerID FROM salesinfo)
    
    如果使用连接(JOIN)来完成这个查询工作,速度将会快很多。尤其是当salesinfo表中对CustomerID建有索引的话,性能将会更好。