Java问答知识总结篇-MySQL


Java 问答知识总结篇-基础知识

Java 问答知识总结篇-JVM

Java 问答知识总结篇-多线程&并发编程

Java 问答知识总结篇-网络基础

Java 问答知识总结篇-Spring

Java 问答知识总结篇-Spring Boot

Java 问答知识总结篇-Mybatis

Java 问答知识总结篇-MySQL

Java 问答知识总结篇-Redis

Java 问答知识总结篇-MQ

Java 问答知识总结篇-Nginx

Java 问答知识总结篇-分布式

Java 问答知识总结篇-Spring Cloud

Java 问答知识总结篇-Dubbo

Java 问答知识总结篇-Zookeeper

Java 问答知识总结篇-ElasticSearch

Java 问答知识总结篇-Netty

Java 问答知识总结篇-场景分析题

MySQL 系列文章:MySQL

数据库的三范式是什么

第一范式:列不可再分

第二范式:行可以唯一区分,主键约束

第三范式:表的非主属性不能依赖于其他表的非主属性外键约束

三大范式是一级一级依赖的,第二范式建立在第一范式上,第三范式建立第一第二范式上。

MySQL 数据库引擎有哪些

如何查看 MySQL 提供的所有存储引擎。

通过 show engines;可以查看,结果如下:

MySQL 常用引擎包括:MYISAM、Innodb、Memory、MERGE

  • MYISAM:全表锁,拥有较高的执行速度,不支持事务,不支持外键,并发性能差,占用空间相对较小,对事务完整性没有要求,以 select、insert 为主的应用基本上可以使用这引擎;
  • InnoDB:行级锁,提供了具有提交、回滚和崩溃恢复能力的事务安全,支持自动增长列,支持外键约束,并发能力强,占用空间是 MYISAM 的 2.5 倍,处理效率相对会差一些;
  • Memory:全表锁,存储在内存中,速度快,但会占用和数据量成正比的内存空间且数据在 MySQL 重启时会丢失,默认使用 HASH 索引,检索效率非常高,但不适用于范围查找,主要用于那些内容变化不频繁的代码表;
  • MERGE:是一组 MYISAM 表的组合。

说说 InnoDB 与 MyISAM 的区别

  • InnoDB 支持事务,MyISAM 不支持,对于 InnoDB 每一条 SQL 语言都默认封装成事务,自动提交,这样会影响速度,所以最好把多条 SQL 语言放在 begin 和 commit 之间,组成一个事务;
  • InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;(但是目前使用 InnoDB 都不再使用外键这一特性了)
  • InnoDB 是聚集索引,数据文件是和索引绑在一起的,必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的;
  • InnoDB 不保存表的具体行数,执行select count(*) from table时需要全表扫描。而 MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;
  • 在 MySQL 5.1 及之前的版本中,MyISAM 是默认的存储引擎,而在 MySQL 5.5 版本以后,默认使用 InnoDB 存储引擎;
  • MyISAM 不支持行级锁,换句话说,MyISAM 会对整张表加锁,而不是针对行。同时,MyISAM 不支持事务和外键。MyISAM 可被压缩,存储空间较小,而且 MyISAM 在筛选大量数据时非常快;
  • InnoDB 是事务型引擎,当事务异常提交时,会被回滚。同时,InnoDB 支持行锁。此外,InnoDB 需要更多存储空间,会在内存中建立其专用的缓冲池用于高速缓冲数据和索引。InnoDB 支持自动崩溃恢复特性。
  • InnoDB 不支持全文索引,而 MyISAM 支持全文索引,查询效率上 MyISAM 要高。

SQL 约束有哪几种?

  • NOT NULL:用于控制字段的内容一定不能为空(NULL)。
  • UNIQUE:控件字段内容不能重复,一个表允许有多个 Unique 约束。
  • PRIMARY KEY:也是用于控件字段内容不能重复,但它在一个表只允许出现一个。
  • FOREIGN KEY:用于预防破坏表之间连接的动作,也能防止非法数据插入外键列,因为它必须是它指向的那个表中的值之一。
  • CHECK:用于控制字段的值范围。

数据库的事务

什么是事务

事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。事务是逻辑上的一组操作,要么都执行,要么都不执行。

事务最经典也经常被拿出来说例子就是转账了。

假如小明要给小红转账 1000 元,这个转账会涉及到两个关键操作就是:将小明的余额减少 1000 元,将小红的余额增加 1000 元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。

事务的特性

原子性(Atomic)、一致性(Consistency)、隔离性(Isolation)、持久性(Durabiliy)。简称 ACID。

  • 原子性: 组成一个事务的多个数据库操作是一个不可分割的原子单元,只有所有操作都成功,整个事务才会提交。任何一个操作失败,已经执行的任何操作都必须撤销,让数据库返回初始状态。
  • 一致性: 事务操作成功后,数据库所处的状态和它的业务规则是一致的。即数据不会被破坏。如 A 转账 100 元给 B,不管操作是否成功,A 和 B 的账户总额是不变的。
  • 隔离性: 在并发数据操作时,不同的事务拥有各自的数据空间,它们的操作不会对彼此产生干扰。
  • 持久性: 一旦事务提交成功,事务中的所有操作都必须持久化到数据库中。

MYISAM 引擎和 InnoDB 都支持锁,为何 MYISAM 没有事务

MYISAM 引擎是针对当前表做增删改操作时加锁,没有事务开启和事务提交机制,一旦增删改成功后,就不可进行回滚。如果在一个连续操作过程,涉及多张表的增删改,没有事务的情况下,就有可能靠前的操作成功,数据成功修改,靠后的操作失败,数据未修改成功,最终导致脏数据的产生。但是在 Innodb 引擎下就可以解决这种脏数据的问题,事务内出现异常,事务内的所有操作都会被回滚。

索引是什么

官方介绍索引是帮助 MySQL 高效获取数据的数据结构。更通俗的说,数据库索引好比是一本书前面的目录,能加快数据库的查询速度;

一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往是存储在磁盘上的文件中的(可能存储在单独的索引文件中,也可能和数据一起存储在数据文件中)。

我们通常所说的索引,包括聚集索引、覆盖索引、组合索引、前缀索引、唯一索引等,没有特别说明,默认都是使用 B+树结构组织(多路搜索树,并不一定是二叉的)的索引。

MySQL 中 in 和 exists 区别

MySQL 中的 in 语句是把外表和内表作 hash 连接,而 exists 语句是对外表作 loop 循环,每次 loop 循环再对内表进行查询。一直大家都认为 exists 比 in 语句的效率要高,这种说法其实是不准确的。这个是要区分环境的。

如果查询的两个表大小相当,那么用 in 和 exists 差别不大。

如果两个表中一个较小,一个是大表,则子查询表大的用 exists,子查询表小的用 in。

not in 和 not exists:如果查询语句使用了 not in,那么内外表都进行全表扫描,没有用到索引;而 not exists 的子查询依然能用到表上的索引。所以无论那个表大,用 not exists 都比 not in 要快。

简单说一说 drop、delete 与 truncate 的区别

SQL 中的dropdeletetruncate都表示删除,但是三者有一些差别deletetruncate只删除表的数据不删除表的结构,速度一般来说: drop> truncate >deletedelete语句是dml语句,这个操作会放到rollback segement中,事务提交之后才生效; 如果有相应的trigger,执行的时候将被触发。truncate,dropddl, 操作立即生效,原数据不放到rollback segment中,不能回滚,操作不触发trigger。如果表中存在自增的字段,如自增 ID,truncate会将 ID 重置,从 1 开始,而delete不行,依然从当前自增的 ID 延续。

