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

Posted

技术标签:

【中文标题】MySQL select_expr 意外影响查询执行计划【英文标题】:MySQL select_expr unexpectedly impacting query execution plan 【发布时间】:2015-08-20 03:43:28 【问题描述】:

我在 mysql(5.6.23 社区服务器)中遇到了一个意外问题——更改我的 select 语句中的字段列表正在更改查询执行计划并对查询性能产生巨大影响。

我认为展示问题的最佳方式是通过示例。如果我创建两个简单的表:

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 语句中包含了两个“随机”字段——但它的速度要慢三倍以上。另请注意,我已通过echo 3 | sudo tee /proc/sys/vm/drop_caches 和 innodb 缓冲池通过在执行每个查询之前重新启动 mysql 清除了 linux 磁盘缓存

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

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 语句中包含随机字段似乎会导致查询计划放弃使用索引(这就是我对“额外”列的阅读)。

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

我考虑过一种变通方法——一旦我得到第一个查询的输出——我可以运行以下命令:

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)

为了完整起见——我已经包含了我用来填充表数据的过程(请注意,如果你打算自己运行它,你可能想要更改 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 ;

2009 年 9 月更新

我应该指出,为了简单起见,我只在示例中包含了一列。在我的生产环境中 - 有问题的表有 69 列。我们还使用了 Hibernate——它将在其 select 语句中选择所有 69 列。结果 - 覆盖索引是不实用的。

我的期望是让 MySQL 认识到 - 通过使用 'limit 1' 子句 - 我只需要来自 1 行的数据,因此它会:

    1.使用索引值找到这一行的PRIMARY KEY;和 2. 去磁盘只读取这1行的值。

相反,它似乎在扫描磁盘上每一行的数据 - 使得查询特别昂贵(我不确定这是在做什么 - 但是当我要求这两个附加列的值)。

我在 PostGres 上尝试了上述相同的示例,发现它正在执行我对 MySQL 的预期,并且速度难以置信(快了 200 倍以上):

#     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 
--------+--------+---------+--------
 999984 | 614113 | 9999840 | 622718
(1 row)

Time: 17.973 ms

这是来自 PostGres 的查询计划:

# explain 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;
                                                  QUERY PLAN                                                   
---------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.86..599.14 rows=1 width=16)
   ->  Nested Loop  (cost=0.86..63819201.12 rows=106672 width=16)
         ->  Index Scan Backward using table2_pkey on table2 t2  (cost=0.43..313745.06 rows=10000175 width=12)
         ->  Index Scan using table1_pkey on table1 t1  (cost=0.42..6.34 rows=1 width=8)
               Index Cond: (id = t2.table1)
               Filter: (value = 1)
(6 rows)

Time: 2.407 ms

所以你可以看到它使用索引来找到正确的行,然后只读取这一行的数据。

有没有办法强制 MySQL 采用相同的方法?

或者我是否需要在代码中解决问题(即先获取主索引,然后让 Hibernate 获取这一行)?


2015 年 9 月 12 日更新

好的 - 我想我明白你在说什么。 Using index 语句意味着 MySQL 使用索引。它不需要去数据来获取结果集的值。换句话说 - Using index 表示 MySQL 正在使用覆盖索引('id' - PRIMARY KEY - 隐式包含在 'value' 键中)。

所以实际上我解释错了。我原本以为这意味着索引的使用已经完全放弃了。

这是来自SHOW SESSION STATUS LIKE 'Handler%' 的结果(两个查询的输出相同):

+----------------------------+--------+
| Variable_name              | Value  |
+----------------------------+--------+
| Handler_commit             | 1      |
| Handler_delete             | 0      |
| Handler_discover           | 0      |
| Handler_external_lock      | 4      |
| Handler_mrr_init           | 0      |
| Handler_prepare            | 0      |
| Handler_read_first         | 0      |
| Handler_read_key           | 11158  |
| Handler_read_last          | 0      |
| Handler_read_next          | 122727 |
| Handler_read_prev          | 0      |
| Handler_read_rnd           | 1      |
| Handler_read_rnd_next      | 111571 |
| Handler_rollback           | 0      |
| Handler_savepoint          | 0      |
| Handler_savepoint_rollback | 0      |
| Handler_update             | 0      |
| Handler_write              | 111570 |
+----------------------------+--------+
18 rows in set (0.25 sec)

但感谢您的解释,我想我现在对 MySQL 的解释输出有了更好的理解。第二个查询是:

    从“table1”和“value”键开始。使用此键,它会从符合t1.value = 1 标准的行中查找所有 t1.id 值;那么它 在步骤 1 返回的集合中查找所有具有“table1”值的“table2”值。它处理t2.table1=t1.id 标准

