MySQL 在结合 join 和 range 时不使用整个索引

Posted

技术标签:

【中文标题】MySQL 在结合 join 和 range 时不使用整个索引【英文标题】:MySQL doesn't use entire index when combining join and range 【发布时间】:2017-08-29 07:19:37 【问题描述】:

我正在尝试优化连接两个表并应用范围条件的简单查询。 从下面的解释计划中,您可以看到索引 inv_quantity_on_hand 仅被部分使用(4 个字节,仅用于第一列 - inv_item_sk)。我希望使用整个索引,因为索引的第二部分 (inv_quantity_on_hand) 用于范围条件的 WHERE 子句中。

请注意,这只发生在连接和范围条件下。将范围条件替换为常量相等比较 (inv_quantity_on_hand = 5) 将更改解释计划,mysql 将使用整个索引。

这似乎是这个错误的一个实例:https://bugs.mysql.com/bug.php?id=8569。

我用 MySQL 5.7 检查过它,它仍然发生。请问有人能想出一个好的解决方法吗?

架构结构:

CREATE TABLE `inventory` (
    `inv_date_sk` INT(11) NOT NULL,
    `inv_item_sk` INT(11) NOT NULL,
    `inv_warehouse_sk` INT(11) NOT NULL,
    `inv_quantity_on_hand` INT(11) DEFAULT NULL,
    PRIMARY KEY (`inv_date_sk` , `inv_item_sk` , `inv_warehouse_sk`),
    KEY `inv_w` (`inv_warehouse_sk`),
    KEY `inv_i` (`inv_item_sk`),
    KEY `inv_quantity_on_hand_index` (`inv_item_sk` , `inv_quantity_on_hand`),
    CONSTRAINT `inv_d` FOREIGN KEY (`inv_date_sk`)
        REFERENCES `date_dim` (`d_date_sk`)
        ON DELETE NO ACTION ON UPDATE NO ACTION,
    CONSTRAINT `inv_i` FOREIGN KEY (`inv_item_sk`)
        REFERENCES `item` (`i_item_sk`)
        ON DELETE NO ACTION ON UPDATE NO ACTION,
    CONSTRAINT `inv_w` FOREIGN KEY (`inv_warehouse_sk`)
        REFERENCES `warehouse` (`w_warehouse_sk`)
        ON DELETE NO ACTION ON UPDATE NO ACTION
)  ENGINE=INNODB DEFAULT CHARSET=UTF8

CREATE TABLE `item` (
    `i_item_sk` INT(11) NOT NULL,
    `i_item_id` CHAR(16) NOT NULL,
    `i_rec_start_date` DATE DEFAULT NULL,
    `i_rec_end_date` DATE DEFAULT NULL,
    `i_item_desc` VARCHAR(200) DEFAULT NULL,
    `i_current_price` DECIMAL(7 , 2 ) DEFAULT NULL,
    `i_wholesale_cost` DECIMAL(7 , 2 ) DEFAULT NULL,
    `i_brand_id` INT(11) DEFAULT NULL,
    `i_brand` CHAR(50) DEFAULT NULL,
    `i_class_id` INT(11) DEFAULT NULL,
    `i_class` CHAR(50) DEFAULT NULL,
    `i_category_id` INT(11) DEFAULT NULL,
    `i_category` CHAR(50) DEFAULT NULL,
    `i_manufact_id` INT(11) DEFAULT NULL,
    `i_manufact` CHAR(50) DEFAULT NULL,
    `i_size` CHAR(20) DEFAULT NULL,
    `i_formulation` CHAR(20) DEFAULT NULL,
    `i_color` CHAR(20) DEFAULT NULL,
    `i_units` CHAR(10) DEFAULT NULL,
    `i_container` CHAR(10) DEFAULT NULL,
    `i_manager_id` INT(11) DEFAULT NULL,
    `i_product_name` CHAR(50) DEFAULT NULL,
    PRIMARY KEY (`i_item_sk`),
    KEY `item_color_index` (`i_color`)
)  ENGINE=INNODB DEFAULT CHARSET=UTF8

查询:

SELECT 
    *
FROM
    inventory
        INNER JOIN
    item ON inventory.inv_item_sk = item.i_item_sk
WHERE
    inventory.inv_quantity_on_hand > 100
        AND item.i_color = 'red';

执行计划:

