mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
2300 words
6 minutes
MySQL实战索引、日志与优化
2024-03-17

MySQL#

本篇用来讲解mysql的实战开发注意点


一、为什么业务表默认用 InnoDB#

现在大多数 MySQL 业务表都会选择 InnoDB,而不是 MyISAM。

能力InnoDBMyISAM
事务支持不支持
行级锁支持不支持
崩溃恢复支持较弱
MVCC支持不支持
外键支持不支持

后端系统最怕数据不一致。下单、支付、扣库存、退款这些场景都需要事务和崩溃恢复能力,所以 InnoDB 更适合做业务主库。

查看表的存储引擎:

SHOW TABLE STATUS LIKE 'orders';

建表时也可以显式指定:

CREATE TABLE orders (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL
) ENGINE=InnoDB;

二、InnoDB 的数据按主键组织#

InnoDB 中,主键索引就是聚簇索引。可以简单理解为:整行数据就存放在主键索引的叶子节点上

所以主键设计很重要。

比较推荐:

id BIGINT PRIMARY KEY AUTO_INCREMENT

或者使用趋势递增的分布式 ID。

不太推荐把 UUID 字符串直接作为主键:

id VARCHAR(36) PRIMARY KEY

原因是 UUID 太长、太随机,容易让 B+ 树频繁页分裂,索引也更占空间。

主键设计建议:

  • 尽量短。
  • 尽量稳定。
  • 尽量趋势递增。
  • 不要使用会变化的业务字段。

订单号这种业务字段更适合做唯一索引:

UNIQUE KEY uk_order_no (order_no)

三、B+ 树为什么适合索引#

InnoDB 常用 B+ 树作为索引结构。

下图为B+树示意图 B+树示意图

B+ 树适合数据库索引,主要因为:

  • 树高较低,查询磁盘 IO 次数少。
  • 非叶子节点主要存索引键,可以放更多目录信息。
  • 叶子节点之间有链表,适合范围查询。

例如订单列表:

SELECT id, order_no, total_amount
FROM orders
WHERE user_id = 1001
ORDER BY created_at DESC
LIMIT 20;

如果建立索引:

CREATE INDEX idx_user_created
ON orders(user_id, created_at);

MySQL 可以先通过 user_id 定位到这个用户的订单范围,再利用 created_at 的索引顺序取数据,比全表扫描快很多。


四、二级索引和回表#

InnoDB 的主键索引叶子节点保存完整行数据;普通索引的叶子节点保存的是主键值。

所以通过普通索引查询完整数据时,通常要走两步:

普通索引 -> 找到主键 id -> 回到主键索引查整行数据

这个过程叫回表。

例如:

CREATE INDEX idx_username ON user(username);
SELECT *
FROM user
WHERE username = 'zhangsan';

如果命中 idx_username,MySQL 先找到主键 id,再根据 id 回表查完整字段。

如果只查询索引里已有的字段,就可能避免回表:

SELECT id, username
FROM user
WHERE username = 'zhangsan';

这叫覆盖索引。

实际开发建议:

  • 列表页不要无脑 SELECT *
  • 高频查询可以考虑覆盖索引。
  • 返回字段越多,回表和网络传输成本越高。

五、联合索引怎么设计#

联合索引不是把常用字段随便堆在一起,而是要围绕真实 SQL 设计。

假设订单列表经常这样查:

SELECT id, order_no, total_amount, status, created_at
FROM orders
WHERE user_id = 1001
AND status = 1
AND deleted = 0
ORDER BY created_at DESC
LIMIT 20;

可以考虑:

CREATE INDEX idx_user_status_deleted_created
ON orders(user_id, status, deleted, created_at);

设计联合索引时可以参考:

  1. 等值查询字段放前面,比如 user_idstatusdeleted
  2. 排序或范围字段放后面,比如 created_at
  3. 区分度太低的字段不要单独建索引,比如 deleted
  4. 结合真实 SQL,不要凭感觉建索引。

一个常见坑:

CREATE INDEX idx_status_user ON orders(status, user_id);

如果 status 只有待支付、已支付、已取消几个值,区分度很低,放在最前面通常不如 user_id 合适。


