【问题标题】:Improving MySQL Query Speeds - 150,000+ Rows Returned Slows Query提高 MySQL 查询速度 - 返回 150,000 多行会减慢查询速度
【发布时间】:2019-08-13 13:52:12
【问题描述】:

您好,我目前有一个查询需要 11(秒)才能运行。我有一个显示在网站上的报告,该网站运行 4 个不同的查询,这些查询是相似的,每个都需要 11(秒)才能运行。我真的不希望客户等待所有这些查询运行并显示数据。

我正在使用 4 个不同的 AJAX 请求来调用 API 以获取我需要的数据,这些请求都是同时启动的,但是查询一个接一个地运行。如果有办法让这些查询一次全部运行(并行),因此总加载时间只有 11(秒),这也可以解决我的问题,但我认为这是不可能的。

这是我正在运行的查询:

SELECT device_uuid,
     day_epoch,
     is_repeat
FROM tracking_daily_stats_zone_unique_device_uuids_per_hour
WHERE day_epoch >= 1552435200
AND day_epoch < 1553040000
AND venue_id = 46
AND zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

无论如何我都想不出加快这个查询的速度,下面是表索引的图片和这个查询的解释语句。

我认为上面的查询在 where 条件下使用了相关索引。

如果您有什么可以加快查询速度的方法,请告诉我,我已经研究了 3 天,但似乎无法找出问题所在。将查询时间缩短到最大 5(秒)会很棒。如果我对 AJAX 问题有误,请告诉我,因为这也可以解决我的问题。

" 编辑 "

我遇到了一些很奇怪的东西,可能是导致问题的原因。当我将 day_epoch 范围更改为较小的(第 5 - 9 日)返回 130,000 行时,查询时间为 0.7(秒),但随后我在该范围(第 5 - 10 日)上再添加一天,它返回超过 150,000 行的查询时间是 13(秒)。我已经运行了不同范围的负载,并得出结论,如果返回的行数超过 150,000,这会对查询时间产生巨大影响。

表定义-

CREATE TABLE `tracking_daily_stats_zone_unique_device_uuids_per_hour` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `day_epoch` int(10) NOT NULL,
 `day_of_week` tinyint(1) NOT NULL COMMENT 'day of week, monday = 1',
 `hour` int(2) NOT NULL,
 `venue_id` int(5) NOT NULL,
 `zone_id` int(5) NOT NULL,
 `device_uuid` binary(16) NOT NULL COMMENT 'binary representation of the device_uuid, unique for a single day',
 `device_vendor_id` int(5) unsigned NOT NULL DEFAULT '0' COMMENT 'id of the device vendor',
 `first_seen` int(10) unsigned NOT NULL DEFAULT '0',
 `last_seen` int(10) unsigned NOT NULL DEFAULT '0',
 `is_repeat` tinyint(1) NOT NULL COMMENT 'is the device a repeat for this day?',
 `prev_last_seen` int(10) NOT NULL DEFAULT '0' COMMENT 'previous last seen ts',
 PRIMARY KEY (`id`,`venue_id`) USING BTREE,
 KEY `venue_id` (`venue_id`),
 KEY `zone_id` (`zone_id`),
 KEY `day_of_week` (`day_of_week`),
 KEY `day_epoch` (`day_epoch`),
 KEY `hour` (`hour`),
 KEY `device_uuid` (`device_uuid`),
 KEY `is_repeat` (`is_repeat`),
 KEY `device_vendor_id` (`device_vendor_id`)
) ENGINE=InnoDB AUTO_INCREMENT=450967720 DEFAULT CHARSET=utf8
/*!50100 PARTITION BY HASH (venue_id)
PARTITIONS 100 */

【问题讨论】:

  • 我相信 MySQL 通常一次只能利用一个索引,因此单独索引字段可能不是最佳选择;我建议尝试在(venue_id, day_epoch)(venue_id, zone_id, day_epoch) 上创建一个复合索引。 ...此外,在问题中包括您的表的 CREATE 永远不会受到伤害。
  • GROUP BY 通常用于聚合,您是否希望在未分组的字段中选择半随机值?
  • @Uueerdo 上面的查询不是整个查询我有一个需要 GROUP BY 的外部选择,但我没有费心展示,因为这不是问题,内部查询(如图所示)正在减慢速度。我将尝试创建该复合索引。
  • 如果 GROUP BY 用于外部查询,则不需要包含在您发布的内容中;并且原始查询应该有一个 ) 将您发布的内容与 GROUP BY 之前的外部部分分开。
  • @Uueerdo 包含GROUP BY 的原因是因为这可能会减慢查询速度。我知道你不知道它为什么在那里,我现在已经解释过了。我认为问题是由于我所做的编辑。

