跳至主要內容

MySQL 常见面试题


MySQL 字段类型

整数类型的 UNSIGNED 属性有什么用?

MySQL 中的整数类型可以使用可选的 UNSIGNED 属性来表示不允许负值的无符号整数。使用 UNSIGNED 属性可以将正整数的上限提高一倍,因为它不需要存储负数值。

例如, TINYINT UNSIGNED 类型的取值范围是 0 ~ 255,而普通的 TINYINT 类型的值范围是 -128 ~ 127。INT UNSIGNED 类型的取值范围是 0 ~ 4,294,967,295,而普通的 INT 类型的值范围是 -2,147,483,648 ~ 2,147,483,647。

对于从 0 开始递增的 ID 列,使用 UNSIGNED 属性可以非常适合,因为不允许负值并且可以拥有更大的上限范围,提供了更多的 ID 值可用。

char和varchar的区别

CHAR

  • CHAR类型用于存储固定长度字符串:MySQL总是根据定义的字符串长度分配足够的空间。当存储CHAR值时,MySQL会删除字符串中的末尾空格同时,CHAR值会根据需要采用空格进行剩余空间填充,以方便比较和检索。但正因为其长度固定,所以会占据多余的空间,也是一种空间换时间的策略;
  • CHAR适合存储很短或长度近似的字符串。例如,CHAR非常适合存储密码的MD5值、定长的身份证等,因为这些是定长的值
  • 对于经常变更的数据,CHAR也比VARCHAR更好,因为定长的CHAR类型占用磁盘的存储空间是连续分配的,不容易产生碎片。
  • 对于非常短的列,CHAR比VARCHAR在存储空间上也更有效率。例如用CHAR(1)来存储只有Y和N的值,如果采用单字节字符集只需要一个字节,但是VARCHAR(1)却需要两个字节,因为还有一个记录长度的额外字节。

VARCHAR:

  • VARCHAR类型用于存储可变长度字符串,是最常见的字符串数据类型。它比固定长度类型更节省空间,因为它仅使用必要的空间(根据实际字符串的长度改变存储空间)。

  • VARCHAR需要使用1或2个额外字节记录字符串的长度:如果列的最大长度小于或等于255字节,则只使用1个字节表示,否则使用2个字节。假设采用latinl字符集,一个VARCHAR(10)的列需要11个字节的存储空间。VARCHAR(1000)的列则需要1002 个字节,因为需要2个字节存储长度信息。

  • VARCHAR节省了存储空间,所以对性能也有帮助。但是,由于行是变长的,在UPDATE时可能使行变得比原来更长,这就导致需要做额外的工作。如果一个行占用的空间增长,并且在页内没有更多的空间可以存储,在这种情况下,不同的存储引擎的处理方式是不一样的。例如,MylSAM会将行拆成不同的片段存储,InnoDB则需要分裂页来使行可以放进页内。

  • 操作内存的方式:对于varchar数据类型来说,硬盘上的存储空间虽然都是根据字符串的实际长度来存储空间的,但在内存中是根据varchar类型定义的长度来分配占用的内存空间的,而不是根据字符串的实际长度来分配的。显然,这对于排序和临时表会较大的性能影响。

VARCHAR(100)和 VARCHAR(10)的区别是什么?

VARCHAR(100)和 VARCHAR(10)都是变长类型,表示能存储最多 100 个字符和 10 个字符。因此,VARCHAR (100) 可以满足更大范围的字符存储需求,有更好的业务拓展性。而 VARCHAR(10)存储超过 10 个字符时,就需要修改表结构才可以。

虽说 VARCHAR(100)和 VARCHAR(10)能存储的字符范围不同,但二者存储相同的字符串,所占用磁盘的存储空间其实是一样的,这也是很多人容易误解的一点。

不过,VARCHAR(100) 会消耗更多的内存。这是因为 VARCHAR 类型在内存中操作时,通常会分配固定大小的内存块来保存值,即使用字符类型中定义的长度。例如在进行排序的时候,VARCHAR(100)是按照 100 这个长度来进行的,也就会消耗更多内存。

DECIMAL 和 FLOAT/DOUBLE 的区别是什么?

DECIMAL 和 FLOAT 的区别是:DECIMAL 是定点数,FLOAT/DOUBLE 是浮点数。DECIMAL 可以存储精确的小数值,FLOAT/DOUBLE 只能存储近似的小数值。

DECIMAL 用于存储具有精度要求的小数,例如与货币相关的数据,可以避免浮点数带来的精度损失。

在 Java 中,MySQL 的 DECIMAL 类型对应的是 Java 类 java.math.BigDecimal

int(10)和char(10)的区别?

int(10)中的10表示的是显示数据的长度,而char(10)表示的是存储数据的长度。

为什么不推荐使用 TEXT 和 BLOB?

数据库规范通常不推荐使用 BLOB 和 TEXT 类型,这两种类型具有一些缺点和限制,例如:

  • 不能有默认值。
  • 在使用临时表时无法使用内存临时表,只能在磁盘上创建临时表(《高性能 MySQL》书中有提到)。
  • 检索效率较低。
  • 不能直接创建索引,需要指定前缀长度。
  • 可能会消耗大量的网络和 IO 带宽。
  • 可能导致表上的 DML 操作变慢。
  • ……

DATETIME 和 TIMESTAMP 的区别是什么?

DATETIME 类型没有时区信息,TIMESTAMP 和时区有关。

TIMESTAMP 只需要使用 4 个字节的存储空间,但是 DATETIME 需要耗费 8 个字节的存储空间。但是,这样同样造成了一个问题,Timestamp 表示的时间范围更小。

  • DATETIME:1000-01-01 00:00:00 ~ 9999-12-31 23:59:59
  • Timestamp:1970-01-01 00:00:01 ~ 2037-12-31 23:59:59

Boolean 类型如何表示?

MySQL 中没有专门的布尔类型,而是用 TINYINT(1) 类型来表示布尔值。TINYINT(1) 类型可以存储 0 或 1,分别对应 false 或 true。

为什么不建议使用null作为默认值

Mysql不建议用Null作为列默认值不是因为不能使用索引,而是因为:

  • 索引列存在 NULL 就会导致优化器在做索引选择的时候更加复杂,更加难以优化。比如进行索引统计时,count(1),max(),min() 会省略值为NULL 的行
  • NULL 值是一个没意义的值,但是它会占用物理空间,所以会带来的存储空间的问题,因为 InnoDB 存储记录的时候,如果表中存在允许为 NULL 的字段,那么行格式 (opens new window)中至少会用 1 字节空间存储 NULL 值列表。建议用""或默认值0来代替NULL

不建议使用null作为默认值,并且建议必须设置默认值,原因如下:

  • 既然都不可为空了,那就必须要有默认值,否则不插入这列的话,就会报错;
  • 数据库不应该是用来查问题的,不能靠mysql报错来告知业务有问题,该不该插入应该由业务说了算;
  • 对于DBA来说,允许使用null是没有规范的,因为不同的人不同的用法。

但像合同生效时间获奖时间 等这种不可控字段,是可以不设置默认值的,但同样需要not null

为什么禁止使用外键

  • 外键会降低数据库的性能。在MySQL中,外键会自动加上索引,这会使得对该表的查询等操作变得缓慢,尤其是在大型数据表中。
  • 外键也会限制了表结构的调整和更改。在实际应用中,表结构经常需要进行更改,而如果表之间使用了外键约束,这些更改可能会非常难以实现。因为更改一个表的结构,需要涉及到所有以其为父表的子表,这会导致长时间锁定整个数据库表,甚至可能会导致数据丢失。
  • 在MySQL中,外键约束可能还会引发死锁问题。当想要对多个表中的数据进行插入、更新、删除操作时,由于外键约束的存在,可能会导致死锁,需要等待其他事务释放锁。
  • MySQL中使用外键还会增加开发难度。开发人员需要处理数据在表之间的关系,而这样的处理需要花费更多的时间和精力,以及对数据库的深入理解。同时,外键也会增加代码的复杂度,使得SQL语句变得难以理解和调试。

在阿里巴巴开发手册中也有提到,传送门

使用自增主键有什么好处?

自增主键可以让主键索引尽量地保持递增顺序插入,避免了页分裂,因此索引更紧凑,在查询的时候,效率也就更高。

自增主键保存在什么地方?

不同的引擎对于自增值的保存策略不同:

  • MyISAM引擎的自增值保存在数据文件中。
  • 在MySQL8.0以前,InnoDB引擎的自增值是存在内存中。MySQL重启之后内存中的这个值就丢失了,每次重启后第一次打开表的时候,会找自增值的最大值max(id),然后将最大值加1作为这个表的自增值;MySQL8.0版本会将自增值的变更记录在redo log中,重启时依靠redo log恢复。

自增主键一定是连续的吗?

不一定,有几种情况会导致自增主键不连续。

1、唯一键冲突导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果违反表中定义的唯一索引的唯一约束,会导致插入数据失败。此时表的自增主键的键值是会向后加1滚动的。下次再次插入数据的时候,就不能再使用上次因插入数据失败而滚动生成的键值了,必须使用新滚动生成的键值。

2、事务回滚导致自增主键不连续。当我们向一个自增主键的InnoDB表中插入数据的时候,如果显式开启了事务,然后因为某种原因最后回滚了事务,此时表的自增值也会发生滚动,而接下里新插入的数据,也将不能使用滚动过的自增值,而是需要重新申请一个新的自增值。

3、批量插入导致自增值不连续。MySQL有一个批量申请自增id的策略:

  • 语句执行过程中,第一次申请自增id,分配1个自增id
  • 1个用完以后,第二次申请,会分配2个自增id
  • 2个用完以后,第三次申请,会分配4个自增id
  • 依次类推,每次申请都是上一次的两倍(最后一次申请不一定全部使用)

如果下一个事务再次插入数据的时候,则会基于上一个事务申请后的自增值基础上再申请。此时就出现自增值不连续的情况出现。

4、自增步长不是1,也会导致自增主键不连续。

InnoDB的自增值为什么不能回收利用?

主要为了提升插入数据的效率和并行度。

假设有两个并行执行的事务,在申请自增值的时候,为了避免两个事务申请到相同的自增 id,肯定要加锁,然后顺序申请。

假设事务 A 申请到了 id=2, 事务 B 申请到 id=3,那么这时候表 t 的自增值是 4,之后继续执行。

事务 B 正确提交了,但事务 A 出现了唯一键冲突。

