【MySQL全面教学】MySQL性能优化实战Day13(2026年)


写在前面

大家好,欢迎来到MySQL全面教学系列的第13天。经过了前面12天的学习,我们已经掌握了MySQL的方方面面。今天,我们要进入一个实战性极强 的话题------性能优化

性能优化是MySQL面试的重中之重,也是实际工作中最常遇到的挑战。很多开发者面对慢查询时手足无措,不知道从何下手。今天,我将带你掌握一套完整的性能优化方法论,从发现问题到解决问题,让你在面对性能问题时胸有成竹。

让我们一起开启性能优化之旅!


目录

    • 写在前面
    • 一、性能优化方法论
      • [1.1 性能优化的完整流程](#1.1 性能优化的完整流程)
      • [1.2 优化的层次](#1.2 优化的层次)
      • [1.3 优化原则](#1.3 优化原则)
    • 二、慢查询日志
      • [2.1 开启慢查询日志](#2.1 开启慢查询日志)
      • [2.2 慢查询日志格式](#2.2 慢查询日志格式)
      • [2.3 使用pt-query-digest分析](#2.3 使用pt-query-digest分析)
      • [2.4 使用mysqldumpslow(内置工具)](#2.4 使用mysqldumpslow(内置工具))
    • 三、SQL优化技巧
      • [3.1 避免SELECT *](#3.1 避免SELECT *)
      • [3.2 小表驱动大表](#3.2 小表驱动大表)
      • [3.3 批量操作代替单条](#3.3 批量操作代替单条)
      • [3.4 避免在WHERE中对字段进行运算](#3.4 避免在WHERE中对字段进行运算)
      • [3.5 避免隐式类型转换](#3.5 避免隐式类型转换)
      • [3.6 用UNION ALL代替UNION](#3.6 用UNION ALL代替UNION)
      • [3.7 优化LIMIT分页](#3.7 优化LIMIT分页)
    • 四、索引优化
      • [4.1 覆盖索引](#4.1 覆盖索引)
      • [4.2 最左前缀原则](#4.2 最左前缀原则)
      • [4.3 索引下推(ICP)](#4.3 索引下推(ICP))
      • [4.4 索引失效场景](#4.4 索引失效场景)
      • [4.5 索引优化实战](#4.5 索引优化实战)
    • 五、表结构优化
      • [5.1 字段类型选择](#5.1 字段类型选择)
      • [5.2 垂直拆分](#5.2 垂直拆分)
      • [5.3 水平拆分](#5.3 水平拆分)
      • [5.4 分区表](#5.4 分区表)
    • 六、连接池优化
      • [6.1 为什么需要连接池](#6.1 为什么需要连接池)
      • [6.2 HikariCP参数配置](#6.2 HikariCP参数配置)
      • [6.3 连接池大小设置](#6.3 连接池大小设置)
    • 七、实战:一个慢查询的优化过程
    • 八、踩坑提醒
      • [8.1 过早优化是万恶之源](#8.1 过早优化是万恶之源)
      • [8.2 先定位瓶颈再优化](#8.2 先定位瓶颈再优化)
      • [8.3 索引不是越多越好](#8.3 索引不是越多越好)
      • [8.4 EXPLAIN的坑](#8.4 EXPLAIN的坑)
    • 九、面试高频考点
      • [9.1 如何定位慢SQL?](#9.1 如何定位慢SQL?)
      • [9.2 大表分页怎么优化?](#9.2 大表分页怎么优化?)
      • [9.3 如何分析SQL执行计划?](#9.3 如何分析SQL执行计划?)
      • [9.4 什么情况下索引会失效?](#9.4 什么情况下索引会失效?)
      • [9.5 如何优化COUNT(*)查询?](#9.5 如何优化COUNT(*)查询?)
      • [9.6 连接池大小怎么设置?](#9.6 连接池大小怎么设置?)
    • 十、总结
    • 十一、下一步预告
    • 参考资料
    • 互动话题

一、性能优化方法论

1.1 性能优化的完整流程

性能优化不是盲目地修改代码,而是要有科学的方法论:

复制代码
┌─────────────────────────────────────────────────────────────┐
│                    性能优化流程图                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   1. 发现问题 ──→ 2. 定位瓶颈 ──→ 3. 分析原因 ──→ 4. 优化实施  │
│        ↑                                              │      │
│        └──────────────── 5. 效果验证 ←───────────────┘      │
│                                                             │
└─────────────────────────────────────────────────────────────┘

1.2 优化的层次

优化层次 优化内容 效果 难度
SQL层 SQL语句优化、索引优化 显著
表结构层 字段类型、表拆分 明显
架构层 读写分离、分库分表 显著
系统层 服务器配置、OS参数 中等

1.3 优化原则

sql 复制代码
-- 经验之谈:性能优化的黄金法则

-- 1. 先定位,再优化
-- 不要凭感觉优化,要有数据支撑

-- 2. 先优化SQL,再优化架构
-- 80%的性能问题可以通过SQL优化解决

-- 3. 小步快跑,持续验证
-- 每次只改一处,验证效果后再继续

-- 4. 监控先行
-- 没有监控的优化是盲目的

-- 5. 预防胜于治疗
-- 在开发阶段就考虑性能

二、慢查询日志

2.1 开启慢查询日志

慢查询日志是发现性能问题的第一道防线。

sql 复制代码
-- 查看慢查询日志配置
SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

-- 开启慢查询日志(临时)
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/lib/mysql/slow.log';
SET GLOBAL long_query_time = 1;  -- 超过1秒的查询记录

-- 永久配置(my.cnf)
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 1
log_queries_not_using_indexes = 1  -- 记录未使用索引的查询

2.2 慢查询日志格式

复制代码
# Time: 2024-01-15T10:30:45.123456Z
# User@Host: root[root] @ localhost []  Id:    15
# Query_time: 2.345678  Lock_time: 0.000123 Rows_sent: 10  Rows_examined: 1000000
SET timestamp=1705314645;
SELECT * FROM orders WHERE create_time > '2024-01-01';

字段说明:

字段 说明
Time 查询执行时间
User@Host 执行用户和主机
Query_time 查询总耗时(秒)
Lock_time 等待锁的时间
Rows_sent 返回的行数
Rows_examined 扫描的行数

2.3 使用pt-query-digest分析

pt-query-digest是Percona Toolkit中的慢查询分析工具,功能强大。

bash 复制代码
# 安装Percona Toolkit
yum install percona-toolkit

# 分析慢查询日志
pt-query-digest /var/lib/mysql/slow.log

# 输出到文件
pt-query-digest /var/lib/mysql/slow.log > slow_query_report.txt

# 分析最近1小时的慢查询
pt-query-digest --since 1h /var/lib/mysql/slow.log

# 只显示最慢的10条
pt-query-digest --limit 10 /var/lib/mysql/slow.log

pt-query-digest输出示例:

复制代码
# 总体统计
# 110.5s user time, 2.3s system time, 25.56M rss, 78.34M vsz
# Current date: Mon Jan 15 10:35:00 2024
# Hostname: mysql-server
# Files: /var/lib/mysql/slow.log
# Overall: 1.23k total, 45 unique, 2.45 QPS, 0.35x concurrency

# 查询排名
# Rank Query ID           Response time  Calls R/Call V/M   Item
# ==== ================== ============== ===== ====== ===== ====
#    1 0x3A...            125.3450 45.2%   234 0.5356  0.12 SELECT orders
#    2 0x8B...             78.2340 28.3%   156 0.5015  0.08 SELECT users

2.4 使用mysqldumpslow(内置工具)

bash 复制代码
# 查看最慢的10条SQL
mysqldumpslow -s t -t 10 /var/lib/mysql/slow.log

# 查看访问次数最多的10条SQL
mysqldumpslow -s c -t 10 /var/lib/mysql/slow.log

# 参数说明
# -s: 排序方式 (t:时间, c:次数, l:锁定时间, r:返回记录数)
# -t: 显示前N条
# -g: 正则匹配

三、SQL优化技巧

3.1 避免SELECT *

sql 复制代码
-- 不推荐:查询所有列
SELECT * FROM employees WHERE department_id = 10;

-- 推荐:只查询需要的列
SELECT employee_id, first_name, last_name, email 
FROM employees 
WHERE department_id = 10;

-- 原因:
-- 1. 减少网络传输
-- 2. 减少内存使用
-- 3. 可能使用覆盖索引

3.2 小表驱动大表

sql 复制代码
-- 不推荐:大表驱动小表
SELECT * FROM huge_table h
INNER JOIN small_table s ON h.id = s.huge_id
WHERE s.status = 'ACTIVE';

-- 推荐:小表驱动大表(使用STRAIGHT_JOIN强制顺序)
SELECT * FROM small_table s
STRAIGHT_JOIN huge_table h ON h.id = s.huge_id
WHERE s.status = 'ACTIVE';

-- 或者确保优化器选择正确的驱动表
SELECT * FROM small_table s
INNER JOIN huge_table h ON h.id = s.huge_id
WHERE s.status = 'ACTIVE';

3.3 批量操作代替单条

sql 复制代码
-- 不推荐:循环插入
INSERT INTO logs (message) VALUES ('log1');
INSERT INTO logs (message) VALUES ('log2');
INSERT INTO logs (message) VALUES ('log3');
-- ... 1000次

-- 推荐:批量插入
INSERT INTO logs (message) VALUES 
('log1'), ('log2'), ('log3'), ...;

-- 推荐:使用LOAD DATA
LOAD DATA INFILE '/tmp/data.csv' 
INTO TABLE logs 
FIELDS TERMINATED BY ',';

-- 批量大小建议:1000-10000条/批次

3.4 避免在WHERE中对字段进行运算

sql 复制代码
-- 不推荐:对字段进行运算,无法使用索引
SELECT * FROM employees WHERE YEAR(hire_date) = 2023;
SELECT * FROM orders WHERE amount * 0.9 > 1000;

-- 推荐:将运算移到右侧
SELECT * FROM employees 
WHERE hire_date >= '2023-01-01' AND hire_date < '2024-01-01';

SELECT * FROM orders WHERE amount > 1000 / 0.9;

3.5 避免隐式类型转换

sql 复制代码
-- 表结构:phone VARCHAR(20),有索引

-- 不推荐:隐式类型转换,索引失效
SELECT * FROM users WHERE phone = 13800138000;

-- 推荐:类型一致
SELECT * FROM users WHERE phone = '13800138000';

3.6 用UNION ALL代替UNION

sql 复制代码
-- 不推荐:UNION会去重,消耗性能
SELECT name FROM table_a
UNION
SELECT name FROM table_b;

-- 推荐:UNION ALL不去重,性能更好
SELECT name FROM table_a
UNION ALL
SELECT name FROM table_b;

-- 如果确定没有重复数据,一定用UNION ALL

3.7 优化LIMIT分页

sql 复制代码
-- 不推荐:深分页性能差
SELECT * FROM orders 
ORDER BY create_time DESC 
LIMIT 1000000, 10;

-- 推荐1:使用覆盖索引+子查询
SELECT * FROM orders o
INNER JOIN (
    SELECT order_id FROM orders 
    ORDER BY create_time DESC 
    LIMIT 1000000, 10
) tmp ON o.order_id = tmp.order_id;

-- 推荐2:使用游标/书签(上一页的最大ID)
SELECT * FROM orders 
WHERE create_time < '2024-01-15 10:00:00'
ORDER BY create_time DESC 
LIMIT 10;

-- 推荐3:使用冗余字段优化
-- 添加page_num字段,按页查询

四、索引优化

4.1 覆盖索引

覆盖索引:查询的所有列都在索引中,无需回表查询。

sql 复制代码
-- 创建复合索引
CREATE INDEX idx_employee_dept_name ON employees(department_id, first_name, last_name);

-- 覆盖索引查询(Extra: Using index)
SELECT first_name, last_name 
FROM employees 
WHERE department_id = 10;

-- 非覆盖索引查询(需要回表)
SELECT first_name, last_name, email  -- email不在索引中
FROM employees 
WHERE department_id = 10;

4.2 最左前缀原则

sql 复制代码
-- 创建复合索引 (a, b, c)
CREATE INDEX idx_abc ON table_name(a, b, c);

-- 可以使用索引的情况
WHERE a = 1
WHERE a = 1 AND b = 2
WHERE a = 1 AND b = 2 AND c = 3
WHERE a = 1 AND c = 3  -- a使用索引,c不使用
WHERE a = 1 AND b > 2 AND c = 3  -- a,b使用索引,c不使用

-- 不能使用索引的情况
WHERE b = 2
WHERE b = 2 AND c = 3
WHERE c = 3

4.3 索引下推(ICP)

MySQL 5.6+引入的优化,将WHERE条件下推到存储引擎层过滤。

sql 复制代码
-- 索引:(name, age)
SELECT * FROM users WHERE name LIKE '张%' AND age = 20;

-- 无ICP:先根据name找到所有记录,再回表过滤age
-- 有ICP:在存储引擎层就用age过滤,减少回表次数

查看ICP是否开启:

sql 复制代码
SHOW VARIABLES LIKE 'optimizer_switch';
-- 确保index_condition_pushdown=on

4.4 索引失效场景

sql 复制代码
-- 1. 使用函数或表达式
WHERE YEAR(create_time) = 2023  -- 失效
WHERE create_time >= '2023-01-01'  -- 有效

-- 2. 隐式类型转换
WHERE phone = 13800138000  -- 失效(字符串字段用数字查)
WHERE phone = '13800138000'  -- 有效

-- 3. LIKE以通配符开头
WHERE name LIKE '%张%'  -- 失效
WHERE name LIKE '张%'   -- 有效

-- 4. OR条件使用不当
WHERE id = 1 OR age = 20  -- 可能失效(除非都有索引)

-- 5. 不等于、NOT IN
WHERE status != 1  -- 可能失效
WHERE status NOT IN (1, 2)  -- 可能失效

-- 6. IS NULL / IS NOT NULL
WHERE name IS NULL  -- 可能失效(取决于数据分布)

-- 7. 范围查询后失效
-- 索引(a, b, c)
WHERE a = 1 AND b > 2 AND c = 3  -- c不使用索引

4.5 索引优化实战

sql 复制代码
-- 场景:优化订单查询
-- 原表:orders(order_id, user_id, status, create_time, amount)

-- 需求1:根据用户查询订单
CREATE INDEX idx_user_id ON orders(user_id);

-- 需求2:根据状态和创建时间查询
CREATE INDEX idx_status_time ON orders(status, create_time);

-- 需求3:根据用户和状态查询(覆盖索引)
CREATE INDEX idx_user_status ON orders(user_id, status, amount, create_time);

-- 查询示例
SELECT amount, create_time 
FROM orders 
WHERE user_id = 100 AND status = 1;
-- 使用覆盖索引,无需回表

五、表结构优化

5.1 字段类型选择

数据类型 推荐用法 不推荐
INT 整数ID、状态码 不要用INT存手机号
BIGINT 大整数、时间戳 小数据量不要用
VARCHAR 变长字符串 长度固定用CHAR
CHAR 固定长度(MD5、手机号) 变长数据不要用
DECIMAL 精确小数(金额) 不要用FLOAT/DOUBLE
DATETIME 日期时间 时间戳用INT/BIGINT
TIMESTAMP 记录时间(自动更新) 范围有限(1970-2038)
JSON 灵活结构数据 不要存大量JSON
sql 复制代码
-- 经验之谈:字段类型选择原则

-- 1. 能用INT不用BIGINT(节省空间)
-- 2. 金额用DECIMAL(19,4),不要用FLOAT
-- 3. 状态用TINYINT,不要用VARCHAR
-- 4. 定长用CHAR,变长用VARCHAR
-- 5. 大文本用TEXT,不要存在主表

5.2 垂直拆分

将宽表拆分成多个窄表,减少IO。

sql 复制代码
-- 原表:users(user_id, name, email, phone, address, profile, settings, ...)
-- 拆分后:

-- 主表:频繁访问的核心字段
CREATE TABLE users (
    user_id BIGINT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    email VARCHAR(100) NOT NULL,
    phone VARCHAR(20),
    status TINYINT DEFAULT 1,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 扩展表:不常访问的大字段
CREATE TABLE user_profiles (
    user_id BIGINT PRIMARY KEY,
    address TEXT,
    bio TEXT,
    avatar_url VARCHAR(500),
    FOREIGN KEY (user_id) REFERENCES users(user_id)
);

-- 设置表:用户配置
CREATE TABLE user_settings (
    user_id BIGINT PRIMARY KEY,
    notification_enabled TINYINT DEFAULT 1,
    theme VARCHAR(20) DEFAULT 'light',
    FOREIGN KEY (user_id) REFERENCES users(user_id)
);

5.3 水平拆分

将大表按某种规则拆分成多个小表。

sql 复制代码
-- 按用户ID取模拆分
-- users_0, users_1, users_2, users_3

-- 获取表名
SET @table_index = user_id % 4;
SET @sql = CONCAT('SELECT * FROM users_', @table_index, ' WHERE user_id = ?');
PREPARE stmt FROM @sql;
EXECUTE stmt USING @user_id;
DEALLOCATE PREPARE stmt;

-- 或者使用分库分表中间件:ShardingSphere、MyCat

5.4 分区表

MySQL分区表将大表在物理上分成多个小文件。

sql 复制代码
-- 按范围分区
CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY,
    user_id BIGINT NOT NULL,
    amount DECIMAL(10,2),
    create_time DATETIME NOT NULL
) PARTITION BY RANGE (YEAR(create_time)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION pfuture VALUES LESS THAN MAXVALUE
);

-- 按HASH分区
CREATE TABLE logs (
    log_id BIGINT PRIMARY KEY,
    message TEXT,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) PARTITION BY HASH(log_id) PARTITIONS 4;

-- 查询只访问特定分区(分区裁剪)
SELECT * FROM orders WHERE create_time >= '2023-01-01' AND create_time < '2024-01-01';
-- 只扫描p2023分区

六、连接池优化

6.1 为什么需要连接池

数据库连接是昂贵的资源,频繁创建销毁会严重影响性能。连接池可以复用连接,减少开销。

6.2 HikariCP参数配置

HikariCP是Java生态中性能最好的连接池。

properties 复制代码
# HikariCP配置示例
# 基础配置
spring.datasource.hikari.jdbc-url=jdbc:mysql://localhost:3306/mydb
spring.datasource.hikari.username=root
spring.datasource.hikari.password=password
spring.datasource.hikari.driver-class-name=com.mysql.cj.jdbc.Driver

# 连接池大小(黄金公式:connections = ((core_count * 2) + effective_spindle_count))
spring.datasource.hikari.maximum-pool-size=20
spring.datasource.hikari.minimum-idle=10

# 连接超时
spring.datasource.hikari.connection-timeout=30000

# 空闲连接超时
spring.datasource.hikari.idle-timeout=600000

# 连接最大生命周期
spring.datasource.hikari.max-lifetime=1800000

# 连接测试
spring.datasource.hikari.connection-test-query=SELECT 1

6.3 连接池大小设置

复制代码
连接池大小计算公式(来自PostgreSQL建议,同样适用于MySQL):

connections = ((core_count * 2) + effective_spindle_count)

示例:
- 4核CPU,单块SSD
- connections = (4 * 2) + 1 = 9

实际建议:
- 小型应用:10-20
- 中型应用:20-50
- 大型应用:50-100(配合读写分离)

注意:连接数不是越大越好!

七、实战:一个慢查询的优化过程

7.1 问题发现

sql 复制代码
-- 业务反馈:订单查询页面加载很慢,需要10秒以上
-- 慢查询日志发现:

SELECT 
    o.order_id,
    o.user_id,
    o.amount,
    o.status,
    o.create_time,
    u.username,
    u.email,
    p.product_name,
    p.price
FROM orders o
LEFT JOIN users u ON o.user_id = u.user_id
LEFT JOIN order_items oi ON o.order_id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.product_id
WHERE o.create_time > '2023-01-01'
ORDER BY o.create_time DESC
LIMIT 10000, 10;

-- Query_time: 10.234567
-- Rows_examined: 5000000

7.2 问题分析

使用EXPLAIN分析执行计划:

sql 复制代码
EXPLAIN SELECT ...;

-- 输出:
-- id | select_type | table | type  | possible_keys | key  | rows    | Extra
-- 1  | SIMPLE      | o     | ALL   | NULL          | NULL | 5000000 | Using where; Using filesort
-- 1  | SIMPLE      | u     | eq_ref| PRIMARY       | PRI  | 1       | NULL
-- 1  | SIMPLE      | oi    | ref   | order_id      | ord  | 3       | NULL
-- 1  | SIMPLE      | p     | eq_ref| PRIMARY       | PRI  | 1       | NULL

问题诊断:

  1. orders表全表扫描(type=ALL)
  2. 没有使用索引
  3. 使用filesort排序
  4. 深分页LIMIT 10000, 10

7.3 优化过程

第一步:添加索引
sql 复制代码
-- 为create_time添加索引
CREATE INDEX idx_create_time ON orders(create_time);

-- 效果:type从ALL变为range,但仍有filesort
第二步:优化分页
sql 复制代码
-- 使用覆盖索引+子查询
SELECT 
    o.order_id,
    o.user_id,
    o.amount,
    o.status,
    o.create_time,
    u.username,
    u.email
FROM (
    SELECT order_id, user_id, amount, status, create_time
    FROM orders
    WHERE create_time > '2023-01-01'
    ORDER BY create_time DESC
    LIMIT 10000, 10
) o
LEFT JOIN users u ON o.user_id = u.user_id;

-- 订单详情单独查询(避免JOIN过多)
第三步:使用书签分页
sql 复制代码
-- 前端记录上一页最后一条的create_time
-- 第一次查询
SELECT order_id, create_time 
FROM orders 
WHERE create_time > '2023-01-01'
ORDER BY create_time DESC 
LIMIT 10;
-- 返回最后一条create_time = '2023-12-01 10:00:00'

-- 下一页查询
SELECT order_id, create_time 
FROM orders 
WHERE create_time < '2023-12-01 10:00:00'  -- 使用书签
ORDER BY create_time DESC 
LIMIT 10;

7.4 最终优化方案

sql 复制代码
-- 1. 创建复合索引
CREATE INDEX idx_ct_uid ON orders(create_time, user_id);

-- 2. 优化后的查询
SELECT 
    o.order_id,
    o.user_id,
    o.amount,
    o.status,
    o.create_time,
    u.username,
    u.email
FROM orders o
INNER JOIN users u ON o.user_id = u.user_id
WHERE o.create_time > '2023-01-01'
  AND o.create_time < '2024-01-01'
ORDER BY o.create_time DESC
LIMIT 100;

-- 3. 分页优化(使用书签)
-- 前端维护create_time游标

7.5 优化效果

指标 优化前 优化后 提升
查询时间 10.2秒 10毫秒 1000倍
扫描行数 500万 100 50000倍
使用索引 -
回表次数 500万 100 -

八、踩坑提醒

8.1 过早优化是万恶之源

sql 复制代码
-- 经验之谈:不要过度优化

-- 1. 先让系统跑起来,再考虑优化
-- 2. 优化要有数据支撑,不要凭感觉
-- 3. 80/20法则:优化最关键的20%代码

-- 反例:为了"优化"而写的复杂SQL
SELECT * FROM (
    SELECT * FROM (
        SELECT ... FROM ...
    ) t1
) t2;
-- 可能还不如简单查询快

8.2 先定位瓶颈再优化

sql 复制代码
-- 使用SHOW PROFILE定位瓶颈
SET profiling = 1;

-- 执行查询
SELECT * FROM large_table WHERE ...;

-- 查看分析结果
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;

-- 输出:
-- Starting: 0.000010
-- Opening tables: 0.000005
-- System lock: 0.000003
-- Table lock: 0.000004
-- init: 0.000015
-- optimizing: 0.000008
-- statistics: 0.000050  <-- 这里耗时多,可能是统计信息不准
-- preparing: 0.000012
-- executing: 0.000002
-- Sending data: 2.345678  <-- 主要耗时在这里
-- ...

8.3 索引不是越多越好

sql 复制代码
-- 每个索引都会增加写操作的开销
-- 索引过多会导致:
-- 1. INSERT/UPDATE/DELETE变慢
-- 2. 占用更多磁盘空间
-- 3. 优化器选择困难

-- 建议:
-- 1. 单表索引不超过5个
-- 2. 复合索引字段不超过3个
-- 3. 定期清理无用索引

-- 查找无用索引
SELECT 
    t.TABLE_SCHEMA,
    t.TABLE_NAME,
    s.INDEX_NAME,
    s.CARDINALITY,
    t.TABLE_ROWS,
    ROUND(s.CARDINALITY / t.TABLE_ROWS, 2) AS selectivity
FROM information_schema.STATISTICS s
JOIN information_schema.TABLES t ON s.TABLE_SCHEMA = t.TABLE_SCHEMA 
    AND s.TABLE_NAME = t.TABLE_NAME
WHERE t.TABLE_SCHEMA = 'your_db'
AND s.CARDINALITY / t.TABLE_ROWS < 0.1  -- 选择性低于10%
ORDER BY selectivity;

8.4 EXPLAIN的坑

sql 复制代码
-- EXPLAIN显示的行数是估算值,不是实际扫描行数
-- 要以实际执行为准

-- 使用EXPLAIN ANALYZE(MySQL 8.0.18+)获取实际执行信息
EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 100;

-- 或者使用SHOW STATUS查看实际行数
FLUSH STATUS;
SELECT * FROM orders WHERE user_id = 100;
SHOW STATUS LIKE 'Handler_read%';

九、面试高频考点

9.1 如何定位慢SQL?

答案:

  1. 慢查询日志:开启slow_query_log,设置long_query_time
  2. SHOW PROCESSLIST:查看正在执行的慢查询
  3. performance_schema:使用events_statements_history_long表
  4. 业务监控:应用层记录慢查询API
  5. pt-query-digest:分析慢查询日志

9.2 大表分页怎么优化?

答案:

  1. 覆盖索引+子查询:先查ID,再关联详情
  2. 书签/游标分页:记录上一页边界值
  3. 限制分页深度:只允许翻到100页以内
  4. 使用搜索引擎:Elasticsearch等
  5. 延迟关联:先分页取ID,再JOIN取详情

9.3 如何分析SQL执行计划?

答案:

使用EXPLAIN查看执行计划,关注以下字段:

  • type:访问类型,从好到差:system > const > eq_ref > ref > range > index > ALL
  • key:实际使用的索引
  • rows:估算扫描行数
  • Extra:额外信息,关注Using filesort、Using temporary、Using index

9.4 什么情况下索引会失效?

答案:

  1. 对索引列使用函数或表达式
  2. 隐式类型转换
  3. LIKE以%开头
  4. OR条件使用不当
  5. 不等于(!=、<>)和NOT IN
  6. IS NULL(取决于数据分布)
  7. 复合索引不满足最左前缀

9.5 如何优化COUNT(*)查询?

答案:

  1. 使用近似值:SHOW TABLE STATUS
  2. 使用缓存:Redis缓存计数
  3. 使用汇总表:定时统计写入
  4. 使用覆盖索引:COUNT(索引列)
  5. 限制条件:先过滤再COUNT

9.6 连接池大小怎么设置?

答案:

公式:connections = ((core_count * 2) + effective_spindle_count)

实际建议:

  • 4核8G:10-20连接
  • 8核16G:20-50连接
  • 配合读写分离:每个实例50-100连接

注意:连接数不是越大越好,过多会导致CPU上下文切换开销。


十、总结

今天我们学习了MySQL性能优化的完整方法论:

核心知识点

  1. 性能优化流程:发现问题 → 定位瓶颈 → 分析原因 → 优化实施 → 效果验证

  2. 慢查询分析

    • 开启慢查询日志
    • 使用pt-query-digest分析
    • 关注Query_time、Rows_examined
  3. SQL优化技巧

    • 避免SELECT *
    • 小表驱动大表
    • 批量操作代替单条
    • 优化深分页
  4. 索引优化

    • 覆盖索引
    • 最左前缀原则
    • 避免索引失效
  5. 表结构优化

    • 合理选择字段类型
    • 垂直拆分
    • 水平拆分/分区

优化原则

  • 先定位,再优化:用数据说话
  • 先SQL,再架构:80%问题在SQL层
  • 小步快跑:每次只改一处,持续验证
  • 监控先行:没有监控的优化是盲目的

十一、下一步预告

Day14 - MySQL备份与恢复

在下一篇文章中,我们将学习:

  • 备份的重要性(删库跑路的故事)
  • mysqldump和xtrabackup的使用
  • 主从复制架构
  • 数据恢复实战
  • 制定完善的备份策略

数据安全是DBA的生命线,备份恢复是必备技能,敬请期待!


参考资料

MySQL 8.0 Reference Manual - Optimization


互动话题

  1. 你在工作中遇到过哪些性能问题?是如何解决的?
  2. 对于深分页问题,你还有什么好的解决方案?
  3. 分享一个你优化SQL的成功案例,优化前后性能提升了多少?

欢迎在评论区留言讨论,如果觉得文章有帮助,别忘了点赞收藏哦!我们下节课见!


本文是MySQL全面教学系列第13篇,系列文章持续更新中...

相关推荐
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第84题】【Mysql篇】第14题:为什么用 InnoDB 存储引擎的表建议用整型的自增主键?
java·开发语言·数据库·mysql·面试
小江的记录本1 小时前
【JVM虚拟机】JVM调优:常用JVM参数、调优核心指标、OOM排查、GC日志分析、Arthas工具使用(附《思维导图》+《面试高频考点清单》)
java·jvm·spring boot·后端·python·spring·面试
张彦峰ZYF2 小时前
检索增强生成(RAG)系统的基础:全面深入矢量数据库
数据库·大模型·rag
程序员cxuan2 小时前
我花了两天时间,终于把 Codex 额度掉太快的问题整明白了!!
人工智能·后端·程序员
IT_陈寒2 小时前
Vue这个动态响应坑把我整不会了
前端·人工智能·后端
金銀銅鐵2 小时前
[Java] 用图形化界面演示 iadd, isub, iconst_<i> 指令的效果
java·后端·python
AskHarries2 小时前
做国内还是出海
后端
牧羊狼的狼2 小时前
MySQL 四大索引失效写法 + 业务替代最优解决方案
mysql·索引失效
日月云棠2 小时前
10 Integer —— 最常用的整数包装类深度解析
java·后端