MySQL select_expr 意外影响查询执行计划

MySQL select_expr unexpectedly impacting query execution plan

我 运行 遇到 MySQL(5.6.23 社区服务器)的意外问题 – 更改我的 select 语句中的字段列表会更改查询执行计划并对查询性能产生巨大影响。

我认为证明问题的最佳方式是举例。如果我创建两个简单的 tables:

create table table1 (
  id INT AUTO_INCREMENT PRIMARY KEY,
  random INT,
  value INT,
  KEY (value)
);

create table table2 (
  id INT AUTO_INCREMENT PRIMARY KEY,
  table1 INT,
  random INT,
  CONSTRAINT FOREIGN KEY (table1) REFERENCES table1 (id)
);

然后用基本数据填充它们(我用来做这个的过程在问题的底部)——然后我可以比较以下两个查询的性能:

mysql> select t1.id, t2.id from table2 t2 join table1 t1 on t2.table1=t1.id where t1.value = 1 order by t2.id desc limit 1;
+---------+----------+
| id      | id       |
+---------+----------+
| 1109700 | 11097000 |
+---------+----------+
1 row in set (1.23 sec)

mysql> select t1.id, t1.random, t2.id, t2.random from table2 t2 join table1 t1 on t2.table1=t1.id where t1.value = 1 order by t2.id desc limit 1;
+---------+--------+----------+--------+
| id      | random | id       | random |
+---------+--------+----------+--------+
| 1109700 | 749465 | 11097000 | 538840 |
+---------+--------+----------+--------+
1 row in set (4.06 sec)

请注意,这两个查询之间的唯一区别是在 select 语句中包含两个 'random' 字段——但速度要慢三倍多。另请注意,我已经通过 echo 3 | sudo tee /proc/sys/vm/drop_caches 和 innodb 缓冲池清除了 linux 磁盘缓存,方法是在执行每个查询之前重新启动 mysql

以下是这两个查询的查询计划:

mysql> desc select t1.id, t2.id from table2 t2 join table1 t1 on t2.table1=t1.id where t1.value = 1 order by t2.id desc limit 1;
+----+-------------+-------+------+---------------+--------+---------+------------+-------+----------------------------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref        | rows  | Extra                                        |
+----+-------------+-------+------+---------------+--------+---------+------------+-------+----------------------------------------------+
|  1 | SIMPLE      | t1    | ref  | PRIMARY,value | value  | 5       | const      | 22312 | Using index; Using temporary; Using filesort |
|  1 | SIMPLE      | t2    | ref  | table1        | table1 | 5       | test.t1.id |     4 | Using index                                  |
+----+-------------+-------+------+---------------+--------+---------+------------+-------+----------------------------------------------+
2 rows in set (0.00 sec)

mysql> desc select t1.id, t1.random, t2.id, t2.random from table2 t2 join table1 t1 on t2.table1=t1.id where t1.value = 1 order by t2.id desc limit 1;
+----+-------------+-------+------+---------------+--------+---------+------------+-------+---------------------------------+
| id | select_type | table | type | possible_keys | key    | key_len | ref        | rows  | Extra                           |
+----+-------------+-------+------+---------------+--------+---------+------------+-------+---------------------------------+
|  1 | SIMPLE      | t1    | ref  | PRIMARY,value | value  | 5       | const      | 22312 | Using temporary; Using filesort |
|  1 | SIMPLE      | t2    | ref  | table1        | table1 | 5       | test.t1.id |     4 | NULL                            |
+----+-------------+-------+------+---------------+--------+---------+------------+-------+---------------------------------+
2 rows in set (0.00 sec)

所以在 select 语句中包含 运行dom 字段似乎导致查询计划放弃使用索引(无论如何这是我对 'Extra' 列的阅读).

我应该指出 – 虽然在上面的示例中性能差异是三倍 – 但在我的生产数据库上影响更为明显;慢了将近 40 倍。这是因为生产中 table 的大小要大得多,并且 table 索引通常会被缓存(但记录数据不会)。

我考虑过变通方法 – 一旦我得到第一个查询的输出 – 我可以 运行 以下操作:

mysql> select t1.id, t1.random, t2.id, t2.random from table2 t2 join table1 t1 on t2.table1=t1.id where t1.value = 1 and t2.id = 11097000 order by t2.id desc;
+---------+--------+----------+--------+
| id      | random | id       | random |
+---------+--------+----------+--------+
| 1109700 | 749465 | 11097000 | 538840 |
+---------+--------+----------+--------+
1 row in set (0.01 sec)

为了完整性——我已经包括了我用来填充 table 数据的过程(请注意,如果您打算 运行 这个,您可能想要更改 rows_to_insert 的值你自己 - 1,000,000 行对我来说花了四个小时):

DELIMITER $
CREATE PROCEDURE random_fill()
BEGIN
    DECLARE counter1, counter2, table1_id, rows_to_insert INT;

    SET counter1 = 0;
    SET rows_to_insert = 1000000;

    label1: LOOP
        SET counter1 = counter1 + 1;

        INSERT INTO table1 ( random, value ) VALUES ( CEIL( RAND() * rows_to_insert ), CEIL( RAND() * 99 ) );
        SET table1_id = LAST_INSERT_ID();

        SET counter2 = 0;
        label2: LOOP
           SET counter2 = counter2 + 1;

           INSERT INTO table2 ( table1, random ) VALUES (  table1_id, CEIL( RAND() * rows_to_insert ) );
           IF counter2 < 10 THEN
              ITERATE label2;
           END IF;
           LEAVE label2;
        END LOOP label2;

        IF counter1 < rows_to_insert THEN
           ITERATE label1;
        END IF;
        LEAVE label1;
    END LOOP label1;
END$
DELIMITER ;

您的问题的答案远远超过这里的空间。我会给你一些线索,再加上一些参考资料。

"Composite" 索引通常很有用。特别是,当查询为 WHERE last = 'James' AND first = 'Rick'.

时,INDEX(last, first) 优于 INDEX(last), INDEX(first)

"covering" 索引是这样一种索引,其中 所有 SELECT 所需的列都在一个索引中。 "Inclusion of random fields" 远离 "covering"。

EXPLAIN(别名DESC)中,Using index是可以使用覆盖索引的线索。

在 InnoDB 中,PRIMARY KEY 隐式附加在每个辅助键的末尾。在您的第一个示例中 KEY(value) 实际上是复合索引 KEY(value, id).

有时会愚弄人们的一件事是,当他们 运行 一个查询并且它很慢时,他们 运行 另一个查询(或相同)并且它更快。答案通常是第一次查询很慢,因为从磁盘中获取数据;第二个在 RAM(缓存)中找到东西,因此速度更快(通常是 10 倍)。

使用明显索引的一个常见原因是优化器认为索引需要获取超过 20% 的行。相反,它决定忽略索引并爆破数据可能更快。 (20% 随月相变化。)请注意,索引是指向数据的指针的排序列表。扫描索引可能很快,但访问数据可能代价高昂。

参考资料
Cookbook on building an INDEX from a SELECT
A bunch more on indexing.

Linux 磁盘缓存(?)无关紧要。 InnoDB 在其 "buffer_pool" 中将内容缓存在 RAM 中。 (因为你是 运行ning 5.6,我假设你的表是 InnoDB,而不是 MyISAM。我在这里说的几件事需要针对 MyISAM 进行修改。)