针对有序范围查询优化MySQL索引

4

我正在寻求帮助,解决我当前正在服务器上运行的一些有攻击性的MySQL查询。我的目标是展示最昂贵的eBay物品,并显示过去一个月内结束时间的物品。

我正在使用MySQL 5.1。

我的查询语句如下('ebay_items'表有约350,000行):

explain SELECT `ebay_items`.* FROM `ebay_items` 
WHERE (endtime > NOW()-INTERVAL 1 MONTH) ORDER BY price desc\G;

产生的结果:

*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: ebay_items
type: range
possible_keys: endtime
key: endtime
key_len: 9
ref: NULL
rows: 71760
Extra: Using where; Using filesort
1 row in set (0.00 sec)

这个查询会导致使用71760行进行昂贵的“文件排序”。

show indexes on ebay_items;

收益率(我只包括了相关的索引,即“endtime”):
*************************** 7. row ***************************
Table: ebay_items
Non_unique: 1
Key_name: endtime
Seq_in_index: 1
Column_name: endtime
Collation: A
Cardinality: 230697
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment: 
*************************** 8. row ***************************
Table: ebay_items
Non_unique: 1
Key_name: endtime
Seq_in_index: 2
Column_name: price
Collation: A
Cardinality: 230697
Sub_part: NULL
Packed: NULL
Null: YES
Index_type: BTREE
Comment: 

只有复合endtime索引(endtime,price)的“endtime”键被使用。据我所知,当与“order by”子句一起处理范围查询时,MySQL不会有效地利用复合索引。
有人发现了解决这个问题的好方法吗?我主要想在数据库层面上解决它(通过更智能地使用索引或模式更改),但我也愿意听取建议。
我可以避免范围查询的一种方法是定期运行后台任务,每隔X小时标记ebay_items上的枚举类型字段为“<1天”,“<1周”,“<1个月”等。我希望以更清晰的方式解决这个问题。
有没有办法以高效的方式执行带有order by子句的MySQL范围查询?非常感谢您的帮助!
编辑:Kohányi Róbert指出我应该澄清查询遇到的确切问题。查询结果导致磁盘I/O在其持续时间内被占用。如果同时运行几个这样的查询,进程会被挤满,机器会锁定。我的假设是文件排序正在消耗I/O。
我还应该提到表正在使用MyISAM引擎。使用InnoDB引擎是否更具性能且I/O密集度更低?再次感谢。

你的 price 列上有索引吗? - Andronicus
你好Riyono。我在price列上没有明确的索引。你认为这会有帮助吗? - Will Sulzer
起初我认为这会有所帮助,但 Kohányi Róbert 很好地解释了 MySQL 在 WHEREGROUP BY 有不同键时的行为。你只需要特定范围内的“一个月前以内”的数据吗?如果是的话,我建议你创建不同的表来保存这个类别中的数据和其他数据。也许可以使用 ebay_items 表来存储活跃的月份,然后定期将超过一个月的项目移动到 ebay_items_history 或类似的表中。 - Andronicus
@WillSulzer 我根据您的新输入重新评估了问题。请查看我的更新答案。 - Kohányi Róbert
1个回答

7

介绍

我很喜欢你的问题,所以我尝试使用MySQL并尝试找到问题的源头。为此,我创建了一些测试。

数据

我使用一个叫做随机数据生成器的工具生成了10万行样本数据(文档有点过时,但仍然可用)。我传递给gendata.pl的配置文件如下。

$tables = {
  rows => [100000],
  names => ['ebay_items'],
  engines => ['MyISAM'],
  pk => ['int auto_increment']
};

$fields = {
  types => ['datetime', 'int'],
  indexes => [undef]
};

$data = {
  numbers => [
    'tinyint unsigned', 
    'smallint unsigned', 
    'smallint unsigned',
    'mediumint unsigned'
  ],
  temporals => ['datetime']
}; 

我已经运行了两个不同的测试批次:一个使用了MyISAM表,另一个使用了InnoDB。(因此,您只需在上面的片段中将MyISAM替换为InnoDB即可。)

表格

该工具创建了一个表格,其中列被称为pkcol_datetimecol_int。 我已经重命名它们以匹配您的表格列。 生成的表格如下所示。

+---------+----------+------+-----+---------+----------------+
| Field   | Type     | Null | Key | Default | Extra          |
+---------+----------+------+-----+---------+----------------+
| endtime | datetime | YES  | MUL | NULL    |                |
| id      | int(11)  | NO   | PRI | NULL    | auto_increment |
| price   | int(11)  | YES  | MUL | NULL    |                |
+---------+----------+------+-----+---------+----------------+

索引

该工具不会自动创建索引,因为我希望手动创建它们。

CREATE INDEX `endtime` ON `ebay_items` (endtime, price);
CREATE INDEX `price` ON `ebay_items` (price, endtime);
CREATE INDEX `endtime_only` ON `ebay_items` (endtime);
CREATE INDEX `price_only` ON `ebay_items` (price);

查询

我使用的查询语句。

SELECT `ebay_items`.* 
FROM `ebay_items`  
FORCE INDEX (`endtime|price|endtime_only|price_only`)
WHERE (`endtime` > '2009-01-01' - INTERVAL 1 MONTH) 
ORDER BY `price` DESC

(有一个索引可以使用四个不同的查询。我在这里使用了 2009-01-01 代替 NOW(),因为该工具似乎生成的日期都在2009年左右。)

解释

下面是针对MyISAM(顶部)和InnoDB(底部)表中每个索引的上述查询的EXPLAIN输出。

endtime

           id: 1
  select_type: SIMPLE
        table: ebay_items
         type: range
