批量更新大型 InnoDB 中的未索引列 table

bulk update to an unindexed column in a large InnoDB table

我在 Mysql 5.1 数据库中有一个 InnoDB table,它有大约 2700 万行。这个 table 有三个未编制索引的 mediumint unsigned 列,我希望它们能够定期、全局地重置为“0”。例如:

update myTable set countA = 0;

这个非常简单的更新查询遇到了 InnoDB 的行级锁定问题。锁定太多行后,更新查询失败并出现记录良好的错误:

ERROR 1206 (HY000): The total number of locks exceeds the lock table size

问题在于如此大的 table 单个行锁的数量已经超过了为存储锁分配的 space。

我找到了一些关于如何处理这个问题的建议:

Lock the whole table to turn off row-locking
这似乎是最好、最干净的解决方案,我对这个特定的 table 在这些不常见的操作中被锁定几分钟没有问题。问题是,给定的解决方案实际上对我不起作用。也许它可以用于旧版本的 Mysql?

Increase the size of the lock buffer
通过增加 Mysql 变量 innodb_buffer_pool_size 的值,我们可以为行锁创建更多空间。我对这个解决方案非常不满意table,因为即使我可以分配足够的 space,随着我的 table 的增长,我也在为失败做好准备。此外,这似乎是一个糟糕的设置,需要创建数 GB 的可以说是不必要的锁。

Index the affected columns (see comments)
如果我们对适当索引支持的单个列进行批量更新,那么 InnoDB 可以避免锁定所有行。通过使用索引,它可以只锁定受影响的行。我实际上试过了,但发现管理这三个索引使我的增量更新慢了很多。由于我将有数千万个更新查询为每个需要重置计数的实例调整这三个计数,所以我不想牺牲增量更新的效率。

Update the column in batches
源文档将此描述为一种变通方法,但我发现它在某种程度上非常有效:

update myTable set countA = 0 where countA != 0 limit 500000;

通过重复执行此操作,直到受影响的行数少于指定的 limit,所有行都会得到更新。这个解决方案对我来说在特别大的 tables 上崩溃了,因为可以在单个迭代中更新的行数急剧下降,因为 Mysql 必须进一步寻找匹配的行。到 1,000 行更新对于一次执行来说太多时,我还有数百万个非零值要更新。

那我还有什么可能呢?

  1. 停止使用 InnoDB:这需要对我当前的流程进行一些其他重组,但我会考虑。
  2. 将计数列移出主 table:如果我有一个 CountA table,那么我可以使用 delete from CountA 重置计数,并且我可以使用内部检索计数加入反对主要 table。这会减慢我对单个计数的更新,因为在有条件地更新或在 CountA table 中插入一行之前,我必须从主 table 获取 ID。不太好,但我会考虑。
  3. 其他既是干净的解决方案又可以预期与我的 tables 一起合理增长的解决方案?

Update: With the help of the accepted response, I now have a batch-processing implementation which gets to job done in about five minutes. Though I would prefer that batch processing wouldn't be necessary, until a more direct solution comes around it seems to be. In case it helps the next person to stumble over this question, here's my related Java JDBC code. (The blog post linked from the accepted answer is recommended reading too.)

    int batchsize = 10_000;
    PreparedStatement pstmt = connection.prepareStatement
            ("UPDATE tableName SET countA = 0, countB = 0, countC = 0 "
                       + "WHERE id BETWEEN ? AND ?");
    for (int left = 0; left < maxId; left += batchsize) {
        pstmt.setInt(1, left + 1);
        pstmt.setInt(2, left + batchsize);
        pstmt.executeUpdate();
    }
    pstmt.close();

计划A

我喜欢分块(批处理)。但是,您的代码草图效率不高。添加 OFFSET 没有帮助。相反,see my blog about walking through the table carefully。即找到'next' 100-1000行;执行 UPDATE;环形。 (注意:每个块应该是它自己的事务。)

"finding the next N rows and remembering where you left off" 的技术取决于 PRIMARY KEY。我的博客涵盖了大多数场景(数字、字符串、稀疏等)。 (该博客讨论 DELETE,但应该很容易将 table 调整为 UPDATE。)

InnoDB 有利于分块,因为 PRIMARY KEY 是集群的。因此,每个块必须读取最少数量的块。

B计划

使用并行 table ("move count columns out of main table") 可能是个好主意,因为要接触的磁盘块数量会更少,因此可以类似于计划 A,但速度更快。使用相同的 PRIMARY KEY(无 AUTO_INCREMENT)。

C计划

(1) 平行 table(如计划 B),加上 (2) 缺少的行意味着值 = 0。然后,通过 TRUNCATE TABLE 实现清算(与方案 A 不同)。由于您要清除三列,因此规则为

  • 当任何值更改为非零时,请确保该行存在于并行 table 中,并根据需要设置值(为其他值加上零)。可能 INSERT ... ON DUPLICATE KEY UPDATE... 是最优的。
  • 在查找值 (SELECT) 时,执行 LEFT JOINIFNULL(col, 0) 以获得值或 0。

计划 X(非初学者)

索引列会造成伤害 -- 当您更新索引列时,数据和索引都必须更改。