普通视图

Received before yesterday陶其的个人博客

面试笔记:MySQL 相关03 – SQL语法与查询优化

作者陶其
2025年11月20日 13:03

感谢订阅陶其的个人博客!

SQL语法与查询优化

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》


01. 子查询与连接(join)查询

子查询:是嵌套在主查询中的查询,按“返回结果类型”和“是否依赖主查询”分类,核心是 “子结果驱动主查询”;
连接查询:是多表关联,核心是 “通过关联条件合并表数据”,按“是否保留不匹配行”分类,底层依赖驱动表和被驱动表的关联逻辑。

以下给出了4种核心子查询和5种核心连接查询,以及一种内部优化查询。

01. 标量子查询

返回单个值。

  • 写法格式:子查询返回 1 行 1 列,可作为主查询的字段或条件值;
    -- 示例:查询订单数最多的用户名称(子查询返回最大订单数)
    SELECT username FROM user WHERE user_id = (SELECT MAX(user_id) FROM order WHERE status = 1);
  • 性能开销:极低,仅执行 1 次子查询,结果直接代入主查询;
  • 适用场景:主查询需依赖单个聚合值(如最大值、平均值)或特定单行数据。

02. 列子查询

返回单列多行。

  • 写法格式:子查询返回 1 列 N 行,主查询用IN/NOT IN/ANY/ALL匹配;
    -- 示例:查询有未支付订单的用户(子查询返回未支付订单的用户ID列表)
    SELECT username FROM user WHERE user_id IN (SELECT DISTINCT user_id FROM order WHERE status = 0);
  • 性能开销:中低,MySQL 5.7 + 会将其优化为 “半连接”(避免逐行匹配),子查询结果会被物化(存入临时表);
  • 适用场景:主查询需匹配多个候选值(如 “在某个集合中”),子查询结果集不宜过大(建议 1 万行内)。

03. 行子查询

返回多行多列。

  • 写法格式:子查询返回 N 行 N 列(通常 1-2 列),主查询用IN匹配行数据;
    -- 示例:查询与“用户张三的上海地址”完全匹配的用户(返回id和city两列)
    SELECT username FROM user WHERE (user_id, city) IN (SELECT user_id, city FROM user_addr WHERE username = '张三' AND city = '上海');
  • 性能开销:中,子查询结果集需按行匹配,建议结果集控制在 1 千行内;
  • 适用场景:主查询需匹配 “多字段组合条件”(如联合主键、多维度筛选)。

04. 关联子查询

依赖主查询字段。

  • 写法格式:子查询用主查询的字段作为条件(EXISTS是典型用法),需用表别名区分;
    -- 示例:查询有有效订单的用户(子查询依赖主查询的user_id,找到匹配即停止)
    SELECT username FROM user u WHERE EXISTS (SELECT 1 FROM order o WHERE o.user_id = u.user_id AND o.status = 1);
  • 性能开销:中高,主查询每扫描 1 行,子查询触发 1 次(类似 “嵌套循环”);若子查询关联字段无索引,会导致全表扫描(如 10 万行主表触发 10 万次子查询);
  • 适用场景:主查询需 “存在性校验”(如 “是否有相关数据”),优先用EXISTS(找到匹配行立即停止)替代IN(需遍历全量结果集)。

05. 内连接

INNER JOIN,默认连接方式。

  • 写法格式:仅保留两表中 “关联条件完全匹配” 的行(取交集),INNER可省略;
    -- 示例:查询用户及其有效订单(仅返回有订单的用户和匹配的订单)
    SELECT u.username, o.order_no FROM user u
    INNER JOIN order o ON u.user_id = o.user_id  -- 关联条件(必须写ON),INNER 可省略
    WHERE o.status = 1;
  • 性能开销:低,MySQL 会自动选择 “小表作为驱动表”,若关联字段有索引,被驱动表可快速匹配(避免全表扫描);
  • 适用场景:需获取多表 “交集数据”(如用户 + 关联订单、商品 + 关联分类),是业务中最常用的连接方式。

06. 左外连接

LEFT JOIN / LEFT OUTER JOIN

  • 写法格式:保留左表所有行,右表无匹配时用NULL填充,OUTER可省略;
    -- 示例:查询所有用户及其订单(无订单的用户也会显示,订单字段为NULL)
    SELECT u.username, o.order_no FROM user u
    LEFT JOIN order o ON u.user_id = o.user_id
    WHERE o.status = 1 OR o.order_no IS NULL;  -- 注意:过滤右表需包含NULL
  • 性能开销:中低,驱动表是左表(需全扫左表),右表关联字段有索引则性能接近内连接;无索引则性能下降明显;
  • 适用场景:需保留主表全量数据(如用户表),关联从表(如订单表)的可选数据(如 “统计所有用户的订单情况,包括无订单用户”)。

07. 右外连接

RIGHT JOIN / RIGHT OUTER JOIN

  • 写法格式:保留右表所有行,左表无匹配时用NULL填充,逻辑与左连接相反;
    -- 示例:查询所有订单及其所属用户(无匹配用户的订单也显示,用户字段为NULL)
    SELECT u.username, o.order_no FROM user u
    RIGHT JOIN order o ON u.user_id = o.user_id
    WHERE o.create_time > '2024-01-01';
  • 性能开销:与左连接一致,驱动表是右表(需全扫右表);
  • 适用场景:需保留从表全量数据(如订单表),关联主表(如用户表)的可选数据(如 “统计所有近 3 个月订单,包括已删除用户的订单”)。

08. 全外连接

FULL JOIN / FULL OUTER JOIN

  • 写法格式:保留左右两表所有行,无匹配时用NULL填充;MySQL 不直接支持FULL JOIN,需用UNION合并左连接和右连接;
    -- 示例:查询所有用户和所有订单的关联数据(保留双方无匹配的行)
    SELECT u.username, o.order_no FROM user u
    LEFT JOIN order o ON u.user_id = o.user_id
    UNION  -- 去重合并
    SELECT u.username, o.order_no FROM user u
    RIGHT JOIN order o ON u.user_id = o.user_id;
  • 性能开销:高,需执行两次连接 + 去重操作,数据量大时效率低;
  • 适用场景:极少用,仅需 “完整保留两表所有数据” 的特殊场景(数据对账、全量统计)。

09. 交叉连接

CROSS JOIN,笛卡尔积

  • 写法格式:无关联条件,两表数据完全组合(行数 = 左表行数 × 右表行数),CROSS JOIN可省略;
    -- 示例:用户表(3行)和商品表(2行)交叉连接,结果为6行
    SELECT u.username, p.product_name FROM user u
    CROSS JOIN product p;
  • 性能开销:极高,行数呈指数级增长(1 万行 ×1 万行 = 1 亿行),几乎不用;
  • 适用场景:仅用于 “全组合场景”(如生成所有用户 + 所有商品的测试数据),必须配合WHERE过滤(否则会撑爆内存)。

10. 半连接

半连接不是独立的查询语法,而是 MySQL 5.7 + 默认启用的对列子查询(如IN子查询)的内部优化手段,目的是减少子查询与主查询的匹配开销。

用于优化“主查询匹配子查询结果集”的场景(如IN/EXISTS列子查询)。

核心逻辑是 “只判断匹配与否,不返回子查询的完整数据”,避免逐行匹配的低效问题。

简单说:普通子查询是 “子查询返回所有结果→主查询逐行比对”,半连接是 “主查询与子查询表直接关联→找到匹配行就停止”,本质是将子查询转化为类似JOIN的关联逻辑,提升性能。

触发条件:子查询是“列子查询”(返回单列多行),且子查询表与主查询表无关联(非关联子查询),例如WHERE id IN (SELECT uid FROM t2 WHERE status=1)

写法格式:无特殊语法,仍用普通IN/EXISTS子查询写法,MySQL 会自动触发半连接优化(可通过EXPLAIN查看type列是否有SIMPLE/HASH JOIN,而非SUBQUERY)。

-- 示例:查询有未支付订单的用户(MySQL会自动用半连接优化)
SELECT username FROM user u WHERE u.user_id IN (SELECT DISTINCT o.user_id FROM order o WHERE o.status = 0);

性能开销

  • 低 – 中:远优于未优化的子查询(避免子查询结果集物化后逐行匹配)。
  • 优化逻辑:MySQL 会选择以下方式之一执行半连接:
    • 哈希半连接:对小表(子查询表)建哈希表,主表逐行匹配哈希键(适合大表)。
    • 嵌套循环半连接:小表驱动大表,找到匹配行立即停止扫描(适合小表)。
    • 物化半连接:子查询结果存入临时表并建索引,主表关联临时表(适合子查询结果集较小)。

适用场景

  • 主查询需判断 “字段是否在子查询结果集中”(如IN/EXISTS)。
  • 子查询表数据量适中(1 万行内最佳),且子查询有过滤条件(如WHERE status=1),能减少结果集大小。
  • 替代场景:若子查询结果集极大(10 万行 +),建议手动改写为JOIN(半连接优化效果会下降)。

注意项:

  • 半连接是 MySQL 自动优化,无需专门手动去写语法,但需知道 “IN子查询在 5.7 + 会被优化为半连接”,避免面试官问 “INJOIN哪个快” 时只说表面结论。
  • 实践中,若EXPLAIN显示子查询类型为SUBQUERY(未触发半连接),可通过 “子查询加DISTINCT” 或 “改写为JOIN” 强制优化(如上述示例加DISTINCT让结果集唯一,更易触发半连接)。

总结

1. 子查询 vs 连接查询:

  • 简单场景(如单值匹配、存在性校验)用子查询(写法简洁);
  • 复杂多表关联(如 3 表以上、需筛选多字段)用连接查询(性能更优,易优化);
  • 关联子查询尽量改写为JOIN(避免嵌套循环,如EXISTS适合小表,JOIN适合大表)。

2. 性能优化关键:

  • 连接查询:关联字段必须建索引(如order.user_id),小表驱动大表(减少循环次数);
  • 子查询:避免NOT INNULL 值会导致结果异常),用NOT EXISTS替代;
  • 外连接:右表过滤条件写在ON(关联时过滤),左表过滤条件写在WHERE(关联后过滤)。

3. Java 开发实践场景:

  • 列表查询(如用户列表 + 订单数):用LEFT JOIN + GROUP BY
  • 详情查询(如订单详情 + 用户信息):用INNER JOIN
  • 存在性校验(如判断用户是否有未支付订单):用EXISTS子查询。

02. 子查询与join性能对比

子查询与join性能对比及适用场景

关键结论:

  • 简单场景(单表校验、单值匹配)用子查询(简洁);
  • 多表关联、复杂聚合、主表数据量大时,优先用 JOIN(性能更稳定)。

03. 复杂查询的执行逻辑

1. group by/having

GROUP BYHAVING 是 MySQL 中用于数据分组聚合的核心语法,执行逻辑: “数据过滤→分组→聚合→二次过滤” 。

1. 核心执行逻辑

GROUP BY 的作用是 “按指定字段将数据分组”;
HAVING 则是 “对分组后的结果进行过滤”。

整体执行流程分四步,顺序不可颠倒:

1) 原始数据过滤(WHERE 子句);

  • 先通过 WHERE 筛选符合条件的原始数据(如 WHERE status = 1 过滤无效数据),减少后续处理的数据量。
  • 原理:WHERE 是 “分组前过滤”,仅保留满足条件的行,不涉及聚合操作,可利用索引快速筛选(如 status 字段有索引时,直接定位有效行)。

2) 分组操作(GROUP BY 子句);

  • GROUP BY 后的字段(如 GROUP BY user_id)将上一步过滤后的数据分组,相同值的行被归为一组。
  • 底层处理:
    • 若分组字段有索引,MySQL 会直接按索引顺序分组(无需额外排序,效率高);
    • 若无索引,会创建临时表存储分组数据,或对数据进行文件排序(Using temporaryUsing filesort,可通过 EXPLAIN 查看)。

3) 聚合计算(聚合函数);

  • 对每个分组执行聚合函数(如 COUNT(*) 统计行数、SUM(amount) 计算总和),生成每个分组的聚合结果。
  • 原理:聚合函数仅作用于分组内的数据,每个分组最终输出一行结果(包含分组字段和聚合值)。

4) 分组结果过滤(HAVING 子句);

  • HAVING 筛选聚合后的分组(如 HAVING COUNT(*) > 5 保留行数超 5 的组),最终返回符合条件的分组。
  • 原理:HAVING 是 “分组后过滤”,可直接使用聚合函数(因聚合结果已生成)。

2. 关键区别与实践注意

1) WHERE 与 HAVING 的核心区别

  • WHERE 作用于分组前的原始数据,不能使用聚合函数(如 WHERE COUNT(*) > 5 会报错);
  • HAVING 作用于分组后的结果,可以使用聚合函数(如 HAVING SUM(amount) > 1000 合法)。

2) 性能优化实践(重点)

  • 优先用 WHERE 过滤:尽量在分组前通过 WHERE 减少数据量(如 GROUP BY user_id HAVING user_id > 100 可改为 WHERE user_id > 100 GROUP BY user_id,减少分组计算量)。
  • 分组字段加索引:避免临时表和文件排序(如 GROUP BY create_time 时,给 create_time 建索引,EXPLAIN 中无 Using temporary 即为优化生效)。
  • 避免 SELECT 非分组字段:MySQL 5.7+ 默认开启 ONLY_FULL_GROUP_BY 模式,SELECT 后只能出现 GROUP BY 字段或聚合函数(如 SELECT user_id, username, COUNT(*)username 未在 GROUP BY 中,会报错,需规范写法)。

3. 典型场景举例

  • 统计高频用户:查询 “订单数超 10 单的用户及其总消费”,用 GROUP BY user_id HAVING COUNT(*) > 10
  • 按时间分组分析:查询 “每月订单金额超 10 万的月份”,用 GROUP BY month(create_time) HAVING SUM(amount) > 100000

2. limit 分页

LIMIT 分页是 MySQL 中用于从查询结果中截取指定范围数据的核心语法(如 LIMIT offset, size 取第 offset + 1offset + size 行),其执行逻辑围绕 “全量查询→排序→截取” 展开,实际使用中需重点关注性能问题。

1. 基本执行逻辑

LIMIT 本身不参与数据筛选,而是对 “查询 + 排序后” 的结果集进行截取,核心流程分三步:

1) 执行主查询获取原始数据

  • 先执行 WHEREJOIN 等条件筛选,得到所有符合条件的行(如 SELECT * FROM order WHERE status=1 筛选有效订单)。

2) 排序处理(若有 ORDER BY

  • 若包含 ORDER BY(分页几乎必带,否则结果无序),MySQL 会按指定字段排序:
    • 若排序字段有索引(如 ORDER BY create_timecreate_time 有索引),直接利用索引顺序获取有序数据(效率高);
    • 若无索引,需在内存或磁盘中进行 “文件排序”(Using filesort,可通过 EXPLAIN 查看),排序过程会消耗额外 CPU/IO。

3) 截取指定范围数据

  • 从排序后的完整结果集中,跳过前 offset 行,返回接下来的 size 行(如 LIMIT 20, 10 即跳过前 20 行,返回 10 行)。