如果允许事务 A 把自增 id 回退,也就是把表 t 的当前自增值改回 2,那么就会出现这样的情况:表里面已经有 id=3 的行,而当前的自增 id 值是 2。

接下来,继续执行的其他事务就会申请到 id=2,然后再申请到 id=3。这时,就会出现插入语句报错“主键冲突”。

而为了解决这个主键冲突,有两种方法:

  • 每次申请 id 之前,先判断表里面是否已经存在这个 id。如果存在,就跳过这个 id。但是,这个方法的成本很高。因为,本来申请 id 是一个很快的操作,现在还要再去主键索引树上判断 id 是否存在。
  • 把自增 id 的锁范围扩大,必须等到一个事务执行完成并提交,下一个事务才能再申请自增 id。这个方法的问题,就是锁的粒度太大,系统并发能力大大下降。

可见,这两个方法都会导致性能问题。

因此,InnoDB 放弃了“允许自增 id 回退”这个设计,语句执行失败也不回退自增 id。

自增主键会遇到什么问题?

使用数据库的自增主键,虽然在很多情况下都很方便,但也可能遇到一些问题:

  • 插入性能问题:对于非常高并发的插入操作,自增主键可能会成为性能瓶颈。因为每次插入新记录时,都需要获取一个新的自增ID,这个操作是串行的,无法并发执行。
  • 主键耗尽:如果表的记录非常多,可能会出现自增主键耗尽的情况。尤其是对于定义为整型的自增主键,如果插入的记录数超过了整型的最大值,就无法再插入新的记录。
  • 分布式系统问题:在分布式系统中,如果多个数据库节点都需要生成自增主键,就需要保证生成的主键在全局是唯一的。这通常需要引入额外的机制或工具,比如分布式ID生成器。

utf8 、utf8mb3和 utf8mb4的区别

utf8mb3:只支持最长三个字节的BMP(Basic Multilingual Plane,基本多文种平面)字符(不支持补充字符)。

utf8mb4:mb4即 most bytes 4,即最多使用4个字节来表示完整的UTF-8,具有以下特征:

  • 支持BMP和补充字符。
  • 每个多字节字符最多需要四个字节。

utf8mb4是utf8的超集并完全兼容它,是MySQL 在 5.5.3 版本之后增加的一个新的字符集,能够用四个字节存储更多的字符,几乎包含了世界上所有能看到见的语言字符。

  • 差异比较
差异点utf8mb3utf8mb4
最大使用字节数34
支持字符类型BMPBMP+其它字符
字符类型常见的 Unicode 字符常见的 Unicode 字符 + 部分罕用汉字 + emoji表情 + 新增的 Unicode 字符等
Unicode范围U0000 - U+FFFF(即BMP)U0000 - U+10FFFF
占用存储空间略小(如CHAR(10) 需要10 * 3 = 30 个字节的空间;VARCHAR 类型需要额外使用1个字节来记录字符串的长度)稍大(如CHAR(10) 需要 10 * 4 = 40 个字节的空间;VARCHAR 类型需要额外使用2个字节来记录字符串的长度)
兼容性切换至utf8mb4 一般不会有问题,但要注意存储空间够不够、排序规则是否变化切换至utf8mb3可能会有问题,字符丢失、报错或乱码
安全性稍低,更容易被恶意字符串攻击较高,保留恶意字符串,然后报错或乱码提示

如何选择?一句话就是,根据具体的业务需求和实际情况,选择最合适的字符集。

Mysql基本架构

详细内容查看:Mysql语句执行流程

MySQL主要分为 Server 层和存储引擎层:

  • Server 层:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
  • 存储引擎: 主要负责数据的存储和读取。server 层通过api与存储引擎进行通信。

MySQL Server 层主要由下面几部分构成:

  • 连接器:建立连接,管理连接、校验用户身份;
  • 查询缓存:查询语句如果命中查询缓存则直接返回,否则继续往下执行。MySQL 8.0 已删除该模块;
  • 解析 SQL,通过解析器对 SQL 查询语句进行词法分析、语法分析,然后构建语法树,方便后续模块读取表名、字段、语句类型;
  • 执行 SQL:执行 SQL 共有三个阶段:
    • 预处理阶段:检查表或字段是否存在;将 select * 中的 * 符号扩展为表上的所有列。
    • 优化阶段:基于查询成本的考虑, 选择查询成本最小的执行计划(选择使用哪个索引);
    • 执行阶段:根据执行计划执行 SQL 查询语句,从存储引擎读取记录,返回给客户端;

查询语句执行流程?

查询语句的执行流程如下:权限校验、查询缓存、分析器、优化器、权限校验、执行器、引擎。

举个例子,查询语句如下:

select * from user where id > 1 and name = '大彬';
  1. 首先检查权限,没有权限则返回错误;
  2. MySQL8.0以前会查询缓存,缓存命中则直接返回,没有则执行下一步;
  3. 词法分析和语法分析。提取表名、查询条件,检查语法是否有错误;
  4. 两种执行方案,先查 id > 1 还是 name = '大彬',优化器根据自己的优化算法选择执行效率最好的方案;
  5. 校验权限,有权限就调用数据库引擎接口,返回引擎的执行结果。

更新语句执行过程?

更新语句执行流程如下:分析器、权限校验、执行器、引擎、redo logprepare状态)、binlogredo logcommit状态)

举个例子,更新语句如下:

update user set name = '大彬' where id = 1;
  1. 先查询到 id 为1的记录,有缓存会使用缓存。
  2. 拿到查询结果,将 name 更新为大彬,然后调用引擎接口,写入更新数据,innodb 引擎将数据保存在内存中,同时记录redo log,此时redo log进入 prepare状态。
  3. 执行器收到通知后记录binlog,然后调用引擎接口,提交redo logcommit状态。
  4. 更新完成。

为什么记录完redo log,不直接提交,而是先进入prepare状态?

假设先写redo log直接提交,然后写binlog,写完redo log后,机器挂了,binlog日志没有被写入,那么机器重启后,这台机器会通过redo log恢复数据,但是这个时候binlog并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。

存储引擎

常见的存储引擎有哪些?

MySQL中常用的四种存储引擎分别是: MyISAMInnoDBMEMORYARCHIVE。MySQL 5.5版本后默认的存储引擎为InnoDB

InnoDB存储引擎

InnoDB是MySQL默认的事务型存储引擎,使用最广泛,基于聚簇索引建立的。InnoDB内部做了很多优化,如能够自动在内存中创建自适应hash索引,以加速读操作。

优点:支持事务和崩溃修复能力;引入了行级锁和外键约束。

缺点:占用的数据空间相对较大。

适用场景:需要事务支持,并且有较高的并发读写频率。

MyISAM存储引擎

数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,可以使用MyISAM引擎。MyISAM会将表存储在两个文件中,数据文件.MYD和索引文件.MYI

优点:访问速度快。

缺点:MyISAM不支持事务和行级锁,不支持崩溃后的安全恢复,也不支持外键。

适用场景:对事务完整性没有要求;表的数据都会只读的。

MEMORY存储引擎

MEMORY引擎将数据全部放在内存中,访问速度较快,但是一旦系统奔溃的话,数据都会丢失。

MEMORY引擎默认使用哈希索引,将键的哈希值和指向数据行的指针保存在哈希索引中。

优点:访问速度较快。

缺点

  1. 哈希索引数据不是按照索引值顺序存储,无法用于排序。
  2. 不支持部分索引匹配查找,因为哈希索引是使用索引列的全部内容来计算哈希值的。
  3. 只支持等值比较,不支持范围查询。
  4. 当出现哈希冲突时,存储引擎需要遍历链表中所有的行指针,逐行进行比较,直到找到符合条件的行。

ARCHIVE存储引擎

ARCHIVE存储引擎非常适合存储大量独立的、作为历史记录的数据。ARCHIVE提供了压缩功能,拥有高效的插入速度,但是这种引擎不支持索引,所以查询性能较差。

MyISAM和InnoDB的区别?

  1. 存储结构的区别。每个MyISAM在磁盘上存储成三个文件。文件的名字以表的名字开始,扩展名指出文件类型。 .frm文件存储表定义。数据文件的扩展名为.MYD (MYData)。索引文件的扩展名是.MYI (MYIndex)。InnoDB所有的表都保存在同一个数据文件中(也可能是多个文件,或者是独立的表空间文件),InnoDB表的大小只受限于操作系统文件的大小,一般为2GB。
  2. 存储空间的区别。MyISAM支持支持三种不同的存储格式:静态表(默认,但是注意数据末尾不能有空格,会被去掉)、动态表、压缩表。当表在创建之后并导入数据之后,不会再进行修改操作,可以使用压缩表,极大的减少磁盘的空间占用。InnoDB需要更多的内存和存储,它会在主内存中建立其专用的缓冲池用于高速缓冲数据和索引。
  3. 可移植性、备份及恢复。MyISAM数据是以文件的形式存储,所以在跨平台的数据转移中会很方便。在备份和恢复时可单独针对某个表进行操作。对于InnoDB,可行的方案是拷贝数据文件、备份 binlog,或者用mysqldump,在数据量达到几十G的时候就相对麻烦了。
  4. 是否支持行级锁。MyISAM 只支持表级锁,用户在操作myisam表时,select,update,delete,insert语句都会给表自动加锁,如果加锁以后的表满足insert并发的情况下,可以在表的尾部插入新的数据。而InnoDB 支持行级锁和表级锁,默认为行级锁。行锁大幅度提高了多用户并发操作的性能。
  5. 是否支持事务和崩溃后的安全恢复。 MyISAM 不提供事务支持。而InnoDB 提供事务支持,具有事务、回滚和崩溃修复能力。
  6. 是否支持外键。MyISAM不支持,而InnoDB支持。
  7. 是否支持MVCC。MyISAM不支持,InnoDB支持。应对高并发事务,MVCC比单纯的加锁更高效。
  8. 是否支持聚集索引。MyISAM不支持聚集索引,InnoDB支持聚集索引。
  9. 全文索引。MyISAM支持 FULLTEXT类型的全文索引。InnoDB不支持FULLTEXT类型的全文索引,但是innodb可以使用sphinx插件支持全文索引,并且效果更好。
  10. 表主键。MyISAM允许没有任何索引和主键的表存在,索引都是保存行的地址。对于InnoDB,如果没有设定主键或者非空唯一索引,就会自动生成一个6字节的主键(用户不可见)。
  11. 表的行数。MyISAM保存有表的总行数,如果select count(*) from table;会直接取出该值。InnoDB没有保存表的总行数,如果使用select count(*) from table;就会遍历整个表,消耗相当大,但是在加了where条件后,MyISAM和InnoDB处理的方式都一样。

