MySQL 面经

1、什么是 MySQL?

MySQL 是一个开源的关系型数据库,现在隶属于 Oracle 公司。是我们国内使用频率最高的一种数据库,我本地安装的是比较新的 8.0 版本。

1.1 怎么删除/创建一张表?

可以使用 DROP TABLE 来删除表,使用 CREATE TABLE 来创建表。创建表的时候,可以通过 PRIMARY KEY 设定主键。

CREATE TABLE users (
    id INT AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(100),
    PRIMARY KEY (id)
);

1.2 请写一个升序/降序的 SQL 语句?

在 SQL 中,可以使用 ORDER BY 子句来对查询结果进行升序或者降序。默认情况下,查询结果是升序的,如果需要降序,可以通过 DESC 关键字来实现。如果需对多个字段进行排序,例如按工资降序,按名字升序,就可以 ORDER BY salary DESC, name ASC 来完成:

SELECT id, name, salary
FROM employees
ORDER BY salary DESC, name ASC;

1.3 MySQL出现性能差的原因有哪些?

可能是 SQL 查询使用了全表扫描,也可能是查询语句过于复杂,如多表 JOIN 或嵌套子查询。也有可能是单表数据量过大。通常情况下,添加索引就能解决大部分性能问题。对于一些热点数据,还可以通过增加 Redis 缓存,来减轻数据库的访问压力。

2、两张表怎么进行连接?

可以通过内连接 inner join、外连接 outer join、交叉连接 cross join 来合并多个表的查询结果。

2.1 什么是内连接?

内连接用于返回两个表中有匹配关系的行。假设有两张表:用户表和订单表,想查询有订单的用户就可以使用内连接 users INNER JOIN orders,按照用户 ID 关联就行了。

SELECT users.name, orders.order_id
FROM users
INNER JOIN orders ON users.id = orders.user_id;

只有那些在两个表中都存在 user_id 的记录才会出现在查询结果中。

2.2 什么是外连接?

和内连接不同,外连接不仅返回两个表中匹配的行,还返回没有匹配的行,用 null 来填充。外连接又分为左外连接 left join 和右外连接 right join。left join 会保留左表中符合条件的所有记录,如果右表中有匹配的记录,就返回匹配记录,否则就用 null 填充,常用于某表中有,但另外一张表中可能没有的数据的查询场景。假设要查询所有用户以及他们的订单,即使用户没有下单,就可以使用左连接:

SELECT users.id, users.name, orders.order_id
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

右连接就是左连接的镜像,right join 会保留右表中符合条件的所有记录。

2.3 什么是交叉连接?

交叉连接会返回两张表的笛卡尔积,将左表的每一行与右表的每一行进行组合,返回的行数是两张表行数的乘积。

SELECT A.id, B.id
FROM A
CROSS JOIN B;

3、内连接、左连接、右连接有什么区别?

MySQL 的连接主要分为内连接和外连接,外连接又可以分为左连接和右连接。
在这里插入图片描述
内连接可以用来找出两个表中共同的记录,相当于两个数据集的交集。左连接和右连接可以用来找出两个表中不同的记录,相当于两个数据集的并集。两者的区别是,左连接会保留左表中符合条件的所有记录,右连接则刚好相反。

4、说一下数据库的三大范式?

在这里插入图片描述

  1. 第一范式:表的每一列都是不可分割的基本数据单元:在这里插入图片描述
  2. 第二范式:在满足1NF的基础上,非主键字段必须完全依赖于整个主键。
    在这里插入图片描述
  3. 第三范式:在满足2NF的基础上,非主键字段不能间接依赖其他非主键字段(消除传递依赖)。
    在这里插入图片描述

4.1 建表的时候需要考虑哪些问题?

首先需要考虑表是否符合数据库的三大范式,确保字段不可再分,消除非主键依赖,确保字段仅依赖于主键等。然后在选择字段类型时,应该尽量选择合适的数据类型。在字符集上,尽量选择 utf8mb4,不仅可以支持中文和英文,还可以支持表情符号等。当数据量较大时,比如上千万行数据,需要考虑分表。比如订单表,可以采用水平分表的方式来分散单表存储压力。

5、varchar 与 char 的区别?

  1. varchar 是可变长度的字符类型,理论上最多可以容纳 65535 个字符,但考虑到字符集,以及 MySQL 需要 1 到 2 个字节来表示字符串长度,所以实际上最大可以容纳到 65533。
  2. char 是固定长度的字符类型,当定义一个 CHAR(10) 字段时,不管实际存储的字符长度是多少,都只会占用 10 个字符的空间。如果插入的数据小于 10 个字符,剩余的部分会用空格填充。(GBK 中文占 2 字节,UTF-8 占 3 字节)

6、blob 和 text 有什么区别?

blob 用于存储二进制数据,比如图片、音频、视频、文件等;但实际开发中,我们都会把这些文件存储到 OSS 或者文件服务器上,然后在数据库中存储文件的 URL。text 用于存储文本数据,比如文章、评论、日志等。

7、DATETIME 和 TIMESTAMP 有什么区别?

  1. DATETIME 直接存储日期和时间的完整值,与时区无关。
  2. TIMESTAMP 存储的是 Unix 时间戳,1970-01-01 00:00:01 UTC 以来的秒数,受时区影响。
    另外,DATETIME 的默认值为 null,占用 8 个字节;TIMESTAMP 的默认值为当前时间 CURRENT_TIMESTAMP,占 4 个字节,实际开发中更常用,因为可以自动更新。

8、in 和 exists 的区别?

使用 IN 时,MySQL 会首先执行子查询,然后将子查询的结果集用于外部查询的条件。这意味着子查询的结果集需要全部加载到内存中。而 EXISTS 会对外部查询的每一行,执行一次子查询。如果子查询返回任何行,则 EXISTS 条件为真。EXISTS 关注的是子查询是否返回行,而不是返回的具体值。

-- IN 的临时表可能成为性能瓶颈
SELECT * FROM users 
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);
-- EXISTS 可以利用关联索引
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders o 
            WHERE o.user_id = u.id AND o.amount > 100);

IN 适用于子查询结果集较小的情况。如果子查询返回大量数据,IN 的性能可能会下降,因为它需要将整个结果集加载到内存。而 EXISTS 适用于子查询结果集可能很大的情况。由于 EXISTS 只需要判断子查询是否返回行,不需要加载整个结果集,因此在某些情况下性能更好,特别是当子查询可以使用索引时。

8.1 NULL 值陷阱了解吗?

  1. IN: 如果子查询的结果集中包含 NULL 值,可能会导致意外的结果。例如,WHERE column IN (subquery),如果 subquery 返回 NULL,则 column IN (subquery) 永远不会为真,除非 column 本身也为 NULL。
  2. EXISTS: 对 NULL 值的处理更加直接。EXISTS 只是检查子查询是否返回行,不关心行的具体值,因此不受 NULL 值的影响。

9、记录货币用什么字段类型比较好?

货币在数据库中 MySQL 常用 Decimal 和 Numeric 类型表示,这两种类型被 MySQL 实现为同样的类型。他们被用于保存与货币有关的数据。例如 salary DECIMAL(9,2),9 (precision) 代表将被用于存储值的总的小数位数,而 2 (scale) 代表将被用于存储小数点后的位数。存储在 salary 列中的值的范围是从-9999999.99 到 9999999.99。DECIMAL 和 NUMERIC 值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。之所以不使用 float 或者 double 是为了避免二进制误差。

10、怎么存储 emoji?

MySQL 的 utf8 字符集仅支持最多 3 个字节的 UTF-8 字符,但是 emoji 表情(😊)是 4 个字节的 UTF-8 字符,所以在 MySQL 中存储 emoji 表情时,需要使用 utf8mb4 字符集。MySQL 8.0 已经默认支持 utf8mb4 字符集。

ALTER TABLE mytable CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

11、drop、delete 与 truncate 的区别?

  1. drop:属于 DDL,不可回滚,从数据库中删除表,所有数据行,索引和权限也会被删除,删除速度最快。
  2. delete:属于 DML,可回滚,表结构还在,删除表的全部或者一部分数据行,删除速度慢,需要逐行删除 。
  3. truncate:属于 DDL,不可回滚,表结构还在,删除表中的所有数据,删除速度快。
    因此,在不再需要一张表的时候,用 drop;在想删除部分数据行时候,用 delete;在保留表而删除所有数据的时候用 truncate。

12、UNION 与 UNION ALL 的区别?

  1. 如果使用 UNION,合并后会去除重复的记录行。
  2. 如果使用 UNION ALL,合并后不会去除重复的记录行。
  3. 从效率上说,UNION ALL 要比 UNION 快很多,如果合并没有刻意要删除重复行,那么就使用 UNION All。

13、count(*)、count(1) 与 count(列名) 的区别?

  1. count(*):统计表中所有行的数量,无论列值是否为 NULL。
  2. count(1):与 count(*) 功能完全相同,1 是一个常量值,不依赖任何列。
  3. count(列名):仅统计指定列中非 NULL 值​​的行数。若某行的该列为 NULL,则不计入统计。

14、SQL 查询语句的执行顺序了解吗?

在这里插入图片描述

  1. FROM:对 FROM 子句中的左表 <left_table> 和右表 <right_table> 执行笛卡儿积,产生虚拟表 VT1。
  2. ON:对虚拟表 VT1 应用 ON 筛选,只有那些符合 <join_condition> 的行才被插入虚拟表 VT2 中。
  3. JOIN:如果指定了 OUTER JOIN,那么保留表中未匹配的行作为外部行添加到虚拟表 VT2 中,产生虚拟表 VT3。
  4. WHERE:对虚拟表 VT3 应用 WHERE 过滤条件,只有符合 <where_condition> 的记录才被插入虚拟表 VT4 中。
  5. GROUP BY:根据 GROUP BY 子句中的列,对 VT4 中的记录进行分组操作,产生 VT5。
  6. CUBE|ROLLUP:对表 VT5 进行 CUBE 或 ROLLUP 操作,产生表 VT6。
  7. HAVING:对虚拟表 VT6 应用 HAVING 过滤器,只有符合 <having_condition> 的记录才被插入虚拟表 VT7 中。
  8. SELECT:第二次执行 SELECT 操作,选择指定的列,插入到虚拟表 VT8 中。
  9. DISTINCT:去除重复数据,产生虚拟表 VT9。
  10. ORDER BY:将虚拟表 VT9 中的记录按照 <order_by_list> 进行排序操作,产生虚拟表 VT10。
  11. LIMIT:取出指定行的记录,产生虚拟表 VT11,并返回给查询用户。

15、介绍一下 MySQL 的常用命令?

在这里插入图片描述

15.1 说说数据库操作命令?

  1. 创建数据库:
CREATE DATABASE database_name;
  1. 删除数据库:
DROP DATABASE database_name;
  1. 选择数据库:
USE database_name;

15.2 说说表操作命令?

  1. 创建表:
CREATE TABLE table_name (
    column1 datatype,
    column2 datatype,
    ...
);
  1. 删除表:
DROP TABLE table_name;
  1. 显示所有表:
SHOW TABLES;
  1. 查看表结构:
DESCRIBE table_name;
  1. 修改表
ALTER TABLE table_name ADD column_name datatype;

15.3 CRUD 命令?

  1. 插入数据:
INSERT INTO table_name (column1, column2, ...) VALUES (value1, value2, ...);
  1. 查询数据:
SELECT column_names FROM table_name WHERE condition;
  1. 更新数据:
UPDATE table_name SET column1 = value1, column2 = value2 WHERE condition;
  1. 删除数据:
DELETE FROM table_name WHERE condition;

15.4 说说索引和约束的创建修改命令?

  1. 创建索引:
CREATE INDEX index_name ON table_name (column_name);
  1. 添加主键约束:
ALTER TABLE table_name ADD PRIMARY KEY (column_name);
  1. 添加外键约束:
ALTER TABLE table_name ADD CONSTRAINT fk_name FOREIGN KEY (column_name) REFERENCES parent_table (parent_column_name);

15.5 说说用户和权限管理的命令?

  1. 创建用户:
CREATE USER 'username'@'host' IDENTIFIED BY 'password';
  1. 授予权限:
GRANT ALL PRIVILEGES ON database_name.table_name TO 'username'@'host';
  1. 撤销权限:
REVOKE ALL PRIVILEGES ON database_name.table_name FROM 'username'@'host';
  1. 删除用户:
DROP USER 'username'@'host';

15.6 说说事务控制的命令?

  1. 开始事务:
START TRANSACTION;
  1. 提交事务:
COMMIT;
  1. 回滚事务:
ROLLBACK;

16、MySQL bin 目录下的可执行文件了解吗?

  1. mysql:客户端程序,用于连接 MySQL 服务器。
  2. mysqladmin:MySQL 管理工具,可以用来执行一些管理操作。
  3. mysqlcheck:MySQL 命令行工具,用于检查、修复、分析和优化数据库表。
  4. mysqldump:MySQL 数据库备份工具,用于创建一个或多个 MySQL 数据库的 SQL 转储文件。
  5. mysqlimport:用于从文本文件中导入数据到数据库表中,非常适合用于批量导入数据。
  6. mysqlshow:用于显示 MySQL 数据库服务器中的数据库、表、列等信息。
  7. mysqlbinlog:用于查看 MySQL 二进制日志文件的内容,可以用于恢复数据、查看数据变更等。

17、MySQL 第 3-10 条记录怎么查?

可以使用 limit 语句,结合偏移量 offset 和行数 row_count 来实现:

