尽管覆盖索引,MySQL MyISAM 慢计数()查询
Posted
技术标签:
【中文标题】尽管覆盖索引,MySQL MyISAM 慢计数()查询【英文标题】:MySQL MyISAM slow count() query despite covering index 【发布时间】:2015-03-07 22:48:38 【问题描述】:我正在拔头发,试图找出我做错了什么。 表格很简单:
CREATE TABLE `icd_index` (
`icd` char(5) NOT NULL,
`core_id` int(11) NOT NULL,
`dx_order` tinyint(4) NOT NULL,
PRIMARY KEY (`icd`,`dx_order`,`core_id`),
KEY `core` (`core_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
如您所见,我创建了一个覆盖表的所有三列的覆盖索引,并在core_id
上创建了一个附加索引,用于潜在的连接。这是一个一对多的链接表,每个core_id
映射到一个或多个icd
。该表包含 6500 万行。
所以,这就是问题所在。假设我想知道有多少人的 icd 代码为“25000”。 [那是糖尿病,如果你想知道的话]。我编写了一个如下所示的查询:
SELECT COUNT(core_id) FROM icd_index WHERE icd='25000'
这需要超过 60 秒才能执行。我原以为 icd 列在被覆盖的索引中排在第一位,因此计数会很快。
更令人困惑的是,一旦我运行了一次查询,它现在运行得非常快。我认为这是因为查询被缓存了,但即使我RESET QUERY CACHE
,查询现在也可以在几分之一秒内运行。但是,如果我等待的时间足够长,它似乎又变慢了——我不知道为什么。
我遗漏了一些明显的东西。我需要一个单独的icd
索引吗?这是使用 65M 行可以获得的最佳性能吗?为什么运行查询然后重置缓存会影响速度?结果是否存储在索引中?
编辑:我正在运行 mysql 5.6(以防万一)。
这是查询的EXPLAIN
:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE icd_index ref PRIMARY PRIMARY 15 const 910104 Using where; Using index
【问题讨论】:
你能试试SELECT COUNT(*) FROM icd_index where icd = '25000'
吗?
同样的结果。我第一次执行它是 70 秒。如果我重新执行它,它是瞬时的(大概来自缓存)。如果我重置缓存并运行它,它是 0.7 秒。我正在使用 MySQL 5.6,以防万一。
您尝试过不同的存储引擎吗? MyISAM 已经过时了,我不知道它是否可以利用这些天可用的所有硬件。
我最初是从 InnoDB 开始的。但是在 InnoDB 中计算整个表的速度非常慢,所以我切换到了 MyISAM。我不需要事务,因为这个数据库是只读的——数据是固定的,永远不会改变。但如果切换到 InnoDB 会有所帮助,我会考虑(尽管这样做需要花费数小时的时间......)另外,我在查询中添加了说明,以防有帮助
InnoDB
必须配置为快速。您可以选择将多少 RAM 专用于 InnoDB,并使用它来存储它在那里使用的数据。可以从 RAM 而不是磁盘访问的 6500 万行是非常非常快速的操作。魔术变量称为innodb_buffer_pool_size
。另外,还有TokuDB
,另一个出色的存储引擎。另外,您使用的是机械硬盘还是 SSD?
【参考方案1】:
这就是发生的事情。
The SELECT COUNT (...) icd_index where icd='25000'
将使用索引,它是一个与数据分离的 BTree。但它以这种方式扫描它:
-
找到第一个具有 icd='25000' 的条目。这几乎是瞬间完成的。
向前扫描,直到发现 icd 发生变化。这将只扫描索引,不接触数据。根据 EXPLAIN,将有大约 910,104 个索引条目需要扫描。
现在让我们看一下该索引的 BTree。根据索引中的字段,每行正好是 22 个字节,加上会有一些开销(估计 40%)。 MyISAM 索引块为 1KB(参见 InnoDB 的 16KB)。我估计每个块有 33 行。 910,104/33 表示需要读取大约 27K 块来执行 COUNT。 (注意COUNT(core_id)
需要检查core_id
是否为空,COUNT(*)
不需要;这是一个很小的区别。)在普通硬盘驱动器上读取 27K 块大约需要 270 秒。你很幸运能在 60 秒内完成。
第二次运行在 key_buffer 中找到了所有这些块(假设 key_buffer_size 至少为 27MB),因此它不必等待磁盘。因此它要快得多。 (这忽略了查询缓存,您有智慧刷新或使用 SQL_NO_CACHE。)
5.6 恰好是无关紧要的(但感谢提及),因为自 4.0 或更早版本以来此过程没有改变(除了 utf8 不存在;更多内容如下)。
切换到 InnoDB 会在几个方面有所帮助。 PRIMARY KEY 将与数据“聚集”在一起,而不是存储为单独的 BTree。因此,一旦数据或 PK 被缓存,另一个立即可用。块的数量更像是 5K,但它们将是 16KB 块。如果缓存是冷的,这些可能会更快地加载。
你问“我需要一个单独的 icd 索引吗?”——这会将 MyISAM BTree 的大小缩小到每行大约 21 个字节,所以 BTree 的大小将是大约 21/27 的大小,没有太大的改进(在至少对于冷缓存情况)。
另一个想法是,如果 icd
总是数字并且总是数字,使用MEDIUMINT UNSIGNED
,如果它可以有前导零,则附加ZEROFILL
。
糟糕,我没有注意到字符集。 (我已经修正了上面的数字,但让我详细说明一下。)
CHAR(5) 允许 5 个字符。 ascii 每个字符占用 1 个字节。 utf8 每个字符最多占用 3 个字节。 所以,CHAR(5) CHARACTER SET utf8 占用 15 个字节总是。将列更改为 CHAR(5) CHARACTER SET ascii
会将其缩小到 5 个字节。
将其更改为 MEDIUMINT UNSIGNED ZEROFILL 会将其缩小到 3 个字节。
缩小数据将使 I/O 加速大致成比例的量(在其他两个字段允许另外 6 个字节之后。
【讨论】:
65M 行 --> 4GB innodb_buffer_pool_size;你有至少 6GB 的内存吗? 感谢您的出色回答。我很快就会升级内存。 ICD 需要是 CHAR,因为某些代码以字母开头。但是我可以毫无问题地切换到ASCII。我也可以切换到 InnoDB —— 事实上,我可以双向重新创建表,看看哪个更快。 converting to InnoDB 的提示。如果您反复重新填充表格,我可以向您展示一种零停机时间的方法。如果您的“工作集”小于整个表,则可能不需要升级 RAM。但是,对于 InnoDB,buffer_pool 应设置为 RAM 的 70% 左右(如果您的内存小于 4GB,则小于该值。 警告:ZEROFILL
属性已弃用。 (如果需要,有解决方法。)【参考方案2】:
感谢以上所有人的帮助。鉴于上述建议,我完全重建了数据库,如下所示:
-
我说服服务器管理员将我的 RAM 增加到 6G。
我将所有表都切换到了带有 ASCII 字符集的 InnoDB。
当我将数据从MyISAM移动到InnoDB时,在插入新表之前,我按照覆盖索引的顺序对所有数据进行了排序,所以新表完全正确排序。不知道这是否真的有帮助,但它似乎不会受到伤害。
我修改了数据库设置,特别是 InnoDB 缓冲池大小并将其增加到 256M。
圣母啊,现在真快。上面的简单计数查询现在运行时间不到 2 秒。不确定以上哪个最有效(但在缓冲池大小增加之前查询速度很快)
【讨论】:
【参考方案3】:我的一个查询也发生了同样的事情。 MyISAM 表正在使用文件排序来执行简单的 SELECT 语句。
我最终切换到 InnoDB,问题消失了。我不知道为什么。
【讨论】:
以上是关于尽管覆盖索引,MySQL MyISAM 慢计数()查询的主要内容,如果未能解决你的问题,请参考以下文章