如何在不锁定 MYSQL 6.2 中的 table 的情况下从另一个 table 复制数据?
How to copy data from another table without lock the table in MYSQL 6.2?
我有一个 table,它拥有 Mysql 服务器中的所有历史数据,而且它非常庞大(大约 7 亿行)。我正在创建一个具有相同列但分区的新 table,然后我需要将旧 table 中的所有数据复制到新分区 table 中。我已经有了正确的脚本来执行此操作,但我认为它可能会锁定 table。我不希望发生这种情况,因为它在生产服务器上。我应该怎么做才能避免锁定 table?
假定表格具有完全相同的列,您可以执行如下操作:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;
INSERT INTO NEW_TABLE (SELECT * FROM OLD_TABLE);
COMMIT ;
我根据 Wistar's 评论添加了一些额外的解释。这里可以使用的读取级别是:
- READ COMMITTED:关于一致(非锁定)读取的有点类似于 Oracle 的隔离级别:每个一致读取,即使在同一事务中,设置和读取自己的新鲜快照
- READ UNCOMMITTED:SELECT 语句以非锁定方式执行,但可能会使用行的可能早期版本。因此,使用这个隔离级别,这样的读取是不一致的。这也称为脏读。否则,此隔离级别的工作方式类似于 READ COMMITTED。
- REPEATABLE READ:这是 InnoDB 的默认隔离级别。对于一致性读,与READ COMMITTED隔离级别有一个重要区别:同一个事务内的所有一致性读都读取第一次读建立的快照。此约定意味着如果您在同一事务中发出多个普通(非锁定)SELECT 语句,则这些 SELECT 语句也彼此一致。
- SERIALIZABLE:这个级别类似于可重复读取,但 InnoDB 隐式地将所有普通 SELECT 语句转换为 SELECT ... 锁定共享模式,如果自动提交被禁用。如果启用了自动提交,则 SELECT 是它自己的事务。因此已知它是只读的,如果作为一致(非锁定)读取执行并且不需要为其他事务阻塞,则可以序列化。 (如果其他事务修改了选定的行,要强制普通 SELECT 阻塞,请禁用自动提交。)
希望对您有所帮助。
我不知道你的脚本是什么,但我建议你用卡盘插入。 See this example.
如果您使用 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
,您插入的行的版本可能不正确。如果你用一个选择插入所有行,那么你不仅会有锁而且它不会是最好的性能明智。
你可以做类似
INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET 0 )
INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET 1000 )
INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET 2000 )
...
或使用 prepare statement 和 while
CREATE PROCEDURE myproc()
BEGIN
@rows :=0
SELECT COUNT(*) FROM OLD_TABLE into @rows
DECLARE i int DEFAULT 0;
WHILE i <= @rows DO
PREPARE stmt1 FROM 'INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET ? )'
EXECUTE stmt1 USING @i;
DEALLOCATE PREPARE stmt1;
SET i = i + 1000;
END WHILE;
END
当然,您可以根据您的配置通过更改 LIMIT
大小
来调整块大小
分块复制。你有 AUTO_INCREMENT
PRIMARY KEY
吗?如果是这样就做
WHERE id >= $x AND id < $x + 1000
如果有很多差距,或者如果您有其他问题,请参阅 other techniques for efficiently chunking。
The evils of Pagination via OFFSET
.
更好的是,使用 Percona 的 pt-online-schema-alter
。它完成了我所描述内容背后的大部分思考,plus 它允许您在复制完成时写入 table。 (它使用 TRIGGERs
来实现它。)
为了减少使用 OFFSET
的缺点,this article 描述了在数字主键 id
可用于强制使用时使用 JOIN
的可能方法适当的指数。请注意,为了跟踪进程,会创建一个 "procedure_log" table 并在处理一批后逐渐更新:
对于MySQL:
DROP PROCEDURE IF EXISTS copyTable;
DELIMITER |
CREATE PROCEDURE copyTable()
BEGIN
DECLARE batchSize INT DEFAULT 100;
DECLARE i INT DEFAULT 0;
DECLARE rowCount INT;
# Note that we use a WHERE clause to prevent a full table scan / use the index properly
SET rowCount = (SELECT COUNT(id) FROM my_table WHERE id IS NOT NULL);
CREATE TABLE IF NOT EXISTS my_table_copy LIKE my_table;
CREATE TABLE IF NOT EXISTS procedure_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, entry TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );
WHILE i <= rowCount DO
INSERT IGNORE INTO my_table_copy (
SELECT source.* FROM (
SELECT id FROM my_table ORDER BY id LIMIT i, batchSize
) tmp
JOIN my_table source ON source.id = tmp.id
ORDER BY source.id
);
SET i = i + batchSize;
INSERT INTO procedure_log (entry) VALUES (CONCAT('Copied batch from my_table => my_table_copy, batch: ', batchSize, ', offset: ', i, ', rowCount: ', rowCount));
END WHILE;
END |
DELIMITER ;
我有一个 table,它拥有 Mysql 服务器中的所有历史数据,而且它非常庞大(大约 7 亿行)。我正在创建一个具有相同列但分区的新 table,然后我需要将旧 table 中的所有数据复制到新分区 table 中。我已经有了正确的脚本来执行此操作,但我认为它可能会锁定 table。我不希望发生这种情况,因为它在生产服务器上。我应该怎么做才能避免锁定 table?
假定表格具有完全相同的列,您可以执行如下操作:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED ;
INSERT INTO NEW_TABLE (SELECT * FROM OLD_TABLE);
COMMIT ;
我根据 Wistar's 评论添加了一些额外的解释。这里可以使用的读取级别是:
- READ COMMITTED:关于一致(非锁定)读取的有点类似于 Oracle 的隔离级别:每个一致读取,即使在同一事务中,设置和读取自己的新鲜快照
- READ UNCOMMITTED:SELECT 语句以非锁定方式执行,但可能会使用行的可能早期版本。因此,使用这个隔离级别,这样的读取是不一致的。这也称为脏读。否则,此隔离级别的工作方式类似于 READ COMMITTED。
- REPEATABLE READ:这是 InnoDB 的默认隔离级别。对于一致性读,与READ COMMITTED隔离级别有一个重要区别:同一个事务内的所有一致性读都读取第一次读建立的快照。此约定意味着如果您在同一事务中发出多个普通(非锁定)SELECT 语句,则这些 SELECT 语句也彼此一致。
- SERIALIZABLE:这个级别类似于可重复读取,但 InnoDB 隐式地将所有普通 SELECT 语句转换为 SELECT ... 锁定共享模式,如果自动提交被禁用。如果启用了自动提交,则 SELECT 是它自己的事务。因此已知它是只读的,如果作为一致(非锁定)读取执行并且不需要为其他事务阻塞,则可以序列化。 (如果其他事务修改了选定的行,要强制普通 SELECT 阻塞,请禁用自动提交。)
希望对您有所帮助。
我不知道你的脚本是什么,但我建议你用卡盘插入。 See this example.
如果您使用 SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED
,您插入的行的版本可能不正确。如果你用一个选择插入所有行,那么你不仅会有锁而且它不会是最好的性能明智。
你可以做类似
INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET 0 )
INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET 1000 )
INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET 2000 )
...
或使用 prepare statement 和 while
CREATE PROCEDURE myproc()
BEGIN
@rows :=0
SELECT COUNT(*) FROM OLD_TABLE into @rows
DECLARE i int DEFAULT 0;
WHILE i <= @rows DO
PREPARE stmt1 FROM 'INSERT INTO NEW TABLE (SELECT * FROM OLD_TABLE LIMIT 1000 OFFSET ? )'
EXECUTE stmt1 USING @i;
DEALLOCATE PREPARE stmt1;
SET i = i + 1000;
END WHILE;
END
当然,您可以根据您的配置通过更改 LIMIT
大小
分块复制。你有 AUTO_INCREMENT
PRIMARY KEY
吗?如果是这样就做
WHERE id >= $x AND id < $x + 1000
如果有很多差距,或者如果您有其他问题,请参阅 other techniques for efficiently chunking。
The evils of Pagination via OFFSET
.
更好的是,使用 Percona 的 pt-online-schema-alter
。它完成了我所描述内容背后的大部分思考,plus 它允许您在复制完成时写入 table。 (它使用 TRIGGERs
来实现它。)
为了减少使用 OFFSET
的缺点,this article 描述了在数字主键 id
可用于强制使用时使用 JOIN
的可能方法适当的指数。请注意,为了跟踪进程,会创建一个 "procedure_log" table 并在处理一批后逐渐更新:
对于MySQL:
DROP PROCEDURE IF EXISTS copyTable;
DELIMITER |
CREATE PROCEDURE copyTable()
BEGIN
DECLARE batchSize INT DEFAULT 100;
DECLARE i INT DEFAULT 0;
DECLARE rowCount INT;
# Note that we use a WHERE clause to prevent a full table scan / use the index properly
SET rowCount = (SELECT COUNT(id) FROM my_table WHERE id IS NOT NULL);
CREATE TABLE IF NOT EXISTS my_table_copy LIKE my_table;
CREATE TABLE IF NOT EXISTS procedure_log ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, entry TEXT, timestamp DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );
WHILE i <= rowCount DO
INSERT IGNORE INTO my_table_copy (
SELECT source.* FROM (
SELECT id FROM my_table ORDER BY id LIMIT i, batchSize
) tmp
JOIN my_table source ON source.id = tmp.id
ORDER BY source.id
);
SET i = i + batchSize;
INSERT INTO procedure_log (entry) VALUES (CONCAT('Copied batch from my_table => my_table_copy, batch: ', batchSize, ', offset: ', i, ', rowCount: ', rowCount));
END WHILE;
END |
DELIMITER ;