/ / Боротьба з оптимізацією рейок ДЕ НЕ В запиті в Rails - sql, ruby-on-rails, postgresql, оптимізація, оптимізація запитів

Бореться за оптимізацію рейок, КОГО НЕ ВІДЗНАЧАЄТЬСЯ запит у Rails - sql, rubin-on-rails, postgresql, optimization, query-optimization

Ось запит, як він є у рейках:

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(...) частина вкорочена. У ньому є близько 10 000 ідентифікаторів.

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

Повна версія тут.

Здається, з цим щось не так Index Scan using users_pkey on users де сканування індексу займає дуже багато часу. Незважаючи на те, що є індекс на age, number_of_photos і id:

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 - це масив ідентифікаторів користувачів, який не можна пропустити. A 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

І все ще виникає та сама проблема з поясненням аналізу. Знову ж таки, список ідентифікаторів користувачів вирізаний:

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

Повна версія тут.

Будь-які думки про те, як я можу оптимізувати цей запит в rails + postgres?

EDIT: Ось відповідні моделі:

Модель користувача

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

Пропускає модель

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

Відповіді:

1 для відповіді № 1

Проблема зі швидкістю, ймовірно, пов'язана з довгим списком ідентифікаторів у to_skip (близько 60 Кб), передається як масив. Тоді рішенням було б переробити його в результат підзапиту, щоб постгрес міг краще оптимізувати запит.

При будівництві to_skip, спробуйте використати select замість pluck. pluck повертає масив, який ви потім передаєте в основний запит. selectу свою чергу повертається ActiveRecord::Relation, sql якого можна включити до основного запиту, потенційно зробивши його більш ефективним.

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

Поки код вашої моделі не розміщений, важкоробити більш конкретні пропозиції. Загальним напрямком, який я б дослідив, було б спробувати об'єднати всі відповідні кроки в один запит, щоб дозволити базі даних робити оптимізацію.

UPDATE

Запит Active Record за допомогою select виглядатиме приблизно так (я пропустив taggable речі, як здається, не сильно впливає на продуктивність):

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

Це запит 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" = 1
))
AND ("users"."age" BETWEEN 0 AND 25)
LIMIT 20

Спробуйте запустити свій запит таким чином і подивіться, як це впливає на продуктивність.