2. 核心性能问题:offset 过大导致效率低下

offset 很大时(如 LIMIT 100000, 10),性能会急剧下降,原因是:

  • MySQL 必须先扫描并排序前 100010 行数据,然后丢弃前 100000 行,仅返回最后 10 行,大量计算被浪费;
  • 若排序无索引(依赖文件排序),会产生临时文件,进一步加剧性能损耗(10 万行数据排序可能耗时数百毫秒)。

3. 实践优化方案(重点,体现工程经验)

针对 offset 过大的问题,结合 Java 开发中常见的分页场景(如列表页、历史记录查询),优化手段如下:

1) 基于 “主键 / 唯一索引” 分页(最常用)

利用主键或唯一索引的有序性,通过 WHERE 条件直接定位起始位置,避免全量扫描:

-- 代替 LIMIT 100000, 10(低效)
SELECT * FROM order 
WHERE id > 100000  -- 上一页最后一条数据的id
ORDER BY id 
LIMIT 10;

原理:主键索引是有序的,id > 100000 可直接定位到起始行,无需扫描前 10 万行,性能提升 10 倍以上。

2) 避免 SELECT *,只查必要字段

减少数据传输量和内存占用,尤其大表(如包含 text 字段的表):

-- 只查需要的字段(如订单号、金额、时间)
SELECT order_no, amount, create_time FROM order 
WHERE id > 100000 
LIMIT 10;

3) 确保排序字段有索引

分页必须带 ORDER BY,且排序字段需建索引(如 create_time 索引),避免文件排序:

-- 给 create_time 建索引:CREATE INDEX idx_order_create_time ON order(create_time)
SELECT * FROM order 
WHERE status=1 
ORDER BY create_time DESC  -- 利用索引排序
LIMIT 20, 10;

4) 限制最大分页页数

业务上避免允许用户访问过大页数(如 “只显示前 100 页”),超过则提示 “数据过多,请缩小范围”(如电商平台常见做法),从源头减少大 offset 场景。

04. order by 的排序原理

MySQL 的ORDER BY用于对查询结果按指定字段排序,其核心原理是 “利用索引有序性直接取数” 或 “无索引时通过内存 / 磁盘排序”,性能差异主要源于是否能借助索引避免额外排序操作。

1. 核心排序原理

1. 利用索引排序

Using index,高效。

若排序字段(或多字段排序的前缀)有有序索引(如 B + 树索引,本身按字段值有序存储),MySQL 会直接沿索引顺序读取数据,无需额外排序,这是最优情况。

  • 原理:B + 树索引的叶子节点按索引字段值升序(或降序,取决于建索引时的指定)排列,ORDER BY字段与索引顺序一致时,可直接通过索引定位并返回有序数据,避免排序开销。
  • 示例:
    订单表order有索引idx_create_timecreate_time升序),执行SELECT id, create_time FROM order ORDER BY create_time时,MySQL 直接沿idx_create_time的叶子节点顺序取数,无需排序(EXPLAINExtra列显示Using index)。

2. 文件排序

Using filesort,低效。

若排序字段无索引,或排序顺序与索引顺序不一致(如索引是升序,ORDER BY用降序且无对应降序索引),MySQL 会触发 “文件排序”:先将数据加载到内存 / 临时文件,完成排序后再返回结果。

  • 执行步骤:

    1. 从表中读取符合WHERE条件的行(无索引时全表扫描);
    2. 将 “排序字段值 + 行指针(指向原始数据位置)” 存入sort_buffer(排序缓冲区);
    3. sort_buffer装不下所有数据,会将数据分块,先在内存中排序,再写入临时文件(磁盘);
    4. 最后合并所有分块的排序结果,得到全局有序的结果集,再通过行指针取原始数据返回。
  • 示例:
    订单表ordercreate_time无索引,执行SELECT * FROM order ORDER BY create_time时,MySQL 会先扫描全表,将create_time和行指针存入sort_buffer,排序后再取完整数据(EXPLAINExtra列显示Using filesort)。

2. 影响排序性能的关键因素

  1. 是否使用索引:索引排序性能远高于文件排序(毫秒级 vs 秒级,数据量大时差距更大)。
  2. sort_buffer大小:由sort_buffer_size参数控制(默认 256KB),若数据量超过该值,会触发磁盘临时文件排序(IO 开销剧增)。
  3. 查询字段多少SELECT *会导致sort_buffer中存储更多字段(增加内存占用),而只查必要字段可减少sort_buffer压力,加速排序。

3. 实践优化建议

1. 给排序字段建合适的索引:

  • 单字段排序:直接给排序字段建索引(如ORDER BY create_time → 建idx_create_time)。
  • 多字段排序(如ORDER BY a, b DESC):建联合索引(a, b DESC),索引顺序与排序顺序完全一致(避免索引失效)。

2. 避免SELECT *,只查必要字段:

  • 例如,列表页只需idnamecreate_time,则SELECT id, name, create_time ...,减少sort_buffer中存储的数据量,避免磁盘排序。

3. 控制排序数据量:
先用WHERE过滤无效数据(如WHERE status=1),减少进入排序阶段的行数(如从 100 万行滤到 1 万行,排序效率提升 100 倍)。

4. 大结果集排序用分页配合索引:
如 “按时间排序分页查询订单”,建(create_time, id)联合索引,用WHERE create_time > ?定位分页起点,避免全量排序。

4. 总结

ORDER BY的优化核心是 “能用上索引就避免文件排序”:索引排序依赖 B + 树的有序性,高效低耗;文件排序需内存 / 磁盘排序,性能差。实际开发中,通过 “建匹配索引 + 精简查询字段 + 提前过滤数据” 可显著提升排序性能,这也是处理订单列表、用户日志等需排序场景的常规优化手段。

05. 慢查询的定位与分析

MySQL 的慢查询(执行时间超过阈值的 SQL)是性能优化的核心切入点,定位与分析需结合日志工具、执行计划和业务场景。

1. 慢查询的定位

慢查询的定位:通过日志捕获目标 SQL。

定位慢查询的核心是开启慢查询日志,记录执行时间超标的 SQL,为后续分析提供依据。

1. 慢查询日志配置(5.7 版本)
通过修改my.cnf(或my.ini)配置,开启并定制日志规则:

slow_query_log = 1  # 开启慢查询日志(默认:0,关闭)
slow_query_log_file = /var/log/mysql/slow.log  # 日志文件路径(需MySQL有写入权限)
long_query_time = 1  # 慢查询阈值(单位秒,默认10秒,建议生产设1-2秒)
log_queries_not_using_indexes = 1  # 记录未使用索引的查询(即使未超阈值,可选)
log_output = FILE  # 日志输出方式(FILE/table,5.7默认:FILE)

配置后重启 MySQL 生效,也可通过SET GLOBAL动态开启(无需重启,适合临时排查):

SET GLOBAL slow_query_log = 1;
SET GLOBAL long_query_time = 1;

2. 日志收集工具

  • mysqldumpslow(自带工具):简单统计慢查询,适合快速定位高频问题:

    # 查看访问次数最多的10条慢查询
    mysqldumpslow -s c -t 10 /var/log/mysql/slow.log
    # 查看总执行时间最长的10条慢查询
    mysqldumpslow -s t -t 10 /var/log/mysql/slow.log

    优势:轻量,适合初步筛选;
    不足:无法分析趋势或复杂统计。

  • pt-query-digest(Percona 工具包):生产环境常用,可按执行时间、频率、用户等维度分析,输出 SQL 模板、平均耗时、扫描行数等关键信息:

    pt-query-digest /var/log/mysql/slow.log > slow_analysis.txt

    优势:能识别重复 SQL(如参数不同但结构相同的查询),定位 “隐形高频慢查询”(单条耗时 0.8 秒,但每秒执行 100 次,总耗时 80 秒)。

2. 慢查询的分析

慢查询的分析:通过执行计划定位根因。

捕获慢查询后,需用EXPLAIN分析其执行逻辑,重点关注是否全表扫描、是否用对索引、是否有额外排序 / 临时表。

1. 核心分析工具:EXPLAIN

对慢查询执行EXPLAIN,查看关键字段:

  • type:访问类型(性能从好到差:const > eq_ref > ref > range > ALL)。ALL表示全表扫描(需优先优化)。
  • key:实际使用的索引(NULL表示未用索引)。
  • rows:预估扫描行数(值越大,效率越低)。
  • Extra:额外信息(关键警告:Using filesort(需排序且无索引)、Using temporary(需创建临时表)、Using where; Using filesort(全表扫描后排序,性能极差))。

