【问题标题】:How to optimise a large table in MySQL, when can I benefit from partitioning?如何优化 MySQL 中的大表,何时可以从分区中受益?
【发布时间】:2016-11-10 04:51:08
【问题描述】:

总的来说,日期范围分区和内存配置实现了我的目标。

我需要增加分配给 innodb_buffer_pool_size 的内存,因为默认的 8M 太低了。 Rick James 推荐 70% of RAM 进行此设置,他有很多很棒的信息。

Edlerd 对这两个建议都是正确的 :-)

我将数据拆分为每月分区,然后运行 ​​6,000 行响应查询,最初需要 6 到 12 秒。它现在在不到一秒的时间内完成 (.984/.031)。我使用默认的 innodb 缓冲区大小(innodb_buffer_pool_size = 8M)运行它,以确保它不仅仅是内存增加。

然后我设置 innodb_buffer_pool_size = 4G 并以 .062/.032 的更好响应运行查询。

我还想提一下,增加内存还提高了我的 Web 应用程序和服务的整体速度,这些应用程序和服务接收和写入该表的消息,我对这个配置设置产生的巨大差异感到震惊。我的 Web 服务器的第一个字节时间 (TTFB) 现在几乎与 MySQL Workbench 相当,有时会达到 20 秒。

我还发现slow query log file 是识别问题的绝佳工具,在那里我看到它表明我的 innodb_buffer_pool_size 很低并且突出显示了所有性能不佳的查询。这也确定了我需要索引其他表的区域。

编辑 2016-11-12 解决方案

我正在重构一个记录遥测数据的大表,它已经运行了大约 4-5 个月,并且已经生成了大约 4-5 个月。 5400 万条记录,平均行大小约为。 380 字节。

我开始发现我的一个原始数据查询在 24 小时内返回设备的所有日志时出现了一些性能滞后。

最初我以为是索引,但我认为是 MySQL 需要处理的 I/O 量。一个典型的 24 小时查询将包含 2.2k 3k 到 9k 条记录,我实际上希望支持大约 7 天的导出。

我在数据库性能调优方面没有经验,所以仍然只是在学习。我正在考虑一些策略。

  1. 根据原始数据查询调整复合索引,尽管我认为我的索引没问题,因为解释计划显示 100% 的命中率。
  2. 考虑创建一个覆盖索引以包含所有需要的行
  3. 按日期实现范围分区: a) 保留每月分区。例如。过去 6 个月 b) 将任何较旧的内容移至存档表。
  4. 使用原始数据创建一个单独的表(垂直分区)并将其与主查询表的 ID 连接。不确定这是我的问题,因为我的索引正在工作。
  5. 更改我的查询以分批提取数据并限制,然后按创建的日期限制 X 排序并继续,直到不再返回任何记录。
  6. 查看服务器配置

1,2(索引): 我会用我的查询来修改我的索引,但我认为我在这里很好,因为 Explain 显示 100% 命中,除非我读错了。

我会在重建时尝试使用覆盖索引,但我如何确定设置错误的连锁效应?例如。插入速度受到影响。

如何最好地监控我的表在实时环境中的性能?

编辑:我刚刚开始使用slow log file,它看起来是查找问题的好工具,我想performance_schema 上的查询可能是另一种选择?

3(分区): 我已经阅读了一些关于分区的内容,但不确定我的数据大小是否会产生很大的不同。

Rick James suggests >1M 记录,我有 54M 并且希望在归档之前保留大约 300M,我的表是否足够复杂以受益?

我必须自己测试一下,因为我没有使用任何这些东西的经验,而且这对我来说都是理论上的。如果它不适合我的需求,我只是不想走这条路。

4(通过“加入”详细信息表进行垂直分区):我认为没有表扫描问题并且我需要所有行,所以我不确定这种技术是否有用.

5(使用限制并再次获取):如果我在单个请求中使用较少的时间,这会释放服务器吗?我会以同一连接上的更多命令为代价获得更好的 I/O 吞吐量吗?

6(查看配置):另一部分是查看安装 MySQL 时使用的默认非开发人员配置,也许有一些可以调整的设置? :-)

感谢阅读,渴望听到任何和所有建议。

以下仅供参考:

表格:

CREATE TABLE `message_log` (
    `db_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
    `db_created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
    `created` datetime DEFAULT NULL,
    `device_id` int(10) unsigned NOT NULL,
    `display_name` varchar(50) DEFAULT NULL,
    `ignition` binary(1) DEFAULT NULL COMMENT 'This is actually IO8 from the falcom device',
    `sensor_a` float DEFAULT NULL,
    `sensor_b` float DEFAULT NULL,
    `lat` double DEFAULT NULL COMMENT 'default GPRMC format ddmm.mmmm \n',
    `lon` double DEFAULT NULL COMMENT 'default GPRMC longitude format dddmm.mmmm ',
    `heading` float DEFAULT NULL,
    `speed` float DEFAULT NULL,
    `pos_validity` char(1) DEFAULT NULL,
    `device_temp` float DEFAULT NULL,
    `device_volts` float DEFAULT NULL,
    `satellites` smallint(6) DEFAULT NULL, /* TINYINT will suffice */
    `navdist` double DEFAULT NULL,
    `navdist2` double DEFAULT NULL,
    `IO0` binary(1) DEFAULT NULL COMMENT 'Duress',
    `IO1` binary(1) DEFAULT NULL COMMENT 'Fridge On/Off',
    `IO2` binary(1) DEFAULT NULL COMMENT 'Not mapped',
    `msg_name` varchar(20) DEFAULT NULL, /* Will be removed */
    `msg_type` varchar(16) DEFAULT NULL, /* Will be removed */
    `msg_id` smallint(6) DEFAULT NULL,
    `raw` text, /* Not needed in primary query, considering adding to single table mapped to this ID or a UUID correlation ID to save on @ROWID query */
PRIMARY KEY (`db_id`),
KEY `Name` (`display_name`),
KEY `Created` (`created`),
KEY `DeviceID_AND_Created` (`device_id`,`created`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

DeviceID_AND_Created 是主索引。我需要 PK 聚集索引,因为我在汇总表中使用记录 ID,该记录 ID 跟踪给定设备的最后一条消息。创建的将是分区列,所以我猜它也会添加到 PK 集群中?

查询:

SELECT 
    ml.db_id, ml.db_created, ml.created, ml.device_id, ml.display_name, bin(ml.ignition) as `ignition`, 
    bin(ml.IO0) as `duress`, bin(ml.IO1) as `fridge`,ml.sensor_a, ml.sensor_b, ml.lat, ml.lon, ml.heading, 
    ml.speed,ml.pos_validity, ml.satellites, ml.navdist2, ml.navdist,ml.device_temp, ml.device_volts,ml.msg_id
FROM message_log ml 
WHERE ml.device_id = @IMEI
AND ml.created BETWEEN @STARTDATE AND DATE_ADD(@STARTDATE,INTERVAL 24 hour) 
ORDER BY ml.db_id;

这将返回给定 24 小时期间的所有日志,目前大约为 24 小时。 3k 到 9k 行,平均行大小 381 字节,一旦我删除了 TEXT 字段之一(原始)

【问题讨论】:

  • 请提供一些滞后数字以及查询计划以显示正在发生的事情
  • @SamiKuhmonen 嗨,修改我的回复。刚刚查看了慢查询日志,发现数据比我想象的要多得多,所以输出较慢可能是由于某些设备产生的数据较多。我已经更新了解释计划,虽然命中率差不多。我可以使用其他指标吗?我正在将此表导入 DEV,并将尽快创建一些分区。
  • 为什么不发布解决方案作为答案,而不是将它们编辑到问题中?这个问题现在看起来很混乱。

标签: mysql indexing database-performance partitioning


【解决方案1】:

按日期实施范围分区:a) 保留每月分区。例如。过去 6 个月 b) 将任何较旧的内容移至存档表。

这是一个非常的好主意。我猜所有的写入都在最新的分区中,你只会查询最近的数据。您总是希望数据和索引适合内存的情况。所以读取时没有磁盘 i/o。

根据您的用例,每周有一个分区甚至可能是明智之举。然后,您只需在内存中保留最多两周的数据即可读取过去 7 天。

如果您使用 innodb 作为引擎,您可能还需要调整缓冲区大小(即 innodb_buffer_pool_size),或者在使用 myisam 引擎时调整 myisam_key_cache。

另外,将 ram 添加到 DB 机器通常会有所帮助,因为操作系统可以将数据文件保存在内存中。

如果您有大量写入,您还可以调整其他选项(即使用 innodb_log_buffer_size 将写入持久化到磁盘的频率)。这是为了让脏页在内存中的停留时间更长,避免过于频繁地将它们写回磁盘。

【讨论】:

  • 太棒了,这是我的方向,我也在研究 innodb_buffer_pool_size,我认为这是一个很大的障碍,因为它目前设置为 8Mb。一旦它在专用服务器上(Rick James 推荐的 70% 的 RAM),我将增加到 2GB 并添加更多。我现在只是创建分区,所以很快就会运行一些查询。你知道 PARTITION BY RANGE(to_days(created)) 然后从 201607 VALUES LESS THAN (to_days('2016-08-01')) 分区使用 to_days() 是否更好?
  • 开发分区仍在创建中...但是我刚刚更新了 innodb_buffer_pool_size ,令人惊讶的是,整个董事会都发生了改进。我把它增加到 4G,一切都开始运行得更快了!初始查询现在在 1.5 秒内完成!