标签: mysql performance query-optimization


【解决方案1】:

直接的解决方案是将此查询特定的索引添加到表中:

ALTER TABLE tracking_daily_stats_zone_unique_device_uuids_per_hour 
ADD INDEX complex_idx (`venue_id`, `day_epoch`, `zone_id`)

警告此查询更改在 DB 上可能需要一段时间。

然后在你调用的时候强制它:

SELECT device_uuid,
     day_epoch,
     is_repeat
FROM tracking_daily_stats_zone_unique_device_uuids_per_hour
USE INDEX (complex_idx)
WHERE day_epoch >= 1552435200
AND day_epoch < 1553040000
AND venue_id = 46
AND zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

它绝对不是通用的,但应该适用于这个特定的查询。

更新当你有分区表时,你可以通过强制特定的PARTITION 来获利。在我们的例子中,因为那是venue_id,所以强制它:

SELECT device_uuid,
     day_epoch,
     is_repeat
FROM tracking_daily_stats_zone_unique_device_uuids_per_hour
PARTITION (`p46`)
WHERE day_epoch >= 1552435200
AND day_epoch < 1553040000
AND zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

其中p46pvenue_id = 46 的串联字符串

如果你走这条路,还有另一个技巧。您可以从 WHERE 子句中删除 AND venue_id = 46。因为该分区中没有其他数据。

【讨论】:

  • 我目前正在添加索引,但我有这么多数据需要一段时间 :)
  • 我尝试使用这个索引并等待了大约 5 分钟,得到了 500 错误。
  • 是的...如果该查询仍在执行,请检查您的服务器。既然你说你的表已经分区了,你可以简化这个新索引,只需要两列ADD INDEX complex_idx (`day_epoch`, `zone_id`)。但在这种情况下,我认为您不应该强制使用此索引,而是允许服务器优化器分析查询。
  • 我刚刚检查过,失败的查询不再运行。那么现在最好不要强制索引?您认为这可能是 SQL 配置吗?我们正在使用具有 30GB RAM 和 8 个 vCPU 的 AWS RDS 服务器,我怀疑服务器规格是否存在问题。
  • 是的,我同意服务器硬件看起来不错。只需再尝试一次创建索引,但更简单的一次没有venue_id,因为它已经分区了。关于分区的另一个问题。你有办法从venue_id 检测分区ID吗?如果是,我们可以在查询中添加分区强制
【解决方案2】:

如果更改条件的顺序会发生什么?先放venue_id = ?。顺序很重要。

现在它首先检查所有行:
- day_epoch &gt;= 1552435200
- 然后,剩余的设置为day_epoch &lt; 1553040000
- 然后,剩余的设置为venue_id = 46
- 然后,剩余的设置为zone_id IN (102,105,108,110,111,113,116,117,118,121,287)

处理繁重的查询时,您应该始终尝试使第一个“选择器”最有效。您可以通过对 1(或组合)索引使用适当的索引来做到这一点,并确保第一个选择器的范围缩小最多(至少对于整数,如果是字符串,则需要另一种策略)。


有时,查询很慢。当您拥有大量数据(和/或没有足够的资源)时,您实际上无法对此做任何事情。这就是您需要另一个解决方案的地方:制作一个汇总表。我怀疑您向访问者显示 150.000 行 x4。您可以总结它,例如,每小时或每隔几分钟,然后从这种方式中选择较小的表格。


题外话:在插入/更新/删除时为所有内容添加索引only slows you down。索引最少的列,仅在您实际过滤时使用(例如,在 WHERE 或 GROUP BY 中使用)。

【讨论】:

  • 确实有道理。我试了一下,但不幸的是它并没有提高查询速度。我只是觉得奇怪的是,通过在日期范围中添加额外的一天,它会从 0.7 秒变为 13 秒
  • 你在day_epochs之间?不,有道理。意味着日期过滤器“返回”很多到下一个条件。这可能是您的服务器/资源的临界点:)
  • 是的,例如,当我在 5 日和 9 日之间查询时需要 0.7 秒,但是当我在 5 日和 10 日之间查询时需要 13 秒。所以一点点额外的数据就会产生巨大的影响。这可能与 MySQL 配置有关吗?就像附注一样,每天返回大约 20k 条记录,所以通过增加一天,这并不像额外的一天有 100k+ 条记录,所以我认为查询速度不会受到太大影响。
  • @Martijn - 优化器可以并且将重新排列 WHERE 子句项。手动重新排列它们没有任何好处。另一方面,复合索引中列的顺序可能非常很重要。
  • @Lukerayner - 优化器很可能选择了不同的索引来使用,但它的伤害大于帮助。为这两种情况提供EXPLAIN SELECT ...(如果可能)。