六、索引失效的高频坑#

这些写法在面试和真实开发里都很常见。

1. 函数包住字段#

SELECT *
FROM orders
WHERE DATE(created_at) = '2026-03-06';

更推荐:

SELECT *
FROM orders
WHERE created_at >= '2026-03-06 00:00:00'
AND created_at < '2026-03-07 00:00:00';

2. 左模糊查询#

SELECT *
FROM user
WHERE username LIKE '%san';

如果业务允许,改成前缀查询更容易利用索引:

SELECT *
FROM user
WHERE username LIKE 'zhang%';

3. 字符串不加引号#

假设 phoneVARCHAR

SELECT *
FROM user
WHERE phone = 13800000000;

更推荐:

SELECT *
FROM user
WHERE phone = '13800000000';

4. 联合索引的最左匹配原则#

索引是:

CREATE INDEX idx_user_status_created
ON orders(user_id, status, created_at);

这个查询通常更容易命中:

SELECT *
FROM orders
WHERE user_id = 1001
AND status = 1;

这个查询就不一定能充分利用:

SELECT *
FROM orders
WHERE status = 1;

七、EXPLAIN 看什么#

优化 SQL 前,先用 EXPLAIN 看执行计划:

EXPLAIN
SELECT id, order_no, total_amount
FROM orders
WHERE user_id = 1001
AND status = 1
ORDER BY created_at DESC
LIMIT 20;

重点看这些字段:

字段重点
type访问类型,ALL 通常表示全表扫描
possible_keys理论上可能用到的索引
key实际使用的索引
rows预计扫描行数
Extra是否出现 Using filesortUsing temporary

常见 type 从差到好大致是:

ALL -> index -> range -> ref -> const

看到 ALL 不一定马上加索引,但大表上出现 ALL 要重点检查。

看到 Using filesort 也不一定是灾难,但如果数据量大、接口慢,就要考虑索引是否能同时满足过滤和排序。


八、Buffer Pool:为什么内存很重要#

InnoDB 不会每次查询都直接读磁盘,它会把数据页和索引页缓存在 Buffer Pool 里。

可以简单理解为:

应用查询数据
先看 Buffer Pool 有没有
有:直接从内存读
没有:从磁盘读入 Buffer Pool

所以 MySQL 性能很大程度上依赖内存命中率。

这也解释了为什么同一条 SQL:

  • 第一次查可能慢。
  • 第二次查可能快。

因为第二次可能命中了缓存。


九、redo log、undo log、binlog#

MySQL 日志很多,最常见的是 redo log、undo log、binlog。

日志所属作用
redo logInnoDB保证崩溃恢复
undo logInnoDB支持回滚和 MVCC
binlogMySQL Server主从复制、数据恢复

redo log#

redo log 记录“数据页做了什么修改”。

事务提交时,MySQL 不一定立刻把所有数据页刷到磁盘,但会先写 redo log。这样即使突然宕机,重启后也可以根据 redo log 恢复已提交的数据。

这就是 WAL:

Write Ahead Log:先写日志,再写数据页

undo log#

undo log 记录数据修改前的旧版本。

它有两个作用:

  • 事务回滚时,可以恢复旧数据。
  • MVCC 查询时,可以读取旧版本数据。

比如事务把余额从 100 改成 200,undo log 会保留旧值 100。事务回滚时,就能恢复回 100。

binlog#

binlog 记录数据库层面的写操作,常用于:

  • 主从复制。
  • 数据恢复。
  • 审计变更。

主库执行更新后,binlog 会记录下来,从库再重放这条变更。


十、一次 UPDATE 背后发生了什么#

例如:

UPDATE product
SET stock = stock - 1
WHERE id = 100
AND stock > 0;

大致过程可以理解为:

  1. 根据主键或索引找到对应数据页。
  2. 如果数据页不在 Buffer Pool,就从磁盘读入内存。
  3. 对记录加锁,防止并发修改冲突。
  4. 写 undo log,方便回滚。
  5. 修改内存中的数据页。
  6. 写 redo log,保证崩溃恢复。
  7. 写 binlog,用于复制和恢复。
  8. 事务提交,释放锁。