什么是内联接、左外联接、右外联接

  • 内联接(Inner Join): 匹配 2 张表中相关联的记录;
  • 左外联接(Left Outer Join): 除了匹配 2 张表中相关联的记录外,还会匹配左表中剩余的记录,右表中未匹配到的字段用 NULL 表示;
  • 右外联接(Right Outer Join): 除了匹配 2 张表中相关联的记录外,还会匹配右表中剩余的记录,左表中未匹配到的字段用 NULL 表示。在判定左表和右表时,要根据表名出现在 Outer Join 的左右位置关系。

什么是存储过程?有哪些优缺点?

存储过程是一些预编译的 SQL 语句。

  1. 更加直白的理解:存储过程可以说是一个记录集,它是由一些 T-SQL 语句组成的代码块,这些 T-SQL 语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后再给这个代码块取一个名字,在用到这个功能的时候调用他就行了。

  2. 存储过程是一个预编译的代码块,执行效率比较高,一个存储过程替代大量 T_SQL 语句 ,可以降低网络通信量,提高通信速率,可以一定程度上确保数据安全。

但是,在互联网项目中,其实是不太推荐存储过程的,比较出名的就是阿里的《Java 开发手册》中禁止使用存储过程,我个人的理解是,在互联网项目中,迭代太快,项目的生命周期也比较短,人员流动相比于传统的项目也更加频繁,在这样的情况下,存储过程的管理确实是没有那么方便,同时,复用性也没有写在服务层那么好。

并发事务带来哪些问题

在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。

脏读(Dirty read): 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。

丢失修改(Lost to modify): 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。 例如:事务 1 读取某表中的数据 A=20,事务 2 也读取 A=20,事务 1 修改 A=A-1,事务 2 也修改 A=A-1,最终结果 A=19,事务 1 的修改被丢失。

不可重复读(Unrepeatableread): 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。(针对修改数据)

幻读(Phantom read): 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。(针对新增、删除数据)

不可重复读和幻读区别: 不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。

事务隔离级别有哪些?MySQL 的默认隔离级别

SQL 标准定义了四个隔离级别:

READ-UNCOMMITTED(读取未提交): 最低的隔离级别,允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读;

READ-COMMITTED(读取已提交): 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生;

REPEATABLE-READ(可重复读): 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生;

SERIALIZABLE(可串行化): 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。

隔离级别 脏读 不可重复读 幻读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

MySQL InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLE-READ(可重复读),Oracle 默认采用的 READ_COMMITTED 隔离级别。我们可以通过 SELECT @@tx_isolation; 命令来查看。

这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 REPEATABLE-READ(可重复读) 事务隔离级别下使用的是 Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server) 是不同的。所以说 InnoDB 存储引擎的默认支持的隔离级别是 REPEATABLEREAD(可重复读) 已经可以完全保证事务的隔离性要求,即达到了 SQL 标准的 SERIALIZABLE(可串行化) 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 READ-COMMITTED(读已提交) ,但是你要知道的是 InnoDB 存储引擎默认使用 REPEATABLE-READ(可重复读) 并不会有任何性能损失。

InnoDB 存储引擎在分布式事务的情况下一般会用到 SERIALIZABLE(可串行化) 隔离级别。

事务的实现原理

事务是基于重做日志文件(redo log)和回滚日志(undo log)实现的。

每提交一个事务必须先将该事务的所有日志写入到重做日志文件进行持久化,数据库就可以通过重做日志来保证事务的原子性和持久性。

每当有修改事务时,还会产生 undo log,如果需要回滚,则根据 undo log 的反向语句进行逻辑操作,比如 insert 一条记录就 delete 一条记录。undo log 主要实现数据库的一致性。

MySQL 事务日志介绍下?

innodb 事务日志包括 redo log 和 undo log。

undo log 指事务开始之前,在操作任何数据之前,首先将需操作的数据备份到一个地方。redo log 指事务中操作的任何数据,将最新的数据备份到一个地方。

事务日志的目的:实例或者介质失败,事务日志文件就能派上用场。

redo log

redo log 不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入 redo 中。具体的落盘策略可以进行配置 。防止在发生故障的时间点,尚有脏页未写入磁盘,在重启 MySQL 服务的时候,根据 redo log 进行重做,从而达到事务的未入磁盘数据进行持久化这一特性。RedoLog 是为了实现事务的持久性而出现的产物。

undo log

undo log 用来回滚行记录到某个版本。事务未提交之前,Undo 保存了未提交之前的版本数据,Undo 中的数据可作为数据旧版本快照供其他并发事务进行快照读。是为了实现事务的原子性而出现的产物,在 MySQL innodb 存储引擎中用来实现多版本并发控制。

什么是 MySQL 的 binlog?

MySQL 的 binlog 是记录所有数据库表结构变更(例如 CREATE、ALTER TABLE)以及表数据修改(INSERT、UPDATE、DELETE)的二进制日志。binlog 不会记录 SELECT 和 SHOW 这类操作,因为这类操作对数据本身并没有修改,但你可以通过查询通用日志来查看 MySQL 执行过的所有语句。

MySQL binlog 以事件形式记录,还包含语句所执行的消耗的时间,MySQL 的二进制日志是事务安全型的。binlog 的主要目的是复制和恢复。

binlog 有三种格式,各有优缺点:

  • statement: 基于 SQL 语句的模式,某些语句和函数如 UUID, LOAD DATA INFILE 等在复制过程可能导致数据不一致甚至出错

  • row: 基于行的模式,记录的是行的变化,很安全。但是 binlog 会比其他两种模式大很多,在一些大表中清除大量数据时在 binlog 中会生成很多条语句,可能导致从库延迟变大。

  • mixed: 混合模式,根据语句来选用是 statement 还是 row 模式。

具体参考文章:MySQL 数据库系列(九):MySQL 之 binlog 数据恢复和回滚

在事务中可以混合使用存储引擎吗?

尽量不要在同一个事务中使用多种存储引擎,MySQL 服务器层不管理事务,事务是由下层的存储引擎实现的。

如果在事务中混合使用了事务型和非事务型的表(例如 InnoDB 和 MyISAM 表),在正常提交的情况下不会有什么问题。

但如果该事务需要回滚,非事务型的表上的变更就无法撤销,这会导致数据库处于不一致的状态,这种情况很难修复,事务的最终结果将无法确定。所以,为每张表选择合适的存储引擎非常重要。

什么是 MVCC 以及其实现原理

MVCC:即多版本并发控制。MVCC 的实现,是通过保存数据在某个时间点的快照来实现的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。

MVCC 实现原理

对于 InnoDB ,聚簇索引记录中包含 3 个隐藏的列:

  • ROW ID:隐藏的自增 ID,如果表没有主键,InnoDB 会自动按 ROW ID 产生一个聚集索引树。
  • 事务 ID:记录最后一次修改该记录的事务 ID。
  • 回滚指针:指向这条记录的上一个版本。

我们拿上面的例子,对应解释下 MVCC 的实现原理,如下图:

