【问题标题】:Speed of mysql query on tables containing blob depends on filesystem cache对包含 blob 的表的 mysql 查询速度取决于文件系统缓存
【发布时间】:2012-03-19 16:02:15
【问题描述】:

我有一个大约有 120k 行的表,其中包含一个带有 BLOB 的字段(每个条目的大小不超过 1MB,通常要小得多)。我的问题是,每当我运行查询该表上的任何列( 包括 BLOB 列)时,如果文件系统缓存为空,则大约需要 40 英寸才能完成。同一张表上的所有后续查询都需要小于 1'' (从命令行客户端测试,在服务器本身上)。查询中返回的行数从空集到 60k+ 不等

我已经消除了查询缓存,所以它与它无关。 该表是 myisam,但我也尝试将其更改为 innodb(并设置 ROW_FORMAT=COMPACT),但没有任何运气。

如果我删除 BLOB 列,查询总是很快。

所以我假设服务器从磁盘(或其中的一部分)读取 blob,并且文件系统缓存它们。问题在于,在流量高且内存有限的服务器上,文件系统缓存每隔一段时间就会刷新一次,所以这个特定的查询一直给我带来麻烦。

所以我的问题是,有没有一种方法可以大大加快速度,而无需从表中删除 blob 列?

这里有 2 个示例查询,一个接一个地运行,以及解释、索引和表定义:

mysql> SELECT ct.score FROM completed_tests ct where ct.status != 'deleted' and ct.status != 'failed' and score < 100;
Empty set (48.21 sec)
mysql> SELECT ct.score FROM completed_tests ct where ct.status != 'deleted' and ct.status != 'failed' and score < 99;
Empty set (1.16 sec)

mysql> explain SELECT ct.score FROM completed_tests ct where ct.status != 'deleted' and ct.status != 'failed' and score < 99;
+----+-------------+-------+-------+---------------+--------+---------+------+-------+-------------+
| id | select_type | table | type  | possible_keys | key    | key_len | ref  | rows  | Extra       |
+----+-------------+-------+-------+---------------+--------+---------+------+-------+-------------+
|  1 | SIMPLE      | ct    | range | status,score  | status | 768     | NULL | 82096 | Using where |
+----+-------------+-------+-------+---------------+--------+---------+------+-------+-------------+
1 row in set (0.00 sec)


mysql> show indexes from completed_tests;
+-----------------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| Table           | Non_unique | Key_name    | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment |
+-----------------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+
| completed_tests |          0 | PRIMARY     |            1 | id          | A         |      583938 |     NULL | NULL   |      | BTREE      |         |
| completed_tests |          1 | users_login |            1 | users_LOGIN | A         |       11449 |     NULL | NULL   | YES  | BTREE      |         |
| completed_tests |          1 | tests_ID    |            1 | tests_ID    | A         |         140 |     NULL | NULL   |      | BTREE      |         |
| completed_tests |          1 | status      |            1 | status      | A         |           3 |     NULL | NULL   | YES  | BTREE      |         |
| completed_tests |          1 | timestamp   |            1 | timestamp   | A         |      291969 |     NULL | NULL   |      | BTREE      |         |
| completed_tests |          1 | archive     |            1 | archive     | A         |           1 |     NULL | NULL   |      | BTREE      |         |
| completed_tests |          1 | score       |            1 | score       | A         |         783 |     NULL | NULL   | YES  | BTREE      |         |
| completed_tests |          1 | pending     |            1 | pending     | A         |           1 |     NULL | NULL   |      | BTREE      |         |
+-----------------+------------+-------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+

