【问题标题】:best indexing strategy for sum/case querysum/case 查询的最佳索引策略
【发布时间】:2013-07-15 05:33:09
【问题描述】:

我有一个以 innodb 作为存储引擎的 MySQL 数据库,并且我有许多采用基本形式的查询:

SELECT bd.billing,
  SUM(CASE WHEN tc.transaction_class = 'c'  THEN bd.amount ELSE 0 END) AS charges,
  SUM(CASE WHEN tc.transaction_class = 'a' THEN bd.amount ELSE 0 END) AS adjustments,
  SUM(CASE WHEN tc.transaction_class = 'p' THEN bd.amount ELSE 0 END) AS payments,
  SUM(bd.amount) AS balance_this_month
FROM billing_details bd
JOIN transaction_classes tc ON tc.transaction_code = bd.transaction_code
WHERE bd.entry_date BETWEEN '2013-06-04' AND '2013-07-01'
GROUP BY billing;

我正在尝试为采用这种形式的查询制定索引列的最佳策略。在我开始之前,只有单个列上的索引,并且解释显示正在读取 150 万行(正如您在此处看到的那样,只有一个月的数据量)。

我的第一次尝试将这个数字降低到 ~300,000,这是通过索引(entry_date、billing、transaction_code)实现的。在做了更多阅读(特别是高性能 MySQL)之后,我决定将 entry_date(通常是一个范围表达式)作为我最左边的列并不是最佳的,所以我尝试了(计费、事务代码、条目日期)并解释显示更像 4 -500,000 行。仍然比第一个数字有所改进,但随着我深入挖掘,我开始怀疑:

对于此类查询,我可以合理地期望从最佳索引中得到什么?我猜,因为我正在执行一个聚合函数,它总是会构建一个临时表并进行文件排序......或者是吗?我读得越多,我就越困惑。我的直觉是使用 entry_date 作为最左边的列,因为它是我的 where 子句中的唯一规定。更多的研究让我相信我应该把它放在最正确的位置,因为我正在查询一系列日期。但是我读到的只是真正谈论 where 子句——它只有 entry_date:这样的 sum/case 查询呢?我能否以有益的方式向该索引添加数量,或者除非我重新设计架构/查询,否则我会被我所拥有的东西所困扰吗?

