MySQL INSERT... 选择包含 4.2 亿条记录的大型数据集

Posted

技术标签:

【中文标题】MySQL INSERT... 选择包含 4.2 亿条记录的大型数据集【英文标题】:MySQL INSERT... SELECT Large Dataset of 420 Million Records 【发布时间】:2019-03-30 16:44:51 【问题描述】:

我有一个包含大约 4.2 亿条记录的大型数据集,我能够使用 LOAD DATA INFILE 语句在大约 15 分钟内将它们及时加载到临时表中。我需要这个临时表来暂存数据,因为我在将其加载到最终目的地之前对其进行了一些清理。

临时表定义为:

CREATE TABLE `temporary_data` (
  `t_id` smallint(10) unsigned NOT NULL,
  `s_name` varchar(512) NOT NULL,
  `record_type` varchar(512) NOT NULL,
  `record_value` varchar(512) NOT NULL
) ENGINE=MyISAM;

需要加载此数据的目标表称为my_data,其定义为:

CREATE TABLE `my_data` (
  `s_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `t_id` smallint(10) unsigned NOT NULL,
  `s_name` varchar(63) NOT NULL,
  PRIMARY KEY (`s_id`),
  UNIQUE KEY `IDX_MY_DATA_S_NAME_T_ID` (`t_id`,`s_name`) USING BTREE,
  KEY `IDX_MY_DATA_S_NAME` (`s_name`) USING BTREE,
  CONSTRAINT `FK_MY_DATA_MY_PARENT` FOREIGN KEY (`t_id`) REFERENCES `my_parent` (`t_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

问题是,将临时表中的数据加载到my_data 的查询非常慢,因为我怀疑这是因为my_data 包含两个索引和一个主键。到目前为止,这个查询已经运行了 6 个多小时:

INSERT IGNORE INTO my_data (t_id, s_name)
SELECT t_id, s_name
FROM temporary_data;

我需要确定一种方法来加快此查询的速度,以便及时完成(最好在 30 分钟内完成)。

我考虑过的一些方法:

    禁用索引:我可能能够通过禁用/删除IDX_MY_DATA_S_NAME 索引而侥幸成功,但我依靠唯一索引 (IDX_MY_DATA_S_NAME_T_ID) 来保持数据清洁。这是一个每天都会自动运行的过程,不可避免地会有一些重复。另外,当我再次启用索引时,在这么大的数据集上重建索引似乎同样耗时。 使用 DATA OUTFILE: 将清理后的数据直接导出并重新导入到 my_data。我在某处看到了这个推荐,但在考虑之后,索引/PK 仍然是重新插入的争论点。 交换表格:my_data 替换为 temporary_data 听起来很吸引人,但该表格对于 s_id 字段有很多外键关系,因此我希望确保这种方法值得麻烦禁用外键并重新启用它们。子表包含的记录将明显少于my_data,因此在这方面重新启用外键可能可以忽略不计。 LOAD DATA INFILE directly: 使用语句的 SET 部分中的条件将数据直接加载到 my_data 以使所有字段 NULL 当它不符合我最初应用于 @ 的清理标准时987654338@ 在将其加载到 my_data 之前。这很 hacky,但它依赖于假设 LOAD DATA INFILE 将比 INSERT 更快... SELECT 即使面对索引,由于表上的唯一约束,在运行后只会删除一行空值.

这些听起来都不是非常棒的想法。如果有人有任何提示,我会全力以赴。

【问题讨论】:

show status like '%inno%wait%'; 显示什么? Innodb_buffer_pool_wait_free 0 Innodb_log_waits 0 Innodb_row_lock_current_waits 0 Innodb_row_lock_waits 0 我正在使用每个表模式的单独文件,我看到 .ibd 文件继续增长。 show processlist 表明它也在 sending data 状态下执行。 查看技巧和技巧here 感谢您的链接。我一定会检查出来的。 【参考方案1】:

去掉s_id,估计没用了。然后推广UNIQUE(t_id, s_name) to be thePRIMARY KEY`。这减少了为插入的每一行执行的测试次数。

考虑禁用FOREIGN KEYs;毕竟,他们需要执行可能是多余的检查。

INSERT IGNORE INTO my_data (t_id, s_name)
    SELECT t_id, s_name
    FROM temporary_data
    ORDER BY t_id, s_name;  -- Add this

这样,插入不会在目标表中跳跃,从而(希望)避免大量 I/O。

你在增加桌子吗?还是换掉?如果替换,有更好的方法。

更多...

您是否注意到INSERT IGNORE 为每一行未插入的行浪费了一个AUTO_INCREMENT 值?让我们尝试另一种方法...

INSERT INTO my_data (t_id, s_name)
    SELECT t.t_id, t.s_name
        FROM temporary_data AS t
        LEFT JOIN my_data AS m  USING(t_id, s_name)
        WHERE m.s_id IS NULL
        ORDER BY t.t_id, t.s_name;

ORDER BY 避免在INSERT 期间跳来跳去。LEFT JOIN 将活动限制为“新”行。 不会销毁任何 AUTO_INCREMENT 值。

每次将插入多少行?如果是数百万,那么最好将其分成块。请参阅我的discussion 分块。它可能比建立一个巨大的撤消轨迹最终折腾更快。

进一步讨论 -- 给定

my_data:  PRIMARY KEY(s_id)  -- and s_id is AUTO_INCREMENT
my_data:  INDEX(t_id, s_name)
INSERT...SELECT...ORDER BY (t_id, s_name)  -- same as index

这些是有效的:

由于ORDER BY 和二级索引相同,索引的添加将高效完成。 同时,新的AUTO_INCREMENT 值将在表格的“末尾”按顺序生成。

如果(t_id, s_name) 是唯一的,那就更好了。那我们可以考虑彻底去掉s_id,把两个索引改成这个:

PRIMARY KEY(t_id, s_name)

如果其他表引用s_id,这将是一个问题。 可能的解决方法是保留 s_id 并拥有

PRIMARY KEY(t_id, s_name)
INDEX(s_id)   -- sufficient for AUTO_INCREMENT

我对大局和其他查询的了解不够,无法判断要采取的方向。所以我最初的建议(在“进一步讨论”之前)是“保守的”。

【讨论】:

不幸的是,我正在扩充。这是我为加载 4.2 亿条记录而创建的日常 ETL 工作。目标是添加新的并且永远不会删除任何。大约12小时后完成。我无法摆脱s_id,因为它被用作其他大约七个表的外键,这些表比my_data 中存储的表要稀疏得多。我以这种方式设计它,因此我不会为稍后出现的可选数据存储一堆空字段,从而节省一些空间。我将使用ORDER BY 子句尝试新查询,看看是否有帮助。我也在努力迁移到 SSD。 @Adam - 我在答案中添加了更多内容。@Adam 如果 PK 将保持 s_id,您是否仍建议保留 ORDER BY t.t_id, t.s_name; 子句?即使 t_id 和 s_name 的复合唯一索引不是 PK,每次强制它们以相同的顺序插入也会“使”它以 PK 顺序插入对吗? @Adam - 请参阅我的“进一步讨论”。 感谢您对此提供的所有帮助。有趣的是,当我现在使用 ORDER BY 运行查询时,我在进程列表中看到它由于 ORDER BY 子句而创建了一个排序索引,并在我的 Digital Ocean 框中运行 df -h /mnt/* 显示了卷的大小我将 mysql 的 tmpdir 映射到稳步增加。然后大约 20-30 分钟后,进程列表显示命令现在处于“睡眠”状态,并且 tmpdir 空间被释放。结果没有被插入,也没有错误。它只是说进程正在休眠。

以上是关于MySQL INSERT... 选择包含 4.2 亿条记录的大型数据集的主要内容,如果未能解决你的问题,请参考以下文章

MySQL-01-笔记

130 MySQL记录操作

MYSQL:存储过程,插入一行,然后通过 LAST_INSERT_ID() 选择它

TSQL 帮助 | INSERT 语句的选择列表包含的项目少于插入列表 [关闭]

MySQL INSERT INTO ... 值和选择

怎样将mysql中表格数据装换成insert 语句?