【问题标题】:Optimize query for table with hundreds of millions of rows优化数亿行表的查询
【发布时间】:2012-10-13 10:36:08
【问题描述】:

这感觉就像是“为我做作业”之类的问题,但我真的被困在这里,试图让这个查询对一个有很多行的表快速运行。 Here's a SQLFiddle 显示架构(或多或少)。

我已经使用过索引,试图获得能够显示所有必需列但没有取得多大成功的东西。这是create

CREATE TABLE `AuditEvent` (
    `auditEventId` bigint(20) NOT NULL AUTO_INCREMENT,
    `eventTime` datetime NOT NULL,
    `target1Id` int(11) DEFAULT NULL,
    `target1Name` varchar(100) DEFAULT NULL,
    `target2Id` int(11) DEFAULT NULL,
    `target2Name` varchar(100) DEFAULT NULL,
    `clientId` int(11) NOT NULL DEFAULT '1',
    `type` int(11) not null,
    PRIMARY KEY (`auditEventId`),
    KEY `Transactions` (`clientId`,`eventTime`,`target1Id`,`type`),
    KEY `TransactionsJoin` (`auditEventId`, `clientId`,`eventTime`,`target1Id`,`type`)
)

和(一个版本)select

select ae.target1Id, ae.type, count(*)
from AuditEvent ae
where ae.clientId=4
    and (ae.eventTime between '2011-09-01 03:00:00' and '2012-09-30 23:57:00')
group by ae.target1Id, ae.type;

我最终也得到了“使用临时文件”和“使用文件排序”。我尝试删除count(*) 并改用select distinct,这不会导致“使用文件排序”。如果有办法返回join 以获取计数,这可能没问题。

最初,决定跟踪创建审计记录时存在的目标的 target1Name 和 target2Name。我也需要这些名字(最新的就可以了)。

目前,查询(上图,缺少 target1Name 和 target2Name 列)在大约 5 秒内运行约 2400 万条记录。我们的目标是数亿,我们希望查询继续沿着这些路线执行(希望将其保持在 1-2 分钟以内,但我们希望它做得更好),但我担心的是一次我们达到了它不会达到的大量数据(正在模拟更多的行)。

我不确定获取附加字段的最佳策略。如果我将列直接添加到select 中,我会丢失查询中的“使用索引”。我尝试将join 返回到表中,它保留了“使用索引”但大约需要 20 秒。

我确实尝试将 eventTime 列更改为 int 而不是 datetime,但这似乎并没有影响索引的使用或时间。

【问题讨论】:

  • 你目前的查询时机是什么,“快速”下你了解什么?
  • 你有关于 clientId 和 eventTime 的索引吗?还要验证您是否有那些您正在使用 eventTime 索引并且没有进行全表扫描。
  • 您是否尝试过以下解决方法:1)将 DATETIME 更改为 INT; 2)通过client_id进行分区; 3) 数据现实性如何:数据是否有可能存在时间,并且可以移动到某种历史表中?
  • 为什么没有人提到所使用的存储引擎和一切背后的硬件?通过设置正确的每个索引只能做这么多,其余的取决于硬件。并且不使用具有大 buffer_pool 的 InnoDB 意味着大量磁盘 IO 和具有约 400ish IOPS 的机械驱动器 - 当然,数百万行的性能会很糟糕。
  • @NickSpacek - here is some interesting reading,但大多数情况下您想要做的是增加名为 innodb_buffer_pool 的变量 - 我通常将它放在可用 RAM 的 90% 左右。另一件事是您应该拥有能够超过 500 IOPS 的快速磁盘子系统(SSD 消灭那里的机械驱动器,范围从 40k IOPS 向上)。

标签: mysql query-optimization


【解决方案1】:

正如您可能理解的那样,这里的问题是范围条件 ae.eventTime between '2011-09-01 03:00:00' and '2012-09-30 23:57:00' (一如既往)破坏了 Transactions 索引的有效使用(即索引实际上仅用于 clientId 方程和第一部分范围条件和索引不用于分组)。

大多数情况下,解决方案是用相等检查替换范围条件(在您的情况下,引入period 列,将eventTime 分组并用period IN (1,2,3,4,5) 替换BETWEEN 子句)。但这可能会成为您餐桌的开销。

您可能会尝试的另一个解决方案是添加另一个索引(如果不再使用,可能替换Transactions):(clientId, target1Id, type, eventTime),并使用以下查询:

SELECT
  ae.target1Id,
  ae.type,
  COUNT(
    NULLIF(ae.eventTime BETWEEN '2011-09-01 03:00:00' 
                            AND '2012-09-30 23:57:00', 0)
  ) as cnt,
FROM AuditEvent ae
WHERE ae.clientId=4
GROUP BY ae.target1Id, ae.type;

这样,您将 a) 将范围条件移到末尾,b) 允许使用索引进行分组,c) 使索引成为查询的 覆盖索引(即查询不需要磁盘IO操作)

UPD1: 对不起,昨天我没有仔细看你的帖子,没有注意到你的问题是检索target1Nametarget2Name。首先,我不确定您是否正确理解Using index 的含义。 Using index 的缺失并不意味着查询没有使用索引,Using index 意味着索引本身包含足够的数据来执行子查询(即索引正在覆盖)。由于target1Nametarget2Name 不包含在任何索引中,因此获取它们的子查询不会有Using index

如果您的问题只是如何将这两个字段添加到您的查询中(您认为这足够快),那么只需尝试以下操作:

SELECT a1.target1Id, a1.type, cnt, target1Name, target2Name
FROM (
  select ae.target1Id, ae.type, count(*) as cnt, MAX(auditEventId) as max_id
  from AuditEvent ae
  where ae.clientId=4
      and (ae.eventTime between '2011-09-01 03:00:00' and '2012-09-30 23:57:00')
  group by ae.target1Id, ae.type) as a1
JOIN AuditEvent a2 ON a1.max_id = a2.auditEventId
;

【讨论】:

  • 实际上两者都是有效的答案;我希望提高查询的性能/获得有关构建它的不同方法的建议,此外我想知道检索非索引列的最佳方法。与我尝试过的查询相比,您的两个建议都提高了性能!
  • @nickSpacek,好的,我很高兴它有帮助 =)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-08-23
  • 2019-06-12
  • 2013-08-12
  • 2019-04-17
  • 2015-01-14
  • 1970-01-01
  • 2013-08-17
相关资源
最近更新 更多