
00:00:00
外卖卷:非常划算 扫码领劵 省点小钱钱
文章发布较早,内容可能过时,阅读注意甄别。
MySQL 存储引擎有哪些?
想象一下,你开了一家图书馆,里面要放各种各样的书。这些书怎么摆、怎么管理、怎么借阅,就是咱们说的“存储引擎”。
MySQL 默认帮你准备了几种不同的“管理方式”,最常用的主要有两种:
除了这两个,还有一些“小众”的,比如 Memory(内存)、CSV(逗号分隔值)等等,但它们的应用场景比较特殊,咱们先不展开,主要把精力放在 InnoDB 和 MyISAM 上。
它们之间有什么区别?
既然是两种不同的管理方式,那肯定有各自的特点和优缺点。咱们还是用图书馆的比喻来说:
InnoDB:高并发、安全、稳定,就像一个“现代化图书馆”
最大特点:支持事务(ACID)
支持行级锁(Row-level Locking)
支持外键(Foreign Key)
数据恢复能力强(Crash Recovery)
缺点:相对来说,占用的磁盘空间会大一些,写入性能在某些极端情况下(比如大量插入不相关的短数据)可能会略低于 MyISAM。
MyISAM:读写快、简单粗暴,就像一个“传统图书馆”
最大特点:读写速度快(特别是读)
只支持表级锁(Table-level Locking)
不支持事务(ACID)
不支持外键
缺点:数据安全性差(容易出现数据不一致),并发性差,不支持崩溃恢复(一旦崩溃,数据可能就丢失或损坏了)。
聚簇索引和非聚簇索引是什么?
聚簇索引:数据和索引“绑定”在一起。
非聚簇索引:索引和数据“分离”,索引只存“位置信息”。
它们之间有什么区别?
聚簇索引
MySQL InnoDB 中的聚簇索引:
特点:
非聚簇索引
MySQL InnoDB 中的非聚簇索引:
特点:
从索引的存储结构角度来分类:
从索引的逻辑功能和约束性角度来分类:
普通索引
唯一索引
主键索引
全文索引
CHAR、VARCHAR、TEXT 类型字段。以前只有 MyISAM 引擎支持,现在 InnoDB 也支持了。组合索引
(col1, col2, col3) 上创建了组合索引,那么当你查询时只使用 col1,或者使用 (col1, col2),或者使用 (col1, col2, col3),这个索引都能生效。但如果你只使用 col2 或 col3,或者使用 (col2, col3),则这个索引不会生效,或者只能部分生效。 从索引的数据结构和算法角度来分类:
B+ 树索引
哈希索引
=)。=),不支持 <、>、BETWEEN 等操作。R-树索引 / 空间索引
SPATIAL 关键字来创建 R-树索引,InnoDB 引擎需要使用其他方式(如使用 B+ 树索引存储 Muti-Polygon)来模拟空间索引。倒排索引
想象一下,你有一家巨大的图书馆(这就是你的数据库),里面藏着无数的书籍(这就是你的数据)。
为了方便读者(用户)快速找到书,你需要一个索引(目录)。
现在,我们来看看 MySQL 为什么选择了 B+ 树这个“目录”:
核心原因一:因为它找书又快又省力(查询效率高)
分层目录,快速定位(减少磁盘 IO):
只在最底层放书,目录更精简(非叶子节点不存数据,存储更多索引):
核心原因二:因为它能连着找书,特别方便(范围查询友好)
id BETWEEN 100 AND 200)。核心原因三:因为它存储效率高,充分利用硬件优势(磁盘页对齐)
想象一下你有一本厚厚的电话簿(这就是你的索引),上面按姓名的顺序排好了:
现在,你给电话簿建了一个“组合索引”,也就是你把姓名分成了好几部分来排序,比如:
INDEX(姓, 名, 小名)
这意味着电话簿是这样排序的:
最左前缀匹配原则,说的就是:
你查电话簿的时候,只要你提供了最左边的一部分信息(或者说,你提供了从最左边开始,连续的几部分信息),电话簿(索引)就能帮你快速找到!
我们来举几个例子:
能用上索引的情况(符合最左前缀原则):
WHERE 姓 = '张'WHERE 姓 = '张' AND 名 = '小明'WHERE 姓 = '张' AND 名 = '小明' AND 小名 = 'Jack'不能完全用上索引的情况(不符合最左前缀原则):
WHERE 名 = '小明'WHERE 小名 = 'Jack'WHERE 姓 = '张' AND 小名 = 'Jack'为什么叫“最左前缀”呢?
就是说,你的查询条件,必须从你建立索引的字段列表的最左边开始,连续地提供信息。
比如 INDEX(A, B, C):
A = ? ✔️ (最左边 A)A = ? AND B = ? ✔️ (最左边 A,然后是 B)A = ? AND B = ? AND C = ? ✔️ (最左边 A,然后 B,然后 C)B = ? ❌ (没从最左边 A 开始)C = ? ❌ (没从最左边 A 开始)A = ? AND C = ? ❌ (跳过了 B,索引只能用到 A)我们来计算一下 MySQL 三层 B+ 树(假设指的是非叶子节点有两层,叶子节点一层,也就是总共 3 层)能够存储的数据量。
首先,需要明确几个前提和假设:
BIGINT 类型,占用 8 字节。计算步骤:
第一层:叶子节点(存储实际数据行)
16 KB / 1 KB/行 = 16 行数据第二层:非叶子节点(存储索引键和指向叶子节点的指针)
索引键大小 + 指针大小 = 8字节 (BIGINT) + 6字节 (指针) = 14字节16 KB / 14 字节/项 = 16 * 1024 字节 / 14 字节/项 ≈ 1170 项第三层:根节点(最顶层的非叶子节点,存储索引键和指向第二层的指针)
总共能存储的数据量计算:
1170 个第二层节点。1170 个叶子节点。16 行数据。总数据行数 = 1170 × 1170 × 16 = 21,902,400 行
结论:
在理想情况下,一个 3 层的 B+ 树(根节点、中间层、叶子层)大约可以存储超过 2000 万(2 千多万)行数据!
这个数字意味着什么?
核心概念:回表是指当 MySQL 使用辅助索引进行查询时,发现辅助索引的叶子节点只存储了索引列的值和主键值,而没有存储查询所需的所有列数据时,需要根据辅助索引叶子节点获取到的主键值,再回到主键索引中去查找完整的行数据。
打个比方:想象你有一本大字典(这就是你的主键索引),里面包含了每个词语的所有详细信息(词性、解释、例句等等)。这本字典是按照词语的字母顺序排列的,所以你知道词语就能直接找到它的所有信息。
现在,你又有一本小册子(这就是你的辅助索引),这本小册子是按照“词语的笔画数”来排序的。但是,这本小册子里面只记录了:
现在,我们来看两种查询场景:
场景一:不用回表(直接通过索引覆盖查询所需的所有列)
SELECT 词语 FROM 字典 WHERE 笔画数 = 3;场景二:需要回表(辅助索引无法覆盖查询所需的所有列)
SELECT 词语, 解释, 例句 FROM 字典 WHERE 笔画数 = 3;MySQL 中实际的对应关系:
当发生回表时,流程是这样的:
为什么会有回表?
为了节省存储空间和提高查询效率。如果每个辅助索引都把所有列的数据都存一遍,那会占用大量的磁盘空间,而且更新数据时也需要更新所有索引,效率会很低。所以 MySQL 选择只在辅助索引中存储主键值,需要时再通过主键值去主键索引中获取完整数据。
如何避免回表(提高性能)?
SELECT age, id FROM users WHERE age = 25; 如果 (age, id) 是一个联合辅助索引,那么查询只需要在这个索引中就能完成,不会回表。SELECT *,只查询你真正需要的列。 age,就 SELECT age FROM users WHERE age = 25;。如果 age 是辅助索引,就不会回表。理解回表对于优化 MySQL 查询性能非常重要,特别是当数据量很大时,减少回表次数能显著提升查询速度。
MySQL 中使用索引不一定有效。 这是一个常见的误区,认为只要加了索引就万事大吉。实际上,索引是一把双刃剑,用得好能提升性能,用不好反而可能拖慢系统。
为什么使用索引不一定有效?
INDEX(a, b, c),查询 WHERE b = 1 或 WHERE a = 1 AND c = 2 都无法充分利用索引。YEAR(date_col) = 2023)、类型转换(隐式或显式)、数学运算等,会导致索引失效。 WHERE TO_DAYS(date_col) > XXXWHERE id + 1 = 10WHERE name LIKE '%suffix' (左模糊匹配)!=、< >、NOT IN、NOT LIKE 等负向查询有时会导致索引失效或效果不佳。OR 连接的条件如果索引列不同,也可能导致索引失效。<, <=, >, >=, BETWEEN) 本身可以使用索引,但如果范围过大,MySQL 可能会认为全表扫描更快。WHERE gender = '男' 时,如果男性占了总人数的 90%,MySQL 可能会认为直接全表扫描更划算,从而放弃使用索引。如何排查索引效果?
排查索引效果是 SQL 优化中最关键的一步。MySQL 提供了强大的工具 EXPLAIN 来帮助我们分析 SQL 执行计划。
核心工具:EXPLAIN 语句可以模拟优化器执行 SQL 查询,从而知道 MySQL 是如何处理你的 SQL 语句的。它会显示表的连接顺序、如何使用索引、扫描的行数等信息。
使用方法:在你的 SELECT 语句前加上 EXPLAIN 关键字即可:
EXPLAIN SELECT id, name, age FROM users WHERE age > 25 AND gender = 'female';EXPLAIN 结果中的关键字段及含义:
id: SELECT 查询的序列号,表示查询中每个 SELECT 语句的执行顺序。select_type: SELECT 类型,如 SIMPLE (简单查询), PRIMARY (主查询), SUBQUERY (子查询), UNION (UNION 中的第二个或后面的查询)。table: 查询的表名。partitions: 匹配到的分区信息(如果使用了分区表)。type: 最重要的字段之一! 表示 MySQL 查找数据的方式,从差到好依次是: ALL: 全表扫描,最差,通常意味着没有用到索引或者索引失效。index: 全索引扫描,比ALL好一些,因为它扫描的是索引而不是数据行,但仍然扫描了整个索引。通常发生在查询的列都在索引中,但 where 条件无法进一步过滤时。range: 范围扫描,通过索引查找一个给定范围内的行,如WHERE id BETWEEN 1 AND 10。ref: 非唯一性索引扫描,例如使用非唯一索引或唯一索引的前缀来查找。eq_ref: 唯一性索引扫描,常用于联接查询,表示前一个表的每一行都匹配到当前表的一个唯一行。const, system: 查询优化器能将查询转换为一个常量。非常快,通常是主键或唯一索引等值查询。null: 优化器在优化阶段分解查询,不需要访问表或索引。ref, eq_ref, const 是最佳类型,range 也不错,应尽量避免 ALL 和 index。possible_keys: MySQL 可能选择的索引。key: 实际使用的索引。 如果为NULL,则表示没有使用索引。key_len: 使用的索引的长度。对于联合索引,这个值可以帮助你判断索引使用了多少列。ref: 表示哪个列或常量与key一起使用来查找行。rows: 非常重要的字段! 估算 SQL 语句会扫描的行数。这个数字越小越好。filtered: 表示通过这个表条件过滤出的行百分比。Extra: 非常重要的字段! 额外信息,通常能提供很多优化线索: Using filesort: 出现了文件排序(在内存或磁盘上进行排序),通常说明ORDER BY的列没有索引,或索引无法用于排序。性能较差。Using temporary: 使用了临时表来处理查询,通常发生在GROUP BY或ORDER BY不同的列,或者复杂子查询中。性能较差。Using index: 表示使用了覆盖索引(Covering Index),查询的所有列都在索引中,无需回表。这是非常高效的,性能很好。Using where: 表示 MySQL 将通过WHERE子句来过滤结果。通常配合Using index或Using index condition出现。Using index condition: MySQL 5.6 引入的索引条件下推(Index Condition Pushdown, ICP)。表示 MySQL 会在存储引擎层(索引层)进行条件过滤,而不是将所有匹配的索引条目都读取出来再在 MySQL 服务器层过滤。可以减少回表次数。排查索引效果的步骤:
pt-query-digest,或 MySQL Enterprise Monitor)找到耗时长的 SQL 语句。EXPLAIN 分析:对慢查询语句使用 EXPLAIN。type 字段:ALL 或 index,说明索引效果很差或没用上。range, ref, eq_ref, const,通常表示索引使用得不错。key 字段:NULL,说明没有用到任何索引。rows 字段:rows 值应该尽可能小。如果 rows 很大,即使 type 不是 ALL,也可能意味着索引选择性差或范围过大。Extra 字段:Using filesort 或 Using temporary:尝试优化 ORDER BY 或 GROUP BY 字段,考虑添加索引或调整索引顺序。Using index:恭喜你,这是一个覆盖索引,效率非常高。Using index 但 type 是 ref/range:可能存在回表,如果查询列很多,考虑是否能调整为覆盖索引。WHERE条件,使其符合最左前缀原则。优化LIKE查询(避免左模糊)。USE INDEX 或 FORCE INDEX 强制 MySQL 使用某个索引(但不推荐,因为可能未来数据变化后,强制索引反而变慢)。在 MySQL 中建立索引是一项非常重要的优化工作,但绝不是越多越好,也不是随意建立。以下是在建立索引时需要注意的关键事项:
索引不是越多越好,要权衡利弊
SELECT 语句)。INSERT、DELETE、UPDATE 操作时,除了修改数据,还需要同步更新所有关联的索引。索引越多,更新成本越高。选择合适的列建立索引
WHERE 子句中经常使用的列:这是最主要的考虑因素,因为索引的主要目的是加速过滤条件。JOIN (连接) 条件中使用的列:JOIN 操作中 ON 子句里的列通常需要索引,以加速连接过程。ORDER BY (排序) 和 GROUP BY (分组) 子句中使用的列:如果这些列有索引,可以避免使用文件排序(filesort)和临时表(Using temporary),大大提高效率。不重复值数量 / 总行数VARCHAR 类型的列,可以考虑使用前缀索引(如 INDEX(name(10)),只索引前 10 个字符),但需要权衡区分度。TEXT, BLOB)建立完整索引:这些字段通常很大,直接索引会占用大量空间。如果需要,考虑使用前缀索引或全文索引。理解联合索引(复合索引)和最左前缀原则
INDEX(col1, col2, col3)。WHERE col1 = ? (有效)WHERE col1 = ? AND col2 = ? (有效)WHERE col1 = ? AND col2 = ? AND col3 = ? (有效)WHERE col2 = ? (无效)WHERE col1 = ? AND col3 = ? (只能利用 col1 部分)col1, col2, col3,那么 INDEX(col1, col2, col3) 可能会成为一个覆盖索引,避免回表。避免索引失效的情况
WHERE YEAR(date_col) = 2023 会使 date_col 上的索引失效。WHERE name LIKE '%abc' 不会使用 name 上的索引,因为无法利用 B+ 树的顺序。'abc%' 可以。!=、NOT IN 通常会导致全表扫描。OR 连接条件:如果 OR 两边的条件涉及不同索引列,通常会导致索引失效(除非优化器可以合并索引)。主键索引(聚簇索引)的特殊性
考虑索引覆盖(Covering Index)
SELECT 列、ORDER BY 列),那么就称这个索引为覆盖索引。EXPLAIN 结果中,Extra 字段出现 Using index 就表示使用了覆盖索引。定期分析和优化索引
EXPLAIN:这是分析 SQL 执行计划和索引使用情况最直接、最有效的方法。ANALYZE TABLE:偶尔运行 ANALYZE TABLE <tablename> 可以更新表的统计信息,帮助查询优化器做出更准确的判断。生产环境的注意事项
MySQL 中的索引数量不是越多越好。
索引虽然能提高查询效率,但它也带来了额外的成本,这些成本会随着索引数量的增加而显著上升。
增加磁盘空间占用:
降低写入(INSERT, UPDATE, DELETE)性能:
增加查询优化器的复杂性:
增加内存消耗:
维护成本:
总结:
索引是一种空间换时间的策略,用额外的存储空间和写入性能开销来换取查询性能的提升。当索引数量过多时,这些成本会超出它带来的收益,导致系统整体性能下降。因此,建立索引时需要精心设计,只创建那些真正能带来显著性能提升的、有用的索引。
EXPLAIN 语句是 MySQL 提供的一个强大的工具,用于分析 SQL 查询语句的执行计划。通过它可以了解 MySQL 如何处理查询,包括表连接顺序、索引使用情况、扫描行数等,从而发现潜在的性能瓶颈。
使用方法:
在任何 SELECT, INSERT, UPDATE, DELETE 语句(MySQL 5.6.3 及更高版本也支持 EXPLAIN FOR CONNECTION)的前面加上 EXPLAIN 关键字即可。
EXPLAIN SELECT column1, column2 FROM your_table WHERE column3 = 'value' ORDER BY column4;EXPLAIN 结果中的关键字段及其含义:
理解这些字段是分析执行计划的关键。
id:
select_type:
SIMPLE:简单查询(不包含 UNION 或子查询)。PRIMARY:最外层的 SELECT 查询。SUBQUERY:子查询中的第一个 SELECT 查询。DERIVED:派生表查询(FROM 子句中的子查询)。UNION:UNION 中的第二个或后面的 SELECT 查询。UNION RESULT:UNION 的结果。table:
partitions: (MySQL 5.7+ 新增)
type: 最重要的字段之一!
const > eq_ref > ref > range > index > ALL。const / system: 查询优化器能将查询转换为一个常量。非常快,通常是主键或唯一索引的等值查询。eq_ref: 唯一性索引扫描,表示前一个表的每一行都匹配到当前表的一个唯一行。通常用于联接查询。ref: 非唯一性索引扫描,例如使用非唯一索引或唯一索引的前缀来查找。range: 范围扫描,通过索引查找一个给定范围内的行(如 BETWEEN, >, <, IN)。index: 全索引扫描,MySQL 扫描了整个索引来查找匹配的行。比 ALL 好,因为只扫描索引,不需要回表,但仍然是全表级别的。通常发生在覆盖索引查询,但没有 where 条件过滤。ALL: 全表扫描。 这是最差的类型,意味着 MySQL 将遍历整个表来找到匹配的行。通常表明没有使用到索引,或者索引失效。应极力避免。possible_keys:
key:
NULL,则表示没有使用索引。这是你判断索引是否生效的关键。key_len:
ref:
key 一起使用来查找行。例如 const 表示常量,db.col_name 表示一个列。rows: 非常重要的字段!
filtered: (MySQL 5.1+ 新增)
rows * filtered / 100 表示最终将有多少行与下一张表进行连接。Extra: 非常重要的字段!
Using filesort: 警告! MySQL 需要对结果集进行外部排序(在内存或磁盘上),通常是因为 ORDER BY 的列没有索引,或索引无法用于排序。性能较差。Using temporary: 警告! MySQL 使用了临时表来处理查询,通常发生在 GROUP BY 或 DISTINCT 与 ORDER BY 不同的列,或者复杂子查询中。性能较差。Using index: 非常棒! 表示使用了覆盖索引(Covering Index),查询所需的所有列都在索引中,无需回表。效率极高。Using where: 表示 MySQL 将通过 WHERE 子句来过滤结果。通常配合 Using index 或 Using index condition 出现。Using index condition: MySQL 5.6 引入的索引条件下推(ICP)。表示 MySQL 会在存储引擎层(索引层)进行条件过滤,而不是将所有匹配的索引条目都读取出来再在 MySQL 服务器层过滤。可以减少回表次数。Using join buffer (Block Nested Loop) / Using join buffer (Batched Key Access): 表示使用了连接缓冲区,通常发生在没有索引的连接条件上。分析步骤:
type:首要目标是避免 ALL 和 index。争取达到 ref、eq_ref、const。range 也是可以接受的。key:确保实际使用了你期望的索引。rows:这个值越小越好,它直接关联到性能。Extra:特别关注是否有 Using filesort 或 Using temporary。如果有,表明排序或分组效率低,需要考虑添加索引或调整 SQL。出现 Using index 则是好兆头。SQL 调优是一个系统性的过程,通常涉及以下几个方面:
找出慢查询
long_query_time 阈值的 SQL 语句。这是最常用的方法。 slow_query_log = 1,slow_query_log_file = /path/to/slow.log,long_query_time = 1 (表示 1 秒)。SHOW PROCESSLIST:查看当前正在执行的 SQL 语句,了解哪些查询耗时。pt-query-digest (Percona Toolkit):对慢查询日志进行分析,统计和汇总最慢的 SQL。使用 EXPLAIN 分析执行计划
这是 SQL 调优的核心步骤。
优化索引
WHERE 条件列:优先考虑。JOIN 条件列:确保连接列有索引。ORDER BY 和 GROUP BY 列:考虑创建联合索引以覆盖这些操作,避免 filesort 和 temporary。Using index)。%keyword)。OR 连接不同索引列的条件。优化 SQL 语句本身
SELECT 列):避免 SELECT *,特别是当表很大,或者查询结果集很大时。这可以减少网络传输、内存消耗和回表操作。JOIN 或者 EXISTS / NOT EXISTS 来优化,通常 JOIN 性能更好。JOIN 语句:JOIN 条件列有索引。JOIN 类型(INNER JOIN, LEFT JOIN 等)。COUNT(*):COUNT(*) 通常需要全表扫描。WHERE 条件,确保 WHERE 条件有索引。COUNT(*) 是为了判断是否存在记录,用 EXISTS 或 LIMIT 1 更高效。LIMIT 分页:LIMIT offset, rows 当 offset 很大时效率会很低。SELECT * FROM table_name WHERE id > (SELECT MAX(id) FROM table_name WHERE condition LIMIT offset, 1) LIMIT rows; 或者通过覆盖索引优化 LIMIT。HAVING:HAVING 是在 GROUP BY 之后进行过滤,如果过滤条件可以提前到 WHERE 中,就尽量提前。UNION ALL 代替 UNION:如果不需要去重,UNION ALL 效率更高。SELECT DISTINCT 大量数据:DISTINCT 会增加额外的排序和去重开销。INSERT INTO table_name VALUES (...), (...), (...); 比单条插入效率高。调整数据库配置
innodb_buffer_pool_size:最重要的参数,决定了 InnoDB 缓存数据和索引的内存大小。越大越好(在服务器内存允许的情况下)。innodb_log_file_size:影响事务日志写入性能。tmp_table_size / max_heap_table_size:影响内存中临时表的大小,过小可能导致临时表写入磁盘。sort_buffer_size:排序缓冲区大小。join_buffer_size:连接缓冲区大小。数据库结构优化(Schema Design)
INT 而不是 BIGINT,CHAR 而不是 VARCHAR(255) 如果长度固定)。JOIN 操作,但会增加数据一致性的维护成本。MySQL 的 InnoDB 存储引擎使用 B+ 树作为其索引结构。查询数据时,MySQL 会根据查询条件(特别是 WHERE 子句)来决定如何利用 B+ 树索引来定位数据。
B+ 树的结构回顾:
查询数据的全过程(根据查询条件分类):
场景一:通过主键查询(使用聚簇索引)
假设表 user 有主键 id,查询 SELECT * FROM user WHERE id = 123;
id 值 123 与当前节点中的键值,通过二分查找法(或类似高效查找算法)快速定位到下一个子节点的指针。100 和 200,123 在 100 和 200 之间,则指向对应的子节点。id = 123 精确找到包含该主键值的叶子节点。id = 123 对应的那一行所有数据。特点:效率非常高,通常只需要很少的几次磁盘 I/O(因为 B+ 树层数很少,且直接获取完整数据)。
场景二:通过辅助索引查询,且需要回表(非覆盖索引)
假设表 user 有辅助索引 idx_name 在 name 列上,查询 SELECT * FROM user WHERE name = 'Alice';
idx_name 的根节点开始。name 值 'Alice',逐层向下遍历辅助索引的 B+ 树,直到达到叶子节点层。name = 'Alice' 的条目。name 值和对应的主键值(假设是 id)。'Alice',会得到多个 id 值,如 id = 101, id = 205, id = 310。101, 205, 310),MySQL 需要再次进行一次查询操作。特点:比主键查询慢,因为需要两次查找(一次辅助索引,一次聚簇索引)且可能多次回表 I/O。回表次数越多,性能越差。
场景三:通过辅助索引查询,且不需要回表(覆盖索引)
假设表 user 有辅助索引 idx_name_age 在 (name, age) 列上,查询 SELECT name, age FROM user WHERE name = 'Alice';
idx_name_age 的根节点开始。name 值 'Alice',逐层向下遍历辅助索引的 B+ 树,直到达到叶子节点层。name = 'Alice' 的条目。SELECT 列表只包含 name 和 age 这两列,而这两列的值都在 idx_name_age 这个辅助索引的叶子节点中可以直接获取到(辅助索引的叶子节点存储了 name, age 以及主键 id)。name 和 age 的值,而无需再回表到聚簇索引去查找。特点:效率非常高,接近主键查询,因为它只需要遍历一次辅助索引的 B+ 树,避免了回表操作。EXPLAIN 结果中 Extra 字段会显示 Using index。
count(*)、count(1) 和 count(字段名) 有什么区别?这三个 COUNT 函数在功能上都用于计算行数,但在执行效率和统计范围上有一些细微的区别。
count(*)
count(*) 会找到一个最小的(通常是主键)辅助索引进行遍历,然后计数。因为它不需要读取实际数据行,只需要读取索引的叶子节点。如果没有任何辅助索引,会选择聚簇索引(全表扫描),效率最低。count(*) 的执行速度非常快,是 O(1) 操作。COUNT(*) 是官方推荐的统计行数方式。 MySQL 的优化器会对其进行优化,选择最高效的索引来计数,不关心列值。count(1)
1 只是一个常量,表示每找到一行就计数一次。count(*) 类似,对于 InnoDB,它也会选择一个最小的辅助索引(或者主键索引)进行遍历计数,因为它也不关心具体的列值。count(1) 比 count(*) 效率更高,但实际上,MySQL 优化器对 count(*) 有特殊优化,两者的性能几乎没有区别,甚至在某些版本 count(*) 可能略优。count(1) 和 count(*) 性能相同。count(字段名)
count(*) / count(1) 的主要区别在于对 NULL 值的处理。 count(字段名) 不会统计 NULL 值,而 count(*) 和 count(1) 会。count(字段名) 的效率会低于 count(*) 或 count(1),因为它需要去读取字段值来判断是否为 NULL,这可能导致更多的 IO 操作(回表),特别是当这个字段没有索引时。总结:
COUNT(*) 来统计行数。它最简洁,且 MySQL 优化器对其有最好的优化。COUNT(字段名)。但要注意性能影响。VARCHAR 和 CHAR 是 MySQL 中用于存储字符串的两种主要数据类型,它们在存储方式、空间占用、性能以及用途上都有显著区别。
CHAR(M) (定长字符串)
M 表示字符数,无论实际存储的字符串有多长,它都会占用 M 个字符的存储空间。不足 M 的字符会用空格填充到 M 长度。M * 字符集最大字节数 的空间。例如,CHAR(10) 使用 UTF8 字符集,将占用 10 * 3 = 30 字节(因为 UTF8 一个字符最大 3 字节),即使只存了一个字符。M,会在右侧填充空格直到 M 长度。CHAR(32))、国家代码(CHAR(2))、性别(CHAR(1))。CHAR 可能提供更好的性能。VARCHAR(M) (变长字符串)
M 表示字符数,它只存储实际需要的字符数,外加 1 或 2 个字节来记录字符串的实际长度。实际字符串长度 + 1 或 2 字节(取决于 M 的大小)。 M <= 255,需要 1 个字节记录长度。M > 255,需要 2 个字节记录长度。VARCHAR(100) 使用 UTF8 字符集,存储 'hello' (5 个字符) 只占用 5 * 3 + 1 = 16 字节。主要区别总结表格:
| 特性 | CHAR(M) | VARCHAR(M) |
|---|---|---|
| 存储方式 | 定长,M 个字符 | 变长,实际字符长度 + 1 或 2 字节用于记录长度 |
| 空间占用 | 固定占用,M * 字符集最大字节数 | 弹性占用,实际字符长度 * 字符集最大字节数 + (1或2) |
| 尾部空格 | 存储时填充,检索时自动去除(可能丢失) | 存储和检索都保留 |
| 最大长度 | M (字符数) | M (字符数) |
| 存储上限 | M 最大为 255 字符 | M 最大为 65535 字符,但受限于行最大长度 65535 字节 |
| 写入性能 | 相对快 | 相对慢(可能导致行迁移) |
| 读取性能 | 相对快 | 相对慢 |
| 适用场景 | 长度固定或接近固定,例如 MD5、身份证号 | 长度不固定,例如姓名、地址、文章标题 |
| 内部处理 | 不会产生碎片(若 M 不变) | 可能产生碎片,导致行迁移 |
选择建议:
VARCHAR:在大多数情况下,如果字符串长度不固定,或者你无法确定确切的固定长度,VARCHAR 是更好的选择,因为它能节省大量的存储空间。存储空间的节省通常比 CHAR 带来的微小性能提升更重要,尤其是在大表中。CHAR:CHAR(1) 表示性别,虽然 VARCHAR(1) 更节省空间,但其额外的长度字节可能导致实际占用空间一样。想象一下你去银行办业务,比如你要从 A 账户取钱,然后存到 B 账户。这个过程是不能分开的,必须“要么都成功,要么都失败”。如果取了钱却没存上,那钱就丢了!MySQL 事务就是为了保证这种“打包操作”的可靠性。它有四个“承诺”:
原子性(要么全做完,要么一点没做)
Undo Log 里。万一事务中间出错了(比如程序崩了),或者你想取消(回滚)这次操作,MySQL 就能找到 Undo Log 里的记录,把数据“还原”回原来的样子。就像你在玩游戏,一步走错了,能点“回退”回到上一步。持久性(一旦提交,就板上钉钉了)
Redo Log 里,这个日志写起来飞快。只要 Redo Log 写进硬盘了,MySQL 就敢告诉你:“搞定!你的钱已经存上了!”即使这时候电脑突然关机了,重启后 MySQL 也能通过 Redo Log,把之前没来得及写进数据文件的操作,“重新执行”一遍,保证你的数据不会丢。就像你写作业,先写草稿,草稿纸写完了就认为写完了,回头再慢慢誊到正式本上。隔离性(大家互不干扰,井水不犯河水)
一致性(数据总是合法的)
所以,MySQL 实现事务就像一个紧密配合的团队,Undo Log 负责“后悔”,Redo Log 负责“记下并重做”,锁和 MVCC 负责“隔离”,所有这些努力都是为了最终保证数据是“一致”的。
MVCC 全称是 Multi-Version Concurrency Control,翻译过来就是多版本并发控制。
大白话解释:
想象一下,数据库里有一行数据,比如“小明,10 岁”。 现在,小红想读小明的信息,同时小刚想把小明的年龄改成 11 岁。
如果他们都直接操作同一份数据,那小红读的时候,小明年龄可能突然从 10 变成 11,数据就乱了。
MVCC 的解决方案就像是:
Undo Log 里),然后新生成一个“小明,11 岁”的版本。Read View),告诉它:“你只能看这个时间点之前已经提交的数据版本。”这样,小红在读“小明,10 岁”的时候,小刚可以毫无阻碍地把小明改成“11 岁”,他们之间互不干扰,都不用互相等待(也就是“无锁地读取”)。
核心思想:
Undo Log 里,通过指针串联起来,形成一条“版本链”。好处:大大提高了数据库同时处理读写请求的能力,让数据库运行更流畅。
MySQL 就像一个非常严谨的“会计”,它会把数据库里发生的各种重要事情都仔仔细细地记录在不同的“账本”里,这些“账本”就是日志。主要的有三种:
binlog (二进制日志)
INSERT INTO ...,UPDATE ...),或者这些语句具体改了哪些行的数据。binlog 里,然后传给从数据库。从数据库照着 binlog 再做一遍,这样主从数据就一致了。binlog 里记录的操作一步步重放,就能把数据恢复到最新的状态。binlog 才会记录这些操作。redo log (重做日志)
redo log 先写到硬盘了,MySQL 就敢说事务“提交成功”了。万一这时候服务器突然断电,重启后,MySQL 会检查 redo log,把里面还没写到数据文件里的那些已提交的修改,“重做”一遍,确保数据是最终写入的。这就是保证事务“持久性”的秘密武器。undo log (回滚日志)
ROLLBACK),undo log 就发挥作用了。MySQL 会根据 undo log 里的记录,把数据恢复到事务开始前的样子。这就是保证事务“原子性”的秘密武器。undo log 里存储的历史数据版本,就能让读事务看到一个稳定的、不被其他写事务影响的数据快照。三者核心区别:
| 特性 | binlog (二进制日志) | redo log (重做日志) | undo log (回滚日志) |
|---|---|---|---|
| 关注点 | “发生了什么”(记录事件) | “怎么改的”(记录物理修改) | “怎么撤销”(记录旧数据/反操作) |
| 作用 | 跨库同步(主从)、完整恢复 | 保证持久性、崩溃恢复(未写数据文件) | 保证原子性(回滚)、实现 MVCC |
| 类型 | 逻辑日志(SQL 语句/行事件) | 物理日志(数据页修改) | 逻辑日志(旧值/反向操作) |
| 谁用它 | MySQL 服务器层(所有引擎) | InnoDB 存储引擎特有 | InnoDB 存储引擎特有 |
| 方向 | 向前恢复(从头到尾重放) | 向前重做(重启后重做已提交事务) | 向后回滚(撤销当前事务,提供历史版本) |
| 存储 | 通常无限追加,按文件大小或时间轮转 | 循环写入,写满后覆盖旧的 | 存储旧版本,事务提交后待 MVCC 不再引用才清除 |
事务隔离级别就像是你在图书馆看书时,希望不受旁边同学干扰的程度。干扰程度越低,隔离级别就越高,但同时可能效率也会降低一些。
MySQL(以及 SQL 标准)定义了四种事务隔离级别,从低到高分别是:
读未提交 (Read Uncommitted, RU)
读已提交 (Read Committed, RC)
可重复读 (Repeatable Read, RR)
串行化 (Serializable)
MySQL 默认的事务隔离级别是:可重复读 (Repeatable Read, RR)。
为什么选择这个级别?
MySQL 之所以选择 可重复读 (RR) 作为默认隔离级别,主要是因为它在数据一致性和并发性能之间找到了一个较好的平衡点。
避免了“脏读”和“不可重复读”:
脏读 是非常严重的问题,会导致数据逻辑混乱。RR 级别能够完全避免。不可重复读 也会让事务内部的数据不一致,导致程序逻辑错误。RR 级别通过 MVCC(多版本并发控制)完美解决了这个问题,保证了事务内多次读取同一行数据的结果是相同的。并发性能较好:
读已提交 (RC) 的隔离性更强,但 RR 级别在大部分读操作时,不需要加锁。它主要依靠 MVCC(多版本并发控制)来实现隔离,读操作和写操作可以并行进行,互不阻塞,大大提高了并发处理能力。串行化 (Serializable) 级别虽然最安全,但读操作也需要加锁,会导致大量的锁等待,并发性能极差,通常只在对数据一致性要求极高且并发不高的场景下使用。满足大多数业务需求:
RR 级别能很好地满足这种需求。虽然在理论上 RR 级别依然有“幻读”的问题(即插入新行导致的问题),但在 MySQL 的 InnoDB 存储引擎中,通过间隙锁(Gap Lock)的机制,也额外解决了“幻读”的问题。所以,在 InnoDB 引擎下,RR 级别几乎能完全避免脏读、不可重复读和幻读。总结:可重复读 提供了一个在保证足够强的数据一致性(避免脏读和不可重复读,并且在 InnoDB 下通过间隙锁解决了幻读)的同时,又能保持良好并发性能的折衷方案,因此成为 MySQL 的默认选择。
这些都是在并发事务处理时可能出现的数据不一致性问题。我们还是用银行账户的例子来说明:
假设有一个银行账户 A,初始余额是 1000 元。
脏读 (Dirty Read)
读未提交 (Read Uncommitted)不可重复读 (Non-Repeatable Read)
读未提交 (Read Uncommitted) 和 读已提交 (Read Committed)幻读 (Phantom Read)
WHERE age > 18)时,发现查询结果集的行数发生了变化(多了或少了行),这是因为另一个已提交事务在期间插入或删除了符合查询条件的新行。users 表,当前只有 id=1, id=2 的用户。id > 0 的用户,查到 2 条记录 (id=1, id=2)id=3,并提交事务id > 0 的用户,查到 3 条记录 (id=1, id=2, id=3)。读未提交 (Read Uncommitted)、读已提交 (Read Committed) 和 理论上的 可重复读 (Repeatable Read)。 可重复读 (RR) 级别允许幻读,但 MySQL 的 InnoDB 存储引擎通过“间隙锁(Gap Lock)”机制,在 可重复读 (RR) 隔离级别下额外解决了幻读问题,实现了更强的隔离性。所以,在 MySQL 的 InnoDB 中,默认的 RR 级别通常被认为能避免所有这三种问题。MySQL 中的锁就像是数据库为了管理并发访问,给大家分配的“通行证”或者“权限”。不同的锁类型,代表了不同的访问权限和限制程度。
按锁的粒度分:
全局锁 (Global Lock)
表级锁 (Table Lock)
ALTER TABLE)操作或特定 SQL(如 LOCK TABLES)时也会使用。行级锁 (Row Lock)
按锁的功能分(主要针对行级锁):
共享锁 (Shared Lock / S Lock)
SELECT ... LOCK IN SHARE MODE; 或 SELECT ... FOR SHARE; (MySQL 8.0)排他锁 (Exclusive Lock / X Lock)
UPDATE、DELETE、INSERT 语句会自动加排他锁。也可以手动加:SELECT ... FOR UPDATE;意向锁 (Intention Lock)
IS Lock (Intention Shared Lock):表示事务打算在某些行上加共享锁。IX Lock (Intention Exclusive Lock):表示事务打算在某些行上加排他锁。间隙锁 (Gap Lock) 和临键锁 (Next-Key Lock)
可重复读 (RR) 隔离级别下使用,主要目的是解决“幻读”问题。MySQL 的二阶段提交 (Two-Phase Commit, 2PC) 机制,主要发生在 InnoDB 存储引擎与 binlog(二进制日志)同时工作时,用于确保事务的原子性和持久性,尤其是在崩溃恢复场景下,保证数据一致性。
大白话解释:
想象你有个重要任务,需要同时完成两件事:
redo log 记录数据修改)。binlog 记录操作)。如果这两件事不能同时完成,比如你写完日记电脑就崩了,老板却以为你没写(binlog 没记录);或者你告诉老板完成了,但日记没写完电脑就崩了(redo log 没刷盘),数据就对不上了。
二阶段提交就是为了解决这个“日记和老板报告”的同步问题,确保它们要么都成功,要么都失败。
两个阶段:
第一阶段:准备阶段 (Prepare Phase)
redo log,并标记为 prepare 状态:当事务执行过程中,所有的修改都会先写入到 InnoDB 的 redo log 中。在事务提交前,MySQL 会强制把这些 redo log 刷盘(写入磁盘),并把这条 redo log 标记为“准备好提交”状态(prepare 状态)。第二阶段:提交阶段 (Commit Phase)
binlog:在 redo log 处于 prepare 状态后,MySQL 才会开始把这个事务对应的操作写入 binlog。一旦 binlog 写入成功并刷盘,就代表这个事务已经逻辑上提交了。redo log,并标记为 commit 状态:最后,MySQL 会告诉 InnoDB,这个事务在 binlog 层面已经搞定了,InnoDB 就可以安全地把 redo log 中对应的事务标记为“已提交”状态(commit 状态),并释放相关锁。binlog),老板确认收到后,你再在日记上盖个“已上报”的章。为什么这样设计?(崩溃恢复时的重要性)
redo log 未刷盘,或未标记 prepare):事务会回滚。因为 binlog 也还没写,相当于这个事务没发生过。binlog 刷盘前崩溃:数据库重启后,会发现 redo log 处于 prepare 状态,但 binlog 中没有这个事务的记录。此时 MySQL 知道这个事务还没有完全完成,会将其回滚。binlog 刷盘后,redo log 标记 commit 前崩溃:数据库重启后,会发现 binlog 中有这个事务的记录,而 redo log 处于 prepare 状态(或还未标记 commit)。此时 MySQL 知道 binlog 已经记录了,说明这个事务应该被提交,它会继续完成 redo log 的提交操作,确保数据最终一致。这个过程就像是协调两个独立的系统(InnoDB 存储引擎和 MySQL Server 的 binlog 模块)进行同步,确保在任何情况下,数据都保持一致。
死锁就像交通堵塞,两辆车都想往前开,但又互相挡住了对方的路,谁也动不了。在数据库中,死锁就是两个或更多事务互相等待对方释放资源,导致它们都无法继续执行。
大白话解释:
为什么会发生死锁?
通常是因为事务之间交叉持有和请求锁资源。比如:
这时,事务 A 等着 B 释放行 2,事务 B 等着 A 释放行 1,互相僵持,就死锁了。
怎么发现死锁?
MySQL 的 InnoDB 存储引擎有死锁检测机制。它会定期检查有没有事务形成这种循环等待的僵局。
发生死锁后如何解决?(MySQL 的自动处理)
当 InnoDB 检测到死锁后,它不会让所有事务一直等下去。它会选择一个“牺牲品”(通常是修改行数最少的事务,因为回滚的代价最小),强制回滚这个事务。
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction),然后它的所有修改都会被 undo log 撤销。所以,作为使用者,大部分时候你不需要手动去解决正在发生的死锁,MySQL 会自动检测并处理。
作为开发者,如何避免和优化死锁?
虽然 MySQL 会自动处理死锁,但频繁的死锁会降低数据库性能,因为它会导致事务回滚和重试。所以,我们应该尽量减少死锁的发生:
固定访问顺序:尽可能让所有事务以相同的顺序访问并锁定资源。比如,如果事务 A 先锁表 X 再锁表 Y,那事务 B 也应该这样做。
缩小事务范围:尽量让事务短小精悍,减少事务持有锁的时间。
批量操作时加锁:如果需要对多行数据进行批量操作(比如更新),可以考虑在事务开始时一次性锁定所有需要的行,或者使用 SELECT ... FOR UPDATE 在查询时就加好排他锁,而不是分批处理,避免中间被其他事务插队。
降低隔离级别(慎用):如果业务允许,可以将隔离级别从 可重复读 降到 读已提交。读已提交 隔离级别因为每次 SELECT 都会获取新数据,理论上会减少一些死锁的可能(但也引入了不可重复读的问题)。但通常不推荐为了死锁而降低隔离级别,因为这可能引入其他数据一致性问题。
为查询增加索引:没有索引的查询可能会导致行锁升级为表锁,或者导致全表扫描,从而增加死锁的概率。确保你的查询都走了合适的索引。
通过以上措施,可以有效地降低死锁发生的频率,提高数据库的稳定性和性能。
什么是深度分页?
想象一下你在网上购物,想看第 1000 页的商品。通常的分页查询可能是这样的:
SELECT * FROM products ORDER BY id LIMIT 1000000, 10;
(意思是跳过前 100 万条记录,然后取 10 条)
这个 LIMIT offset, count 的 offset(偏移量)如果特别大,比如几十万、几百万甚至上千万,就叫做“深度分页”。
深度分页有什么问题?
如何解决深度分页问题?
解决深度分页的核心思想就是:避免扫描大量无用数据。
基于上一页的最大 ID (或索引值) 进行优化 (推荐,最常用)
id_max_page1)。 SELECT * FROM products ORDER BY id LIMIT 10;SELECT * FROM products WHERE id > id_max_page1 ORDER BY id LIMIT 10;先分页获取 ID,再根据 ID 获取详情 (适用于复杂查询)
SELECT id FROM products ORDER BY id LIMIT 1000000, 10; (只查 ID,这一步也可能慢,但比查所有字段要快很多)SELECT * FROM products WHERE id IN (查到的这10个id); (根据 ID 精准查询,非常快)offset 之前的数据,只不过扫描的字段少了,IO 减少了。如果 offset 极大,第一步仍会慢。使用搜索引擎(如 Elasticsearch)或专门的 OLAP 数据库 (适合超大数据量)
总结:对于大多数深度分页问题,基于上一页最大 ID 的优化 是最常用和最有效的方案。
什么是主从同步?
想象你有两台电脑,一台是“主电脑”(主库),另一台是“从电脑”(从库)。你在主电脑上做的任何文件修改、新建等操作,都会自动、实时地同步到从电脑上,让两台电脑的文件保持一致。
MySQL 的主从同步(也叫复制,Replication)就是这个意思:一台 MySQL 数据库(主库)的写操作,会自动、实时地同步到另一台或多台 MySQL 数据库(从库)上,保持数据一致。
为什么需要主从同步?
它是如何实现的?
MySQL 主从同步主要依靠 binlog(二进制日志),其核心过程有三个角色:
binlog。binlog 来同步数据。binlog 发送给从库。binlog 内容,并把这些内容写入到从库自己的一个特殊文件,叫做 Relay Log(中继日志)。整个过程就像一个“快递”服务:
binlog。binlog 的内容发过去。binlog 的内容,然后把这些内容原封不动地抄写到从库自己的一个 Relay Log(中继日志)小本本上。什么是主从同步延迟?
理想情况下,主从数据应该是实时的,但现实中总会有一些滞后。主从同步延迟就是指:从库执行主库的 binlog 操作,比主库实际执行要慢的时间差。
为什么会出现延迟?
导致延迟的原因有很多,主要有:
INSERT, UPDATE, DELETE)太多太快,产生的 binlog 太多,从库来不及读取和重放。binlog,从库重放这个大事务时会耗费很长时间,导致其他小事务也被阻塞。如何处理(解决/减少)主从同步延迟?
解决延迟的思路就是:提高从库处理 binlog 的速度,或减少主库 binlog 的压力,或优化网络。
开启从库的并行复制(多线程复制):
slave_parallel_workers 参数,让多个 SQL Thread 并行重放 relay log。这是解决延迟最常见和最有效的方法之一。优化主库写入,减少大事务:
优化从库的硬件配置:
优化 SQL 语句,建立合理索引:
UPDATE 和 DELETE 语句,务必使用索引字段作为条件。读写分离做得更彻底:
使用半同步复制或无损复制(牺牲一定性能换取数据安全):
binlog 并写入 relay log 后才返回成功。这能保证 binlog 不会丢失,减少主从延迟的可能性(因为主库会等从库),但会稍微增加主库写入延迟。
评论