如图,首先 insert 语句向表 t1 中插入了一条数据,a 字段为 1,b 字段为 1, ROW ID 也为 1 ,事务 ID 假设为 1,回滚指针假设为 null。当执行 update t1 set b=666 where a=1 时,大致步骤如下:

  • 数据库会先对满足 a=1 的行加排他锁;
  • 然后将原记录复制到 undo 表空间中;
  • 修改 b 字段的值为 666,修改事务 ID 为 2;
  • 并通过隐藏的回滚指针指向 undo log 中的历史记录;
  • 事务提交,释放前面对满足 a=1 的行所加的排他锁。

在前面实验的第 6 步中,session2 查询的结果是 session1 修改之前的记录,这个记录就是来自 undolog 中。

因此可以总结出 MVCC 实现的原理大致是:

InnoDB 每一行数据都有一个隐藏的回滚指针,用于指向该行修改前的最后一个历史版本,这个历史版本存放在 undo log 中。如果要执行更新操作,会将原记录放入 undo log 中,并通过隐藏的回滚指针指向 undo log 中的原记录。其它事务此时需要查询时,就是查询 undo log 中这行数据的最后一个历史版本。

MVCC 最大的好处是读不加锁,读写不冲突,极大地增加了 MySQL 的并发性。通过 MVCC,保证了事务 ACID 中的 I(隔离性)特性。

为什么要加锁?

当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况。若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性。保证多用户环境下保证数据库完整性和一致性。

为什么要分库分表?

分表

比如你单表都几千万数据了,你确定你能扛住么?绝对不行,单表数据量太大,会极大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。

分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在 200 万以内。

分库

分库就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

这就是所谓的分库分表。

用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?

这个其实就是看看你了解哪些分库分表的中间件,各个中间件的优缺点是啥?然后你用过哪些分库分表的中间件。

比较常见的包括:

  • cobar
  • TDDL
  • atlas
  • sharding-jdbc
  • mycat

cobar

阿里 b2b 团队开发和开源的,属于 proxy 层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库 join 和分页等操作。

TDDL

淘宝团队开发的,属于 client 层方案。支持基本的 crud 语法和读写分离,但不支持 join、多表查询等语法。目前使用的也不多,因为还依赖淘宝的 diamond 配置管理系统。

atlas

360 开源的,属于 proxy 层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在 5 年前了。所以,现在用的公司基本也很少了。

sharding-jdbc