SELECT * FROM table_name LIMIT 2, 8;
  • 2:偏移量,表示跳过前两条记录,从第 3 条记录开始。
  • 8:行数,表示从偏移量开始,返回 8 条记录。
    偏移量是从 0 开始的,即第一条记录的偏移量是 0;如果想从第 3 条记录开始,偏移量就应该是 2。

18、用过哪些 MySQL 函数?

MySQL 支持很多内置函数,包括字符串函数、数值函数、日期和时间函数、汇总函数、逻辑函数、格式化函数、类型转换函数等。

18.1 用过哪些字符串函数来处理文本?

  1. LENGTH():返回字符串的长度。
  2. CONCAT():连接两个或多个字符串。
  3. REPLACE():替换字符串中的某部分。
  4. SUBSTRING():从字符串中提取子字符串。
  5. LOWER() 和 UPPER():分别将字符串转换为小写或大写。
  6. TRIM():去除字符串两侧的空格或其他指定字符。
-- 获取字符串长度
SELECT LENGTH('沉默 王二') AS string_length;
-- 连接字符串
SELECT CONCAT('沉默', ' ', '王二') AS concatenated_string;
-- 替换字符串内容
SELECT REPLACE('沉默 王二', '王二', 'MySQL') AS replaced_string;
-- 提取子字符串
SELECT SUBSTRING('沉默 王二', 1, 5) AS substring;
-- 字符串转小写
SELECT LOWER('HELLO WORLD') AS lower_case;
-- 字符串转大写
SELECT UPPER('hello world') AS upper_case;
-- 去除字符串两侧的空格
SELECT TRIM('  沉默 王二  ') AS trimmed_string;

18.2 用过哪些数值函数?

  1. ABS():返回一个数的绝对值。
  2. CEILING():返回大于或等于给定数值的最小整数。
  3. FLOOR():返回小于或等于给定数值的最大整数。
  4. ROUND():四舍五入到指定的小数位数。
  5. MOD():返回除法操作的余数。
-- 返回绝对值
SELECT ABS(-123) AS absolute_value;
-- 向上取整
SELECT CEILING(123.45) AS ceiling_value;
-- 向下取整
SELECT FLOOR(123.45) AS floor_value;
-- 四舍五入
SELECT ROUND(123.4567, 2) AS rounded_value;
-- 余数
SELECT MOD(10, 3) AS modulus;

18.3 用过哪些日期和时间函数?

  1. NOW():返回当前的日期和时间。
  2. CURDATE():返回当前的日期。
  3. CURTIME():返回当前的时间。
  4. DATE_ADD() 和 DATE_SUB():在日期上加上或减去指定的时间间隔。
  5. DATEDIFF():返回两个日期之间的天数。
  6. YEAR(),MONTH(),DAY():分别返回日期的年、月、日部分。
-- 返回当前日期和时间
SELECT NOW() AS current_date_time;
-- 返回当前日期
SELECT CURDATE() AS current_date;
-- 返回当前时间
SELECT CURTIME() AS current_time;
-- 在日期上添加天数
SELECT DATE_ADD(CURDATE(), INTERVAL 10 DAY) AS date_in_future;
-- 计算两个日期之间的天数
SELECT DATEDIFF('2024-12-31', '2024-01-01') AS days_difference;
-- 返回日期的年份
SELECT YEAR(CURDATE()) AS current_year;

18.4 用过哪些汇总函数?

  1. SUM():计算数值列的总和。
  2. AVG():计算数值列的平均值。
  3. COUNT():计算某列的行数。
  4. MAX() 和 MIN():分别返回列中的最大值和最小值。
  5. GROUP_CONCAT():将多个行值连接为一个字符串。
-- 创建一个表并插入数据进行聚合查询
CREATE TABLE sales (
    product_id INT,
    sales_amount DECIMAL(10, 2)
);

INSERT INTO sales (product_id, sales_amount) VALUES (1, 100.00);
INSERT INTO sales (product_id, sales_amount) VALUES (1, 150.00);
INSERT INTO sales (product_id, sales_amount) VALUES (2, 200.00);

-- 计算总和
SELECT SUM(sales_amount) AS total_sales FROM sales;
-- 计算平均值
SELECT AVG(sales_amount) AS average_sales FROM sales;
-- 计算总行数
SELECT COUNT(*) AS total_entries FROM sales;
-- 最大值和最小值
SELECT MAX(sales_amount) AS max_sale, MIN(sales_amount) AS min_sale FROM sales;

18.5 用过哪些逻辑函数?

  1. IF():如果条件为真,则返回一个值;否则返回另一个值。
  2. CASE:根据一系列条件返回值。
  3. COALESCE():返回参数列表中的第一个非 NULL 值。
-- IF函数
SELECT IF(1 > 0, 'True', 'False') AS simple_if;
-- CASE表达式
SELECT CASE WHEN 1 > 0 THEN 'True' ELSE 'False' END AS case_expression;
-- COALESCE函数
SELECT COALESCE(NULL, NULL, 'First Non-Null Value', 'Second Non-Null Value') AS first_non_null;

18.6 用过哪些格式化函数?

  1. FORMAT():格式化数字为格式化的字符串,通常用于货币显示。
-- 格式化数字
SELECT FORMAT(1234567.8945, 2) AS formatted_number;

18.6 用过哪些类型转换函数?

  1. CAST():将一个值转换为指定的数据类型。
  2. CONVERT():类似于CAST(),用于类型转换。
-- CAST函数
SELECT CAST('2024-01-01' AS DATE) AS casted_date;
-- CONVERT函数
SELECT CONVERT('123', SIGNED INTEGER) AS converted_number;

19、说说 SQL 的隐式数据类型转换?

在 SQL 中,当不同数据类型的值进行运算或比较时,会发生隐式数据类型转换。比如:当一个整数和一个浮点数相加时,整数会被转换为浮点数,然后再相加。当一个字符串和一个整数相加时,字符串会被转换为整数,然后再相加。数据类型隐式转换会导致意想不到的结果,所以要尽量避免隐式转换。

SELECT 1 + 1.0; -- 结果为 2.0
SELECT '1' + 1; -- 结果为 2

可以通过显式转换来规避这种情况:

SELECT CAST('1' AS SIGNED INTEGER) + 1; -- 结果为 2

20、说说 SQL 的语法树解析?

抽象语法树(AST)是 SQL 解析过程的中间表示,使用树形结构表示 SQL 语句的层次和逻辑。语法树由节点组成,每个节点表示 SQL 语句中的一个语法元素。

  1. 根节点:通常是 SQL 语句的主要操作,例如 SELECT、INSERT、UPDATE、DELETE 等。
  2. 内部节点:表示语句中的操作符、子查询、连接操作等。例如,WHERE 子句、JOIN 操作等。
  3. 叶子节点:表示具体的标识符、常量、列名、表名等。例如,users 表、id 列、常量 1 等。
    以一个简单的 SQL 查询语句为例:
SELECT name, age FROM users WHERE age > 18;

这个查询语句的语法树可以表示为:

          SELECT
         /      \
     Columns     FROM
    /      \      |
  name      age  users
               |
             WHERE
               |
            age > 18

21、说说 MySQL 的基础架构?

MySQL 的架构大致可以分为三层,从上到下依次是:连接层、服务层、和存储引擎层。
在这里插入图片描述

  1. 连接层主要负责客户端连接的管理,包括验证用户身份、权限校验、连接管理等。通过数据库连接池来提升连接的处理效率。
  2. 服务层是 MySQL 的核心,主要负责查询解析、优化、执行等操作。在服务层 SQL 语句会经过解析、优化器优化,然后转发到存储引擎执行,并返回结果。服务层包含查询解析器、优化器、执行计划生成器、缓存(如查询缓存)、日志模块等。
  3. 存储引擎层负责数据的实际存储和提取,是 MySQL 架构中与数据交互最直接的层。MySQL 支持多种存储引擎,如 InnoDB、MyISAM、Memory 等。

21.1 binlog 写入在哪一层?

binlog 在服务层,负责记录 SQL 语句的变化。它记录了所有对数据库进行更改的操作,用于数据恢复、主从复制等。

22、一条查询语句如何执行?

在这里插入图片描述

  1. 客户端发送 SQL 查询语句到 MySQL 服务器。
  2. MySQL 服务器的连接器开始处理这个请求,跟客户端建立连接、获取权限、管理连接。
  3. 解析器对 SQL 语句进行解析,检查语句是否符合 SQL 语法规则,确保引用的数据库、表和列都是存在的,并处理 SQL 语句中的名称解析和权限验证。
  4. 优化器负责确定 SQL 语句的执行计划,这包括选择使用哪些索引,以及决定表之间的连接顺序等。
  5. 执行器会调用存储引擎的 API 来进行数据的读写。
  6. MySQL 的存储引擎是插件式的,不同的存储引擎在细节上面有很大不同。例如,InnoDB 是支持事务的,而 MyISAM 是不支持的。之后,会将执行结果返回给客户端。
  7. 客户端接收到查询结果,完成这次查询请求。

23、一条更新语句怎么执行的?

更新语句的执行是服务层和引擎层配合完成,数据除了要写入表中,还要记录相应的日志。
在这里插入图片描述

  1. 执行器先找引擎获取 ID = 2 这一行。ID 是主键,存储引擎检索数据,找到这一行。如果 ID = 2 这一行所在的数据页本来就在内存中,就直接返回给执行器;否则,需要先从磁盘读入内存,然后再返回。
  2. 执行器拿到引擎给的行数据把这个值加上 1,比如原来是 N,现在就是 N + 1,得到新的一行数据,再调用引擎接口写入这行新数据。
  3. 引擎将这行新数据更新到内存中,并将这个更新操作记录到 redo log,此时 redo log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。
  4. 执行器生成这个操作的 binlog,并把 binlog 写入磁盘。
  5. 执行器调用引擎的提交事务接口,引擎把刚刚写入的 redo log 改成提交(commit)状态,更新完成。

24、说说 MySQL 的数据存储形式?

MySQL 是以表的形式存储数据的,而表空间的结构则由段、区、页、行组成。
在这里插入图片描述

  1. 段(Segment):表空间由多个段组成,常见的段有数据段、索引段、回滚段等。创建索引时会创建两个段,数据段和索引段,数据段用来存储叶子节点中的数据;索引段用来存储非叶子节点的数据。回滚段包含了事务执行过程中用于数据回滚的旧数据。
  2. 区(Extent):段由一个或多个区组成,区是一组连续的页,通常包含 64 个连续的页,也就是 1M 的数据。使用区而非单独的页进行数据分配可以优化磁盘操作,减少磁盘寻道时间,特别是在大量数据进行读写时。
  3. 页(Page):页是 InnoDB 存储数据的基本单元,标准大小为 16 KB,索引树上的一个节点就是一个页。也就意味着数据库每次读写都是以 16 KB 为单位的。
  4. 行(Row):InnoDB 采用行存储方式,意味着数据按照行进行组织和管理,行数据可能有多个格式,比如说 COMPACT、REDUNDANT、DYNAMIC 等。MySQL 8.0 默认的行格式是 DYNAMIC,由COMPACT 演变而来,意味着这些数据如果超过了页内联存储的限制,则会被存储在溢出页中。

25、MySQL 有哪些常见存储引擎?

MySQL 支持多种存储引擎,常见的有 InnoDB、MyISAM、MEMORY 等。
在这里插入图片描述

25.1 如何切换 MySQL 的数据引擎?

可以通过 alter table 语句来切换 MySQL 的数据引擎。不过不建议,应该提前设计好到底用哪一种存储引擎。

ALTER TABLE your_table_name ENGINE=InnoDB;

26、那存储引擎应该怎么选择?

  1. 大多数情况下,使用默认的 InnoDB 就对了,InnoDB 可以提供事务、行级锁、外键、B+ 树索引等能力。
  2. MyISAM 适合读更多的场景。
  3. MEMORY 适合临时表,数据量不大的情况。由于数据都存放在内存,所以速度非常快。

27、InnoDB 和 MyISAM 主要有什么区别?

InnoDB 和 MyISAM 之间的区别主要表现在:存储结构、事务支持、锁粒度、索引类型、主键必需、表的具体行数、外键支持等方面。

  1. 存储结构:
  • InnoDB:用两种格式的文件来存储,.frm 文件存储表的定义;.ibd 存储数据和索引。
  • MyISAM:用三种格式的文件来存储,.frm 文件存储表的定义;.myd 存储数据;.myi 存储索引。
  1. 事务支持:
  • InnoDB:支持事务。
  • MyISAM:不支持事务。
  1. 最小锁粒度:
  • InnoDB:行级锁,并发写入性能高。
  • MyISAM:表级锁,高并发中写操作存在性能瓶颈。
  1. 索引类型:
  • InnoDB 为聚簇索引,索引和数据不分开。
    在这里插入图片描述
  • MyISAM 为非聚簇索引,索引和数据分开存储,索引保存的是数据文件的指针。
    在这里插入图片描述
  1. 外键支持:
  • InnoDB:支持外键。
  • MyISAM:不支持外键。
  1. 主键必需:
  • InnoDB:必须有主键。
  • MyISAM:可以没有主键。
  1. 表的具体行数:
  • InnoDB:表的具体行数需要扫描整个表才能返回。
  • MyISAM:具体行数存储在表的属性中。

28、InnoDB 的 Buffer Pool了解吗?

