我们有这张桌子:
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似乎并不关心我们只需要一行的事实:在这种情况下,使用完整索引似乎总是最佳选择。
我做了一些测试,得出以下观察结果:
由于我们在很多地方使用这种访问模式,并且我们有一个自定义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);