MyISAM table `count` 和 `group by` 非常慢

MyISAM table very slow for `count` with `group by`

以下是我的show create table我的table:

CREATE TABLE `tcm_myisam` (
  `time` int(10) unsigned NOT NULL,
  `asn` int(10) NOT NULL,
  `pop` char(3) NOT NULL,
  `country` char(2) NOT NULL,
  `requests` float DEFAULT NULL,
  `rtt` float DEFAULT NULL,
  `rexb` float DEFAULT NULL,
  `nae` float DEFAULT NULL,
  `nf` float DEFAULT NULL,
  `override` float DEFAULT NULL,
  PRIMARY KEY (`time`,`asn`,`pop`,`country`),
  KEY `tcm_asn_country_idx` (`asn`,`country`) USING BTREE
) ENGINE=MyISAM DEFAULT CHARSET=utf8

table是一个日志。每 5 分钟我 运行 一个脚本来向这个 table 添加大约 500,000 行,每行由 (time, asn, pop, country) 唯一键控。对于给定的 asn, pop, country 三元组,每次脚本 运行 时我都会计算几个指标,然后将这些指标转储到 table。以这种方式附加到 table 之后,这些行永远不会被修改——尽管我确实删除了超过 90 天的数据。

在整整 90 天后,我们收集了大约每 5 分钟 500,000 行:

12 (runs per hour) * 24 (hours) * 90 (days) * 500000 (rows) = 13 BILLION rows

由于索引,一些(相当复杂的)查询 运行 尽管行数很大,但速度非常快:

select
    time,
    coalesce(sum(rtt*requests)/sum(requests), 0) as avg_rtt,
    coalesce(sum(rexb*requests)/sum(requests), 0) as avg_rexb,
    coalesce(sum(nae*requests)/sum(requests), 0) as avg_nae,
    coalesce(sum(nf*requests)/sum(requests), 0) as avg_nf,
    coalesce(sum(override*requests)/sum(requests), 0) as avg_override
from
    tcm_myisam
where
    asn = 7018 and
    country = "US"
group by
    time, asn, country
order by time asc;

25920 rows in set, 4012 warnings (15.55 sec)

有些查询甚至是即时的:

select distinct(time) from tcm_myisam;

25920 rows in set (0.00 sec)

但是这个特定的查询运行比我认为的要慢很多

select time, count(*) from tcm_myisam group by time;

25920 rows in set (25 min 55.87 sec)

有谁知道为什么这么慢?

更新

下面是非常慢的查询的 EXPLAIN:

mysql> explain select time, count(*) from tcm_myisam group by time;
+----+-------------+------------+------------+-------+---------------+---------+---------+------+-------------+----------+-------------+
| id | select_type | table      | partitions | type  | possible_keys | key     | key_len | ref  | rows        | filtered | Extra       |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+-------------+----------+-------------+
|  1 | SIMPLE      | tcm_myisam | NULL       | index | PRIMARY       | PRIMARY | 23      | NULL | 13343405769 |   100.00 | Using index |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+-------------+----------+-------------+

看起来它正在使用索引(根据 Using index 位),但它仍然 运行 慢得离谱。由于我的主键最左边的列是 time,这应该是一个简单的语句

回复@RickJames

注意: @RickJames 修改了他的 post 以回应此。有关详细信息,请参阅他 post 的 "Edit:" 部分。

由于我想 post 作为回应的数量很大,所以我无法将其放入评论中。因此,我针对您在回答中提出的每一点修改了我的 post。

Use InnoDB, not MyISAM

我实际上有两个独立的 tables,因为我正在执行性能实验 -- tcm_myisamtcm_innodb

也就是说,考虑 MyISAM 的决定并不是轻率的。 InnoDB 提供了 很多 的功能,超越了 MyISAM,none 我需要:

因为 MyISAM tables 提供更小的磁盘占用空间(从磁盘读取的数据更少)并提供更简单的 t运行saction 模型,查询开销减少。一般建议是 "if you perform a lot of reads, MyISAM may be faster. If you perform a lot of writes, InnoDB is always faster"。我碰巧属于 MyISAM 优于 InnoDB 的少数用例之一。

