返回

MySQL多条件查询慢?优化实战:索引与查询技巧

mysql

MySQL 多条件查询慢,外键无明显提升的优化方案

手上有个 MySQL 表,数据量巨大,未来可能达到上亿条。有个程序经常查询这些数据,我已经做了些优化,大部分情况下速度都很快,因为我们只查和地理位置相关的一小部分数据。

表结构是这样的:

CREATE TABLE `prism_actions` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `action_time` timestamp NOT NULL default CURRENT_TIMESTAMP,
  `action_type` varchar(25) NOT NULL,
  `player` varchar(16) NOT NULL,
  `world` varchar(255) NOT NULL,
  `x` int(11) NOT NULL,
  `y` int(11) NOT NULL,
  `z` int(11) NOT NULL,
  `block_id` mediumint(5) unsigned NOT NULL,
  `block_subid` mediumint(5) unsigned NOT NULL,
  `data` varchar(255) NOT NULL,
  PRIMARY KEY  (`id`),
  KEY `x` (`x`),
  KEY `action_type` (`action_type`),
  KEY `player` (`player`),
  KEY `block_id` (`block_id`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

我在常用的 WHERE 条件字段上加了几个普通索引,如果查询只用一个条件,速度非常快。

测试用的表里有 2200 万条记录。

比如:

SELECT prism_actions.id FROM prism_actions WHERE prism_actions.action_type = 'block-break' LIMIT 1000;
1000 rows in set (0.00 sec)

SELECT prism_actions.id FROM prism_actions WHERE prism_actions.block_id = 2 LIMIT 1000;
1000 rows in set (0.01 sec)

问题在于,查询条件越多,速度越慢。

SELECT prism_actions.id FROM prism_actions WHERE prism_actions.action_type = 'block-break' AND prism_actions.block_id = 2 LIMIT 1000;
1000 rows in set (0.79 sec)

0.79 秒勉强能接受,但还没加完所有条件呢。

实际的查询语句大概是这样的:

SELECT prism_actions.id FROM prism_actions WHERE prism_actions.action_type = 'block-break' AND prism_actions.player = 'viveleroi' AND prism_actions.block_id = 2 LIMIT 1000;
1000 rows in set (2.22 sec)

单条件 0.01 秒,两个条件 0.79 秒,三个条件 2.2 秒,太慢了。

我会继续研究怎么更好地设计索引,但我现在对表结构和索引还算满意。

可是,我该怎么让这些条件一起用的时候快一点呢?

更新

我花时间把表改成了外键形式。player, action_type, world 列的数据被移到了单独的表里,原表里存它们对应的 ID。数据迁移花了几个小时。

不过,重新跑之前的查询语句,发现有的快了点,有的几乎没变化。

上面那个 0.79 秒的查询,转换后的版本差不多还是这么快:

SELECT prism_actions.id FROM prism_actions WHERE prism_actions.actiontype_id = 1 AND prism_actions.block_id = 2 LIMIT 1000;
1000 rows in set (0.73 sec)

block_id 列保留了原来表结构的索引。

player_id 条件的查询原本非常慢,我给这列加了索引,现在飞快。

但是,用了几个真实用户的查询例子,改成新的表结构,速度几乎没变。

SELECT prism_actions.id FROM prism_actions WHERE (prism_actions.actiontype_id = 2 OR prism_actions.actiontype_id = 1) AND (prism_actions.player_id = 1127) AND prism_actions.action_time >= '2013-02-22 07:47:54' LIMIT 1000;

之前要 5.83 秒,现在要 5.29 秒

看起来是时间戳的问题。把上面查询里的时间戳条件去掉,0.01 秒就出结果了。给时间戳加索引没用,有什么建议吗?

到目前为止,我只看到某些地方稍微快了一点点,节省了一点点文件空间(因为字符串重复存储减少了)但没发现什么特别值得让成百上千个有这么大数据量的用户花上一整天时间迁移数据的好处。

有没有什么其他的索引建议?

一、问题原因分析

综合以上, 性能瓶颈主要有这几点:

  1. MyISAM 引擎: MyISAM 对并发写入支持不好,且表锁机制可能导致写操作阻塞读操作。虽然这篇博客里没有提及写操作,但实际业务大概率是读写并存。
  2. 多条件查询与索引选择: MySQL 在处理多条件查询时,一次查询通常只能使用一个索引。即使各个字段都有单独的索引,也未必能达到最佳效果。
  3. OR 条件: 查询中包含 OR 操作符, OR 使得 MySQL 难以有效使用索引,它倾向于进行全表扫描。
  4. action_time 上的范围查询: 使用时间戳字段 (action_time) 进行范围查询(>=),但这个字段上没有索引或索引效果不好。
  5. 外键的误区: 将字段拆分到单独的表中并使用外键,这通常是数据库设计的最佳实践。但这本身不直接提升查询速度。外键主要用于保证数据完整性,速度的提升其实还是来自于新的、更有效的索引(例如对player_id建立的索引)。

二、解决方案

下面逐个击破这些瓶颈:

1. 考虑更换存储引擎为 InnoDB

InnoDB 引擎比 MyISAM 有更好的并发性能,支持行级锁,更适合高并发读写场景。

  • 原理:

    • InnoDB 使用行级锁,减少了锁冲突,提高了并发性。
    • InnoDB 支持事务,提供了 ACID 特性,数据更安全。
    • InnoDB 使用聚集索引,数据按主键顺序存储,对主键查询非常快。
  • 操作:

    ALTER TABLE prism_actions ENGINE=InnoDB;
    
  • 注意事项: 转换成InnoDB会有一个短暂时间不可访问. 表越大,时间越久.

2. 建立复合索引

根据查询条件, 创建合适的复合索引,能大幅提升查询速度。复合索引按照 “最左前缀” 原则生效。要仔细评估常用的查询条件, 设计最佳组合。

  • 原理: 复合索引将多个列组合在一起创建索引,查询时可以直接利用这个索引,避免回表。

  • 代码示例(针对你的特定查询): 针对你的案例中的几个关键查询:

    -- 针对 action_type 和 block_id 的查询
    ALTER TABLE prism_actions ADD INDEX idx_actiontype_blockid (action_type, block_id);
    
    -- 针对 action_type, player 和 block_id 的查询
    ALTER TABLE prism_actions ADD INDEX idx_actiontype_player_blockid (action_type, player, block_id);
    
    -- 针对 actiontype_id, player_id 和 action_time 的查询 (去掉OR, 下文有优化建议)
    -- 先假设只有 actiontype_id = 2 的情况
    ALTER TABLE prism_actions ADD INDEX idx_actiontypeid_playerid_time (actiontype_id, player_id, action_time);
    
  • 注意: 先不着急建立太多的索引,先通过后面的优化OR和时间戳的方式优化现有语句. 然后根据explain结果,再按需增加.

3. 优化 OR 条件查询

OR 拆成 UNION ALL

  • 原理:
    UNION ALLOR 更容易被 MySQL 优化,UNION ALL 连接的两个查询可以分别使用索引,避免全表扫描。

  • 代码:

    SELECT prism_actions.id
    FROM prism_actions
    WHERE prism_actions.actiontype_id = 2 AND prism_actions.player_id = 1127 AND prism_actions.action_time >= '2013-02-22 07:47:54'
    UNION ALL
    SELECT prism_actions.id
    FROM prism_actions
    WHERE prism_actions.actiontype_id = 1 AND prism_actions.player_id = 1127 AND prism_actions.action_time >= '2013-02-22 07:47:54'
    LIMIT 1000; -- 注意 LIMIT 的位置
    

    这样就把之前的 or 语句优化掉了。同时我们可以通过观察explain,判断要不要创建 idx_actiontypeid_playerid_time (actiontype_id, player_id, action_time)

  • UNION ALLUNION: UNION ALL 不会去重,速度更快。如果你的查询结果确定不需要去重, 就用 UNION ALL

4. 优化时间范围查询

action_time 加索引。

  • 原理: 索引可以加快范围查询的速度,使 MySQL 快速定位到符合时间条件的数据。

  • 操作:

    ALTER TABLE prism_actions ADD INDEX idx_action_time (action_time);
    

但是!即使加了索引,如果符合这个时间范围的数据仍然很多(比如你这里是 >= 某个时间, 没有上限), 还是可能会慢。还可以这样优化:

  • 分区:
    可以按照时间 (按天、周、月等) 对表进行分区, 可以让查询只扫描特定的分区, 大幅提高效率。分区对数据量巨大的表格非常有帮助。但这里不能随意给出方案,你需要告诉我业务场景,数据写入频率,等等.才能给出好的方案.
  • 限制更严格的时间范围: 如果业务上允许, 加更严格的时间上限。
    * 只读旧数据: 如果旧数据仅仅是读取,那么可以考虑将就数据进行归档处理.
    * 分库分表 :属于更高级的操作。可以大幅降低数据量,但比较复杂,这里先不做过多讲解, 因为目前索引+分区足够了。

5. 理解外键

外键的作用主要是为了保证数据一致性。不直接加速查询, 外键不加速,加速的是索引. 针对你当前actiontype_id,player_id都已经加上索引,速度提升上来了. 可以不需要过分强调外键。

6. 查看执行计划(EXPLAIN)

在每个 SQL 查询语句前加上 EXPLAIN,看看 MySQL 是如何执行这个查询的。重点关注以下几列:

  • type: 连接类型,越往后性能越差:system > const > eq_ref > ref > range > index > ALL。 争取优化到 refrange
  • possible_keys: 可能用到的索引。
  • key: 实际使用的索引。
  • rows: 扫描的行数,越少越好。
  • Extra : 其他信息. 注意这里有没有using filesort 或者 using temporary,有则意味着很可能有性能问题.

根据 EXPLAIN 的结果,不断调整索引策略。

7. 其他建议

  • 硬件升级: 如果预算充足,最直接的办法是升级硬件(CPU、内存、SSD)。
  • 数据库版本: 使用新版本的mysql,会有新的性能优化.
  • 定期维护: 定期对表进行 OPTIMIZE TABLE,整理碎片,可以提高性能.
  • 缓存: 对于读多写少的场景,引入缓存(如 Redis)能显著减轻数据库压力。不过,需要注意数据一致性的问题。

以上就是针对你当前这个 MySQL 查询慢问题的优化思路。核心在于合理使用索引, 优化 SQL,理解存储引擎. 多使用explain验证结果. 根据explain,选择最合适的索引方案.