十一、锁和死锁#

InnoDB 支持行锁,但行锁不是绝对只锁一行。是否能精准锁行,和索引命中有关。

例如:

UPDATE orders
SET status = 2
WHERE id = 1001;

如果 id 是主键,通常只锁对应记录。

但如果条件没有索引:

UPDATE orders
SET status = 2
WHERE order_no = 'A202603060001';

如果 order_no 没有索引,MySQL 可能扫描更多数据,锁范围也可能扩大。

死锁例子#

两个事务加锁顺序不一致,就可能死锁:

事务A:先锁订单1,再锁订单2
事务B:先锁订单2,再锁订单1

结果:

事务A等事务B释放订单2
事务B等事务A释放订单1

减少死锁的建议:

  • 多个事务按固定顺序访问资源。
  • SQL 条件尽量命中索引。
  • 事务里不要做太多耗时操作。
  • 锁住数据后尽快提交。
  • 批量更新时控制批次大小。

查看最近一次死锁信息:

SHOW ENGINE INNODB STATUS;

十二、并发例子(库存问题)#

扣库存是 MySQL 里很经典的并发场景。

不推荐先查再改:

SELECT stock
FROM product
WHERE id = 100;
UPDATE product
SET stock = stock - 1
WHERE id = 100;

并发下,多个请求可能都看到库存还有,然后一起扣,导致超卖。

更稳的写法:

UPDATE product
SET stock = stock - 1
WHERE id = 100
AND stock > 0;

然后判断影响行数:

影响行数 = 1:扣减成功
影响行数 = 0:库存不足

这个写法把“判断库存”和“扣减库存”放到一条 SQL 里,能减少并发问题。


十三、订单列表索引优化例子#

假设订单表有 500 万数据,接口 SQL 是:

SELECT id, order_no, total_amount, status, created_at
FROM orders
WHERE user_id = 1001
AND deleted = 0
ORDER BY created_at DESC
LIMIT 20;

如果只有单列索引:

CREATE INDEX idx_user_id ON orders(user_id);

MySQL 可以先筛出用户订单,但排序可能还要额外处理。

更贴合这个查询的联合索引:

CREATE INDEX idx_user_deleted_created
ON orders(user_id, deleted, created_at);

这样索引同时服务于:

  • user_id 过滤。
  • deleted 过滤。
  • created_at 排序。

如果列表只返回这些字段,还可以考虑覆盖索引:

CREATE INDEX idx_order_list
ON orders(user_id, deleted, created_at, id, order_no, total_amount, status);

但覆盖索引不是越长越好。索引字段越多,写入和维护成本越高。是否值得,要看这个接口的调用频率和性能瓶颈。


十四、深分页优化例子#

传统分页:

SELECT id, title, created_at
FROM article
ORDER BY id
LIMIT 100000, 20;

这表示 MySQL 要跳过前 100000 条,再取 20 条。页数越深越慢。

如果业务能接受“下一页”模式,可以改成游标分页:

SELECT id, title, created_at
FROM article
WHERE id > 100000
ORDER BY id
LIMIT 20;

接口返回时带上最后一条 id,下一页继续用这个 id 查询。

这种方式不适合所有场景,但对信息流、评论列表、后台日志很常用。


十五、MySQL 常用排查命令#

查看表结构:

DESC orders;

查看建表语句:

SHOW CREATE TABLE orders;

查看索引:

SHOW INDEX FROM orders;

查看当前事务隔离级别:

SELECT @@transaction_isolation;

查看慢查询是否开启:

SHOW VARIABLES LIKE 'slow_query_log';

查看慢查询阈值:

SHOW VARIABLES LIKE 'long_query_time';

查看 InnoDB 状态:

SHOW ENGINE INNODB STATUS;

Share

If this article helped you, please share it with others!

MySQL实战索引、日志与优化
https://mizuki.mysqil.com/posts/mysql/
Author
梦幻晨风
Published at
2024-03-17
License
CC BY-NC-SA 4.0

Some information may be outdated

Table of Contents