在我的测试中,"rather complex" 查询针对给定的 ASN 和国家 运行 聚合了所有时间的多个指标,在 MyISAM 上大约需要 15 秒,在 InnoDB 上大约需要 20 秒。

[Get] rid of the secondary index

建议这样做的唯一原因是 "soften the blow" InnoDB 的更大 table 大小。一般来说,如果您根据列进行分组或选择,最好在其上建立索引。告诉我删除与我分组所依据的列完全匹配的索引是愚蠢的。

Change this [query] to this [query]

我(显然是错误的)相信为了出现在 where 子句中,列必须是 group by 子句的一部分。但是,这两个查询的执行时间相同。您的版本只更简洁几个字符 - 性能增益为零

And change the indexes to [this order]

我在此处 post 进行的查询并不是对数据执行的唯一查询。最常见的数据查询 运行 是 return 给定时间的所有数据 - 因此出于集群原因,将 time 作为我的主索引中的第一列是有意义的。我还同时附加给定 time 的所有数据,并执行定期数据库维护以 p运行e 所有早于某个 time 的数据。由于我对数据库所做的唯一写入是按时间聚类的,因此以任何其他方式对数据进行聚类是没有意义的。

事实上,我在此处 post 编辑的 "absurdly slow" 查询是从这种选择给定时间的所有数据的常见用例中诞生的。我需要估计这些基于时间的组的文件大小,所以我要计算每次有多少行。

通过将我的主键更改为 (asn, country, time, pop) 它可能会适度提高我 post 编辑的 "rather complex" 查询的性能,但它会破坏我的大多数其他查询的性能

Are you deliberately using NULL?

在收集指标时,某些指标可能不可用。要么是因为我的一个数据源无法 return 数据,要么是因为我们目前没有特定 ASN+country+pop 对的数据。如果我们没有 any 指标的数据(如果我们无法计算 rttrexbnfnaeor override) 那么我们就不会为那个 ASN+country+pop 插入一行。但是,如果我们至少有一个指标(也许我们有足够的数据来计算 rtt 但不足以计算 nae),那么我们用 NULL

填充缺失的列

如果我们简单地将 NULL 列替换为 0 之类的内容,那么我们就有低估平均值的风险

I don't think sum(rtt*requests)/sum(rtt) is "avg_rtt"

好消息 -- 这是一个错字

Don't use utf8 for country

实际上我最初在创建table时并没有指定字符集(这是MySQL默认分配的,并且在我输入show create table tcm_myisam时出现在输出中)

我会尝试更改字符集,但我预计性能不会因此发生有意义的变化

Slow Queries

这个

select distinct(time) from tcm_mysiam;

花费了 0.00 秒,因为我的数据是按时间聚类和索引的,所以它能够从元数据 tables 中回答查询,而不是执行 table 扫描

select time, count(*) from tcm_myisam group by time;

如果我的理解是正确的,

也应该能够使用这些元数据table——但事实并非如此

Deleting after 90 days

到目前为止,我只从 1 月初开始收集数据,所以我们还没有完整的 90 天数据(这意味着 "delete" 声明尚未 运行之前的数据库)。为了在达到约 130 亿行后测试性能,我 运行 一个脚本在测试数据库上生成假数据。

我的印象是,通过将 time 作为我的主键(因此按时间聚类),删除会很快。但是,我会考虑将分区作为一个额外的步骤来提高性能。

Summary table

此摘要table 已存在。存在 500k 行的批次,以便我们可以深入了解这些摘要的计算方式。

例如,如果摘要 table 显示:"India saw a spike in RTT at 5pm three days ago",我们可以深入研究三天前下午 5 点印度的所有数据,以确定哪些 ASN 或 POP 受到了影响。

附录: 我目前有两个摘要 table。 returns 每个国家/地区所有指标的最小值、最大值和加权平均值(汇总所有 ASN 和 POP 值)。 returns 每个 ASN 的所有指标的最小值、最大值和加权平均值(汇总所有国家和 POP 值)。这些摘要 table 有效地缩减了我的密钥:

