1. 什么是内连接、外连接、交叉连接、笛卡尔积呢?
①、内连接(inner join):返回两个表中连接字段匹配的行。如果一个表中的行在另一个表中没有匹配的行,则这些行不会出现在查询结果中。
②、外连接(outer join):不仅返回两个表中匹配的行,还返回左表、右表或两者中未匹配的行。
③、交叉连接(cross join):返回第一个表中的每一行与第二个表中的每一行的组合,这种类型的连接通常用于生成笛卡尔积。
④、笛卡尔积:数学中的一个概念,例如集合 A={a,b},集合 B={0,1,2},那么 A✖️B={<a,0>,<a,1>,<a,2>,<b,0>,<b,1>,<b,2>,}
。
2. MySQL 的内连接、左连接、右连接有什么区别?
MySQL 的连接主要分为内连接和外连接,外连接又可以分为左连接和右连接。
MySQL-joins-来源菜鸟教程
①、inner join
内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集。
只有当两个表中都有匹配的记录时,这些记录才会出现在查询结果中。如果某一方没有匹配的记录,则该记录不会出现在结果集中。
内联可以用来找出两个表中共同的记录,相当于两个数据集的交集。
②、left join
返回左表(FROM 子句中指定的表)的所有记录,以及右表中匹配记录的记录。如果右表中没有匹配的记录,则结果中右表的部分会以 NULL 填充。
③、right join
刚好与左联相反,返回右表(FROM 子句中指定的表)的所有记录,以及左表中匹配记录的记录。如果左表中没有匹配的记录,则结果中左表的部分会以 NULL 填充。
#3.说一下数据库的三大范式?
三大范式的作用是为了减少数据冗余,提高数据完整性。
①、第一范式(1NF):确保表的每一列都是不可分割的基本数据单元
第二范式(2NF):在 1NF 的基础上属性完全依赖于主键
③、第三范式(3NF):在 2NF 的基础上,消除非主键列对主键的传递依赖,即非主键列只依赖于主键列,不依赖于其他非主键列
4.varchar 与 char 的区别?
char:
- char 表示定长字符串,长度是固定的;
- 如果插入数据的长度小于 char 的固定长度时,则用空格填充;
- 因为长度固定,所以存取速度要比 varchar 快很多,甚至能快 50%,但正因为其长度固定,所以会占据多余的空间,是空间换时间的做法;
- 对于 char 来说,最多能存放的字符个数为 255,和编码无关
varchar:
- varchar 表示可变长字符串,长度是可变的;
- 插入的数据是多长,就按照多长来存储;
- varchar 在存取方面与 char 相反,它存取慢,因为长度不固定,但正因如此,不占据多余的空间,是时间换空间的做法;
- 对于 varchar 来说,最多能存放的字符个数为 65532
6.DATETIME 和 TIMESTAMP 的异同?
相同点:
- 两个数据类型存储时间的表现格式一致。均为
YYYY-MM-DD HH:MM:SS
- 两个数据类型都包含「日期」和「时间」部分。
- 两个数据类型都可以存储微秒的小数秒(秒后 6 位小数秒)
区别:
DATETIME 和 TIMESTAMP 的区别
- 日期范围:DATETIME 的日期范围是
1000-01-01 00:00:00.000000
到9999-12-31 23:59:59.999999
;TIMESTAMP 的时间范围是1970-01-01 00:00:01.000000
UTC到 ``2038-01-09 03:14:07.999999
UTC - 存储空间:DATETIME 的存储空间为 8 字节;TIMESTAMP 的存储空间为 4 字节
- 时区相关:DATETIME 存储时间与时区无关;TIMESTAMP 存储时间与时区有关,显示的值也依赖于时区
- 默认值:DATETIME 的默认值为 null;TIMESTAMP 的字段默认不为空(not null),默认值为当前时间(CURRENT_TIMESTAMP)
7.MySQL 中 in 和 exists 的区别?
MySQL 中的 in 语句是把外表和内表作 hash 连接,而 exists 语句是对外表作 loop 循环,每次 loop 循环再对内表进行查询。我们可能认为 exists 比 in 语句的效率要高,这种说法其实是不准确的,要区分情景:
- 如果查询的两个表大小相当,那么用 in 和 exists 差别不大。
- 如果两个表中一个较小,一个是大表,则子查询表大的用 exists,子查询表小的用 in。
- not in 和 not exists:如果查询语句使用了 not in,那么内外表都进行全表扫描,没有用到索引;而 not extsts 的子查询依然能用到表上的索引。所以无论那个表大,用 not exists 都比 not in 要快
8.MySQL 里记录货币用什么字段类型比较好?
货币在数据库中 MySQL 常用 Decimal 和 Numric 类型表示,这两种类型被 MySQL 实现为同样的类型。他们被用于保存与货币有关的数据。
例如 salary DECIMAL(9,2),9(precision)代表将被用于存储值的总的小数位数,而 2(scale)代表将被用于存储小数点后的位数。存储在 salary 列中的值的范围是从-9999999.99 到 9999999.99。
DECIMAL 和 NUMERIC 值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。
之所以不使用 float 或者 double 的原因:因为 float 和 double 是以二进制存储的,所以有一定的误差。
9.MySQL 怎么存储 emoji?
MySQL 的 utf8 字符集仅支持最多 3 个字节的 UTF-8 字符,但是 emoji 表情(😊)是 4 个字节的 UTF-8 字符,所以在 MySQL 中存储 emoji 表情时,需要使用 utf8mb4 字符集。
ALTER TABLE mytable CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
MySQL 8.0 已经默认支持 utf8mb4 字符集,
10.drop、delete 与 truncate 的区别?
三者都表示删除,但是三者有一些差别:
区别 | delete | truncate | drop |
---|---|---|---|
类型 | 属于 DML | 属于 DDL | 属于 DDL |
回滚 | 可回滚 | 不可回滚 | 不可回滚 |
删除内容 | 表结构还在,删除表的全部或者一部分数据行 | 表结构还在,删除表中的所有数据 | 从数据库中删除表,所有数据行,索引和权限也会被删除 |
删除速度 | 删除速度慢,需要逐行删除 | 删除速度快 | 删除速度最快 |
因此,在不再需要一张表的时候,用 drop;在想删除部分数据行时候,用 delete;在保留表而删除所有数据的时候用 truncate。
11.UNION 与 UNION ALL 的区别?
- 如果使用 UNION,会在表链接后筛选掉重复的记录行
- 如果使用 UNION ALL,不会合并重复的记录行
- 从效率上说,UNION ALL 要比 UNION 快很多,如果合并没有刻意要删除重复行,那么就使用 UNION All
12.count(1)、count(*) 与 count(列名) 的区别?
三种计数方式
执行效果:
- count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为 NULL
- count(1)包括了忽略所有列,用 1 代表代码行,在统计结果的时候,不会忽略列值为 NULL
- count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者 0,而是表示 null)的计数,即某个字段值为 NULL 时,不统计。
执行速度:
- 列名为主键,count(列名)会比 count(1)快
- 列名不为主键,count(1)会比 count(列名)快
- 如果表多个列并且没有主键,则 count(1) 的执行效率优于 count(*)
- 如果有主键,则 select count(主键)的执行效率是最优的
- 如果表只有一个字段,则 select count(*)最优
13.一条 SQL 查询语句的执行顺序?
查询语句执行顺序
- FROM:对 FROM 子句中的左表<left_table>和右表<right_table>执行笛卡儿积(Cartesianproduct),产生虚拟表 VT1
- ON:对虚拟表 VT1 应用 ON 筛选,只有那些符合<join_condition>的行才被插入虚拟表 VT2 中
- JOIN:如果指定了 OUTER JOIN(如 LEFT OUTER JOIN、RIGHT OUTER JOIN),那么保留表中未匹配的行作为外部行添加到虚拟表 VT2 中,产生虚拟表 VT3。如果 FROM 子句包含两个以上表,则对上一个连接生成的结果表 VT3 和下一个表重复执行步骤 1)~步骤 3),直到处理完所有的表为止
- WHERE:对虚拟表 VT3 应用 WHERE 过滤条件,只有符合<where_condition>的记录才被插入虚拟表 VT4 中
- GROUP BY:根据 GROUP BY 子句中的列,对 VT4 中的记录进行分组操作,产生 VT5
- CUBE|ROLLUP:对表 VT5 进行 CUBE 或 ROLLUP 操作,产生表 VT6
- HAVING:对虚拟表 VT6 应用 HAVING 过滤器,只有符合<having_condition>的记录才被插入虚拟表 VT7 中。
- SELECT:第二次执行 SELECT 操作,选择指定的列,插入到虚拟表 VT8 中
- DISTINCT:去除重复数据,产生虚拟表 VT9
- ORDER BY:将虚拟表 VT9 中的记录按照<order_by_list>进行排序操作,产生虚拟表 VT10。11)
- LIMIT:取出指定行的记录,产生虚拟表 VT11,并返回给查询用户
67.介绍一下 MySQL 的常用命令(补充)
2024 年 03 月 13 日增补,可以先向面试官确认一下,“您提到的常用命令是指数据库表的增删改查 SQL 吗?”得到确认答复后可以根据下面这张思维导图作答:
#说说数据库操作命令?
①、创建数据库:
CREATE DATABASE database_name;
②、删除数据库:
DROP DATABASE database_name;
③、选择数据库:
USE database_name;
#说说表操作命令?
①、创建表:
CREATE TABLE table_name (
column1 datatype,
column2 datatype,
...
);
②、删除表:
DROP TABLE table_name;
③、显示所有表:
SHOW TABLES;
④、查看表结构:
DESCRIBE table_name;
⑤、修改表(添加列):
ALTER TABLE table_name ADD column_name datatype;
#说说 CRUD 命令?
①、插入数据:
INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...);
②、查询数据:
SELECT column_names FROM table_name WHERE condition;
③、更新数据:
UPDATE table_name SET column1 = value1, column2 = value2 WHERE condition;
④、删除数据:
DELETE FROM table_name WHERE condition;
#说说索引和约束的创建修改命令?
①、创建索引:
CREATE INDEX index_name ON table_name (column_name);
②、添加主键约束:
ALTER TABLE table_name ADD PRIMARY KEY (column_name);
③、添加外键约束:
ALTER TABLE table_name ADD CONSTRAINT fk_name FOREIGN KEY (column_name) REFERENCES parent_table (parent_column_name);
#说说用户和权限管理的命令?
①、创建用户:
CREATE USER 'username'@'host' IDENTIFIED BY 'password';
②、授予权限:
GRANT ALL PRIVILEGES ON database_name.table_name TO 'username'@'host';
③、撤销权限:
REVOKE ALL PRIVILEGES ON database_name.table_name FROM 'username'@'host';
④、删除用户:
DROP USER 'username'@'host';
#说说事务控制的命令?
①、开始事务:
START TRANSACTION;
②、提交事务:
COMMIT;
③、回滚事务:
ROLLBACK;
68.介绍一下 MySQL bin 目录下的可执行文件(补充)
- mysql:客户端程序,用于连接 MySQL 服务器
- mysqldump:一个非常实用的 MySQL 数据库备份工具,用于创建一个或多个 MySQL 数据库级别的 SQL 转储文件,包括数据库的表结构和数据。对数据备份、迁移或恢复非常重要。
- mysqladmin:mysql 后面加上 admin 就表明这是一个 MySQL 的管理工具,它可以用来执行一些管理操作,比如说创建数据库、删除数据库、查看 MySQL 服务器的状态等。
- mysqlcheck:mysqlcheck 是 MySQL 提供的一个命令行工具,用于检查、修复、分析和优化数据库表,对数据库的维护和性能优化非常有用。
- mysqlimport:用于从文本文件中导入数据到数据库表中,非常适合用于批量导入数据。
- mysqlshow:用于显示 MySQL 数据库服务器中的数据库、表、列等信息。
- mysqlbinlog:用于查看 MySQL 二进制日志文件的内容,可以用于恢复数据、查看数据变更等。
69.MySQL 第 3-10 条记录怎么查?
在 MySQL 中,要查询第 3 到第 10 条记录,可以使用 limit 语句,结合偏移量 offset 和行数 row_count 来实现。
limit 语句用于限制查询结果的数量,偏移量表示从哪条记录开始,行数表示返回的记录数量。
SELECT * FROM table_name LIMIT 2, 8;
- 2:偏移量,表示跳过前两条记录,从第三条记录开始。
- 8:行数,表示从偏移量开始,返回 8 条记录。
偏移量是从 0 开始的,即第一条记录的偏移量是 0;如果想从第 3 条记录开始,偏移量就应该是 2。
73.用过哪些 MySQL 函数?
#用过哪些字符串函数来处理文本?
CONCAT()
: 连接两个或多个字符串。LENGTH()
: 返回字符串的长度。SUBSTRING()
: 从字符串中提取子字符串。REPLACE()
: 替换字符串中的某部分。LOWER()
和UPPER()
: 分别将字符串转换为小写或大写。TRIM()
: 去除字符串两侧的空格或其他指定字符。
用过哪些数值函数?
ABS()
: 返回一个数的绝对值。CEILING()
: 向上取整FLOOR()
: 线下取证。ROUND()
: 四舍五入到指定的小数位数。MOD()
: 返回除法操作的余数。
用过哪些日期和时间函数?
NOW()
: 返回当前的日期和时间。CURDATE()
: 返回当前的日期。CURTIME()
: 返回当前的时间。DATE_ADD()
和DATE_SUB()
: 在日期上加上或减去指定的时间间隔。DATEDIFF()
: 返回两个日期之间的天数。DAY()
,MONTH()
,YEAR()
: 分别返回日期的日、月、年部分
用过哪些汇总函数?
SUM()
: 计算数值列的总和。AVG()
: 计算数值列的平均值。COUNT()
: 计算某列的行数。MAX()
和MIN()
: 分别返回列中的最大值和最小值。GROUP_CONCAT()
: 将多个行值连接为一个字符串。
用过哪些逻辑函数?
IF()
: 如果条件为真,则返回一个值;否则返回另一个值。CASE
: 根据一系列条件返回值。COALESCE()
: 返回参数列表中的第一个非 NULL 值
用过哪些格式化函数?
FORMAT()
: 格式化数字为格式化的字符串,通常用于货币显示。
-- 格式化数字
SELECT FORMAT(1234567.8945, 2) AS formatted_number;
用过哪些类型转换函数?
CAST()
: 将一个值转换为指定的数据类型。CONVERT()
: 类似于CAST()
,用于类型转换。
-- CAST函数
SELECT CAST('2024-01-01' AS DATE) AS casted_date;
-- CONVERT函数
SELECT CONVERT('123', SIGNED INTEGER) AS converted_number;
77.说说 SQL 的隐式数据类型转换?(补充)
2024 年 04 月 25 日增补
在 SQL 中,当不同数据类型的值进行运算或比较时,会发生隐式数据类型转换。
比如说,当一个整数和一个浮点数相加时,整数会被转换为浮点数,然后再进行相加。
SELECT 1 + 1.0; -- 结果为 2.0
比如说,当一个字符串和一个整数相加时,字符串会被转换为整数,然后再进行相加。
SELECT '1' + 1; -- 结果为 2
数据类型隐式转换会导致意想不到的结果,所以要尽量避免隐式转换。
可以通过显式转换来规避这种情况。
SELECT CAST('1' AS SIGNED INTEGER) + 1; -- 结果为 2
14.说说 MySQL 的基础架构?
1. 连接层(Connection Layer)
连接层负责处理客户端连接、授权认证及安全管理。。
2. 服务层(Service Layer)
服务层处理所有与 SQL 相关的操作,包括查询解析、分析、优化和执行。这个层次包括以下几个关键组件:
查询解析器(Parser): 将 SQL 语句解析为内部数据结构(解析树)。
查询优化器(Optimizer): 优化查询,选择最佳的执行计划(如选择合适的索引)。
缓存和缓冲(Cache and Buffer): 包括查询缓存、键缓存、InnoDB 缓冲池等,用于提高查询性能。
SQL 执行引擎(SQL Execution Engine): 根据优化器生成的执行计划,执行 SQL 语句。
3. 存储引擎层(Storage Engine Layer)
存储引擎层负责数据的存储和检索。MySQL 支持多种存储引擎,每种存储引擎有其特定的特性和用途。
InnoDB: 默认存储引擎,支持事务、行级锁定和外键。
MyISAM: 不支持事务,适用于只读或只写操作较多的场景,提供表级锁定。
Memory: 将数据存储在内存中,适用于临时数据和需要高速访问的场景。
15.一条 SQL 查询语句在 MySQL 中如何执行的?
二哥的 Java 进阶之路:SQL 执行
第一步,客户端发送 SQL 查询语句到 MySQL 服务器。
第二步,MySQL 服务器的连接器开始处理这个请求,跟客户端建立连接、获取权限、管理连接。
第三步,解析器开始对 SQL 语句进行解析,检查语句是否符合 SQL 语法规则,确保引用的数据库、表和列都存在,并处理 SQL 语句中的名称解析和权限验证。
第四步,优化器负责确定 SQL 语句的执行计划,这包括选择使用哪些索引,以及决定表之间的连接顺序等。优化器会尝试找出最高效的方式来执行查询。
第五步,执行器会调用存储引擎的 API 来进行数据的读写。
第六步,MySQL 的存储引擎是插件式的,不同的存储引擎在细节上面有很大不同。例如,InnoDB 是支持事务的,而 MyISAM 是不支持的。之后,会将执行结果返回给客户端
第七步,客户端接收到查询结果,完成这次查询请求。
存储引擎
#16.MySQL 有哪些常见存储引擎?
MySQL 支持多种存储引擎,常见的有 MyISAM、InnoDB、MEMORY 等。MEMORY 并不常用。
17.那存储引擎应该怎么选择?
- 大多数情况下,使用默认的 InnoDB 就对了,InnoDB 可以提供事务、行级锁等能力。
- MyISAM 适合读更多的场景。
- MEMORY 适合临时表,数据量不大的情况。由于数据都存放在内存,所以速度非常快
19.MySQL 日志文件有哪些?分别介绍下作用?
三分恶面渣逆袭:MySQL的主要日志
MySQL 的日志文件主要包括:
①、错误日志(Error Log):记录 MySQL 服务器启动、运行或停止时出现的问题。
②、慢查询日志(Slow Query Log):记录执行时间超过 long_query_time 值的所有 SQL 语句。这个时间值是可配置的,默认情况下,慢查询日志功能是关闭的。可以用来识别和优化慢 SQL。
③、一般查询日志(General Query Log):记录所有 MySQL 服务器的连接信息及所有的 SQL 语句,不论这些语句是否修改了数据。
④、二进制日志(Binary Log):记录了所有修改数据库状态的 SQL 语句,以及每个语句的执行时间,如 INSERT、UPDATE、DELETE 等,但不包括 SELECT 和 SHOW 这类的操作。
⑤、重做日志(Redo Log):记录了对于 InnoDB 表的每个写操作,不是 SQL 级别的,而是物理级别的,主要用于崩溃恢复。
⑥、回滚日志(Undo Log,或者叫事务日志):记录数据被修改前的值,用于事务的回滚。
20.binlog 和 redo log 有什么区别?
binlog,即二进制日志,对所有存储引擎都可用,是 MySQL 服务器级别的日志,用于数据的复制、恢复和备份。而 redo log 主要用于保证事务的持久性,是 InnoDB 存储引擎特有的日志类型。
binlog 记录的是逻辑 SQL 语句,而 redo log 记录的是物理数据页的修改操作,不是具体的 SQL 语句。
redo log 是固定大小的,通常配置为一组文件,使用环形方式写入,旧的日志会在空间需要时被覆盖。binlog 是追加写入的,新的事件总是被添加到当前日志文件的末尾,当文件达到一定大小后,会创建新的 binlog 文件继续记录
1.一条更新语句怎么执行的了解吗?
更新语句的执行是 Server 层和引擎层配合完成,数据除了要写入表中,还要记录相应的日志。
update 执行
- 执行器先找引擎获取 ID=2 这一行。ID 是主键,存储引擎检索数据,找到这一行。如果 ID=2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
- 执行器拿到引擎给的行数据,把这个值加上 1,比如原来是 N,现在就是 N+1,得到新的一行数据,再调用引擎接口写入这行新数据。
- 引擎将这行新数据更新到内存中,同时将这个更新操作记录到 redo log 里面,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
- 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
- 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。
从上图可以看出,MySQL 在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对 binlog 进行写入,在 InnoDB 内进行 redo log 的写入。
不仅如此,在对 redo log 写入时有两个阶段的提交,一是 binlog 写入之前prepare
状态的写入,二是 binlog 写入之后commit
状态的写入
22.那为什么要两阶段提交呢?
为什么要两阶段提交呢?直接提交不行吗?
我们可以假设不采用两阶段提交的方式,而是采用“单阶段”进行提交,即要么先写入 redo log,后写入 binlog;要么先写入 binlog,后写入 redo log。这两种方式的提交都会导致原先数据库的状态和被恢复后的数据库的状态不一致。
先写入 redo log,后写入 binlog:
在写完 redo log 之后,数据此时具有crash-safe
能力,因此系统崩溃,数据会恢复成事务开始之前的状态。但是,若在 redo log 写完时候,binlog 写入之前,系统发生了宕机。此时 binlog 没有对上面的更新语句进行保存,导致当使用 binlog 进行数据库的备份或者恢复时,就少了上述的更新语句。从而使得id=2
这一行的数据没有被更新。
先写 redo log,后写 bin log 的问题
先写入 binlog,后写入 redo log:
写完 binlog 之后,所有的语句都被保存,所以通过 binlog 复制或恢复出来的数据库中 id=2 这一行的数据会被更新为 a=1。但是如果在 redo log 写入之前,系统崩溃,那么 redo log 中记录的这个事务会无效,导致实际数据库中id=2
这一行的数据并没有更新。
先写 bin log,后写 redo log 的问题
简单说,redo log 和 binlog 都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致
SQL 优化
#24.慢 SQL 如何定位呢?
顾名思义,慢 SQL 也就是执行时间较长的 SQL 语句,MySQL 中 long_query_time 默认值是 10 秒,也就是执行时间超过 10 秒的 SQL 语句会被记录到慢查询日志中。
定位慢 SQL 主要通过两种手段:
- 慢查询日志:开启 MySQL 慢查询日志,再通过一些工具比如 mysqldumpslow 去分析对应的慢查询日志,找出问题的根源。
- 服务监控:可以在业务的基建中加入对慢 SQL 的监控,常见的方案有字节码插桩、连接池扩展、ORM 框架过程,对服务运行中的慢 SQL 进行监控和告警。
也可以使用 show processlist;
查看当前正在执行的 SQL 语句,找出执行时间较长的 SQL。
找到对应的慢 SQL 后,使用 EXPLAIN 命令查看 MySQL 是如何执行 SQL 语句的,这会帮助我们找到问题的根源。
EXPLAIN SELECT * FROM your_table WHERE conditions;
SQL 的执行过程:
- 客户端发送 SQL 语句给 MySQL 服务器。
- 如果查询缓存打开则会优先查询缓存,如果缓存中有对应的结果,直接返回给客户端。不过,MySQL 8.0 版本已经移除了查询缓存。
- 分析器对 SQL 语句进行语法分析,判断是否有语法错误。
- 搞清楚 SQL 语句要干嘛后,MySQL 还会通过优化器生成执行计划。
- 执行器调用存储引擎的接口,执行 SQL 语句。
SQL 执行过程中,优化器通过成本计算预估出执行效率最高的方式,基本的预估维度为:
- IO 成本:从磁盘读取数据到内存的开销。
- CPU 成本:CPU 处理内存中数据的开销。
基于这两个维度,可以得出影响 SQL 执行效率的因素有:
①、IO 成本
- 数据量:数据量越大,IO 成本越高。所以要避免
select *
;尽量分页查询。 - 数据从哪读取:尽量通过索引加快查询。
②、CPU 成本
- 尽量避免复杂的查询条件,如有必要,考虑对子查询结果进行过滤。
- 尽量缩减计算成本,比如说为排序字段加上索引,提高排序效率;比如说使用 union all 替代 union,减少去重处理。
25.有哪些方式优化 SQL?
我在进行 SQL 优化的时候,主要通过以下几个方面进行优化:
沉默王二:SQL 优化
#如何避免不必要的列?
比如说尽量避免使用 select *
,只查询需要的列,减少数据传输量。
#如何进行分页优化?
当数据量巨大时,传统的LIMIT
和OFFSET
可能会导致性能问题,因为数据库需要扫描OFFSET + LIMIT
数量的行。
延迟关联(Late Row Lookups)和书签(Seek Method)是两种优化分页查询的有效方法。
①、延迟关联
延迟关联适用于需要从多个表中获取数据且主表行数较多的情况。它首先从索引表中检索出需要的行 ID,然后再根据这些 ID 去关联其他的表获取详细信息。
SELECT e.id, e.name, d.details
FROM employees e
JOIN department d ON e.department_id = d.id
ORDER BY e.id
LIMIT 1000, 20;
延迟关联后:
SELECT e.id, e.name, d.details
FROM (
SELECT id
FROM employees
ORDER BY id
LIMIT 1000, 20
) AS sub
JOIN employees e ON sub.id = e.id
JOIN department d ON e.department_id = d.id;
首先对employees
表进行分页查询,仅获取需要的行的 ID,然后再根据这些 ID 关联获取其他信息,减少了不必要的 JOIN 操作。
②、书签(Seek Method)
书签方法通过记住上一次查询返回的最后一行的某个值,然后下一次查询从这个值开始,避免了扫描大量不需要的行。
假设需要对用户表进行分页,根据用户 ID 升序排列。
SELECT id, name
FROM users
ORDER BY id
LIMIT 1000, 20;
书签方式:
SELECT id, name
FROM users
WHERE id > last_max_id -- 假设last_max_id是上一页最后一行的ID
ORDER BY id
LIMIT 20;
优化后的查询不再使用OFFSET
,而是直接从上一页最后一个用户的 ID 开始查询。这里的last_max_id
是上一次查询返回的最后一行的用户 ID。这种方法有效避免了不必要的数据扫描,提高了分页查询的效率。
#如何进行索引优化?
正确地使用索引可以显著减少 SQL 的查询时间,通常可以从索引覆盖、避免使用 !=
或者 <>
操作符、适当使用前缀索引、避免列上函数运算、正确使用联合索引等方面进行优化。
①、利用覆盖索引
使用非主键索引查询数据时需要回表,但如果索引的叶节点中已经包含要查询的字段,那就不会再回表查询了,这就叫覆盖索引。
举个例子,现在要从 test 表中查询 city 为上海的 name 字段。
select name from test where city='上海'
如果仅在 city 字段上添加索引,那么这条查询语句会先通过索引找到 city 为上海的行,然后再回表查询 name 字段,这就是回表查询。
为了避免回表查询,可以在 city 和 name 字段上建立联合索引,这样查询结果就可以直接从索引中获取。
alter table test add index index1(city,name);
②、避免使用 != 或者 <> 操作符
!=
或者 <>
操作符会导致 MySQL 无法使用索引,从而导致全表扫描。
例如,可以把column<>'aaa'
,改成column>'aaa' or column<'aaa'
,就可以使用索引了。
优化策略就是尽可能使用 =
、>
、<
、BETWEEN
等操作符,它们能够更好地利用索引。
③、适当使用前缀索引
适当使用前缀索引可以降低索引的空间占用,提高索引的查询效率。
比如,邮箱的后缀一般都是固定的@xxx.com
,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引:
alter table test add index index2(email(6));
需要注意的是,MySQL 无法利用前缀索引做 order by 和 group by 操作。
④、避免列上使用函数
在 where 子句中直接对列使用函数会导致索引失效,因为数据库需要对每行的列应用函数后再进行比较,无法直接利用索引。
select name from test where date_format(create_time,'%Y-%m-%d')='2021-01-01';
可以改成:
select name from test where create_time>='2021-01-01 00:00:00' and create_time<'2021-01-02 00:00:00';
通过日期的范围查询,而不是在列上使用函数,可以利用 create_time 上的索引。
⑤、正确使用联合索引
正确地使用联合索引可以极大地提高查询性能,联合索引的创建应遵循最左前缀原则,即索引的顺序应根据列在查询中的使用频率和重要性来安排。
select * from messages where sender_id=1 and receiver_id=2 and is_read=0;
那就可以为 sender_id、receiver_id 和 is_read 这三个字段创建联合索引,但是要注意索引的顺序,应该按照查询中的字段顺序来创建索引。
alter table messages add index index3(sender_id,receiver_id,is_read);
#如何进行 JOIN 优化?
对于 JOIN 操作,可以通过优化子查询、小表驱动大表、适当增加冗余字段、避免 join 太多表等方式来进行优化。
①、优化子查询:使用 JOIN 代替子查询
子查询,特别是在 select 列表和 where 子句中的子查询,往往会导致性能问题,因为它们可能会为每一行外层查询执行一次子查询。
使用子查询:
select name from A where id in (select id from B);
使用 JOIN 代替子查询:
select A.name from A join B on A.id=B.id;
②、小表驱动大表
在执行 JOIN 操作时,应尽量让行数较少的表(小表)驱动行数较多的表(大表),这样可以减少查询过程中需要处理的数据量。
比如 left join,左表是驱动表,所以 A 表应小于 B 表,这样建立连接的次数就少,查询速度就快了。
select name from A left join B;
③、适当增加冗余字段
在某些情况下,通过在表中适当增加冗余字段来避免 JOIN 操作,可以提高查询效率,尤其是在高频查询的场景下。
比如,我们有一个订单表和一个商品表,查询订单时需要显示商品名称,如果每次都通过 JOIN 操作查询商品表,会降低查询效率。这时可以在订单表中增加一个冗余字段,存储商品名称,这样就可以避免 JOIN 操作。
select order_id,product_name from orders;
④、避免使用 JOIN 关联太多的表
《阿里巴巴 Java 开发手册》上就规定,不要使用 join 关联太多的表,最多不要超过 3 张表。
因为 join 太多表会降低查询的速度,返回的数据量也会变得非常大,不利于后续的处理。
如果业务逻辑允许,可以考虑将复杂的 JOIN 查询分解成多个简单查询,然后在应用层组合这些查询的结果。
#如何进行排序优化?
MySQL 生成有序结果的方式有两种:一种是对结果集进行排序操作,另外一种是按照索引顺序扫描得出的自然有序结果。
因此在设计索引的时候要充分考虑到排序的需求。
select id, name from users order by name;
如果 name 字段上有索引,那么 MySQL 可以直接利用索引的有序性,避免排序操作。
#如何进行 UNION 优化?
UNION 操作用于合并两个或者多个 SELECT 语句的结果集。
①、条件下推
条件下推是指将 where、limit 等子句下推到 union 的各个子查询中,以便优化器可以充分利用这些条件进行优化。
假设我们有两个查询分支,需要合并结果并过滤:
SELECT * FROM (
SELECT * FROM A
UNION
SELECT * FROM B
) AS sub
WHERE sub.id = 1;
可以改写成:
SELECT * FROM A WHERE id = 1
UNION
SELECT * FROM B WHERE id = 1;
通过将查询条件下推到 UNION 的每个分支中,每个分支查询都只处理满足条件的数据,减少了不必要的数据合并和过滤。
索引
#27.为什么使用索引会加快查询?
数据库文件是存储在磁盘上的,磁盘 I/O 是数据库操作中最耗时的部分之一。没有索引时,数据库会进行全表扫描(Sequential Scan),这意味着它必须读取表中的每一行数据来查找匹配的行(时间效率为 O(n))。当表的数据量非常大时,就会导致大量的磁盘 I/O 操作。
有了索引,就可以直接跳到索引指示的数据位置,而不必扫描整张表,从而大大减少了磁盘 I/O 操作的次数。
MySQL 的 InnoDB 存储引擎默认使用 B+ 树来作为索引的数据结构,而 B+ 树的查询效率非常高,时间复杂度为 O(logN)。
索引文件相较于数据库文件,体积小得多,查到索引之后再映射到数据库记录,查询效率就会高很多。
28.能简单说一下索引的分类吗?
MySQL 的索引可以显著提高查询的性能,可以从三个不同的维度对索引进行分类(功能、数据结构、存储位置):
二哥的 Java 进阶之路:索引类型
#说说从功能上的分类:
①、主键索引: 表中每行数据唯一标识的索引,强调列值的唯一性和非空性。
②、唯一索引: 保证数据列中每行数据的唯一性,但允许有空值。
③、普通索引: 基本的索引类型,用于加速查询。
④、全文索引:特定于文本数据的索引,用于提高文本搜索的效率。
说说从数据结构上分类:
①、B+树索引:最常见的索引类型,一种将索引值按照一定的算法,存入一个树形的数据结构中(二叉树),每次查询都从树的根节点开始,一次遍历叶子节点,找到对应的值。查询效率是 O(logN)。
也是 InnoDB 存储引擎的默认索引类型。
②、Hash 索引:基于哈希表的索引,查询效率可以达到 O(1),但是只适合 = 和 in 查询,不适合范围查询。
Hash 索引在原理上和 Java 中的 HashMap 类似,当发生哈希冲突的时候也是通过拉链法来解决。
说说从存储位置上分类:
①、聚簇索引:聚簇索引的叶子节点保存了一行记录的所有列信息。也就是说,聚簇索引的叶子节点中,包含了一个完整的记录行。
②、非聚簇索引:它的叶子节点只包含一个主键值,通过非聚簇索引查找记录要先找到主键,然后通过主键再到聚簇索引中找到对应的记录行,这个过程被称为回表。
InnoDB 存储引擎的主键使用的是聚簇索引,MyISAM 存储引擎不管是主键索引,还是二级索引使用的都是非聚簇索引。
#29.创建索引有哪些注意点?
尽管索引能提高查询性能,但不当的使用也会带来一系列问题。在加索引时需要注意以下几点:
①、选择合适的列作为索引
- 经常作为查询条件(WHERE 子句)、排序条件(ORDER BY 子句)、分组条件(GROUP BY 子句)的列是建立索引的好候选。
- 区分度低的字段,例如性别,不要建索引
- 频繁更新的字段,不要作为主键或者索引
- 不建议用无序的值(例如身份证、UUID )作为索引,当主键具有不确定性,会造成叶子节点频繁分裂,出现磁盘存储的碎片化
②、避免过多的索引
- 每个索引都需要占用额外的磁盘空间。
- 更新表(INSERT、UPDATE、DELETE 操作)时,所有的索引都需要被更新。
- 维护索引文件需要成本;还会导致页分裂,IO 次数增多。
③、利用前缀索引和索引列的顺序
- 对于字符串类型的列,可以考虑使用前缀索引来减少索引大小。
- 在创建复合索引时,应该根据查询条件将最常用作过滤条件的列放在前面。
30.索引哪些情况下会失效呢?
- 在索引列上使用函数或表达式:如果在查询中对索引列使用了函数或表达式,那么索引可能无法使用,因为数据库无法预先计算出函数或表达式的结果。例如:
SELECT * FROM table WHERE YEAR(date_column) = 2021
。 - 使用不等于(
<>
)或者 NOT 操作符:这些操作符通常会使索引失效,因为它们会扫描全表。 - 使用 LIKE 操作符,但是通配符在最前面:如果 LIKE 的模式串是以“%”或者“_”开头的,那么索引也无法使用。例如:
SELECT * FROM table WHERE column LIKE '%abc'
。 - OR 操作符:如果查询条件中使用了 OR,并且 OR 两边的条件分别涉及不同的索引,那么这些索引可能都无法使用。
- 如果 MySQL 估计使用全表扫描比使用索引更快时(通常是小表或者大部分行都满足 WHERE 子句),也不会使用索引。
- 联合索引不满足最左前缀原则时,索引会失效
1.索引不适合哪些场景呢?
- 数据表较小:当表中的数据量很小,或者查询需要扫描表中大部分数据时,数据库优化器可能会选择全表扫描而不是使用索引。在这种情况下,维护索引的开销可能大于其带来的性能提升。
- 频繁更新的列:对于经常进行更新、删除或插入操作的列,使用索引可能会导致性能下降。因为每次数据变更时,索引也需要更新,这会增加额外的写操作负担。
#性别字段要建立索引吗?
性别字段通常不适合建立索引。因为性别字段的选择性(区分度)较低,独立索引效果有限。
如果性别字段又很少用于查询,表的数据规模较小,那么建立索引反而会增加额外的存储空间和维护成本。
如果性别字段确实经常用于查询条件,数据规模也比较大,可以将性别字段作为复合索引的一部分,与选择性较高的字段一起加索引,会更好一些。
#什么是区分度?
区分度(Selectivity)是衡量一个字段在数据库表中唯一值的比例,用来表示该字段在索引优化中的有效性。
区分度 = 字段的唯一值数量 / 字段的总记录数;接近 1,字段值大部分是唯一的。例如,用户的唯一 ID,一般都是主键索引。接近 0,则说明字段值重复度高。
例如,一个表中有 1000 条记录,其中性别字段只有两个值(男、女),那么性别字段的区分度只有 0.002。
高区分度的字段更适合拿来作为索引,因为索引可以更有效地缩小查询范围。
32.索引是不是建的越多越好呢?
当然不是。
- 索引会占据磁盘空间
- 索引虽然会提高查询效率,但是会降低更新表的效率。比如每次对表进行增删改操作,MySQL 不仅要保存数据,还有保存或者更新对应的索引文件。
33.为什么 InnoDB 要使用 B+树作为索引?
MySQL 的默认存储引擎是 InnoDB,它采用的是 B+树索引,B+树是一种自平衡的多路查找树,和红黑树、二叉平衡树不同,B+树的每个节点可以有 m 个子节点,而红黑树和二叉平衡树都只有 2 个。
和 B 树不同,B+树的非叶子节点只存储键值,不存储数据,而叶子节点存储了所有的数据,并且构成了一个有序链表。
这样做的好处是,非叶子节点上由于没有存储数据,就可以存储更多的键值对,再加上叶子节点构成了一个有序链表,范围查询时就可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。查询的效率会更高。
B+树的页是单向链表还是双向链表?如果从大值向小值检索,如何操作?
B+树的叶子节点是通过双向链表连接的,这样可以方便范围查询和反向遍历。
- 当执行范围查询时,可以从范围的开始点或结束点开始,向前或向后遍历,这使得查询更为灵活。
- 在需要对数据进行逆序处理时,双向链表非常有用。
如果需要在 B+树中从大值向小值进行检索,可以按以下步骤操作:
- 定位到最右侧节点:首先,找到包含最大值的叶子节点。这通常通过从根节点开始向右遍历树的方式实现。
- 反向遍历:一旦定位到了最右侧的叶子节点,可以利用叶节点间的双向链表向左遍历。
为什么 MongoDB 索引用 B树,而 MySQL 用 B+ 树?
B树的特点是每个节点都存储数据,相邻的叶子节点之间没有指针链接。
B+树的特点是非叶子节点只存储索引,叶子节点存储数据,并且相邻的叶子节点之间有指针链接。
那么在查找单条数据时,B 树的查询效率可能会更高,因为每个节点都存储数据,所以最好情况就是 O(1)。
但由于 B 树的节点之间没有指针链接,所以并不适合做范围查询,因为范围查询需要遍历多个节点。
而 B+ 树的叶子节点之间有指针链接,所以适合做范围查询,因为可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。
MySQL 属于关系型数据库,所以范围查询会比较多,所以采用了 B+树;但 MongoDB 属于非关系型数据库,在大多数情况下,只需要查询单条数据,所以 MongoDB 选择了 B 树
35.为什么要用 B+ 树,而不用普通二叉树?
普通二叉树存在退化的情况,如果它退化成链表,就相当于全表扫描。
#为什么不用平衡二叉树呢?
读取数据的时候,是从磁盘先读到内存。平衡二叉树的每个节点只存储一个键值和数据,而 B+ 树可以存储更多的节点数据,树的高度也会降低,因此读取磁盘的次数就会下降,查询效率就快。
36.为什么用 B+ 树而不用 B 树呢?
B+ 树相比较 B 树,有这些优势:
①、更高的查询效率
B+树的所有值(数据记录或指向数据记录的指针)都存在于叶子节点,并且叶子节点之间通过指针连接,形成一个有序链表。
这种结构使得 B+树非常适合进行范围查询,因为可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。
而 B 树的数据分布在整个树中,进行范围查询时可能需要遍历树的多个层级。
②、更高的空间利用率
在 B+树中,非叶子节点不存储数据,只存储键值,这意味着非叶子节点可以拥有更多的键,从而有更多的分叉。
这导致树的高度更低,进一步降低了查询时磁盘 I/O 的次数,因为每一次从一个节点到另一个节点的跳转都可能涉及到磁盘 I/O 操作。
③、查询效率更稳定
B+树中所有叶子节点深度相同,所有数据查询路径长度相等,保证了每次搜索的性能稳定性。而在 B 树中,数据可以存储在内部节点,不同的查询可能需要不同深度的搜索
37.Hash 索引和 B+ 树索引区别是什么?
- B+ 树索引可以进行范围查询,Hash 索引不能。
- B+ 树索引支持联合索引的最左侧原则,Hash 索引不支持。
- B+ 树索引支持 order by 排序,Hash 索引不支持。
- Hash 索引在等值查询上比 B+ 树索引效率更高。
- B+ 树使用 like 进行模糊查询的时候,
LIKE 'abc%'
的话可以起到索引优化的作用,Hash 索引无法进行模糊查询。
#MySQL 模糊查询怎么查,什么情况下模糊查询不走索引?
MySQL 中进行模糊查询主要使用 LIKE 语句,结合通配符 %(代表任意多个字符)和 _(代表单个字符)来实现。
SELECT * FROM table WHERE column LIKE '%xxx%';
这个查询会返回所有 column 列中包含 xxx 的记录。
但是,如果模糊查询的通配符 % 出现在搜索字符串的开始位置,如 LIKE '%xxx'
,MySQL 将无法使用索引,因为数据库必须扫描全表以匹配任意位置的字符串。
38.聚簇索引与非聚簇索引的区别?
在聚簇索引中,表中的行是按照键值(索引)的顺序存储的。这意味着表中的实际数据行和键值之间存在物理排序的关系。因此,每个表只能有一个聚簇索引。
在非聚簇索引中,索引和数据是分开存储的,索引中的键值指向数据的实际存储位置。因此,非聚簇索引也被称为二级索引或辅助索引或非主键索引。表可以有多个非聚簇索引。
这意味着,当使用非聚簇索引检索数据时,数据库首先在索引中查找,然后通过索引中的指针去访问表中实际的数据行,这个过程称为“回表”(Bookmark Lookup)。
举例来说:
- InnoDB 采用的是聚簇索引,如果没有显式定义主键,InnoDB 会选择一个唯一的非空列作为隐式的聚簇索引;如果这样的列也不存在,InnoDB 会自动生成一个隐藏的行 ID 作为聚簇索引。这意味着数据与主键是紧密绑定的,行数据直接存储在索引的叶子节点上。
- MyISAM 采用的是非聚簇索引,表数据存储在一个地方,而索引存储在另一个地方,索引指向数据行的物理位置。
39.回表了解吗?
回表是指在数据库查询过程中,通过非聚簇索引(secondary index)查找到记录的主键值后,再根据这个主键值到聚簇索引(clustered index)中查找完整记录的过程。
回表操作通常发生在使用非聚簇索引进行查询,但查询的字段不全在该索引中,必须通过主键进行再次查询以获取完整数据。
换句话说,数据库需要先查找索引,然后再根据索引回到数据表中去查找实际的数据。
因此,使用非聚簇索引查找数据通常比使用聚簇索引要慢,因为需要进行两次磁盘访问。当然,如果索引所在的数据页已经被加载到内存中,那么非聚簇索引的查找速度也可以非常快。
例如:select * from user where name = '张三';
,会先从辅助索引中找到 name='张三' 的主键 ID,然后再根据主键 ID 从主键索引中找到对应的数据行。
回表记录越多好吗?
回表记录越多并不是一件好事。事实上,回表的代价是很高的,尤其在记录较多时,回表操作会显著影响查询性能。
因为每次回表操作都需要进行一次磁盘 I/O 读取操作。如果回表记录很多,会导致大量的磁盘 I/O。
索引覆盖(Covering Index)可以减少回表操作,将查询的字段都放在索引中,这样不需要回表就可以获取到查询结果了。
40.覆盖索引了解吗?
在辅助索引里面,不管是单列索引还是联合索引,如果 select 的数据列只用辅助索引中就能够取得,不用去查主键索引,这时候使用的索引就叫做覆盖索引,避免了回表。
41.什么是最左前缀原则?
最左前缀原则,也叫最左匹配原则,或者最左前缀匹配原则。
最左匹配原则是指在使用联合索引(即包含多列的索引)时,查询条件从索引的最左列开始并且不跳过中间的列。
如果一个复合索引包含(col1, col2, col3)
,那么它可以支持 col1
、col1,col2
和 col1, col2, col3
的查询优化,但不会优化只有 col2 或 col3 的查询。
也就说,在进行查询时,如果没有遵循最左前缀,那么索引可能不会被利用,导致查询效率降低
79.MySQL如何查看查询是否用到了索引?
可以通过 EXPLAIN
关键字来查看是否使用了索引。
EXPLAIN SELECT * FROM table WHERE column = 'value';
其结果中的 key
值显示了查询是否使用索引,如果使用了索引,会显示索引的名称
锁
43.MySQL 中有哪几种锁,列举一下?
按锁粒度划分的话,MySQL 的锁有:
- 表锁:开销小,加锁快;锁定力度大,发生锁冲突概率高,并发度最低;不会出现死锁。
- 行锁:开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。
- 页锁:开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般
#按兼容性如何划分?
按兼容性划分的话,MySQL 的锁有:
- 共享锁(S Lock),也叫读锁(read lock),相互不阻塞。
- 排他锁(X Lock),也叫写锁(write lock),排它锁是阻塞的,在一定时间内,只有一个请求能执行写入,并阻止其它锁读取正在写入的数据
按加锁机制如何划分?
①、乐观锁
乐观锁基于这样的假设:冲突在系统中出现的频率较低,因此在数据库事务执行过程中,不会频繁地去锁定资源。相反,它在提交更新的时候才检查是否有其他事务已经修改了数据。
可以通过在数据表中使用版本号(Version)或时间戳(Timestamp)来实现,每次读取记录时,同时获取版本号或时间戳,更新时检查版本号或时间戳是否发生变化。
如果没有变化,则执行更新并增加版本号或更新时间戳;如果检测到冲突(即版本号或时间戳与之前读取的不同),则拒绝更新。
②、悲观锁
悲观锁假设冲突是常见的,因此在数据处理过程中,它会主动锁定数据,防止其他事务进行修改。
可以直接使用数据库的锁机制,如行锁或表锁,来锁定被访问的数据。常见的实现是 SELECT FOR UPDATE
语句,它在读取数据时就加上了锁,直到当前事务提交或回滚后才释放。
44.说说 InnoDB 里的行锁实现?
- Record Lock 记录锁
记录锁就是直接锁定某行记录。当我们使用唯一性的索引(包括唯一索引和聚簇索引)进行等值查询且精准匹配到一条记录时,此时就会直接将这条记录锁定。例如select * from t where id =6 for update;
就会将id=6
的记录锁定。
- Gap Lock 间隙锁
间隙锁(Gap Locks) 的间隙指的是两个记录之间逻辑上尚未填入数据的部分,是一个左开右开空间。
间隙锁
间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个record
,此时就会将对应的间隙区间锁定。例如select * from t where id =3 for update;
或者select * from t where id > 1 and id < 6 for update;
就会将(1,6)区间锁定。
- Next-key Lock 临键锁
临键指的是间隙加上它右边的记录组成的左开右闭区间。比如上述的(1,6]、(6,8]等。
临键锁
临键锁就是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分record
记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个 record 的右边的临键区间。例如select * from t where id > 5 and id <= 7 for update;
会锁住(4,7]、(7,+∞)。mysql 默认行锁类型就是临键锁(Next-Key Locks)
。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
间隙锁(Gap Locks)
和临键锁(Next-Key Locks)
都是用来解决幻读问题的,在已提交读(READ COMMITTED)
隔离级别下,间隙锁(Gap Locks)
和临键锁(Next-Key Locks)
都会失效!
46.MySQL 的乐观锁和悲观锁了解吗?
- 悲观锁(Pessimistic Concurrency Control):
悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能被改动,一个事务拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。
数据库中的行锁,表锁,读锁,写锁均为悲观锁。
- 乐观锁(Optimistic Concurrency Control)
乐观锁认为数据的变动不会太频繁。
乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。
事务在从数据库中取数据时,会将该数据的版本也取出来(v1),当事务对数据变动完毕想要将其更新到表中时,会将之前取出的版本 v1 与数据中最新的版本 v2 相对比,如果 v1=v2,那么说明在数据变动期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时 version 会加 1,以此来表明数据已被变动。
如果,v1 不等于 v2,那么说明数据变动期间,数据被其他事务改动了,此时不允许数据更新到表中,一般的处理办法是通知用户让其重新操作。不同于悲观锁,乐观锁通常是由开发者实现的。
47.MySQL 遇到过死锁问题吗,你是如何解决的?
排查死锁的一般步骤是这样的:
(1)查看死锁日志 show engine innodb status;
(2)找出死锁 sql
(3)分析 sql 加锁情况
(4)模拟死锁案发
(5)分析死锁日志
(6)分析死锁结果
当然,这只是一个简单的流程说明,实际上生产中的死锁千奇百怪,排查和解决起来没那么简单。
事务
#48.MySQL 事务的四大特性说一下?
什么是原子性?
原子性子性意味着事务中的所有操作要么全部完成,要么全部不完成,它是不可分割的单位。如果事务中的任何一个操作失败了,整个事务都会回滚到事务开始之前的状态,如同这些操作从未被执行过一样。
#什么是一致性?
一致性确保事务从一个一致的状态转换到另一个一致的状态。
#什么是隔离性?
隔离性意味着并发执行的事务是彼此隔离的,一个事务的执行不会被其他事务干扰。
隔离性主要是为了解决事务并发执行时可能出现的问题,如脏读、不可重复读、幻读等。
数据库系统通过事务隔离级别(如读未提交、读已提交、可重复读、串行化)来实现事务的隔离性。
#什么是持久性?
持久性确保事务一旦提交,它对数据库所做的更改就是永久性的,即使发生系统崩溃,数据库也能恢复到最近一次提交的状态。通常,持久性是通过数据库的恢复和日志机制来实现的,确保提交的事务更改不会丢失。
49.那 ACID 靠什么保证的呢?
MySQL 通过事务、undo log、redo log 来确保 ACID。
如何保证原子性?
MySQL 通过 undo log 来确保原子性(Atomicity)。
当事务开始时,MySQL 会在undo log
中记录事务开始前的旧值。如果事务执行失败,MySQL 会使用undo log
中的旧值来回滚事务开始前的状态;如果事务执行成功,MySQL 会在某个时间节点将undo log
删除。
#如何保证一致性?
如果其他三个特性都得到了保证,那么一致性(Consistency)就自然而然得到保证了。
#如何保证隔离性?
MySQL 定义了多种隔离级别,通过 MVCC 来确保每个事务都有专属自己的数据版本,从而实现隔离性(Isolation)。
在 MVCC 中,每行记录都有一个版本号,当事务尝试读取记录时,会根据事务的隔离级别和记录的版本号来决定是否可以读取。
#如何保证持久性?
redo log 是一种物理日志,当执行写操作时,MySQL 会先将更改记录到 redo log 中。当 redo log 填满时,MySQL 再将这些更改写入数据文件中。
如果 MySQL 在写入数据文件时发生崩溃,可以通过 redo log 来恢复数据文件,从而确保持久性
50.事务的隔离级别有哪些?MySQL 的默认隔离级别是什么?
事务的隔离级别定了一个事务可能受其他事务影响的程度,MySQL 支持的四种隔离级别分别是:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和串行化(Serializable)。
三分恶面渣逆袭:事务的四个隔离级别
#什么是读未提交?
读未提交是最低的隔离级别,在这个级别,当前事务可以读取未被其他事务提交的数据,以至于会出现“脏读”、“不可重复读”和“幻读”的问题。
#什么是读已提交?
当前事务只能读取已经被其他事务提交的数据,可以避免“脏读”现象。但不可重复读和幻读问题仍然存在。
#什么是可重复读?
确保在同一事务中多次读取相同记录的结果是一致的,即使其他事务对这条记录进行了修改,也不会影响到当前事务。
可重复读是 MySQL 默认的隔离级别,避免了“脏读”和“不可重复读”,但可能会出现幻读。
#什么是串行化?
最高的隔离级别,通过强制事务串行执行来避免并发问题,可以解决“脏读”、“不可重复读”和“幻读”问题。
但会导致大量的超时和锁竞争问题。
A 事务未提交,B 事务上查询到的是旧值还是新值?
在 MySQL 的默认隔离级别(可重复读)下,如果事务 A 修改了数据但未提交,事务 B 将看到修改之前的数据。
这是因为在可重复读隔离级别下,MySQL 将通过多版本并发控制(MVCC)机制来保证一个事务不会看到其他事务未提交的数据,从而确保读一致性
51.什么是脏读、不可重复读、幻读呢?
脏读指的是一个事务能够读取另一个事务尚未提交的数据。如果读到的数据在之后被回滚了,那么第一个事务读取到的就是无效的数据。
不可重复读指的是在同一事务中执行相同的查询时,返回的结果集不同。这是由于在事务过程中,另一个事务修改了数据并提交。
幻读指的是在同一事务中执行相同的查询时,返回的结果集中出现了之前没有的数据行。这是因为在事务过程中,另一个事务插入了新的数据并提交。
可以通过设置隔离级别为可串行化来避免幻读,代价是降低并发性能。
同的隔离级别,在并发事务下可能会发生什么问题?
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read Uncommited 读取未提交 | 是 | 是 | 是 |
Read Commited 读取已提交 | 否 | 是 | 是 |
Repeatable Read 可重复读 | 否 | 否 | 是 |
Serialzable 可串行化 | 否 | 否 | 否 |
#
52.事务的各个隔离级别都是如何实现的?
#读未提交是如何实现的?
不提供任何锁机制来保护读取的数据,允许读取未提交的数据(即脏读)。
读已提交和可重复读通过 MVCC 机制中的 ReadView 来实现。
- READ COMMITTED:每次读取数据前都生成一个 ReadView,保证每次读操作都是最新的数据。
- REPEATABLE READ:只在第一次读操作时生成一个 ReadView,后续读操作都使用这个 ReadView,保证事务内读取的数据是一致的。
#串行化是如何实现的?
事务在读操作时,必须先加表级共享锁,直到事务结束才释放;事务在写操作时,必须先加表级排他锁,直到事务结束才释放。
53.MVCC 了解吗?怎么实现的?
MVCC 是多版本并发控制(Multi-Version Concurrency Control)的简称,主要用来解决数据库并发问题。
在支持 MVCC 的数据库中,当多个用户同时访问数据时,每个用户都可以看到一个在某一时间点之前的数据库快照,并且能够无阻塞地执行查询和修改操作,而不会相互干扰。
在传统的锁机制中,如果一个事务正在写数据,那么其他事务必须等待写事务完成才能读数据,MVCC 允许读操作访问数据的一个旧版本快照,同时写操作创建一个新的版本,这样读写操作就可以并行进行,不必等待对方完成。
在 MySQL 中,特别是 InnoDB 存储引擎,MVCC 是通过版本链和 ReadView 机制来实现的。
如果两个 AB 事务并发修改一个变量,那么 A 读到的值是什么,怎么分析。
当两个事务 A 和 B 并发修改同一个变量时,A 事务读取到的值取决于多个因素,包括事务的隔离级别、事务的开始时间和提交时间等。
- 读未提交:在这个级别下,事务可以看到其他事务尚未提交的更改。如果 B 更改了一个变量但尚未提交,A 可以读到这个更改的值。
- 读提交:A 只能看到 B 提交后的更改。如果 B 还没提交,A 将看到更改前的值。
- 可重复读:在事务开始后,A 总是读取到变量的相同值,即使 B 在这期间提交了更改。这是通过 MVCC 机制实现的。
- 可串行化:A 和 B 的操作是串行执行的,如果 A 先执行,那么 A 读到的值就是 B 提交前的值;如果 B 先执行,那么 A 读到的值就是 B 提交后的值。
高可用/性能
#54.数据库读写分离了解吗?
读写分离的基本原理是将数据库读写操作分散到不同的节点上
读写分离
读写分离的基本实现是:
- 数据库服务器搭建主从集群,一主一从、一主多从都可以。
- 数据库主机负责读写操作,从机只负责读操作。
- 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
- 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。
55.那读写分离的分配怎么实现呢?
一般有两种方式:程序代码封装和中间件封装。
程序代码封装指在代码中抽象一个数据访问层 ,实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离:
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。
对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。
56.主从复制原理了解吗?
MySQL 的主从复制(是一种数据同步机制,用于将数据从一个主数据库(master)复制到一个或多个从数据库(slave)。
广泛用于数据备份、灾难恢复和数据分析等场景。
- 在主服务器上,所有修改数据的语句(如 INSERT、UPDATE、DELETE)会被记录到二进制日志中。
- 主服务器上的一个线程(二进制日志转储线程)负责读取二进制日志的内容发送给从服务器。
- 从服务器接收到二进制日志内容后,会将这些内容写入自己的中继日志(Relay Log)。中继日志是从服务器上的一个本地存储。
- 从服务器上有一个 SQL 线程会读取中继日志,并在本地数据库上执行,从而将更改应用到从数据库中,完成同步。
57.主从同步延迟怎么处理?
主从同步延迟的原因
一个服务器开放多个个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取 binlog 的线程仅有一个,当某个 SQL 在从服务器上执行的时间稍长 或者由于某个 SQL 要进行锁表就会导致,主服务器的 SQL 大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。
主从同步延迟的解决办法
解决主从复制延迟有几种常见的方法:
- 写操作后的读操作指定发给数据库主服务器
例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个 bug。
- 读从机失败后再读一次主机
这就是通常所说的 "二次读取" ,二次读取和业务无绑定,只需要对底层数据库访问的 API 进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。
- 关键业务读写操作全部指向主机,非关键业务采用读写分离
例如,对于一个用户管理系统来说,注册 + 登录的业务读写操作全部访问主机,用户的介绍、爰好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
58.你们一般是怎么分库的呢?
分库分表是为了解决单库单表数据量过大导致数据库性能下降的一种解决方案。
分库的策略有两种:
①、垂直分库:按照业务模块将不同的表拆分到不同的库中,例如,用户表、订单表、商品表等分到不同的库中。
②、水平分库:按照一定的策略将一个表中的数据拆分到多个库中,例如,按照用户 id 的 hash 值将用户表拆分到不同的库中。
59.那你们是怎么分表的?
当单表数据增量过快,业界流传的说法是超过 500 万的数据量就要考虑分表了。
将文章和文章详情做了分表处理,因为文章的详情数据量会比较大,而文章的基本信息数据量会比较小。
垂直拆分可以减轻只查询文章基本数据,不需要附带文章详情时的查询压力。
当然了,当表的数据量过大时,仍然要考虑水平分表,将一个表的数据分散到多个表中,以减轻单表的查询压力。
比如说我们将文章表拆分成多个表,如 article_0、article_9999、article_19999 等。
60.水平分表有哪几种路由方式?
为了实现水平分表,需要设计合适的路由策略来确定数据应该存储在哪个表中,具体哪个表,由分片键(Sharding Key)来决定,分片键的选择应满足以下条件:
- 高区分度:分片键的值应尽量均匀分布,以避免数据倾斜。
- 查询频率高:选择经常在查询条件中使用的字段作为分片键,有助于提高查询效率。
- 写入频率高:选择经常被写入的字段,可以均匀分布写入负载。
那常见的路由策略有三种,分别是范围路由、Hash 路由和配置路由。
什么是范围路由?
范围路由是根据某个字段的值范围进行分表。这种方式适用于分片键具有顺序性或连续性的场景。
范围路由的优点是实现简单,可以随着数据的增加平滑地扩充新的表。适用于按时间或按顺序增长的字段(如时间戳、订单号等)。缺点是可能出现数据倾斜问题,导致某些表的数据量明显大于其他表。
什么是 Hash 路由?
哈希路由是通过对分片键进行哈希计算,然后取模来确定数据存储的表。哈希值决定了数据分布,通常能较好地平衡数据量。
哈希路由的优点是数据可以均匀分布,避免了数据倾斜,但范围查询时可能会涉及多个表,性能较差
什么是配置路由?
配置路由是通过配置表来确定数据存储的表,适用于分片键不规律的场景。
配置路由的优点是可以根据实际情况灵活配置。缺点是需要额外的配置表,维护成本较高。
61.不停机扩容怎么实现?
- 第一阶段:在线双写,查询走老库
- 建立好新的库表结构,数据写入旧库的同时,也写入拆分的新库
- 数据迁移,使用数据迁移程序,将旧库中的历史数据迁移到新库
- 使用定时任务,新旧库的数据对比,把差异补齐
- 第二阶段:在线双写,查询走新库
- 完成了历史数据的同步和校验
- 把对数据的读切换到新库
- 第三阶段:旧库下线
- 旧库不再写入新的数据
- 经过一段时间,确定旧库没有请求之后,就可以下线老库
62.常用的分库分表中间件有哪些?
- sharding-jdbc
- Mycat
63.那你觉得分库分表会带来什么问题呢?
从分库的角度来讲:
- 事务的问题
使用关系型数据库,有很大一点在于它保证事务完整性。
而分库之后单机事务就用不上了,必须使用分布式事务来解决。
- 跨库 JOIN 问题
在一个库中的时候我们还可以利用 JOIN 来连表查询,而跨库了之后就无法使用 JOIN 了。
此时的解决方案就是在业务代码中进行关联,也就是先把一个表的数据查出来,然后通过得到的结果再去查另一张表,然后利用代码来关联得到最终的结果。
还有可以适当的冗余一些字段。比如以前的表就存储一个关联 ID,但是业务时常要求返回对应的 Name 或者其他字段。这时候就可以把这些字段冗余到当前表中,来去除需要关联的操作。
还有一种方式就是数据异构,通过 binlog 同步等方式,把需要跨库 join 的数据异构到 ES 等存储结构中,通过 ES 进行查询。
从分表的角度来看:
- 数据迁移,容量规划,扩容等问题
数据的迁移,容量如何规划,未来是否可能再次需要扩容,等等,都是需要考虑的问题。
- ID 问题
数据库表被切分后,不能再依赖数据库自身的主键生成机制,所以需要一些手段来保证全局主键唯一。
- 还是自增,只不过自增步长设置一下。比如现在有三张表,步长设置为 3,三张表 ID 初始值分别是 1、2、3。这样第一张表的 ID 增长是 1、4、7。第二张表是 2、5、8。第三张表是 3、6、9,这样就不会重复了。
- UUID,这种最简单,但是不连续的主键插入会导致严重的页分裂,性能比较差。
- 分布式 ID,比较出名的就是 Twitter 开源的 sonwflake 雪花算法