【问题标题】:Poor query performance with index compared to without index与没有索引相比,有索引的查询性能较差
【发布时间】:2018-05-15 14:17:29
【问题描述】:

我正在使用 MySQL 5.6 并且有一个由 DATE 类型的“network_date”列分区的表(每天都有 一个分区,例如“2018-05-01”,每个分区包含大约 400,000 行)。该表有两个复合索引(不是唯一的),其中也包括“network_date”列(首先按 6 列的顺序)。索引是:

  1. _daily_ad_level_demand_idx:network_date、publisher_network_id、display_advertiser_id、business_rule_id、campaign_id、ad_id
  2. _daily_ad_level_supply_idx:network_date、publisher_network_id、publisher_id、widget_id

但是,根据 EXPLAIN 命令,在运行以下查询时:

EXPLAIN EXTENDED SELECT 
    network_date,
    SUM(COALESCE(ad_view, 0)) AS ad_view,
    SUM(COALESCE(ad_spend_network, 0)) AS ad_spend_network,
    SUM(COALESCE(ad_click, 0)) AS ad_click,
    campaign_id,
    display_advertiser_id,
    publisher_network_id,
    ad_id
FROM
    daily_ad_level
WHERE
    (publisher_network_id = 16020)
    AND network_date BETWEEN STR_TO_DATE('2018-04-15 00:00:00.000000',
        '%Y-%m-%d %H:%i:%S.%f') AND STR_TO_DATE('2018-05-12 23:59:59.999000',
        '%Y-%m-%d %H:%i:%S.%f')
GROUP BY campaign_id, network_date, display_advertiser_id, 
publisher_network_id, ad_id

优化器没有选择索引,并且正在进行全表扫描。 你可以在这里看到结果: EXPLAIN command output with 'network_date' included in index

在做了一些研究并对此感到困惑之后,我决定从索引中删除“network_date”列 - 分区修剪无论如何都应该进行必要的查找,因此将它包含在索引中似乎是多余的。再次运行 EXPLAIN 命令显示现在正在选择一个索引。你可以在这里看到结果: EXPLAIN command output with no 'network_date' included in index

在查询持续时间方面,优化器选择索引时性能下降:从 9.75 秒到 12.4 秒...问题是为什么???

分析 first explain 命令输出(没有使用索引的那个),可以看到 'filtered' 和 'rows' 列的值分别为 50.00 和 4,474,281。是不是优化器推断出全表扫描比使用只消除大约一半行的索引便宜? 如果是这样,我希望在第二种情况下会出现相同的行为,但事实并非如此:优化器选择了一个索引并且查询执行得很差。

有人知道可能导致这种行为的原因吗?

【问题讨论】:

  • 在这里猜测:包含network_date 的优化器可以看到您的条件不是很严格,因此它选择表扫描。没有它,优化器会看到publisher_network_id 可能非常严格,所以它使用索引方法,即使实际上它更慢。
  • 你能显示SHOW CREATE TABLE daily_ad_level
  • @RaymondNijland 创建表脚本太长...该表有 500 个分区和 70 列。我认为重要的是要提到该表有 no 主键
  • 我不知道该表在哪个引擎上运行。但我知道在 InnoDB 表上没有主键是不好的...我在 dba 上发布了我的一个小警告帖子stackexchange 关于为什么你应该在 InnoDB 表中始终拥有 PRIMARY KEY 或 UNIQUE 键dba.stackexchange.com/questions/48072/…
  • @RaymondNijland 忘了说:InnoDB 存储引擎

标签: mysql database database-performance query-performance


【解决方案1】:

在阅读了你们的 cmets 家伙之后,我想到 group by 列的顺序会显着影响查询性能,也就是说,如果我按列重新排序 group 以匹配索引列订单(并添加查询中当前缺少的额外列 - business_rule_id) - 结果在 0.23 秒内获取,而之前为 9.23 秒!此外,优化器这次选择了正确的索引。这是修改后的查询:

SELECT 
    network_date,
    SUM(COALESCE(ad_view, 0)) AS ad_view,
    SUM(COALESCE(ad_spend_network, 0)) AS ad_spend_network,
    SUM(COALESCE(ad_click, 0)) AS ad_click,
    campaign_id,
    display_advertiser_id,
    publisher_network_id,
    ad_id
FROM
    daily_ad_level
WHERE
    (publisher_network_id = 16020)
    AND network_date BETWEEN STR_TO_DATE('2018-04-15 00:00:00.000000',
        '%Y-%m-%d %H:%i:%S.%f') AND STR_TO_DATE('2018-05-12 23:59:59.999000',
        '%Y-%m-%d %H:%i:%S.%f')
    GROUP BY  network_date, publisher_network_id ,display_advertiser_id, 
    business_rule_id, campaign_id, ad_id ;

你可以在这里看到结果截图:Optimized Query Output

这是未优化的结果截图:Unoptimized Query Output

虽然结果并不完全相同(由于 group by 子句中添加了 business_rule_id 列),但它仍然很好地说明了优化器的“思维方式”,因此通过正确的调整,所需的结果可以实现。

