如何优化 MySQL 中的大型 table,我什么时候可以从分区中获益?
How to optimise a large table in MySQL, when can I benefit from partitioning?
综上所述,日期 运行ge 分区和内存配置达到了我的目标。
我需要增加分配给 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 应用程序和服务的整体速度,该应用程序和服务向此 table 接收和写入消息,我对差异之大感到震惊此配置设置已完成。我的 Web 服务器的首字节时间 (TTFB) 现在几乎与 MySQL Workbench 相当,后者有时会达到 20 秒。
我还发现 slow query log file 是一个很好的识别问题的工具,我在那里看到它表明我的 innodb_buffer_pool_size 很低,并突出显示了所有性能不佳的查询。这也确定了我需要索引其他 table 的区域。
编辑 2016-11-12 解决方案
我正在重构一个记录遥测数据的大型 table,它已经 运行 了大约 4-5 个月,并且已经生成了大约。 5400 万条记录,平均行大小约为。 380 字节。
我开始发现我的一个原始数据查询有一些性能滞后,returns 设备在 24 小时内的所有日志。
一开始以为是索引,后来想是I/O需要处理的量MySQL。典型的 24 小时查询将包含 2.2k 3k 到 9k 条记录,实际上我希望支持大约 7 天的导出。
我在数据库性能调优方面没有经验,所以仍然只是在摸索。我正在考虑一些策略。
- 根据对原始数据的查询调整复合索引,尽管我认为我的索引还可以,因为解释计划显示 100% 的命中率。
- 考虑创建覆盖索引以包含所有需要的行
- 按日期实施 运行ged 分区:
a) 保留每月分区。例如。最近 6 个月
b) 将任何较旧的内容移动到存档 table。
- 使用原始数据创建一个单独的 table(垂直分区)并将其与主查询 table 的 ID 连接。不确定这是我的问题,因为我的索引正在运行。
- 将我的查询更改为有限制地分批提取数据,然后按创建的日期限制 X 排序并继续,直到不再返回任何记录。
- 查看服务器配置
1,2(索引):
我会用我的查询重新处理我的索引,但我认为我在这里很好,因为 Explain 显示 100% 命中,除非我读错了。
我会在重建它们时尝试使用覆盖索引,但是我如何确定设置错误的连锁反应?例如。插入速度受到影响。
如何最好地监控我的 table 在实时环境中的性能?
编辑: 我刚开始使用 slow log file which looks like a good tool for finding issues and I suppose a query on the performance_schema 可能是另一种选择?
3(分区):
我已经阅读了一些关于分区的内容,但不确定我的数据大小是否会有很大的不同。
Rick James suggests >1M 记录,我有 54M,想在归档前保持 300M 左右,我的 table 是否足够复杂,可以从中受益?
我必须自己测试一下,因为我对这些东西没有任何经验,而且对我来说都是理论上的。如果不符合我的需要,我只是不想走这条路table。
4(通过“连接”详细信息进行垂直分区 table): 我认为没有 table 扫描问题,我需要所有行,所以我不确定这种技术是否有用。
5(使用限制并再次获取): 如果我在单个请求中使用更少的时间,这会释放服务器吗?在同一连接上以更多命令为代价,我会看到更好的 I/O 吞吐量吗?
6(查看配置): 另一部分是查看安装时使用的默认非开发人员配置 MySQL,也许有一些设置那可以调整吗? :-)
感谢阅读,渴望听到任何和所有的建议。
以下仅供参考:
TABLE:
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 聚簇索引,因为我在摘要 table 中使用记录 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;
这 returns 给定 24 小时内的所有日志,目前大约是。 3k 到 9k 行,平均行大小 381 字节,一旦我删除其中一个 TEXT 字段(原始)
Implement ranged partitioning by date: a) Keep monthly partitions. E.g. last 6 months b) Move anything older to archive table.
这是一个非常的好主意。我想所有的写入都将在最新的分区中,您将只查询最近的数据。您总是希望您的数据和索引适合内存。所以没有磁盘 i/o 读取。
根据您的用例,每周有一个分区甚至可能是明智的。然后你只需要在内存中保留最多两周的数据来读取最近 7 天。
如果您使用 innodb 作为引擎或 myisam_key_cache 使用 myisam 引擎,您可能还需要调整缓冲区大小(即 innodb_buffer_pool_size)。
另外将 ram 添加到数据库机器通常会有所帮助,因为 os 然后可以将数据文件存储在内存中。
如果您有大量写入,您还可以调整其他选项(即使用 innodb_log_buffer_size 将写入持久保存到磁盘的频率)。这是为了让脏页在内存中停留更长的时间,避免过于频繁地将它们写回磁盘。
对于那些好奇的人,以下是我用来创建分区和配置内存的内容。
创建分区
已更新 PK 以包含分区中使用的范围列
ALTER TABLE message_log
CHANGE COLUMN created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
DROP PRIMARY KEY,
ADD PRIMARY KEY (db_id, created);
使用 ALTER 添加分区 TABLE。
事后看来,我应该将每个分区创建为单个 ALTER 语句并在后续分区上使用 Reorganize Partition (and 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 Resize and Rick Jame's page on memory
你也可以在 选项文件 菜单中的 MySQL Workbench 然后 innoDB标签。您在此处所做的任何更改都将写入配置文件,但您需要停止并启动 MySQL 以读取配置,否则您也可以设置全局值以使其生效。
太划算了!我得到 4 次提及,即使没有写评论或回答。我正在写一个答案,因为我可能会有一些进一步的改进...
是的,PARTITION BY RANGE(TO_DAYS(...))
是正确的方法。 (可能有 小 个备选方案。)
4GB RAM 的 70% 空间紧张。确保没有交换。
您提到了一个查询。如果是主要关注的,那么这个会好一点:
PRIMARY KEY(device_id, created, db_id), -- desired rows will be clustered
INDEX(db_id) -- to keep AUTO_INCREMENT happy
如果您不清除旧数据,那么即使没有分区,上述关键建议也能提供同样高的效率。
lat/lon representation 说 DOUBLE
太过分了。
注意 inefficiency of UUID,尤其是对于大表。
综上所述,日期 运行ge 分区和内存配置达到了我的目标。
我需要增加分配给 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 应用程序和服务的整体速度,该应用程序和服务向此 table 接收和写入消息,我对差异之大感到震惊此配置设置已完成。我的 Web 服务器的首字节时间 (TTFB) 现在几乎与 MySQL Workbench 相当,后者有时会达到 20 秒。
我还发现 slow query log file 是一个很好的识别问题的工具,我在那里看到它表明我的 innodb_buffer_pool_size 很低,并突出显示了所有性能不佳的查询。这也确定了我需要索引其他 table 的区域。
编辑 2016-11-12 解决方案
我正在重构一个记录遥测数据的大型 table,它已经 运行 了大约 4-5 个月,并且已经生成了大约。 5400 万条记录,平均行大小约为。 380 字节。
我开始发现我的一个原始数据查询有一些性能滞后,returns 设备在 24 小时内的所有日志。
一开始以为是索引,后来想是I/O需要处理的量MySQL。典型的 24 小时查询将包含 2.2k 3k 到 9k 条记录,实际上我希望支持大约 7 天的导出。
我在数据库性能调优方面没有经验,所以仍然只是在摸索。我正在考虑一些策略。
- 根据对原始数据的查询调整复合索引,尽管我认为我的索引还可以,因为解释计划显示 100% 的命中率。
- 考虑创建覆盖索引以包含所有需要的行
- 按日期实施 运行ged 分区: a) 保留每月分区。例如。最近 6 个月 b) 将任何较旧的内容移动到存档 table。
- 使用原始数据创建一个单独的 table(垂直分区)并将其与主查询 table 的 ID 连接。不确定这是我的问题,因为我的索引正在运行。
- 将我的查询更改为有限制地分批提取数据,然后按创建的日期限制 X 排序并继续,直到不再返回任何记录。
- 查看服务器配置
1,2(索引): 我会用我的查询重新处理我的索引,但我认为我在这里很好,因为 Explain 显示 100% 命中,除非我读错了。
我会在重建它们时尝试使用覆盖索引,但是我如何确定设置错误的连锁反应?例如。插入速度受到影响。
如何最好地监控我的 table 在实时环境中的性能?
编辑: 我刚开始使用 slow log file which looks like a good tool for finding issues and I suppose a query on the performance_schema 可能是另一种选择?
3(分区): 我已经阅读了一些关于分区的内容,但不确定我的数据大小是否会有很大的不同。
Rick James suggests >1M 记录,我有 54M,想在归档前保持 300M 左右,我的 table 是否足够复杂,可以从中受益?
我必须自己测试一下,因为我对这些东西没有任何经验,而且对我来说都是理论上的。如果不符合我的需要,我只是不想走这条路table。
4(通过“连接”详细信息进行垂直分区 table): 我认为没有 table 扫描问题,我需要所有行,所以我不确定这种技术是否有用。
5(使用限制并再次获取): 如果我在单个请求中使用更少的时间,这会释放服务器吗?在同一连接上以更多命令为代价,我会看到更好的 I/O 吞吐量吗?
6(查看配置): 另一部分是查看安装时使用的默认非开发人员配置 MySQL,也许有一些设置那可以调整吗? :-)
感谢阅读,渴望听到任何和所有的建议。
以下仅供参考:
TABLE:
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 聚簇索引,因为我在摘要 table 中使用记录 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;
这 returns 给定 24 小时内的所有日志,目前大约是。 3k 到 9k 行,平均行大小 381 字节,一旦我删除其中一个 TEXT 字段(原始)
Implement ranged partitioning by date: a) Keep monthly partitions. E.g. last 6 months b) Move anything older to archive table.
这是一个非常的好主意。我想所有的写入都将在最新的分区中,您将只查询最近的数据。您总是希望您的数据和索引适合内存。所以没有磁盘 i/o 读取。
根据您的用例,每周有一个分区甚至可能是明智的。然后你只需要在内存中保留最多两周的数据来读取最近 7 天。
如果您使用 innodb 作为引擎或 myisam_key_cache 使用 myisam 引擎,您可能还需要调整缓冲区大小(即 innodb_buffer_pool_size)。
另外将 ram 添加到数据库机器通常会有所帮助,因为 os 然后可以将数据文件存储在内存中。
如果您有大量写入,您还可以调整其他选项(即使用 innodb_log_buffer_size 将写入持久保存到磁盘的频率)。这是为了让脏页在内存中停留更长的时间,避免过于频繁地将它们写回磁盘。
对于那些好奇的人,以下是我用来创建分区和配置内存的内容。
创建分区
已更新 PK 以包含分区中使用的范围列
ALTER TABLE message_log CHANGE COLUMN created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, DROP PRIMARY KEY, ADD PRIMARY KEY (db_id, created);
使用 ALTER 添加分区 TABLE。
事后看来,我应该将每个分区创建为单个 ALTER 语句并在后续分区上使用 Reorganize Partition (and 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 Resize and Rick Jame's page on memory
你也可以在 选项文件 菜单中的 MySQL Workbench 然后 innoDB标签。您在此处所做的任何更改都将写入配置文件,但您需要停止并启动 MySQL 以读取配置,否则您也可以设置全局值以使其生效。
太划算了!我得到 4 次提及,即使没有写评论或回答。我正在写一个答案,因为我可能会有一些进一步的改进...
是的,PARTITION BY RANGE(TO_DAYS(...))
是正确的方法。 (可能有 小 个备选方案。)
4GB RAM 的 70% 空间紧张。确保没有交换。
您提到了一个查询。如果是主要关注的,那么这个会好一点:
PRIMARY KEY(device_id, created, db_id), -- desired rows will be clustered
INDEX(db_id) -- to keep AUTO_INCREMENT happy
如果您不清除旧数据,那么即使没有分区,上述关键建议也能提供同样高的效率。
lat/lon representation 说 DOUBLE
太过分了。
注意 inefficiency of UUID,尤其是对于大表。