mysql语句

exist和in的区别?

exists用于对外表记录做筛选。exists会遍历外表,将外查询表的每一行,代入内查询进行判断。当exists里的条件语句能够返回记录行时,条件就为真,返回外表当前记录。反之如果exists里的条件语句不能返回记录行,条件为假,则外表当前记录被丢弃。

select a.* from A awhere exists(select 1 from B b where a.id=b.id)

in是先把后边的语句查出来放到临时表中,然后遍历临时表,将临时表的每一行,代入外查询去查找。

select * from Awhere id in(select id from B)

子查询的表比较大的时候,使用exists可以有效减少总的循环次数来提升速度;当外查询的表比较大的时候,使用in可以有效减少对外查询表循环遍历来提升速度。

truncate、delete与drop区别?

  • TRUNCATE:快速清空表,但保留结构。适用于需要清空但不删除表的场景。
  • DELETE:逐行删除数据,可以选择性删除某些记录。适用于只想删除部分数据的场合,且支持事务。
  • DROP:删除整个表定义及其所有数据。适用于不再需要表结构或其数据的情况。

相同点:

  1. truncate和不带where子句的delete、以及drop都会删除表内的数据。
  2. droptruncate都是DDL语句(数据定义语言),执行后会自动提交。

不同点:

  1. truncate 和 delete 只删除数据不删除表的结构;drop 语句将删除表的结构被依赖的约束、触发器、索引;
  2. 一般来说,执行速度: drop > truncate > delete。

having和where区别?

  • 二者作用的对象不同,where子句作用于表和视图,having作用于组。
  • where在数据分组前进行过滤,having在数据分组后进行过滤。

count(*) 和 count(1)哪个快?

按照性能排序是:count(*) = count(1) > count(主键字段) > count(字段)

  1. count(主键字段)的执行过程:

比如说,id是主键字段。

  • 如果表里只有主键索引,那么,InnoDB 循环遍历聚簇索引,将读取到的记录返回给 server 层,然后读取记录中的 id 值,并根据 id 值判断是否为 NULL,如果不为 NULL,就将 count 变量加 1。
  • 如果表里有二级索引时,InnoDB 循环遍历的对象就不是聚簇索引,而是二级索引。因为相同数量的二级索引记录可以比聚簇索引记录占用更少的存储空间,所以二级索引树比聚簇索引树小,这样遍历二级索引的 I/O 成本比遍历聚簇索引的 I/O 成本小,因此「优化器」优先选择的是二级索引。
  1. count(1) 的执行过程:

如果表里只有主键索引,没有二级索引时。那么,InnoDB 循环遍历聚簇索引(主键索引),将读取到的记录返回给 server 层,但是不会读取记录中的任何字段的值,因为 count 函数的参数是 1,不是字段,所以不需要读取记录中的字段值。参数 1 很明显并不是 NULL,因此 server 层每从 InnoDB 读取到一条记录,就将 count 变量加 1。

显然,count(1) 相比 count(主键字段) 少一个步骤,就是不需要读取记录中的字段值,所以通常会说 count(1) 执行效率会比 count(主键字段) 高一点。

但是,如果表里有二级索引时,InnoDB 循环遍历的对象就二级索引了。

  1. count(*) 的执行过程(mysql官方文档推荐):

count(*) 其实等于 count(0),也就是说,当你使用 count(*) 时,MySQL 会将 * 参数转化为参数 0 来处理。

所以,count(*) 执行过程跟 count(1) 执行过程基本一样的,性能没有什么差异。

  1. count(字段) 的执行过程:采用全表扫描的方式来统计

MySQL多表查询时有哪些连接方式

当进行多表查询时,在 MySQL 中常用的连接方式有以下几种:

  • 内连接(INNER JOIN):返回同时满足连接条件的行。它通过比较连接列的值,将两个或多个表中匹配的行组合在一起。
  • 左外连接(LEFT JOIN):返回左表中的所有行,以及与左表匹配的右表的行。如果右表中没有匹配的行,对应的列将填充为 NULL。
  • 右外连接(RIGHT JOIN):返回右表中的所有行,以及与右表匹配的左表的行。如果左表中没有匹配的行,对应的列将填充为 NULL。
  • 自连接(Self JOIN):将单个表视为两个独立的表,使用别名来引用同一个表。这种连接适用于在同一个表中根据某些条件关联不同的行。
  • 交叉连接(CROSS JOIN):返回两个表的笛卡尔积,即所有可能的组合。它将第一个表的每一行与第二个表的每一行进行组合。

还有一个是全外连接(FULL JOIN):返回左右两个表中的所有行。如果某个表中没有匹配的行,对应的列将填充为 NULL。需要注意 MySQL 不支持 FULL JOIN 可以使用UNION ALL 模拟。

为什么大厂不建议使用多表join

最主要的原因就是join的效率比较低:MySQL是使用了嵌套循环(Nested-Loop Join)的方式来实现关联查询的,就是要通过两层循环,用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出。

  1. 性能问题
    • 多表 JOIN 会增加查询的复杂性,可能导致性能下降,特别是在数据量大时。
    • 数据库需要在执行查询时处理更多的行和列,这可能导致更高的 I/O 操作和内存使用。
  2. 可读性和维护性
    • 复杂的 JOIN 查询会使 SQL 语句变得难以理解,导致维护成本增加。
    • 当查询需要频繁修改时,复杂的 JOIN 会让代码更容易出错。
  3. 索引利用率
    • 多表 JOIN 可能会导致数据库无法有效利用索引,影响查询的优化。
    • 如果 JOIN 的字段没有适当的索引,查询性能会显著下降。
  4. 锁竞争
    • 多表 JOIN 可能导致更长时间的行锁或表锁,从而增加锁竞争的可能性,影响并发性能。
  5. 数据完整性
    • 复杂的 JOIN 查询可能掩盖数据问题或不一致性,使得调试较为困难。
    • 难以确保在 JOIN 查询中返回的数据符合业务逻辑和数据完整性要求。

那应该怎么写?

  • 分解查询:在内存中自己做关联,即先从数据库中把数据查出来之后,再次查询,然后再进行数据封装。
  • 考虑数据冗余:在某些情况下,可以考虑数据冗余来减少 JOIN 的需要。
  • 数据冗余,宽表:就是基于一定的join关系,把数据库中多张表的数据打平做一张大宽表,可以同步到ES或者干脆直接在数据库中直接查都可以

UNION 与UNION ALL 的区别?

UNION和UNION ALL是在SQL中用于合并查询结果集的操作符,它们之间存在以下区别:

  • UNION:UNION用于合并两个或多个查询结果集,并去除重复的行。它将多个查询的结果合并为一个结果集,并自动去除重复的行。在执行UNION操作时,数据库会进行额外的去重操作,这可能会带来一定的性能开销。
  • UNION ALL:UNION ALL同样用于合并查询结果集,但不去除重复的行。它将多个查询的结果简单地合并在一起,包括重复的行。相比于UNION,UNION ALL不进行去重操作,因此执行效率更高。

总结来说:在使用时,可以根据具体的需求来选择合适的操作符。如果需要去除重复的行,可以使用UNION;如果不需要去重,或者对性能要求较高,可以使用UNION ALL。需要注意的是,使用UNION或UNION ALL时,要求被合并的查询结果的列数和列类型保持一致。

where 1 = 1的作用?会影响性能吗?

WHERE 1=1 表达式的值始终为 TRUE,因此它不会对查询结果产生任何实际的限制或筛选作用。它的主要作用是为条件语句提供一个基础条件,方便在构建动态SQL时,后续的查询条件可以更加灵活地拼接。

对于性能而言,WHERE 1=1 表达式并不会造成明显的性能损失。数据库优化器会意识到 1=1 永远为 TRUE,因此它不会对查询的执行计划产生任何实际影响。WHERE 1=1 在执行时不会进行额外的计算,因为它本质上是一个常量布尔值。

  • 查询优化:MySQL数据库会在查询时自动优化查询条件,对于 WHERE 1=1 这样的条件,它不会引入额外的计算负担。优化器通常会跳过 1=1,并且直接处理剩下的查询条件。
  • 执行计划:在 MySQL 的查询执行计划中,WHERE 1=1 通常不会出现在最终的执行过程中。优化器会识别到它的无用性,并忽略它对查询结果的影响。
  • 影响微乎其微:如果查询中包含大量复杂的条件,WHERE 1=1 的影响几乎可以忽略不计。它只是一个常量,任何现代数据库都能轻松处理这样简单的条件

索引

详细内容请查看:Mysql索引分类

什么是索引?

索引是存储引擎用于提高数据库表的访问速度的一种数据结构。它可以比作一本字典的目录,可以帮你快速找到对应的记录。

索引一般存储在磁盘的文件中,它是占用物理空间的。

索引的优缺点?

优点:

  • 加快数据查找的速度
  • 为用来排序或者是分组的字段添加索引,可以加快分组和排序的速度
  • 加快表与表之间的连接

缺点:

  • 建立索引需要占用物理空间
  • 会降低表的增删改的效率,因为每次对表记录进行增删改,需要进行动态维护索引,导致增删改时间变长

索引的作用?

数据是存储在磁盘上的,查询数据时,如果没有索引,会加载所有的数据到内存,依次进行检索,读取磁盘次数较多。有了索引,就不需要加载所有数据,因为B+树的高度一般在2-4层,最多只需要读取2-4次磁盘,查询速度大大提升。

什么情况下需要建索引?

  1. 经常用于查询的字段;即对经常出现在 WHERE 子句中的列创建索引。
  2. 经常用于连接的字段建立索引,可以加快连接的速度
  3. 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度;例如对 ORDER BY、GROUP BY 或者 DISTINCT 操作中出现的列建立索引。

什么情况下不建索引?

  1. where条件中用不到的字段不适合建立索引
  2. 表记录较少。比如只有几百条数据,没必要加索引。
  3. 需要经常增删改。需要评估是否适合加索引
  4. 参与列计算的列不适合建索引
  5. 区分度不高的字段不适合建立索引,如性别,只有男/女/未知三个值。加了索引,查询效率也不会提高。

索引的数据结构

索引的数据结构主要有B+树和哈希表,对应的索引分别为B+树索引和哈希索引。InnoDB引擎的索引类型有B+树索引和哈希索引,默认的索引类型为B+树索引。