2. 常见慢查询原因及分析(结合EXPLAIN

  • 全表扫描(type=ALLkey=NULL
    • 原因:查询条件无索引(如:WHERE status=1status无索引),或索引失效(如:WHERE name LIKE '%abc'左模糊匹配导致索引失效)。
  • JOIN 关联无索引(rows极大)
    • 原因:被驱动表的关联字段无索引(如t1 JOIN t2 ON t2.uid = t1.idt2.uid无索引),导致被驱动表全表扫描。
  • 排序无索引(Extra=Using filesort
    • 原因:ORDER BY字段无索引,或排序顺序与索引不一致(如索引是create_time ASC,但查询用ORDER BY create_time DESC且无降序索引)。
  • 子查询嵌套(type=DEPENDENT SUBQUERY
    • 原因:关联子查询导致主表每行触发一次子查询(如WHERE EXISTS (SELECT 1 FROM t2 WHERE t2.uid = t1.id)),无索引时性能骤降。

3. 慢查询的优化方向

分析出原因后,针对性优化:

1. 加索引

  • 为查询条件、JOIN 关联字段、排序字段建索引(如:WHERE+ORDER BY字段建联合索引,如idx_status_create_time (status, create_time))。

2. 改写 SQL

  • 子查询改 JOIN(如关联子查询改写为INNER JOIN,减少嵌套循环);
  • 避免左模糊匹配(LIKE '%abc'改为LIKE 'abc%',或用全文索引);
  • 大分页改范围查询(LIMIT 100000,10改为WHERE id > 100000 LIMIT 10)。

3. 控制数据量

  • WHERE提前过滤无效数据(如WHERE create_time > '2024-01-01'),减少扫描和排序行数。

4. 8.0 版本的优化

  • 慢查询日志支持表存储:除文件外,可将日志写入mysql.slow_log表(log_output = TABLE),支持 SQL 查询分析(如按时间范围统计)。
  • 日志内容更丰富:新增query_id(唯一标识)、rows_affected等字段,方便关联性能模式(Performance Schema)数据。
  • 动态调整更灵活:long_query_time支持小数(如 0.5 秒),且修改后立即生效(无需重启连接)。

5. 总结

慢查询定位与分析的核心流程是:开启日志捕获 → 工具统计筛选 → EXPLAIN分析执行计划 → 针对性优化(索引 / 改写 SQL)

实践中,需结合业务场景(如订单表慢查询多与user_idcreate_time索引相关),优先解决 “高频 + 高耗时” 的慢查询。

06. 复杂SQL的拆分与改写

大表分页优化:延迟关联、书签分页。

MySQL 大表分页是业务中高频痛点(如订单列表、日志查询),普通LIMIT offset, sizeoffset较大时(如LIMIT 100000,10)性能极差:因 MySQL 需扫描前 10 万行再丢弃,仅取最后 10 行,IO 开销剧增。

复杂 SQL 拆分与改写的核心思路是减少无效扫描行数,常用优化手段包括延迟关联书签分页

1. 普通分页的性能瓶颈

普通分页 SQL 示例(订单表order,按create_time逆序):

SELECT * FROM `order` ORDER BY create_time DESC LIMIT 100000, 10;

问题:当offset=100000时,MySQL 需先遍历索引找到第 10 万行的位置(即使有create_time索引),再回表读取 10 行数据,前 10 万行的扫描属于无效开销,数据量越大越慢。

2. 关键优化方法

1. 延迟关联(延迟回表)

概念:先通过覆盖索引获取目标数据的主键(或唯一键),再关联原表获取完整字段,避免 “早期回表” 带来的大量 IO。

原理:利用索引的 “覆盖查询” 特性(只查主键,无需回表)快速定位目标行,再通过主键关联原表取数,减少扫描和回表的数据量。

优化后 SQL 示例:

-- 子查询用索引获取主键(覆盖索引,无需回表)
SELECT o.* 
FROM `order` o 
JOIN (SELECT id FROM `order` ORDER BY create_time DESC LIMIT 100000, 10) tmp 
ON o.id = tmp.id;
  • 优势:子查询仅扫描索引获取id(轻量),关联原表时仅取 10 行完整数据,性能比普通分页提升 5-10 倍(10 万 + offset 场景)。
  • 适用场景:需展示完整字段、支持任意跳页(如第 100 页、第 200 页)的列表。

2. 书签分页(游标分页 / 键集分页)

概念:用 “上一页最后一条记录的唯一标识”(书签,如idcreate_time+id)作为条件,替代offset直接定位下一页起点,完全避免无效扫描。

原理:利用主键 / 唯一索引的有序性,通过WHERE条件跳过前面数据,直接从书签位置取数。

优化后 SQL 示例:假设上一页最后一条记录的create_time='2024-01-01 10:00:00'id=100000,下一页查询:

SELECT * 
FROM `order` 
WHERE create_time < '2024-01-01 10:00:00' 
   OR (create_time = '2024-01-01 10:00:00' AND id < 100000) 
ORDER BY create_time DESC, id DESC 
LIMIT 10;
  • 关键:用create_time+id作为书签(create_time可能重复,需加id保证唯一性),通过索引直接定位,无offset开销。
  • 优势:性能极致(无论翻多少页,扫描行数固定为LIMIT size);
  • 局限:不支持 “跳页”(如直接到第 100 页),仅适合 “上一页 / 下一页”“加载更多” 场景(APP / 移动端常用)。

3. 覆盖索引分页

覆盖索引分页:若查询字段(如id, order_no, create_time)全部包含在索引中,直接用索引查询,无需回表:

-- 索引`idx_create_time_order_no (create_time DESC, order_no, id)`覆盖所有查询字段
SELECT id, order_no, create_time 
FROM `order` 
ORDER BY create_time DESC 
LIMIT 100000, 10;

4. 限制最大 Offset

业务上禁止offset超过阈值(如 10000),引导用户通过筛选条件(如 “按时间范围筛选”)缩小数据范围,避免大 offset 查询。

5. 分表分页

超大规模表(亿级)需水平分表(如按create_time分表),分页时先定位分表,再在分表内分页,避免跨表扫描。

3. 总结

大表分页优化的核心是 “用索引定位替代无效扫描”

  • 需支持跳页选延迟关联,通过覆盖索引减少回表;
  • 移动端 / 加载更多选书签分页,用唯一键直接定位;
  • 极致性能选覆盖索引分页,避免回表开销。

07. JOIN优化

小表驱动大表、 避免 cross join

1. JOIN 优化的核心逻辑

MySQL 中 JOIN 的底层实现以Nested Loop Join(嵌套循环连接)为主:外层循环遍历 “驱动表” 的数据,内层循环用驱动表的字段去 “被驱动表” 中匹配数据。。就像Java里的双层for循环。

优化的核心是减少外层循环次数 + 降低内层匹配成本,这也是 “小表驱动大表” 的原理基础。

2. 优化1:小表驱动大表

1. 原理

“小表” 指参与 JOIN 的数据集更小的表(而非物理表大小)。
用小表做驱动表(外层循环),大表做被驱动表(内层循环),可显著减少外层循环次数,从而降低整体 IO 和匹配开销。

  • 举例:小表有 100 行,大表有 100 万行
    • 小表驱动大表:外层循环 100 次,内层每次匹配大表 → 总匹配 100 次;
    • 大表驱动小表:外层循环 100 万次,内层每次匹配小表 → 总匹配 100 万次;
    • 性能差异一目了然。

2. 实践判断与示例

  • 如何判断 “小表”:
    • 通过EXPLAINrows字段(预估扫描行数),行数少的就是 “小表”;
    • 或业务逻辑中明确数据量更小的表(如字典表、配置表)。
  • SQL 示例(用户表user是小表,订单表order是大表):
-- 推荐:小表user驱动大表order(INNER JOIN中MySQL会自动优化,但LEFT JOIN需显式控制)
SELECT u.username, o.order_no 
FROM user u 
INNER JOIN `order` o ON u.id = o.user_id;

-- LEFT JOIN需注意:左表是驱动表,因此左表必须是小表
SELECT u.username, o.order_no 
FROM user u  -- 小表放左表(驱动表)
LEFT JOIN `order` o ON u.id = o.user_id;

3. 注意点

  • INNER JOIN:MySQL 优化器会自动选择小表作为驱动表,无需手动调整;
  • LEFT JOIN:驱动表固定为左表,因此必须将小表放在左表位置(否则会用大表驱动小表,性能暴跌);
  • RIGHT JOIN:驱动表固定为右表,需将小表放在右表位置(实际开发中建议用LEFT JOIN替代,更符合阅读习惯)。

3. 优化2:避免 CROSS JOIN(笛卡尔积)

1. CROSS JOIN 的危害

CROSS JOIN会返回两张表的笛卡尔积(行数 = 表 A 行数 × 表 B 行数),若表 A 有 1 万行、表 B 有 10 万行,结果会有 10 亿行,直接导致数据库 CPU/IO 打满、查询超时。

2. 常见触发场景

  • 显式使用CROSS JOIN关键字且无ON条件;
  • JOIN时遗漏ON关联条件(如SELECT * FROM A JOIN B);
  • WHERE条件无法过滤笛卡尔积(如关联字段值全为 NULL)。

3. 如何避免

  • 任何JOIN必须加ON关联条件(即使是INNER JOIN);
  • 禁止显式使用无关联条件的CROSS JOIN
  • 若需关联但无直接字段,通过业务逻辑补充关联条件(如时间范围、状态过滤)。

4. 优化补充

1. 被驱动表的关联字段必须加索引
被驱动表的ON字段(如order.user_id)需建索引,否则内层循环会全表扫描,即使小表驱动大表也会很慢。

2. 避免SELECT *
只查询需要的字段,减少数据传输量(尤其大表有大字段如TEXT时)。

3. 提前过滤数据
WHERE先过滤驱动表和被驱动表的无效数据(如WHERE o.status=1),减少参与 JOIN 的行数。

5. 总结

MySQL JOIN 优化的核心是:

  • 小表驱动大表:利用嵌套循环的特性减少外层循环次数,LEFT JOIN需手动控制驱动表;
  • 杜绝笛卡尔积:JOIN必须加ON条件,避免CROSS JOIN
  • 索引兜底:被驱动表的关联字段加索引,是 JOIN 性能的基础保障。

08. count的性能差异

count(1)、count(*)、count(字段)

1. count 的本质

count 的本质:统计 “非 NULL 行数”。

count()是聚合函数,核心作用是统计查询结果集中符合条件的 “非 NULL 行数”,不同参数的差异在于 “统计范围” 和 “是否需要判断 NULL”,这直接决定性能。

2. 三种 count 的区别与性能对比

1. count(*)

统计所有行数(含 NULL)。

逻辑:专门用于统计表 / 结果集的总行数。
MySQL 对其有特殊优化:会优先选择最小的非聚集索引(覆盖索引)扫描,若没有则用聚集索引,避免全表扫描。

特点:包含 NULL 值(因为不判断字段,只统计行数),性能最优。

2. count(1)

统计所有行数(占位符)。

逻辑:用常量1作为占位符,统计所有行数(无论字段是否为 NULL)。
MySQL 优化器会将其与count(*)视为等价,底层执行计划完全相同。

特点:性能与count(*)几乎无差异,属于 “语法糖”。

3. count(字段)

统计字段非 NULL 的行数。

逻辑:需逐行检查该字段是否为 NULL,仅统计非 NULL 的行数。性能分两种情况:

  • 字段是索引字段:用索引扫描(比全表快),但需判断 NULL
  • 字段是非索引字段:全表扫描 + 判断 NULL,性能最差。

特点:性能低于count(*)/count(1),且结果可能不等于总行数(因为字段可能为 NULL)。

3. 性能排序

从快到慢:count(*)count(1) > ount(索引字段) > count(非索引字段)

关键原因:

  • count(*)/count(1)无需判断字段 NULL,且 MySQL 会选最优索引;
  • count(字段)需额外判断 NULL,非索引字段还需全表扫描。

4. 实践建议

  • 统计总行数:优先用count(*)(语义最清晰,MySQL 优化最好),而非count(1)count(主键)
  • 统计字段非 NULL 行数:若需此逻辑,确保字段加索引(如count(user_name)user_name有索引);
  • 避免误区:
    • 认为count(主键)更快:实际count(*)会选更小的索引,比主键索引(聚集索引)扫描更快;
    • 认为count(1)count(*)快:MySQL 优化后两者无区别,count(*)更符合语义。

5. 总结

  • 核心差异:count(*)/count(1)统计总行数(含 NULL),性能最优;count(字段)统计非 NULL 行数,性能较差;
  • 实践选择:统计总数用count(*),统计非 NULL 字段用count(索引字段)
  • 优化关键:利用 MySQL 对count(*)的索引优化,避免count(非索引字段)的全表扫描。

6. 扩展

统计useremailnull的总行数,email是普通索引,是SELECT COUNT(email) FROM user快,还是SELECT COUNT(*) FROM user WHERE email IS NOT NULL快?

答:基本相同,但是不推荐使用后者,推荐使用COUNT(email)。因为这个全程是索引,虽然需要索引扫描,但是后者使用了WHERE条件,同样也是需要进行索引扫描,同时还多出了一步条件过滤。同时COUNT(email)语义更明确。

09. INJOIN哪个快

先明确两者的底层逻辑

  • IN:属于 “子查询 / 值列表匹配”,本质是将子查询结果加载到内存后做匹配(或直接匹配值列表),适合 “单字段匹配” 场景;
  • JOIN:属于 “多表关联”,底层以Nested Loop Join为主(小表驱动大表),通过关联字段索引直接匹配,适合 “多字段关联取值” 场景。

  • IN的优势:语法简单,适合 “单字段匹配” 的简单场景;
  • JOIN的优势:支持多字段关联,大结果集下利用索引更高效,是复杂关联的首选。

10. MySQL的函数

MySQL 函数按功能可分为内置函数(系统提供,重点)和自定义函数(用户编写)。

1. 核心内置函数

1. 字符串函数

处理文本数据。

用于字符串拼接、截取、替换等,是业务中最常用的函数类别。

  • CONCAT(str1, str2,...):字符串拼接(如:拼接用户姓名和手机号:CONCAT(username, '-', mobile));
  • SUBSTRING(str, pos, len):截取子串(如:取手机号后 4 位:SUBSTRING(mobile, 8, 4));
  • REPLACE(str, old, new):替换字符串(如:清理内容中的特殊字符:REPLACE(content, '<', ''));
  • LENGTH(str):获取字符串长度(如:校验用户名长度:WHERE LENGTH(username) > 6)。

实践:用户信息格式化、文本内容清洗场景高频使用。

2. 数值函数

处理数字运算。

用于数值计算、取整、进制转换等。

  • ROUND(num, n):四舍五入(如:金额保留 2 位小数:ROUND(amount, 2));
  • ABS(num):取绝对值(如:计算差值的绝对值:ABS(score1 - score2));
  • CEIL(num)/FLOOR(num):向上 / 向下取整(如:订单数量向上取整:CEIL(total/10));
  • SUM(num)/AVG(num):求和 / 平均值(报表统计核心函数)。

实践:金额计算、数据统计场景必备。

3. 日期时间函数

处理时间数据。

用于时间获取、格式化、差值计算,是业务中仅次于字符串函数的高频函数。

  • NOW()/CURDATE():获取当前时间(含时分秒)/ 当前日期(仅年月日);
  • DATE_FORMAT(date, fmt):时间格式化(如:订单时间转字符串:DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s'));
  • DATEDIFF(date1, date2):计算日期差值(如:用户注册天数:DATEDIFF(NOW(), register_time));
  • DATE_ADD(date, INTERVAL expr unit):时间增减(如:计算 7 天后的日期:DATE_ADD(NOW(), INTERVAL 7 DAY))。

实践:订单时间筛选、会员有效期计算、报表时间维度统计。

4. 聚合函数

数据统计分析。

用于对多行数据进行聚合计算,需结合GROUP BY使用。

  • COUNT(*):统计总行数(用户数、订单数统计);
  • SUM(field):求和(销售额、销量统计);
  • MAX(field)/MIN(field):最大值 / 最小值(最高订单金额、最早注册时间);
  • GROUP_CONCAT(field):分组拼接字符串(如:查询用户的所有订单号:GROUP_CONCAT(order_no))。

实践:报表系统、数据看板的核心函数。

5. 条件函数

逻辑判断。

用于实现 SQL 中的条件分支,替代复杂的WHERE判断。

  • IF(condition, val1, val2):简单条件判断(如:判断用户状态:IF(status=1, '正常', '禁用'));
  • CASE WHEN:复杂条件分支(如:订单状态映射:CASE status WHEN 0 THEN '待支付' WHEN 1 THEN '已支付' ELSE '已取消' END);
  • IFNULL(val, default)NULL 值替换(如:用户昵称为空时显示默认值:IFNULL(nickname, '游客'))。

实践:查询结果格式化、动态状态展示。

6. 其他常用函数

  • UUID():生成唯一标识符(分布式场景临时 ID 生成);
  • INSTR(str, substr):查找子串位置(如:判断内容是否含关键词:INSTR(content, 'MySQL') > 0);
  • CAST(val AS type):类型转换(如:字符串转数字:CAST(score AS UNSIGNED))。

2. 面试高频考点

函数使用的性能陷阱。

1. 函数操作索引字段会导致索引失效

这是面试必问点!若对索引字段使用函数,MySQL 无法使用索引,会触发全表扫描。

  • 反例:WHERE DATE(create_time) = '2024-01-01'(对索引字段create_timeDATE()函数,索引失效);
  • 正例:WHERE create_time >= '2024-01-01' AND create_time < '2024-01-02'(直接匹配字段范围,利用索引)。这个之所以能利用索引,是因为没有使用函数破坏索引的有序性,同时MySQL将给出的查询条件值'2024-01-01''2024-01-02'隐式的转换为了DATETIME类型,从而避免了将整个表的create_time字段转成其他类型,保证了原索引的可用性。

2. 聚合函数的NULL处理

  • COUNT(field)会忽略 NULL 值,COUNT(*)统计所有行(含 NULL);
  • SUM(field)NULL 值会被视为 0,但建议用IFNULL(field, 0)显式处理(避免语义歧义)。

3. 自定义函数的慎用场景

  • 自定义函数(CREATE FUNCTION)可实现复杂逻辑,但执行效率低(无法并行执行),且可能导致锁表,高并发场景建议用存储过程或应用层代码替代。

3. 总结

MySQL 函数是提升 SQL 灵活性的核心工具。

  • 分类记忆:重点掌握字符串、日期、聚合、条件函数的常用用法;
  • 性能陷阱:索引字段避免使用函数,否则索引失效;
  • 实践优先:简单逻辑用内置函数,复杂逻辑优先应用层处理(避免 SQL 臃肿)。

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关02 – 索引》

喜欢面试笔记:MySQL 相关03 – SQL语法与查询优化这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

面试笔记:MySQL 相关02 – 索引

作者陶其
2025年11月20日 12:53

感谢订阅陶其的个人博客!

索引

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》


01. 各种树的原理和特性

学习树,是为了便于学习索引。索引的核心作用是“加速查询”,而高效的树结构正是实现这一点的关键。

答:

1. “树”形象的理解

可以从“现实中的树”类比数据结构的“树”,它和路边的树长得很像,只是“倒过来”了。

想象一棵简化的苹果树:

  • 最底下的 “树根” 是起点,对应数据结构中树的根节点(只有一个);
  • 从树根往上长的 “树干” 会分杈出 “树枝”,这些树枝就是父节点;
  • 树枝再分杈出更细的枝桠,这些细枝桠就是子节点(一个父节点可以有多个子节点);
  • 最顶端的 “苹果”(没有再分杈的部分),就是叶子节点。
    数据结构里的 “树”,就是这样一种 “分层、有分支” 的结构:
  • 所有节点(根、父、子、叶子)都像 “果实” 一样,存储着数据(或索引);
  • 节点之间的 “连接”(比如根到父、父到子)像 “树枝”,表示数据之间的关系;
  • 整个结构是 “单向” 的:只能从根往下找子节点,不能从子节点反推回根(就像苹果不会自己长回树枝)。

举个更具体的例子:

  • 如果用树存 “班级学生名单”,根节点可以是 “三年级”;
  • 根节点的子节点(父节点)可以是 “三班”“五班”;
  • “三班” 的子节点可以是 “男生组”“女生组”;
  • “男生组” 的子节点(叶子节点)就是具体的学生:“小明”“小李”…

这种结构的核心好处是:查找效率高。
比如想找 “三年级三班男生组的小明”,你不用遍历所有学生,只需从根(三年级)→ 三班 → 男生组 → 小明,一步一步按 “分支” 找,比在一堆乱序的名单里翻快得多。

2. 二叉查找树

“不合适”的树(Binary Search Tree)

特性:每个节点最多 2 个子节点(左小右大),查询时从根节点开始,比当前节点小就走左子树,大就走右子树。

问题:容易 “失衡”。比如插入一串递增数据,会退化成链表(左子树为空,只有右子树),查询效率从 O (logn) 暴跌到 O (n)(和遍历链表一样慢)。

为什么 MySQL 不用:数据库索引需要稳定高效的查询,二叉树的失衡问题无法满足。

3. 平衡二叉树

又叫:AVL树/红黑树
解决 “失衡”,但仍有局限

特性:在二叉查找树基础上,通过旋转保持 “平衡”(左右子树高度差不超过 1),确保查询效率稳定在 O (logn)。

问题:还是 “二叉”(每个节点最多 2 个子节点),导致树的 “高度过高”。
比如存 100 万条数据,平衡二叉树的高度大概是 20 层(2^20≈100 万)。

为什么 MySQL 不用:索引数据存在磁盘上,每次查询需要从磁盘读数据(一次 IO 读一个 “页”,比如 4KB)。树高 20 就意味着最多要读 20 次磁盘,IO 成本太高。

4. B 树

多路平衡查找树,降低高度

特性:

  • “多路”:每个节点可以有多个子节点(比如 100 个),不再是二叉。
  • “平衡”:所有叶子节点在同一层,避免某条路径过长。
  • 节点存 “数据”:每个节点不仅存索引键,还直接存对应的数据(或数据地址)。

优势:高度大大降低。比如每个节点有 100 个子节点,存 100 万数据,树高只需 3 层(100^3=100 万),最多 3 次 IO 就能查到数据,比平衡二叉树高效得多。

为什么 MySQL 不完全用它:范围查询不方便。比如查 “id 从 100 到 1000”,B 树需要回溯父节点找下一个范围,效率低。

5. B + 树

MySQL 索引的 “标准答案”
B + 树是 B 树的变种,专门为数据库索引优化设计,核心特性完美适配索引需求:

非叶子节点只存索引,不存数据:

  • 非叶子节点仅保留索引键(比如 id),不存实际数据,这样一个节点能存更多索引键,子节点数量更多(比如 200 个),树高更低(100 万数据只需 2-3 层),IO 次数更少。

叶子节点存完整数据,且首尾相连:

  • 所有实际数据只存在叶子节点,且叶子节点之间用链表连接(形成有序链表)。

对索引的核心价值:

  • 单值查询快:和 B 树一样,通过索引键快速定位到叶子节点,IO 少。
  • 范围查询无敌:比如查 “id>100”,找到第一个 id=100 的叶子节点后,直接顺着链表往后扫,不用回溯父节点,效率极高(MySQL 中范围查询很频繁,这是关键)。

6. MySQL 为什么选 B + 树做索引?

  1. 树高低,IO 次数少(磁盘读写效率高);
  2. 叶子节点有序相连,范围查询(如 between、in)效率远超其他树;
  3. 非叶子节点只存索引,节点存储密度高,进一步降低树高。

02. 索引的作用

答:

  1. 加速查询,降低IO:索引通过有序结构(如B+树)将分散的数据按索引键排序,查询时无需全表扫描,而是通过索引快速定位目标数据所在的磁盘位置,大幅减少磁盘IO次数(比如从扫描全表100万行降到扫描几十行);

  2. 保证数据唯一性,强化业务约束:通过唯一索引(含主键索引),数据库会强制约束索引键的值不重复,直接实现 “业务唯一标识” 需求(如用户 ID、订单号不可重复),避免手动校验的繁琐和风险。

  3. 优化排序与分组操作:索引本身是有序的,当查询包含order by(排序)、group by(分组)时,可直接利用索引的有序性避免全表数据的额外排序(减少内存 / 磁盘临时表开销),提升这类操作的效率。

03. 常见索引类型及其特性

答:

1. 主键索引

Primary Key

特性:

  • 强制唯一性(表中唯一标识一行数据),且不允许NULL值;
  • 一张表只能有一个主键索引;
  • InnoDB 中主键索引是聚簇索引(叶子节点直接存储整行数据),无需回表,查询效率最高。

适用场景:作为表的唯一标识(如用户 ID、订单 ID),确保数据唯一性并加速行查询。

2. 唯一索引

Unique Index

特性:

  • 确保索引列的值唯一,但允许NULL值(且NULL可出现多次);
  • 一张表可创建多个唯一索引;
  • 基于 B + 树结构,非聚簇索引(叶子节点存主键值,需回表查数据);
  • 查询时找到一个匹配值就停止扫描(无需确认是否有重复),比普通索引少了 “继续校验” 的步骤,回表前的索引定位效率更高。

适用场景:需唯一约束但非主键的字段(如手机号、邮箱,允许未填写即NULL)。

3. 普通索引

Normal Index

特性:

  • 最基础的索引类型,无唯一性约束,允许重复值和NULL;
  • 基于 B + 树结构,非聚簇索引,仅用于加速查询,不影响数据本身的约束;
  • 查询时需扫描所有匹配的索引节点(可能有多个重复值),再批量回表取数据,比唯一索引多了 “扫描重复索引” 的开销。

适用场景:高频查询的非唯一字段(如商品分类、用户昵称),单纯提升查询效率。

4. 联合索引

Composite Index

特性:

  • 由多个字段组合创建(如(a, b, c)),遵循最左前缀原则(查询需包含最左字段才能命中索引);
  • 索引按第一个字段排序,第一个字段相同则按第二个,以此类推;
  • 可覆盖多字段查询,减少回表(如查询a, b时,若联合索引包含a, b,则无需回表);
  • 效率依赖查询条件:仅命中最左前缀且为覆盖索引时(无需回表),效率接近普通索引;若未命中全部前缀或需回表,效率会下降。
  • 整体比普通索引慢的核心:索引结构是多字段排序,定位时需匹配多个字段的有序性,逻辑比单字段索引复杂。

适用场景:多字段组合查询(如 “按用户 ID + 订单状态查询”),需合理设计字段顺序(区分度高的字段放左侧)。

5. 前缀索引

Prefix Index

特性:

  • 仅对字符串字段的前 N 个字符创建索引(如index(name(10))),大幅节省存储空间,但索引选择性低(重复率高);
  • 适用于长字符串(如 URL、长文本),但可能降低索引选择性(重复率升高),且无法用于order by/group by或覆盖索引。
  • 查询时需扫描更多索引节点才能定位目标,且无法使用覆盖索引(必须回表),额外增加 IO 开销。

适用场景:长字符串字段的模糊查询(如like ‘abc%’),平衡空间与查询效率。

6. 全文索引

Full-Text Index

特性:

  • 针对大文本内容(如文章、评论)的关键词搜索,针对大文本分词匹配(而非like的字符匹配,非 B + 树精确查找,需先分词、匹配倒排索引,再关联原表数据);
  • 效率远高于like ‘%关键词%’,但仅支持MATCH() AGAINST()语法,且有最小 / 最大词长限制。
  • 逻辑复杂度远高于其他索引,仅适合关键词检索,单值精确查询效率远低于 B + 树结构的索引。

适用场景:全文检索需求(如博客系统的文章关键词搜索)。

7. 索引效率排序

这些索引类型,按照查询速度和效率,从高到低排序

基于InnoDB引擎、但是精确查询场景:
查询效率从高到低排序:主键索引 > 唯一索引 > 普通索引 > 联合索引(命中最左前缀 + 覆盖索引)> 前缀索引 > 全文索引
但是并非绝对,依旧需要结合使用场景进行判断。
排序前提:默认是「单值精确查询」,若为「范围查询」,主键索引和唯一索引的差距会缩小(范围查询需扫描多个节点)。

04. 聚簇索引与非聚簇索引

答:
聚簇索引与非聚簇索引的核心区别是:数据存储位置
聚簇索引,又叫做聚集索引,它的叶子节点存储整行数据;
非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,需通过主键回表查询完整数据(覆盖索引除外)。

05. 最左前缀原则

答:
联合索引的最左前缀原则:
查询条件必须从联合索引的最左字段开始匹配,且连续匹配,才能用到索引的对应部分。

举个例子:联合索引(name, age, score)

  • 能命中索引的情况(从左到右连续匹配):

    • where name = '张三'(只用name,命中索引的name部分);
    • where name = '张三' and age = 20(用name+age,命中索引的name+age部分);
    • where name = '张三' and age = 20 and score > 90(用name+age+score,命中索引的全部三部分)。
  • 不能命中索引的情况(跳过左字段或不连续):

    • where age = 20(跳过最左的name,完全无法命中);
    • where name = '张三' and score > 90(跳过中间的age,只能命中name部分,score无法利用索引)。

联合索引含范围查询或模糊查询能否命中索引?
对于范围查询(大于、小于等)会导致“当前字段右侧的索引字段失效”,但左侧已匹配的字段仍能命中,(当前字段也能命中);
对于模糊查询,如果是前缀模糊(例如:"快乐大本营",like '快乐%'),能命中当前字段及左侧的索引,但当前字段右侧的索引失效;如果是后缀/全模糊查询(例如:%大本营%乐大%),会导致当前字段及右侧字段的索引都失效,仅当前字段左侧已匹配索引有效。

06. 索引的维护成本

答:
MySQL索引的维护成本本质是“空间换时间”的代价,主要集中在“写入性能损耗”、“存储空间占用”、“索引碎片维护”。索引数量越多,结构越复杂,则维护成本越高。

1. 写入操作(插入/更新/删除)的性能损耗

  • 写入数据时,不仅要修改表数据,还需同步维护索引的 B + 树结构。
  • 插入:可能触发 B + 树节点分裂(如节点满时),需重新组织索引排序;若为唯一索引,还需额外校验唯一性(扫描索引确认无重复)。
  • 更新:若更新的是索引列,需先删除旧索引条目,再插入新索引条目,相当于两次索引操作;非索引列更新不影响索引,但仍需维护聚簇索引的物理顺序(InnoDB)。
  • 删除:不会立即释放索引空间,仅标记为 “删除”,后续需通过碎片整理回收,且可能触发 B + 树节点合并(如节点数据过少时)。

2. 存储空间的额外占用

  • 索引需独立存储 B + 树结构,一张表的索引越多、字段越长,占用的磁盘空间越大。
  • 示例:大表的联合索引、长字符串的前缀索引,可能占用与数据本身相当的存储空间;聚簇索引虽无需额外存数据,但非聚簇索引(如唯一、普通索引)需存储主键值,叠加后空间开销显著。

3. 索引碎片与定期维护开销

  • 频繁插入 / 删除后,B + 树会产生 “空洞”(已删除但未释放的空间),导致索引碎片增多。
  • 碎片会降低查询效率(磁盘 IO 增多),需定期执行维护操作(如OPTIMIZE TABLE、重建索引),这些操作会锁表或占用大量 IO 资源,影响业务高峰期性能。

4. 优化器的决策负担

  • 表中索引过多时,MySQL 优化器需遍历所有可能的索引组合,评估最优查询计划,导致查询解析时间变长(尤其复杂查询场景)。

5. 如何优化维护策略

  • 不同索引的维护成本差异:联合索引 > 普通索引 > 唯一索引 > 主键索引(结构越复杂,维护时 B + 树调整逻辑越繁琐);前缀索引、全文索引的维护成本高于单字段索引(前者需处理部分字符,后者需维护分词倒排索引)。
  • 平衡维护成本的核心原则:避免 “过度索引”,仅为高频查询字段创建索引;优先选择窄索引(短字段、少字段组合),减少存储空间和维护开销;定期监控索引使用率,删除无效索引(如长期未被使用的索引)。

07. 回表查询与索引覆盖

答:
回表查询是 “非聚簇索引查询后需二次查聚簇索引拿完整数据” 的过程;
索引覆盖是 “查询字段(即所需的返回字段)全在索引中,无需二次查询” 的优化场景。
二者是 “需回表” 与 “免回表” 的对立关系,直接影响查询效率。

1. 回表查询(Bookmark Lookup)

1. 定义
当使用非聚簇索引(如唯一索引、普通索引、联合索引)查询时,若查询字段未完全包含在该索引中,需先通过非聚簇索引找到 “索引键 + 主键值”,再用主键值查询聚簇索引(主键索引) ,才能获取整行完整数据,这个 “二次查询” 的过程就是回表。

2. 核心逻辑(结合 InnoDB 引擎)

  • 非聚簇索引的叶子节点仅存储 “索引键 + 主键值”,不存完整行数据。
  • 若查询需要非索引字段(如用唯一索引email查user表的主键id和非索引字段name),必须通过主键值回聚簇索引 “兜底”,才能拿到name。

3. 示例

-- 表结构:id(主键)、email(唯一索引)、name(非索引字段)
select id, name from user where email = 'a@test.com';

第一步:通过唯一索引email(非聚簇)找到主键id=100
第二步:用id=100查询聚簇索引,拿到name字段,完成查询;
这两步共同构成回表查询,额外增加了一次聚簇索引查询的 IO 开销。

2. 索引覆盖(Covering Index)

1. 定义
当查询的所有字段(包括筛选条件、返回字段)都包含在某一个索引中时,则无需回表,仅通过该索引就能获取所有需要的数据,这个索引就是 “覆盖索引”,也叫紧凑索引,对应的查询就是覆盖索引查询

2. 核心优势
避免回表,减少一次磁盘 IO(聚簇索引查询),大幅提升效率;
索引数据量远小于全表数据,查询时扫描的数据量更少。

3. 示例

-- 联合索引:idx_email_name(email, name)(包含email和name字段)
select email, name from user where email = 'a@test.com';

查询的筛选字段email、返回字段name均在联合索引中;
直接通过该联合索引就能拿到所有需要的数据(返回字段),无需回聚簇索引,实现 “一次查询完成”。

3. 补充项

触发条件对比:

  • 回表:非聚簇索引 + 查询字段超出索引范围;
  • 索引覆盖:查询字段(筛选 + 返回)完全匹配某一索引(单字段索引或联合索引)。

实战价值:
索引覆盖是优化回表开销的核心手段,设计索引时可将高频查询字段加入联合索引(如idx_userid_status(user_id, status)),避免回表;
聚簇索引查询天然支持 “索引覆盖”(叶子节点存完整数据),无需回表,这也是其查询效率最高的原因之一。

易混点:
索引覆盖的关键是 “字段全包含”,与索引类型无关(单字段索引、联合索引均可作为覆盖索引);
前缀索引无法实现索引覆盖(仅存字段前 N 个字符,无法返回完整字段值)。

4. 不能触发索引覆盖示例

SELECT id, name, email FROM user WHERE email = 'xxx@qq.com';
-- 在user表中,id是主键,email是唯一索引,name是普通索引

结论:不能触发索引覆盖。
索引覆盖的核心要求是查询的所有字段(筛选条件 + 返回字段)必须完全包含在同一个索引中,跨索引无法实现覆盖。

如何修改才能触发索引覆盖?

  • 创建联合唯一索引 unique index idx_email_name(email, name)
  • 筛选字段email、返回字段emailnameid(主键会自动包含在非聚簇索引中),全部包含在这个联合索引里;
  • 查询时无需回表,直接通过该联合索引就能获取所有需要的数据,触发索引覆盖。

08. 如何合理使用索引?

以业务查询为导向,平衡查询效率与维护成本,避免 “过度索引” 或 “无效索引”。

  1. 优先为 “高频查询字段” 建索引,低频查询不建;
  2. 联合索引需遵循 “最左前缀 + 区分度优先(区分度高,重复值少)” 原则;
  3. 避免索引失效场景,确保索引被正确使用;
  4. 利用 “索引覆盖” 减少回表,提升效率;
  5. 根据字段特性选择合适的索引类型;
  6. 定期维护索引,清理无效索引。

合理使用索引的核心是 “按需设计、避免失效、注重维护”。
“索引不是越多越好,而是越合适越好”。

09. 索引失效场景

MySQL 索引失效的本质是:查询条件破坏了索引的有序性或匹配规则;
常见场景集中在:“字段操作”、“类型不匹配”、“查询语法不当”三类;
避免核心是:让查询条件贴合索引设计规则。

以下是常见索引失效场景 + 避免方法:

1. 对索引字段做函数 / 运算操作

失效场景:查询时对索引字段用函数(如substrdate_format)或数学运算,会导致 MySQL 无法利用索引的有序性,只能全表扫描。

  • 示例:where substr(name, 1, 3) = '张三'(name是普通索引)、where age + 1 = 20(age是普通索引)。

避免方法:将函数 / 运算移到等号右侧,或提前计算结果。

  • 优化后:where name like '张三%'where age = 19

2. 隐式类型转换

失效场景:查询条件中字段类型与传入值类型不匹配,MySQL 会自动做类型转换,导致索引失效。

  • 示例:where phone = '13800138000'(phone是int类型,字符串转数字)、where id = '100'(id是int,字符串转数字)。
    避免方法:确保查询值类型与字段类型完全一致。
  • 优化后:where phone = 13800138000where id = 100

3. 模糊查询(非前缀匹配)

失效场景:like的模糊查询若以%开头(后缀模糊 / 全模糊),会破坏索引有序性,导致索引失效;前缀模糊(%在末尾)可正常使用索引。

  • 示例:where name like '%张三'(后缀模糊,失效)、where name like '%张三%'(全模糊,失效)。
    避免方法:优先用前缀模糊查询;若需全模糊,改用全文索引(如fulltext index)或应用层分词。
  • 优化后:where name like '张三%'(前缀模糊)、MATCH(name) AGAINST('张三')(全文索引)。

4. 联合索引不满足最左前缀原则

失效场景:联合索引(如(a, b, c))需从左到右连续匹配,跳过左侧字段或不连续匹配,会导致索引失效或部分失效。

  • 示例:where b = 2 and c = 3(跳过最左a,全失效)、where a = 1 and c = 3(跳过中间b,仅a部分生效,c失效)。
    避免方法:查询条件需包含联合索引的最左字段,且按索引字段顺序匹配;若高频查询b + c,可单独建联合索引(b, c)
  • 优化后:where a = 1 and b = 2 and c = 3(全生效)、where b = 2 and c = 3(改用(b, c)联合索引)。

5. 范围查询后字段失效

失效场景:联合索引中,某字段用><>=<=between做范围查询后,其右侧的索引字段会失效。

  • 示例:where a = 1 and b > 2 and c = 3(联合索引(a, b, c)c失效)。
    避免方法:将范围查询字段放在联合索引的最右侧;若需多字段范围查询,改用覆盖索引或拆分查询。
  • 优化后:where a = 1 and b > 2(仅用a + b索引)、建联合索引(a, b, c)并确保查询字段覆盖(select a, b, c,避免回表)。

6. or连接非索引字段

失效场景:or连接的查询条件中,若有一个字段无索引,会导致整个查询无法使用索引(MySQL 会选择全表扫描)。

  • 示例:where name = '张三' or address = '北京'name有索引,address无索引,全失效)。
    避免方法:确保or连接的所有字段都有索引;或改用union all拆分查询(需字段一致)。
  • 优化后:where name = '张三' union all where address = '北京'address补建索引)。

7. is not null/not in/not exists

失效场景:对索引字段用is not null(部分场景失效)、not innot exists,会破坏索引的匹配逻辑,导致失效(is null通常可使用索引)。

  • 示例:where name is not nullname是普通索引,可能失效)、where id not in (1,2,3)id是主键索引,大数据量下失效)。
    避免方法:is not null改用union拼接非空结果;not in改用left join ... on ... is null;小数据量not in可接受,大数据量必优化。
  • 优化后:where id in (select id from user) union ...is not null替代)、select * from user u left join tmp t on u.id = t.id where t.id is nullnot in替代)。