Buffer Pool 是 InnoDB 存储引擎中的一个内存缓冲区,它会将数据以页(page)为单位保存在内存中,当有查询请求需要读取数据时,会优先从 Buffer Pool 获取数据,避免直接访问磁盘。也就是说,即便我们只访问了一行数据的一个字段,InnoDB 也会将整个数据页加载到 Buffer Pool 中,以便后续的查询。修改数据时,也会先在缓存页面中修改。当缓存中的数据页被修改后,会在 Buffer Pool 中变为脏页,不会立刻写回到磁盘。InnoDB 会定期将这些脏页刷新到磁盘,保证数据的一致性。通常采用 LRU 算法来管理缓存页,将最近最少使用的数据移出缓存,为新数据腾出空间。

Buffer Pool 能够显著减少对磁盘的访问,从而提升数据库的读写性能。调优上,我们可以设置合理的 Buffer Pool 大小(通常为物理内存的 70%),并配置多个 Buffer Pool 实例(通过 innodb_buffer_pool_instances)来提升并发能力。

29、MySQL 日志文件有哪些?分别介绍下作用?

在这里插入图片描述
MySQL 的日志文件主要包括:

  1. 错误日志(Error Log):记录 MySQL 服务器启动、运行或停止时出现的问题。
  2. 慢查询日志(Slow Query Log):记录执行时间超过 long_query_time 值的所有 SQL 语句,默认关闭。
  3. 一般查询日志(General Query Log):记录所有 MySQL 服务器的连接信息以及所有的 SQL 语句。
  4. 二进制日志(Binary Log):记录了所有修改数据库状态的写 SQL 语句,以及每个语句的执行时间。
  5. 重做日志(Redo Log):记录了对于 InnoDB 表的每个写操作,不是 SQL 级别的,而是物理级别的。
  6. 回滚日志(Undo Log):记录数据被修改前的值,用于事务的回滚。

29.1 请重点说说 binlog?

binlog 是一种逻辑日志,会在磁盘上记录下数据库的所有修改操作,以便进行主从复制和数据恢复。当发生数据丢失时,binlog 可以将数据库恢复到特定的时间点。主服务器(master)上的二进制日志可以被从服务器(slave)读取,从而实现数据同步。binlog 包括两类文件:

  1. 二进制索引文件(.index)
  2. 二进制日志文件(.00000*)
    binlog 默认是没有启用的。要启用它,需要在 MySQL 的配置文件(my.cnf 或 my.ini)中设置 log_bin 参数。

29.2 有了 binlog 为什么还要 undo log redo log?

  1. binlog:主要用于数据恢复和主从复制。binlog 是 MySQL Server 层提供的日志,独立于存储引擎。
  2. redo log:主要用于数据持久化和崩溃恢复。redo log 是 InnoDB 存储引擎特有的日志,用于记录数据的物理修改。
  3. undo log:主要用于支持事务回滚和多版本并发控制(MVCC)。undo log 是 InnoDB 存储引擎提供的逻辑日志,用于记录数据的逻辑操作,如删除、更新前的数据快照。

当一个事务在 MySQL 中执行时,redo log、undo log 和 binlog 共同协作以确保数据的可靠性和一致性:

  1. 事务启动时,undo log 开始记录修改前的数据快照,以便在发生错误或显式回滚时恢复数据。
  2. 数据被修改时,InnoDB 会将修改记录到 redo log 中,同时也会生成相应的 undo log。
  3. 事务提交时,InnoDB 首先将 redo log 刷入磁盘,然后再将整个事务的操作记录到 binlog 中。这一过程称为两阶段提交,确保 binlog 和 redo log 的一致性(原子性)。
  4. 数据库发生崩溃时,InnoDB 会使用 redo log 进行恢复,确保数据不会丢失。binlog 可以用来做主从复制或数据恢复到特定时间点。

29.3 说说 redo log的工作机制?

redo log 由两部分组成:ib_logfile0 和 ib_logfile1。这两个文件的总大小是固定的,默认情况下每个文件为 48MB,共 96MB。它们以循环的方式写入,即当写满后,从头开始覆盖旧的日志。每次修改数据时,都会生成一个新的日志序列号,用于标记 redo log 中的日志位置,以确保数据恢复的一致性。当一个事务对数据进行修改时,InnoDB 会首先将这些修改记录到 redo log 中,而不是直接写入磁盘的数据文件。具体步骤分为三步:

  1. 数据被修改时,将修改操作记录到 redo log buffer 中(这是内存中的一个日志缓冲区)。
  2. 事务提交时,InnoDB 首先将 redo log 刷入磁盘(ib_logfile0、ib_logfile1 等),保证事务的持久性。
  3. 数据库发生崩溃时,InnoDB 会在重启时读取 redo log,找到最近一次的检查点,然后从该检查点开始,重放 redo log 中的日志记录,将所有已提交事务的修改重做一遍,恢复数据库到崩溃前的一致性状态。

29.4 说说 WAL?

WAL(Write-Ahead Logging,预写日志)的核心思想是先写日志,再写数据,即在对数据进行任何修改之前,必须先将修改的日志记录(redo log)持久化到磁盘。通过先写日志,确保系统在发生故障时可以通过重做日志恢复数据。

30、binlog 和 redo log 有什么区别?

binlog,即二进制日志,对所有存储引擎都可用,是 MySQL 服务器级别的日志,用于数据的复制、恢复和备份。而 redo log 主要用于保证事务的持久性,是 InnoDB 存储引擎特有的日志类型。binlog 记录的是逻辑 SQL 语句,而 redo log 记录的是物理数据页的修改操作,不是具体的 SQL 语句。redo log 是固定大小的,通常配置为一组文件,用环形方式写入,旧的日志会在空间需要时被覆盖。binlog 是追加写入的,新的事件总是被添加到当前日志文件的末尾,当文件达到一定大小后,会创建新的 binlog 文件继续记录。

31、为什么要两阶段提交呢?

为什么要两阶段提交呢?直接提交不行吗?

我们可以假设不采用两阶段提交的方式,而是采用单阶段进行提交,即要么先写入 redo log,后写入 binlog;要么先写入 binlog,后写入 redo log。这两种方式的提交都会导致原先数据库的状态和被恢复后的数据库的状态不一致。

  1. 先写入 redo log,后写入 binlog:
    在写完 redo log 之后,数据此时具有 crash-safe 能力,因此系统崩溃,数据会恢复成事务开始之前的状态。但是,若在 redo log 写完时候,binlog 写入之前,系统发生了宕机。此时 binlog 没有对上面的更新语句进行保存,导致当使用 binlog 进行数据库的备份或者恢复时,就少了上述的更新语句。从而使得 id = 2 这一行的数据没有被更新。
  2. 先写入 binlog,后写入 redo log:
    写完 binlog 之后,所有的语句都被保存,所以通过 binlog 复制或恢复出来的数据库中 id = 2 这一行的数据会被更新为 a = 1。但是如果在 redo log 写入之前,系统崩溃,那么 redo log 中记录的这个事务会无效,导致实际数据库中 id = 2 这一行的数据并没有更新。

32、redo log 怎么刷入磁盘?

redo log 的写入不是直接落到磁盘,而是在内存中设置了 redo log buffer 的连续内存空间,也就是 redo 日志缓冲区。
在这里插入图片描述

32.1 什么时候会刷入磁盘?

  1. redo log buffer 空间不足时:
    redo log buffer 的大小是有限的,如果不停的往这个有限大小的 redo log buffer 里塞入日志,很快它就会被填满。如果当前写入 redo log buffer 的 redo 日志量已经占了 redo log buffer 总容量的一半,就需要把这些日志刷到磁盘上。
  2. 事务提交时:
    在事务提交时,为了保证持久性,会把 redo log buffer 中的日志全部刷到磁盘。注意,这时候除了本事务,可能还会刷入其它事务的日志。
  3. 后台线程:
    有一个后台线程,大约每秒都会刷新一次 redo log buffer 中的 redo log 到磁盘。
  4. 正常关闭服务器时。
  5. 触发 checkpoint 规则。
    重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称为重做日志块(redo log block),块的大小是固定的 512 字节。我们的 redo log 是固定大小的,可以看作是一个逻辑上的 log group,由一定数量 log block 组成。它的写入方式是从头到尾开始写,写到末尾又回到开头循环写。

其中有两个标记位置:

write pos 是当前记录的位置,一边写一边向后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到磁盘。当 write_pos 追上 checkpoint 时,表示 redo log 日志已经写满。这时候就不能接着往里写数据了,需要执行checkpoint规则腾出可写空间。所谓的 checkpoint 规则,就是 checkpoint 触发后,将 redo log buffer 中的日志页都刷到磁盘。

33、慢 SQL 怎么定位呢?

33.1 什么是慢 SQL?

慢 SQL 也就是执行时间较长的 SQL 语句,MySQL 中 long_query_time 默认值是 10 秒,也就是执行时间超过 10 秒的 SQL 语句会被记录到慢查询日志中。不过,生产环境中,10 秒太久了,超过 1 秒的都可以认为是慢 SQL 了。

33.2 SQL 的执行过程了解吗?

  1. 客户端发送 SQL 语句给 MySQL 服务器。
  2. 如果查询缓存打开则会优先查询缓存,缓存中有对应的结果就直接返回。不过,MySQL 8.0 已经移除了查询缓存。
  3. 分析器对 SQL 语句进行语法分析,判断是否有语法错误。
  4. 分析完 SQL 语句后,MySQL 会通过优化器生成执行计划。
  5. 执行器调用存储引擎的接口,执行 SQL 语句。
    在这里插入图片描述
    SQL 执行过程中,优化器通过成本计算预估出执行效率最高的方式,基本的预估维度为:
  • IO 成本:从磁盘读取数据到内存的开销。
  • CPU 成本:CPU 处理内存中数据的开销。
    基于这两个维度,可以得出影响 SQL 执行效率的因素有:
  1. IO 成本:
  • 数据量:数据量越大,IO 成本越高。所以要避免 select *;尽量分页查询。
  • 数据从哪读取:尽量通过索引加快查询。
  1. CPU 成本:
  • 尽量避免复杂的查询条件,如有必要,考虑对子查询结果进行过滤。
  • 尽量缩减计算成本,比如说为排序字段加上索引,提高排序效率;比如说使用 union all 替代 union,减少去重处理。

33.3 如何优化慢 SQL?

首先,找到那些比较慢的 SQL,可以通过启用慢查询日志,记录那些超过指定执行时间的查询。也可以使用 show processlist; 命令查看当前正在执行的 SQL 语句,找出执行时间较长的 SQL。或在业务基建中加入对慢 SQL 的监控,常见的方案有字节码插桩、连接池扩展、ORM 框架扩展。然后使用 EXPLAIN 查看查询执行计划,判断查询是否使用了索引,是否有全表扫描等。最后,根据分析结果,通过添加或优化索引、调整查询语句或者增加内存缓冲区来优化 SQL。

33.4 慢 SQL 日志怎么开启?

慢 SQL 日志的开启方式有多种,比如说直接编辑 MySQL 的配置文件 my.cnf 或 my.ini,设置 slow_query_log 参数为 1,设置 slow_query_log_file 参数为慢查询日志的路径,设置 long_query_time 参数为慢查询的时间阈值。然后重启 MySQL 服务就好了,也可以通过 set global 命令动态设置。

34、有哪些方式优化 SQL?

我在进行 SQL 优化的时候,主要通过以下几个方面进行优化:
在这里插入图片描述

34.1 如何避免不必要的列?

比如说尽量避免使用 select *,只查询需要的列,减少数据传输量。

SELECT * FROM employees WHERE department_id = 5;

改成:

SELECT employee_id, first_name, last_name FROM employees WHERE department_id = 5;

34.2 如何进行分页优化?

当数据量巨大时,​​LIMIT-OFFSET​​ 可能会导致性能问题,因为数据库需要扫描 LIMIT + OFFSET 数量的行。延迟关联(Late Row Lookups)和书签(Seek Method)是两种优化分页查询的有效方法。

  1. 延迟关联:延迟关联适用于需要从多个表中获取数据且主表行数较多的情况。它首先从索引表中检索出需要的行 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 操作。

  1. 书签:书签方法通过记住上一次查询返回的最后一行的某个值,然后下一次查询从这个值开始,避免了扫描大量不需要的行。假设需要对用户表进行分页,根据用户 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。这种方法有效避免了不必要的数据扫描,提高了分页查询的效率。

34.3 如何进行索引优化?

正确使用索引可以显著减少 SQL 查询时间,通常可以从索引覆盖、避免使用 != 或者 <> 操作符、适当使用前缀索引、避免列上函数运算、正确使用联合索引等方面进行优化。

  1. 索引覆盖:使用非主键索引查询数据时需要回表,但如果索引的叶节点中已经包含要查询的字段,那就不会再回表查询了,这就叫覆盖索引。
select name from test where city='上海'

建立联合索引:

alter table test add index index1(city,name);
  1. 避免使用 != 或者 <> 操作符:!= 或者 <> 操作符会导致 MySQL 无法使用索引,从而导致全表扫描。例如,可以把column<>‘aaa’,改成column>‘aaa’ or column<‘aaa’,就可以使用索引了。优化策略就是尽可能使用 =、>、<、BETWEEN 等操作符,它们能够更好地利用索引。
  2. 适当使用前缀索引:适当使用前缀索引可以降低索引的空间占用,提高索引的查询效率。比如,邮箱的后缀一般都是固定的 @xxx.com,那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引:
alter table test add index index2(email(6));

需要注意的是,MySQL 无法利用前缀索引做 order by 和 group by 操作。

  1. 正确使用联合索引:正确地使用联合索引可以极大地提高查询性能,联合索引的创建应遵循最左前缀原则,即索引的顺序应根据列在查询中的使用频率和重要性来安排。
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);

