【问题标题】:MySQL using slower index for queryMySQL 使用较慢的索引进行查询
【发布时间】:2020-08-10 14:16:46
【问题描述】:

我有以下疑问:

select  *
    from  test_table
    where  app_id = 521
      and  is_deleted=0
      and  category in (7650)
      AND  created_timestamp >= '2020-07-28 18:19:26'
      AND  created_timestamp <= '2020-08-04 18:19:26'
    ORDER BY  created_timestamp desc
    limit  30

所有四个字段,app_id、is_deleted、category 和 created_timestamp 都被索引。但是,app_idis_deleted 的基数非常小(各 3 个)。 category 字段分布均匀,但 created_timestamp 似乎是此查询的一个非常好的索引选择。

但是,MySQL 没有使用created_timestamp 索引,因此需要 4 秒才能返回。如果我强制 MySQL 使用 USE INDEX (created_timestamp) 使用 created_timestamp 索引,它会在 40 毫秒内返回。

我检查了解释命令的输出以了解发生这种情况的原因,发现 MySQL 正在使用以下参数执行查询:

自动索引决策,耗时 > 4s

type: index_merge
key: category,app_id,is_deleted
rows: 10250
filtered: 0.36
Using intersect(category,app_id,is_deleted); Using where; Using filesort

强制索引使用:

Use index created_timestamp, takes < 50ms
type: range
key: created_timestamp
rows: 47000
filtered: 0.50
Using index condition; Using where; Backward index scan

MySQL 可能认为扫描的行数越少越好,这也是有道理的,但是为什么在这种情况下查询需要永远返回呢?如何修复此查询?

【问题讨论】:

  • Using intersect 就像做三个查询,找到表的几个子集,找到所有三个子集中都存在的行。您应该考虑按该顺序在(app_id, is_deleted, created_timestamp, category) 上定义多列索引。
  • @BillKarwin - 如果IN 有多个值,我可能会同意您的订购。当只有一个id时,会优化为=,此时,category明显优于日期范围。
  • @RickJames Putting created_timestamp 首先消除了文件排序。第四列无论哪种方式都不能作为 SQL 层查找进行搜索,但至少可以通过 InnoDB 索引条件下推进行过滤。
  • @BillKarwin - 对于category IN (7650),其优化与category = 7650 相同,它将通过category
  • 我假设它在查询的一般情况下会有多个值。

标签: mysql indexing


【解决方案1】:

using 交集using filesort 都是为了性能而付出的代价。最好能消除这些。

这是一个测试。我假设IN ( ... ) 谓词有时可能有多个值,因此它将是 range 类型的查询,并且不能优化为相等。

CREATE TABLE `test_table` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `app_id` int(11) NOT NULL,
  `is_deleted` tinyint(4) NOT NULL DEFAULT '0',
  `category` int(11) NOT NULL,
  `created_timestamp` timestamp NOT NULL,
  `other` text,
  PRIMARY KEY (`id`),
  KEY `a_is_ct_c` (`app_id`,`is_deleted`,`created_timestamp`,`category`),
  KEY `a_is_c_ct` (`app_id`,`is_deleted`,`category`,`created_timestamp`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

如果我们使用您的查询并提示优化器使用第一个索引(category 之前的created_timestamp),我们会得到一个同时消除两者的查询:

EXPLAIN SELECT * FROM test_table FORCE INDEX (a_is_ct_c) 
WHERE  app_id = 521
  AND  is_deleted=0
  AND  category in (7650,7651,7652)
  AND  created_timestamp >= '2020-07-28 18:19:26' 
  AND  created_timestamp <= '2020-08-04 18:19:26'
ORDER BY created_timestamp DESC\G

           id: 1
  select_type: SIMPLE
        table: test_table
   partitions: NULL
         type: range
possible_keys: a_is_ct_c
          key: a_is_ct_c
      key_len: 13
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using index condition

而如果我们使用第二个索引(categorycreated_timestamp 之前),那么至少使用交集消失了,但我们仍然有一个文件排序:

EXPLAIN SELECT * FROM test_table FORCE INDEX (a_is_c_ct) 
WHERE  app_id = 521
  AND  is_deleted=0
  AND  category in (7650,7651,7652)
  AND  created_timestamp >= '2020-07-28 18:19:26' 
  AND  created_timestamp <= '2020-08-04 18:19:26'
ORDER BY created_timestamp DESC\G

               id: 1
  select_type: SIMPLE
        table: test_table
   partitions: NULL
         type: range
possible_keys: a_is_c_ct
          key: a_is_c_ct
      key_len: 13
          ref: NULL
         rows: 3
     filtered: 100.00
        Extra: Using index condition; Using filesort

“使用索引条件”是 InnoDB 在存储引擎级别过滤第四列的功能。这称为Index condition pushdown

【讨论】:

    【解决方案2】:

    给定查询的最佳索引,以及其他一些:

    INDEX(app_id, is_deleted,  -- put first, in either order
          category,            -- in this position, assuming it might have multiple INs
          created_timestamp)   -- a range; last.
    

    “索引合并相交”可能总是比拥有一个等效的复合索引更糟糕。

    请注意,优化器的另一种选择是忽略WHERE 而专注于ORDER BY,尤其是因为LIMIT 30。然而,这是非常危险的。它可能不得不扫描整个表而不找到所需的 30 行。显然,它必须查看大约 47000 行才能找到第 30 行。

    使用上面的索引,它只会触及 30 行(或更少)。

    “所有四个字段,...都已编入索引。” -- 这是一个常见的误解,尤其是数据库新手。一个查询使用多个索引非常罕见。因此,最好尝试“复合”索引,这可能会更好。

    如何为给定的SELECT 构建最佳INDEXhttp://mysql.rjweb.org/doc.php/index_cookbook_mysql

    【讨论】:

    • 我会把is_deleted放在第一位,因为这个列很可能出现在大多数查询中,您只想显示“未删除”实体。因此,该索引对任何查询都很有用,可以针对 is_deleted 进行测试。
    • @dognose - 复合索引中各个列的“基数”无关紧要。可以这样想:4 列将 [逻辑上] 连接成一个长字符串,并根据该长字符串对索引进行排序。
    • @dognose - 或者您是否指出某些查询不包括 is_deleted?我将提供另一个省略它的复合索引。 (警告:这不是通用规则,但可能对您的表格有用。)
    • 我知道它是如何工作的。这里不讨论基数。想一想,在一个系统中,实体没有“删除”,而只是标记为已删除,如果您访问实时数据,每个查询都包含where deleted = 0。因此,将其放在首位将允许索引用于几乎每个查询,即使没有给出 app-id(app-id 对我来说听起来像是一个更特殊的视图/过滤器,就像所有已知应用程序的子列表。 ) 只是说,我把它放在第一位,并不意味着它是正确的。
    • @nimbudew - IN 和范围的混合 - 没有好的答案。在某些情况下,一份订单有效;另一个中的另一个。对于 IN 中的 1 项,类别需要放在首位。另见我与比尔的讨论。提供两个索引可能是有益的——优化器可以选择,它可能选择更好的。
    猜你喜欢
    • 1970-01-01
    • 2012-06-11
    • 1970-01-01
    • 2012-02-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多