1

我有一个以 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(通常是一个范围表达式)作为我最左边的列并不是最优的,所以我尝试了 (billing, transaction_code, entry_date) 并解释显示更像 4 -500,000 行。仍然比第一个数字有所改进,但随着我深入挖掘,我开始怀疑:

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

4

1 回答 1

2

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

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

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

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

... ON transaction_classes (transaction_code, transaction_class)

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

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

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,以确定它是否包含在内。

如果谓词 onentry_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 子句,并且它正在提取所有行,那么这种类型的计划(很可能)会比执行排序操作快得多。)

于 2013-07-16T21:00:04.857 回答