34.4 如何进行 JOIN 优化?

对于 JOIN 操作,可以通过优化子查询、小表驱动大表、适当增加冗余字段、避免 join 太多表等方式来进行优化。

  1. 优化子查询:子查询,特别是在 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;
  1. 小表驱动大表:在执行 JOIN 操作时,应尽量让行数较少的表(小表)驱动行数较多的表(大表),这样可以减少查询过程中需要处理的数据量。比如 left join,左表是驱动表,所以 A 表应小于 B 表,这样建立连接的次数就少了,查询速度就快了:
select name from A left join B;
  1. 适当增加冗余字段:在某些情况下,通过在表中适当增加冗余字段来避免 JOIN 操作,可以提高查询效率,尤其是在高频查询的场景下。比如,我们有一个订单表和一个商品表,查询订单时需要显示商品名称,如果每次都通过 JOIN 操作查询商品表,会降低查询效率。这时可以在订单表中增加一个冗余字段,存储商品名称,这样就可以避免 JOIN 操作。
select order_id,product_name from orders;
  1. 避免使用 JOIN 关联太多的表:《阿里巴巴 Java 开发手册》上就规定,不要使用 join 关联太多的表,最多不要超过 3 张表。因为 join 太多表会降低查询的速度,返回的数据量也会变得非常大,不利于后续处理。如果业务逻辑允许,可以考虑将复杂的 JOIN 查询分解成多个简单查询,然后在应用层组合这些查询的结果。

34.5 如何进行排序优化?

MySQL 生成有序结果的方式有两种:一种是对结果集进行排序,另外一种是按照索引顺序扫描得出的自然有序结果。因此在设计索引的时候要充分考虑到排序的需求。

select id, name from users order by name;

如果 name 字段上有索引,那么 MySQL 可以直接利用索引的有序性,避免排序操作。

34.6 如何进行 UNION 优化?

UNION 操作用于合并两个或者多个 SELECT 语句的结果集。

  1. 条件下推:指将 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 的分支中,每个分支查询都只处理满足条件的数据,减少了不必要的数据合并和过滤。

35、怎么看执行计划 explain,如何理解其中各个字段的含义?

explain 是 MySQL 提供的一个用于查看查询执行计划的工具,可以帮助我们分析查询语句的性能瓶颈,找出慢 SQL 的原因。使用方式也非常简单,在 select 语句前加上 explain 关键字就可以了。

explain select * from students where id = 9

在这里插入图片描述
explain 的输出结果中包含了很多字段,下面是一些常见的字段含义:

  1. id:查询的标识符。
  2. select_type:查询的类型,常见的类型有:
  • SIMPLE:简单查询,不包含子查询或者 UNION 查询。
  • PRIMARY:查询中如果包含子查询,则最外层查询被标记为 PRIMARY。
  • SUBQUERY:子查询。
  • DERIVED:派生表的 SELECT,FROM 子句的子查询。
  1. table:查的哪个表。
  2. type:表示 MySQL 在表中找到所需行的方式,性能从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL。
  • system,表只有一行,一般是系统表,往往不需要进行磁盘 IO,速度非常快。
  • const:表中只有一行匹配,或通过主键或唯一索引获取单行记录。通常用于使用主键或唯一索引的精确匹配查询,性能非常高。
  • eq_ref:外层表的查询结果返回一行,用于多表关联且使用主键或唯一索引的查询。效率很高,适合多表关联查询。
  • ref:使用非唯一索引或前缀索引查询的情况,返回符合条件的多行记录。通常用于普通索引或联合索引查询。
  • range:只检索给定范围的行,使用索引来检索。在where语句中使用 between…and、<、>、<=、in 等条件查询 type 都是 range。
  • index:全索引扫描,即扫描整个索引而不访问数据行。
  • ALL:全表扫描,效率最低。
  1. possible_keys:可能会用到的索引,但并不一定实际被使用。
  2. keys:实际使用的索引。如果为 NULL,则没有使用索引。
  3. key_len:MySQL 决定使用的索引长度(以字节为单位)。当表有多个索引可用时,key_len 字段可以帮助识别哪个索引最有效。通常情况下,更短的 key_len 意味着数据库在比较键值时需要处理更少的数据。
  4. ref:用于与索引列比较的值来源。
  • const:表示常量,这个值是在查询中被固定的。例如在 WHERE column = 'value’中。
  • 一个或多个列的名称,通常在 JOIN 操作中,表示 JOIN 条件依赖的字段。
  • NULL,表示没有使用索引,或者查询使用的是全表扫描。
  1. rows:估算查到结果集需要扫描的数据行数,原则上 rows 越少越好。
  2. Extra 列:附加信息。
  • Using index:表示只利用了索引。
  • Using where:表示使用了 WHERE 过滤。
  • Using temporary :表示使用了临时表来存储中间结果。

35.1 type的执行效率等级,达到什么级别比较合适?

从高到低的效率排序是 system、const、eq_ref、ref、range、index 和 ALL。一般情况下,建议 type 值达到 const、eq_ref 或 ref,因为这些类型表明查询使用了索引进行精确匹配,效率高。如果是范围查询,range 也是可以接受的。通常要避免出现 ALL 类型,因为它表示全表扫描,性能最低。

36、为什么使用索引会加快查询?

数据库文件是存储在磁盘上的,磁盘 I/O 是数据库操作中最耗时的部分之一。没有索引时,数据库会进行全表扫描,这意味着它必须读取表中的每一行数据来查找匹配的行(时间效率为 O(n))。当表的数据量非常大时,就会导致大量的磁盘 I/O 操作。有了索引,就可以直接跳到索引指示的数据位置,而不必扫描整张表,从而大大减少了磁盘 I/O 操作的次数。MySQL 的 InnoDB 存储引擎默认使用 B+ 树来作为索引的数据结构,而 B+ 树的查询效率非常高,时间复杂度为 O(log n)。索引文件相较于数据库文件,体积小得多,查到索引后再映射到数据库记录,查询效率就会高很多。索引就好像书的目录,通过目录去查找对应的章节内容会比一页一页的翻书快很多。可通过 create index 创建索引,比如:

create index idx_name on students(name);

36.1 索引优化举例?

在实际开发中,我们可以通过合理使用单字段索引、复合索引和覆盖索引来优化查询。例如如果要加速查询 age 字段的条件,我们可以在 age 字段上创建索引。

CREATE INDEX idx_age ON users(age);

如果查询涉及多个字段 age 和 name,可以使用复合索引来提高查询效率。

CREATE INDEX idx_age_name ON users(age, name);

当我们只需要查询部分字段时 SELECT name FROM users WHERE age = 30;,覆盖索引可以提升查询效率。

CREATE INDEX idx_age_name ON users(age, name);

由于 age 和 name 字段都在索引中,MySQL 直接从索引中获取结果,无需回表查找。

37、能简单说一下索引的分类吗?

MySQL 的索引可以显著提高查询的性能,可以从三个不同的维度对索引进行分类(功能、数据结构、存储位置):
在这里插入图片描述

37.1 说说从功能上的分类?

  1. 主键索引:表中每行数据唯一标识的索引,强调列值的唯一性和非空性。创建表的时候,可以直接指定主键索引:
CREATE TABLE users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(255) NOT NULL,
    email VARCHAR(255)
);

id 列被指定为主键索引,同时,MySQL 会自动为这个列创建一个聚簇索引(主键索引一定是聚簇索引)。可以通过 show index from table_name 查看索引信息,比如前面创建的 users 表:
在这里插入图片描述

  • Non_unique:如果索引不能包含重复词,则为 0;如果可以,则为 1。可以帮助我们区分是唯一索引还是普通索引。
  • Key_name:索引的名称。如果索引是主键,那么这个值是 PRIMARY。
  • Column_name:索引所包含的字段名。
  • Index_type:索引的类型,比如 BTREE、HASH 等。
  1. 唯一索引:保证数据列中每行数据的唯一性,但允许有空值。可以通过下面的语句创建唯一索引:
CREATE UNIQUE INDEX idx_username ON users(username);

在这里插入图片描述

  1. 普通索引:基本的索引类型,用于加速查询。可以通过下面的语句创建普通索引:(Non_unique 为 1)
CREATE INDEX idx_email ON users(email);
  1. 全文索引:特定于文本数据的索引,用于提高文本搜索的效率。假设有一个名为 articles 的表,下面这条语句在 content 列上创建了一个全文索引。
CREATE FULLTEXT INDEX idx_article_content ON articles(content);

37.2 说说从数据结构上分类?

  1. B+ 树索引:最常见的索引类型,将索引值按照一定的算法,存入一个树形的数据结构中(二叉树),每次查询都从树的根节点开始,一次遍历叶子节点,找到对应的值。查询效率是 O(log n)。是 InnoDB 存储引擎的默认索引类型。B+ 树是 B 树的升级版,B+ 树中的非叶子节点都不存储数据,只存储索引。叶子节点中存储了所有的数据,并且构成一个从小到大的有序双向链表,使得在完成一次树的遍历定位到范围查询的起点后,可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。这在处理大范围的查询时特别高效。
    在这里插入图片描述
    因为 B+ 树是 InnoDB 的默认索引类型,所以创建 B+ 树的时候不需要指定索引类型。
  2. Hash 索引:基于哈希表的索引,查询效率可以达到 O(1)。Hash 索引在原理上和 Java 中的 HashMap 类似,发生哈希冲突的时候也是通过拉链法来解决。
    在这里插入图片描述
    可以通过下面的语句创建哈希索引:
CREATE TABLE example_hash (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255),
    UNIQUE HASH (name)
) ENGINE=MEMORY;

注意,我们这里创建的是 MEMORY 存储引擎,InnoDB 并不提供直接创建哈希索引的选项,因为 B+ 树索引能够很好地支持范围查询和等值查询,满足了大多数数据库操作的需要。InnoDB 存储引擎内部使用了一种名为自适应哈希索引(Adaptive Hash Index, AHI)的技术。自适应哈希索引并不是由用户显式创建的,而是 InnoDB 根据数据访问的模式自动建立和管理的。当 InnoDB 发现某个索引被频繁访问时,会在内存中创建一个哈希索引,加速对该索引的访问。可以通过下面的语句查看自适应哈希索引的状态:

SHOW VARIABLES LIKE 'innodb_adaptive_hash_index';

如果返回的值是 ON,说明自适应哈希索引是开启的。

37.3 说说从存储位置上分类?

  1. 聚簇索引:叶子节点保存了一行记录的所有列信息。也就是说聚簇索引的叶子节点中,包含了一个完整的记录行。
  2. 非聚簇索引:叶子节点只包含一个主键值,通过非聚簇索引查找记录要先找到主键,然后通过主键再到聚簇索引中找到对应的记录行,这个过程被称为回表查询。
    InnoDB 存储引擎的主键使用的是聚簇索引,MyISAM 存储引擎主键索引和二级索引使用的都是非聚簇索引。

38、创建索引有哪些注意点?

  1. 选择合适的列作为索引:
  • 经常作为查询条件(WHERE 子句)、排序条件(ORDER BY 子句)、分组条件(GROUP BY 子句)的列。
  • 区分度低的字段,例如性别,不要建索引。
  • 频繁更新的字段,不要建索引。
  1. 避免过多的索引:
  • 因为每个索引都需要占用额外的磁盘空间。
  • 更新表(INSERT、UPDATE、DELETE 操作)的时候,索引都需要被更新。
  1. 利用前缀索引和索引列的顺序:
  • 对于字符串类型的列,可以考虑使用前缀索引来减少索引大小。
  • 在创建联合索引时,应该根据查询条件将最常用的放在前面,遵守最左前缀原则。

39、索引哪些情况下会失效呢?

  1. 在索引列上使用函数或表达式:索引可能无法使用,因为 MySQL 无法预先计算出函数或表达式的结果。例如:SELECT * FROM table WHERE YEAR(date_column) = 2021。
  2. 使用不等于(<>)或者 NOT 操作符:因为它们会扫描全表。
  3. 使用 LIKE 语句,但通配符在前面:以 % 或者 _ 开头,索引也无法使用。例如:SELECT * FROM table WHERE column LIKE ‘%abc’。
  4. 联合索引,但 WHERE 不满足最左前缀原则,索引无法起效。如:SELECT * FROM table WHERE column2 = 2,联合索引为 (column1, column2)。

40、索引不适合哪些场景呢?

  1. 数据表较小:当表中的数据量很小,或者查询需要扫描表中大部分数据时,数据库优化器可能会选择全表扫描而不是使用索引。在这种情况下,维护索引的开销可能大于其带来的性能提升。
  2. 频繁更新的列:对于经常进行更新、删除或插入操作的列,使用索引可能会导致性能下降。因为每次数据变更时,索引也需要更新,这会增加额外的写操作负担。

40.1 性别字段要建立索引吗?

性别字段通常不适合建立索引。因为性别字段的选择性(区分度)较低,独立索引效果有限。如果性别字段又很少用于查询,表的数据规模较小,那么建立索引反而会增加额外的存储空间和维护成本。如果性别字段确实经常用于查询条件,数据规模也比较大,可以将性别字段作为复合索引的一部分,与选择性较高的字段一起加索引,会更好一些。

40.2 什么是区分度?

区分度是衡量一个字段在数据库表中唯一值的比例,用来表示该字段在索引优化中的有效性。区分度 = 字段的唯一值数量 / 字段的总记录数;接近 1,字段值大部分是唯一的。例如,用户的唯一 ID,一般都是主键索引。接近 0,则说明字段值重复度高。