各位大神指点,谢谢!

【讨论】:

  • business_rule_id 在哪个表中?
【解决方案2】:

您应该首先使用相等运算符 (=) 对字段进行索引。然后,您应该使用范围运算符(>、

尝试添加这个索引:

ALTER TABLE `daily_ad_level` ADD INDEX `daily_ad_level_idx_id_date` (`publisher_network_id`,`network_date`);

【讨论】:

  • “我没有理由为组中的列建立索引,因为我认为优化器不会选择它们”阅读此dev.mysql.com/doc/refman/8.0/en/group-by-optimization.html
  • @RaymondNijland,你指的是哪一部分?根据我的经验(以及 MySQL 文档的说明),一旦索引中有一个“范围”列,就没有必要从 GROUP BY 子句中添加列,因为它们无论如何都不会被使用。跨度>
  • 函数不在列上,而是在硬编码值上,然后与列值进行比较,所以在这种情况下是可以的。
  • 如果您不使用索引优化 GROUP BY,MySQL 优化器将始终使用“文件排序”或“临时表”或解决大量记录时速度慢的结果。 .
  • @Mihai,谢谢,尽管我提到了这一点,但我还是忽略了它,哈哈。编辑了我的答案。
【解决方案3】:

我建议添加两个索引并重写查询。

ALTER TABLE daily_ad_level
ADD INDEX daily_ad_level_idx_id_date (publisher_network_id, network_date);

还有

ALTER TABLE daily_ad_level
ADD INDEX daily_ad_level_idx_campaign_id_network_date_display_advertiser_id_publisher_network_id_ad_id (campaign_id, network_date, display_advertiser_id, 
publisher_network_id, ad_id);

查询重写

我假设 ad_id 列是您表中的主键

SELECT
    network_date,
    SUM(COALESCE(ad_view, 0)) AS ad_view,
    SUM(COALESCE(ad_spend_network, 0)) AS ad_spend_network,
    SUM(COALESCE(ad_click, 0)) AS ad_click,
    campaign_id,
    display_advertiser_id,
    publisher_network_id,
    ad_id
FROM (

    SELECT
     ad_id
    FROM  
     daily_ad_level
    WHERE
          publisher_network_id = 16020
        AND
          network_date BETWEEN STR_TO_DATE('2018-04-15 00:00:00.000000',
            '%Y-%m-%d %H:%i:%S.%f') AND STR_TO_DATE('2018-05-12 23:59:59.999000',
            '%Y-%m-%d %H:%i:%S.%f') 
    ) AS daily_ad_level_filterd

    INNER JOIN 
     daily_ad_level
    ON
     daily_ad_level_filterd.ad_id = daily_ad_level.ad_id 

    GROUP BY 
      campaign_id, network_date, display_advertiser_id, 
    publisher_network_id, ad_id

【讨论】:

    【解决方案4】:

    第 1 步 - 更好的索引

    不要以network_date 开始索引,以它结束它们。为什么?一般来说,一旦你达到了“范围”测试,你就不能使用更多的索引列。

    您的第一个查询只需要

    INDEX(publisher_network_id, network_date)  -- in this order
    

    当优化大于可以缓存在 RAM(buffer_pool)中的表时,压倒性的考虑是磁盘命中。该索引可最大限度地减少磁盘命中次数。

    无关:我认为没有必要将日期时间包装在 STR_TO_DATE 中。

    第 2 步 - 折腾分区如果不需要

    您是否出于某种原因使用PARTITIONs

    • 性能——不太可能有帮助;肯定不比我刚才推荐的INDEX 好。
    • 清除旧记录 -- 一个很好的理由。

    我无法分析您查询的其余部分,因为不知道每列在哪个表中。例如,如果 GROUP BY 列不都在一个表中,则 没有方式为此使用索引。

    如果您的表中有超过 50 个分区,那么您会遇到其他低效率问题。在这种情况下建议切换到每周或每月分区。

    还有其他我们应该考虑的查询吗?

    第 3 步 - 更好的集群主键

    • 摆脱分区(除非您需要它进行清除),并且
    • 使PRIMARY KEY (publisher_network_id, network_date) 开头。 (添加 id 或使其独一无二的任何必要内容,因为 PK 必须是唯一的。)

    为什么会更好?然后所有必要的行连续(“聚集”)在一起,从而最大限度地减少磁盘命中次数。

    当然,GROUP BY 会有一个临时表、排序等,但这实际上可能发生在 RAM 中。

    第 4 步 - 汇总表

    数据仓库涉及“报告”。由于需要读取多少行,从原始数据中提取它们的成本非常高。构建和维护一个汇总表,其中包含每一天的每个键组合的行。然后针对该表运行“报告”;它的运行速度可能快 10

    关于汇总表的更多信息:http://mysql.rjweb.org/doc.php/summarytables

    【讨论】:

      猜你喜欢
      • 2014-08-08
      • 2016-08-26
      • 1970-01-01
      • 2012-05-12
      • 1970-01-01
      • 2010-12-04
      • 2012-02-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多