Как переместить сортировку на уровень базы данных

У меня есть приложение Rails, которое использует postgresql для базы данных, которая сортирует разные типы пользователей по местоположению, а затем точками репутации, которые они получают для различных действий на сайте. Это пример запроса

 @lawyersbylocation = User.lawyers_by_province(province).sort_by{ |u| -u.total_votes }

Запрос вызывает область authorists_by_province в модели User.rb:

 scope :lawyers_by_province, lambda {|province|
  joins(:contact).
  where( contacts: {province_id: province},
         users: {lawyer: true})

  }

И затем, все еще на модели User.rb, он вычисляет точки репутации, которые у них есть.

 def total_votes
    answerkarma = AnswerVote.joins(:answer).where(answers: {user_id: self.id}).sum('value') 
    contributionkarma = Contribution.where(user_id: self.id).sum('value')
    bestanswer = BestAnswer.joins(:answer).where(answers: {user_id: self.id}).sum('value') 
    answerkarma + contributionkarma + bestanswer
 end

Мне сказали, что если сайт достигнет определенного количества пользователей, он станет невероятно медленным, потому что он сортирует в Ruby, а не на уровне базы данных. Я знаю, что комментарий относится к методу total_votes, но я не уверен, что lawyers_by_province происходит на уровне базы данных или в рубине, поскольку он использует помощники Rails для запроса db. Похоже на то, что мне нравится, но я не уверен в эффективности этого эффекта.

Можете ли вы показать мне, как написать это, чтобы запрос выполнялся на уровне db и, следовательно, более эффективным способом, который не нарушит мой сайт?

Update Вот три схемы для моделей в методе total_votes.

 create_table "answer_votes", force: true do |t|
    t.integer  "answer_id"
    t.integer  "user_id"
    t.integer  "value"
    t.boolean  "lawyervote"
    t.boolean  "studentvote"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "answer_votes", ["answer_id"], name: "index_answer_votes_on_answer_id", using: :btree
  add_index "answer_votes", ["lawyervote"], name: "index_answer_votes_on_lawyervote", using: :btree
  add_index "answer_votes", ["studentvote"], name: "index_answer_votes_on_studentvote", using: :btree
  add_index "answer_votes", ["user_id"], name: "index_answer_votes_on_user_id", using: :btree



create_table "best_answers", force: true do |t|
    t.integer  "answer_id"
    t.integer  "user_id"
    t.integer  "value"
    t.datetime "created_at"
    t.datetime "updated_at"
    t.integer  "question_id"
  end

  add_index "best_answers", ["answer_id"], name: "index_best_answers_on_answer_id", using: :btree
  add_index "best_answers", ["user_id"], name: "index_best_answers_on_user_id", using: :btree



create_table "contributions", force: true do |t|
    t.integer  "user_id"
    t.integer  "answer_id"
    t.integer  "value"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

  add_index "contributions", ["answer_id"], name: "index_contributions_on_answer_id", using: :btree
  add_index "contributions", ["user_id"], name: "index_contributions_on_user_id", using: :btree

Кроме того, здесь приведена схема контактов, в которой содержится domain_id, используемая в области lawyers_by_province на модели user.rb.

  create_table "contacts", force: true do |t|
    t.string   "firm"
    t.string   "address"
    t.integer  "province_id"
    t.string   "city"
    t.string   "postalcode"
    t.string   "mobile"
    t.string   "office"
    t.integer  "user_id"
    t.string   "website"
    t.datetime "created_at"
    t.datetime "updated_at"
  end

Update Пытаясь применить ответ @Shawn, я поместил этот метод в модель user.rb

 def self.total_vote_sql
    "( " +
    [
     AnswerVote.joins(:answer).select("user_id, value"),
     Contribution.select("user_id, value"),
     BestAnswer.joins(:answer).select("user_id, value")
    ].map(&:to_sql) * " UNION ALL " + 
    ") as total_votes "
  end

а затем в контроллере, я сделал это (ставя User перед total_vote_sql)

