性能优化

1. SQL 执行流程

了解SQL语句的执行流程有助于进行性能优化。以下是MySQL处理SQL查询的完整流程:

flowchart TD
    A[SQL语句] --> B[语法解析器]
    B --> C[语义检查]
    C --> D{查询缓存?}
    
    D -->|命中| E[直接返回缓存结果]
    D -->|未命中| F[查询优化器]
    
    F --> G[生成执行计划]
    G --> H[成本评估]
    H --> I[选择最优计划]
    
    I --> J[执行器]
    J --> K[存储引擎]
    K --> L[读取数据]
    L --> M[返回结果]
    
    M --> N[写入查询缓存]
    
    E --> O[返回客户端]
    N --> O
    
    style A fill:#fff3e0,stroke:#e65100,stroke-width:2px
    style F fill:#e3f2fd,stroke:#1565c0,stroke-width:2px
    style I fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px
    style O fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px

各组件说明:

组件 功能
语法解析器 将SQL解析为语法树,检查语法错误
语义检查 检查表、列是否存在,权限是否足够
查询缓存 缓存查询结果,避免重复执行
查询优化器 生成多种执行计划,选择最优方案
执行器 调用存储引擎执行计划
存储引擎 实际读取/写入数据 (InnoDB/MyISAM)

2. AI 查询优化器

-- 启用 AI 查询优化器
SET GLOBAL optimizer_switch = 'ai_optimization=on';

-- 使用 AI 优化提示
SELECT /*+ AI_OPTIMIZE */ * FROM users WHERE age > 25;

-- 分析查询计划(AI 增强版)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 25;

-- 查看 AI 优化建议
SELECT * FROM sys.ai_query_recommendations
WHERE query_hash = MD5('YOUR_QUERY');

-- 自动应用优化建议
CALL sys.apply_ai_optimizations('YOUR_QUERY_HASH');

2. 查询缓存优化

-- 启用查询缓存(MySQL 9.x 智能缓存)
SET GLOBAL query_cache_type = ON;
SET GLOBAL query_cache_size = 67108864; -- 64MB

-- 监控缓存命中率
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC LIMIT 10;

-- 查看缓存统计
SHOW STATUS LIKE 'Qcache%';
3. SQL 语句优化技巧(点击展开)

3.1 避免全表扫描

-- ❌ 全表扫描(效率低)
SELECT * FROM users WHERE age != 25;
SELECT * FROM users WHERE age NOT IN (20, 30);

-- ✅ 使用索引(效率高)
SELECT * FROM users WHERE age = 25;

-- ❌ 前导通配符(无法使用索引)
SELECT * FROM users WHERE name LIKE '%zhang%';

-- ✅ 后置通配符(可以使用索引)
SELECT * FROM users WHERE name LIKE 'zhang%';

-- ✅ 使用覆盖索引
SELECT name, age FROM users WHERE name LIKE 'zhang%';

3.2 优化 LIMIT 分页

-- ❌ 低效的分页(深度分页问题)
SELECT * FROM orders ORDER BY id LIMIT 100000, 10;
-- MySQL 会先扫描前 100010 条记录,然后丢弃前 100000 条

-- ✅ 优化方案1:基于主键的范围查询
SELECT * FROM orders 
WHERE id > 100000 
ORDER BY id 
LIMIT 10;

-- ✅ 优化方案2:延迟关联
SELECT o.*
FROM orders o
INNER JOIN (
    SELECT id FROM orders
    ORDER BY id
    LIMIT 100000, 10
) t ON o.id = t.id;

-- ✅ 优化方案3:记录上一页最后一条 ID
SELECT * FROM orders 
WHERE id > 100000 
ORDER BY id 
LIMIT 10;
-- 假设上一页最后一条是 id=100000

3.3 避免 SELECT *

-- ❌ SELECT * 会读取所有列,增加 IO
SELECT * FROM orders WHERE customer_id = 100;

-- ✅ 只查询需要的列
SELECT order_no, amount, created_at 
FROM orders WHERE customer_id = 100;

-- ✅ 使用覆盖索引时,效果更明显
CREATE INDEX idx_cover ON orders(customer_id, order_no, amount);
SELECT order_no, amount FROM orders WHERE customer_id = 100;
-- 完全在索引中完成,不需要回表