所以最后需要满足的是order by t2.id desc 子句和limit 1 子句的应用。正是在这里,我认为 MySQL 可以做得更好。它正在创建一个临时表,然后进行文件排序。但它似乎是在制作临时表之前获取所有行的值。但是因为order by 子句在PRIMARY KEY 上——从技术上讲,它还不需要获取数据。它可以满足order by 子句,然后应用limit 子句,然后获取完成结果集所需的数据。实际上是这样的:

解决方案 1(1.30 秒 - 快一点)

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.29 sec)

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

总共 1.30 秒。

但最有效的方法是 PostGres 创建的计划 - 从表 2 开始并向后移动,直到找到适合 t2.table1=t1.idt1.value = 1 子句的一行。实际上是这样的:

解决方案 2(0.04 秒 - 最快)

mysql> select t1.id, t1.random, t2.id, t2.random
    -> from table2 t2
    -> straight_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 (0.04 sec)

目前 - MySQL 和 Hibernate 的默认行为会产生这种情况:

解决方案 3(3.82 秒 - 最慢)

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 (3.82 sec)

使用 Hibernate - 我不能使用 straight_join 子句,所以我不认为 SOLUTION 2 是一个选项。

但我可以通过解决方法获得 SOLUTION 1,首先我从表 2 中获取 PRIMARY KEY,然后让 Hibernate 获取这一行。

我的分析正确吗?还有其他我错过的选择吗?

为了完整起见,我已经包含了 SOLUTION 2 的 MySQL 查询计划(没有更多的 Using temporary; Using filesort):

mysql> desc select t1.id, t1.random, t2.id, t2.random
    -> from table2 t2
    -> straight_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      | t2    | index  | table1        | PRIMARY | 4       | NULL           |    1 | Using where |
|  1 | SIMPLE      | t1    | eq_ref | PRIMARY,value | PRIMARY | 4       | test.t2.table1 |    1 | Using where |
+----+-------------+-------+--------+---------------+---------+---------+----------------+------+-------------+
2 rows in set (0.05 sec)

2015 年 9 月 14 日更新

一旦我更好地理解了 MySQL 和 PostGres 查询计划之间的区别 - 我可以立即看到 MySQL 查询计划执行得更好的场景:

默认 MySQL 查询:

mysql> select t1.id, t1.random, t2.id, t2.random
    -> from table2 t2 join table1 t1 on t2.table1=t1.id
    -> where t1.value = 111
    -> order by t2.id desc limit 1;
Empty set (0.00 sec)

straight_joinMySQL查询:

mysql> select t1.id, t1.random, t2.id, t2.random
    -> from table2 t2
    -> straight_join table1 t1 on t2.table1=t1.id
    -> where t1.value = 111 order by t2.id desc limit 1;
Empty set (6.29 sec)

查询与我的原始示例相同 - 唯一的区别是我现在正在寻找 111 的 value(不在此数据集中)。

所以 PostGres 查询计划只有在回滚到 table2 时尽早发现命中才会更快。哪一个——根据我的生产数据——我知道会是这样。所以 SOLUTION 2 是理想的,但我认为通过 Hiberate 是不可能的。

这是 JSON 解释的输出:

mysql> explain format=JSON 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\G
*************************** 1. row ***************************
EXPLAIN: 
  "query_block": 
    "select_id": 1,
    "ordering_operation": 
      "using_temporary_table": true,
      "using_filesort": true,
      "nested_loop": [
        
          "table": 
            "table_name": "t1",
            "access_type": "ref",
            "possible_keys": [
              "PRIMARY",
              "value"
            ],
            "key": "value",
            "used_key_parts": [
              "value"
            ],
            "key_length": "5",
            "ref": [
              "const"
            ],
            "rows": 22312,
            "filtered": 100
          
        ,
        
          "table": 
            "table_name": "t2",
            "access_type": "ref",
            "possible_keys": [
              "table1"
            ],
            "key": "table1",
            "used_key_parts": [
              "table1"
            ],
            "key_length": "5",
            "ref": [
              "test.t1.id"
            ],
            "rows": 4,
            "filtered": 100
          
        
      ]
    
  

1 row in set, 1 warning (0.00 sec)

但不幸的是,我认为它没有告诉我们任何新的东西。

我确实想到了第四个可能的解决方案——那就是在单个 SQL 语句中执行 SOLUTION 1。我想出了这个:

解决方案 4(与解决方案 1 的速度相同)

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

解释如下:

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