(time, asn, country, pop) -> (time, country)
(time, asn, country, pop) -> (time, asn)

我不会将 "count of rows" 添加到这些摘要 table 中。因此,通过添加我可以使用摘要 table 比使用原始 table.

更快地获得每次的总计数

此外,我没有 table 给定时间的 return 有意义数据的摘要:

(time, asn, country, pop) -> (time)

这样的table不仅可以包括"count of rows",还可以包括"number of rows which exceeded a certain threshold"或"number of distinct ASNs"。所以我将添加这样一个 table 并调整我的应用程序以在适当的时候从中读取。

Absurdly slow

我很清楚阅读全部 130 亿行 需要时间。即使在连接到专用 PCI-e 3.0x4 线路(大约 32 GB/s 带宽)的 M.2 SSD 上,我们也需要 5-8 秒才能从磁盘读取主键....那就是如果我们正在读取所有 130 亿行

我的索引目标是避免一次读取所有 130 亿行。所有 130 亿行都必须可用(我们是否应该选择读取它们),但我们一次最多只能读取 500,000 行(当我们在给定时间内请求 "all data" 时)。因此,我们不是读取 130 亿个主键,而是读取 26000 个 "time" 键来筛选出我们真正想要的 500,000 行,然后读取这 500,000 行。总共从磁盘(索引+数据)读取了 526,000 行,磁盘减少了 5-6 个数量级 I/O.

在大多数情况下,这很有效。我当然没有专用 PCI-e 3.0x4 线路上的 M.2 SSD。我在共享 SATA 线路上有一个糟糕的盘片磁盘,它正在被同一台机器上的其他应用程序 运行ning 同时写入和读取。我很幸运看到 50 MB/s 读取速度。尽管如此,我还是看到查询在 1 分钟内完成(通常)。

然而 select time, count(*) 查询让我感到困惑,因为我认为这会利用我的索引,而是它扫描了整个 table(导致 25 分钟我的破磁盘的执行时间)

所以我原来的问题的关键是:

如何在使用 group by 时获取 count(*) 查询以利用索引提高性能?

请注意,更简单的查询 select count(*) from tcm_myisam 使用 table 元数据和 return 几乎是即时的。

架构和查询更改

使用 InnoDB,而不是 MyISAM。这将导致磁盘占用空间显着增加;下面,我建议摆脱二级索引,这将减轻打击。不过,足迹可能是原来的两倍。

编辑:InnoDB的原因:(1)崩溃安全,(2)PK效率。 InnoDB虽然有"more overhead",但近十年来所有的性能提升都是针对InnoDB的。因此,尽管 "overhead",InnoDB 通常还是一样快或更快。我想知道在添加我的索引建议后 InnoDB 是否会继续优于 MyISAM。

改变这个

where
    asn = 7018 and
    country = "US"
group by
    time, asn, country
order by time asc;

对此:

WHERE asn = 7018
  AND country = "US"
GROUP BY time
ORDER BY time ASC;

并将索引更改为

PRIMARY KEY(asn, country, time, pop)  -- in this order

编辑:"eliminate this index which exactly matches the columns" -- 因为PK是索引,所以我没有去掉索引。此外,由于 PK 与数据 "clustered",this 查询在 InnoDB 中本质上 运行 比 MyISAM 更快。 (MyISAM 必须在 PK BTree 和数据之间来回反弹;InnoDB 不需要。)

编辑:我从 GROUP BY 中去掉了 asncountry,这样 GROUP BYORDER BY 可以相同,从而避免一种额外的种类。 (它与 WHERE 无关,只是注意到这两列是用 = 测试的,因此与 GROUP BY 无关。)

编辑:"The queries I posted here are not the only queries being performed on the data." -- 好吧,在我也看到他们之前,我无法完成对您的帮助。我已经为所提供的查询提供了建议。我的建议可能会或可能不会 帮助或伤害 其他查询。

编辑 "it makes sense to have time be the first column in my primary index for clustering reasons" -- 是和否。'Yes',如果主要 activity 是 INSERTing。 'No' 如果主要 activity 是 SELECTing and/or 如果集群提供了显着的性能提升。