B+树索引

B+ 树是基于B 树和叶子节点顺序访问指针进行实现,它具有B树的平衡性,并且通过顺序访问指针来提高区间查询的性能。

在 B+ 树中,节点中的 key 从左到右递增排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1

进行查找操作时,首先在根节点进行二分查找,找到key所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出key所对应的数据项。

MySQL 数据库使用最多的索引类型是BTREE索引,底层基于B+树数据结构来实现。

mysql> show index from blog\G;
*************************** 1. row ***************************
        Table: blog
   Non_unique: 0
     Key_name: PRIMARY
 Seq_in_index: 1
  Column_name: blog_id
    Collation: A
  Cardinality: 4
     Sub_part: NULL
       Packed: NULL
         Null:
   Index_type: BTREE
      Comment:
Index_comment:
      Visible: YES
   Expression: NULL

哈希索引

哈希索引是基于哈希表实现的,对于每一行数据,存储引擎会对索引列进行哈希计算得到哈希码,并且哈希算法要尽量保证不同的列值计算出的哈希码值是不同的,将哈希码的值作为哈希表的key值,将指向数据行的指针作为哈希表的value值。这样查找一个数据的时间复杂度就是O(1),一般多用于精确查找。

为什么不用hash表

这与hash表的原理有关。

使用hash表的目的是为了尽可能的散列,因此在使用hash表的时候要选择hash算法,避免hash碰撞和hash冲突。

这就导致了hash表存储的数据是无序的,当需要进行范围查询的时候,只能挨个进行遍历对比,效率极低

所以:

  1. 不用hash表的原因在于不适合范围查询
  2. 容易导致全表扫描,因为可能存在不同的key经过hash运算后值相同
  3. 索引列上的值相同的话,易造成hash冲突,效率低下

为什么不用二叉树

如果将一个乱序的数据放入二叉树中,效率会高,但是如果数据是有顺序的,比如1、2、3、4、5,则二叉树将会编程一个链表的样式,失去了二叉树的优势

总结:有可能退化成链表,读取数据需要频繁IO

为什么不用红黑树(或二叉平衡树)

红黑树也是一种二叉平衡树,红黑树可以有效解决掉顺序数据一次放入二叉树而导致的形成链表的结果,但是红黑树一个节点只能存储一个数据,就导致如果是大量的数据,红黑树的高度就不可控,如果一个红黑树是20的高度,要查询的数据在叶子结点,则表示需要需磁盘20次IO,效率还是不高

总结:相比于二叉树能避免退化成链表,但IO次数还是会很高

为什么不用B树而用B+树

B树比红黑树的优势是,B树是一个节点上存储多个数据,比如磁盘的一页数据,这样的横向扩展,相同的数据量就可以比红黑树减少更多的高度,从而减少了磁盘的IO次数

B树:data为数据的磁盘地址,还可能是整条列的数据

  1. 数据是分散在每个结点上的,所有结点元素不重复,
  2. 结点元素从左到右递增
  3. 叶子结点具有相同的深度,叶子结点的指针为空

B+树:data为数据的磁盘地址,还可能是整条列的数据

  1. 叶子结点包含了所有的索引字段,以及数据的磁盘地址,而非叶子结点不再存储data数据,作用只是便于查找的冗余索引
  2. 非叶子结点是从子结点选取一页的开头来作为自己的值,指针为子结点那页的地址
  3. 每一个结点里的值都是排好序的
  4. 叶子结点之间是用「双向链表」进行连接,这样方便了范围查找,比如where col > 10,这是mysql对B+的变种,也是对比B树的一个优势
  5. 由于data可能会很大,非叶子结点在不存储data后,非叶子可以存储的元素则会变多,还可以降低树的高度,提高了查询的效率,这是与B树对比,B+树的一个优势

总结:B+树相比于B树,非叶子节点不再存储data数据,非叶子可以存储的元素则会变多,一个非叶子节点就可以存储更多的索引数据,更进一步降低树的高度,提高了查询的效率。相比于hash表,B+树利用叶子节点之间的指针可以进行范围查询

索引有什么分类?

  • 主键索引:名为primary的唯一非空索引,不允许有空值。

  • 唯一索引:索引列中的值必须是唯一的,但是允许为空值。唯一索引和主键索引的区别是:唯一索引字段可以为null且可以存在多个null值,而主键索引字段不可以为null。唯一索引的用途:唯一标识数据库表中的每条记录,主要是用来防止数据重复插入。

  • 组合索引:在表中的多个字段组合上创建的索引,只有在查询条件中使用了这些字段的左边字段时,索引才会被使用,使用组合索引时需遵循最左前缀原则。

  • 全文索引:只能在CHARVARCHARTEXT类型字段上使用全文索引。

  • 普通索引:普通索引是最基本的索引,它没有任何限制,值可以为空。

  • 前缀索引:前缀索引是指对字符类型字段的前几个字符建立的索引,而不是在整个字段上建立的索引,前缀索引可以建立在字段类型为 char、 varchar、binary、varbinary 的列上。

什么是最左匹配原则?

如果 SQL 语句中用到了组合索引中的最左边的索引,那么这条 SQL 语句就可以利用这个组合索引去进行匹配。当遇到范围查询(><betweenlike)就会停止匹配,后面的字段不会用到索引。

(a,b,c)建立索引,查询条件使用 a/ab/abc 会走索引,使用 bc 不会走索引。

(a,b,c,d)建立索引,查询条件为a = 1 and b = 2 and c > 3 and d = 4,那么a、b和c三个字段能用到索引,而d无法使用索引。因为遇到了范围查询。

如下图,对(a, b) 建立索引,a 在索引树中是全局有序的,而 b 是全局无序,局部有序(当a相等时,会根据b进行排序)。直接执行b = 2这种查询条件无法使用索引。

最左前缀
最左前缀

当a的值确定的时候,b是有序的。例如a = 1时,b值为1,2是有序的状态。当a = 2时候,b的值为1,4也是有序状态。 当执行a = 1 and b = 2时a和b字段能用到索引。而执行a > 1 and b = 2时,a字段能用到索引,b字段用不到索引。因为a的值此时是一个范围,不是固定的,在这个范围内b值不是有序的,因此b字段无法使用索引。

什么是聚集索引?

InnoDB使用表的主键构造主键索引树,同时叶子节点中存放的即为整张表的记录数据。聚集索引叶子节点的存储是逻辑上连续的,使用双向链表连接,叶子节点按照主键的顺序排序,因此对于主键的排序查找和范围查找速度比较快。

聚集索引的叶子节点就是整张表的行记录。InnoDB 主键使用的是聚簇索引。聚集索引要比非聚集索引查询效率高很多。

对于InnoDB来说,聚集索引一般是表中的主键索引,如果表中没有显示指定主键,则会选择表中的第一个不允许为NULL的唯一索引。如果没有主键也没有合适的唯一索引,那么InnoDB内部会生成一个隐藏的主键作为聚集索引,这个隐藏的主键长度为6个字节,它的值会随着数据的插入自增。

什么是覆盖索引?

select的数据列只用从索引中就能够取得,不需要回表进行二次查询,也就是说查询列要被所使用的索引覆盖。对于innodb表的二级索引,如果索引能覆盖到查询的列,那么就可以避免对主键索引的二次查询。

不是所有类型的索引都可以成为覆盖索引。覆盖索引要存储索引列的值,而哈希索引、全文索引不存储索引列的值,所以MySQL使用b+树索引做覆盖索引。

对于使用了覆盖索引的查询,在查询前面使用explain,输出的extra列会显示为using index

比如user_like 用户点赞表,组合索引为(user_id, blog_id)user_idblog_id都不为null

explain select blog_id from user_like where user_id = 13;

explain结果的Extra列为Using index,查询的列被索引覆盖,并且where筛选条件符合最左前缀原则,通过索引查找就能直接找到符合条件的数据,不需要回表查询数据。

explain select user_id from user_like where blog_id = 1;

explain结果的Extra列为Using where; Using index, 查询的列被索引覆盖,where筛选条件不符合最左前缀原则,无法通过索引查找找到符合条件的数据,但可以通过索引扫描找到符合条件的数据,也不需要回表查询数据。

索引的设计原则?

选择合适的列:

  • 经常用于查询的字段;即对经常出现在 WHERE 子句中的列创建索引。
  • 经常用于连接的字段建立索引,可以加快连接的速度
  • 经常需要排序的字段建立索引,因为索引已经排好序,可以加快排序查询速度;例如对 ORDER BY、GROUP BY 或者 DISTINCT 操作中出现的列建立索引。

优化多列索引:

  • 复合索引:对多个列组合查询的情况,考虑使用复合索引而非单个列索引,以减少表扫描次数。
  • 利用最左前缀原则:确保复合索引的列顺序遵循最左前缀原则,即将使用最频繁的列放在最前面。

控制索引的数量和类型 :

  • 避免过多索引: 索引有利于查询,但每个新增索引会影响插入、更新、删除的性能。应在查询效率与维护成本之间取得平衡。
  • 适用索引类型: 选择合适类型的索引,例如 B-tree 索引用于大多数情况,Full-text 索引用于全文搜索,Hash 索引用于等值查询(如内存表)。

考虑数据分布和选择性 :

  • 高选择性的列: 高基数、高选择性(具有许多不同值)的列索引往往能提高查询效率。
  • 均匀分布的数据: 索引在数据分布均匀时效果最佳,避免对只有少数不同值的列(低基数)设置索引。

避免冗余和重复索引

  • 合并索引:考虑合并类似用途的索引,减少冗余。
  • 定期清理:定期审查和清理不再使用或效果不佳的索引。

其他注意点:

  • 覆蓋索引: 当索引包含查询所需的所有列时,称为覆盖索引,能有效减少读取数据页的次数。尽量使查询使用覆盖索引。
  • 索引列的区分度越高,索引的效果越好。比如使用性别这种区分度很低的列作为索引,效果就会很差。
  • 避免给"大字段"建立索引。尽量使用数据量小的字段作为索引。因为MySQL在维护索引的时候是会将字段值一起维护的,那这样必然会导致索引占用更多的空间,另外在排序的时候需要花费更多的时间去对比。
  • 尽量使用短索引,对于较长的字符串进行索引时应该指定一个较短的前缀长度,因为较小的索引涉及到的磁盘I/O较少,查询速度更快。
  • 频繁增删改的字段不要建立索引。假设某个字段频繁修改,那就意味着需要频繁的重建索引,这必然影响MySQL的性能

