在短时间内为拥有 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 万用户。
我看过各种解释不同方法的博客,但我不想冒险在我的生产数据库中这样做。
我找到的方法:
下面的方法建议将代码添加到 在迁移中生成 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
我已经通过 rake 任务编写了这个 运行 方法:
下面的问题是 运行ning 4 天,到目前为止只生成了 400 000 个 slug。我想快点做,但不知道怎么做。
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_name 和 last_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" 通常要好得多。但如果您还没有 UNIQUE
或 PRIMARY
密钥,则很难做到这一点。 PRIMARY
更好,因为它有聚类。 (请提供SHOW CREATE TABLE
,这样我就不用瞎猜了。)
如果您的示例代码每次都从 table 的开头重新开始,那么它与使用 OFFSET
一样糟糕 - 每次迭代都会比前一次慢,因为它正在跳过越来越多的行。
添加一列后,请务必检查对 table 的所有引用 -- SELECT *
现在将多一列(不使用 *
的原因之一)。 UPDATEs
和 INSERTs
可以处理缺少的列,但您需要检查一下。
更新
有两个步骤 -- 添加 slug
列并填充它。您已经完成了第一步。
要执行第二步,我建议使用 AUTO_INCREMENT PRIMARY KEY
一次单步执行 table 100 行。 100 足够低,不会太具侵入性。 AI PK 将涵盖整个 table,并且非常高效,因此您不需要缓慢 OFFSET
或搜索 un-slugged 集。我讨论了有效的分块 here。它是在考虑 DELETE
的情况下编写的,但这些技术适用于 UPDATE
.
我想为我的用户添加一列 table 并为所有用户生成 slug。问题是我的数据库中有超过 100 万用户。
我看过各种解释不同方法的博客,但我不想冒险在我的生产数据库中这样做。
我找到的方法:
下面的方法建议将代码添加到 在迁移中生成 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
我已经通过 rake 任务编写了这个 运行 方法: 下面的问题是 运行ning 4 天,到目前为止只生成了 400 000 个 slug。我想快点做,但不知道怎么做。
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_name 和 last_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" 通常要好得多。但如果您还没有 UNIQUE
或 PRIMARY
密钥,则很难做到这一点。 PRIMARY
更好,因为它有聚类。 (请提供SHOW CREATE TABLE
,这样我就不用瞎猜了。)
如果您的示例代码每次都从 table 的开头重新开始,那么它与使用 OFFSET
一样糟糕 - 每次迭代都会比前一次慢,因为它正在跳过越来越多的行。
添加一列后,请务必检查对 table 的所有引用 -- SELECT *
现在将多一列(不使用 *
的原因之一)。 UPDATEs
和 INSERTs
可以处理缺少的列,但您需要检查一下。
更新
有两个步骤 -- 添加 slug
列并填充它。您已经完成了第一步。
要执行第二步,我建议使用 AUTO_INCREMENT PRIMARY KEY
一次单步执行 table 100 行。 100 足够低,不会太具侵入性。 AI PK 将涵盖整个 table,并且非常高效,因此您不需要缓慢 OFFSET
或搜索 un-slugged 集。我讨论了有效的分块 here。它是在考虑 DELETE
的情况下编写的,但这些技术适用于 UPDATE
.