现在 25920 rows in set, 4012 warnings (15.55 sec) 将 运行 显着加快。但是您还应该使用

检查警告
SHOW WARNINGS LIMIT 20;

您是故意使用 NULL 吗?或者列可以是 NOT NULL?算术会不会乱了?

我不认为 sum(rtt*requests)/sum(rtt) 是 "avg_rtt"。也许除以 sum(requests)??

不要为 country 使用 utf8;也许也不适合 pop

编辑:在某些 versions/engines 中,这将占用 6 个字节。更大 table --> 较慢的查询(有点)。

慢查询

这个

select distinct(time) from tcm_myisam;

花了 0.00 秒,要么是因为 MyISAM,要么是因为您打开了查询缓存。它可能应该被关闭,因为现金由于插入而每 5 分钟清除一次。

编辑:我很好奇。你能提供EXPLAIN select ...吗?还要用 select SQL_NO_CACHE ... 计时以避免 QC。可能对 SELECT DISTINCT 进行了优化,跳过了索引。

select time, count(*) from tcm_myisam group by time;

需要 table 扫描,所以它注定会很慢,并且随着 table 的增长而变慢。稍后我会提出解决方案。

90 天后删除

你测试过这个吗?你见过它有多贵吗?让我们用 PARTITIONing 来解决这个问题。我建议 PARTITION BY RANGE(TO_DAYS(time))。那将需要大约 16 个分区。您每周 DROP PARTITION 一次,每周 REORGANIZE 一次。详情在这里:http://mysql.rjweb.org/doc.php/partitionmaint

这将使 "delete" 瞬间发生。它会减慢一些原始查询的速度,但我认为这种权衡是值得的。速度变慢的原因是必须从 16 个分区中的每个分区中获取一些行。

编辑:"the deletes would be fast [if time is first]" -- 它变得比这更复杂。在 MyISAM 中,会在数据中刻出一个巨大的洞。这个洞将由随后的 INSERTs 填补,直到下一个 "delete"。随着时间的推移,MyISAM table 将变得越来越碎片化。使用 InnoDB,也会有 "hole",但基本上没有 "fragmentation"。在这两种情况下 table 都不会缩小;会有免费的 space。是的,如果 PK starts with time,删除会比我建议的 PK 快一些。然而 DROP PARTITION 将比 DELETE.

快得多

编辑:"should also be able to use these metadata tables" -- 唯一接近 "metadata" 的是 MyISAM 保持 行计数。这对于既没有 WHERE 也没有 GROUP BYCOUNT(*) 来说肯定更好。但是只针对那个查询

编辑:"we read 26000 "时间“过滤我们实际需要的 500,000 行的键”——注意 PARTITION BY (TO_DAYS(time)) 允许粗略的 WHERE time BETWEEN .. AND .. 另外到 WHERE 中的任何其他内容(例如 asn)。也就是说,分区给出了二维索引的粗略近似。所以...即使我从PK开始就移动了time,你仍然不需要读取130亿行来获得一个很短的时间范围。任何过滤到一周以下的查询都只会命中 1 或 2 个分区(取决于时间范围与分区的对齐方式),因此只有 1 或 20 亿行,而不是 13。

总结Table

通常,在像这样的数据仓库情况下,构建和维护 "Summary Table" 可以显着提高性能(可能是 10 倍)。

在你的情况下,代替(或除了)投掷500K raw 行到 Fact table 中,总结它们并将它们放入另一个 table。然后对 table.

执行 SELECTs

不明白为什么每批有 500K 行,我不能更具体。

关于摘要 table 的一些通用信息:http://mysql.rjweb.org/doc.php/summarytables

编辑:"aggregates several metrics across all time" -- 摘要 tables.

的主要原因

慢得离谱

13 billlion 行(PK 需要 200GB?)read 需要时间。它将是 I/O-bound。我的更改将使该查询 运行 变慢;但这是一个重要的问题吗?一个 suitable 摘要 table 可以更快地获得计数。