possible_keys: endtime
          key: endtime
      key_len: 9
          ref: NULL
         rows: 25261
        Extra: Using where; Using filesort

           id: 1
  select_type: SIMPLE
        table: ebay_items
         type: range
possible_keys: endtime
          key: endtime
      key_len: 9
          ref: NULL
         rows: 21026
        Extra: Using where; Using index; Using filesort

价格

           id: 1
  select_type: SIMPLE
        table: ebay_items
         type: index
possible_keys: NULL
          key: price
      key_len: 14
          ref: NULL
         rows: 100000
        Extra: Using where

         id: 1
  select_type: SIMPLE
        table: ebay_items
         type: index
possible_keys: NULL
          key: price
      key_len: 14
          ref: NULL
         rows: 100226
        Extra: Using where; Using index

endtime_only

           id: 1
  select_type: SIMPLE
        table: ebay_items
         type: range
possible_keys: endtime_only
          key: endtime_only
      key_len: 9
          ref: NULL
         rows: 11666
        Extra: Using where; Using filesort

          id: 1
  select_type: SIMPLE
        table: ebay_items
         type: range
possible_keys: endtime_only
          key: endtime_only
      key_len: 9
          ref: NULL
         rows: 21270
        Extra: Using where; Using filesort

仅价格

           id: 1
  select_type: SIMPLE
        table: ebay_items
         type: index
possible_keys: NULL
          key: price_only
      key_len: 5
          ref: NULL
         rows: 100000
        Extra: Using where

           id: 1
  select_type: SIMPLE
        table: ebay_items
         type: index
possible_keys: NULL
          key: price_only
      key_len: 5
          ref: NULL
         rows: 100226
        Extra: Using where

根据这些信息,我决定在测试中使用endtime_only索引,因为我需要针对MyISAM表和InnoDB表运行查询。但是,正如你所看到的,最合理的endtime索引似乎是最好的选择。

测试

为了测试使用MyISAM表和InnoDB表时查询的效率(关于生成的I/O活动),我编写了以下简单的Java程序。

static final String J = "jdbc:mysql://127.0.0.1:3306/test?user=root&password=root";
static final String Q = "SELECT * FROM ebay_items FORCE INDEX (endtime_only) WHERE (endtime > '2009-01-01'-INTERVAL 1 MONTH) ORDER BY price desc;";

public static void main(String[] args) throws InterruptedException {
  for (int i = 0; i < 1000; i++)
    try (Connection c = DriverManager.getConnection(J);
        Statement s = c.createStatement()) {
      TimeUnit.MILLISECONDS.sleep(10L);
      s.execute(Q);
    } catch (SQLException ex) {
      ex.printStackTrace();
    }
}

设置

我在戴尔Vostro 1015笔记本电脑上运行了Windows二进制MySQL 5.5,处理器为Intel Core Duo T6670 @ 2.20 GHz,内存为4 GB。Java程序通过TCP/IP与MySQL服务器进程通信。

状态

我使用Process Explorer捕获了针对MyISAM和InnoDB表运行测试之前和之后的mysqld进程状态。

之前

mysqld Performance tab

mysqld Disk and Network tab

之后—MyISAM

mysqld Performance tab / MyISAM

mysqld Disk and Network tab / MyISAM

之后—InnoDB

mysqld Performance tab / InnoDB

mysqld Disk and Network tab / InnoDB

结论

基本上,两次运行只有在使用MyISAM引擎的表时才存在大量单独的I/O读取。这两个测试都运行了50-60秒。在使用MyISAM时,CPU的最大负载约为42%,而在使用InnoDB时约为38%。

我不太确定高数量的I/O读取的影响是什么,但在这种情况下,较小的数字更好(可能)。如果您的表中有一些其他列(而不是您指定的列),并且具有一些非默认的MySQL配置(关于缓冲区大小等),则可能会使用磁盘资源。


非常感谢您深思熟虑的回答。我已经通过描述中的查询澄清了我的问题。该查询会占用我的磁盘I/O,并与其他资源发生冲突。您提到第三次运行减少了文件排序所需遍历的行数,这是一个很好的观点。我需要对单列“endtime”查询进行更多实验,并向您反馈结果。 - Will Sulzer
@WillSulzer 我猜测 filesort 是导致 I/O 问题的罪魁祸首。你可以对实际的 filesort 操作持续时间和排序行数进行基准测试(请参阅手册中的 filesort Probes 部分)。相对于每次运行,使用单独的索引查询会带来多少性能提升或下降也是很有趣的。缓存是否是一个选项?如果不是,你可能需要重新设计方案,但为了帮助你更好地解决问题,我们需要更多的细节信息。 - Kohányi Róbert
这对我来说有点过度了,你有什么确凿的证据表明InnoDB做得更好吗? - ajreal
@ajreal 我没有其他的东西了。而且,我并不是说InnoDB做得更好(我在之前的回答中曾经这样说过,但我删掉了),只是使用MyISAM引擎与回答中描述的表和数据,会产生很多读取操作。OP说他遇到了I/O卡顿,也许这与此有关。我尝试复制他的设置和问题,但我能做到的最好的就是这个回答。如果你有比我展示的更有说服力的东西,请与我们分享。 - Kohányi Róbert
遗憾的是,实际上,我一直在等待你的回答 :) - ajreal
@ajreal 我认为原帖的设置更加复杂(更多的表、列、数据、用户、并发查询、更新和自定义MySQL设置),我无法复制。话虽如此,我并不是一个DBA或者SQL专家。这就是我在我的知识范围内所能做到的。对此我表示抱歉。尽管如此,我希望我的回答能够对某些问题有所启示。 - Kohányi Róbert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接