【问题标题】:Improving a Active Record / Postgresql Query Further进一步改进 Active Record / Postgresql 查询
【发布时间】:2017-12-18 23:04:13
【问题描述】:

跟进我的问题here,我正在尝试进一步改进搜索。我们首先搜索一个回放表(搜索 2k 条记录),然后获取与该表关联的唯一玩家(每 10 个,因此 20k 条记录)并呈现 JSON。这是通过控制器完成的,搜索内容为:

def index
 @replays = Replay.includes(:players).where(map_id: params['map_id'].to_i).order(id: :desc).limit(2000)
 render json: @replays[0..2000].to_json(include: [:players])
end 

表现:

Completed 200 OK in 254032ms (Views: 34.1ms | ActiveRecord: 20682.4ms)

实际的 Active Record 搜索如下:

Replay Load (80.4ms)  SELECT  "replays".* FROM "replays" WHERE "replays"."map_id" = $1 ORDER BY "replays"."id" DESC LIMIT $2  [["map_id", 1], ["LIMIT", 2000]]
Player Load (20602.0ms)  SELECT "players".* FROM "players" WHERE "players"."replay_id" IN (117217...

这主要是有效的,但仍然需要非常多的时间。有没有办法提高性能?

【问题讨论】:

  • 只是一个快速的仅供参考(正如我按照上一个问题进行的那样) - 现在您不需要 index 第二行上的 [0..2000]。 limit@GustavMauler 对此进行了介绍。
  • 是的,对不起。我从来没有真正做过一个数据量这么大(大多较小)的个人项目,所以额外的 [0..2000](除了冗余)会对性能有那么大的影响吗?
  • 应该不会有太大影响 - 最好事先限制查询,因为它会减少对数据库的要求。这可能是我的新答案的失败 :) 不过,在您的代码中这绝对是多余的,并且实际上还会为您提供 2,001 条记录,因为它从 0 开始计数。
  • 我想知道为什么第二个查询需要这么长时间?数据库列上是否缺少索引?还是有太多玩家无法记忆?返回的 JSON 有多大?
  • 这绝对是索引问题。 json 相当大(2k 重播 = 20k 玩家),但还没有到应该花这么长时间的地步。

标签: ruby-on-rails ruby postgresql activerecord


【解决方案1】:

你被这个问题困扰https://postgres.cz/wiki/PostgreSQL_SQL_Tricks_I#Predicate_IN_optimalization

当值列表长于八十个数字时,我发现了关于 IN 谓词优化可能性的 pg_performance 注释。对于更长的列表,最好使用多值创建常量子查询:

选择 * 从选项卡 其中 x IN (1,2,3,..n); -- n > 70

-- 更快的情况 选择 * 从选项卡 WHERE x IN (VALUES(10),(20));

对大量项目使用 VALUES 会更快,因此不要将它用于少量值。

基本上,具有一长串值的SELECT * FROM WHERE IN ((1),(2)...) 非常慢。如果您可以将其转换为值列表(例如SELECT * FROM WHERE IN (VALUES(1),(2) ...)

),则速度会快得离谱

不幸的是,由于这发生在活动记录中,因此对查询进行控制有点棘手。您可以避免使用includes 调用,只需手动构造 SQL 以加载所有子记录,然后手动建立关联。

或者,您可以修改活动记录。这是我在 Rails 4.2 上的初始化程序中所做的。

module PreloaderPerformance
  private
  def query_scope(ids)
    if ids.count > 100
      type = klass.columns_hash[association_key_name.to_s].sql_type
      values_list = ids.map do |id|
        if id.kind_of?(Integer)
          " (#{id})"
        elsif type == "uuid"
          " ('#{id.to_s}'::uuid)"
        else
          " ('#{id.to_s}')"
        end
      end.join(",")

      scope.where("#{association_key_name} in (VALUES #{values_list})")
    else
      super
    end
  end
end

module ActiveRecord
  module Associations
    class Preloader
      class Association #:nodoc:
        prepend PreloaderPerformance
      end
    end
  end
end

这样做我发现我的一些查询速度提高了 50 倍,目前还没有任何问题。请注意,它没有经过全面的实战测试,我敢打赌,如果您的关联使用唯一数据类型来处理外键关系,它会出现一些问题。在我的数据库中,我只使用 uuid 或整数来进行关联。关于猴子修补核心导轨行为的常见警告适用。

【讨论】:

【解决方案2】:

我知道find_each 可以用于批量查询,这可能会减轻这里的内存负载。您能否尝试以下方法,看看它对时间的影响?

Replay.where(map_id: params['map_id'].to_i).includes(:players).find_each(batch_size: 100).map do |replay|
  replay.to_json(includes: :players)
end

我不确定这是否可行。可能是映射否定了批处理的好处 - 肯定有更多查询,但它会使用更少的内存,因为它不需要一次存储超过 20k 条记录。

试一试,看看它的外观 - 也调整批量大小,看看它对事情的影响。

有一个警告,您不能应用限制,因此请记住这一点。

我相信其他人会想出一个更巧妙的解决方案,但同时希望这可能会有所帮助。如果您检查速度时感觉很糟糕,请告诉我,我将删除此答案:)

【讨论】:

  • 我很快就会解决这个问题。限制的主要原因实际上是阻止它搜索所有记录,所以我有一个合理的样本量。因此,如果性能好,那么失去限制实际上是有益的。
  • 是的,可以理解 - 我不确定这是否会带来改进,尽管我知道它过去曾对我派上用场一两次!很想知道你是怎么过的。
  • 经过试验,我认为急切加载会获得更好的性能,并且允许我在选择时施加限制。
猜你喜欢
  • 2011-10-22
  • 2013-02-27
  • 2013-05-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多