40.2 MySQL 查看字段区分度的命令?

在 MySQL 中,可以通过 COUNT(DISTINCT column_name) 和 COUNT(*) 的比值来计算字段的区分度。例如:

SELECT 
    COUNT(DISTINCT gender) / COUNT(*) AS gender_selectivity
FROM 
    users;

40.3 什么样的字段适合加索引?什么不适合?

适合加索引的字段包括:

  1. 经常出现在 WHERE 子句中的字段,如 SELECT * FROM users WHERE age = 30 中的 age 字段,加上索引后可以快速定位到满足条件的记录。
  2. 经常用于 JOIN 的字段,如 SELECT * FROM users u JOIN orders o ON u.id = o.user_id 中的 user_id 字段,加上索引后可以避免多表扫描。
  3. 经常出现在 ORDER BY 或 GROUP BY 子句中的字段,例如:SELECT * FROM users ORDER BY age 中的 age 字段。加上索引后可以避免额外的排序操作。
  4. 高区分度的字段,查询时可以有效减少返回的数据行,比如用户 ID、邮箱等。
    对应的,不适合加索引的字段包括:
  5. 低区分度字段,如性别、状态等。
  6. 经常更新的字段,如用户的登录时间、登录次数等。
  7. 不经常出现在查询条件中的字段,如用户的生日、地址等。
  8. 使用函数、运算符的字段。

41、索引是不是建的越多越好?

不是:

  1. 索引会占据磁盘空间。
  2. 索引虽然会提高查询效率,但是会降低更新表的效率。每次对表进行增删改操作,MySQL 不仅要更新数据,还要更新对应的索引文件。

41.1 说说索引优化的思路?

  1. 选择合适的索引类型:
  • 如果需要等值查询和范围查询,选择 B+ 树索引。
  • 如果是用于处理文本数据的全文搜索,选择全文索引。
  1. 创建适当的索引:
  • 创建组合索引时,应将查询中最常用、区分度高的列放在前面。对于查询条件 WHERE age = 18 AND gender = ‘女’ AND city = ‘洛阳’,如果 age 列的值相对较为分散,可以优先考虑将 age 放在组合索引的第一位。
  • 使用 SELECT 语句时,尽量选择覆盖索引来避免不必要的回表操作,也就是说,索引中包含了查询所需的所有列;但要注意,覆盖索引的列数不宜过多,否则会增加索引的存储空间。

42、为什么 InnoDB 要使用 B+树作为索引?

MySQL 的默认存储引擎是 InnoDB,它采用的是 B+ 树索引,是 B 树的升级版。B 树是一种自平衡的多路查找树,和红黑树、二叉平衡树不同,B 树的每个节点可以有 m 个子节点,而红黑树和二叉平衡树只有 2 个。对于红黑树、二叉平衡树这种 2 分支的树,由于更高,从磁盘往内存中读数据的时候读的次数就更多,需要更多的磁盘 IO,因为每一层都可能需要从磁盘加载新的节点。 B 树的节点大小通常与页的大小对齐,这样每次从磁盘加载一个节点时,可以正好是一个页的大小。因为 B 树的节点可以有多个子节点,可以填充更多的信息以达到一页的大小。
在这里插入图片描述
B 树的一个节点通常包括三个部分:

  • 键值:即表中的主键。
  • 指针:存储子节点的信息。
  • 数据:表记录中除主键外的数据。
    但是因为 B 树的每个节点上都存了数据,就导致每个节点能存储的键值和指针变少了,因为每一页的大小是固定的。于是 B+ 树就来了,B+ 树的非叶子节点只存储键值不存储数据,叶子节点存储了所有的数据,构成了一个有序链表。
    请添加图片描述

这样做的好处是,非叶子节点上由于没有存储数据,就可以存储更多的键值对。由此一来,查找数据进行的磁盘 IO 就更少了,查询的效率也就更高了。加上叶子节点构成了一个有序链表,范围查询时就可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。

42.1 简版回答:

MySQL 的默认存储引擎是 InnoDB,它采用的是 B+ 树索引,B+ 树是一种自平衡的多路查找树,和红黑树、二叉平衡树不同,B+ 树的每个节点可以有 m 个子节点,而红黑树和二叉平衡树都只有 2 个。和 B 树不同,B+ 树的非叶子节点只存储键值,不存储数据,而叶子节点存储了所有的数据,并且构成了一个有序链表。这样做的好处是,非叶子节点上由于没有存储数据,就可以存储更多键值,再加上叶子节点构成了一个有序链表,范围查询时就可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。查询的效率会更高。

42.2 B+ 树的页是单向链表还是双向链表?如果从大值向小值检索,如何操作?

B+ 树的叶子节点是通过双向链表连接的,这样可以方便范围查询和反向遍历。

  • 当执行范围查询时,可以从范围的开始点或结束点开始,向前或向后遍历,这使得查询更为灵活。
  • 在需要对数据进行逆序处理时,双向链表非常有用。
    如果需要在 B+ 树中从大值向小值进行检索,可以按以下步骤操作:
  • 定位到最右侧节点:首先,找到包含最大值的叶子节点。这通常通过从根节点开始向右遍历树的方式实现。
  • 反向遍历:一旦定位到了最右侧的叶子节点,可以利用叶节点间的双向链表向左遍历。

42.3 为什么 MongoDB 索引用 B 树,而 MySQL 用 B+ 树?

B 树的特点是每个节点都存储数据,相邻的叶子节点之间没有指针链接。B+ 树的特点是非叶子节点只存储索引,叶子节点存储数据,并且相邻的叶子节点之间有指针链接。那么在查找单条数据时,B 树的查询效率可能会更高,因为每个节点都存储数据,所以最好情况就是 O(1)。而 B+ 树的叶子节点之间有指针链接,所以适合做范围查询,因为可以直接通过叶子节点间的指针顺序访问整个查询范围内的所有记录,而无需对树进行多次遍历。MySQL 属于关系型数据库,所以范围查询会比较多,所以采用了 B+ 树;但 MongoDB 属于非关系型数据库,在大多数情况下,只需要查询单条数据,所以 MongoDB 选择了 B 树。

43、一棵 B+ 树能存储多少条数据呢?

在这里插入图片描述
假如我们的主键 ID 是 bigint 类型,长度为 8 个字节。指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节。所以非叶子节点(页大小 16 KB)可以存储 16384/14=1170 个这样的单元(键值+指针)。一个指针指向一个存放记录的页,一页可以放 16 条数据(假设 1 条数据 1 KB),树深度为 2 的时候,可以存放 117016=18720 条数据。同理,树深度为 3 的时候,可以存储的数据为 11701170*16=21902400条记录。理论上,在 InnoDB 存储引擎中,B+ 树的高度一般为 3 层,就可以满足千万级数据的存储。查找数据的时候,一次页的查找代表一次 IO,当我们通过主键索引查询的时候,最多只需要 3 次 IO 就可以了。

43.1 InnoDB 使用数据页存储数据?默认数据页大小 16K,我现在有一张表,有 2kw 数据,我这个 B+ 树的高度有几层?

假如我们的主键 ID 是 bigint 类型,长度为 8 个字节。指针大小在 InnoDB 源码中设置为 6 字节,这样一共 14 字节。所以非叶子节点(页大小 16 KB)可以存储 16384/14=1170 个这样的单元(键值+指针)。假设一行数据的大小为 1KB,那么一页的叶子节点就可以存储 16 条数据。对于 3 层的 B+ 树,第一层叶子节点数 x 第二层叶子节点数 x 一页能够存储的数据量 = 1170117016 = 21902400 条数据。

43.2 每个叶子节点能存放多少条数据?

B+ 树索引的每个叶子节点对应一个数据页,默认大小为 16KB。假设一行数据的大小为 1KB,那么一页的叶子节点就可以存储 16 条数据。

44、为什么不用普通二叉树?

普通二叉树存在退化的情况,如果它退化成链表,就相当于全表扫描。

44.1 为什么不用平衡二叉树呢?

平衡二叉树因为只有 2 叉,高度比较高,磁盘 I/O 次数就会比较多。B+ 树是 N 叉,每一层可以存储更多的节点数,树的高度就会降低,因此读取磁盘的次数就会下降,查询效率就快。

45、为什么用 B+ 树而不用 B 树呢?

B+ 树相比 B 树有 2 个显著优势:

  1. 首先,B+ 树的非叶子节点不存储数据,能包含更多的键值指针,因此在相同节点容量下,B+ 树的层级更少,树的高度更低。较少的树层级意味着查找路径更短,磁盘 I/O 次数更少。
  2. 其次,B+ 树的叶子节点通过链表相连,非常适合范围查询,如 ORDER BY 和 BETWEEN。只需要找到符合条件的第一个叶子节点,顺序扫描后续的叶子节点就可以了。而 B 树的每次范围查询都需要回溯到父节点,查询效率较低。

45.1 B+ 树的时间复杂度是多少?

树的高度 h 为: h = l o g m n h = log_mn h=logmn,其中 n 是数据总量,m 是阶数(最多节点数)。每层需要做一次二分查找,复杂度为 O ( l o g m ) O(logm) O(logm)
总复杂度为: O ( l o g m n ∗ l o g m ) = O ( l o g n ) O(log_mn * logm) = O(logn) O(logmnlogm)=O(logn)。(默认底数是 2)

45.2 了解快排吗?

快速排序是一种基于分治法的高效排序算法。其核心思想是:

  1. 选择一个基准值。
  2. 将数组分为两部分,左边小于基准值,右边大于或等于基准值。
  3. 对左右两部分递归排序,最终合并。

45.3 为什么用 B+ 树不用跳表呢?

  1. 跳表基于链表,节点分布不连续,会频繁触发随机磁盘访问,性能较差。
  2. 跳表需要逐节点遍历链表,范围查询性能不如 B+ 树。

45.4 B+ 树的范围查找怎么做的?

B+ 树索引的范围查找主要依赖叶子节点之间的双向链表来完成:

  1. 从 B+ 树的根节点开始,通过索引键值逐层向下,找到第一个满足条件的叶子节点。
  2. 利用叶子节点之间的双向链表,从起始节点开始,依次向后遍历每个节点。直到索引值超过查询范围,或者遍历到链表末尾。比如说在下面这棵 B+ 树上查找 45:
    在这里插入图片描述
    从根节点开始,因为比 25 大,所以从右子树开始。因为 45 比 35 大,所以和右边的索引比较,右侧的索引也是 45,所以继续往右子树查找。从叶子节点 45 开始,依次遍历,找到 45。

46、Hash 索引和 B+ 树索引区别是什么?

  1. B+ 树索引可以进行范围查询,Hash 索引不能。
  2. B+ 树索引支持 order by 排序,Hash 索引不支持。
  3. B+ 树索引支持联合索引的最左前缀原则,Hash 索引不支持。
  4. B+ 树使用 like 进行模糊查询的时候,LIKE ‘abc%’ 的话可以起到索引优化的作用,Hash 索引无法进行模糊查询。
  5. Hash 索引在等值查询上比 B+ 树索引效率更高。

46.1 MySQL 模糊查询怎么查,什么情况下模糊查询不走索引?

MySQL 中进行模糊查询主要使用 LIKE 语句,结合通配符 _(代表单个字符)和 %(代表任意多个字符)来实现。若通配符在前则无法使用索引。

47、聚簇索引与非聚簇索引的区别?

MySQL 默认的存储引擎是 InnoDB,InnoDB 的索引按照 B+ 树的结构存储,不同类型的索引有不同的存储方式。主键索引是按照聚簇索引的方式存储的,叶子节点存储的是整行数据,数据和索引在同一个 B+ 树中。而普通索引、唯一索引是按照非聚簇索引的方式存储的,每个辅助索引都是独立的 B+ 树,叶子节点存储的是主键值,通过主键值回到主键索引中查找完整的数据,俗称回表。每个表只能有一个聚簇索引;但可以有多个非聚簇索引。举例来说:

  • InnoDB 采用的是聚簇索引,如果没有显式定义主键,InnoDB 会选择一个唯一的非空列作为隐式的聚簇索引;如果这样的列也不存在,InnoDB 会自动生成一个隐藏的行 ID 作为聚簇索引。这意味着数据与主键是紧密绑定的,行数据直接存储在索引的叶子节点上。
  • MyISAM 采用的是非聚簇索引,表数据存储在一个地方,而索引存储在另一个地方,索引指向数据行的物理位置。

48、回表了解吗?

回表指在数据库查询过程中,通过非聚簇索引(secondary index)查找到记录的主键值后,再根据这个主键值到聚簇索引(clustered index)中查找完整记录的过程。回表操作通常发生在使用非聚簇索引进行查询,但查询的字段不全在该索引中,必须通过主键进行再次查询以获取完整数据。因此,使用非聚簇索引查找数据通常比使用聚簇索引要慢,因为需要进行两次磁盘访问。当然,如果索引所在的数据页已经被加载到内存中,那么非聚簇索引的查找速度也可以非常快。例如:select * from user where name = ‘张三’;,会先从辅助索引中找到 name=‘张三’ 的主键 ID,然后再根据主键 ID 从主键索引中找到对应的数据行。
在这里插入图片描述

48.1 回表记录越多好吗?

回表记录越多并不是一件好事。事实上,回表的代价是很高的,尤其在记录较多时,回表操作会显著影响查询性能。因为每次回表操作都需要进行额外的磁盘 I/O 读取操作。如果回表记录很多,会导致大量的磁盘 I/O。索引覆盖(Covering Index)可以减少回表操作,将查询的字段都放在索引中,这样不需要回表就可以获取到查询结果了。

