为什么 DISTINCT 返回错误的结果

问题说明

在通过 SELECT DISTINCT 进行查询的时候发现结果一直返回空, 然而通过 SELECT 却能返回正常的结果, 如下所示:

-- Server version: 5.6.38-83.0-log Percona Server

mysql > select distinct(name) from t_web_column where column_id IN (946390, 946391, 946392, 946393);
Empty set (0.00 sec)

mysql > select name from t_web_column where column_id IN (946390, 946391, 946392, 946393);
+------+
| name |
+------+
| Test |
| Test |
| Test |
| Test |
+------+

表结构则相对简单, 通过 EXPLAIN 查看, Extra 列显示 Using index for group-by 信息:

CREATE TABLE `t_web_column` (
  `column_id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(200) DEFAULT NULL,
  `column` varchar(200) DEFAULT NULL,
  PRIMARY KEY (`column_id`),
  UNIQUE KEY `index` (`name`,`column`),
  KEY `name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8

mysql > explain distinct(name) from t_web_column where column_id IN (946390, 946391, 946392, 946393)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t_web_column
         type: range
possible_keys: PRIMARY,index
          key: index
      key_len: 603
          ref: NULL
         rows: 13
        Extra: Using where; Using index for group-by

mysql-bug-87598 来看, 这可能是因为 Using index for group-by 特性引起的问题. 下面则简单介绍为什么会出现该问题.

Using index for group-by 是什么

MySQL 的优化器对 GROUP BY 相关的优化中, 通过以下方式实现数据的扫描:

Loose Index Scan(稀疏索引扫描)
Tight Index Scan(紧凑索引扫描)

实际上, SELECT DISTINCT 也是隐含的 GROUP BY 行为, SQL 检索的数据直接可以从索引获取并且是有序的, 则优化器就只需要检索一部分数据即可得到结果. 这种即为稀疏索引扫描, 使用这种方式, 通过 EXPLAIN 查看的时候 Extra 列就会显示以下信息:

Using index for group-by

更多见: mysql-group-by-optimization

为什么返回空结果

参考官方 mysql-bug-87207 给出的信息来看:

Incorrect results could occur on a table with a unique index when the
optimizer chose a loose index scan even though the unique index had
no index extensions.

Problem:
--------
This problem occurs under the following conditions:
1) Table has a unique index.
2) Query checks whether a loose index scan is applicable to
this query. And mistakenly chooses it despite unique
indexes having no index extensions.

产生此类问题需要满足两个条件:

1. 表中含有唯一键;
2. 优化器会检查是否可以使用松散索引扫描, 如果可以, 即便唯一索引没有索引扩展, 优化器也可能选择唯一索引;

对应上述的表结构的唯一键 index(name, column), 在 distinct 查询中, 优化器会优先选择 index 索引. 如果关闭索引扩展, 优化器便会选择常规的主键查询:

mysql > set optimizer_switch='use_index_extensions=off';

mysql > explain distinct(name) from t_web_column where column_id IN (946390, 946391, 946392, 946393)\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: t_web_column
         type: range
possible_keys: PRIMARY,index
          key: PRIMARY
      key_len: 4
          ref: NULL
         rows: 17
        Extra: Using where; Using temporary

解决该问题也很简单, 参考官方的提交 git-commit-7352f13 信息, 在进行稀疏索引扫描的时候, 索引扩展不再适用于唯一索引:

Solution:
---------
Index extensions are not applicable to UNIQUE indexes for
loose index scans.

So Field::is_part_of_actual_key should also consider the
HA_NOSAME flag.

如何避免此类问题?

实际的业务中, 可以通过以下两种方式解决:

临时修改参数

可以在查询前关闭索引扩展, 禁止使用 Using index for group-by:

set optimizer_switch='use_index_extensions=off';

或者直接关闭全局变量:

set global optimizer_switch='use_index_extensions=off';

当然, 我们同样需要了解关闭 use_index_extensions 会产生什么影响? 索引扩展主要用于 InnoDB 引擎中, 其会自动将主键扩展到第二索引中, 加速一些比如 ref, range, index_merge 之类的 sql 查询. 我们在实际的业务使用中, 如果存在合适的索引, 关闭索引扩展是不会有任何问题的, 更多可以参考 mysql-index-extension.

升级版本

参考 mysql-bug-87207, 可以升级到下述或之上的版本彻底解决此类问题:

Fixed in 
  5.6.39
  5.7.21
  8.0.4

参考

mysql-bug-87207
mysql-bug-87598
percona-PS-1820