8. 索引选择性差(失效等价场景)

失效场景:索引字段重复率极高(如 “性别” 字段,仅男 / 女),MySQL 优化器会判断 “全表扫描比索引查询更快”,主动放弃使用索引。

  • 示例:where gender = '男'gender建了索引,但全表 80% 是男性,索引失效)。
    避免方法:不针对低选择性字段建单字段索引;若需查询,将其作为联合索引的右侧字段(如(age, gender)),通过高选择性字段先过滤。

9. 补充

  1. 验证索引是否失效的核心方法:explain分析执行计划,若key字段为NULL或非目标索引,说明索引失效。
  2. 特殊情况:Innodb的聚簇索引(主键)即使有上述场景(如is not null),通常也不会完全失效,因聚簇索引的物理存储特性,优化器更倾向使用。
  3. 避免失效的核心原则:不破坏索引的有序性、不改变字段的原始形态、让查询条件贴合索引设计。

10. explain执行计划分析

explain 是 MySQL 分析 SQL 执行计划的核心工具。
通过输出 12 个字段(面试重点关注 7 个核心字段),可判断索引是否生效、查询是否全表扫描、是否存在文件排序 / 临时表等性能问题。

1. explain 核心作用

  1. 判断 SQL 是否使用了目标索引(避免索引失效);
  2. 识别全表扫描、文件排序、临时表等低效操作;
  3. 分析表的连接顺序、查询类型(简单 / 复杂查询);
  4. 预估查询扫描的行数,评估查询效率。