3.4 合理使用 JOIN

-- ❌ 大表 JOIN 小表(效率低)
SELECT o.*, c.name
FROM orders o  -- 大表
LEFT JOIN customers c ON o.customer_id = c.id;

-- ✅ 使用小表驱动大表(EXPLAIN 中 type 最好)
SELECT o.*, c.name
FROM customers c  -- 小表
LEFT JOIN orders o ON o.customer_id = c.id;

-- 确保 JOIN 条件有索引
CREATE INDEX idx_order_customer ON orders(customer_id);
CREATE INDEX idx_customer_id ON customers(id);

-- 避免多表 JOIN,适当拆分为多个查询

3.5 批量操作优化

-- ❌ 逐条插入(效率低)
INSERT INTO users (name) VALUES ('a');
INSERT INTO users (name) VALUES ('b');
INSERT INTO users (name) VALUES ('c');

-- ✅ 批量插入(效率高)
INSERT INTO users (name) VALUES
    ('a'), ('b'), ('c'), ('d'), ('e');

-- 批量插入建议每次 1000-5000 条

-- 禁用索引加速批量插入
ALTER TABLE users DISABLE KEYS;
INSERT INTO users (name) VALUES ...;  -- 批量插入
ALTER TABLE users ENABLE KEYS;

-- 使用 LOAD DATA INFILE(最快)
LOAD DATA INFILE '/path/to/file.csv'
INTO TABLE users
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n'
(name);

4. 常用配置优化

4.1 缓冲区配置

-- InnoDB 缓冲池大小(建议设置为可用内存的 60-80%)
SET GLOBAL innodb_buffer_pool_size = 4294967296;  -- 4GB

-- 缓冲池实例数量(多核CPU建议设置多个实例)
SET GLOBAL innodb_buffer_pool_instances = 4;

-- 日志文件大小
SET GLOBAL innodb_log_file_size = 1073741824;  -- 1GB

-- 刷新日志频率(平衡性能和数据安全)
SET GLOBAL innodb_flush_log_at_trx_commit = 2;
-- 1: 每次事务刷新(最安全,默认)
-- 2: 每秒刷新(性能好,崩溃可能丢失1秒数据)
-- 0: 不立即刷新(最快,崩溃可能丢失大量数据)

4.2 连接配置

-- 最大连接数
SET GLOBAL max_connections = 500;

-- 连接超时时间
SET GLOBAL wait_timeout = 600;  -- 10分钟
SET GLOBAL interactive_timeout = 600;

-- 线程缓存大小
SET GLOBAL thread_cache_size = 50;

-- 查询缓存大小
SET GLOBAL query_cache_size = 671>;  -- 64MB
SET GLOBAL query_cache_limit = 1048576;  -- 单个查询最大1MB

4.3 临时表和排序缓冲

-- 临时表大小
SET GLOBAL tmp_table_size = 134217728;  -- 128MB
SET GLOBAL max_heap_table_size = 134217728;

-- 排序缓冲大小
SET GLOBAL sort_buffer_size = 4194304;  -- 4MB

-- 读取缓冲大小
SET GLOBAL read_buffer_size = 2097152;  -- 2MB
SET GLOBAL read_rnd_buffer_size = 4194304;  -- 4MB

-- 查看配置
SHOW VARIABLES LIKE '%buffer%';
SHOW VARIABLES LIKE '%size%';

5. 慢查询分析

5.1 开启慢查询日志

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

-- 查看慢查询
SELECT * FROM mysql.slow_log
ORDER BY start_time DESC
LIMIT 10;

-- 使用 mysqldumpslow 工具分析
-- mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
-- -s: 排序方式 (t=时间, c=次数)
-- -t: 显示前N条

5.2 使用 EXPLAIN 分析

-- EXPLAIN 分析查询计划
EXPLAIN SELECT * FROM users WHERE age > 25;

-- 关键字段解读:
-- type: 连接类型
--   system > const > eq_ref > ref > range > index > ALL
--   const/eq_ref/ref/range 都是可接受的
--   ALL 表示全表扫描,需要优化