mysql> show create table completed_tests;
+-----------------+--------------------------------------
| Table           | Create Table                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
+-----------------+--------------------------------------
| completed_tests | CREATE TABLE `completed_tests` (
  `id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
  `users_LOGIN` varchar(100) DEFAULT NULL,
  `tests_ID` mediumint(8) unsigned NOT NULL DEFAULT '0',
  `test` longblob,
  `status` varchar(255) DEFAULT NULL,
  `timestamp` int(10) unsigned NOT NULL DEFAULT '0',
  `archive` tinyint(1) NOT NULL DEFAULT '0',
  `time_start` int(10) unsigned DEFAULT NULL,
  `time_end` int(10) unsigned DEFAULT NULL,
  `time_spent` int(10) unsigned DEFAULT NULL,
  `score` float DEFAULT NULL,
  `pending` tinyint(1) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `users_login` (`users_LOGIN`),
  KEY `tests_ID` (`tests_ID`),
  KEY `status` (`status`),
  KEY `timestamp` (`timestamp`),
  KEY `archive` (`archive`),
  KEY `score` (`score`),
  KEY `pending` (`pending`)
) ENGINE=InnoDB AUTO_INCREMENT=117996 DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED
1 row in set (0.00 sec)

我最初在mysql query slow at first fast afterwards 上发布了这个,但我现在有更多信息,所以我重新发布为一个不同的问题 我也在mysql forum 上发布了这个,但我还没有收到回复

一如既往地提前感谢

【问题讨论】:

  • +1 写得很好,完整的问题。我希望你能得到一个好的答案(我什么都没有 :-) 如果你在 mysql 论坛上得到一个答案而没有人在这里回答,请发布答案(作为下面的答案),等待必要的 48 小时,然后接受它.您不会获得积分,但它会显示为其他搜索此主题的人的已回答问题。祝你好运。
  • 无法回答“为什么”部分。我可以建议你不要关心这个。好的,你知道你的第一个查询很慢。所以呢?据我了解,任何后续查询都很快。所以知道构建你的应用程序知道这个事实。在某些时候添加“预热”阶段,例如当客户必须在登录表单中输入字符时。就像那样......这比玩缓存设置要好。
  • 这是一个例子,谷歌如何解决同样的问题,http://code.google.com/appengine/docs/adminconsole/instances.html#Warmup_Requests
  • 这不仅是第一个查询很慢,只要系统内存可用性低就会发生
  • 哦 " 是分钟?我以为你在这里谈论秒...忽略我的 cmets。

标签: mysql performance caching blob


【解决方案1】:

MySQL 中 BLOB (=TEXT) 存储的设计似乎完全有缺陷且违反直觉。我遇到了几次同样的问题,但找不到任何权威的解释。我终于找到的最详细的分析是 2010 年的这篇文章:http://www.mysqlperformanceblog.com/2010/02/09/blob-storage-in-innodb/

普遍的看法和期望是 BLOB/TEXT 存储在主行存储之外(例如,请参阅 this answer)。不过,这不是真的。这里有几个问题(我是根据上面给出的文章):

  1. 如果一个 BLOB 项的大小为几 KB,则它直接包含在行数据中。因此,即使您只选择非 BLOB 列,引擎仍然必须从磁盘加载所有 BLOB。假设您有 1M 行,每行包含 100 个字节的非 blob 数据和 5000 个字节的 blob 数据。您选择所有非 blob 列,并期望 MySQL 每行从磁盘读取大约 100-120 字节,总共 100-120 MB(BLOB 地址+20)。然而,实际情况是 MySQL 将所有 BLOB 作为行存储在同一个磁盘块中,所以它们都必须一起读取即使不使用,因此从磁盘读取的数据大小约为 5100 MB = 5 GB - 这比您预期的多 50 倍,意味着查询执行速度要慢 50 倍

    当然,这种设计有一个优势:当您需要所有列时,包括 blob 列,当 blob 与行一起存储时,SELECT 查询比在外部存储时更快:您避免(有时)每个额外的页面访问排。但是,这不是 BLOB 的典型用例,数据库引擎不应该针对这种情况进行优化。如果您的数据非常小以至于可以排成一行,并且无论是否需要,您都可以在每个查询中加载它 - 那么您将使用 VARCHAR 类型而不是 BLOB/TEXT。

  2. 即使出于某种原因(长行或长 blob)BLOB 值存储在外部,它的 768 字节前缀 仍保留在行本身中。让我们以前面的示例为例:您在每行中有 100 字节的非 blob 数据,但现在 blob 列包含每个 1 MB 的项目,因此它们必须保存在外部。非 blob 列的 SELECT 必须每行读取大约 800 个字节(非 blob + blob 前缀),而不是 100-120 - 这又是 7 倍 磁盘传输比您预期,以及 7 倍的速度查询执行。

  3. 外部 BLOB 存储在磁盘空间使用方面无效:它以 16 KB 的块为单位分配空间,单个块不能容纳多个项目,因此如果您的 blob 很小并且每个占用 8 KB,实际分配的空间是两倍那么大。

我希望这一设计有一天会得到解决:MySQL 会将所有大小的 blob 存储在外部存储中,在 DB 中不保留任何前缀,外部存储分配对于各种大小的项目都很有效。在此发生之前,分离 BLOB/TEXT 列似乎是唯一合理的解决方案 - 分离到另一个表或文件系统(每个 BLOB 值保存为一个文件)。

[2019-10-15 更新]

InnoDB 文档现在为上述问题提供了最终答案:

https://dev.mysql.com/doc/refman/8.0/en/innodb-row-format.html

COMPACT 行格式中,内联存储 BLOB/TEXT 值的 768 字节前缀的情况确实成立。根据文档,“对于每个非 NULL 可变长度字段 (...) 内部部分是 768 字节”。

但是,您可以改用 DYNAMIC 行格式。使用这种格式:

"InnoDB 可以存储长可变长度列值 (...)完全脱离页面,聚集索引记录仅包含指向溢出页面的 20 字节指针。 (...) 小于或等于 40 字节的 TEXT 和 BLOB 列存储在行中。"

这里,一个 BLOB 值可以占用最多 40 字节的内联存储,这比 COMPACT 模式下的 768 字节要好得多,并且在您的情况下看起来是更合理的方法希望在表中混合 BLOB 和非 BLOB 类型,并且仍然能够非常快速地扫描多行。此外,扩展的(超过 20 字节)内联存储仅用于 20-40 字节大小的值;对于较大的值,与 COMPACT 模式不同,仅存储 20 字节指针(无前缀)。因此,扩展的 40 字节存储在实践中很少使用,并且可以安全地假设内联存储的平均大小仅为 20 字节(或者更少,如果您倾向于在 BLOB 中保留许多小于 20B 的小值)。总而言之,在大多数情况下,为了在 InnoDB 中实现 BLOB 列的良好可预测性能,似乎 DYNAMIC 行格式而不是 COMPACT 应该是默认选择。

可以在此处找到如何检查 InnoDB 中实际物理存储的示例:

https://dba.stackexchange.com/a/210430/177276

至于 MyISAM,它显然 根本不为 BLOB 提供页外存储(只是内联)。在这里查看更多信息:

【讨论】:

  • 这不是真的,虽然那不是引擎特定的吗?主要文档/代码只会概述引擎必须包含的一些参数。看起来即使 SQL Server 将BLOB 内联也可能是有问题的:dba.stackexchange.com/questions/174678/… 只要数据没有完全存储在 SQL 之外,单独切换模式(表所在的数据库)的能力似乎是值得的。
  • @DannieP 这些问题没有得到解决——我怀疑它们永远不会得到解决。这样做会严重影响向后兼容性。以合理的方式存储 BLOB 的责任由用户承担。
  • 当使用 DYNAMIC 模式时,我是否还应该为我的 BLOB 列使用不同的表?还是 DYNAMIC 解决了这个问题?
  • @amosguata 我认为 DYNAMIC 主要解决了这个问题,只有在极少数情况下(当 20-40 字节产生大量开销时),出于性能原因,您可能仍希望将 BLOB 保留在单独的表中。
【解决方案2】:

只需将索引或索引添加到在 WHERE 查询带有 blob 的表之后使用的字段。

例如您有 2 个包含这些字段的表

users : USERID, NAME, ...
userphotos : BLOBID, BLOB, USERNO, ...

select * from userphotos where USERNO=123456; 

通常这可以正常工作。当您有许多大图像(例如 BLOB、MEDIUMBLOB 或 LONGBLOB 总共超过 5GB)时,这将需要很长时间(超过几分钟),而 BLOBID 是主键。

如果 WHERE 子句中没有关于 BLOB 表字段的索引,MySQL 会以某种方式搜索包括图像在内的整个数据。当您的数据变得越来越大时,这需要很长时间。如果您为字段 USERNO 创建索引,这将加速您的数据库,并且它将独立于整个数据的大小。

解决方案:

**Add Index to the USERNO at userphotos**

作为对您问题的回答,您应该为 ct.status 创建索引

【讨论】:

    【解决方案3】:

    我在这个问题上做了一段时间的研究。许多人建议在单独的表中使用只有一个主键的 Blob,并将 Blob 元数据存储在另一个表中,并使用 Blob 表的外键。有了这个,性能会大大提高。

    【讨论】:

    • 是的,这就是我决定做的事情,但我还没有在现实世界的场景中测试过它来发布在这里。
    • 是的,这就是我最终所做的,并且性能问题已得到修复。尽管如此,我仍然需要编辑无数行代码,我希望能够进行纯粹的数据库表重组,但结果证明这比我能管理的要困难。
    【解决方案4】:

    在两个相关列上添加复合索引应该允许在不直接访问表数据的情况下执行这些查询。

    CREATE INDEX `IX_score_status` ON `completed_tests` (`score`, `status`);
    

    如果您能够切换到 MariaDB,那么您可以充分利用表消除优化。这将允许您将 BLOB 字段拆分到它自己的表中,并使用视图使用 LEFT JOIN 重新创建现有的表结构。这样,只有在执行查询明确需要时,它才会访问 BLOB 数据。

    【讨论】:

    • 复合索引会有所帮助,但我的实际查询比我发布的查询要复杂得多(连接多个表),所以我希望找到一个解决这个问题的解决方案。不过,我可能会专注于寻找索引和查询重构的合适组合,如果出现问题,我会在这里发布。关于 MariaDB,我没听说过,不错(但不幸的是,这不是我的选择)+1
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-28
    • 2020-01-17
    • 1970-01-01
    • 2010-10-17
    • 1970-01-01
    • 2011-01-13
    相关资源
    最近更新 更多