49、联合索引了解吗?

联合索引指的是一个索引包含多个列。联合索引的创建语法如下:

CREATE INDEX index_name ON table_name (column1, column2, ...);

49.1 联合索引底层的存储结构是怎样的?

在 MySQL 中,联合索引的底层存储结构是 B+ 树。B+ 树是一种多路搜索树,它的每个节点最多包含 M 个子节点。

49.2 联合索引的叶子节点存的什么内容?

比如说有这样一个联合索引 idx_c2_c3(c2 和 c3 列,主键是 c1),那么叶子节点存储的是 c2、c3 索引列的值和 c1 主键列的值。这样,当查询时,可以先通过联合索引找到对应的主键值,然后再通过主键值找到完整的数据行。

50、覆盖索引了解吗?

覆盖索引(Covering Index)指的是一种索引能够覆盖查询中所涉及的所有列,换句话说,查询所需的数据全部都可以从索引中直接获取,而无需访问数据表的行数据(也就是无需回表)。通常情况下,索引中只包含表中的某些字段,数据库在通过索引查找到满足条件的记录后,还需要回到表中获取其它字段的数据,这个过程叫做回表。

51、什么是最左前缀原则?

使用联合索引时,应遵守最左前缀原则。即:使用联合索引时,查询条件从索引的最左列开始并且不跳过中间的列。如果一个复合索引包含(col1, col2, col3),那么它可以支持 col1、col1, col2 和 col1, col2, col3 的查询优化,但不支持只有 col2 或 col3 的查询。也就说,在进行查询时,如果没有遵循最左前缀原则,那么联合索引可能不会被利用,导致全表扫描。

51.1 为什么不从最左开始查,就无法匹配呢?

比如有一个 user 表,我们给 name 和 age 建立了一个联合索引 (name, age)。

ALTER TABLE user add INDEX comidx_name_phone (name,age);

联合索引在 B+ 树中是复合的数据结构,按照从左到右的顺序依次建立搜索树 (name 在左边,age 在右边)。
在这里插入图片描述我们使用 where name= ‘张三’ and age = ‘20’ 去查询的时候, B+ 树会优先比较 name 来确定下一步应该搜索的方向,往左还是往右。如果 name 相同的时候再比较 age。但如果查询条件没有 name,就不知道应该怎么查了,因为 name 是 B+ 树中的前置条件,没有 name,索引就派不上用场了。

51.2 联合索引 (a, b),where a = 1 和 where b = 1,效果是一样的吗?

不一样。WHERE a = 1 能有效利用联合索引,因为 a 是联合索引的第一个字段,符合最左前缀原则。而 WHERE b = 1 无法利用该联合索引,因为缺少 a 的匹配条件,MySQL 会选择全表扫描。

51.3 联合索引 (a, b, c),where b = 1,能走吗,where a = 1,能走吗。

WHERE b = 1 无法利用联合索引,因为缺少 a 的匹配条件,MySQL 会选择全表扫描。WHERE a = 1 能有效利用联合索引,因为 a 是联合索引的第一个字段,符合最左前缀匹配原则。

52、什么是索引下推优化?

索引下推(Index Condition Pushdown (ICP) )是 MySQL 5.6 时添加的,它允许在访问索引时对数据进行过滤,从而减少回表的次数。例如有一张 user 表,建了一个联合索引(name, age),查询语句:select * from user where name like ‘张%’ and age=10;,没有索引下推优化的情况下:MySQL 会先根据 name like ‘张%’ 查找条件匹配的数据,对于符合索引条件的每一条记录,都会去访问对应的数据行,并在 Server 层过滤 age =10 这个条件。这样就等于说即使 age 不等于 10,MySQL 也会执行回表操作。有索引下推的情况下,MySQL 可以在存储引擎层检查 name like ‘张%’ and age=10 的条件,而不仅仅是 name like ‘张%’。这就意味着不符合 age = 10 条件的记录将会在索引扫描时被过滤掉,从而减少了回表的次数。

53、如何查看是否用到了索引?

可以通过 EXPLAIN 关键字来查看是否使用了索引。

EXPLAIN SELECT * FROM table WHERE column = 'value';

在这里插入图片描述
其结果中的 key 值显示了查询是否使用索引,如果使用了索引,会显示索引的名称。比如 PRIMARY 就表明查询语句使用了主键索引。

53.1 (A,B,C) 联合索引 select * from table where a=? and b in (?,?) and c>? 会走索引吗?

这个查询会使用到联合索引 (A,B,C),因为条件是按照索引列 A、B、C 的顺序来的,这是理想的使用场景。

  1. 对于 A=? 这个条件是一个精确匹配,MySQL 会使用索引来定位到满足条件 A=? 的记录。
  2. 对于 B IN (?, ?) 这个条件指定了 B 列可以取两个可能值。MySQL 会利用索引来查找所有匹配 A=? 且 B 列为这两个值中任意一个的记录。
  3. 对于 C>? 这个条件是一个范围查询。在已经根据 A 和 B 筛选的基础上,MySQL 会继续利用索引来查找 C 列值大于指定值的记录。

53.2 联合索引 abc,a=1,c=1/b=1,c=1/a=1,c=1,b=1 走不走索引?

示例 1(a=1,c=1):

EXPLAIN SELECT * FROM tbn WHERE A=1 AND C=1\G -- \G会垂直展示

在这里插入图片描述
key 是 idx_abc,表明 a=1,c=1 会使用联合索引。但因为缺少了 B 字段的条件,所以 MySQL 可能无法利用索引直接定位到精确的行,而是使用索引来缩小搜索范围。最终,MySQL 需要检查更多的行(rows: 3)来找到满足所有条件的结果集,但总体来说,使用索引明显比全表扫描要高效得多。

示例 2(b=1,c=1):

EXPLAIN SELECT * FROM tbn WHERE B=1 AND C=1\G

在这里插入图片描述
在 idx_abc 索引中,A 是最左边的列,但是查询没有包含 A,因此 MySQL 无法利用这个索引。

示例 3(a=1,c=1,b=1):

EXPLAIN SELECT * FROM tbn WHERE A=1 AND C=1 AND B=1\G

在这里插入图片描述
key 是 idx_abc,表明 a=1,c=1,b=1 会使用联合索引。并且 rows=1,因为查询条件包含了联合索引 idx_abc 中所有列的等值条件,并且条件的顺序与索引列的顺序相匹配,使得查询能够准确、快速地定位到目标数据。最左前缀原则的核心是从最左列开始连续使用,但所有列均为等值查询的情况可以突破顺序限制。

53.3 联合索引的一个场景题:(a,b,c) 联合索引,(b,c) 是否会走索引?

不符合最左前缀原则,不会走索引。

53.4 建立联合索引(a,b,c),where c = 5 是否会用到索引?为什么?

不符合最左前缀原则,不会走索引。

53.5 sql 中使用 like,如果遵循最左前缀匹配查询是不是一定会用到索引?

既然遵循最左前缀匹配,说明一定是联合索引,那么查询是一定会用到索引的。但如果查询条件中的 like 通配符 % 出现在搜索字符串的开始位置,例如 age = 18 and name LIKE ‘%xxx’,MySQL 会先使用联合索引 age_name 找到 age 符合条件的所有行,然后再进行 name 字段的过滤。

54、MySQL 中有哪几种锁,列举一下?

在这里插入图片描述

  1. 按锁粒度划分的话,MySQL 的锁有:
  • 表锁:开销小,加锁快;锁定力度大,发生锁冲突概率高,并发度最低;不会出现死锁。
  • 行锁:开销大,加锁慢;锁定粒度小,发生锁冲突的概率低,并发度高;会出现死锁。
  • 页锁:介于表锁和行锁之间,会出现死锁。
  1. 按兼容性划分的话,MySQL 的锁有:
  • 共享锁(Share Lock,S Lock),也叫读锁(read lock),相互不阻塞。
  • 排他锁(Exclusive Lock,X Lock),也叫写锁(write lock),排它锁是阻塞的,一定时间内,只有一个请求能执行写入,并阻止其它锁读取正在写入的数据。
  1. 按加锁机制划分的话,MySQL 的锁有:
  • 乐观锁:假设冲突在系统中出现的频率较低,在数据库事务的执行过程中,不会频繁地锁定资源,在提交更新的时候才检查是否有其他事务已经修改了数据。可以通过在数据表中使用版本号(Version)或时间戳(Timestamp)实现,每次读取记录时,同时获取版本号或时间戳,更新时检查版本号或时间戳是否发生变化。如果没有变化,则执行更新并增加版本号或更新时间戳;如果检测到冲突(即版本号或时间戳与之前读取的不同),则拒绝更新。
  • 悲观锁:假设冲突在系统中出现的频率较高,在数据库事务的执行过程中,会主动地锁定数据,检查是否有其他事务已经修改了数据。可以直接使用数据库的锁机制,如行锁或表锁,来锁定被访问的数据。常见的实现是 SELECT FOR UPDATE 语句,在读取数据时显式加排他锁(UPDATE SET 隐式),直到当前事务提交或回滚后才释放。

54.1 如何解决库存超卖问题?

按照乐观锁的方式:(高并发性)

UPDATE inventory SET count = count - 1, version = version + 1 WHERE product_id = 1 AND version = current_version;

按照悲观锁的方式:(强一致性)

START TRANSACTION;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE;
UPDATE inventory SET count = count - 1 WHERE product_id = 1;
COMMIT;

55、全局锁和表级锁了解吗?

全局锁就是对整个数据库实例进行加锁,MySQL 中,可以使用 FLUSH TABLES WITH READ LOCK 命令来获取全局读锁。全局锁的作用是保证在备份数据库时,数据不会发生变化。当我们需要备份数据库时,可以先获取全局读锁,然后再执行备份操作。

55.1 表锁了解吗?

表锁就是锁住整个表。在 MySQL 中,可以使用 LOCK TABLES 命令来锁定表。表锁可以分为读锁(共享锁)和写锁(排他锁)。

LOCK TABLES your_table READ;
-- 执行读操作
UNLOCK TABLES;

读锁允许多个事务同时读取被锁定的表,但不允许任何事务进行写操作。

LOCK TABLES your_table WRITE;
-- 执行写操作
UNLOCK TABLES;

写锁允许一个事务对表进行读写操作,其他事务不能对该表进行读写。

在进行大规模的数据导入、导出或删除操作时,为了防止其他事务对数据进行并发操作,可以使用表锁。或者在进行表结构变更(如添加列、修改列类型)时,为了确保变更期间没有其他事务访问或修改该表,可以使用表锁。

56、说说 MySQL 的行锁?

行级锁(Row Lock)是数据库锁机制中最细粒度的锁,主要用于对单行数据进行加锁,确保数据的一致性和完整性。在 MySQL 中,InnoDB 存储引擎支持行级锁。通过 SELECT … FOR UPDATE 可以加排他锁,通过 LOCK IN SHARE MODE 可以加共享锁。比如说:

START TRANSACTION;

-- 加排他锁,锁定某一行
SELECT * FROM your_table WHERE id = 1 FOR UPDATE;
-- 对该行进行操作
UPDATE your_table SET column1 = 'new_value' WHERE id = 1;

COMMIT;
START TRANSACTION;

-- 加共享锁,锁定某一行
SELECT * FROM your_table WHERE id = 1 LOCK IN SHARE MODE;
-- 只能读取该行,不能修改

COMMIT;

高并发环境下,行级锁能够提高系统的并发性能,因为锁的粒度较小,只会锁住特定的行,不会影响其他行的操作。

56.1 select for update 加锁有什么需要注意的?

如果查询条件使用了索引(特别是主键索引或唯一索引),SELECT FOR UPDATE 会锁定特定的行,即行级锁,这样锁的粒度较小,不会影响未涉及的行或其他并发操作。但如果查询条件未使用索引,SELECT FOR UPDATE 可能锁定整个表或大量的行,因为查询需要执行全表扫描。假设有一张名为 orders 的表,包含以下数据:

CREATE TABLE orders (
    id INT PRIMARY KEY,
    order_no VARCHAR(255),
    amount DECIMAL(10,2),
    status VARCHAR(50),
    INDEX (order_no)  -- order_no 上有索引
);

如果我们通过主键索引执行 SELECT FOR UPDATE,只会锁定特定的行:

START TRANSACTION;
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- 对 id=1 的行进行操作
COMMIT;

由于 id 是主键,所以只会锁定 id=1 这行,不会影响其他行的操作。但如果没有索引,就需要执行全表扫描,SELECT FOR UPDATE 可能会锁定表中所有符合条件的记录,甚至是整个表,因为 MySQL 需要检查每一行。

56.2 说说 InnoDB 的行锁实现?

  1. Record Lock 记录锁:直接锁定某行记录。当使用等值查询时,就会直接锁定这条记录。
  2. Gap Lock 间隙锁:在范围查询时锁定记录之间的间隙,防止其他事务在该范围内插入新记录。
  3. Next-key Lock 临键锁:临键指的是间隙加上它右边的记录组成的左开右闭区间。比如 (1,6]、(6,8] 等。临键锁就是记录锁和间隙锁的结合,即除了锁住记录本身,还要锁住索引之间的间隙。MySQL 的默认行锁类型。当使用等值查询匹配到一条记录的时候,临键锁会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
  4. Insert Intention Lock 插入意向锁:事务在插入记录时需要判断插入位置是否被别的事务加了意向锁 。如果有,插入操作需要等待,直到拥有 gap 锁的那个事务提交。提高了插入操作的并发性,解决幻读。