【解决方案3】:

450M 行相当大。因此,我将讨论各种可以提供帮助的问题。

收缩数据 大表会导致更多的 I/O,这是主要的性能杀手。 (“小”表倾向于保持缓存,并且没有 I/O 负担。)

  • 任何类型的INT,甚至INT(2) 都占用4 个字节。一个“小时”可以轻松放入一个 1 字节的 TINYINT。这节省了超过 1GB 的数据,加上 INDEX(hour) 中的类似数量。
  • 如果可以导出hourday_of_week,请不要将它们作为单独的列。这将节省更多空间。
  • 有什么理由使用 4 字节 day_epoch 而不是 3 字节 DATE?或者,您可能确实需要一个 5 字节的 DATETIMETIMESTAMP

最优指数(采取#1)

如果它始终是单个venue_id,那么这要么是最佳索引处的良好第一次切割:

INDEX(venue_id, zone_id, day_epoch)

首先是常量,然后是IN,然后是范围。优化器在很多情况下都能很好地做到这一点。 (不清楚IN 子句中的数量 项是否会导致效率低下。)

更好的主键(更好的索引)

对于AUTO_INCREMENT,可能没有充分的理由在 PK 中包含 auto_inc 列之后的列。也就是说,PRIMARY KEY(id, venue_id) 并不比PRIMARY KEY(id) 好。

InnoDB 根据PRIMARY KEY 对数据的 BTree 进行排序。因此,如果您要获取几行 并且 可以根据 PK 将它们安排为彼此相邻,您将获得额外的性能。 (参见“集群”。)所以:

PRIMARY KEY(venue_id, zone_id, day_epoch,  -- this order, as discussed above;
            id)    -- to make sure that the entire PK is unique.
INDEX(id)      -- to keep AUTO_INCREMENT happy

而且,我同意删除任何未使用的索引,包括我上面推荐的索引。索引标志很少有用(is_repeat)。

UUID

一旦表非常大,为 UUID 编制索引可能会严重影响性能。这是因为 UUID/GUID 的随机性,导致在索引中插入新条目的 I/O 负担不断增加。

多维

假设day_epoch 有时是多天,您似乎有 2 或 3 个“维度”:

  • 日期范围
  • 区域列表
  • 场地。

INDEXes 是一维的。问题就在于此。但是,PARTITIONing 有时会有所帮助。我在http://mysql.rjweb.org/doc.php/partitionmaint 中将其作为“案例2”简要讨论。

没有很好的方法来获得3维,所以让我们专注于2。

  • 您应该对某个“范围”进行分区,例如day_epochzone_id
  • 之后,您应该决定在PRIMARY KEY 中放入什么,以便进一步利用“集群”。

A 计划:假设您一次只搜索一个 venue_id

PARTITION BY RANGE(day_epoch)  -- see note below

PRIMARY KEY(venue_id, zone_id, id)

B 计划:假设您有时会为venue_id IN (.., .., ...) 进行搜索,因此它不会成为 PK 的第一列:

好吧,我在这里没有什么好的建议;所以让我们开始 A 计划吧。

RANGE 表达式必须是数字。您的 day_epoch 可以正常工作。更改为DATE,需要BY RANGE(TO_DAYS(...)),这工作正常。

您应该将分区数限制为 50 个。(上面提到的 81 个也不错。)问题是“很多”分区会带来不同的低效率; “太少”的分区会导致“为什么要麻烦”。

请注意,分区表的最佳 PK 几乎总是不同而不是等效的非分区表。

请注意,我不同意在 venue_id 上进行分区,因为很容易将该列放在 PK 的开头。

分析

假设您搜索单个 venue_id 并使用我建议的分区和 PK,SELECT 的执行方式如下:

  1. 过滤日期范围。这可能会将活动限制为单个分区。
  2. 钻入该分区的数据 BTree 以找到 venue_id
  3. 从那里跳房子,登陆所需的zone_ids
  4. 对于每个,根据日期进一步过滤。

【讨论】:

    猜你喜欢
    • 2011-10-04
    • 1970-01-01
    • 2019-11-22
    • 2017-12-24
    • 2015-11-27
    • 2012-03-31
    • 1970-01-01
    • 2016-06-08
    • 2019-10-09
    相关资源
    最近更新 更多