2. 7 个核心字段解析

1. type:访问类型

核心,判断查询效率的关键

含义:表示 MySQL 如何访问表中的数据(即查询方式),取值决定查询效率,从优到差排序:
system > const > eq_ref > ref > range > index > ALL

关键取值解读:

  • system:表中只有 1 行数据(如系统表),效率最高(罕见);
  • const:通过主键 / 唯一索引查询,匹配 1 行数据(如where id=100),高效;
  • eq_ref:多表连接时,被连接表通过主键 / 唯一索引匹配,每行只返回 1 行(如join on 主键);
  • ref:通过普通索引 / 联合索引前缀匹配,返回多行匹配数据(如where name=’张三’,name 是普通索引);
  • range:范围查询(><betweenin),只扫描索引的某一范围(比ref差,但比全表扫描好);
  • index:扫描整个索引树(索引全扫描),比ALL好(索引数据量小于全表);
  • ALL:全表扫描(最差),需避免(通常是索引失效或未建索引)。

判断标准:type至少要达到range级别,最优是refconst,出现ALL说明存在性能问题。

2. key:实际使用的索引

含义:表示 MySQL 实际选择的索引(若为NULL,说明未使用任何索引,索引失效或无合适索引);
判断标准:若key不是你设计的目标索引(如预期用idx_email,但keyNULL),说明索引失效,需排查原因(结合索引失效场景)。

3. rows:预估扫描行数

含义:MySQL 优化器预估的、查询需要扫描的行数(非精确值,但可反映效率);
判断标准:行数越少越好,若rows远大于表中实际数据量,可能是统计信息过时(需执行analyze table更新),或索引设计不合理。

4. extra:额外执行信息

核心!暴露性能隐患

含义:记录 SQL 执行的额外操作,重点关注 “好的标识” 和 “坏的标识”:

  • 优质标识:
    • Using index:触发覆盖索引,无需回表(高效,面试加分点);
    • Using index condition:索引下推(ICP),减少回表次数(高效)。
  • 性能隐患标识(必须避免):
    • Using filesort:需在内存 / 磁盘中排序(未利用索引有序性,如order by字段无索引);
    • Using temporary:创建临时表存储中间结果(如group by无索引、多表连接无合适索引,性能极差);
    • Using where:全表扫描后过滤数据(type=ALL时出现,说明无索引可用);
    • Using join buffer:多表连接时未用索引,使用连接缓冲区(低效)。

5. id:查询执行顺序

含义:表示查询中每个select子句的执行顺序(数字越大越先执行,相同数字按从上到下顺序);
应用场景:复杂查询(子查询、join)中,判断表的连接顺序是否合理(如小表驱动大表)。

6. select_type:查询类型

含义:区分简单查询和复杂查询,面试高频取值:

  • SIMPLE:简单查询(无子查询、无 union);
  • SUBQUERY:子查询(select中嵌套select);
  • DERIVED:派生表(from中嵌套select);
  • UNIONunion连接的第二个及以后的查询。

面试价值:说明查询的复杂程度,复杂查询(如多层子查询)可能导致优化器选择低效执行计划,需考虑拆分 SQL。

7. table:当前查询的表

含义:显示 SQL 查询的表名(或别名),多表连接时按id顺序显示表的执行顺序。

加项

  1. 如何判断索引是否生效:
    看key字段是否为目标索引(非NULL),同时type不是ALL/index

  2. 如何判断查询是否高效:
    typerange + keyNULL + rows值小 + extrafilesort/temporary

  3. 常见问题排查:

    • type=ALL + key=NULL:索引失效或未建索引,排查索引失效场景;
    • extrafilesortorder by/group by字段未建索引,需添加索引;
    • extratemporarygroup by无索引或多表连接无合适索引,优化索引设计。
  4. explain extended:在explain基础上增加filtered字段(过滤行数占比),filtered越高说明过滤效果越好;

  5. explain format=json:输出 JSON 格式的详细执行计划,适合复杂查询分析;

  6. 执行计划是 “预估” 而非 “实际”:优化器可能因统计信息过时、索引选择性差等误判,需结合实际执行耗时验证。

11. 升序索引与降序索引

在 MySQL 中,索引的升序(ASC)和降序(DESC)是通过创建索引时指定排序方向来设置的,核心目的是匹配查询中ORDER BY的排序需求,避免额外的文件排序(Using filesort)或反向扫描带来的额外开销。

ORDER BY排序时,如果排序字段存在索引,但是索引的顺序与排序的顺序不一致,则排序时导致优化器选择“反向扫描索引”,性能虽然比文件排序快非常多,但是依旧不如排序和索引同向的效果好。

比如:create_time字段存在索引idx_create_time,索引是升序(ASC)的,但是查询排序时,需要逆序排序(比如ORDER BY create_time DESC),那么优化器选择“反向扫描索引”。

1. 升序 / 降序索引的创建语法

MySQL 中创建索引时,默认是升序(ASC),可显式指定ASC(升序)或DESC(降序)。语法如下:

-- 升序索引(默认,可省略ASC)
CREATE INDEX idx_create_time_asc ON 表名(create_time ASC);

-- 降序索引(显式指定DESC)
CREATE INDEX idx_create_time_desc ON 表名(create_time DESC);