当当开源的,属于 client 层方案。确实之前用的还比较多一些,因为 SQL 语法支持也比较多,没有太多限制,而且目前推出到了 2.0 版本,支持分库分表、读写分离、分布式 id 生成、柔性事务(最大努力送达型事务、TCC 事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从 2017 年一直到现在,是有不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案

mycat

基于 cobar 改造的,属于 proxy 层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于 sharding jdbc 来说,年轻一些,经历的锤炼少一些。

如何对数据库如何进行垂直拆分或水平拆分的?

水平拆分的意思,就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。(总结:访问量大,抗并发,动态扩容

垂直拆分的意思,就是把一个有很多字段的表给拆分成多个表或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

分库分表的方式

  • 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了。
  • 按照某个字段 hash 一下(一般使用主键 ID,hash 针对的是非数值类字段),再根据 hash 值对数据库的数量求余,确定命中的数据库,均匀分散,这个较为常用。
  • 数值类主键 ID,直接对数据库数量求余,命中数据库,均匀分散。

range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。

hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表。

具体可以参考文章:MySQL 数据库系列(十):MySQL 之分库分表

大表如何优化

当 MySQL 单表记录数过大时,数据库的 CRUD 性能会明显下降,一些常见的优化措施如下:

限定数据的范围

务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;

读/写分离

经典的数据库拆分方案,主库负责写,从库负责读;

垂直分区

根据数据库里面数据表的相关性进行拆分。 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。

简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。 如下图所示,这样来说大家应该就更容易理解了。

  • 垂直拆分的优点: 可以使得列数据变小,在查询时减少读取的 Block 数,减少 I/O 次数。此外,垂直分区可以简化表的结构,易于维护。
  • 垂直拆分的缺点: 主键会出现冗余,需要管理冗余列,并会引起 Join 操作,可以通过在应用层进行 Join 来解决。此外,垂直分区会让事务变得更加复杂。

水平分区

保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。

水平拆分是指数据表行的拆分,表的行数超过 200 万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。

水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升 MySQL 并发能力没有什么意义,所以水平拆分最好分库。

水平拆分能够支持非常大的数据量存储,应用端改造也少,但分片事务难以解决,跨节点 Join 性能较差,逻辑复杂。《Java 工程师修炼之道》的作者推荐尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度 ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络 I/O。

下面补充一下数据库分片的两种常见方案:

客户端代理: 分片逻辑在应用端,封装在 jar 包中,通过修改或者封装 JDBC 层来实现。 当当网的 Sharding-JDBC 、阿里的 TDDL 是两种比较常用的实现。

中间件代理: 在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。 我们现在谈的 Mycat 、360 的 Atlas、网易的 DDB 等等都是这种架构的实现。

参考文章:MySQL 大表优化方案

分库分表之后,id 主键如何处理

因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。

生成全局 id 有下面这几种方式:

  • UUID: 不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
  • 数据库自增 id : 两台数据库分别设置不同步长,生成不重复 ID 的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
  • 利用 redis 生成 id : 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
  • Twitter 的 snowflake 算法 : 算法 GitHub 地址
  • 美团的 Leaf 分布式 ID 生成系统: Leaf 是美团开源的分布式 ID 生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper 等中间件。感觉还不错。美团技术团队的一篇文章:技术博客

说说在 MySQL 中一条查询 SQL 是如何执行的

  • 取得链接: 获取到 MySQL 中的连接器,并验证权限;
  • 查询缓存: key 为 SQL 语句,value 为查询结果,如果查到就直接返回。不建议使用缓存,在 MySQL 8.0 版本已经将查询缓存删除,也就是说 MySQL 8.0 版本后不存在此功能;
  • 分析器: 分为词法分析和语法分析。此阶段只是做一些 SQL 解析,语法校验。所以一般语法错误在此阶段;
  • 优化器: 是在表里有多个索引的时候,决定使用哪个索引;或者一个语句中存在多表关联的时候(join),决定各个表的连接顺序;
  • 执行器: 通过分析器让 SQL 知道你要干啥,通过优化器知道该怎么做,通过执行器开始执行语句。

什么是 MySQL 主从同步?

主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。

因为复制是异步进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表。

MySQL 主从同步的目的?为什么要做主从同步?

  1. 通过增加从服务器来提高数据库的性能,在主服务器上执行写入和更新,在从服务器上向外提供读功能,可以动态地调整从服务器的数量,从而调整整个数据库的性能。
  2. 提高数据安全,因为数据已复制到从服务器,从服务器可以终止复制进程,所以,可以在从服务器上备份而不破坏主服务器相应数据。
  3. 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能。
  4. 数据备份。一般我们都会做数据备份,可能是写定时任务,一些特殊行业可能还需要手动备份,有些行业要求备份和原数据不能在同一个地方,所以主从就能很好的解决这个问题,不仅备份及时,而且还可以多地备份,保证数据的安全。

MySQL 主从复制流程和原理?

基本原理流程,是 3 个线程以及之间的关联。

  • 主:binlog 线程,记录下所有改变了数据库数据的语句,放进 master 上的 binlog 中;

  • 从:io 线程,在使用 start slave 之后,负责从 master 上拉取 binlog 内容,放进自己的 relay log 中;

  • 从:sql 执行线程,执行 relay log 中的语句。

复制过程如下:

Binary log:主数据库的二进制日志

Relay log:从服务器的中继日志

第一步: master 在每个事务更新数据完成之前,将该操作记录串行地写入到 binlog 文件中。

第二步: salve 开启一个 I/O Thread,该线程在 master 打开一个普通连接,主要工作是 binlog dump process。如果读取的进度已经跟上了 master,就进入睡眠状态并等待 master 产生新的事件。I/O 线程最终的目的是将这些事件写入到中继日志中。

第三步: SQL Thread 会读取中继日志,并顺序执行该日志中的 SQL 事件,从而与主数据库中的数据保持一致。

索引是什么?

索引是一种特殊的文件(InnoDB 数据表上的索引是表空间的一个组成部分),它们包含着对数据表里所有记录的引用指针。

索引是一种数据结构。数据库索引,是数据库管理系统中一个排序的数据结构,以协助快速查询、更新数据库表中数据。索引的实现通常使用 B 树及其变种 B+树。更通俗的说,索引就相当于目录。为了方便查找书中的内容,通过对内容建立索引形成目录。而且索引是一个文件,它是要占据物理空间的。

MySQL 索引的建立对于 MySQL 的高效运行是很重要的,索引可以大大提高 MySQL 的检索速度。比如我们在查字典的时候,前面都有检索的拼音和偏旁、笔画等,然后找到对应字典页码,这样然后就打开字典的页数就可以知道我们要搜索的某一个 key 的全部值的信息了。

索引有什么优缺点

索引的优点

  • 可以大大加快数据的检索速度,这也是创建索引的最主要的原因。
  • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。

索引的缺点

  • 时间方面:创建索引和维护索引要耗费时间,具体地,当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,会降低增/改/删的执行效率;
  • 空间方面:索引需要占物理空间。

4. 说一说索引的底层实现?

Hash 索引

基于哈希表实现,只有精确匹配索引所有列的查询才有效,对于每一行数据,存储引擎都会对所有的索引列计算一个哈希码(hash code),并且 Hash 索引将所有的哈希码存储在索引中,同时在索引表中保存指向每个数据行的指针。

图片来源:面试官:深入谈谈你对 MySQL 索引的理解

B-Tree 索引(MySQL 使用 B+Tree)

B-Tree 能加快数据的访问速度,因为存储引擎不再需要进行全表扫描来获取数据,数据分布在各个节点之中

B+Tree 索引

是 B-Tree 的改进版本,同时也是数据库索引索引所采用的存储结构。数据都在叶子节点上,并且增加了顺序访问指针,每个叶子节点都指向相邻的叶子节点的地址。相比 B-Tree 来说,进行范围查找时只需要查找两个节点,进行遍历即可。而 B-Tree 需要获取所有节点,相比之下 B+Tree 效率更高。

B+Tree 性质:

  • n 棵子 tree 的节点包含 n 个关键字,不用来保存数据而是保存数据的索引。

  • 所有的叶子结点中包含了全部关键字的信息,及指向含这些关键字记录的指针,且叶子结点本身依关键字的大小自小而大顺序链接。

  • 所有的非终端结点可以看成是索引部分,结点中仅含其子树中的最大(或最小)关键字。

  • B+ 树中,数据对象的插入和删除仅在叶节点上进行。

  • B+树有 2 个头指针,一个是树的根节点,一个是最小关键码的叶节点。

索引的种类有哪些?

  1. 从存储结构上来划分:BTree 索引(B-Tree 或 B+Tree 索引),Hash 索引,full-index 全文索引,R-Tree 索引。这里所描述的是索引存储时保存的形式,

  2. 从应用层次来分:普通索引,唯一索引,复合索引

  3. 根据中数据的物理顺序与键值的逻辑(索引)顺序关系:聚集索引,非聚集索引。

平时讲的索引类型一般是指在应用层次的划分。

  • 普通索引:即一个索引只包含单个列,一个表可以有多个单列索引
  • 复合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并
  • 唯一索引:索引列的值必须唯一,但允许有空值
  • 聚簇索引(聚集索引):并不是一种单独的索引类型,而是一种数据存储方式。具体细节取决于不同的实现,InnoDB 的聚簇索引其实就是在同一个结构中保存了 B-Tree 索引(技术上来说是 B+Tree)和数据行。
  • 非聚簇索引:不是聚簇索引,就是非聚簇索引

为什么 MySQL 的索引要使用 B+树而不是其它树形结构?比如 B 树?

B-tree:

  • B+树的磁盘读写代价更低:B+树的内部节点并没有指向关键字具体信息的指针,因此其内部节点相对 B(B-)树更小,如果把所有同一内部节点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多,一次性读入内存的需要查找的关键字也就越多,相对IO读写次数就降低了。

  • 由于 B+树的数据都存储在叶子结点中,分支结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是 B 树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以 B+树更加适合在区间查询的情况,所以通常 B+树用于数据库索引。

Hash:虽然可以快速定位,但是没有顺序,IO 复杂度高。

  • 基于 Hash 表实现,只有 Memory 存储引擎显式支持哈希索引 ;

  • 适合等值查询,如=、in()、<=>,不支持范围查询 ;

  • 因为不是按照索引值顺序存储的,就不能像 B+Tree 索引一样利用索引完成排序

  • Hash 索引在查询等值时非常快 ;

  • 因为 Hash 索引始终索引的所有列的全部内容,所以不支持部分索引列的匹配查找 ;

  • 如果有大量重复键值得情况下,哈希索引的效率会很低,因为存在哈希碰撞问题 。

二叉树:树的高度不均匀,不能自平衡,查找效率跟数据有关(树的高度),并且 IO 代价高。

红黑树:树的高度随着数据量增加而增加,IO 代价高。

不使用平衡二叉树的原因如下:

最大原因:深度太大(因为一个节点最多只有 2 个子节点),一次查询需要的 I/O 复杂度为 O(lgN),而 b+tree 只需要 O(log_mN),而其出度 m 非常大,其深度一般不会超过 4。平衡二叉树逻辑上很近的父子节点,物理上可能很远,无法充分发挥磁盘顺序读和预读的高效特性。

参考文章:MySQL 数据库系列(六):MySQL 之索引数据结构分析

讲一讲聚簇索引与非聚簇索引?

在 InnoDB 里,索引 B+ Tree 的叶子节点存储了整行数据的是主键索引,也被称之为聚簇索引,即将数据存储与索引放到了一块,找到索引也就找到了数据。

而索引 B+ Tree 的叶子节点存储了主键的值的是非主键索引,也被称之为非聚簇索引、二级索引。

聚簇索引与非聚簇索引的区别:

  • 非聚集索引与聚集索引的区别在于非聚集索引的叶子节点不存储表中的数据,而是存储该列对应的主键(行号)

  • 对于 InnoDB 来说,想要查找数据我们还需要根据主键再去聚集索引中进行查找,这个再根据聚集索引查找数据的过程,我们称为回表。第一次索引一般是顺序 IO,回表的操作属于随机 IO。需要回表的次数越多,即随机 IO 次数越多,我们就越倾向于使用全表扫描 。

  • 通常情况下, 主键索引(聚簇索引)查询只会查一次,而非主键索引(非聚簇索引)需要回表查询多次。当然,如果是覆盖索引的话,查一次即可

注意:MyISAM 无论主键索引还是二级索引都是非聚簇索引,而 InnoDB 的主键索引是聚簇索引,二级索引是非聚簇索引。我们自己建的索引基本都是非聚簇索引。

非聚簇索引一定会回表查询吗?

不一定,这涉及到查询语句所要求的字段是否全部命中了索引,如果全部命中了索引,那么就不必再进行回表查询。一个索引包含(覆盖)所有需要查询字段的值,被称之为”覆盖索引”。

举个简单的例子,假设我们在员工表的年龄上建立了索引,那么当进行select score from student where score > 90的查询时,在索引的叶子节点上,已经包含了 score 信息,不会再次进行回表查询。

联合索引是什么?为什么需要注意联合索引中的顺序?

MySQL 可以使用多个字段同时建立一个索引,叫做联合索引。在联合索引中,如果想要命中索引,需要按照建立索引时的字段顺序挨个使用,否则无法命中索引。

具体原因为:

MySQL 使用索引时需要索引有序,假设现在建立了”name,age,school”的联合索引,那么索引的排序为: 先按照 name 排序,如果 name 相同,则按照 age 排序,如果 age 的值也相等,则按照 school 进行排序。

当进行查询时,此时索引仅仅按照 name 严格有序,因此必须首先使用 name 字段进行等值查询,之后对于匹配到的列而言,其按照 age 字段严格有序,此时可以使用 age 字段用做索引查找,以此类推。因此在建立联合索引的时候应该注意索引列的顺序,一般情况下,将查询需求频繁或者字段选择性高的列放在前面。此外可以根据特例的查询或者表结构进行单独的调整。

MyISAM 和 InnoDB 实现 BTree 索引方式的区别

MyISAM

  • B+Tree 叶节点的 data 域存放的是数据记录的地址。在索引检索的时候,首先按照 B+Tree 搜索算法搜索索引,如果指定的 Key 存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
  • 索引文件和数据文件是分离的。

InnoDB

  • InnoDB 的 B+Tree 索引分为主索引(聚集索引)和辅助索引(非聚集索引)。一张表一定包含一个聚集索引构成的 B+ 树以及若干辅助索引的构成的 B+ 树。
  • 辅助索引的存在并不会影响聚集索引,因为聚集索引构成的 B+ 树是数据实际存储的形式,而辅助索引只用于加速数据的查找,所以一张表上往往有多个辅助索引以此来提升数据库的性能。
  • 就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大。再例如,用非单调的字段作为主键在 InnoDB 中不是个好主意,因为 InnoDB 数据文件本身是一颗 B+Tree,非单调的主键会造成在插入新记录时数据文件为了维持 B+Tree 的特性而频繁的分裂调整,十分低效,而使用自增字段作为主键则是一个很好的选择。

什么是最左匹配原则?

最左优先,以最左边的为起点任何连续的索引都能匹配上。同时遇到范围查询(>、<、between、like)就会停止匹配。
例如:b = 2 如果建立(a,b)顺序的索引,是匹配不到(a,b)索引的;但是如果查询条件是 a = 1 and b = 2,就可以,因为**优化器会自动调整 a,b 的顺序**。再比如 a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,因为 c 字段是一个范围查询,它之后的字段会停止匹配。

最左匹配原则的原理

MySQL 中的索引可以以一定顺序引用多列,这种索引叫作联合索引。最左匹配原则都是针对联合索引来说的。

  • 我们都知道索引的底层是一颗 B+树,那么联合索引当然还是一颗 B+树,只不过联合索引的健值数量不是一个,而是多个。构建一颗 B+树只能根据一个值来构建,因此数据库依据联合索引最左的字段来构建 B+树。

例子:假如创建一个(a,b)的联合索引,那么它的索引树是这样的可以看到 a 的值是有顺序的,1,1,2,2,3,3,而 b 的值是没有顺序的 1,2,1,4,1,2。所以 b = 2 这种查询条件没有办法利用索引,因为联合索引首先是按 a 排序的,b 是无序的。

同时我们还可以发现在 a 值相等的情况下,b 值又是按顺序排列的,但是这种顺序是相对的。所以最左匹配原则遇上范围查询就会停止,剩下的字段都无法使用索引。例如 a = 1 and b = 2,a,b 字段都可以使用索引,因为在 a 值确定的情况下 b 是相对有序的,而 a>1 and b=2,a 字段可以匹配上索引,但 b 值不可以,因为 a 的值是一个范围,在这个范围中 b 是无序的。

优点:最左前缀原则的利用也可以显著提高查询效率,是常见的 MySQL 性能优化手段。

讲一讲前缀索引?

因为可能我们索引的字段非常长,这既占内存空间,也不利于维护。所以我们就想,如果只把很长字段的前面的公共部分作为一个索引,就会产生超级加倍的效果。但是,我们需要注意,order by 不支持前缀索引 。

流程是:

先计算完整列的选择性 :select count(distinct col_1)/count(1) from table_1

再计算不同前缀长度的选择性 :select count(distinct left(col_1,4))/count(1) from table_1

找到最优长度之后,创建前缀索引 : create index idx_front on table_1 (col_1(4))

了解索引下推吗?

MySQL 5.6 引入了索引下推优化。默认开启,使用SET optimizer_switch = ‘index_condition_pushdown=off’;可以将其关闭。

  • 有了索引下推优化,可以在减少回表次数

  • 在 InnoDB 中只针对二级索引有效

官方文档中给的例子和解释如下:

在 people_table 中有一个二级索引(zipcode,lastname,address),查询是SELECT * FROM people WHERE zipcode=’95054′ AND lastname LIKE ‘%etrunia%’ AND address LIKE ‘%Main Street%’

  • 如果没有使用索引下推技术,则 MySQL 会通过zipcode=’95054’从存储引擎中查询对应的数据,返回到 MySQL 服务端,然后 MySQL 服务端基于lastname LIKE ‘%etrunia%’ and address LIKE ‘%Main Street%’来判断数据是否符合条件。

  • 如果使用了索引下推技术,则 MYSQL 首先会返回符合zipcode=’95054’的索引,然后根据lastname LIKE ‘%etrunia%’ and address LIKE ‘%Main Street%’来判断索引是否符合条件。如果符合条件,则根据该索引来定位对应的数据,如果不符合,则直接 reject 掉。

总结:之前是针对数据做筛选,涉及回表查询数据,如果使用索引下推,针对的是索引,最终得到结果再回表查询。

怎么查看 MySQL 语句有没有用到索引?

通过 explain,如以下例子:

EXPLAIN SELECT * FROM employees.titles WHERE emp_no='10001' AND title='Senior Engineer' AND from_date='1986-06-26';

id select_type table partitions type possible_keys key key_len ref filtered rows Extra
1 SIMPLE titles null const PRIMARY PRIMARY 59 const,const,const 10 1
  • id:在⼀个⼤的查询语句中每个SELECT关键字都对应⼀个唯⼀的 id ,如explain select * from s1 where id = (select id from s1 where name = 'egon1');第一个 select 的 id 是 1,第二个 select 的 id 是 2。有时候会出现两个 select,但是 id 却都是 1,这是因为优化器把子查询变成了连接查询 。

  • select_type:select 关键字对应的那个查询的类型,如 SIMPLE,PRIMARY,SUBQUERY,DEPENDENT,SNION 。

  • table:每个查询对应的表名 。

  • type:type 字段比较重要, 它提供了判断查询是否高效的重要依据。通过 type 字段, 我们判断此次查询是 全表扫描 还是 索引扫描 等。如 const(主键索引或者唯一二级索引进行等值匹配的情况下),ref(普通的⼆级索引列与常量进⾏等值匹配),index(扫描全表索引的覆盖索引) 。

    通常来说, 不同的 type 类型的性能关系如下:
    ALL < index < range ~ index_merge < ref < eq_ref < const < system
    ALL 类型因为是全表扫描, 因此在相同的查询条件下, 它是速度最慢的.
    index 类型的查询虽然不是全表扫描, 但是它扫描了所有的索引, 因此比 ALL 类型的稍快.

  • possible_key:查询中可能用到的索引 (可以把用不到的删掉,降低优化器的优化时间)

  • key:此字段是 MySQL 在当前查询时所真正使用到的索引。

  • filtered:查询器预测满足下一次查询条件的百分比。

  • rows:也是一个重要的字段. MySQL 查询优化器根据统计信息,估算 SQL 要查找到结果集需要扫描读取的数据行数。
    这个值非常直观显示 SQL 的效率好坏, 原则上 rows 越少越好。

  • extra:表示额外信息,如 Using where,Start temporary,End temporary,Using temporary 等。

参考文章:MySQL 数据库系列(四):MySQL 数据库的执行计划怎么看

为什么官方建议使用自增长主键作为索引?

结合 B+Tree 的特点,自增主键是连续的,在插入过程中尽量减少页分裂,即使要进行页分裂,也只会分裂很少一部分。并且能减少数据移动,每次插入都是插入到最后。总之就是减少分裂和移动的频率

插入连续的数据:

图片来自:面试官:深入谈谈你对 MySQL 索引的理解

插入非连续的数据:

如何创建索引?

创建索引有三种方式。

  1. 在执行CREATE TABLE时创建索引
CREATE TABLE user_index2 (
	id INT auto_increment PRIMARY KEY,
	first_name VARCHAR (16),
	last_name VARCHAR (16),
	id_card VARCHAR (18),
	information text,
	KEY name (first_name, last_name),
	FULLTEXT KEY (information),
	UNIQUE KEY (id_card)
);
  1. 使用ALTER TABLE命令去增加索引。
ALTER TABLE table_name ADD INDEX index_name (column_list);

ALTER TABLE 用来创建普通索引、UNIQUE 索引或 PRIMARY KEY 索引。

其中 table_name 是要增加索引的表名,column_list 指出对哪些列进行索引,多列时各列之间用逗号分隔。

索引名 index_name 可自己命名,缺省时,MySQL 将根据第一个索引列赋一个名称。另外,ALTER TABLE 允许在单个语句中更改多个表,因此可以在同时创建多个索引。

  1. 使用CREATE INDEX命令创建。
CREATE INDEX index_name ON table_name (column_list);

创建索引时需要注意什么?

  • 非空字段:应该指定列为 NOT NULL,除非你想存储 NULL。在 mysql 中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。应该用 0、一个特殊的值或者一个空串代替空值
  • 取值离散大的字段:可以通过 count(distinct col)/count() 函数查看字段的差异值,返回值越大说明字段的唯一值越多字段的离散程度高;
  • 索引字段越小越好:数据库的数据存储以页为单位一页存储的数据越多一次 IO 操作获取的数据越大效率越高;
  • 频繁增删改的列不要建立索引,会导致索引频繁的维护,带来的性能降低;
  • 表记录太少不要建立索引。(数据少没有建立索引的必要)

建索引的原则有哪些?

  1. 最左前缀匹配原则,非常重要的原则,mysql 会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如 a = 1 and b = 2 and c > 3 and d = 4,如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d 的顺序可以任意调整。(在 SQL 执行之前会有优化器对 SQL 进行优化,虽然 abdc 和实际查询语句不符合最左前缀原则,会被优化为符合)

  2. =和 in 可以乱序,比如 a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,mysql 的查询优化器会帮你优化成索引可以识别的形式。

  3. 尽量选择区分度高的列作为索引,区分度的公式是 count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就是 0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要 join 的字段我们都要求是 0.1 以上,即平均 1 条扫描 10 条记录。(总结:区分度达到 0.1 以上适合建立索引)

  4. 索引列不能参与计算,保持列“干净”,比如 from_unixtime(create_time) = ’2014-05-29’就不能使用到索引,原因很简单,b+树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成 create_time = unix_timestamp(’2014-05-29’)。(索引类字段不要通过函数计算,但是可以对其查询条件进行计算)

  5. 尽量的扩展索引,不要新建索引。比如表中已经有 a 的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

使用索引查询一定能提高查询的性能吗?

通常通过索引查询数据比全表扫描要快。但是我们也必须注意到它的代价。

索引需要空间来存储,也需要定期维护, 每当有记录在表中增减或索引列被修改时,索引本身也会被修改。 这意味着每条记录的 INSERT,DELETE,UPDATE 将为此多付出 4,5 次的磁盘 I/O。 因为索引需要额外的存储空间和处理,那些不必要的索引反而会使查询反应时间变慢。使用索引查询不一定能提高查询性能,索引范围查询(INDEX RANGE SCAN)适用于两种情况:

  • 基于一个范围的检索,一般查询返回结果集小于表中记录数的 30%。
  • 基于非唯一性索引的检索。

什么情况下不走索引(索引失效)?

  1. 使用 != 或者 <> 导致索引失效

  2. 类型不一致导致的索引失效

  3. 函数导致的索引失效

如:

SELECT * FROM `user` WHERE DATE(create_time) = '2020-09-03';

如果使用函数在索引列,这是不走索引的。

  1. 运算符导致的索引失效
SELECT * FROM `user` WHERE age - 1 = 20;

如果你对列进行了(+,-,*,/,!), 那么都将不会走索引。

  1. OR 引起的索引失效
SELECT * FROM `user` WHERE `name` = '张三' OR height = '175';

OR 导致索引是在特定情况下的,并不是所有的 OR 都是使索引失效,如果 OR 连接的是同一个字段,那么索引不会失效,反之索引失效

  1. 模糊搜索导致的索引失效
SELECT * FROM `user` WHERE `name` LIKE '%冰';

%放在匹配字段前是不走索引的,放在后面才会走索引。

  1. NOT IN、NOT EXISTS 导致索引失效

参考文章:MySQL 数据库系列(五):MySQL 之 SQL 优化十大口诀了解一下

MySQL 建表的约束条件有哪些?

  • 主键约束(Primay Key Coustraint):唯一性,非空性
  • 唯一约束(Unique Counstraint):唯一性,可以空,但只能有一个
  • 检查约束(Check Counstraint):对该列数据的范围、格式的限制
  • 默认约束(Default Counstraint):该数据的默认值
  • 外键约束(Foreign Key Counstraint):需要建立两表间的关系并引用主表的列

索引这么多优点,为什么不对表中的每一个列创建一个索引呢?

  1. 当对表中的数据进行增加、删除和修改的时候,索引也要动态的维护,这样就降低了数据的维护速度。
  2. 索引需要占物理空间,除了数据表占数据空间之外,每一个索引还要占一定的物理空间,如果要建立聚簇索引,那么需要的空间就会更大。
  3. 创建索引和维护索引要耗费时间,这种时间随着数据量的增加而增加。

MySQL 中 varchar 与 char 的区别?varchar(30) 中的 30 代表的涵义

  • varchar 与 char 的区别,char 是一种固定长度的类型,varchar 则是一种可变长度的类型;
  • varchar(30) 中 30 的涵义最多存放 30 个字符。varchar(30) 和 (130) 存储 hello 所占空间一样,但后者在排序时会消耗更多内存,因为 ORDER BY col 采用 fixed_length 计算 col 长度(memory 引擎也一样);
  • 对效率要求高用 char,对空间使用要求高用 varchar。

为什么 SELECT COUNT(*) FROM table 在 InnoDB 比 MyISAM 慢

对于 SELECT COUNT(*) FROM table 语句,在没有 WHERE 条件的情况下,InnoDB 比 MyISAM 可能会慢很多,尤其在大表的情况下。因为,InnoDB 是去实时统计结果,会全表扫描;而 MyISAM 内部维持了一个计数器,预存了结果,所以直接返回即可。

MySQL 索引类型有哪些

  • 主键索引: 索引列中的值必须是唯一的,不允许有空值。
  • 普通索引: MySQL 中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值。
  • 唯一索引: 索引列中的值必须是唯一的,但是允许为空值。
  • 全文索引: 只能在文本类型 CHAR,VARCHAR,TEXT 类型字段上创建全文索引。字段长度比较大时,如果创建普通索引,在进行 like 模糊查询时效率比较低,这时可以创建全文索引。MyISAM 和 InnoDB 中都可以使用全文索引。
  • 空间索引: MySQL 在 5.7 之后的版本支持了空间索引,而且支持 OpenGIS 几何数据模型。MySQL 在空间索引这方面遵循 OpenGIS 几何数据模型规则。
  • 前缀索引: 在文本类型如 CHAR,VARCHAR,TEXT 类列上创建索引时,可以指定索引列的长度,但是数值类型不能指定。
  • 组合索引: 组合索引的使用,需要遵循最左前缀匹配原则(最左匹配原则)。一般情况下在条件允许的情况下使用组合索引替代多个单列索引使用。

请说说 MySQL 数据库的锁

关于 MySQL 的锁机制,可能会问很多问题,不过这也得看面试官在这方面的知识储备。

MySQL 中的锁从类别上来讲,分为共享锁和排它锁,也就是读锁和写锁。从粒度上来讲,分为行级锁(INNODB 引擎)、表级锁(MYISAM 引擎)、页级锁(BDB 引擎)。

根据类别区分

  • 共享锁: 不堵塞,多个用户可以同一时刻读取同一个资源,相互之间没有影响;
  • 排它锁: 一个写操作阻塞其他的读锁和写锁,这样可以只允许一个用户进行写入,防止其他用户读取正在写入的资源。

根据粒度区分

  • 表锁: 系统开销最小,会锁定整张表,MyISAM 使用表锁。
    • 表级锁是 MySQL 中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分 MySQL 引擎支持。最常使用的 MYISAM 与 INNODB 都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。
    • 开销小,加锁快;不会出现死锁;锁定粒度大,发出锁冲突的概率最高,并发度最低。
  • 行锁: 容易出现死锁,发生冲突概率低,并发高,InnoDB 支持行锁(必须有索引才能实现,否则会自动锁全表,那么就不是行锁了)。
    • 行级锁是 MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为共享锁和排他锁。
    • 开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。
  • 页级锁:
    • 页级锁是 MySQL 中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。BDB 支持页级锁
    • 开销和加锁时间界于表锁和行锁之间;会出现死锁;锁定粒度界于表锁和行锁之间,并发度一般

MyISAM 和 InnoDB 存储引擎使用的锁:

  • MyISAM 采用表级锁(table-level locking)。
  • InnoDB 支持行级锁(row-level locking)、页级锁、表级锁,默认为行级锁。

数据库的乐观锁和悲观锁是什么?怎么实现的?

数据库管理系统(DBMS)中的并发控制的任务是确保在多个事务同时存取数据库中同一数据时不破坏事务的隔离性和统一性以及数据库的统一性。乐观并发控制(乐观锁)和悲观并发控制(悲观锁)是并发控制主要采用的技术手段。

  • 悲观锁:假定会发生并发冲突,屏蔽一切可能违反数据完整性的操作。在查询完数据的时候就把事务锁起来,直到提交事务。实现方式:使用数据库中的锁机制

  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性。在修改数据的时候把事务锁起来,通过 version 的方式来进行锁定。实现方式:乐观锁一般会使用版本号机制或 CAS 算法实现。

两种锁的使用场景

从上面对两种锁的介绍,我们知道两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。

但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行 retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。

InnoDB 引擎的行锁是怎么实现的?

InnoDB 是基于索引来完成行锁。

例: select * from tab_with_index where id = 1 for update;

for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么 InnoDB 将完成表锁,并发将无从谈起。

什么是死锁?怎么解决?

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。

常见的解决死锁的方法:

  1. 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。

  2. 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;

  3. 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率。

如果业务处理不好可以用分布式事务锁或者使用乐观锁。

隔离级别与锁的关系

在 Read Uncommitted 级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突

在 Read Committed 级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;

在 Repeatable Read 级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。

SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

优化锁方面的意见?

  • 使用较低的隔离级别
  • 设计索引,尽量使用索引去访问数据,加锁更加精确,从而减少锁冲突
  • 选择合理的事务大小,给记录显示加锁时,最好一次性请求足够级别的锁。例如,修改数据的话,最好申请排他锁,而不是先申请共享锁,修改时在申请排他锁,这样会导致死锁
  • 不同的程序访问一组表的时候,应尽量约定一个相同的顺序访问各表,对于一个表而言,尽可能的固定顺序的获取表中的行。这样大大的减少死锁的机会。
  • 尽量使用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
  • 不要申请超过实际需要的锁级别
  • 数据查询的时候不是必要,不要使用加锁。MySQL 的 MVCC 可以实现事务中的查询不用加锁,优化事务性能:MVCC 只在 committed read(读提交)和 repeatable read (可重复读)两种隔离级别
  • 对于特定的事务,可以使用表锁来提高处理速度活着减少死锁的可能。

说说什么是锁升级

  • MySQL 行锁只能加在索引上,如果操作不走索引,就会升级为表锁。因为 InnoDB 的行锁是加在索引上的,如果不走索引,自然就没法使用行锁了,原因是 InnoDB 是将 primary key index 和相关的行数据共同放在 B+ 树的叶节点。InnoDB 一定会有一个 primary key,secondary index 查找的时候,也是通过找到对应的 primary,再找对应的数据行。
  • 当非唯一索引上记录数超过一定数量时,行锁也会升级为表锁。测试发现当非唯一索引相同的内容不少于整个表记录的二分之一时会升级为表锁。因为当非唯一索引相同的内容达到整个记录的二分之一时,索引需要的性能比全文检索还要大,查询语句优化时会选择不走索引,造成索引失效,行锁自然就会升级为表锁。

使用 MySQL 的索引应该注意些什么

MySQL 如何做到高可用方案

MySQL 高可用,意味着不能一台 MySQL 出了问题,就不能访问了。

  • MySQL 高可用: 分库分表,通过 MyCat 连接多个 MySQL
  • MyCat 也得高可用: Haproxy,连接多个 MyCat
  • Haproxy 也得高可用: 通过 keepalived 辅助 Haproxy

如何定位及优化 SQL 语句的性能问题?

对于低性能的 SQL 语句的定位,最重要也是最有效的方法就是使用执行计划,MySQL 提供了 explain 命令来查看语句的执行计划。 我们知道,不管是哪种数据库,或者是哪种数据库引擎,在对一条 SQL 语句进行执行的过程中都会做很多相关的优化,对于查询语句,最重要的优化方式就是使用索引。

而执行计划,就是显示数据库引擎对于 SQL 语句的执行的详细情况,其中包含了是否使用索引,使用什么索引,使用的索引的相关信息等。

大表数据查询,怎么优化

  • 优化 shema、sql 语句、索引;
  • 第二加缓存,memcached, redis;
  • 主从复制,读写分离;
  • 垂直拆分,对大表中的热点字段和非热点字段进行分类,减小单表的体量,提高查询速度;
  • 水平切分,针对数据量大的表,做水平推展,可以是同库的多表拓展,也可以是拓展多个数据库。

超大分页怎么处理?

数据库层面,这也是我们主要集中关注的(虽然收效没那么大),类似于select * from table where age > 20 limit 1000000,10,这种查询其实也是有可以优化的余地的。这条语句需要加载一百万条数据,然后基本上全部丢弃,只取 10 条当然比较慢。当时我们可以修改为select * from table where id in (select id from table where age > 20 limit 1000000,10)。这样虽然也加载了一百万的数据,但是由于索引覆盖,要查询的所有字段都在索引中,所以速度会很快。

解决超大分页,其实主要是靠缓存,可预测性的提前查到内容,缓存至 redis 等 K-V 数据库中,直接返回即可。

在阿里巴巴《Java 开发手册》中,对超大分页的解决办法是类似于上面提到的第一种。

【推荐】利用延迟关联或者子查询优化超多分页场景。

说明:MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当 offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL 改写。

正例:先快速定位需要获取的 id 段,然后再关联:

SELECT a.* FROM 表1 a, (select id from 表1 where 条件 LIMIT 100000,20 ) b where a.id=b.id

统计过慢查询吗?对慢查询都怎么优化过?

在业务系统中,除了使用主键进行的查询,其他的我都会在测试库上测试其耗时,慢查询的统计主要由运维在做,会定期将业务中的慢查询反馈给我们。

慢查询的配置:

[mysqld]
log_output='FILE,TABLE'
slow_query_log='ON'
long_query_time=0.01

配置文件中配置后,需要重启生效,如果需要在数据库不重启的情况下实现慢日志的查询,可以通过命令设定,但是存在数据库重新设置失效的问题。

SET GLOBAL slow_query_log = 'ON'
SET GLOBAL long_query_time= 0.01
SET GLOBAL log_output = 'FILE,TABLE'

配置完成以后,慢查询会被记录,可以通过命令直接查看,也可以使用 mysql 内置的mysqldumpslow将慢查询日志导出。

--- 通过sql查询
select * from mysql.slow_log;

--- 导出日志文件:按照查询时间排序的 10 条 SQL
mysqldumpslow -s -t 10 slow.log

慢查询的优化首先要搞明白慢的原因是什么! 是查询条件没有命中索引?是加载了不需要的数据列?还是数据量太大?

所以优化也是针对这三个方向来的。

  • 首先分析语句,看看是否 load 了额外的数据,可能是查询了多余的行并且抛弃掉了,可能是加载了许多结果中并不需要的列,对语句进行分析以及重写。
  • 分析语句的执行计划,然后获得其使用索引的情况,之后修改语句或者修改索引,使得语句可以尽可能的命中索引。
  • 如果对语句的优化已经无法进行,可以考虑表中的数据量是否太大,如果是的话可以进行横向或者纵向的分表。

如何优化查询过程中的数据访问

  • 访问数据太多导致查询性能下降
  • 确定应用程序是否在检索大量超过需要的数据,可能是太多行或列
  • 确认 MySQL 服务器是否在分析大量不必要的数据行
  • 查询不需要的数据。解决办法:使用limit解决
  • 多表关联返回全部列。解决办法:指定列名
  • 总是返回全部列。解决办法:避免使用SELECT *
  • 重复查询相同的数据。解决办法:可以缓存数据,下次直接读取缓存
  • 是否在扫描额外的记录。解决办法:使用 explain 进行分析,如果发现查询需要扫描大量的数据,但只返回少数的行,可以通过如下技巧去优化:使用索引覆盖扫描,把所有的列都放到索引中,这样存储引擎不需要回表获取对应行就可以返回结果。
  • 改变数据库和表的结构,修改数据表范式
  • 重写 SQL 语句,让优化器可以以更优的方式执行查询。

数据库结构优化

一个好的数据库设计方案对于数据库的性能往往会起到事半功倍的效果。

需要考虑数据冗余、查询和更新的速度、字段的数据类型是否合理等多方面的内容。

  1. 将字段很多的表分解成多个表

对于字段较多的表,如果有些字段的使用频率很低,可以将这些字段分离出来形成新表。

因为当一个表的数据量很大时,会由于使用频率低的字段的存在而变慢。

  1. 增加中间表

对于需要经常联合查询的表,可以建立中间表以提高查询效率。

通过建立中间表,将需要通过联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表的查询。

  1. 增加冗余字段

设计数据表时应尽量遵循范式理论的规约,尽可能的减少冗余字段,让数据库设计看起来精致、优雅。但是,合理的加入冗余字段可以提高查询速度。

表的规范化程度越高,表和表之间的关系越多,需要连接查询的情况也就越多,性能也就越差。

注意:

冗余字段的值在一个表中修改了,就要想办法在其他表中更新,否则就会导致数据不一致的问题。

大表怎么优化?

类似的问题:某个表有近千万数据,CRUD 比较慢,如何优化?分库分表了是怎么做的?分表分库了有什么问题?有用到中间件么?他们的原理知道么?

当 MySQL 单表记录数过大时,数据库的 CRUD 性能会明显下降,一些常见的优化措施如下:

  • 限定数据的范围: 务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
  • 读/写分离: 经典的数据库拆分方案,主库负责写,从库负责读;
  • 缓存: 使用 MySQL 的缓存,另外对重量级、更新少的数据可以考虑;
  • 通过分库分表的方式进行优化,主要有垂直分表和水平分表。

MySQL 中的 group by 和 having 的使用

MySQL 中,当我们用到聚合函数,如 sum,count 后,又需要筛选条件时,having就派上用场了,因为 WHERE 是在聚合前筛选记录的,havinggroup by是组合着用的。

  • 统计相同姓名的用户数量,这个时候可以用group by实现
select id,username,count(id) nums from t_user group by username;
  • 根据上面的需求,统计重名人数高于 100 的名字,这个时候就可以用到having
select id,username,count(id) nums from t_user group by username having nums > 100

注意:having后的判断字段必须是聚合函数返回的结果。


文章作者: 程序猿洞晓
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 程序猿洞晓 !
评论
 上一篇
Java问答知识总结篇-Nginx Java问答知识总结篇-Nginx
以问答的方式构建Java知识体系,另外在问答中提供技术博客的支撑,更具体的解析和剖析内部深度知识点,做一个有深度的问答总结集。内容包括Java基础知识、JVM、Spring、Spring Cloud && Spring Cloud Alibaba、Mybatis、Spring Boot、Mybatis、Redis、MySQL等等。
2022-09-21
下一篇 
Java问答知识总结篇-Spring Cloud Java问答知识总结篇-Spring Cloud
以问答的方式构建Java知识体系,另外在问答中提供技术博客的支撑,更具体的解析和剖析内部深度知识点,做一个有深度的问答总结集。内容包括Java基础知识、JVM、Spring、Spring Cloud && Spring Cloud Alibaba、Mybatis、Spring Boot、Mybatis、Redis、MySQL等等。
2022-09-21
  目录