56.3 执行什么命令会加上间隙锁?

当范围查询与锁定操作(如 FOR UPDATE)结合时,InnoDB 会对查询范围内的记录间隙加上间隙锁。

SELECT * FROM table WHERE column > 10 and column < 20 FOR UPDATE;

会对间隙 (10, 20) 加锁,阻止插入 id=15 等操作。

57、意向锁是什么知道吗?

意向锁是一个表级锁,不是插入意向锁!分为两种类型:意向共享锁(IS Lock)、​​意向排他锁(IX Lock)。

意向锁的出现是为了支持 InnoDB 的多粒度锁,解决的是表锁和行锁共存的问题。当我们需要给一个表加表锁的时,我们需要判断表中有没有数据行被锁定,以确定是否能加成功。假如没有意向锁,那么我们就得遍历表中所有数据行来判断有没有行锁。有了意向锁这个表级锁之后,则我们直接判断一次就知道表中是否有数据行被锁定了。

有了意向锁之后,要执行的事务 A 在申请行锁之前,数据库会自动先给事务 A 申请表的意向锁。当事务 B 去申请表的互斥锁时就会失败,因为表上有意向锁之后事务 B 申请表的互斥锁时会被阻塞。
在这里插入图片描述

58、MySQL 的乐观锁和悲观锁了解吗?

悲观锁认为它保护的数据非常敏感,时刻都可能被改动,当一个事务在拿到悲观锁后,其他事务不能再对该数据进行修改,直到它提交或回滚后。MySQL 中的行锁、表锁都是悲观锁。乐观锁认为数据的改动不会太频繁。通过版本号 (version) 或时间戳 (timestamp) 来实现。悲观锁是 MySQL 自带的,而乐观锁通常需要开发者自己去实现。

59、遇到过死锁问题吗,你是如何解决的?

两个事务访问相同的资源,但是访问顺序不同,导致了死锁。

-- 创建表/插入数据
CREATE TABLE account (
    id INT AUTO_INCREMENT PRIMARY KEY,
    balance INT NOT NULL
);

INSERT INTO account (balance) VALUES (100), (200);

-- 事务 1
START TRANSACTION;
-- 锁住 id=1 的行
UPDATE account SET balance = balance - 10 WHERE id = 1;