-- key: 实际使用的索引
-- rows: 预计扫描的行数(越小越好)
-- Extra: 额外信息
--   Using index: 使用覆盖索引
--   Using filesort: 需要额外排序
--   Using temporary: 使用临时表

-- EXPLAIN ANALYZE(实际执行并分析)
EXPLAIN ANALYZE SELECT * FROM users WHERE age > 25;

6. 表结构优化

6.1 选择合适的数据类型

-- ❌ 使用过大的数据类型
CREATE TABLE t1 (
    id INT,           -- 4字节
    phone VARCHAR(20)
);

-- ✅ 使用合适的数据类型
CREATE TABLE t2 (
    id INT UNSIGNED,     -- 无符号,支持更大范围
    status TINYINT,        -- 1字节,范围 -128~127
    age TINYINT UNSIGNED, -- 0-255
    views BIGINT UNSIGNED,  -- 大访问量
    price DECIMAL(10,2)  -- 精确价格
);

-- 使用 ENUM 代替字符串
CREATE TABLE orders (
    status ENUM('pending', 'processing', 'completed', 'cancelled')
);

-- 使用 NOT NULL
-- NULL 需要额外空间存储,且影响索引效率

6.2 垂直拆分和水平拆分

-- 垂直拆分:将大表拆分为多个小表
-- 原始表:users (id, name, email, avatar, bio, ...) 字段很多

-- 拆分后:
CREATE TABLE users (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    email VARCHAR(100)
);

CREATE TABLE user_profiles (
    user_id INT PRIMARY KEY,
    avatar VARCHAR(255),
    bio TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

-- 水平拆分:按数据范围或哈希分表
-- 按月份分表(适用于日志、订单等)
CREATE TABLE orders_202401 (
    LIKE orders
);

CREATE TABLE orders_202402 (
    LIKE orders
);

-- 使用分区表(MySQL 原生支持)
CREATE TABLE orders_partitioned (
    id INT,
    created_at DATE,
    amount DECIMAL(10,2)
) PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);

7. 读写分离与负载均衡

sequenceDiagram
    participant App as 应用服务器
    participant Master as 主库 (Master)
    participant Slave1 as 从库 (Slave1)
    participant Slave2 as 从库 (Slave2)
    
    App->>Master: 1. INSERT/UPDATE/DELETE
    Master->>Master: 写入binlog
    Master-->>App: 返回成功
    
    par 异步复制
        Master->>Slave1: 2. 传输binlog
        Master->>Slave2: 2. 传输binlog
    end
    
    Slave1->>Slave1: 3. 重放binlog
    Slave2->>Slave2: 3. 重放binlog
    
    Note over App,Master: 读写分离: 写操作→主库, 读操作→从库
    
    App->>Slave1: 4. SELECT查询
    Slave1-->>App: 返回查询结果
    
    App->>Slave2: 5. SELECT查询
    Slave2-->>App: 返回查询结果

主从复制原理:

步骤 主库操作 从库操作
1. 日志记录 将写操作记录到 binlog -
2. 日志传输 将 binlog 发送到从库 接收 binlog
3. 日志重放 - 执行 binlog 中的操作
4. 同步完成 - 更新 relay log 位置

7.1 主从复制配置

-- 主库配置
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-do-db = myapp

-- 从库配置
[mysqld]
server-id = 2
relay-log = relay-bin
read-only = 1

-- 从库设置
CHANGE MASTER TO
    MASTER_HOST='master_host',
    MASTER_USER='repl_user',
    MASTER_PASSWORD='password',
    MASTER_LOG_FILE='mysql-bin.000001',
    MASTER_LOG_POS=154;

START SLAVE;
SHOW SLAVE STATUS\G

7.2 连接池配置

-- 推荐使用连接池(推荐 HikariCP、Druid)
-- 连接池配置建议:

-- 最小空闲连接数
minimum-idle = 5

-- 最大连接数
maximum-pool-size = 20

-- 空闲连接超时(毫秒)
idle-timeout = 300000

-- 连接最大生命周期(毫秒)
max-lifetime = 1800000

-- 连接超时(毫秒)
connection-timeout = 30000

8. B+ 树角度优化建议

从 B+ 树的物理存储特性出发,深入理解索引底层原理,可以制定更有效的优化策略。

8.1 B+ 树高度与 IO 优化