# id | select_type | table     | partitions | type | possible_keys                    | key                        | key_len | ref                  | rows | filtered |  Extra
-----+-------------+-----------+------------+------+----------------------------------+----------------------------+---------+----------------------+-----------------+-------------------------
1    | SIMPLE      | item      |            | ref  | PRIMARY,item_color_index         | item_color_index           | 61      | const                | 384  | 100.00   |  
1    | SIMPLE      | inventory |            | ref  | inv_i,inv_quantity_on_hand_index | inv_quantity_on_hand_index | 4       | tpcds.item.i_item_sk | 615  |  33.33   | Using where; Using index

【问题讨论】:

没有实际问题,您认为 MySQL 应该“使用”整个索引,没有问题,没有解决方法,一切都很好。这就是它与范围一起工作的方式。我不确定您为什么要问这个问题,除了您希望 MySQL 的解释产生不同的统计数据之外,是否存在实际问题? 是的,我认为有问题。我将提供一个日期示例,因为它可能更简单 - 假设范围将数据过滤为仅上个月的数据,因此使用索引可以轻松完成。相反,现在应该扫描全年的数据。与上面的查询相同,有一个索引,MySQL 没有使用它的一部分,没有任何我能理解的充分理由。 没有问题。你不能到处乱扔索引并期望 MySQL 用它做一些神奇的事情并使用它的“更少”或“更多”。你的条件是“where item.i_color = 'red' AND inventory.inv_quantity_on_hand > 100”——你认为 MySQL 到底要做什么?准确地知道,没有任何操作,哪些确切的记录是大于100和red的记录?索引并不神奇,它是一个简单的有序数据结构,你不能指望它与相等运算符一样使用范围。这里没有实际问题,它按预期工作。 感谢您的反馈。也许我没有正确解释自己。查看应用于库存表的条件时,ON 子句使用 inv_item_sk,而 WHERE 子句使用 inv_quantity_on_hand。所以,我不确定我还没有看到解释为什么 MySQL 会选择不使用完全相同的索引来允许它完全搜索这两个列(inv_quantity_on_hand_index),而是选择相同的索引并且只使用第一部分呢?顺便说一句,将范围运算符替换为等于运算符将使 MySQL 使用完整索引。 【参考方案1】:

多列索引存储为不同列的连接。 我认为 MySQL 不会评估多列索引中的子字符串以进行比较。当您使用 inv_quantity_on_hand = 5(或 in(1,2,3,4,5))时,MySQL 将根据您的输入构建字符串以进行比较,以便它可以使用完整索引。使用 between 或 > 基本上提供了无限数量的可能子字符串进行比较(在检查数据类型之前)。构建所有这些字符串并比较它们将比使用第一列的索引(on-clause)花费更多的时间,然后检查 inv_quantity_on_hand “使用 where”。

【讨论】:

谢谢,但是库存表中的相关列都是数字。该表中不涉及任何字符串。 MySQL 文档在 8.3.5 多列索引下声明:“多列索引可以被认为是一个排序数组,其中的行包含通过连接索引列的值创建的值。”连接索引列的值后,结果可能会存储为字符串(MySQL 怎么能在不构建字符串的情况下连接两个整数?) 感谢您的澄清。当检查相同的条件并将范围条件替换为“等于”条件时,将使用多列索引。这似乎是一个只有 join + range 条件的问题,它与我理解的第 8.3.5 节中的实现并不相加。 是的,只有在使用范围条件时才会出现该问题。我很高兴能帮助你。其次,您可以添加一列或多列来标记所有超过阈值(在本例中 >100)的记录并动态更新它们(但这取决于表、使用情况和环境)。然后将标志列作为键的一部分或添加尽可能多的键来覆盖所有标志列(使用大表、高使用率和多个标志列,这可能会变得不方便,但使用较小的表 [ 【参考方案2】:

使用 BETWEEN 条件代替条件运算符

【讨论】:

试过了,但没用。相同的执行计划。无论如何谢谢:)

以上是关于MySQL 在结合 join 和 range 时不使用整个索引的主要内容,如果未能解决你的问题,请参考以下文章

MYSQL - 将 SUM 与 JOIN 结合使用

PDO fetchAll() - json_encode在使用JOIN时不起作用

MySQL结合where子句的join性能

MySql如何将LEFT JOIN查询与NOT IN查询结合起来

mysql关联left join条件on和where条件的区别及结合coalesce函数

MySQL 中的各种 JOIN