在短时间内为拥有 100 万用户的 table 中的用户生成 slug,而不影响 Rails 中的数据库性能?

Generating slug for users in a table with 1 million users in a short time without affecting db performance in Rails?

我想为我的用户添加一列 table 并为所有用户生成 slug。问题是我的数据库中有超过 100 万用户。

我看过各种解释不同方法的博客,但我不想冒险在我的生产数据库中这样做。

我找到的方法:

  1. 下面的方法建议将代码添加到 在迁移中生成 slug 文件本身。

    class AddStatusToUser < ActiveRecord::Migration
      class User < ActiveRecord::Base
      end
    
      def up
        add_column :users, :status, :string
        User.find_each do |user|
          user.status = 'active'
          user.save!
        end
      end
    
      def down
        remove_column :users, :status
      end
    end
    
  2. 我已经通过 rake 任务编写了这个 运行 方法: 下面的问题是 运行ning 4 天,到目前为止只生成了 400 000 个 slug。我想快点做,但不知道怎么做。

find_in_batches:

Yields each batch of records that was found by the find options as an array. The size of each batch is set by the :batch_size option; the default is 1000.

You can control the starting point for the batch processing by supplying the :start option. This is especially useful if you want multiple workers dealing with the same processing queue. You can make worker 1 handle all the records between id 0 and 10,000 and worker 2 handle from 10,000 and beyond (by setting the :start option on that worker).

It’s not possible to set the order. That is automatically set to ascending on the primary key (“id ASC”) to make the batch ordering work. This also mean that this method only works with integer-based primary keys. You can’t set the limit either, that’s used to control the batch sizes.

为了避免数据库性能问题,我为 1000 个用户设置了每次 slug 生成后 2 秒的休眠时间。我应该删除睡眠方法吗?我应该 运行 User.find_each(&:save) 还是方法 1?

task :add_slug_to_all_users => :environment do
  i=0
  batchSize = 1000
  puts "started at :#{Time.now}"
  # find_in_batches method provides the users in batches of 1000 
  # so that the update is not triggered for all the rows at once which may lock the table completely.
  User.where("slug is null and email is not null").find_in_batches(batch_size: batchSize) do |users|
    sleep(2)
    users.each {|u| u.save!; i+=1;} 
    puts "updated #{i} records at: #{Time.now}"
  end
  puts "Completed the Task at: #{Time.now}\n"
end

更新 1:我正在使用 friendly_id gem 生成 slug。

更新 2:我有 运行 SHOW CREATE TABLE users 我得到了这个:

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `first_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `last_name` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `p_views` int(11) DEFAULT '0',
  `p_desc` text COLLATE utf8_unicode_ci,
  `p_title` text COLLATE utf8_unicode_ci,
  `created_at` datetime DEFAULT NULL,
  `updated_at` datetime DEFAULT NULL,
  `t_zone` varchar(255) COLLATE utf8_unicode_ci DEFAULT 'UTC',
  `college` varchar(500) COLLATE utf8_unicode_ci DEFAULT NULL,
  `degree` text COLLATE utf8_unicode_ci,
  `p_no` varchar(15) COLLATE utf8_unicode_ci DEFAULT NULL,
  `slug` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `unique_phone_number` (`p_no`),
  UNIQUE KEY `index_users_on_phone_no` (`p_no`),
  UNIQUE KEY `index_users_on_slug` (`slug`),
  KEY `use_index_on_college` (`college`(255))
) ENGINE=InnoDB AUTO_INCREMENT=2194 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci |

请注意,我已经从上面的结果中删除了大部分字段。 slug 列将 first_namelast_name 的组合存储在 url 友好方式。

例如如果用户名是:

id first_name   last_name
1  Arun         Kumar
2  Arun         Kumar

生成的 slug 看起来像这样:

id slug
1  arun-kumar
2  arun-kumar1

在这种情况下,通用的第 3 方软件只能挡路。你最好去 SQL 做这项工作。

如果 "slug" 是一个简单的序列号,那么添加一个 AUTO_INCREMENT 将是显而易见的解决方案,并且是永久性的解决方案。也就是说,所有未来的添加都会自动生成 slug。这可以用一个语句来完成:

ALTER TABLE t
    ADD COLUMN slug INT UNSIGNED AUTO_INCREMENT,
    INDEX(slug);

slug 可能会更好 PRIMARY KEY (请提供 SHOW CREATE TABLE。)但这可能需要对 table 进行严格的锁定;所以普通索引更好。测试它。可能是 "fast enough".

下一个想法是pt-online-schema-change(见Percona.com),它是一种特殊工具,可以有效地做到ALTERs,几乎零影响。它涉及添加一个 TRIGGER 来捕获写入和复制的分块。需要复制 "last little bit" 带来的轻微影响。最后的 RENAME TABLE real TO old, new TO real; 是原子的,基本上是瞬时的。它甚至可以动态调整 "sleep"。它是一个优秀的工具,具有多年的经验。

但是,ptosc 可能无法添加像 PRIMARY KEY 这样重要的东西,因此我的建议(以上)是普通的 INDEX

设置值(通过 UPDATE),一次一个块,是正确的方法。我在 chunking tips 上写过;旨在 DELETE,但可以适应 UPDATE

在不知道find_in_batches()中的"under the covers"是什么的情况下,我不能说它是好是坏。我知道 OFFSET 几乎总是不好的; "remembering where you left off" 通常要好得多。但如果您还没有 UNIQUEPRIMARY 密钥,则很难做到这一点。 PRIMARY 更好,因为它有聚类。 (请提供SHOW CREATE TABLE,这样我就不用瞎猜了。)

如果您的示例代码每次都从 table 的开头重新开始,那么它与使用 OFFSET 一样糟糕 - 每次迭代都会比前一次慢,因为它正在跳过越来越多的行。

添加一列后,请务必检查对 table 的所有引用 -- SELECT * 现在将多一列(不使用 * 的原因之一)。 UPDATEsINSERTs 可以处理缺少的列,但您需要检查一下。

更新

有两个步骤 -- 添加 slug 列并填充它。您已经完成了第一步。

要执行第二步,我建议使用 AUTO_INCREMENT PRIMARY KEY 一次单步执行 table 100 行。 100 足够低,不会太具侵入性。 AI PK 将涵盖整个 table,并且非常高效,因此您不需要缓慢 OFFSET 或搜索 un-slugged 集。我讨论了有效的分块 here。它是在考虑 DELETE 的情况下编写的,但这些技术适用于 UPDATE.