flowchart LR
    subgraph 理想情况 [理想情况:3 层 B+ 树]
        R1[根节点] --> L1[中间层]
        R1 --> L2[中间层]
        L1 --> D1[叶子层]
        L1 --> D2[叶子层]
        L2 --> D3[叶子层]
        L2 --> D4[叶子层]
    end

    subgraph 优化后 [优化目标]
        T1["IO 次数 ≤ 3"]
        T2["查询延迟 < 10ms"]
    end

    理想情况 --> 优化后

    style 理想情况 fill:#e3f2fd,stroke:#1565c0
    style 优化后 fill:#c8e6c9,stroke:#2e7d32
指标 计算公式 优化建议
非叶子节点容量 16KB / 12 字节 ≈ 1365 键/页 主键越短,每页存储键越多
2 层树最大容量 1365 行 小表无需担心索引深度
3 层树最大容量 1365² ≈ 186 万行 大多数表保持 3 层即可
4 层树最大容量 1365³ ≈ 25 亿行 超大表考虑分库分表
-- 优化策略 1:缩短主键长度
-- ❌ 长主键导致树高度增加
CREATE TABLE users_bad (
    id VARCHAR(36) PRIMARY KEY,  -- UUID,36 字节
    name VARCHAR(100)
);
-- 每页存储键数:16KB / 36 字节 ≈ 455 个
-- 3 层树容量:455² ≈ 20 万行(容量大幅下降)

-- ✅ 短主键优化
CREATE TABLE users_good (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,  -- 8 字节
    name VARCHAR(100)
);
-- 每页存储键数:16KB / 8 字节 ≈ 2048 个
-- 3 层树容量:2048² ≈ 419 万行

-- 查看表的 B+ 树高度估算
SELECT
    table_name,
    table_rows,
    round(data_length / 16384, 0) AS data_pages,
    round(index_length / 16384, 0) AS index_pages,
    CASE
        WHEN table_rows < 1365 THEN '2 层'
        WHEN table_rows < 1870000 THEN '3 层'
        ELSE '4 层+'
    END AS estimated_height
FROM information_schema.tables
WHERE table_schema = 'your_database'
ORDER BY table_rows DESC;

8.2 页分裂与写入优化

flowchart TD
    A[插入新记录] --> B{目标页已满?}
    B -->|否 | C[直接插入
IO: 1 次] B -->|是 | D[页分裂] D --> E[分配新页] E --> F[移动 50% 数据] F --> G[更新父节点指针] G --> H[插入新记录] H --> I[IO: 3-5 次] style C fill:#c8e6c9,stroke:#2e7d32 style I fill:#ffcdd2,stroke:#c62828 style D fill:#fff3e0,stroke:#e65100
-- 页分裂性能影响:
-- - 直接插入:1 次 IO(写 1 个页)
-- - 页分裂:3-5 次 IO(分配新页 + 移动数据 + 更新指针)
-- - 连锁分裂:可能向上传播到根节点

-- 优化策略 2:使用自增主键,避免随机插入
-- ❌ 随机主键导致频繁页分裂
CREATE TABLE orders_bad (
    id VARCHAR(36) PRIMARY KEY,  -- UUID 随机值
    amount DECIMAL(10, 2)
);
-- 插入顺序:uuid_abc, uuid_xyz, uuid_def ...
-- 每次插入可能触发页分裂

-- ✅ 自增主键顺序插入
CREATE TABLE orders_good (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    amount DECIMAL(10, 2)
);
-- 插入顺序:1, 2, 3, 4, 5...
-- 顺序写入,几乎不触发页分裂

-- 必须使用 UUID 时的优化方案
CREATE TABLE orders_uuid (
    id BINARY(16) PRIMARY KEY,  -- 压缩 UUID
    amount DECIMAL(10, 2),
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_time (created_at)  -- 时间索引用于范围查询
);
-- 使用 UUID_TO_BIN(uuid, 1) 优化时间局部性
INSERT INTO orders_uuid (id, amount)
VALUES (UUID_TO_BIN(UUID(), 1), 100.00);

8.3 数据页利用率优化