【问题讨论】:

    标签: mysql performance indexing innodb b-tree


    【解决方案1】:

    从您的查询中,不清楚不合格列(例如entry_date)指的是哪个表。 (为了读者的利益,最好的做法是限定查询中的所有列引用,并在将来将同名的列添加到查询中的其他表时证明您的查询不会出现“不明确的列”异常。)

    我将假设不合格的列来自billing_details 表。

    最有可能覆盖索引的候选者是:

    ... ON billing_details (entry_date, billing, transaction_code, amount)
    
    ... ON transaction_classes (transaction_code, transaction_class)
    

    对于两个表访问,EXPLAIN 应在 extra 列中显示“使用索引”。 (如果 transaction_classes 表足够小,索引可能根本不重要。)

    “覆盖索引”意味着可以完全从索引中满足查询,而不需要引用基础表的页面。

    Optimizing Queries with EXPLAIN http://dev.mysql.com/doc/refman/5.5/en/using-explain.html

    这里的策略是在索引中先获取谓词中的列,因此可以进行索引范围扫描操作。我认为其他列的顺序不太重要。下一个帐单列可能有助于 MySQL 的 GROUP BY,但我认为测试会发现这并不重要。

    JOIN 操作可能受益于连接谓词中列上的索引,在这种情况下,在较小的 transaction_classes“查找”表上。但是,如果内部联接实际上是从 billing_details 表中过滤掉行(在 transaction_classes 表中没有匹配值的行,那么我们可能会将其视为过滤谓词,并具有索引。但是我怀疑,存在外键关系,并且此列在 billing_details 表中不为 NULL,因此 billing_details 表中的每一行在 transaction_classes 表中都有一个匹配行。

    如果billing_details 表中的大多数行正在被访问,那么首先在 GROUP BY 中引用列而不是谓词中的列可能会有所帮助,例如:

    ... ON billing_details (billing, entry_date, transaction_code, amount)
    

    在这种情况下,MySQL 可能能够避免“使用文件排序”操作来将行分组在一起。同样,我认为其他列之后的顺序并不重要。在这种情况下,它将是全索引扫描,而不是范围扫描。索引中的每一行都需要检查 entry_date,以确定它是否包含在内。

    如果 entry_date 上的谓词返回一小部分(例如,小于 10%)的行,则首先使用包含该列的索引的访问计划可能会执行得更好。


    总结

    就该查询的性能而言,获取谓词索引可以显着减少识别要包含的行所需的工作量,而无需访问每一行。

    下一个“大石头”是 GROUP BY。如果您正在访问表中的每一行(根本没有谓词),那么最佳索引位于 GROUP BY 子句中的列上。因为这些值是按此列排序的,所以 MySQL 可以避免必须执行排序操作,这在大型集合上可能会很昂贵。

    除了 billing_details 表上的适当索引之外,您可以做的下一个最好的事情是消除与 transaction_classes 表的连接,并仅使用 transaction_code 列中的值。

    CASE 中条件句的处理不会显着增加查询时间。需要时间的是访问需要处理的值,并对行进行排序以便“分组”。


    跟进

    '使用临时的;在计划中使用 filesort' 是由于 GROUP BY 操作。 MySQL 使用 WHERE 子句的索引来减少行数。现在 MySQL 必须对这些行进行排序。这是意料之中的。

    至少“使用索引”表明 MySQL 完全从索引中获取行,无法访问基础表(这通常会提高性能。)

    避免 GROUP BY (AFAIK) 的“使用文件排序”的唯一方法是使用 GROUP BY 中引用的列作为前导列的索引。

    要查看 MySQL 是否会使用这样的索引,您可以尝试禁用 MySQL 将索引用于 WHERE 子句的能力。执行此操作(用于测试)的最简单方法是将 bd.entry_date 列引用包装在函数的 WHERE 子句中。

    更改谓词,并尝试EXPLAIN,使用其中一些变体

    WHERE DATE(bd.entry_date) BETWEEN 
    WHERE DATE(bd.entry_date) + INTERVAL 0 DAY BETWEEN
    WHERE DATE_FORMAT(bd.entry_date,'%Y-%m-%d') BETWEEN
    

    其中一些(或全部)应该足以禁止 MySQL 使用带有 entry_date 的索引来满足 WHERE 子句。

    通过有效禁用该索引作为选项,MySQL 可能决定使用以billing 列作为前导列的索引,以避免“使用文件排序”操作。 (在这种情况下,索引还必须包含 entry_date 列,因为需要在表中的每一行上检查该列,实际上是对所有行的“全面扫描”。

    同样,对于一小部分行,此查询计划可能会更昂贵。这可能会运行得更慢,但它确实需要测试。 (如果查询根本没有 WHERE 子句,并且它正在提取所有行,那么这种类型的计划(很可能)会比执行排序操作快得多。)

    【讨论】:

    • 道歉 - 我希望澄清查询中的任何歧义,尽管您的假设确实是正确的。
    • @user1706938:无需道歉。这就是我们在商店中滚动的方式,限定所有列引用。我已经多次编辑我的答案,以添加说明和更多细节。 (如果我的回答太乏味,我深表歉意。)
    • 一点也不乏味!我将在桌子上做更多的工作来验证两者是否都返回“使用索引”。不幸的是,在这个表上更改索引需要几个小时才能完成,所以我会回来报告!
    • 我对你关于连接谓词的段落有点困惑。或许,澄清一下 - billing_details 表中的大多数行项目将属于“c”、“p”或“a”交易类别。然而,还有一类没有提到。您假设存在外键关系是正确的。是否可以在 WHERE 子句AND tc.transaction_class != 'O' 中添加一个小的优化?
    • @user1706983:你有点困惑不是问题,我有时不是很清楚。考虑是否没有外键关系,并且 billing_detail 中的大多数行在查找表中没有匹配的行。在这种情况下,该 JOIN 操作实际上将用于“过滤”很多行。在这种情况下,我们更有可能认为它更像 WHERE 子句中的谓词,主要消除行。如果是这种情况,那么我们实际上可能想要一个以该列开头的索引。
    猜你喜欢
    • 2021-02-07
    • 1970-01-01
    • 2014-04-02
    • 2022-08-13
    • 2010-10-25
    • 1970-01-01
    • 2018-09-14
    • 2020-06-06
    • 2017-10-03
    相关资源
    最近更新 更多