随着您的应用程序开始获得更多流量,您可能会注意到它变得更加缓慢。要解决这个问题,大多数人首先要查看服务器上的内存和CPU利用率。通常这是一个瓶颈。但这通常会在问题再次开始缓慢之前解决问题。经常被忽视的一个方面是SQL性能和每个请求执行的查询数。

在这里,我将介绍一些常常被遗忘或忽视的低悬果实。对于没有机会处理大规模部署的应用程序的更多缺乏经验的开发人员来说尤其如此,我当然对所有这些应用程序都感到内疚。

使用列上的索引可以查看常量查找时间

这不是书中通常教导或提到的内容,但列上缺少索引是某些查询需要几秒而不是几毫秒才能执行的最大原因之一。如果您不知道索引是什么,我建议先阅读它们然后再回来阅读本文。

一个User模型是应适用的指数一个非常好的例子。所以你有一个User带有列模型email这是一个非常常见的用例,大多数应用程序使用user_name字段或email字段进行身份验证。

让我们来看看你的控制器可能看起来如何。

class UsersController < ApplicationController
  def sign_in
    @user = User.where("email = ?", params[:email])
    if @user.password == params[:password]
      redirect_to secret_page
    else
      flash[:error] = "your email/password seems to be invalid"
      redirect_to my_sign_in_page
    end
  end
end

现在代码看起来非常好,并没有任何问题。但是这会导致查询速度变慢。当您的数据库有几千条记录时,您可能会注意到此问题。但是,当您添加更多用户时,您的查询执行时间将开始逐渐增加。

这里错误的是该email字段没有索引。因此,当您执行查询时,mysql(或Postgres)必须执行什么称为全索引扫描。这意味着它必须逐行查看表中的每条记录,看它是否与您输入的电子邮件相符。

我们可以通过在电子邮件列上创建索引来轻松解决此问题。首先创建一个新的迁移,然后添加这些行。

add_index :users, :email

无论表大小如何,使用索引都会使查找时间保持不变。

更高级的情况是,当您一起使用usernameemail字段时,您有一个这样的查询

class UsersController < ApplicationController
  def sign_in
    @user = User.where("username = ? OR email = ?", params[:username], params[:email])
    if @user.password == params[:password]
      redirect_to secret_page
    else
      flash[:error] = "your email/password seems to be invalid"
      redirect_to my_sign_in_page
    end
  end
end

会发生什么是您同时查询两个列。为此,有一个复杂版本的索引称为复合索引,它允许您使用来自多个列的数据创建1个索引。要创建复合索引,请在迁移中使用以下代码。该数组可以包含查询中使用的任意数量的字段。

  add_index :users,[:email, :username]

请记住一些警告。索引确实会影响插入(写入)速度。您在表上拥有的索引越多,插入的速度就越慢,因为数据库必须平衡索引树。因此,在创建索引时,请确保您确实需要它并尝试尽可能少地使用它。

使用复合索引时,请确保查询实际上在同一时间使用所有字段。如果您的查询仅使用复合索引中的1个字段,则不会使用索引,因此它将解析为执行全索引扫描。复合键对插入速度的影响也最大,因此尽可能少地使用它们。

多态关系和外键的索引

大多数程序员忘记添加索引的另一个领域是多态关系和外键。如果您有任何多态的类,请不要忘记将索引添加到其列中

# polymorphic example from
# http://guides.rubyonrails.org/association_basics.html#the-has-many-through-association

class Picture < ActiveRecord::Base
  belongs_to :imageable, polymorphic: true
end

class Employee < ActiveRecord::Base
  has_many :pictures, as: :imageable
end

class Product < ActiveRecord::Base
  has_many :pictures, as: :imageable
end

要获得最佳性能,请在上添加索引imageable_id如果您有任何多态关系,请确保您在该*_id字段上有索引

add_index :pictures, :imageable_id

外部表和联接表也是人们忘记应用索引的地方。花几个小时来查看数据库,找到可以通过应用索引获得快速获胜的表格。如果您使用Mac和MySQL,我建议Sequel Pro浏览您的数据库。

仅选择需要的列

我认为不常用的一种ActiveRecord方法是select让我们再举一个User模型的例子username, email, password, created_at在模型中可能还有许多其他领域。

让我们假设您有一个各种管理面板,并且您希望在应用程序中查看用户的所有电子邮件ID。所以在你的控制器中你会写这样的东西

class UsersController < ApplicationController
  def index
    @users = User.all
  end
end

虽然它是完美的合理代码,但它可以做得更好。看到的事情是你只需要email字段,但因为我们没有告诉它到ActiveRecord,它假设我们想要所有的列。所以最终发生的事情是你加载的数据量比实际需要多3倍,因此服务器上的内存消耗量增加了3倍。这可能现在看起来非常无害,但随着数据库中字段数量的增加,它会逐渐增加。

更好的方法是使用select命令并指定要使用的字段。

class UsersController < ApplicationController
  def index
    @users = User.select('email').all
  end
end

select命令仅加载所需的列。在昂贵的查询中执行此操作时,我已经看到了2x - 5x的性能提升。

使用find_each批量加载数据

大多数应用程序需要在后台执行操作,可能会发送一堆电子邮件,生成一些报告,也许它会将数据发送到API等。进入这些内容的代码通常是经过深思熟虑的。在后台执行任务时,请确保您不是一次加载整个表。这可能严重影响其他查询的性能。而是通过以100或1000的批量加载数据来执行任务。ActiveRecord find_each命令就是为此而建的。默认情况下,该find_each命令将以1000个批次的形式从数据库中加载记录,但是当表格具有非常多的列时,您也会传入自定义数字。

# http://api.rubyonrails.org/classes/ActiveRecord/Batches.html#method-i-find_each
Person.find_each do |person|
  person.do_awesome_stuff
end

Person.where("age > 21").find_each do |person|
  person.party_all_night!
end

渴望加载和删除N + 1个查询

消除N + 1查询的主题本身就是一篇文章,但我想提一下。删除N + 1个查询可以获得10倍的性能提升。只是因为有时您可能会发现自己在一个页面上运行了100个查询。因此,您可以通过消除这些来节省您节省的时间。当您的查询导致N + 1呼叫时子弹宝石是一个漂亮的小工具。安装gem并逐页开始查找最严重的违规者。

结论

我上面列出的大多数方法都非常简单,直接且快速实现。与更复杂的方法(如缓存)不同,这些方法不需要任何架构更改。通过进行这些小改动,您不仅可以通过更快地加载页面来让用户满意,还可以通过充分利用现有服务器基础架构来节省资金。

是否存在一些您已经使用或了解的方法,这些方法通常被大多数程序员忽略。通过在下面的评论中提及它们来告诉所有人。

0条评论 顺序楼层
请先登录再回复