MySQL - 性能优化

发布于:2025-07-28 ⋅ 阅读:(19) ⋅ 点赞:(0)

使用 Explain 进行分析

Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。

比较重要的字段有:

  • select_type : 查询类型,有简单查询、联合查询、子查询等
  • key : 使用的索引
  • rows : 扫描的行数

优化数据访问

1. 减少请求的数据量

  • 只返回必要的列: 最好不要使用 SELECT * 语句。
  • 只返回必要的行: 使用 LIMIT 语句来限制返回的数据。
  • 缓存重复查询的数据: 使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。

2. 减少服务器端扫描的行数

最有效的方式是使用索引来覆盖查询。

重构查询方式

1. 切分大查询(Batch Delete)

原方案

DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);

问题

  • 一次性删除大量数据可能导致:
    • 长事务:锁定大量数据,阻塞其他操作。
    • 日志膨胀:事务日志(如 InnoDB 的 undo log)被占满。
    • 主从延迟:大事务在复制时延迟严重。

优化方案:分批删除

rows_affected = 0
do {
    rows_affected = do_query(
        "DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000"
    )
} while rows_affected > 0

优点

  • 减少锁持有时间:每次只锁 1 万行,避免长时间阻塞。
  • 控制资源占用:避免单次事务过大。
  • 可中断性:随时停止不会导致部分数据未删除。

注意事项

  1. 加索引:确保 create 字段有索引,否则分批删除依然全表扫描。
  2. 避免间隙锁:如果使用 InnoDB,建议改用 id 范围分批(避免间隙锁冲突):
DELETE FROM messages WHERE id BETWEEN 1 AND 10000 AND create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
  1. 休眠间隔:每次删除后休眠一段时间(如 100ms),减少对正常业务的影响。

2. 分解大连接查询(Join Decomposition)

原方案(大连接查询)

SELECT * FROM tag
JOIN tag_post ON tag_post.tag_id = tag.id
JOIN post ON tag_post.post_id = post.id
WHERE tag.tag = 'mysql';

潜在问题

  • 缓存失效:任一表更新会导致整个查询缓存失效。
  • 锁竞争:多表连接可能跨表加锁。
  • 执行计划复杂:优化器可能选择低效的连接顺序。

优化方案:分解为单表查询 + 应用层关联

-- 第一步:查询 tag 表
SELECT * FROM tag WHERE tag = 'mysql';
-- 假设返回 tag.id = 1234

-- 第二步:查询 tag_post 表
SELECT * FROM tag_post WHERE tag_id = 1234;
-- 假设返回 post_id 列表 (123, 456, 567)

-- 第三步:查询 post 表
SELECT * FROM post WHERE id IN (123, 456, 567);

优点

  1. 缓存友好
    • 单表查询结果可独立缓存(如 tag.tag='mysql' 的结果被缓存后,其他查询可复用)。
    • 连接查询的缓存一旦失效,所有表关联结果均失效。
  1. 减少锁竞争
    • 单表查询锁定的范围更小。
    • 避免多表连接时潜在的锁升级(如行锁升级为表锁)。
  1. 灵活扩展
    • 易于分库分表(如 post 表和 tag_post 表可部署在不同节点)。
  1. 执行效率
    • MySQL 对 IN() 列表的查询会按主键顺序访问,可能比随机连接更高效(尤其对机械硬盘)。

注意事项

  1. 网络开销:应用层关联需多次查询,适合低延迟网络环境。
  2. IN() 列表限制
    • MySQL 的 IN() 列表长度有限制(默认 max_allowed_packet 限制)。
    • 超长列表可改用临时表或分批查询:
-- 使用临时表
CREATE TEMPORARY TABLE temp_post_ids (id INT PRIMARY KEY);
INSERT INTO temp_post_ids VALUES (123), (456), (567);
SELECT * FROM post WHERE id IN (SELECT id FROM temp_post_ids);
  1. 事务一致性
    • 多次查询可能读到中间状态(需在事务中执行或使用快照读)。

何时选择分解连接查询?

场景

适合分解

适合直接连接

查询缓存命中率高

⭐️ 单表查询缓存更易复用

❌ 连接查询缓存易失效

表数据更新频繁

⭐️ 减少锁竞争和缓存失效

❌ 大连接可能阻塞其他操作

分库分表架构

⭐️ 天然支持跨节点查询

❌ 跨节点连接难以实现

简单查询(如主键查询)

❌ 额外网络开销不划算

⭐️ 直接连接更高效

复杂聚合计算(如 SUM)

❌ 应用层聚合性能差

⭐️ 数据库聚合更高效


其他优化补充

(1)替代 IN() 的方案
  • 临时表:如前述,适合超长列表。
  • JOIN 替代 IN
SELECT post.* FROM post
JOIN (SELECT 123 AS id UNION SELECT 456 UNION SELECT 567) AS ids
ON post.id = ids.id;
(2)连接查询的优化技巧

如果必须使用连接查询:

  1. 确保关联字段有索引(如 tag_post.tag_idtag_post.post_id)。
  2. 控制结果集大小:添加 LIMIT 或筛选条件。
  3. 使用 STRAIGHT_JOIN:手动指定连接顺序(避免优化器选错计划)。

网站公告

今日签到

点亮在社区的每一天
去签到