场景 页利用率 IO 效率 优化方案
顺序插入 90-100% 保持现状
随机插入 50-70% 批量插入,定期 OPTIMIZE
大量删除 30-50% OPTIMIZE TABLE 重建
频繁更新 60-80% 使用行格式压缩
-- 优化策略 3:提高数据页利用率

-- 1. 批量插入,减少页分裂
-- ❌ 单条插入,每批 1000 条
INSERT INTO orders (amount) VALUES (100);  -- 执行 1000 次

-- ✅ 批量插入,每批 1000 条
INSERT INTO orders (amount)
VALUES (100), (100), ..., (100);  -- 1000 个值

-- 2. 定期优化表,整理碎片
OPTIMIZE TABLE orders;
-- 重建表,回收空间,提高页利用率

-- 3. 监控页利用率
SELECT
    table_name,
    round(data_length / 1024 / 1024, 2) AS data_mb,
    round(data_free / 1024 / 1024, 2) AS free_mb,
    round((data_free / (data_length + data_free)) * 100, 2) AS fragmentation_pct
FROM information_schema.tables
WHERE table_schema = 'your_database'
ORDER BY fragmentation_pct DESC;

-- 碎片率 > 20% 时建议优化

8.4 覆盖索引与叶子节点优化

flowchart TD
    subgraph 回表查询 [回表查询:4 次 IO]
        A1[查询] --> A2[非聚簇索引查找]
        A2 --> A3[获取主键]
        A3 --> A4[聚簇索引回表]
        A4 --> A5[返回数据]
    end

    subgraph 覆盖索引 [覆盖索引:2 次 IO]
        B1[查询] --> B2[覆盖索引查找]
        B2 --> B3[直接返回]
    end

    style 回表查询 fill:#ffebee,stroke:#c62828
    style 覆盖索引 fill:#e8f5e9,stroke:#2e7d32
-- 优化策略 4:使用覆盖索引减少回表

-- 场景:查询用户订单统计
-- ❌ 需要回表
SELECT customer_id, amount, created_at
FROM orders WHERE customer_id = 100;
-- 执行过程:
-- 1. 在 idx_customer 中找到 customer_id=100 的记录
-- 2. 获取主键 id
-- 3. 回表到聚簇索引获取 amount, created_at
-- 假设命中 100 条记录,需要回表 100 次

-- ✅ 覆盖索引,无需回表
CREATE INDEX idx_cover ON orders(customer_id, amount, created_at);
SELECT customer_id, amount, created_at
FROM orders WHERE customer_id = 100;
-- 执行过程:
-- 1. 在 idx_cover 中直接获取所有数据
-- 2. 无需回表,IO 减少 50-90%

-- 覆盖索引设计原则:
-- 1. WHERE 条件列放在最左
-- 2. SELECT 列包含在索引中
-- 3. 考虑索引大小与收益比

-- 验证覆盖索引
EXPLAIN SELECT customer_id, amount FROM orders WHERE customer_id = 100;
-- Extra: Using index(表示使用覆盖索引)

8.5 B+ 树优化最佳实践总结

优化方向 核心策略 预期收益
降低树高度 缩短主键长度,使用 INT/BIGINT IO 减少 25-50%
减少页分裂 自增主键,顺序插入 写入性能提升 3-5 倍
提高页利用率 批量操作,定期 OPTIMIZE 空间节省 30-50%
减少回表 覆盖索引,延迟关联 查询性能提升 5-10 倍
预分配空间 设置合适的初始 extent 大小 减少分配开销

9. 并发控制

-- 查看当前锁情况
SELECT * FROM performance_schema.data_locks;

-- 设置事务隔离级别(支持新的隔离级别)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 乐观锁(改进的版本控制)
UPDATE products
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;

-- 查看行锁等待
SELECT * FROM performance_schema.events_waits_current
WHERE EVENT_NAME LIKE '%lock%';

10. 性能监控

-- 实时性能监控
SELECT * FROM sys.statements_with_runtimes_in_95th_percentile;

-- 查看慢查询(AI 分析)
SELECT * FROM sys.statements_with_temp_tables
ORDER BY tmp_tables DESC;

-- 表空间使用情况
SELECT * FROM information_schema.innodb_tablespaces;

-- AI 性能诊断报告
CALL sys.generate_performance_report('your_database');