它有效地创建了一个派生表 (derived2),其中包含所有 t2.id 和我需要它们的顺序 - 然后加入 t1 和 t2 表,这样我就可以获得“随机”值。但是这种方法的好处是当 MySQL 创建临时表时(在解释的第 4 项期间)它是“使用索引” - 因此它比 SOLUTION 3 更快。但我不认为你可以在 Hibernate 中创建派生表,所以我尝试了最后一种解决方案。

解决方案 5(与解决方案 3 的速度相同)

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

还有解释:

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

很遗憾,与 SOLUTION 3 相比,这并没有带来任何好处。从解释计划的输出结果看,此解决方案与 SOLUTION 4 之间的区别并不明显。我唯一能想到的是order by 发生在 SOLUTION 4 的派生表上(这只是索引值),并且只有一次为 SOLUTION 获取了每个值5(包括所有未编入索引的random 值)。

所以我最喜欢的解决方案(最喜欢的首先列出)是:

    解决方案 2 解决方案 4 解决方案 1

我认为在使用 Hibernate 时我无法实现 SOLUTION 2SOLUTION 4 - 所以剩下 SOLUTION 1

在继续解决方案 1 之前,我还应该尝试什么?


2015-09-17 更新 - 结论

好的 - 我已经使用 SOLUTION 1

但是,谢谢你,瑞克。您的帮助非常宝贵。凭借我的新知识,我已经能够优化许多现有查询。

对于正在阅读本文并遇到相同问题的任何人 - 这是问题的摘要和解决方法。

Hibernate 生成的简单但有问题的查询是:

select tran_.id as id13_1_, tran_.deleted as deleted13_1_, join_item1_.id as id9_0_
from tran tran_
inner join item join_item1_ on tran_.item=join_item1_.id
where join_item1_.itemType=120
order by tran_.id desc
limit 100;

请注意,我减少了选择列表中的字段数量并更改了表名,以使示例更易于阅读且更相关。

tran 中有 17,286,852 行,item 中有 971,020 行,包含 105 种不同的项目类型。

上述查询耗时 20 分 47.32 秒执行。路,太长了。

之所以需要这么长时间是因为 MySQL 处理这些步骤的顺序。它会:

    首先使用item 表中itemType 上的索引找到所有正确类型的项目(where itemType=120) 然后将tran 表中item 上的索引用于join (join item on tran.item=item.id) 然后(不确定顺序): a) 加载迄今为止命中的每一行的每一列的数据;和 b) 排序基于tran_.id desc 根据limit 100 子句减少结果集

因此,在步骤 3a) - 它正在加载大量不在最终结果集中的数据。 tran 表中有 69 列 - 在应用 limit 子句之前 - 此结果集有 376,652 行。这是一个非常昂贵且不必要的 IO 练习!

一旦我理解了这一点 - 我意识到步骤 3a) 应该在步骤 4 之后应用。我希望执行顺序是:

    首先使用item 表中itemType 上的索引查找所有正确类型的项目(where itemType=120) 然后在tran 表中使用item 上的索引 根据tran_.id desc排序 根据limit 100 子句减少结果集 从每一列加载剩余 100 行的数据(避免加载其他 376,552 行)

请注意,只有在步骤 1 到 4 中使用的所有字段都包含在查询计划使用的索引中时,才能实现此顺序。在我的示例中,它们是(tran.id 隐式包含在 tran.item 索引中)。

要让 MySQL 执行此操作 - 您首先要在选择列表中仅使用索引值执行步骤 1 到 5(因此您的查询计划将在每个步骤中显示 Using index)。例如(仅更改了 select 子句):

select tran_.id as id13_1_
from tran tran_
inner join item join_item1_ on tran_.item=join_item1_.id
where join_item1_.itemType=120
order by tran_.id desc
limit 100;

然后我可以将此结果集用作派生表来同时提供我的原始查询:

我需要的 100 行;和 他们的订单。

例如:

select tran_.id as id13_1_, tran_.deleted as deleted13_1_, join_item1_.id as id9_0_
from tran tran_
inner join item join_item1_ on tran_.item=join_item1_.id
inner join (select tran_.id as id13_1_
            from tran tran_
            inner join item join_item1_ on tran_.item=join_item1_.id
            where join_item1_.itemType=120
            order by tran_.id desc
           ) as ids
limit 100;

请务必从外部查询中删除 order by 子句。否则 MySQL 会回到它的坏习惯。外部不再需要where 子句。 limit 100 子句实际上可以在内部或外部查询上。

此新查询的查询计划显示仅使用索引值创建的派生表。然后它以tran_.id 开头完成外部查询 - 因此它可以快速找到并仅加载 100 行。

