努力优化 Rails WHERE NOT IN 在 Rails 中的查询

Posted

技术标签:

【中文标题】努力优化 Rails WHERE NOT IN 在 Rails 中的查询【英文标题】:Struggling to optimize rails WHERE NOT IN query in Rails 【发布时间】:2016-07-30 07:41:41 【问题描述】:

这是在 rails 中的查询:

User.limit(20).
  where.not(id: to_skip, number_of_photos: 0).
  where(age: @user.seeking_age_min..@user.seeking_age_max).
  tagged_with(@user.seeking_traits, on: :trait, any: true).
  tagged_with(@user.seeking_gender, on: :trait, any: true).ids

这是EXPLAIN ANALYZE 的输出。请注意,id <> ALL(...) 部分已缩短。里面有大约 10K 个 id。

Limit  (cost=23.32..5331.16 rows=20 width=1698) (actual time=2237.871..2243.709 rows=20 loops=1)
  ->  Nested Loop Semi Join  (cost=23.32..875817.48 rows=3300 width=1698) (actual time=2237.870..2243.701 rows=20 loops=1)
        ->  Merge Semi Join  (cost=22.89..857813.95 rows=8311 width=1702) (actual time=463.757..2220.691 rows=1351 loops=1)
              Merge Cond: (users.id = users_trait_taggings_356a192.taggable_id)
              ->  Index Scan using users_pkey on users  (cost=0.29..834951.51 rows=37655 width=1698) (actual time=455.122..2199.322 rows=7866 loops=1)
                    Index Cond: (id IS NOT NULL)
                    Filter: ((number_of_photos <> 0) AND (age >= 18) AND (age <= 99) AND (id <> ALL ('7066,7065,...,15624,23254'::integer[])))
                    Rows Removed by Filter: 7652
              ->  Index Only Scan using taggings_idx on taggings users_trait_taggings_356a192  (cost=0.42..22767.59 rows=11393 width=4) (actual time=0.048..16.009 rows=4554 loops=1)
                    Index Cond: ((tag_id = 2) AND (taggable_type = 'User'::text) AND (context = 'trait'::text))
                    Heap Fetches: 4554
        ->  Index Scan using index_taggings_on_taggable_id_and_taggable_type_and_context on taggings users_trait_taggings_5df4b2a  (cost=0.42..2.16 rows=1 width=4) (actual time=0.016..0.016 rows=0 loops=1351)
              Index Cond: ((taggable_id = users.id) AND ((taggable_type)::text = 'User'::text) AND ((context)::text = 'trait'::text))
              Filter: (tag_id = ANY ('4,6'::integer[]))
              Rows Removed by Filter: 2
Total runtime: 2243.913 ms

Complete version here.

Index Scan using users_pkey on users 似乎有问题,索引扫描需要很长时间。即使agenumber_of_photosid 上有索引:

add_index "users", ["age"], name: "index_users_on_age", using: :btree
add_index "users", ["number_of_photos"], name: "index_users_on_number_of_photos", using: :btree

to_skip 是一个不可跳过的用户 ID 数组。一个user 有很多skips。每个skip 都有一个partner_id

所以要获取to_skip 我正在做:

to_skip = @user.skips.pluck(:partner_id)

我试图将查询隔离为:

sql = User.limit(20).
  where.not(id: to_skip, number_of_photos: 0).
  where(age: @user.seeking_age_min..@user.seeking_age_max).to_sql

在解释分析中仍然遇到同样的问题。同样,用户 ID 列表被剪断:

Limit  (cost=0.00..435.34 rows=20 width=1698) (actual time=0.219..4.844 rows=20 loops=1)
  ->  Seq Scan on users  (cost=0.00..819629.38 rows=37655 width=1698) (actual time=0.217..4.838 rows=20 loops=1)
        Filter: ((id IS NOT NULL) AND (number_of_photos <> 0) AND (age >= 18) AND (age <= 99) AND (id <> ALL ('7066,7065,...,15624,23254'::integer[])))
        Rows Removed by Filter: 6
Total runtime: 5.044 ms

Complete version here.

关于如何在 rails + postgres 中优化此查询有什么想法吗?

编辑:以下是相关模型:

User model

