MySQL优化器问题与两列上的索引和限制条款

时间:2016-04-18 16:43:24

标签: mysql

我们有这张桌子:

CREATE TABLE `test_table` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `value` FLOAT NOT NULL,
  `session` INT NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `session_time_idx` (`session` ASC,`time` ASC)
  ) ENGINE = InnoDB;

它用于存储不同的“测量会话”,每个会产生数十万行。不同的测量会话可以具有相同或重叠的时间戳范围。然后,我们需要使用以下查询随机访问单个测量值:

SELECT * FROM `test_table` WHERE `session` = 2 AND `time` < '2003-12-02' ORDER BY `time` DESC LIMIT 1;

我们需要查询在测量会话上均匀分布的时间。 “小于”运算符是必要的,因为我们不确切知道每次测量的确切时间,我们只需要找到在给定日期和时间之前执行的最后一次测量。

根据查询中指定的时间,我们有两个可能的结果计划:

mysql> EXPLAIN SELECT * FROM `test_table` WHERE `session` = 2 AND `time` < '2003-12-02' ORDER BY `time` DESC LIMIT 1;
+----+-------------+------------+-------+------------------+------------------+---------+------+------+-------------+
| id | select_type | table      | type  | possible_keys    | key              | key_len | ref  | rows | Extra       |
+----+-------------+------------+-------+------------------+------------------+---------+------+------+-------------+
|  1 | SIMPLE      | test_table | range | session_time_idx | session_time_idx | 8       | NULL | 6050 | Using where |
+----+-------------+------------+-------+------------------+------------------+---------+------+------+-------------+

mysql> EXPLAIN SELECT * FROM `test_table` WHERE `session` = 2 AND `time` < '2005-01-02' ORDER BY `time` DESC LIMIT 1;
+----+-------------+------------+------+------------------+------------------+---------+-------+--------+-------------+
| id | select_type | table      | type | possible_keys    | key              | key_len | ref   | rows   | Extra       |
+----+-------------+------------+------+------------------+------------------+---------+-------+--------+-------------+
|  1 | SIMPLE      | test_table | ref  | session_time_idx | session_time_idx | 4       | const | 127758 | Using where |
+----+-------------+------------+------+------------------+------------------+---------+-------+--------+-------------+

第一个计划使用整个索引(会话和时间),通常会导致开发计算机上的子ms执行时间。

第二个计划仅使用部分索引,然后扫描整个会话的结果,有时会扫描数十万行。毋庸置疑,第二个计划的表现非常差。在开发机器上几十毫秒,这可能会成为慢速生产嵌入式设备的秒。

如果没有使用“LIMIT”子句,则两个查询之间的差异就是与查询匹配的行数。当没有指定“LIMIT”时这是有意义的,因为直接扫描数据可能是一个优势,而不是扫描索引的第二部分和数据。但MySQL似乎并不关心我们只需要一行的事实:在这种情况下,使用完整索引似乎总是最佳选择。

我做了一些测试,得出以下观察结果:

  • 如果我只选择“id”,“time”和/或“session”(不是“value”),则在所有情况下都使用完整索引(因为所有需要的数据都在索引中);因此,虽然稍微麻烦,但首先查询“id”,然后其余数据将起作用
  • 使用“FORCE INDEX(session_time_idx)”确实修复了错误的计划并导致快速查询
  • 按时使用单列索引时不存在任何问题
  • 运行OPTIMIZE TABLE没有任何区别
  • 使用MyIASM代替InnoDB没有区别
  • 使用简单整数而不是TIMESTAMP没有区别(正如预期的那样:TIMESTAMP毕竟是一个整数)
  • 我玩各种参数,包括“max_seeks_for_key”,但我无法修复糟糕的计划

由于我们在很多地方使用这种访​​问模式,并且我们有一个自定义ORM系统,我想知道是否有办法“说服”MySQL做正确的事情而不必为ORM添加“FORCE INDEX”支持

对于解决这个问题的任何其他建议也将不胜感激。

我的设置:64位Ubuntu 14.04上的MySQL Server 5.5.47。 更新:这也适用于MySQL Server 5.6和5.7。

作为参考,这是我用来创建测试设置的脚本:

set @@time_zone = "+00:00";
drop schema if exists `index_test`;
create schema `index_test`;
use `index_test`;

CREATE TABLE `test_table` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `value` FLOAT NOT NULL,
  `session` INT NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `session_time_idx` (`session` ASC,`time` ASC)
  ) ENGINE = InnoDB;

delimiter $$
CREATE PROCEDURE fill(total int)
BEGIN
  DECLARE count int;
  DECLARE countPerSs int;
  DECLARE tim int;
  set count = 0;
  set countPerSs = 100000;
  set tim = unix_timestamp('2000-01-01');
  myloop: LOOP
    insert into `test_table` set `value` = rand(), `session` = count div countPerSs, `time` = from_unixtime(tim);
    set tim = tim + 10 * 60;
    SET count = count + 1;
    IF count < total THEN
      ITERATE myloop;
    END IF;
    LEAVE myloop;
  END LOOP myloop;
END;
$$

delimiter ;
call fill(500000);

0 个答案:

没有答案