MySql insert into select 查询太慢,无法复制 1 亿行

MySql insert into select query is too slow to copy 100 million rows

我有一个 table 包含 100+ 百万行,我想将数据复制到另一个 table。我有1个要求, 1.查询执行不能阻塞对这些数据库table的其他操作, 我写了一个存储过程如下

我计算源中的行数 table 然后有一个循环但在每次迭代中复制 10000 行,启动事务并提交它。然后通过偏移量读取下一个 10000。

CREATE PROCEDURE insert_data()
BEGIN
  DECLARE i INT DEFAULT 0;
  DECLARE iterations INT DEFAULT 0;
  DECLARE rowOffset INT DEFAULT 0;
  DECLARE limitSize INT DEFAULT 10000;
  SET iterations = (SELECT COUNT(*) FROM Table1) / 10000;

  WHILE i <= iterations DO
    START TRANSACTION;
        INSERT IGNORE INTO Table2(id, field2, field3)
            SELECT f1, f2, f3
            FROM Table1
            ORDER BY id ASC
            LIMIT limitSize offset rowOffset;
    COMMIT;
    SET i = i + 1;
    SET rowOffset = rowOffset + limitSize;
  END WHILE;
END$$
DELIMITER ;

查询在未锁定 table 的情况下执行,但在复制几百万行后它变得太慢了。 请提出任何更好的方法来完成任务。 谢谢!

块是关键词。希望您使用的是 InnoDB(在记录级别阻塞)而不是 MyIsam(在 table 级别阻塞)。不知道数据或底层硬件的复杂性,每个循环 10K 条记录可能太大了。

任何 INSERT ... SELECT ... 查询都会对它从 SELECT 中的源 table 读取的行执行 acquire a SHARED lock。但是通过处理较小的行块,锁不会持续太久。

随着您在源 table 中前进,使用 LIMIT ... OFFSET 的查询会越来越慢。每个块 10,000 行,您需要 运行 查询 10,000 次,每次都必须重新开始并扫描 table 以到达新的 OFFSET。

无论您做什么,复制 1 亿行都需要一段时间。它做了很多工作。

我会使用 pt-archiver,一个专为此目的而设计的免费工具。它处理 "chunks" 中的行(或子集)。它会动态调整块的大小,使每个块需要 0.5 秒。

你的方法和 pt-archiver 最大的区别在于 pt-archiver 不使用 LIMIT ... OFFSET,它沿着主键索引遍历,按值而不是按位置选择行块。所以每个块都被更有效地读取。


回复您的评论:

我预计减小批量大小——并增加迭代次数——会使性能问题更糟,而不是更好。

原因是,当您将 LIMITOFFSET 一起使用时,每个查询都必须从 table 的开头重新开始,并计算到 [= 的行数15=] 值。当您遍历 table.

时,它会变得越来越长

运行 20,000 个使用 OFFSET 的昂贵查询将比 运行ning 10,000 个类似查询花费更长的时间。最昂贵的部分不会读取 5,000 或 10,000 行,或将它们插入目标 table。昂贵的部分将一遍又一遍地跳过约 50,000,000 行。

相反,您应该通过 而不是偏移量来迭代 table。

INSERT IGNORE INTO Table2(id, field2, field3)
        SELECT f1, f2, f3
        FROM Table1
        WHERE id BETWEEN rowOffset AND rowOffset+limitSize;

循环前查询MIN(id)和MAX(id),从rowOffset最小值开始,循环到最大值

这就是 pt-archiver 的工作方式。

感谢@Bill Karvin 我按照你的建议删除了偏移量。以下查询效果非常好,

DROP PROCEDURE IF EXISTS insert_identifierdataset;
DELIMITER $$
CREATE PROCEDURE insert_data()
BEGIN
  DECLARE i INT DEFAULT 0;
  DECLARE limitSize INT DEFAULT 2000;
  DECLARE maxId INT DEFAULT 0;

  SET maxId = (SELECT MAX(id) FROM Table1);

  WHILE i <= maxId DO
    START TRANSACTION;
        INSERT IGNORE INTO Table2(id, field1, field2)
            SELECT id, field3, field4
                FROM Table1
                WHERE id> i
                ORDER BY id ASC
                LIMIT limitSize;
    COMMIT;
    SET i = i + limitSize;
  END WHILE;
END$$  

根据我的设置和需要 - 我必须复制 300 到 5 亿行。 及时。在大量低于规格的服务器上。

转储到"csv"; 将结果拆分为多个文件(我的案例 200k 行是最佳的) 导入拆分文件

SELECT a.b.c INTO OUTFILE '/path/dumpfile.csv'   FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"'   LINES TERMINATED BY '\n';

split -a 6 -l 200000 dumpfile.csv FileNamePrefix.  // . allows numbering as ext

for f in ./*;
do 
mysql -uUser -pPassword dbname -e "set autocommit = 0; set unique_checks = 0; set foreign_key_checks = 0; set sql_log_bin=0; LOAD DATA CONCURRENT INFILE '/path/to/split/files/"$f"' IGNORE  INTO TABLE InputTableName FIELDS TERMINATED BY ','  OPTIONALLY ENCLOSED BY '\"'  (a, b, c);commit;"; 
echo "Done: '"$f"' at $(date)"; 
done