@lawyersbyprovince = User.select("users.*, sum(total_votes.value) as total_votes").joins("left outer join #{User.total_vote_sql} on users.id = total_votes.user_id").
                            order("total_votes desc").lawyers_by_province(province)

Это дает мне эту ошибку

ActiveRecord::StatementInvalid in LawyerProfilesController#index

PG::Error: ERROR: column reference "user_id" is ambiguous LINE 1: ..."user_id" = "users"."id" left outer join ( SELECT user_id, v... ^ : SELECT users.*, sum(total_votes.value) as total_votes FROM "users" INNER JOIN "contacts" ON "contacts"."user_id" = "users"."id" left outer join ( SELECT user_id, value FROM "answer_votes" INNER JOIN "answers" ON "answers"."id" = "answer_votes"."answer_id" UNION ALL SELECT user_id, value FROM "contributions" UNION ALL SELECT user_id, value FROM "best_answers" INNER JOIN "answers" ON "answers"."id" = "best_answers"."answer_id") as total_votes on users.id = total_votes.user_id WHERE "contacts"."province_id" = 6 AND "users"."lawyer" = 't' ORDER BY total_votes desc

Update После внесения изменений в сообщение Shawn сообщение об ошибке теперь выглядит следующим образом:

PG::Error: ERROR: column reference "user_id" is ambiguous LINE 1: ..."user_id" = "users"."id" left outer join ( SELECT user_id as... ^ : SELECT users.*, sum(total_votes.value) as total_votes FROM "users" INNER JOIN "contacts" ON "contacts"."user_id" = "users"."id" left outer join ( SELECT user_id as tv_user_id, value FROM "answer_votes" INNER JOIN "answers" ON "answers"."id" = "answer_votes"."answer_id" UNION ALL SELECT user_id as tv_user_id, value FROM "contributions" UNION ALL SELECT user_id as tv_user_id, value FROM "best_answers" INNER JOIN "answers" ON "answers"."id" = "best_answers"."answer_id") as total_votes on users.id = total_votes.tv_user_id WHERE "contacts"."province_id" = 6 AND "users"."lawyer" = 't' ORDER BY total_votes desc

Ответ 1

Предостережение: я довольно новичок в Rails, но это мой метод поддержания здравомыслия, когда вам нужно постоянно идти прямо в базу данных по причинам производительности, что мне нужно делать все время, потому что вы можете иметь только два Следующий

  • Обработка объемных данных
  • Техника с чистыми рельсами
  • Хорошая производительность

В любом случае, как только вам нужно перейти к этим гибридным методологиям, которые являются частью Ruby part-SQL, я чувствую, что вы могли бы также пойти на все зависание и выбрать чистое решение SQL.

  • Легче отлаживать, потому что вы более эффективно изолируете два слоя кода.
  • Легче оптимизировать SQL, потому что у вас больше шансов получить отдельного человека SQL, чтобы посмотреть на него для вас, если это не ваша сильная сторона.

Я думаю, что SQL, который вы ищете здесь, выглядит следующим образом:

with cte_scoring as (
  select
    users.id,
    (select Coalesce(sum(value),0) from answer_votes  where answer_votes.user_id  = users.id) +
    (select Coalesce(sum(value),0) from best_answers  where best_answers.user_id  = users.id) +
    (select Coalesce(sum(value),0) from contributions where contributions.user_id = users.id) total_score
  from
    users join
    contacts on (contacts.user_id = users.id)
  where
    users.lawyer         = 'true'          and
    contacts.province_id = #{province.id})
select   id,
         total_score
from     cte_scoring
order by total_score desc
limit    #{limit_number}

Это должно дать вам наилучшую производительность - подзапросы в SELECT не идеальны, но техника применяет фильтрацию, на которой user_id вы проверяете счет.

Интеграция в Rails: если вы определяете sql_string как код SQL:

scoring = ActiveRecord::Base.connection.execute sql_string

... тогда вы получите массив хэшей назад, с которыми вы работаете:

scoring.each do |lawyer_score|
  lawyer = User.find(lawyer_score["id"])
  score  = lawyer_score["total_score"]
  ...
end

Ответ 2

Вы действительно хотите динамически рассчитывать репутацию Пользователя каждый раз? Правильный способ - предварительно рассчитать репутацию пользователя. В Rails вы сделали бы это следующим образом:

# app/models/reputation_change_observer.rb
class ReputationChangeObserver < ActiveRecord::Observer
  observe :answer, :contribution # observe things linked to a users reputation

  def after_update(record)
    record.user.update_reputation
  end
end

# app/models/user.rb
class User
  # Add a column called "reputation"

  def update_reputation
    answerkarma = AnswerVote.joins(:answer).where(answers: {user_id: self.id}).sum('value') 
    contributionkarma = Contribution.where(user_id: self.id).sum('value')
    bestanswer = BestAnswer.joins(:answer).where(answers: {user_id: self.id}).sum('value') 
    total_votes = contributionkarma + bestanswer

    # Save the updated reputation in the "reputation" field
    self.update_attribute :reputation, total_votes
  end
end

Таким образом, репутация будет вычисляться только один раз, и она будет храниться в базе данных. Затем вы просто отсортировали бы с помощью простого SQL: User.order_by(:reputation).

Если ваш сайт все еще много растет, вы можете выбрать два варианта:

  • Подождите 10-15 минут, прежде чем пересчитать репутацию для одного и того же пользователя (используйте отдельный столбец с именем reputation_timestamp для отслеживания последней оценки пользователя)

  • Всякий раз, когда пользователь отправляет ответ/вклад, просто установите флаг для пользователя с именем reputation_recalc => true. Позже выполните фоновое задание каждые 10-15 минут, запросите всех пользователей, у которых reputation_recalc: true, и вычислите их репутацию, используя тот же метод update_reputation.

Изменить: Небольшой комментарий в коде и незначительное форматирование, комментарий для класса пользователя

Ответ 3

Возьмите союз с вашими итогами голосования, сделайте его подзапросом, присоединитесь к этому запросу пользователей. Это также дает вам атрибут total_votes.

def self.total_vote_sql
    "(select user_id, sum(value) as total_votes from ( " +
    [
     AnswerVote.joins(:answer).select("answers.user_id, value"),
     Contribution.select("user_id, value"),
     BestAnswer.joins(:answer).select("answers.user_id, value")
    ].map(&:to_sql) * " UNION ALL " + 
    ") as total_votes group by user_id) as tv "
end

User.select("users.*, tv.total_votes").
joins("left outer join #{User.total_vote_sql} on users.id = tv.user_id").
order("total_votes desc").lawyers_by_province(province)

(Обратите внимание: я тестировал это на mysql, но postgres должен быть схожим, вам может потребоваться также группировать.) Вы также можете сравнить это с добавлением в пользовательский подзапрос соединений.

Метод total_vote_sql просто получает значение и user_id из каждой таблицы, генерирует sql на каждом из них и затем соединяет их с UNION.


Я редактировал сообщение, чтобы обойти неоднозначную ошибку имени столбца. Это создавало конфликт с объединениями в lawyers_by_province.


Я также редактировал, чтобы разрешить двусмысленный user_id между answer_votes и ответами и best_answers и ответами.


Я добавил внешний подзапрос к соединению для выполнения group_by, необходимого для суммы.

Ответ 4

Другим подходом, который может сработать для вас, является поддержание общих сумм на уровне пользователя с помощью обратных вызовов по трем скоринговым моделям: - answer_value, best_answer_value и value_value (не нулевые и значения по умолчанию нуля)

Хотя это потенциальная проблема блокировки для отдельных записей пользователя, процесс голосования, вероятно, будет достаточно быстрым, чтобы он не был заметным.

Поддерживая отдельные столбцы для трех оценок и создавая основанную на выражении (и, возможно, partial) вы получите очень высокие запросы производительности для Top-n:

create index ..
on     users (
         id,
         answer_value + best_answer_value + contribution_value)
where  lawyer = 'true'

Ответ 5

Для сортировки и фильтрации вы можете использовать gem 'wice_grid' его очень простой в использовании и реализовать... сетка wice.