-- 等待锁住 id=2 的行(事务 2 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 2;

-- 事务 2
START TRANSACTION;
-- 锁住 id=2 的行
UPDATE account SET balance = balance - 10 WHERE id = 2;

-- 等待锁住 id=1 的行(事务 1 已锁住)
UPDATE account SET balance = balance + 10 WHERE id = 1;

解决方法是调整事务的资源访问顺序,保持一致。

60、MySQL 事务的四大特性说一下?

事务是一条或多条 SQL 语句组成的执行单元,要么全部成功,要么全部失败,不存在部分执行的情况。事务具有四个基本特性,也就是 ACID 特性,即原子性、一致性、隔离性和持久性。保证了数据库操作的一致性。
在这里插入图片描述

60.1 什么是原子性?

原子性确保事务中的所有操作要么全部成功,要么全部失败。如果事务中的任何一个操作失败了,整个事务都会回滚到事务开始之前的状态,如同这些操作从未被执行过一样。

60.2 什么是一致性?

一致性确保事务从一个一致的状态转换到另一个一致的状态。(比如银行转账总金额不变)

60.3 什么是隔离性?

隔离性确保一个事务的执行不会被其他事务干扰。隔主要是为了解决并发问题,如脏读、不可重复读、幻读等。数据库系统通过事务隔离级别(如读未提交、读已提交、可重复读、串行化)来实现事务的隔离性。

60.4 什么是持久性?

持久性确保事务一旦提交,对数据库所做的更改就是永久性的,即使发生系统崩溃,数据库也能恢复到最近一次提交的状态。通常,持久性是通过数据库的恢复和日志机制来实现的,确保提交的事务更改不会丢失。

61、ACID 靠什么保证的呢?

MySQL 通过 undo log、redo log、MVCC 来确保 ACID。
在这里插入图片描述

61.1 如何保证原子性?

MySQL 通过 undo log 来确保原子性(Atomicity)。事务开始时,MySQL 会在 undo log 中记录事务开始前的旧值。如果事务执行失败,MySQL 会使用 undo log 中的旧值来回滚事务开始前的状态;如果事务执行成功,MySQL 会在某个时间节点将 undo log 删除。

61.2 如何保证一致性?

如果其他三个特性都得到了保证,那么一致性就自然而然得到保证了。

61.3 如何保证隔离性?

MySQL 定义了多种隔离级别,通过 MVCC 来确保每个事务都有专属自己的数据版本,从而实现隔离性(Isolation)。在 MVCC 中,每行记录都有一个版本号,当事务尝试读取记录时,会根据事务的隔离级别和记录的版本号来决定是否可以读取。

61.4 如何保证持久性?

redo log 是一种物理日志,当执行写操作时,MySQL 会先将更改记录到 redo log 中。当 redo log 填满时,MySQL 再将这些更改写入数据文件中。如果 MySQL 在写入数据文件时发生崩溃,就可以通过 redo log 来恢复数据文件,从而确保持久性(Durability)。

61.5 事务会不会自动提交?

在 MySQL 中,默认情况下事务是自动提交的。每执行一条 SQL 语句,都会被当作一个事务自动提交。如果需要手动控制事务,可以使用 START TRANSACTION 开启事务,并通过 COMMIT 或 ROLLBACK 完成事务。

62、事务的隔离级别有哪些?

事务的隔离级别指定一个事务可能受其他事务影响的程度,MySQL 支持四种隔离级别:读未提交、读已提交、可重复读和串行化。

62.1 什么是读未提交?

当前事务可以读取未被其他事务提交的数据,以至于会出现脏读、不可重复读和幻读的问题。

62.2 什么是读已提交?

当前事务只能读取已经被其他事务提交的数据,可以避免脏读现象。但不可重复读和幻读问题仍然存在。

62.3 什么是可重复读?

可重复读指事务执行过程中看到的数据,和事务启动时看到的数据是一致的。MySQL 默认的隔离级别,避免了脏读和不可重复读,也能在一定程度上避免幻读。

62.4 什么是串行化?

通过强制事务串行执行来解决脏读、不可重复读和幻读问题。但会导致大量的锁竞争问题,进而影响并发性能。

62.5 A 事务未提交,B 事务上查询到的是旧值还是新值?

在 MySQL 的默认隔离级别(可重复读)下,如果事务 A 修改了数据但未提交,事务 B 将看到修改之前的数据。这是因为在可重复读隔离级别下,MySQL 将通过多版本并发控制(MVCC)机制来保证一个事务不会看到其他事务未提交的数据,从而确保读一致性。

62.6 怎么更改事务的隔离级别?

使用 SET SESSION TRANSACTION ISOLATION LEVEL 可以修改当前连接的隔离级别,只影响当前会话。使用 SET GLOBAL TRANSACTION ISOLATION LEVEL 可以修改全局隔离级别,影响新的连接,但不会改变现有会话。

63、什么是脏读、不可重复读、幻读呢?

  1. 脏读:一个事务能够读取另一个事务未提交的数据。如果读到的数据在之后被回滚了,那么第一个事务读取到的就是无效的数据。
  2. 不可重复读:同一事务中执行相同的查询时,返回的结果集不同。这是由于在事务执行过程中,另一个事务修改了数据并提交。
  3. 幻读:同一事务中执行相同的查询时,返回的结果集中出现了之前没有的数据行。这是因为在事务执行过程中,另外一个事务插入了新的数据。

需要解决幻读的场景一般是对数据一致性要求较高的业务,例如银行转账、库存管理等,而在一些只要求最终一致性的应用场景中(如统计功能),可以忽略幻读问题。

63.1 如何避免幻读?

  1. 直接使用最高的隔离级别串行化,但会导致大量的锁竞争。
  2. 使用间隙锁,防止其他事务在间隙范围内插入数据。
  3. 在可重复读的隔离级别下,MVCC 机制会为每个事务维护一个快照,事务读取数据时,只能读取到快照中的数据,而不会读取到其他事务插入的数据。

64、事务的隔离级别是如何实现的?

  1. 读未提交不提供任何锁机制来保护读取的数据,允许读取未提交的数据(即脏读)。
  2. 读已提交和可重复读通过 MVCC 机制中的 ReadView 来实现。
  • 读已提交:每次读取数据前都生成一个 ReadView,保证每次读操作都是最新的数据。
  • 可重复读:只在第一次读操作时生成一个 ReadView,后续读操作都使用这个 ReadView,保证事务内读取的数据是一致的。
  1. 串行化级别下事务读操作时,必须先加表级共享锁,直到事务结束才释放。事务写操作时,必须先加表级排他锁,直到事务结束才释放。

65、MVCC 了解吗?怎么实现的?

MVCC 指的是多版本并发控制,简单来说,就是给我们的 MySQL 数据拍个快照,定格某个时刻数据库的状态。在 MySQL 中,MVCC 是通过版本链和 ReadView 机制来实现的。
在这里插入图片描述

  1. 每条记录包含两个隐藏列:最近修改的事务 ID 和指向 Undo Log 的指针,用于构成版本链。
  2. 每次更新数据时,会生成一个新的数据版本,并将旧版本的数据保存到 Undo Log 中。
  3. 每次读取数据时,会生成一个 ReadView,用于判断哪个版本的数据对当前事务可见。

65.1 什么是版本链?

在 InnoDB 中,每一行数据都有两个隐藏的列:一个是 DB_TRX_ID,另一个是 DB_ROLL_PTR。

  1. DB_TRX_ID:保存创建这个版本的事务 ID。
  2. DB_ROLL_PTR:指向 undo 日志记录的指针,包含了该行的前一个版本的信息。
    当事务更新一行数据时,InnoDB 不会直接覆盖原有数据,而是创建一个新的数据版本,并更新 DB_TRX_ID 和 DB_ROLL_PTR,指向前一个版本和相关的 undo 日志。这样,老版本的数据不会丢失,可以通过版本链找到。

65.2 说说什么是 ReadView?

ReadView(读视图)是 InnoDB 为了实现一致性读而创建的数据结构,它用于确定在特定事务中哪些版本的行记录是可见的。ReadView 主要处理隔离级别为读已提交和可重复读的情况。当事务开始时,InnoDB 会为该事务创建一个 ReadView,这个 ReadView 会记录 4 个重要的信息:

  1. creator_trx_id:创建该 ReadView 的事务 ID。
  2. m_ids:所有活跃事务的 ID 列表,活跃事务是指那些已经开始但尚未提交的事务。
  3. min_trx_id:所有活跃事务中最小的事务 ID。它是 m_ids 数组中最小的事务 ID。
  4. max_trx_id :事务 ID 的最大值加一。换句话说,它是下一个将要生成的事务 ID。

65.3 ReadView 是如何判断记录的某个版本是否可见的?

  1. 如果某个数据版本的 DB_TRX_ID 小于 min_trx_id,则该数据版本在生成 ReadView 之前就已经提交,因此对当前事务是可见的。
  2. 如果某个数据版本的 DB_TRX_ID 大于 max_trx_id,则表示创建该数据版本的事务在生成 ReadView 之后才开始,因此对当前事务是不可见的。
  3. 如果某个数据版本的 DB_TRX_ID 在 min_trx_id 和 max_trx_id 之间,判断 DB_TRX_ID 是否在 m_ids 列表中:
  • 不在,表示创建该数据版本的事务在生成 ReadView 之后已经提交,因此对当前事务也是可见的。
  • 在,表示创建该数据版本的事务仍然活跃,或者在当前事务生成 ReadView 之后开始,因此对当前事务是不可见的。

读事务开启了一个 ReadView,记录了当前活跃事务的 ID 列表(444、555、665),以及最小事务 ID(444)和最大事务 ID(666)。当然还有自己的事务 ID 520,也就是 creator_trx_id。它要读的这行数据的写事务 ID 是 x,也就是 DB_TRX_ID:

  • 如果 x = 110,显然在 ReadView 生成之前就提交了,所以这行数据是可见的。
  • 如果 x = 667,显然是未知世界,所以这行数据对读操作是不可见的。
  • 如果 x = 519,虽然 519 大于 444 小于 666,但是 519 不在活跃事务列表里,所以这行数据是可见的。因为 519 是在 520 生成 ReadView 之前就提交了。
  • 如果 x = 555,虽然 555 大于 444 小于 666,但是 555 在活跃事务列表里,所以这行数据是不可见的。因为不确定有没有提交 555。

65.4 可重复读和读已提交在 ReadView 上的区别是什么?

  1. 读已提交:每次读取数据前都生成一个 ReadView,这样就能保证每次读取的数据都是最新的。
  2. 可重复读:在第一次读取数据时生成一个 ReadView,这个 ReadView 会一直保持到事务结束,保证在事务中多次读取同一行数据时,读取到的数据是一致的。

65.5 如果两个 AB 事务并发修改一个变量,那么 A 读到的值是什么,怎么分析。

当两个事务 A 和 B 并发修改同一个变量时,A 事务读取到的值取决事务的隔离级别、事务的开始时间和提交时间等。

  1. 读未提交:在这个级别下,事务可以看到其他事务尚未提交的更改。如果 B 更改了一个变量但尚未提交,A 可以读到这个更改的值。
  2. 读提交:A 只能看到 B 提交后的更改。如果 B 还没提交,A 将看到更改前的值。
  3. 可重复读:在事务开始后,A 总是读取到变量的相同值,即使 B 在这期间提交了更改。通过 MVCC 机制实现。
  4. 可串行化:A 和 B 的操作是串行执行的,如果 A 先执行,A 读到的值就是 B 提交前的值。如果 B 先执行,A 读到的值就是 B 提交后的值。

66、数据库读写分离了解吗?

读写分离的基本原理是将数据库读写操作分散到不同的节点上,基本实现是:

  1. 数据库服务器搭建主从集群,一主一从、一主多从都可以。
  2. 数据库主机负责读写操作,从机只负责读操作。
  3. 数据库主机通过复制将数据同步到从机,每台数据库服务器都存储了所有的业务数据。
  4. 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。

67、读写分离的分配怎么实现呢?

将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。

  1. 程序代码封装:再代码中抽象一个数据访问层 ,实现读写操作分离和数据库服务器连接的管理。
    在这里插入图片描述
  2. 中间件封装:独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。
    在这里插入图片描述

68、主从复制原理了解吗?

MySQL 的主从复是一种数据同步机制,用于将数据从一个主数据库复制到一个或多个从数据库。广泛用于数据备份、灾难恢复和数据分析等场景。复制过程的主要步骤有:

  1. 在主服务器上,所有修改数据的语句(如 INSERT、UPDATE、DELETE)会被记录到二进制日志中。
  2. 主服务器上的一个线程(二进制日志转储线程)负责读取二进制日志的内容并发送给从服务器。
  3. 从服务器接收到二进制日志数据后,会将这些数据写入自己的中继日志(Relay Log)。中继日志是一个本地存储。
  4. 从服务器上有一个 SQL 线程会读取中继日志,并在本地数据库上执行,从而将更改应用到从数据库中,完成同步。

69、主从同步延迟怎么处理?

主从同步延迟的原因:主服务器开放 N 个连接给客户端,存在高并发的更新操作,但从服务器读取 binlog 的线程仅有一个,当某个 SQL 在从服务器上执行的时间过长或者由于某个 SQL 要进行锁表就会导致主服务器的 SQL 大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。

主从同步延迟的解决办法:

  1. 写操作后读主服务器:对业务的侵入和影响较大。
  2. 读操作失败后再读主服务器:也就是二次读取,主服务器可能顶不住读操作的压力从而宕机。
  3. 关键业务读写操作全部指向主机,非关键业务采用读写分离。

70、你们一般是怎么分库的呢?

  1. 垂直分库:按照业务模块将不同的表拆分到不同的库中,例如,用户表、订单表、商品表等分到不同的库中。
  2. 水平分库:按照一定的策略将一个表中的数据拆分到多个库中,例如,按照用户 id 的 hash 值将用户表拆分到不同的库中。

71、那你们是怎么分表的?

按照淘宝、天猫数据统计,商家数量已有近 3000 万。折中的假设每个商家创建 100 个优惠券,那么优惠券模板表就会接近 30 亿数据量。由于优惠券模板不涉及复杂 SQL,所以取经验值 2000 万,30 亿数据水平拆分成 150 张表。

72、分片策略有哪几种?

  1. 范围分片:根据某个字段的值范围进行分库分表,这种方式适用于分片键具有顺序性或连续性的场景。
  2. Hash 分片:通过对分片键的值进行哈希运算,将数据均匀分布到多个分片中。
  3. 配置路由分片:通过路由配置来确定数据应该存储在哪个表,适用于分片键不规律的场景。

73、不停机扩容怎么实现?

  • 第一阶段:在线双写,查询走老库:
  1. 建立好新的库表结构,数据写入旧库的同时,也写入拆分的新库。
  2. 数据迁移,使用数据迁移程序,将旧库中的历史数据迁移到新库。
  3. 使用定时任务,新旧库的数据对比,把差异补齐。
  • 第二阶段:在线双写,查询走新库:
  1. 完成了历史数据的同步和校验。
  2. 把对数据的读切换到新库。
  • 第三阶段:旧库下线:经过一段时间,确定旧库没有请求之后,下线旧库。

74、怎么分库分表?

分库的场景:

  1. 单个数据库支持的连接数已经不足以满足客户端需求。
  2. 数据量已经超过单个数据库实例的处理能力

分表的场景:

  1. 单表的数据量过大。
  2. 单表存在较高的写入场景,可能引发行锁竞争
  3. 当表中包含大量的 TEXT、LONGTEXT 或 BLOB 等大字段。

74.1 常用的分库分表中间件有哪些?

在选择分库分表框架时,我调研了 ShardingSphere 和它的竞品 MyCat。

  1. 社区活跃度
    MyCat:社区相对较小,更新和新功能开发可能相对缓慢。
    ShardingSphere:由于是 Apache 顶级项目,社区活跃度较高,更新和新功能的开发较为迅速。
  2. 分库分表策略
    Mycat:Mycat 主要支持水平分表和垂直分库,提供相对基础的分片策略。
    ShardingSphere:ShardingSphere 提供了更为灵活和丰富的分库分表策略,支持广泛的分片规则,包括范围、哈希、复合分片等。

75、你觉得分库分表会带来什么问题呢?

  • 分库的角度:
  1. 事务的问题:分库之后必须使用分布式事务来保证事务完整性。
  2. 跨库 JOIN 问题:解决方案就是在业务代码中进行关联或者冗余一些字段。
  • 分表的角度:
  1. 跨节点的 count,order by,group by等问题:只能由业务代码来实现或者用中间件将各表中的数据汇总、排序、分页然后返回。
  2. 数据迁移,容量规划,扩容等问题:可以通过双写回填,动态扩容解决。
  3. ID 问题:解决方案有:自增步长,UUID,分布式 ID 雪花算法。

75.1 id 是怎么生成的?

是通过雪花算法 Snowflake 生成的。

75.2 雪花算法具体是怎么实现的?

核心思想是使用 64 位数字作为全局唯一 ID:

  1. 第 1 位是符号位,永远是 0,表示正数。
  2. 然后是 41 位是时间戳,记录的是当前时间戳减去一个固定的开始时间戳,可以使用 69 年。
  3. 然后是 10 位的工作机器 ID。表示同时支持 1024 台机器。
  4. 最后是 12 位的序列号,每毫秒最多可生成 4096 个 ID。

76、百万级别以上的数据如何删除?

  1. 先删除索引。
  2. 再删除数据。
  3. 重新建立索引。

77、百万千万级大表如何添加字段?

  1. 通过中间表。
  2. 用 pt-online-schema-change。
  3. 先在从库添加再进行主从切换。

78、MySQL cpu 飙升的话,要怎么处理呢?

排查过程:

  1. 使用 top 命令观察,确定是 mysqld 导致还是其他原因。
  2. 如果是 mysqld 导致的,show processlist,查看 session 情况,确定是不是有消耗资源的 sql 在运行。
  3. 找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。
    解决过程:
  4. kill 掉这些线程 (同时观察 cpu 使用率是否下降)。
  5. 进行相应的调整 (比如说加索引、改 sql、改内存参数)
  6. 重新执行这些 SQL。

79、一张表:id,name,age,sex,class,sql 语句:所有年龄为 18 的人的名字?找到每个班年龄大于 18 有多少人?找到每个班年龄排前两名的人?

SELECT name FROM students WHERE age = 18;
SELECT class, COUNT(*) AS number_of_students
FROM students
WHERE age > 18
GROUP BY class;
SELECT a.class, a.name, a.age
FROM students a
WHERE (
	SELECT COUNT(DISTINCT b.age)
	FROM students b
	WHERE b.class = a.class AND b.age > a.age
) < 2
ORDER BY a.class, a.age DESC;

80、有一个查询需求,MySQL 中有两个表,一个表 1000W 数据,另一个表只有几千数据,要做一个关联查询,如何优化。

  1. 为关联字段建立索引,确保两个表中用于 JOIN 操作的字段都有索引。
CREATE INDEX idx_user_id ON users(user_id);
CREATE INDEX idx_user_id ON orders(user_id);
  1. 小表驱动大表,在执行 JOIN 操作时,先过滤小表中的数据。
SELECT u.*, o.*
FROM (
    SELECT user_id
    FROM users
    WHERE some_condition  -- 这里是对小表进行过滤的条件
) AS filtered_users
JOIN orders o ON filtered_users.user_id = o.user_id
WHERE o.some_order_condition;  -- 如果需要,可以进一步过滤大表

81、新建一个表结构,创建索引,将百万或千万级的数据使用 insert 导入,与新建一个表结构,将百万或千万级的数据使用 insert 导入,再创建索引,这两种效率哪个高呢?或者说用时短呢?

  1. 先创建索引再插入数据:这种情况下,数据库需要在每次插入新记录时维护索引结构,随着数据量的增加,索引的维护可能会导致额外的性能开销。
  2. 先插入数据再创建索引:在没有索引的情况下插入数据,数据库不需要在每次插入时更新索引,这会减少插入操作的开销。之后一次性创建索引通常比逐条记录更新索引更快。

81.1 数据库是先建立索引还是先插入数据?

InnoDB 中,如果表定义了主键,那么主键索引就是聚簇索引。如果没有明确指定主键,InnoDB 会自动选择一个唯一索引作为聚簇索引。如果表没有任何唯一索引,InnoDB 将自动生成一个隐藏的行 ID 作为聚簇索引。这意味着当插入新数据时,InnoDB 首先将数据插入聚簇索引。这一步骤实质上是创建索引的一部分,因为数据存放在索引结构中。

82、一个表(name,sex,age,id),select age,id,name from tblname where name=‘paicoding’;怎么建索引?

由于查询条件是 name=‘paicoding’,所以应当为 name 字段建立索引。查询中选择了age、id和name字段,如果这三列经常一起使用,可以考虑建立包含这些字段的联合索引。可以将查询条件中的字段放在联合索引的首位,这样查询时可以利用索引覆盖,直接从索引中获取数据,而不需要再去查找数据行。

83、一个学生成绩表,字段有学生姓名、班级、成绩,求各班前十名?

  1. 第一步,建表:
CREATE TABLE student_scores (
    student_name VARCHAR(100),
    class VARCHAR(50),
    score INT
);
  1. 第二步,插入数据:
INSERT INTO student_scores (student_name, class, score) VALUES
('沉默王二', '三年二班', 88),
('沉默王三', '三年二班', 92),
('沉默王四', '三年二班', 87),
('沉默王五', '三年二班', 85),
('沉默王六', '三年二班', 90),
('沉默王七', '三年二班', 95),
('沉默王八', '三年二班', 82),
('沉默王九', '三年二班', 78),
('沉默王十', '三年二班', 91),
('沉默王十一', '三年二班', 79),
('沉默王十二', '三年三班', 84),
('沉默王十三', '三年三班', 81),
('沉默王十四', '三年三班', 90),
('沉默王十五', '三年三班', 88),
('沉默王十六', '三年三班', 87),
('沉默王十七', '三年三班', 93),
('沉默王十八', '三年三班', 89),
('沉默王十九', '三年三班', 85),
('沉默王二十', '三年三班', 92),
('沉默王二十一', '三年三班', 84);
  1. 第三步,查询各班前十名:
SET @cur_class = NULL, @cur_rank = 0;

SELECT student_name, class, score
FROM (
    SELECT
        student_name,
        class,
        score,
        @cur_rank := IF(@cur_class = class, @cur_rank + 1, 1) AS ranking,
        @cur_class := class
    FROM student_scores
    ORDER BY class, score DESC
) AS ranked
WHERE ranked.rank <= 10;

使用 @cur_class 和 @cur_rank 来跟踪当前行的班级和排名。在 SELECT 语句中,通过检查当前班级(@cur_class)是否与上一行相同来决定排名。如果相同,则增加排名;如果不同,则重置排名为 1。然后通过 ORDER BY 子句确保在计算排名前按班级和分数排序。

### MySQL 试经验和常见问题 #### 数据库基础知识 在Linux系统运维企业试中,除了技术能力和解决问题的能力外,数据库的基础知识也是重要的考核部分[^1]。对于MySQL而言,了解SQL语言和关系型数据库的基本概念是必不可少的。这包括表结构设计、索引创建与优化、事务处理机制等内容。 #### SQL语句编写能力 掌握标准SQL语法并能熟练运用各种子查询、连接操作(JOIN)、聚合函数等高级特性来完成复杂数据检索任务是非常关键的一项技能。例如,在多张表格之间建立联系以提取所需信息: ```sql Select t1.*, t2.price from table1 t1 join (select id, price from table2 order by price DESC limit 10) as t2 on t1.id = t2.id; ``` 这段代码展示了如何先筛选出`table2`中的高价商品列表再将其与另一张表关联起来获取更多细节[^4]。 #### JDBC基础 作为Java应用程序中最常用的持久化层框架之一,JDBC提供了访问不同类型的关系型数据库API接口。熟悉其工作原理有助于更好地理解底层实现逻辑,并能够写出高效稳定的程序代码[^3]。 #### 多结果集合并 当需要将来自不同SELECT命令的结果组合成单一输出时可以采用UNION运算符。需要注意的是两个或多个待联合的选择项必须具有相同数量列数且对应位置上的字段类型相匹配。 #### 性能调优策略 性能问题是很多企业在实际生产环境中遇到的最大挑战之一。因此候选人应该具备一定的分析工具使用经验以及针对特定场景下的解决方案建议,比如通过调整配置参数或者重构低效查询等方式提高整体运行效率。 #### 安全性和备份恢复方案 安全始终是一个重要话题,尤其是在涉及到敏感商业信息保护方更是如此;而定期做好增量/完全备份计划则可以在意外发生后迅速恢复正常业务运作状态。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值