你可能认为我也可以这样写查询:

select tran_.id as id13_1_
from tran tran_ force key (PRIMARY)
straight_join item join_item1_ on tran_.item=join_item1_.id
where join_item1_.itemType=120
order by tran_.id desc
limit 100;

你是对的 - 这要快得多。但只有当我需要的 100 行位于 tran 表的上半部分时。例如,如果我将 limit 子句更改为 limit 376552, 100(执行时间为 46 分 11.87 秒),它很快就会变得很差。

但是 - 你不能(我不认为)在休眠中创建派生表。您可以:

a) 将内部表转换为视图(并创建一个额外的实体类);或 b) 首先运行内表并将结果集放入in 子句中

我选择了选项 b。这是我最初拥有的 Java 代码:

result = getCriteria().setResultTransformer(Criteria.ROOT_ENTITY).setFirstResult(index).setMaxResults(ROW_BUF_SIZE).list();

地点:

getCriteria() 返回一个 org.hibernate.Criteria 对象(指向根实体),其中包括 joinwhereorder by 子句 indexlimit 子句的第一个数字 ROW_BUF_SIZElimit 子句的第二个数字(硬编码为 100)

我改成了:

    Criteria crit = getCriteria().setResultTransformer(Criteria.ROOT_ENTITY).setFirstResult(index).setMaxResults(ROW_BUF_SIZE);
    List<?> ids = crit.setProjection(Property.forName("id")).list();
    crit.setProjection(null).setResultTransformer(Criteria.ROOT_ENTITY);
    if(!ids.isEmpty()) crit.add(Restrictions.in("id", ids)).setFirstResult(0);
    result = crit.list();

所以第 1 行和第 2 行生成内部查询并获取结果集。我重用了相同的 crit 对象 - 所以第 3 行只是恢复在第 2 行创建的投影。第 4 行将添加新的 in 子句(但如果结果集为空则不会 - 因为这会导致 MySQL 异常)。现在,第 5 行得到了与之前代码相同的结果集 - 但它只是得到了 很多 更快(在 2 秒内,而在我的情况下,之前的 20 分钟 47.32 秒)。

【问题讨论】:

【参考方案1】:

您的问题的答案比这里的空间要多得多。我会给你一些线索,以及一些参考资料。

“复合”索引通常很有用。特别是,当查询为 WHERE last = 'James' AND first = 'Rick' 时,INDEX(last, first) 优于 INDEX(last), INDEX(first)

“覆盖”索引是其中 所有 SELECT 所需的列都在单个索引中的索引。 “包含随机场”远离“覆盖”。

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 中缓存东西。 (由于您运行的是 5.6,我假设您的表是 InnoDB,而不是 MyISAM。我在这里说过的几件事需要针对 MyISAM 进行修改。)

【讨论】:

当我知道 where 子句(或 order by 或 group by 等)子句将包含它们 - 但不包含 select 时,我使用复合索引。事实上 - 在我的生产数据库中,我有一个包含姓氏、名字的客户表,并且我有一个复合索引,即 (lastname, firstname) 但我不包括通常出现在选择语句。我描述的问题与 select 语句中的字段有关。清除磁盘缓存只是在运行查询时确保所有事情都相同的一种方式。否则 Linux 会将磁盘缓存到 RAM 中 如果您使用O_DIRECT,则不会;绕过磁盘块的操作系统缓存。 我不知道 O_DIRECT 参数 - 谢谢(虽然我的测试使用的是虚拟机,所以我仍然需要清除虚拟机主机磁盘缓存)。 覆盖索引在我提供的示例中效果很好 - 但在我的生产环境中,有问题的表有 69 列。另外,我们正在使用休眠,它想要全部选择它们 - 所以它不实用。我会更新我原来的问题来说明这一点。然而,我的期望是 MySQL 能够识别 'limit 1' 子句,因此从 1 行获取数据。我在 PostGres 中重复了我的示例,这正是它的作用。我会尽快发布 PostGres 结果。 LIMIT 1只有当有一个索引完全覆盖WHEREORDER BY,并且顺序正确时,才能缩短很多工作。既然有JOIN,那可能是不可能的。

以上是关于MySQL select_expr 意外影响查询执行计划的主要内容,如果未能解决你的问题,请参考以下文章

使用 MySQL 函数或过程提取 SELECT 表达式

MySQL查询数据操作(DQL)

过滤对 crosstab() 查询结果的意外影响

MySQL基础入门学习查询表达式解析 SELECT

Doris -- 查询语法和内置函数

MySQL SELECT语句