class User < ActiveRecord::Base
  acts_as_messageable required: :body, # default [:topic, :body]
                      dependent: :destroy

  has_many :skips, :dependent => :destroy

  acts_as_taggable # Alias for acts_as_taggable_on :tags
  acts_as_taggable_on :seeking_gender, :trait, :seeking_race
  scope :by_updated_date, -> 
    order("updated_at DESC")
  
end

# schema

create_table "users", force: :cascade do |t|
  t.string   "email", default: "", null: false
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
  t.text     "skips", array: true
  t.integer  "number_of_photos", default: 0
  t.integer  "age"
end

add_index "users", ["age"], name: "index_users_on_age", using: :btree
add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree
add_index "users", ["number_of_photos"], name: "index_users_on_number_of_photos", using: :btree
add_index "users", ["updated_at"], name: "index_users_on_updated_at", order: "updated_at"=>:desc, using: :btree

Skips model

class Skip < ActiveRecord::Base
  belongs_to :user
end

# schema

create_table "skips", force: :cascade do |t|
  t.integer  "user_id"
  t.integer  "partner_id"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

add_index "skips", ["partner_id"], name: "index_skips_on_partner_id", using: :btree
add_index "skips", ["user_id"], name: "index_skips_on_user_id", using: :btree

【问题讨论】:

请贴出所有相关型号的代码。 添加了它们。请让我知道它们是否足够。 你有一个专用的Skip 模型和Users.skips 数组字段。后者的原因是什么? 【参考方案1】:

速度问题可能是由于to_skip 中的一长串 ID(大约 60Kb)作为数组传入。然后解决方案是将其重新设计为子查询的结果,以便 postgress 可以更好地优化查询。

在构建to_skip 时,请尝试使用select 而不是pluckpluck 返回一个数组,然后将其传递给主查询。 select 反过来返回 ActiveRecord::Relation,其 sql 可以包含在主查询中,从而可能提高效率。

to_skip = @user.skips.select(:partner_id)

在发布您的模型代码之前,很难提出更具体的建议。我将探索的总体方向是尝试将所有相关步骤合并到一个查询中,让数据库进行优化。

更新

使用 select 的 Active Record 查询看起来像这样(我跳过了 taggable 的东西,因为它似乎对性能影响不大):

User.limit(20).
  where.not(id: @user.skips.select(:partner_id), number_of_photos: 0).
  where(age: 0..25)

这是被执行的 SQL 查询。请注意子查询如何获取要跳过的 ID:

SELECT  "users".* FROM "users"
  WHERE ("users"."number_of_photos" != 0)
    AND ("users"."id" NOT IN (
      SELECT "skips"."partner_id"
        FROM "skips"
        WHERE "skips"."user_id" = 1
    ))
    AND ("users"."age" BETWEEN 0 AND 25)
  LIMIT 20

尝试以这种方式运行您的查询,看看它如何影响性能。

【讨论】:

谢谢。我会试试看。同时我还添加了模型注释+模式 其实现在用select(:partner_id)怎么写查询呢? to_skip = @user.skips.select(:partner_id) results = User.limit(20).where.not(id: to_skip).ids 似乎不起作用 我复制并粘贴了确切的查询并收到 0 个结果。这是查询的“to_sql”: SELECT "users".* FROM "users" WHERE ("users"."number_of_photos" != 0) AND ("users"."id" NOT IN (SELECT "skips". "partner_id" FROM "skips" WHERE "skips"."user_id" = 23254)) AND ("users"."age" BETWEEN 0 AND 25) LIMIT 20 当我将查询更改为使用 PLUCK 时,User.limit(20 )。 where.not(id: @user.skips.pluck(:partner_id), number_of_photos: 0)。 where(age: 0..25) 它按预期工作并返回 20 个项目。 似乎轨道“where.not”工作不正常。当我将查询更改为仅使用“位置”时,它可以正常工作并按预期返回跳过的对象。但是当我使用“where.not”时,它不会返回不在集合中的所有用户。 你在使用pluck时得到了什么SQL?

以上是关于努力优化 Rails WHERE NOT IN 在 Rails 中的查询的主要内容,如果未能解决你的问题,请参考以下文章

Rails `where` 的时间少于查询

Rails:使用 where 子句查找深度嵌套的关联

解构 Rails .joins 和 .where 方法

Rails where 条件使用 NOT NIL

尝试允许通用数组或值查询,如 rails 用于允许 where(a: [1]) 或 where(a: 1) 工作等等

为啥我需要更加努力地使我的 Rails 应用程序适合 RESTful 架构?