区分度不高的字段建索引一定没用吗

区分度不高的字段建索引的问题:

  • 低效的过滤效果: 如果字段的选择性很低(例如只有几个不同的值),那么通过索引过滤数据的效果可能不明显,因为每个值对应太多的行。
  • 索引维护开销: 在更新或插入操作时,所有的索引需要同步更新,低选择性字段的索引可能带来不必要的开销,而收益有限。
  • 增大查询成本: 尽管索引帮助快速定位数据位置,但由于返回的数据集过大,可能会导致后续操作(如排序和过滤)带来额外的系统开销。

那何时低选择性索引可能有用呢 ?

  • 固定值查询: 即使选择性低,如果应用程序中有特定固定的值或者特定过滤条件经常被使用,并要求快速响应(如状态标志字段),索引仍可能提高查询速度。
  • 结合其他高选择性条件: 当低选择性的字段被结合其他有高选择性的字段一起使用时,可能仍然增益明显。
  • 作为覆盖索引的一部分: 如果一个多列索引(复合索引)包含一个低选择性字段,同时能使一个查询成为覆盖索引,这依然可以提升性能。
  • 大数据集中的特定场合: 在非常大的数据集中,占用大量数据的字段索引可能仍有助于减少所需的扫描行数。
  • 统计分析和报告: 如果索引用于统计或聚合操作中,通过索引可以更高效地进行分组操作。

以上属于特定情况,区分度不高的字段建立索引还是要依据具体应用场景和查询场景进行权衡和测试。

索引什么时候会失效?

导致索引失效的情况:

  • 对于组合索引,不是使用组合索引最左边的字段,则不会使用索引
  • 以%开头的like查询如%abc,无法使用索引;非%开头的like查询如abc%,相当于范围查询,会使用索引
  • 查询条件中列类型是字符串,没有使用引号,可能会因为类型不同发生隐式转换,使索引失效
  • 判断索引列是否不等于某个值时
  • 对索引列进行运算
  • 查询条件使用or连接,也会导致索引失效

什么是前缀索引?

有时需要在很长的字符列上创建索引,这会造成索引特别大且慢。使用前缀索引可以避免这个问题。

前缀索引是指对文本或者字符串的前几个字符建立索引,这样索引的长度更短,查询速度更快。

创建前缀索引的关键在于选择足够长的前缀以保证较高的索引选择性。索引选择性越高查询效率就越高,因为选择性高的索引可以让MySQL在查找时过滤掉更多的数据行。

什么是索引下推?

在没有索引下推的情况下,当查询使用复合索引时,MySQL可能需要访问主表来评估不能完全通过索引条件确定的行。例如,如果只有索引的部分条件在索引中能被使用,而其他条件需要读取实际行再进行筛选,传统方式可能会导致更多的全行读取。

在联合索引遍历过程中,对联合索引中包含的字段先做判断,在存储引擎层进行数据过滤,而不是在服务层过滤,直接过滤掉不满足条件的记录,利用索引现有的数据减少回表次数,就叫索引下推

Innodb加索引的时候会锁表吗

在 InnoDB 中,操作索引时是否锁表取决于具体的操作类型。有些情况可能会导致表锁,而有些情况则不会。

  • 在线添加索引(Online DDL):
    • 从 MySQL 5.6 开始,InnoDB 支持在线添加索引。这意味着在添加索引期间,表可以同时进行读取和写入操作,从而不会锁住整张表。
    • 在线 DDL 的特性允许通过特定配置,让表在加索引的同时保持可用状态(如 LOCK=NONE 选项)。
    • 默认情况下,即使是在线添加索引,也可能会对表的某些部分进行短暂的锁定,但不会长期锁表。需要注意的是,在不支持在线 DDL 的旧版本中,添加索引通常是涉及锁表的操作,尤其是在大量数据情况下。
  • 删除索引操作通常不需要长时间锁表,但在删除过程中可能进行短暂的锁定以保证元数据更新的一致性。
  • 对于某些索引修改,可能会引发锁表现象,尤其是在不支持在线 DDL 的情况下或在某些特定场景下,需要评估具体MySQL版本和参数配置。

Mysql索引/SQL优化

对索引使用左或者左右模糊匹配

在MySQL中,LIKE 模糊查询可能会导致性能问题,特别是当使用通配符 % 开头时,因为这通常会导致全表扫描,也就是 like %xx 或者 like %xx% 这两种方式 (左或者左右模糊匹配的时候) 都会造成索引失效。

  • 前缀匹配:一般使用LIKE 'prefix%'的形式,这种情况MySQL能够利用索引
SELECT * FROM users WHERE username LIKE 'seven%';
  • 对于一定需要匹配后缀的情况(即LIKE '%suffix'),可以创建一个辅助列存储反转字符串,并基于此列进行前缀匹配。
ALTER TABLE users ADD reversed_username VARCHAR(255); 
UPDATE users SET reversed_username = REVERSE(username); 
CREATE INDEX idx_reversed_username ON users(reversed_username);

计算(使用函数)

因为索引保存的是索引字段的原始值,而不是经过计算后的值,自然就没办法走索引了。

函数计算或者表达式计算都没办法走索引

//函数计算
select * from t_user where length(name)=6;

//表达式计算
select * from t_user where id + 1 = 10;

不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。

但是在索引范围之外使用函数是不影响索引的: 即函数不在索引范围(即WHERE或ON条件)使用时,索引仍然有效。例如,在 SELECT 列表、ORDER BY、GROUP BY等地方使用函数通常不影响索引的使用。

类型转换

  1. 如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,那么这条语句会走全表扫描。
  2. 但是如果索引字段是整型类型,查询条件中的输入参数即使字符串,是不会导致索引失效,还是可以走索引扫描。

原因在于MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。也就是说,如果索引字段是整型类型,查询条件中的输入参数是字符串,会自动转换成整型,所以索引不会失效。而索引字段是字符串,而输入的是整型,由于是字符串转数字,而索引不是整型类型,所以索引失效了。

因此在使用sql语句时:数值类型禁止加引号,字符串类型必须加引号

联合索引非最左匹配

联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配,否则就会导致索引失效。

原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序。

比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:

  • where a=1;
  • where a=1 and b=2 and c=3;
  • where a=1 and b=2;

因为有查询优化器,所以 a 字段在 where 子句的顺序并不重要。

但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:

  • where b=2;
  • where c=3;
  • where b=2 and c=3;

对于where a = 1 and c = 0 这个语句,前面的a = 1是会走索引的,后面的c不走索引。

where条件的顺序影响索引使用吗

在MySQL中,WHERE条件的顺序确实可能影响索引的使用,尤其是在使用复合索引(多列索引)的情况下。但是,这种影响主要取决于MySQL查询优化器的工作方式,而不是直接由WHERE子句中条件的书写顺序决定的。

  • MySQL的查询优化器通常会尝试重新排列WHERE条件以最优化索引的使用。
  • 对于复合索引,索引的使用遵循"最左前缀"原则。
  • 虽然WHERE条件的顺序通常不会影响索引的使用,但将索引列的条件放在前面可能会使查询计划更易读和理解。

不应使用 or

在 WHERE 子句中or,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。

OR 的含义就是两个只要满足一个即可,因此只有一个条件列是索引列是没有意义的,只要有条件列不是索引列,就会进行全表扫描。

in

尽量使用IN代替OR。但是IN包含的值不应过多,应少于1000个。

因为 IN 通常是走索引的,当IN后面的数据在数据表中超过30%的匹配时是全表的扫描,不会走索引

其实就是 Mysql优化器会根据当前表的情况选择最优解。 Mysql优化器认为走全表扫描 比 走索引+回表快 那就不会走索引

范围查询阻断,后续字段不能走索引

索引

KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`)

SQL语句

select * from _order where shop_id = 1 and created_at > '2021-01-01 00:00:00' and order_status = 10

范围查询还有“IN、between”

相关原理可以看这篇文章 唯一索引范围查询

覆盖索引优化

覆盖索引是指 SQL 中 查询的所有字段,在这个二级索引 B+Tree 的叶子节点上都能找得到那些字段,从二级索引中查询得到记录,而不需要通过聚簇索引查询获得,就可以避免回表的操作。

asc和desc混用

select * from _t where a=1 order by b desc, c asc

desc 和asc混用时会导致索引失效

避免更新索引列值

每当索引列的值发生变化时,数据库必须更新相应的索引结构,更新索引列值可能导致这些树结构的重平衡或重新构建,增加了额外的计算和I/O开销。

不等于、不包含不能用到索引的快速搜索

select * from _order where shop_id=1 and order_status not in (1,2)
select * from _order where shop_id=1 and order_status != 1

在索引上,避免使用NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等

not in一定不走索引吗?

答案是不一定。Mysql优化器会根据当前表的情况选择最优解。

主要在于如果 MySQL 认为 全表扫描 比 走索引+回表效率高, 那么他会选择全表扫描。

重要SQL必须被索引

update、delete的where条件列、order by、group by、distinct字段、多表join字段(on后面的字段)

例如:select id from table_a where name = 'seven' order by address ; 此时建立 name + address的联合索引比较好(此处name条件必须是 = ,如果是范围则无效);如果是order by主键,则只需要在name字段建立索引即可,因为name索引表中是包含主键的,也就是所谓了避免了回表操作。

避免使用子查询

通常情况下,一般建议使用连接查询代替子查询,原因如下:

连接查询(JOIN)子查询
在执行连接查询时,数据库会根据查询优化器的策略将多个表的数据进行合并,然后进行过滤和选择子查询要先执行内部查询,然后再使用其结果进行外部查询。嵌套子查询需要多次扫描数据,并且每次子查询都可能会触发独立的扫描操作,这增加了开销。
数据库优化器在处理连接查询时有更多的优化手段,如排序合并连接、哈希连接和嵌套循环连接等。优化器可以根据统计信息和查询结构进行调整,选择最优的执行计划。子查询有时不能充分利用优化器的优化策略,特别是在嵌套子查询的情况下,优化器可能会生成次优的执行计划。
由于连接查询一次性扫描多个表并进行合并,所以可以充分利用数据缓存,减少I/O操作。子查询可能会导致多次扫描相同的数据,特别是在嵌套子查询和相关子查询的情况下,子查询每次执行都可能触发新的数据扫描,增加了I/O开销。
可以通过JOIN条件有效地过滤数据,减少中间结果的大小。可能会产生较大的中间结果集,需要多次筛选和处理,增加了内存和计算的开销。

order by的坑

已知存在 custom_id 和 order_date 的联合索引

在对数据进行,custom_id 排序的情况下,再对 order_date 进行排序。

SELECT customer_id from orders order by customer_id, order_date

耗时 0.669 秒

当调换排序顺序,就无法走索引了,此时针对custom_id的索引排序就是失效了。

SELECT customer_id from orders order by order_date,customer_id

耗时 1.645 秒

即order by也需满足联合索引的最左匹配原则

同时默认情况下,索引是升序的,如果需要降序排序,那么索引也会失效!!!

SELECT` `customer_id ``from` `orders ``order` `by` `customer_id ``desc``, order_date

MySQL查询 limit 1000,10 和limit 10 速度一样快吗?

两种查询方式。对应 limit offset, sizelimit size 两种方式。

而其实 limit size ,相当于 limit 0, size。也就是从0开始取size条数据。

也就是说,两种方式的区别在于offset是否为0。

先来看下limit sql的内部执行逻辑。

MySQL内部分为server层存储引擎层。一般情况下存储引擎都用innodb。

server层有很多模块,其中需要关注的是执行器是用于跟存储引擎打交道的组件。

执行器可以通过调用存储引擎提供的接口,将一行行数据取出,当这些数据完全符合要求(比如满足其他where条件),则会放到结果集中,最后返回给调用mysql的客户端

以主键索引的limit执行过程为例:

执行select * from xxx order by id limit 0, 10;,select后面带的是星号,也就是要求获得行数据的所有字段信息。

server层会调用innodb的接口,在innodb里的主键索引中获取到第0到10条完整行数据,依次返回给server层,并放到server层的结果集中,返回给客户端。

把offset搞大点,比如执行的是:select * from xxx order by id limit 500000, 10;

server层会调用innodb的接口,由于这次的offset=500000,会在innodb里的主键索引中获取到第0到(500000 + 10)条完整行数据返回给server层之后根据offset的值挨个抛弃,最后只留下最后面的size条,也就是10条数据,放到server层的结果集中,返回给客户端。

可以看出,当offset非0时,server层会从引擎层获取到很多无用的数据,而获取的这些无用数据都是要耗时的。

因此,mysql查询中 limit 1000,10 会比 limit 10 更慢。原因是 limit 1000,10 会取出1000+10条数据,并抛弃前1000条,这部分耗时更大。

深分页怎么优化?

还是以上面的SQL为空:select * from xxx order by id limit 500000, 10;

方法一:延迟关联

从上面的分析可以看出,当offset非常大时,server层会从引擎层获取到很多无用的数据,而当select后面是*号时,就需要拷贝完整的行信息,拷贝完整数据相比只拷贝行数据里的其中一两个列字段更耗费时间。

因为前面的offset条数据最后都是不要的,没有必要拷贝完整字段,所以可以将sql语句修改成:

select * from xxx  where id >=(select id from xxx order by id limit 500000, 1) order by id limit 10;

先执行子查询 select id from xxx by id limit 500000, 1, 这个操作,其实也是将在innodb中的主键索引中获取到500000+1条数据,然后server层会抛弃前500000条,只保留最后一条数据的id。

但不同的地方在于,在返回server层的过程中,只会拷贝数据行内的id这一列,而不会拷贝数据行的所有列,当数据量较大时,这部分的耗时还是比较明显的。

在拿到了上面的id之后,假设这个id正好等于500000,那sql就变成了

select * from xxx  where id >=500000 order by id limit 10;

这样innodb再走一次主键索引,通过B+树快速定位到id=500000的行数据,时间复杂度是lg(n),然后向后取10条数据。

方法二:根据id主键进行排序

记录上次查询的最大ID(或其他唯一标识符),并以此为起点进行下一次查询。这种方法需要有连续的、唯一的列(如自增ID)以用于分页。

select * from xxx where id > start_id order by id limit 10;

通过主键索引,每次定位到start_id的位置,然后往后遍历10个数据,这样不管数据多大,查询性能都较为稳定。

大表查询慢怎么优化?

某个表有近千万数据,查询比较慢,如何优化?

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

  • 合理建立索引。在合适的字段上建立索引,例如在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描
  • 索引优化,SQL优化。索引要符合最左匹配原则等
  • 建立分区。对关键字段建立水平分区,比如时间字段,若查询条件往往通过时间范围来进行查询,能提升不少性能
  • 利用缓存。利用Redis等缓存热点数据,提高查询效率
  • 限定数据的范围。比如:用户在查询历史信息的时候,可以控制在一个月的时间范围内
  • 读写分离。经典的数据库拆分方案,主库负责写,从库负责读
  • 通过分库分表的方式进行优化,主要有垂直拆分和水平拆分
  • 数据异构到es
  • 冷热数据分离。几个月之前不常用的数据放到冷库中,最新的数据比较新的数据放到热库中
  • 升级数据库类型,换一种能兼容MySQL的数据库(OceanBase、TiDB等)

什么时候索引失效反而提升效率

  • 小表查询: 对于非常小的表,MySQL可能会选择全表扫描(忽略索引),因为全表扫描的开销可能比通过索引逐行查找还要低。在这种情况下,索引失效不会损害性能,反而简化了查询。
  • 读取大部分或所有行: 当一个查询返回表中很大比例的行(如 30% 或更多)时,使用索引查找可能会耗时更多,因为数据库必须跳回主数据页以读取完整记录。全表扫描可能更有效,因为它可以逐行顺序读取数据。
  • 低选择性索引: 如果索引列的选择性非常低,例如一个布尔型字段,许多行有相同的值,那么依赖索引可能会产生不必要的开销。全表扫描可以避免索引的搜索和回表开销。
  • 频繁更新的表: 对于包含大量更新操作的表,索引的维护成本可能相对较高。尤其是在频繁更新索引列时,通过避免使用或减少复杂的索引可以减轻写操作的负担。
  • 复杂查询的优化选择: 对于复杂的多表联接查询,优化器有时可以选择执行计划中不使用某个索引(或部分失效)以提高整体联接和计算效率。
  • 数据分布与优化器误判: 在某些特定情况下,如果索引导致MySQL错误地估计数据分布或行数,手动禁用索引或提示优化器使用不同策略可能提升性能。

MySQL缓存相关

查询缓存

执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用

开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。

查询缓存不命中的情况:

  1. 任何两个查询在任何字符上的不同都会导致缓存不命中。
  2. 如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL 库中的系统表,其查询结果也不会被缓存。
  3. 缓存建立之后,MySQL 的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。

为什么 8.0 版本后移除了?缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。 因此,开启查询缓存要谨慎,尤其对于写密集的应用来说更是如此。

为什么要有 Buffer Pool?

虽然说 MySQL 的数据是存储在磁盘里的,但是也不能每次都从磁盘里面读取数据,这样性能是极差的。

要想提升查询性能,那就加个缓存。所以,当数据从磁盘中取出后,缓存内存中,下次查询同样的数据的时候,直接从内存中读取。

为此,Innodb 存储引擎设计了一个缓冲池(Buffer Pool),来提高数据库的读写性能。

  • 当读取数据时,如果数据存在于 Buffer Pool 中,客户端就会直接读取 Buffer Pool 中的数据,否则再去磁盘中读取。
  • 当修改数据时,首先是修改 Buffer Pool 中数据所在的页,然后将其页设置为脏页,最后由后台线程将脏页写入到磁盘。

Buffer Pool详情请查看Buffer Pool的三个链表

MYsql缓存能否替代Redis

  1. Redis缓存支持的场景更多。
    • 实际工作中缓存的结果不单单是Mysql Select语句返回的结果,有可能是在此基础上又加工的结果;而Mysql缓存的是Select语句的结果
    • Redis可以提供更丰富的数据类型的访问,如List、Set、Map、ZSet
  2. Redis缓存命中率要远高于Mysql缓存。
    • Mysql选择要缓存的语句的方式不是根据访问频率,主要是根据select语句里边是否包含动态变化的值,没有动态变化值的则缓存,比如用了now函数就不会缓存。Redis是由客户端自主根据访问频率高进行缓存。
    • Redis丰富的数据结构使得缓存复用率更高,比如缓存的是List,可以随意访问List中的部分元素,比如分页需求
    • Mysql缓存的失效粒度很粗,只要表有更新,涉及该表的所有缓存(不管更新是否会影响缓存)都失效,这样使得缓存的利用率会很低,只是适用更新很少的表
    • 当存在主从结点,并且会从多个结点读取数据时,各个结点的缓存不会同步3. 性能:Redis的查询性能要远高于Mysql缓存,最主要的原因是Redis是全部放在内存的,但是因为mysql缓存的命中率问题使得Mysql无法全部放到内存中。Redis性能好也还有一些其他原因
    • Redis的存储结构有利于读写性能Redis是IO多路复用,可以支持更大的吞吐,Mysql的数据特征使得做成IO多路复用绝大多数情况下也没有意义
    • 数据更新时会同时将该表的所有缓存失效,会使得数据更新的速度变慢。

MySQL 日志

详细请看:Mysql的三大日志

MySQL 中常见的日志有哪些?

MySQL日志主要包括查询日志、慢查询日志、事务日志、错误日志、二进制日志等。其中比较重要的是 bin log(二进制日志)和 redo log(重做日志)和 undo log(回滚日志)。

bin log

bin log是MySQL数据库级别的文件,记录对MySQL数据库执行修改的所有操作,不会记录select和show语句,主要用于恢复数据库和同步数据库。

redo log

redo log是innodb引擎级别,用来记录innodb存储引擎的事务日志,不管事务是否提交都会记录下来,用于数据恢复。当数据库发生故障,innoDB存储引擎会使用redo log恢复到发生故障前的时刻,以此来保证数据的完整性。将参数innodb_flush_log_at_tx_commit设置为1,那么在执行commit时会将redo log同步写到磁盘。

undo log

除了记录redo log外,当进行数据修改时还会记录undo logundo log用于数据的撤回操作,它保留了记录修改前的内容。通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC

bin log和redo log有什么区别?

  1. bin log会记录所有日志记录,包括InnoDB、MyISAM等存储引擎的日志;redo log只记录innoDB自身的事务日志。
  2. bin log只在事务提交前写入到磁盘,一个事务只写一次;而在事务进行过程,会有redo log不断写入磁盘。
  3. bin log是逻辑日志,记录的是SQL语句的原始逻辑;redo log是物理日志,记录的是在某个数据页上做了什么修改。

MySQL的binlog有几种格式?分别有什么区别?

有三种格式,statement,row和mixed。

  • statement:每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
  • row:不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
  • mixed:一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。

undo log 是如何保证事务的原子性的?

通过使用 undo log,数据库系统可以记录事务过程中每次数据修改之前的旧值。当事务失败或被中止时,数据库可以利用这些记录将数据恢复到事务开始之前的状态,从而保证事务的原子性。

Undo log 的这种机制确保了即使在事务执行过程中出现错误,数据库仍然能够保持一致性和原子性。

慢查询日志有什么用?

慢查询日志用于捕捉和记录执行时间超过指定阈值的 SQL 查询。慢查询日志在数据库性能优化和问题诊断中发挥着重要作用。

事务

详细内容请查看:Mysql的事务及隔离级别

事务的四大特性?

事务特性ACID原子性Atomicity)、一致性Consistency)、隔离性Isolation)、持久性Durability)。

  • 原子性(Atomicity):语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undo log
  • 持久性(Durability):保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView
  • 一致性(Consistency):事务追求的最终目标,是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。一致性的实现既需要数据库层面的保障,也需要应用层面的保障

常见的InnoDB是支持事务的,但是MyISAM是不支持事务的

数据库的三大范式

第一范式1NF

确保数据库表字段的原子性。

比如字段 userInfo: 广东省 10086' ,依照第一范式必须拆分成 userInfo: 广东省 userTel: 10086两个字段。

第二范式2NF

首先要满足第一范式,另外包含两部分内容,一是表必须有一个主键;二是非主键列必须完全依赖于主键,而不能只依赖于主键的一部分。

举个例子。假定选课关系表为student_course(student_no, student_name, age, course_name, grade, credit),主键为(student_no, course_name)。其中学分完全依赖于课程名称,姓名年龄完全依赖学号,不符合第二范式,会导致数据冗余(学生选n门课,姓名年龄有n条记录)、插入异常(插入一门新课,因为没有学号,无法保存新课记录)等问题。

应该拆分成三个表:学生:student(stuent_no, student_name, 年龄);课程:course(course_name, credit);选课关系:student_course_relation(student_no, course_name, grade)。

第三范式3NF

首先要满足第二范式,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。

假定学生关系表为Student(student_no, student_name, age, academy_id, academy_telephone),主键为"学号",其中学院id依赖于学号,而学院地点和学院电话依赖于学院id,存在传递依赖,不符合第三范式。

可以把学生关系表分为如下两个表:学生:(student_no, student_name, age, academy_id);学院:(academy_id, academy_telephone)。

2NF和3NF的区别?

  • 2NF依据是非主键列是否完全依赖于主键,还是依赖于主键的一部分。
  • 3NF依据是非主键列是直接依赖于主键,还是直接依赖于非主键。

并发事务带来了哪些问题?

先了解下几个概念:脏读、不可重复读、幻读。

  • 脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
  • 不可重复读是指在对于数据库中的某行记录,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,另一个事务修改了数据并提交了。
  • 幻读是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录。对幻读的正确理解是一个事务内的读取操作的结论不能支撑之后业务的执行。假设事务要新增一条记录,主键为id,在新增之前执行了select,没有发现id为xxx的记录,但插入时出现主键冲突,这就属于幻读,读取不到记录却发现主键冲突是因为记录实际上已经被其他的事务插入了,但当前事务不可见。

不可重复读和幻读有什么区别?

  • 不可重复读的重点是内容修改或者记录减少比如多次读取一条记录发现其中某些记录的值被修改;
  • 幻读的重点在于记录新增比如多次执行同一条查询语句(DQL)时,发现查到的记录增加了。

幻读其实可以看作是不可重复读的一种特殊情况,单独把幻读区分出来的原因主要是解决幻读和不可重复读的方案不一样。

事务隔离级别有哪些?

事务隔离就是为了解决上面提到的脏读、不可重复读、幻读这几个问题。

MySQL数据库为我们提供的四种隔离级别:

  • Serializable (串行化):通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。
  • Repeatable read (可重复读):MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行,解决了不可重复读的问题。
  • Read committed (读已提交):一个事务只能看见已经提交事务所做的改变。可避免脏读的发生。
  • Read uncommitted (读未提交):所有事务都可以看到其他未提交事务的执行结果。

生产环境数据库一般用的什么隔离级别呢?

生产环境大多使用RC。为什么不是RR呢?

可重复读(Repeatable Read),简称为RR
读已提交(Read Commited),简称为RC

缘由一:在RR隔离级别下,存在间隙锁,导致出现死锁的几率比RC大的多!
缘由二:在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行!

也就是说,RC的并发性高于RR。

并且大部分场景下,不可重复读问题是可以接受的。毕竟数据都已经提交了,读出来本身就没有太大问题!

并发事务的控制方式有哪些?

MySQL 中并发事务的控制方式无非就两种:MVCC。锁可以看作是悲观控制的模式,多版本并发控制(MVCC,Multiversion concurrency control)可以看作是乐观控制的模式。

控制方式下会通过锁来显式控制共享资源而不是通过调度手段,MySQL 中主要是通过 读写锁 来实现并发控制。

  • 共享锁(S 锁):又称读锁,事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)。
  • 排他锁(X 锁):又称写锁/独占锁,事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条记录加任何类型的锁(锁不兼容)。

读写锁可以做到读读并行,但是无法做到写读、写写并行。另外,根据根据锁粒度的不同,又被分为 表级锁(table-level locking)行级锁(row-level locking) 。InnoDB 不光支持表级锁,还支持行级锁,默认为行级锁。行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。不论是表级锁还是行级锁,都存在共享锁(Share Lock,S 锁)和排他锁(Exclusive Lock,X 锁)这两类。

MVCC 是多版本并发控制方法,即对一份数据会存储多个版本,通过事务的可见性来保证事务能看到自己应该看到的版本。通常会有一个全局的版本分配器来为每一行数据设置版本号,版本号是唯一的。

MVCC 在 MySQL 中实现所依赖的手段主要是: 隐藏字段、read view、undo log

  • undo log : undo log 用于记录某行数据的多个版本的数据。
  • read view 和 隐藏字段 : 用来判断当前版本数据的可见性。

MySQL 的隔离级别是基于锁实现的吗?

MySQL 的隔离级别基于锁和 MVCC 机制共同实现的。

SERIALIZABLE 隔离级别是通过锁来实现的,READ-COMMITTED 和 REPEATABLE-READ 隔离级别是基于 MVCC 实现的。不过, SERIALIZABLE 之外的其他隔离级别可能也需要用到锁机制,就比如 REPEATABLE-READ 在当前读情况下需要使用加锁读来保证不会出现幻读。

MySQL有哪些锁?

详细请查看:Mysql的锁

表级锁和行级锁了解吗?有什么区别?

MyISAM 仅仅支持表级锁(table-level locking),一锁就锁整张表,这在并发写的情况下性非常差。InnoDB 不光支持表级锁(table-level locking),还支持行级锁(row-level locking),默认为行级锁。

行级锁的粒度更小,仅对相关的记录上锁即可(对一行或者多行记录加锁),所以对于并发写入操作来说, InnoDB 的性能更高。

表级锁和行级锁对比

  • 表级锁: MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
  • 行级锁: MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。

行级锁锁的到底是什么?行级锁的使用有什么注意事项?

MySQL 的 InnoDB 存储引擎实现的行级锁,实际上锁定的是基于索引的行记录。这意味着,行锁针对的是数据表中的索引记录,而非数据表本身的物理行。

InnoDB 的行锁是针对索引字段加的锁,表级锁是针对非索引字段加的锁。当我们执行 UPDATEDELETE 语句时,如果 WHERE条件中字段没有命中唯一索引或者索引失效的话,就会导致扫描全表对表中的所有行记录进行加锁。这个在我们日常工作开发中经常会遇到,一定要多多注意!!!

不过,很多时候即使用了索引也有可能会走全表扫描,这是因为 MySQL 优化器的原因。

InnoDB 有哪几类行锁?

InnoDB 行锁是通过对索引数据页上的记录加锁实现的,MySQL InnoDB 支持三种行锁定方式:

  • 记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
  • 间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
  • 临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。

在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。

一些大厂面试中可能会问到 Next-Key Lock 的加锁范围,详细可以查看:行级锁怎么加的?

共享锁和排他锁

SELECT 的读取锁定主要分为两种方式:共享锁和排他锁。

select * from table where id<6 lock in share mode;--共享锁
select * from table where id<6 for update;--排他锁

这两种方式主要的不同在于LOCK IN SHARE MODE 多个事务同时更新同一个表单时很容易造成死锁。

申请排他锁的前提是,没有线程对该结果集的任何行数据使用排它锁或者共享锁,否则申请会受到阻塞。在进行事务操作时,MySQL会对查询结果集的每行数据添加排它锁,其他线程对这些数据的更改或删除操作会被阻塞(只能读操作),直到该语句的事务被commit语句或rollback语句结束为止。

SELECT... FOR UPDATE 使用注意事项:

  1. for update 仅适用于innodb,且必须在事务范围内才能生效。
  2. 根据主键进行查询,查询条件为like或者不等于,主键字段产生表锁
  3. 根据非索引字段进行查询,会产生表锁

MVCC 实现原理?

详细请查看:ReadView

MVCC(Multiversion concurrency control) 就是同一份数据保留多版本的一种方式,进而实现并发控制。在查询的时候,通过read view和版本链找到对应版本的数据。

作用:提升并发性能。对于高并发场景,MVCC比行级锁开销更小。

MVCC 实现原理如下:

  1. 首先获取事务自己的事务 ID;
  2. 获取 ReadView;
  3. 查询得到的数据,然后与 ReadView 中的事务版本号(m_ids,min_trx_id,max_trx_id)进行比较;
  4. 如果不符合 ReadView 规则,就需要从 Undo Log 中(即根据roll_point)获取历史快照;
  5. 最后返回符合规则的数据。 在隔离级别为读已提交(Read Committed)时,一个事务中的每一次 SELECT 查询都会重新获取一次Read View

Read View的规则

  1. 如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 已经提交的事务生成的,所以该版本的记录对当前事务可见
  2. 如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 才启动的事务生成的,所以该版本的记录对当前事务不可见
  3. 如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:
  • 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见
  • 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

快照读和当前读

表记录有两种读取方式。

  • 快照读:读取的是快照版本。普通的SELECT就是快照读。通过mvcc来进行并发控制的,不用加锁。

  • 当前读:读取的是最新版本。UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE是当前读。

快照读情况下,InnoDB通过mvcc机制避免了幻读现象。而mvcc机制无法避免当前读情况下出现的幻读现象。因为当前读每次读取的都是最新数据,这时如果两次查询中间有其它事务插入数据,就会产生幻读。

select...for update会锁表还是锁行?

如果查询条件用了索引/主键,那么select ... for update就会加行锁。

如果是普通字段(没有索引/主键),那么select ..... for update就会加表锁。

update 是锁行还是锁表?

首先,InnoDB行锁是通过给索引上的索引项加锁来实现的,只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。

  1. 当执行update语句时,where中的过滤条件列,如果用到索引,就是锁行;如果无法用索引,就是锁表。
  2. 如果两个update语句同时执行,第一个先执行触发行锁,但是第二个没有索引触发表锁,因为有个行锁住了,所以还是会等待行锁释放,才能锁表。
  3. 当执行insert或者delete语句时,锁行。

MySQL是如何避免幻读的?

  • 在快照读情况下,MySQL通过mvcc来避免幻读。
  • 在当前读情况下,MySQL通过next-key来避免幻读(加行锁和间隙锁来实现的)。

next-key包括两部分:行锁和间隙锁。行锁是加在索引上的锁,间隙锁是加在索引之间的。

Serializable隔离级别也可以避免幻读,会锁住整张表,并发性极低,一般不会使用。

乐观锁和悲观锁是什么?

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

  • 悲观锁:假定会发生并发冲突,会对操作的数据进行加锁,直到提交事务,才会释放锁,其他事务才能进行修改。实现方式:使用数据库中的锁机制。
  • 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否数据是否被修改过。给表增加version字段,在修改提交之前检查version与原来取到的version值是否相等,若相等,表示数据没有被修改,可以更新,否则,数据为脏数据,不能更新。实现方式:乐观锁一般使用版本号机制或CAS算法实现。

MySQL只操作同一条记录也会死锁吗

在MySQL中,即使多个事务仅对同一条记录进行操作,也可能发生死锁。这通常与InnoDB存储引擎的锁管理机制有关。以下是一些触发这种情况的场景:

索引导致的锁竞争:如果事务在WHERE子句中使用不同的索引来查找相同的行,InnoDB可能会导致多个事务以不同顺序锁住这些索引,从而产生死锁。
自增锁(auto-increment lock):在使用自增列时,不同事务可能在等待获取表级锁来增加自增值,某些情况下也会出现死锁。
外键约束:外键检查过程中可能会涉及多个表,多个事务可能会因为不同的锁顺序而陷入死锁。

读写分离和分库分表了解吗?

什么是读写分离?

见名思意,根据读写分离的名字,我们就可以知道:读写分离主要是为了将对数据库的读写操作分散到不同的数据库节点上。 这样的话,就能够小幅提升写性能,大幅提升读性能。

一般情况下,我们都会选择一主多从,也就是一台主数据库负责写,其他的从数据库负责读。主库和从库之间会进行数据同步,以保证从库中数据的准确性。这样的架构实现起来比较简单,并且也符合系统的写少读多的特点。

如何实现读写分离?

不论是使用哪一种读写分离具体的实现方案,想要实现读写分离一般包含如下几步:

  1. 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
  2. 保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制
  3. 系统将写请求交给主数据库处理,读请求交给从数据库处理。

什么是MySQL主从同步?

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

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

为什么要做主从同步?

  1. 读写分离,使数据库能支撑更大的并发。
  2. 在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能。
  3. 数据备份,保证数据的安全。

分库分表

当单表的数据量达到1000W或100G以后,优化索引、添加从库等可能对数据库性能提升效果不明显,此时就要考虑对其进行切分了。切分的目的就在于减少数据库的负担,缩短查询的时间。

数据切分可以分为两种方式:垂直划分和水平划分。

垂直划分

垂直划分数据库是根据业务进行划分,例如购物场景,可以将库中涉及商品、订单、用户的表分别划分出成一个库,通过降低单库的大小来提高性能。同样的,分表的情况就是将一个大表根据业务功能拆分成一个个子表,例如商品基本信息和商品描述,商品基本信息一般会展示在商品列表,商品描述在商品详情页,可以将商品基本信息和商品描述拆分成两张表。

优点:行记录变小,数据页可以存放更多记录,在查询时减少I/O次数。

缺点

  • 主键出现冗余,需要管理冗余列;
  • 会引起表连接JOIN操作,可以通过在业务服务器上进行join来减少数据库压力;
  • 依然存在单表数据量过大的问题。

水平划分

水平划分是根据一定规则,例如时间或id序列值等进行数据的拆分。比如根据年份来拆分不同的数据库。每个数据库结构一致,但是数据得以拆分,从而提升性能。

优点:单库(表)的数据量得以减少,提高性能;切分出的表结构相同,程序改动较少。

缺点

  • 分片事务一致性难以解决
  • 跨节点join性能差,逻辑复杂
  • 数据分片在扩容时需要迁移

什么是分区表?

分区是把一张表的数据分成N多个区块。分区表是一个独立的逻辑表,但是底层由多个物理子表组成。

当查询条件的数据分布在某一个分区的时候,查询引擎只会去某一个分区查询,而不是遍历整个表。在管理层面,如果需要删除某一个分区的数据,只需要删除对应的分区即可。

分区一般都是放在单机里的,用的比较多的是时间范围分区,方便归档。只不过分库分表需要代码实现,分区则是mysql内部实现。分库分表和分区并不冲突,可以结合使用。

分区表类型

  • range分区:按照范围分区。比如按照时间范围分区
  • list分区:list分区和range分区相似,主要区别在于list是枚举值列表的集合,range是连续的区间值的集合。对于list分区,分区字段必须是已知的,如果插入的字段不在分区时的枚举值中,将无法插入
  • hash分区:可以将数据均匀地分布到预先定义的分区中。

分区的问题?

  1. 打开和锁住所有底层表的成本可能很高。当查询访问分区表时,MySQL 需要打开并锁住所有的底层表,这个操作在分区过滤之前发生,所以无法通过分区过滤来降低此开销,会影响到查询速度。可以通过批量操作来降低此类开销,比如批量插入、LOAD DATA INFILE和一次删除多行数据。
  2. 维护分区的成本可能很高。例如重组分区,会先创建一个临时分区,然后将数据复制到其中,最后再删除原分区。
  3. 所有分区必须使用相同的存储引擎。

MySQL单表多大进行分库分表?

目前主流的有两种说法:

  1. MySQL 单表数据量大于 2000 万行,性能会明显下降,考虑进行分库分表。
  2. 阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。

事实上,这个数值和实际记录的条数无关,而与 实际存储的数据大小有关,详细查看:为什么说MySQL 一般单表不要超过 2000W 行

高度为3的B+树,可以存放多少数据?

InnoDB存储引擎有自己的最小储存单元——页(Page)。

查询InnoDB页大小的命令如下:

mysql> show global status like 'innodb_page_size';
+------------------+-------+
| Variable_name    | Value |
+------------------+-------+
| Innodb_page_size | 16384 |
+------------------+-------+

可以看出 innodb 默认的一页大小为 16384B = 16384/1024 = 16kb。

在MySQL中,B+树一个节点的大小设为一页或页的倍数最为合适。因为如果一个节点的大小 < 1页,那么读取这个节点的时候其实读取的还是一页,这样就造成了资源的浪费。

B+树中非叶子节点存的是key + 指针叶子节点存的是数据行

对于叶子节点,如果一行数据大小为1k,那么一页就能存16条数据。

对于非叶子节点,如果key使用的是bigint,则为8字节,指针在MySQL中为6字节,一共是14字节,则16k能存放 16 * 1024 / 14 = 1170 个索引指针。

于是可以算出,对于一颗高度为2的B+树,根节点存储索引指针节点,那么它有1170个叶子节点存储数据,每个叶子节点可以存储16条数据,一共 1170 x 16 = 18720 条数据。而对于高度为3的B+树,就可以存放 1170 x 1170 x 16 = 21902400 条数据(两千多万条数据),也就是对于两千多万条的数据,我们只需要高度为3的B+树就可以完成,通过主键查询只需要3次IO操作就能查到对应数据。

所以在 InnoDB 中B+树高度一般为3层时,就能满足千万级的数据存储。

Mysql使用

MySQL数据如何同步到Redis缓存?

有两种方案:

1、通过MySQL自动同步刷新Redis,MySQL触发器+UDF函数实现。

过程大致如下:

  1. 在MySQL中对要操作的数据设置触发器Trigger,监听操作
  2. 客户端向MySQL中写入数据时,触发器会被触发,触发之后调用MySQL的UDF函数
  3. UDF函数可以把数据写入到Redis中,从而达到同步的效果

2、解析MySQL的binlog,实现将数据库中的数据同步到Redis。可以通过canal实现。canal是阿里巴巴旗下的一款开源项目,基于数据库增量日志解析,提供增量数据订阅&消费。

canal的原理如下:

  1. canal模拟mysql slave的交互协议,伪装自己为mysql slave,向mysql master发送dump协议
  2. mysql master收到dump请求,开始推送binary log给canal
  3. canal解析binary log对象(原始为byte流),将数据同步写入Redis。

为什么阿里Java手册禁止使用存储过程?

先看看什么是存储过程。

存储过程是在大型数据库系统中,一组为了完成特定功能的SQL 语句集,它存储在数据库中,一次编译后永久有效,用户通过指定存储过程的名字并给出参数(如果该存储过程带有参数)来执行它。

存储过程主要有以下几个缺点:

  1. 存储过程难以调试。存储过程的开发一直缺少有效的 IDE 环境。SQL 本身经常很长,调试要把句子拆开分别独立执行,非常麻烦。
  2. 移植性差。存储过程的移植困难,一般业务系统总会不可避免地用到数据库独有的特性和语法,更换数据库时这部分代码就需要重写,成本较高。
  3. 管理困难。存储过程的目录是扁平的,而不是文件系统那样的树形结构,脚本少的时候还好办,一旦多起来,目录就会陷入混乱。
  4. 存储过程是只优化一次,有的时候随着数据量的增加或者数据结构的变化,原来存储过程选择的执行计划也许并不是最优的了,所以这个时候需要手动干预或者重新编译了。