升序索引(ASC:索引中字段值按从小到大排序,适用于ORDER BY 字段 ASC的查询。
降序索引(DESC:索引中字段值按从大到小排序,适用于ORDER BY 字段 DESC的查询(如 “按创建时间逆序排序的列表”)。

这里面还存在着版本差异:

2. 5.7版本

5.7版本的限制与实践。

5.7 版本对降序索引的支持是 “语法层面” 的,实际存储仍为升序,查询时若用ORDER BY 字段 DESC,优化器会 “反向扫描升序索引”(而非直接使用降序索引),虽然比全表扫描 + 文件排序高效,但仍有一定开销。

场景:查询列表经常按create_time逆序排序(ORDER BY create_time DESC
在 5.7 中,即使创建DESC索引,实际效果与ASC索引差异不大,但仍建议显式创建DESC索引(为 8.0 升级兼容,且优化器可能优先选择):

-- 5.7中创建“伪降序索引”(语法支持,实际升序存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);

-- 查询时,优化器会反向扫描该索引,避免文件排序
SELECT id, order_no FROM order 
WHERE status = 1 
ORDER BY create_time DESC 
LIMIT 10;

5.7 的替代方案(核心优化)

ORDER BY create_time DESC还需配合WHERE条件(如status=1),建议创建联合索引,将过滤字段放前面,排序字段放后面(显式指定DESC):

-- 联合索引:先过滤(status),再排序(create_time DESC)
CREATE INDEX idx_status_create_time_desc ON order(status, create_time DESC);

-- 该索引可同时优化WHERE和ORDER BY,避免全表扫描和文件排序
SELECT id, order_no FROM order 
WHERE status = 1 
ORDER BY create_time DESC 
LIMIT 10;

3. 8.0 版本

8.0 版本的关键优化:真正支持降序索引。

8.0 版本开始,DESC索引会真正按降序存储,查询时若ORDER BY 字段 DESC,可直接匹配降序索引,无需反向扫描,性能比 5.7 提升明显(尤其大数据量排序场景)。

场景:同样按create_time逆序排序

在 8.0 中,创建DESC索引后,ORDER BY create_time DESC会直接使用该索引,EXPLAINExtra列无Using filesort

-- 8.0中创建“真降序索引”(实际按create_time从大到小存储)
CREATE INDEX idx_create_time_desc ON order(create_time DESC);

-- 查询时直接匹配索引,无需排序
SELECT id, order_no FROM order 
ORDER BY create_time DESC 
LIMIT 10;

联合索引的降序支持(8.0 增强)
8.0 允许联合索引中部分字段指定DESC,更精准匹配复杂排序场景。

例如 “按status升序、create_time降序排序”:

-- 联合索引:status升序,create_time降序
CREATE INDEX idx_status_asc_create_time_desc ON order(status ASC, create_time DESC);

-- 完全匹配ORDER BY,无文件排序
SELECT id, order_no FROM order 
ORDER BY status ASC, create_time DESC 
LIMIT 10;

4. 实践建议

1. 优先匹配查询的排序方向:

  • 若查询常用ORDER BY create_time DESC(如订单列表默认 “最新在前”),直接创建create_time DESC索引(8.0 最佳,5.7 次之)。
  • 若排序字段同时有ASCDESC需求(极少),可分别创建升序和降序索引(但需平衡索引维护成本)。

2. 联合索引的排序字段放最后:

  • 索引遵循 “最左前缀原则”,过滤字段(WHERE条件)放前面,排序字段(ORDER BY)放后面,例如WHERE status=1 ORDER BY create_time DESC,联合索引为(status, create_time DESC)。

3. 避免过度创建降序索引:

  • 降序索引与升序索引的维护成本相同(都会占用磁盘空间,影响写入性能),仅对 “高频逆序排序” 的字段创建降序索引,低频场景无需单独创建(依赖优化器反向扫描即可)。

5. 总结

  • 创建方式:通过ASC(默认)或DESC关键字指定,语法简单。
  • 版本差异:5.7 仅语法支持降序索引(实际升序存储),8.0 真正支持(按降序存储,性能更优)。
  • 核心价值:匹配ORDER BY的排序方向,避免文件排序,尤其在大数据量的列表查询(如订单、日志)中,能显著提升性能。

回目录: 《面试笔记:MySQL 相关目录》
上一篇: 《面试笔记:MySQL 相关01 – 基础核心》
下一篇: 《面试笔记:MySQL 相关03 – SQL语法与查询优化》

喜欢面试笔记:MySQL 相关02 – 索引这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

面试笔记:MySQL 相关01 – 基础核心

作者陶其
2025年11月20日 12:37

感谢订阅陶其的个人博客!

基础核心

回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》


01. 整体架构

以5.7版本为基础,8.0的更改会特别标注。

MySQL 采用分层架构,整体可分为 4 层。
连接层、服务层、存储引擎层、文件系统层。

1. 连接层

客户端连接层

  • 负责接收客户端(如Java程序、Navicat等)的TCP/IP连接请求,处理身份认证(用户名、密码、主机权限校验)。
  • 提供连接池机制,复用已建立的连接(避免频繁 TCP 握手 / 挥手的开销),同时管理连接状态(如空闲超时断开)。
  • 核心组件:连接器(验证用户名密码、权限)、连接池(复用已建立的连接,减少握手开销)。
  • 8.0版本变化:
    • 认证插件升级:默认使用caching_sha2_password替代 5.7 的mysql_native_password,加密强度更高,需客户端(如 JDBC 驱动)适配支持。
    • 连接池优化:增强连接复用效率,减少空闲连接占用的资源,同时支持 “连接属性动态修改”(无需重启连接即可调整部分参数)。

2. 服务层

核心处理层

  • 所有存储引擎共享的核心层,负责 SQL 的 “解析 – 优化 – 执行” 全流程,不直接操作数据,仅通过接口调用存储引擎。
  • 包含查询缓存(默认关闭)、解析器、优化器、执行器等核心逻辑。
  • 统一处理日志(如 binlog)、权限二次校验(执行 SQL 前再次确认权限)等通用逻辑。
  • 核心流程:接收 SQL 后,先查查询缓存(若命中直接返回)→ 解析器生成语法树(检查 SQL 语法)→ 优化器生成最优执行计划(如选择索引、连接方式)→ 执行器调用存储引擎接口执行计划。
  • 8.0版本变化:
    • 移除查询缓存组件:因 5.7 中查询缓存命中率极低(数据更新会清空对应表缓存),8.0 直接删除该模块,简化服务层逻辑,避免无效开销。
    • 优化器增强:架构层面支持更多优化规则(如复杂 JOIN 的执行计划调整)。
    • binlog 默认开启:5.7 中 binlog 默认关闭,8.0 默认启用,且架构上支持 binlog 的 “即时回放”(加速主从同步),无需额外配置。

3. 存储引擎层

数据存储层

  • 采用 “插件式架构”,可动态加载不同存储引擎(如 InnoDB、MyISAM),负责数据的实际存储、读取和事务管理。
  • 与服务层通过统一的handler接口交互,服务层无需关心底层数据存储格式(如 InnoDB 的表空间、MyISAM 的文件存储)。
  • 默认存储引擎为 InnoDB,架构上支持事务、行锁等特性的底层实现。
  • 8.0版本变化:
    • 强化 InnoDB 的架构适配:移除其他低效存储引擎(如 Federated),仅保留 InnoDB、MyISAM、Memory 等常用引擎,聚焦 InnoDB 的性能优化。
    • 架构层面支持 InnoDB 的新特性:如自增 ID 持久化(通过 redo log 架构实现)、隐藏索引(架构上支持索引的 “逻辑禁用”,不影响物理存储)。
    • 锁机制架构优化:行锁的范围判断逻辑在架构层面更精准,减少锁冲突(依赖与服务层的接口交互优化)。

4. 文件系统层

  • 负责将数据、日志等持久化到磁盘,依赖操作系统的文件系统(如 ext4、NTFS)。
  • 存储文件类型包括:表空间文件(InnoDB 的.ibd)、日志文件(redo log(重做日志)、binlog(二进制日志)、undo log(回滚日志))、配置文件(my.cnf)等。
  • 8.0版本变化:
    • 日志文件架构调整:redo log 默认存储路径优化,支持更大的日志文件大小(默认单个文件 1GB,5.7 默认 48MB),提升崩溃恢复效率。
    • 表空间文件优化:默认使用独立表空间(5.7 需手动配置),且架构上支持 “表空间加密”(透明数据加密 TDE),文件存储更安全。
    • 自增 ID 存储架构变更:5.7 中自增 ID 存于内存,8.0 架构上改为写入 redo log,重启后可通过日志恢复自增 ID 序列,避免重复。

02. 核心组件

1. 连接器

Connection Manager
核心功能:负责客户端连接的建立、管理与身份认证,是客户端与 MySQL 交互的 “入口”。

  • 接收 TCP 连接请求后,校验用户名、密码及客户端主机权限(基于mysql.user表),通过后分配连接线程。
  • 维护连接状态(如空闲、活跃),默认空闲超时时间为 8 小时(wait_timeout参数控制),超时后自动断开。
  • 认证依赖mysql_native_password插件(默认),加密强度一般,兼容性好。
  • 8.0 版本变化:
    • 默认认证插件升级为caching_sha2_password,采用 SHA-256 加密,安全性更高(需客户端驱动支持,如 JDBC 需 8.0 + 版本)。
    • 优化连接复用机制,减少空闲连接的资源占用,支持 “连接属性动态修改”(如无需重连即可调整部分会话参数)。

2. 查询缓存

Query Cache
核心功能:缓存 SQL 语句与结果集,相同 SQL(字节级一致)可直接返回缓存结果,减少重复计算。

  • 存在但默认关闭(query_cache_type=OFF),需手动开启;缓存以表为单位,表数据更新(增删改)时会清空该表所有缓存。
  • 局限性明显:仅适用于静态数据(如配置表),高并发写场景下命中率极低,反而因缓存维护消耗资源。
  • 8.0 版本变化:
    • 彻底移除查询缓存组件(相关参数如query_cache_size失效),原因是其实际应用价值低,移除后简化了服务层逻辑,减少无效开销。

3. 解析器

Parser
核心功能:对 SQL 语句进行语法分析,生成 “语法树”,确保 SQL 符合语法规则。

  • 解析过程包括:词法分析(识别关键字、表名、字段名等)→ 语法分析(检查 SQL 结构是否合法,如SELECT后是否有字段、WHERE是否搭配条件)。
  • 若语法错误(如关键字拼写错误),直接返回报错(如 “you have an error in your SQL syntax”)。
  • 8.0 版本变化:
    • 核心功能不变,但扩展了对新语法的支持(如WITH RECURSIVE递归查询、降序索引语法),解析效率略有优化(减少语法树生成的内存占用)。

4. 优化器

Optimizer
核心功能:基于语法树生成 “最优执行计划”,目标是最小化执行成本(CPU、IO 开销)。

  • 优化逻辑包括:选择合适的索引(如判断全表扫描 vs 索引扫描更快)、调整多表连接顺序(小表驱动大表减少中间结果集)、简化表达式(如a=1 and a=2直接判定为无效)。
  • 依赖表统计信息(如行数、数据分布),但统计信息更新不及时可能导致执行计划偏差。
  • 8.0 版本变化:
    • 增强统计信息:引入 “直方图”(Histogram),更精准记录数据分布(如字段值的频率),优化器对非均匀分布数据的索引选择更合理。
    • 优化规则扩展:支持复杂 JOIN(如多表嵌套连接)的执行计划调整,子查询优化更彻底(减少 “派生表” 的临时表开销)。

5. 执行器

Executor
核心功能:根据优化器生成的执行计划,调用存储引擎接口执行操作,并返回结果。

  • 执行前再次校验权限(避免连接建立后权限被修改导致的安全问题)。
    • 例如:检查用户是否有目标表的SELECT权限。
  • 通过统一接口(如handler::read_row)调用存储引擎,获取数据后进行过滤、聚合等处理(如WHERE条件过滤、GROUP BY分组)。
  • 8.0 版本变化:
    • 优化接口调用效率,减少与存储引擎的交互次数(如批量读取数据)。
    • 支持 “即时执行”(Instant Execution),对简单查询可跳过部分优化步骤,直接执行,提升响应速度。

6. 日志组件

核心日志模块
负责记录 MySQL 的操作和状态,支撑数据恢复、主从同步等功能。

1. binlog(二进制日志)

  • 5.7 版本:默认关闭,需手动开启(log_bin=ON)。记录所有数据修改操作(增删改、DDL),格式支持STATEMENT(语句)、ROW(行)、MIXED(混合),用于主从同步和时间点恢复。
  • 8.0 版本:默认开启。格式默认ROW(更安全),支持 “即时回放”(Binlog Instant),主从同步时可跳过部分无效日志,提升同步效率。

2. redo log(重做日志,InnoDB 依赖)

  • 5.7 版本:InnoDB 专属,记录数据页的物理修改,采用 “循环写” 机制(固定大小文件),保证崩溃后数据可恢复(先写日志再写磁盘,即 WAL 机制)。
  • 8.0 版本:默认单个日志文件大小从 48MB 增至 1GB,减少日志切换频率;支持 “并行写入”,提升高并发下的日志写入效率。

3. undo log(回滚日志,InnoDB 依赖)

  • 5.7 版本:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,可能因长期积累导致空间膨胀。
  • 8.0 版本:支持 “undo log 自动回收”(通过innodb_undo_log_truncate参数),无需手动清理,减少维护成本。

7. 权限组件

核心功能:管理用户权限,控制对数据库、表、字段的操作权限。

  • 权限存储在mysql库的系统表中(如userdbtables_priv),权限修改后需通过FLUSH PRIVILEGES刷新或重启生效(静态权限)。
  • 支持库级、表级、列级权限,但缺乏细粒度的动态权限(如管理特定日志的权限)。
  • 8.0 版本变化:
    • 引入 “动态权限”(如BINLOG_ADMINBACKUP_ADMIN),权限修改后即时生效,无需刷新或重启。
    • 权限检查逻辑优化,结合角色(Role)管理(5.7 后期引入但不完善,8.0 强化),可批量分配权限,简化权限管理。

8. 总结

MySQL 核心组件的核心逻辑(连接、解析、优化、执行、日志、权限)在 5.7 和 8.0 中保持一致。
8.0 的改进集中在:移除低效组件(查询缓存)、增强安全性(认证插件)、优化性能(优化器、日志)、简化维护(动态权限、undo 回收)等。

03. 存储引擎

MySQL 的存储引擎是负责数据存储、读取及底层特性实现的核心模块,采用 “插件式” 设计,不同引擎支持的功能(如事务、锁机制)差异显著。

1. InnoDB(默认)

InnoDB 是 MySQL 最常用(默认)的存储引擎,以事务支持、高并发为核心优势,5.7 和 8.0 均将其作为默认引擎,版本间优化集中在性能、可靠性和功能扩展。

核心通用特性(5.7 和 8.0 共通)

  • 支持事务(ACID 特性):通过 redo log(保证持久性)、undo log(保证原子性和隔离性)实现。
  • 行级锁:仅锁定修改的行(而非全表),适合高并发写场景(如电商订单更新)。
  • 聚簇索引:数据与主键索引物理存储在一起,查询主键时效率极高。
  • 外键约束:支持表间外键关联(如orders表关联users表的user_id)。

5.7 版本 InnoDB 特性

  • 自增 ID(AUTO_INCREMENT):存储在内存中,重启 MySQL 后可能因未持久化导致重复(需依赖 binlog 恢复,但存在风险)。
  • 索引限制:仅语法支持降序索引(DESC),实际仍按升序存储,查询降序数据时需额外排序。
  • 锁机制:间隙锁(Gap Lock)范围较宽泛,可能导致高并发下锁冲突增加(如批量插入相邻 ID 时)。
  • undo log:默认随表空间存储,长期运行后可能因未自动回收导致磁盘空间膨胀(需手动清理)。

8.0 版本 InnoDB 关键改进

  • 自增 ID 持久化:将自增 ID 写入 redo log,重启后可通过日志恢复,彻底解决 5.7 的重复问题。
  • 索引增强:
    • 真正支持降序索引(DESC),查询降序排序数据时无需额外排序,直接使用索引。
    • 新增 “隐藏索引”(INVISIBLE):可标记索引为隐藏(不影响物理存储),用于临时禁用索引测试性能(无需删除重建)。
  • 锁优化:间隙锁范围更精准,减少非必要锁定(如批量插入时仅锁定实际需要的区间),降低锁冲突。
  • undo log 自动回收:支持innodb_undo_log_truncate参数(默认开启),自动收缩过大的 undo log,减少人工维护成本。
  • 表空间加密:支持透明数据加密(TDE),表空间文件(.ibd)加密存储,提升数据安全性。

2. MyISAM

MyISAM 是早期 MySQL 的默认引擎,因不支持事务和行锁,逐渐被 InnoDB 替代,5.7 和 8.0 中仍保留但应用场景有限。

核心通用特性(5.7 和 8.0 共通)

  • 不支持事务和外键:仅适合无需事务保证的场景。
  • 表级锁:写操作(增删改)会锁定全表,读操作需等待写锁释放,并发写性能差。
  • 独立文件存储:数据存于.MYD文件,索引存于.MYI文件,可直接复制文件迁移表。

5.7 版本:仍有部分场景使用(如只读日志表),但已明确不推荐用于核心业务。
8.0 版本:进一步弱化 MyISAM,默认配置下性能优化倾向 InnoDB,且移除了部分对 MyISAM 的冗余支持(如全文索引的部分优化仅针对 InnoDB)。

3. Memory

内存引擎
数据存储在内存中,适合临时数据处理,性能极快但数据易失(重启丢失)。

5.7 版本特性

  • 支持哈希索引(默认)和 B + 树索引,哈希索引适合等值查询(=),不支持范围查询(>、<)。
  • 表大小受max_heap_table_size限制(默认 16MB),超出后会报错。

8.0 版本改进

  • 优化内存分配机制,减少小表的内存浪费。
  • 支持动态调整max_heap_table_size(无需重建表),更灵活适配临时数据大小。

4. CSV

逗号分割值引擎
数据以 CSV 格式文件存储(.csv),适合数据交换(如与 Excel、文本文件交互)。

5.7 版本特性

  • 表结构存于.frm文件,数据存于.csv文件,可直接用文本编辑器查看 / 修改。
  • 不支持索引和事务,仅适合简单的导入导出场景。

8.0 版本改进

  • 增强兼容性:支持 CSV 文件中包含换行符(需特殊处理),减少导入导出时的格式错误。

5. 总结

  • InnoDB:5.7 已具备事务、行锁核心能力,8.0 通过自增 ID 持久化、索引优化、锁细化等提升可靠性和性能,是所有业务的首选。
  • MyISAM:仅适合只读、低并发场景,8.0 中进一步被边缘化。
  • Memory/CSV:作为辅助引擎,8.0 主要优化了易用性(如动态调整内存表大小),核心功能无本质变化。

04. InnoDB存储引擎

InnoDB作为MySQL的默认且常用的搜索引擎,有如下的核心特性:

1. 事务支持(ACID)

  • 完全支持事务的原子性(A)、一致性(C)、隔离性(I)、持久性(D)。

2. 锁机制

  • 支持行级锁 + 间隙锁(Next-Key Lock),行级锁仅锁定修改行(高并发友好),间隙锁防止幻读。
  • 8.0 变化:间隙锁范围更精准,减少非必要锁定(如批量插入相邻 ID 时仅锁实际区间),降低锁冲突概率。

3. 聚簇索引

  • 数据与主键索引物理存储在一起,形成 B + 树结构,主键查询可直接获取数据。

4. 自增 ID(AUTO_INCREMENT)

  • 自增 ID 存储在内存中,MySQL 重启后可能因未持久化导致序列重复(需依赖 binlog 部分恢复)。
  • 8.0 变化:自增 ID 写入 redo log 持久化,重启后可通过日志恢复序列,彻底解决重复问题。

5. 索引特性

  • 支持 B + 树索引(默认)、全文索引、前缀索引;仅语法支持降序索引(DESC),实际仍按升序存储。
  • 8.0 变化:
    • 真正支持降序索引,查询降序排序数据时无需额外排序,直接复用索引。
    • 新增隐藏索引(INVISIBLE),可临时禁用索引(不删除),方便测试索引性能影响。

6. 日志依赖(redo log/undo log)

  • redo log:循环写的物理日志,记录数据页修改,保证崩溃恢复。
  • undo log:记录数据修改前的状态,用于事务回滚和 MVCC(多版本并发控制),默认随表空间存储,易膨胀需手动清理。
  • 8.0 变化:
    • redo log 默认单个文件大小从 48MB 增至 1GB,减少日志切换开销。
    • undo log 支持自动回收(innodb_undo_log_truncate默认开启),无需手动清理。

7. 外键约束

  • 支持表间外键关联(如:orders.user_id关联users.id),保证数据引用完整性。

8. 安全性增强

  • 无表空间加密功能,数据文件(.ibd)以明文存储。
  • 8.0 变化:新增透明数据加密(TDE),支持表空间文件加密存储,提升敏感数据安全性(如用户密码、支付信息表)。

05. 三大范式和反范式

MySQL 的三大范式(1NF、2NF、3NF)是关系型数据库设计的基础原则,目的是减少数据冗余、保证数据一致性、避免插入 / 更新 / 删除异常
反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN提升查询效率(尤其读多写少场景)。

1. 第一范式(1NF)

第一范式(1NF):字段原子化,不可再分。

  • 核心要求:表中所有字段的值必须是 “原子性” 的(不可再拆分为更小的数据单元)。
  • 目的:避免同一字段存储多维度信息,导致查询和修改混乱。
  • 例子:
    • 反例:用户表的address字段存储 “中国 – 北京 – 朝阳区”(可拆分为countrycitydistrict);
    • 正例:拆分为countrycitydistrict三个字段,每个字段不可再分。
  • 实践:1NF 是基础,几乎所有业务表都需满足(如:订单表的phone字段不存储 “固话 + 手机”,而是单独字段)。

2. 第二范式(2NF)

第二范式(2NF):消除 “部分依赖”,非主属性完全依赖主键。

  • 核心要求:在 1NF 基础上,表的主键必须是 “联合主键”(多字段组成),且所有非主属性必须完全依赖于整个主键,不能仅依赖主键的一部分(即消除 “部分依赖”)。
  • 目的:避免因主键部分字段变化导致的数据异常(如修改部分主键后,非主属性需联动更新)。
  • 例子:
    • 反例:订单项表(联合主键order_id+product_id)中,product_name仅依赖product_id(主键的一部分),属于部分依赖;
    • 正例:product_name应存储在产品表中,订单项表只存product_id,通过关联产品表获取名称(非主属性quantityprice完全依赖order_id+product_id)。
  • 实践:多对多关系的中间表(如 “用户 – 角色” 关联表)需满足 2NF,避免冗余存储角色名称等信息。

3. 第三范式(3NF)

第三范式(3NF):消除 “传递依赖”,非主属性不依赖其他非主属性。

  • 核心要求:在 2NF 基础上,所有非主属性必须直接依赖于主键,不能依赖于其他非主属性(即消除 “传递依赖”)。
  • 目的:避免因某个非主属性变化,导致其他非主属性需联动更新(如 A 依赖主键,B 依赖 A,则 B 传递依赖主键)。
  • 例子:
    • 反例:用户表(主键user_id)中,area_name依赖area_idarea_id依赖user_id,则area_name传递依赖user_id
    • 正例:area_name应存储在区域表中,用户表只存area_id,通过关联区域表获取名称(非主属性仅直接依赖user_id)。
  • 实践:用户表、商品表等核心表需满足 3NF,避免存储 “部门名称”“分类名称” 等可通过关联获取的字段。

4. 反范式

反范式是有意打破范式规则,有策略地保留适量冗余,目的是减少多表 JOIN提升查询效率(尤其读多写少场景)。

  • 核心思路
    通过在表中冗余存储其他表的字段,避免查询时关联多个表(JOIN 操作耗时,尤其大数据量时)。
  • 例子:
    • 电商商品列表页需展示 “商品名称 + 分类名称”,若严格遵循 3NF,需关联product表和category表;
    • 反范式优化:在product表中冗余category_name字段,查询时直接从product表获取,无需 JOIN。
  • 适用场景
    1. 高频查询、低频更新:如商品详情页(查询频繁,分类名称很少修改),冗余后查询性能提升 10 倍以上;
    2. 多表关联复杂:如订单列表需关联用户表、商品表、物流表,冗余 “用户名”“商品名” 后,查询从多表 JOIN 简化为单表查询;
    3. 统计分析场景:报表系统需聚合多维度数据,冗余存储聚合结果(如 “每月销售额”),避免实时计算。
  • 注意事项
    • 冗余字段需同步更新:如category_name修改后,需同步更新product表中的冗余字段(可通过触发器、Java 代码事务保证);
    • 控制冗余范围:只冗余高频查询的核心字段(如名称、状态),避免表过大(如冗余大文本字段会增加存储和 IO 成本)。

5. 总结

  • 范式:适合写多读少、数据一致性要求高的场景(如订单系统、用户中心),通过减少冗余降低更新异常风险;
  • 反范式:适合读多写少、查询性能敏感的场景(如电商列表、报表),通过可控冗余提升查询效率。
  • 实际开发中,很少严格遵循某一范式,而是混合使用(如核心交易表用 3NF 保证一致性,查询表用反范式提升性能)。

06. DDL、DML、DCL、DQL

MySQL 中 DDL、DML、DCL、DQL 是按操作类型划分的四大类 SQL 语言,分别对应:

  • DDL:数据库结构定义;
  • DML:数据操纵;
  • DCL:权限控制;
  • DQL:数据查询。

1. DQL:数据查询语言

Data Query Language。

  • 定义:用于从数据库中查询数据,不修改数据或结构,是业务系统中最频繁的操作。
  • 核心命令:SELECT(含WHEREJOINGROUP BYORDER BYLIMIT等子句)。
  • 作用:从表中提取所需数据,支撑业务展示(如列表页、详情页)、统计分析(如报表)等场景。
  • 实践要点:
    • 是 Java 开发中最常用的 SQL(如查询用户信息、订单列表),需结合索引优化(如WHERE条件加索引、避免SELECT *);
    • 复杂查询(多表关联、聚合)需用EXPLAIN分析执行计划,避免全表扫描或文件排序。

2. DML:数据操纵语言

Data Manipulation Language。

  • 定义:用于修改表中的数据(增、删、改),会改变数据内容,但不改变表结构。
  • 核心命令:
    • INSERT(新增);
    • UPDATE(修改);
    • DELETE(删除)。
  • 作用:处理业务数据的生命周期(如创建订单、更新状态、删除无效记录)。
  • 实践要点:
    • 操作会触发事务(默认自动提交,可通过BEGIN手动控制),需保证原子性(如订单创建时同时扣减库存,失败则回滚);
    • 批量操作优化:INSERTVALUES (),(),()批量插入(比单条循环高效),DELETE/UPDATE避免全表操作(加WHERE条件,如DELETE FROM log WHERE create_time < '2024-01-01');
    • 高频写入场景(如日志)需控制频率,避免锁表影响查询。

3. DDL:数据定义语言

Data Definition Language。

  • 定义:用于定义或修改数据库、表、索引等结构,会改变数据库的元数据(结构信息)。
  • 核心命令:
    • CREATE(创建,如CREATE TABLECREATE INDEX);
    • ALTER(修改,如ALTER TABLE ADD COLUMN);
    • DROP(删除,如DROP TABLE);
    • TRUNCATE(清空表)。
  • 作用:初始化数据库结构(如建表、加字段)、调整表结构(如新增索引、扩展字段)。
  • 实践要点:
    • 执行时可能锁表(尤其ALTER TABLE在 InnoDB 中,大表修改会阻塞读写),生产环境需在低峰期执行,大表建议用在线 DDL 工具(如 pt-online-schema-change);
    • 谨慎使用DROPTRUNCATE(不可逆,TRUNCATE会清空数据且不触发事务回滚);
    • 索引相关 DDL(CREATE INDEX)需评估必要性(索引提升查询但降低写入性能)。

4. DCL:数据控制语言

Data Control Language。

  • 定义:用于管理数据库用户权限和事务控制,控制谁能操作数据、操作范围。
  • 核心命令:`
    • GRANT`(授予权限);
    • REVOKE(撤销权限);
    • COMMIT(提交事务);
    • ROLLBACK(回滚事务)。
  • 作用:保障数据安全(如限制应用账号只能操作指定表)、控制事务一致性。
  • 实践要点:
    • 权限遵循 “最小原则”:应用程序账号只授予SELECT/INSERT/UPDATE等必要权限,禁止DROP/GRANT等高风险权限,避免用 root 账号直接连接业务系统;
    • 事务控制(COMMIT/ROLLBACK)需在 Java 代码中配合业务逻辑(如分布式事务场景,确保多库操作一致性)。

5. 总结

四类语言分工明确:

  • DQL:查数据(业务展示核心);
  • DML:改数据(业务操作核心);
  • DDL:改结构(架构维护核心);
  • DCL:控权限(安全保障核心)。

07. 数据库表字段类型

1. 核心字段类型及适用场景

1. 数值型:适合存储数字(整数、小数)

类型 特点(长度 / 范围) JDK8对应类型 适用场景
TINYINT 1 字节,范围 – 128 ~ 127
(无符号 0 ~ 255)
Byte 状态标识(如status:0 – 禁用、1 – 正常)、
性别(0 – 女、1 – 男)
INT 4 字节,范围 – 21 亿 ~ 21 亿 Integer 普通 ID(如user_idorder_id,中小规模业务足够)、数量(如count
BIGINT 8 字节,范围 ±9e18,
大约±90亿亿
Long 大整数 ID(如分布式 ID、雪花 ID)、
高频增长数据(如万亿级订单量)
DECIMAL(M,D) 高精度小数
(M 总长度,D 小数位)
java.math.BigDecimal 金额(如amountDECIMAL(10,2)表示
最多 10 位,2 位小数)、利率
FLOAT/DOUBLE 单 / 双精度浮点
(有精度损失)
Float/Double 非精确计算(如温度、重量,允许微小误差)

2. 字符串型:适合存储文本

类型 特点(长度 / 范围) JDK8对应类型 适用场景
CHAR(N) 固定长度(N 字节,0 ~ 255),
空格填充
String 长度固定的字符串(如手机号CHAR(11)、身份证号CHAR(18)
VARCHAR(N) 可变长度(0 ~ 65535),
存储实际长度
String 长度不固定的文本(如用户名VARCHAR(50)、地址VARCHAR(200)
TEXT 大文本(最大 64KB) String 较长文本(如商品描述、用户备注,不适合建索引)
MEDIUMTEXT/LONGTEXT 更大文本(16MB/4GB) String 超大文本(如文章内容、日志详情,慎用,会拖慢查询)

3. 日期时间型:适合存储时间

类型 特点(长度 / 范围) JDK8对应类型 适用场景
DATETIME 8 字节,范围 1000 ~ 9999 年,
无时区
java.time.LocalDateTime
(不推荐Date
业务时间(如订单创建时间create_time,不受服务器时区影响)
TIMESTAMP 4 字节,范围 1970 ~ 2038 年,
受时区影响
java.time.LocalDateTime
(不推荐Date
记录系统时间(如最后更新时间update_time,自动随时区转换)
DATE 3 字节,仅日期(年月日) java.time.LocalDate
(不推荐Timestamp
生日、到期日(如birthdayexpire_date
TIME 3 字节,仅时间(时分秒) java.time.LocalTime 时段记录(如会议时长、打卡时间)

4. 特殊类型:针对性场景

类型 特点(长度 / 范围) JDK8对应类型 适用场景
ENUM 枚举(存储整数,显示字符串) String 固定可选值(如pay_typeENUM('WECHAT','ALIPAY','CARD')
SET 集合(多选,最多 64 个值) String 多选项(如tagsSET('hot','new','discount')
BLOB 二进制数据(如图片、文件) byte[](字节数组) 小型二进制(如头像缩略图,大型文件建议存 OSS,库中只存 URL)

2. 经典面试点

1. 数值型:精度与范围陷阱

  • 金额为什么用DECIMAL而非FLOAT
    FLOAT是浮点型,存在精度损失(如0.1 + 0.2 = 0.300000004),而DECIMAL是精确小数,适合金额等强精度场景。

  • INTBIGINT怎么选?
    中小业务(千万级数据)用INT足够;分布式系统(如订单 ID 用雪花 ID,长度 18 位)必须用BIGINT,避免溢出。

2. 字符串型:长度与性能

  • CHARVARCHAR的核心区别?
    CHAR固定长度,查询快但浪费空间(如手机号用CHAR(11)VARCHAR(11)高效,无需计算长度);
    VARCHAR节省空间,但查询需额外解析长度,适合长度波动大的场景(如地址)。

  • 为什么不建议VARCHAR(255)滥用?
    MySQL 中VARCHAR(255)在某些引擎(如 InnoDB)中会按 255 字节分配临时内存,即使实际数据很短,也会浪费内存(尤其排序、JOIN 时),建议按实际需求设长度(如用户名VARCHAR(50))。

  • TEXT类型的坑?
    TEXT字段不适合建索引(即使建索引也只能取前 N 个字符),且查询时会额外 IO,大文本建议拆分表(如商品表存desc_id,关联单独的product_desc表存TEXT内容)。

3. 日期时间型:时区与范围

  • DATETIMETIMESTAMP怎么选?
    业务时间(如订单创建时间)用DATETIME(固定值,不受服务器时区影响);系统时间(如最后修改时间)用TIMESTAMP(自动更新,适配多时区部署)。

  • TIMESTAMP的 2038 年问题?
    因范围是 1970 ~ 2038 年,长期系统需注意(可改用DATETIME规避)。

4. 枚举类型ENUM的利弊

  • 优点:存储高效(用整数存字符串),约束数据合法性(只能选定义的值);
  • 缺点:修改枚举值需ALTER TABLE(DDL 操作,大表阻塞),不适合值频繁变化的场景(如活动状态,建议用TINYINT+ 字典表替代)。

3. 总结

字段类型选择的核心原则:“够用即可,避免冗余

  • 数字优先选小类型(如状态用TINYINT而非INT);
  • 字符串按长度固定与否选CHAR/VARCHAR,避免大文本字段拖累主表;
  • 日期根据时区需求选DATETIME/TIMESTAMP
  • 金额、ID 等关键字段必须保证精度和范围,避免溢出或精度丢失。

回目录: 《面试笔记:MySQL 相关目录》
下一篇: 《面试笔记:MySQL 相关02 – 索引》

喜欢面试笔记:MySQL 相关01 – 基础核心这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

面试笔记:MySQL 相关目录

作者陶其
2025年11月14日 10:54

感谢订阅陶其的个人博客!

MySQL高频问点

1. 基础核心

  • 整体架构
  • 核心组件
  • 存储引擎
  • InnoDB存储引擎
  • 三大范式和反范式
  • DDL/DML/DCL/DQL
  • 数据库表字段类型

2. 索引

  • 各种树
  • 索引的作用
  • 常见索引类型
  • 聚簇索引与非聚簇索引
  • 联合索引的最左前缀原则
  • 索引的维护成本
  • 回表查询与索引覆盖
  • 如何合理使用索引
  • 索引失效场景
  • explain执行计划分析
  • 升序索引与降序索引

3. SQL语法与查询优化

  • 子查询与连接查询
  • 子查询与join性能对比及适用场景
  • 复杂查询(group by/having、limit分页)的执行逻辑
  • order by 的排序原理
  • 慢查询的定位与分析
  • 复杂SQL的拆分与改写(大表分页优化:延迟关联、书签分页)
  • join优化(小表驱动大表、 避免 cross join)
  • count的性能差异(count(1)、count(*)、count(字段))
  • IN和JOIN哪个快
  • MySQL的函数

4. 事务和锁机制

  • 事务
    • ACID
    • 四大隔离级别的定义及实现
    • 默认隔离级别为何是可重复读?
    • 隔离级别与并发问题(脏读、不可重复读、幻读)的对应关系
  • 锁机制
    • 锁的分类
    • 行锁的触发条件
    • MVCC的原理
    • 死锁的产生原因与定位
    • 避免死锁的策略
    • 高并发下如何减少锁竞争

5. 性能调优

  • MySQL核心配置参数的含义与调优依据
  • 连接池配置
  • InnoDB 缓冲池
  • redo log 与 binlog 的协作
  • 刷盘策略对性能与安全性的影响
  • 磁盘IO优化
  • CPU与内存分配
  • 网络优化

6. 高可用与集群架构

  • 主从复制:
    • 复制原理
    • 复制模式
    • 复制延迟的原因与解决
  • 读写分离:
    • 读写分离的实现
    • 一致性问题
    • 分库分表的必要性与拆分策略
  • 高可用架构
    • 主从切换工具
    • 集群方案的适用场景
    • 云原生

7. 数据安全与运维

  • 备份与恢复:
    • 备份类型的选择
    • 基于binlog的时间点恢复流程
    • 大表备份的性能优化
  • 日志系统:
    • binlog
    • redo log
    • 慢查询日志与错误日志的分析
  • 故障付出:
    • 数据库宕机恢复流程
    • 数据一致性校验
    • 索引损坏的修复

8. 进阶特性与版本差异

  • MySQL 8.0新特性:
    • 窗口函数、
    • CTE、
    • 角色管理与权限细化、
    • innoDB的自增锁优化
  • 特殊场景处理:
    • 大表DDL
    • JSON类型的存储与查询优化
    • 地理信息(GIS)功能的实战应用

喜欢面试笔记:MySQL 相关目录这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

面试笔记:Spring Framework 相关

作者陶其
2025年11月12日 16:53

感谢订阅陶其的个人博客!

001. Spring 是如何解决循环依赖的?

答:
什么是循环依赖?
循环依赖指两个或多个 Bean 相互依赖(如 A 依赖 B,B 依赖 A)。
在创建A时,初始化A时需要注入B,那么就需要去创建B;
在创建B时,初始化B时需要注入A,那么就需要去创建A;
此时就形成了循环依赖(程序会死等),最终导致 Bean 创建失败。

如何解决循环依赖问题?
Spring 通过拆分Bean的创建过程,将实例化和初始化分开。

  1. 当创建A时,先实例化A,再对A进行初始化,此时发现需要注入实例B;
  2. Spring此时会将实例化但未完成初始化的A存到缓存中(此处引入三级缓存概念),此时的A是可以被引用的,但并不完整(因为没有完成初始化,b还没有注入);
  3. 此时Spring会去创建B,先实例化B,然后再对B进行初始化,此时发现需要注入实例A;
  4. 此时Spring会把存入缓存的A注入B中(虽然A未完成初始化,但是可以先行注入B,后面再完成A的初始化也是可以的),此时B就创建完成了;
  5. 当B的实例创建完成后,Spring会回到对A实例的初始化步骤来,将成功创建的B注入到A中,此时A也完成了初始化。
  6. 即此时完成了对A和B的实例化和初始化操作,成功解决了循环依赖的问题。

三级缓存是什么?如何被使用的?
这里存在三级缓存的概念,Spring寻找实例化好的bean时会按照一级、二级、三级的顺序依次查找。
注意:三级缓存中不会出现相同名称的bean,当上一级存入相同名称的bean时,下一级同名的bean会被自动删除。
一级缓存存放的是:实例化完成且初始化完成的实例;
二级缓存存放的是:实例化完成但初始化未完成的实例;
三级缓存存放的是:存放Bean 的工厂对象(ObjectFactory),用来生成早期的bean或其代理对象,同时保证三级缓存中不会出现同名的bean实例。

002. Spring Bean 的生命周期

答:
Spring 的完整生命周期(按执行顺序)
实例化 -> 属性注入 -> 生命周期回调 -> 初始化 -> 使用 -> 销毁

各核心阶段的作用

  1. 实例化:Spring 通过反射创建 Bean 的实例;
  2. 属性注入:Spring 将配置文件或注解中定义的属性值(如@Autowired 注入的依赖)赋值给 Bean 实例(此处会产生循环依赖问题);
  3. 生命周期回调(初始化前):Bean 实现 Aware 接口的回调,如 BeanNameAware(获取 Bean 的 id)、BeanFactoryAware(获取 BeanFactory 容器)、ApplicationContextAware(获取 ApplicationContext 上下文);
  4. 初始化:执行自定义初始化逻辑,优先级为:
    @PostConstruct 注解方法
    -> InitializingBean 接口的 afterPropertiesSet()
    -> XML 配置的 init-method 属性指定方法。
  5. 使用:Bean 实例存入 Spring 容器,供应用程序通过 getBean() 或依赖注入方法使用;
  6. 销毁:容器关闭时执行自定义销毁逻辑,优先级:
    @PreDestory 注解方法
    -> DisposableBean 接口的 destory()
    -> XML 配置的 destory-method 属性指定方法。

关键接口/注解说明

  • Aware 系列接口:用于 Bean 获取容器相关信息,无需手动调用,容器自动触发;
  • 初始化相关:@PostConstrut(注解式,最常用)、InitializingBean(接口式)、init-method(XML 配置式);
  • 销毁相关:@PreDestory(注解式)、DisposableBean(接口式)、destroy-method(XML 配置式)。

喜欢面试笔记:Spring Framework 相关这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

是“冒犯”,还是“艺术”?

作者陶其
2025年8月14日 09:23

感谢订阅陶其的个人博客!

刚才看了一篇关于脱口秀的公众号文章:《脱口秀变味记:当笑声里掺杂了太多杂音》。

里面发表了一些关于对现在脱口秀相关热点和舆论的看法。

我也有一些想要聊的,就简单聊一下。

我接触“脱口秀”不算早,但是也不算晚。

从王自健的《80后脱口秀》,到笑果的《脱口秀大会》和《吐槽大会》,再到现在的《喜剧之王》和《脱口秀和TA的朋友们》。

可以说,脱口秀是我平时比较喜欢看的一种综艺类型。同时我也喜欢听相声,只不过脱口秀我喜欢用看,而相声我喜欢用听,特别是入睡的时候。

一开始,我感觉脱口秀特别像是相声,特别是脱口秀里的“漫才”(即双人脱口秀),就更像是说相声了。

后来听多了,才发现脱口秀和相声有明显的不同,即使他们的表现形式确实很像。

以我现在粗浅的理解:

无论是传统的相声,还是创新的相声,其内容基本都具备一定的故事性。同时衍伸铺垫出包袱,使用扎实的基本功和老练的表演技巧表达出来。好的相声会让人听得津津有味,所以比较吃人生阅历、舞台经验和基本功。

而脱口秀首要注重的并非故事性,而是主题性。一段脱口秀可以围绕着一个主题各种展开,无论是横向用多个故事描述,还是死扣一个点进行深度挖掘,都是可以的。好的脱口秀能让人一直沉浸在某种情绪中,比如大量的梗让人笑的停不下来,或者段子让人感同身受从而产生共鸣等。但是脱口秀相对来说是比较吃文本、吃热点,甚至是吃表演技巧的。

好的相声就像是一杯醇香的酒,入口柔顺、回味甘甜、口齿生津、回味无穷;
好的脱口秀就像是一瓶夏日冷藏的可乐,巧妙的刺激多巴胺,能给人带来快乐、单纯的开心、让人精神愉悦、暂时忘掉烦恼。

当然这么区分这两者的区别是很粗浅的,这里也是只浅聊。

这一点比较深刻的区分可以从德云社相声演员阎鹤祥在脱口秀的舞台上说的脱口秀内容能听出来。


之前看脱口秀节目,可以说是我排解苦闷的一个很好的方式。

在这里我可以放松的去听,无防备的去感受段子里的精妙和笑点,我只负责笑就好了,不必像是上网冲浪那样需要带着明辨是非的眼睛和脑子。

以下只聊作品不聊人品。

卡姆那喷涌的原始蓬勃生命力;
庞博的发人深省的观察和高情商的表达;
呼兰的超强文本和缜密逻辑下的搞笑行为;
王建国的“谐音梗”盛世和“尼罗河畔法力无边的法老们”;
鸟鸟的“一个i人自述引发的集体共鸣”;
毛豆疯狂擦汗同时的“无比密集的梗和包袱”;
黑灯和小佳的“独特的另一视角的世界”;
漫才兄弟的“无厘头的搞怪表演”;
王勉的“颇具才华独树一帜的音乐脱口秀”;
童漠男的“无奈又搞笑的行业冥灯”;
周奇墨大魔王的一句“雷森图拜拜”;
杨蒙恩的“遍地是大王,短暂又辉煌”;
广智的“最起码这里他还有法律”的穷哥们段子;
小卉“无比炸场的淘汰感言”等等表演,都可以称之为脱口秀艺术。

即使有“冒犯”行为,也是为了服务脱口秀“艺术”本身。


可是现在的脱口秀,变味儿了。

可以说从“杨笠”开始,但其实在她之前就开始有了一些潜在的征兆,她是爆发舆论最猛的那一个。

而她之后,越来越多的“杂音”开始逐渐充斥脱口秀,让这一个新兴的还算单纯的艺术,迅速的像是清水里滴进了墨汁一样开始变色。

就感觉中文脱口秀这块蛋糕,前面那些人辛辛苦苦的做出来并做大了之后,现在谁都想来啃上一口,反正已经有人演示了“捷径”。

所以现在的脱口秀段子开始为了“冒犯”而“冒犯”,为了证明“政治正确”而去疯狂强调“政治正确”。

这个问题其实无论男女脱口秀演员,都有这样的问题。

他/她们的心思根本没有放在打磨文本、打磨表演上,而是在尝试如何能够快速的让自己有知名度,从而捞上一笔或者先火上一把再捞上一笔。

文本不行、表演不行、那就扯上“政治正确”的大旗、扯上“性别论”的大旗、扯上上班族“牛马”的大旗,从而与听众建立起“伪共鸣”,调动情绪比强化文本和表演来的容易太多了。

而且只要扯上了“政治正确”的大旗,便自然而然处于道德的“制高点”。此时即使有人发现了问题,也不敢直白的说出来,因为人们的情绪是很容易被调动的,此时的听众的理智是相对降低的(此处可以借鉴刘慈欣小说里的理论,当人群人数升高到一定程度的时候,集体意识的理智智商值反而会降低),这就让围攻“道德制高点”的人更容易被人们围攻,误解扭曲其原本的意思。


比如就用目前舆论最大的“两性”问题来具体谈谈。

其实我们欢迎女性脱口秀演员讲出两性的不同处境和问题,这其实是一个很好的主题。甚至是男性脱口秀演员也能去讲,这其实是一个值得深度挖掘探讨的,很好的一个社会话题或者说方向。

让每天上学或者上班的没有过多接触过异性的人们,多一些了解异性性格、行为、心理、处境等情况,也能为这些没怎么接触过异性的人们在接触异性的时候提供一些“科普级”的帮助。

但是现在的脱口秀节目中,部分演员完全是在通过疯狂消费“两性问题”并以“阅读理解式”的夹带私货、推行某性有罪论、捏造成为其受害者等方式去为观众带来更多的感官刺激和精神刺激,在这种刺激下,人们更容易忽略理性思考而产生所谓的“伪共鸣”,并被种下某种不正确观念的潜意识。

这其实早已脱离了表演艺术所需要的表达范畴,成为了为了火一把而不择手段的“便捷”方式。


在人们的朴素认知中,人们把rap普遍认为是不良艺术,rapper是一群不良少年。

那是因为rap圈子的一些人那种种“奇葩”行为造成的。

一手“临时抱佛脚”能成为“金曲”,叛逆脏话等成了行为标榜,这完全是在挑战社会的朴素的主流价值观。

自然不容易被社会所接受。

不信你看周杰伦的歌曲里也有rap,《听妈妈的话》、《霍元甲》、《本草纲目》等,都有rap的元素,你会认为这些歌曲的质量内容很差吗?

我想不会吧,这些无疑都是大众票选出来的金曲,所以你还认为rap本身有问题吗?

有问题的从来不是某种艺术形式,而是为了借助其为载体无所不用其极的火一把的“小丑”罢了。


脱口秀也是这样。

这些年来,脱口秀在中国的流媒体平台一直都是夹缝中求生存,究其原因并不是脱口秀本身存在某些根本性的问题,完全就是某些脱口秀演员的“作死”行为让脱口秀在人们朴素认知中渐渐滑向了“不良”的一边。

而现在的节目里,也开始充斥着“变味”的脱口秀,并且相比来看,那些“传统脱口秀”反而成了老实孩子。因为没有这些新兴观点的“刺激感”和“共鸣感”,造成了“劣币驱逐良币”的现象。

如果节目组或者这个行业的从业者不能及时的“刮骨疗毒”,那么当“良币”全部退出舞台的时候,也是脱口秀这种艺术形式彻底失去其生存土壤的时候。

喜欢是“冒犯”,还是“艺术”?这篇文章吗?您可以点击浏览我的博客主页 发现更多技术分享与生活趣事。

❌