【解决方案2】:

对于好奇的朋友,下面是我用来创建分区和配置内存的。

创建分区

  1. 更新 PK 以包含分区中使用的范围列

    ALTER TABLE message_log 
    CHANGE COLUMN created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    DROP PRIMARY KEY,
    ADD PRIMARY KEY (db_id, created);
    
  2. 使用 ALTER TABLE 添加了分区。

事后看来,我应该将每个分区创建为单个 ALTER 语句并在后续分区上使用 Reorganize Partition(和 here),因为一次性完成会消耗大量资源和时间。

ALTER TABLE message_log 
PARTITION BY RANGE(to_days(created)) (
    partition invalid VALUES LESS THAN (0),
    partition from201607 VALUES LESS THAN (to_days('2016-08-01')),
    partition from201608 VALUES LESS THAN (to_days('2016-09-01')),
    partition from201609 VALUES LESS THAN (to_days('2016-10-01')),
    partition from201610 VALUES LESS THAN (to_days('2016-11-01')),
    partition from201611 VALUES LESS THAN (to_days('2016-12-01')),
    partition from201612 VALUES LESS THAN (to_days('2017-01-01')),
    partition from201701 VALUES LESS THAN (to_days('2017-02-01')),
    partition from201702 VALUES LESS THAN (to_days('2017-03-01')),
    partition from201703 VALUES LESS THAN (to_days('2017-04-01')),
    partition from201704 VALUES LESS THAN (to_days('2017-05-01')),
    partition future values less than (MAXVALUE) 
);

注意:我不确定使用 to_days() 或原始列是否有很大不同,但我已经看到它在大多数示例中都使用过,因此我认为它是最好的练习。

设置缓冲池大小

要更改 innodb_db_buffer_pool_size 的值,您可以找到以下信息: MySQL InnoDB Buffer Pool ResizeRick Jame's page on memory

您也可以在 MySQL Workbench 中的 选项文件 菜单中执行此操作,然后在 innoDB 选项卡中执行此操作。您在此处所做的任何更改都将写入配置文件中,但您需要停止并启动 MySQL 才能读取配置,否则您也可以设置全局值来实时执行。

【讨论】:

    【解决方案3】:

    这样的交易!我得到 4 次提及,即使没有写评论或答案。我正在写一个答案,因为我可能会有一些进一步的改进......

    是的,PARTITION BY RANGE(TO_DAYS(...)) 是正确的选择。 (可能有少量个备选方案。)

    70% 的 4GB RAM 很紧。确保没有交换。

    您提到了一个查询。如果它是主要关注的问题,那么这会稍微好一点:

    PRIMARY KEY(device_id, created, db_id),  -- desired rows will be clustered
    INDEX(db_id)  -- to keep AUTO_INCREMENT happy
    

    如果您不清除旧数据,那么即使没有分区,上述关键建议也能提供同样高的效率。

    lat/lon representationDOUBLE 太过分了。

    当心inefficiency of UUID,尤其是对于大桌子。

    【讨论】:

    • 嗨 Rick,感谢您的回复(以及您的网站!)。我有另一个表存储每个设备 ID 的最后一个消息 ID,然后我加入回 message_log 以获取数据。我想我会在这个更新中失去 PK 集群? (另外,我不再按该字段排序,它的创建)也许更好地复制另一个表中的数据并避免将所有连接在一起。回复:Lat/Lng 我必须阅读那篇文章,我选择了双精度以维持 6 个小数位。还有 UUID,我使用一种 GUID 类型作为 lat/lng 缓存表的 ID,但在 c# 中转换为字节数组并存储在二进制 (16) MySQL 字段中。
    • :-) 错误地假设 Double 优于 Decimal (DEC(8,6)/(9,6)) 并避免了由于截断而导致的浮点数。我需要保持在米范围内,“准确度”也是我们设备的一个特点。我没想过将这些值乘以 1,000,000 以将它们存储为 INT,太棒了!实际上,我可以对所有 float 和 double 类型执行此操作,然后在 UI 或 SQL SELECT 中进行转换——您节省了很多空间!我计划很快在 Lat/Lng 上构建一个查询组件,因此将查看您的提示,已经偏爱 gcd 而不是 hasrsine。 :-)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-10-09
    • 1970-01-01
    • 1970-01-01
    • 2021-05-28
    • 2011-10-14
    • 2010-09-12
    • 1970-01-01
    相关资源
    最近更新 更多