1

私は現在、回答された質問に基づいてユーザーを照合するアプリに取り組んでいます。通常の RoR および ActiveRecord クエリで自分のアルゴリズムを認識しましたが、使用するのが遅すぎます。1 人のユーザーと 100 人の他のユーザーを一致させるには

Completed 200 OK in 17741ms (Views: 106.1ms | ActiveRecord: 1078.6ms)

私のローカルマシンで。しかし、それでも...パフォーマンスを向上させるために、生のSQLでこれを実現したいと考えています。しかし、SQLクエリ内のSQLクエリや、このようなものや計算などに頭を悩ませています。頭が爆発しそうで、どこから始めればよいかさえわかりません。

これが私のアルゴリズムです:

def match(user)
  @a_score = (self.actual_score(user).to_f / self.possible_score(user).to_f) * 100
  @b_score = (user.actual_score(self).to_f / user.possible_score(self).to_f) * 100

  if self.common_questions(user) == []
    0.to_f
  else
    match = Math.sqrt(@a_score * @b_score) - (100 / self.common_questions(user).count)
    if match <= 0
      0.to_f
    else
      match
    end
  end
end

def possible_score(user)
  i = 0
  self.user_questions.select("question_id, importance").find_each do |n|
    if user.user_questions.select(:id).find_by_question_id(n.question_id)
      i += Importance.find_by_id(n.importance).value
    end
  end
  return i
end

def actual_score(user)
  i = 0
  self.user_questions.select("question_id, importance").includes(:accepted_answers).find_each do |n|
    @user_answer = user.user_questions.select("answer_id").find_by_question_id(n.question_id)
    unless @user_answer == nil
      if n.accepted_answers.select(:answer_id).find_by_answer_id(@user_answer.answer_id)
        i += Importance.find_by_id(n.importance).value
      end
    end
  end
  return i
end

基本的に、ユーザーは質問に答え、どの回答を受け入れるか、その質問が自分にとってどれほど重要かを選択します。次に、アルゴリズムは 2 人のユーザーに共通する質問をチェックします。ユーザー 1 が回答した場合はユーザー 2 が受け入れます。そうである場合は、質問ごとにユーザー 2 が与えた重要度が追加され、ユーザー 1 が作成したスコアが構成されます。また、user2 の場合は逆です。可能なスコアで割るとパーセンテージが得られ、両方のパーセンテージを幾何平均に適用すると、両方のユーザーの合計一致パーセンテージが得られます。かなり複雑です。私が十分に説明していない場合は教えてください。これを生のSQLで表現できることを願っています。パフォーマンスがすべてです。

ここに私のデータベーステーブルがあります:

CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "username" varchar(255) DEFAULT '' NOT NULL); (left some unimportant stuff out, it's all there in the databse dump i uploaded)

CREATE TABLE "user_questions" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "user_id" integer, "question_id" integer, "answer_id" integer(255), "importance" integer, "explanation" text, "private" boolean DEFAULT 'f', "created_at" datetime);

CREATE TABLE "accepted_answers" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "user_question_id" integer, "answer_id" integer);

SQL クエリの先頭は次のようになっていると思いますか?

SELECT u1.id AS user1, u2.id AS user2, COALESCE(SQRT( (100.0*actual_score/possible_score) * (100.0*actual_score/possible_score) ), 0) AS match
FROM 

しかし、私は SQL マスターではなく、通常のことしかできないので、頭が爆発しそうです。誰かがこれを理解するのを手伝ってくれることを願っています。または、少なくとも私のパフォーマンスを何らかの形で改善してください!本当にありがとう!

編集:

したがって、ウィザードの回答に基づいて、「possible_score」の優れたSQLステートメントを取得できました

SELECT SUM(value) AS sum_id 
FROM user_questions AS uq1
INNER JOIN importances ON importances.id = uq1.importance
INNER JOIN user_questions uq2 ON uq1.question_id = uq2.question_id AND uq2.user_id = 101
WHERE uq1.user_id = 1

これで「actual_score」を取得しようとしましたが、うまくいきませんでした。これを実行すると、データベース マネージャーがクラッシュしました。

SELECT SUM(imp.value) AS sum_id 
FROM user_questions AS uq1
INNER JOIN importances imp ON imp.id = uq1.importance
INNER JOIN user_questions uq2 ON uq2.question_id = uq1.question_id AND uq2.user_id = 101
INNER JOIN accepted_answers as ON as.user_question_id =  uq1.id AND as.answer_id = uq2.answer_id
WHERE uq1.user_id = 1

EDIT2

わかりました私はばかです!もちろん、「as」をエイリアスとして使用することはできません。それをaaに変更するとうまくいきました!W00T!

4

2 に答える 2

1

SQL ソリューションへの移行を検討されていることは承知していますが、Ruby コードにいくつかの大幅なパフォーマンスの改善を加えることで、手作業でコーディングした SQL を使用する必要がなくなる可能性があります。コードを最適化するときは、プロファイラーを使用して、どの部分が問題なのかを確実に把握することをお勧めします。あなたの例では、各反復中に実行される反復コードとデータベースクエリを削除することで、いくつかの大きな改善ができると思います!

また、ActiveRecord の最新バージョンを使用している場合は、SQL をコーディングする必要なく、サブセレクトを使用してクエリを生成できます。もちろん、データベースに適切なインデックスを作成することは重要です。

コードから推測できることに基づいて、モデルと関係について多くの仮定を立てています。私が間違っている場合はお知らせください。それに応じていくつかの調整を試みます。

def match(user)    
  if self.common_questions(user) == []
    0.to_f
  else
    # Move a_score and b_score calculation inside this conditional branch since it is otherwise not needed.
    @a_score = (self.actual_score(user).to_f / self.possible_score(user).to_f) * 100
    @b_score = (user.actual_score(self).to_f / user.possible_score(self).to_f) * 100
    match = Math.sqrt(@a_score * @b_score) - (100 / self.common_questions(user).count)
    if match <= 0
      0.to_f
    else
      match
    end
  end
end

def possible_score(user)
  # If user_questions.importance contains ID values of importances, then you should set up a relation between UserQuestion and Importance.
  #   I.e. UserQuestion belongs_to :importance, and Importance has_many :user_questions.
  # I'm assuming that user_questions represents join models between users and questions.  
  #   I.e. User has_many :user_questions, and User has_many :questions, :through => :user_questions.  
  #        Question has_many :user_questions, and Question has_many :users, :through => :user_questions
  # From your code this seems like the logical setup.  Let me know if my assumption is wrong.

  self.user_questions.
    joins(:importance).                                             # Requires the relation between UserQuestion and Importance I described above
    where(:question_id => Question.joins(:user_questions).where(:user_id => user.id)). # This should create a where clause with a subselect with recent versions of ActiveRecord
    sum(:value)                                                     # I'm also assuming that the importances table has a `value` column.
end

def actual_score(user)
  user_questions.
    joins(:importance, :accepted_answers).  # It looks like accepted_answers indicates an answers table
    where(:answer_id => Answer.joins(:user_questions).where(:user_id => user.id)).
    sum(:value)
end

UserQuestion は、User、Question、Answer、Importance の間のスーパー結合モデルのようです。コードに関連するモデル リレーションを次に示します (作成できる has_many :through リレーションは含まれません)。あなたはおそらくこれらをすでに持っていると思います:

# User
has_many :user_questions

# UserQuestion
belongs_to :user
belongs_to :question
belongs_to :importance, :foreign_key => :importance  # Maybe rename the column `importance` to `importance_id`
belongs_to :answer

# Question
has_many :user_questions

# Importance
has_many :user_questions

# Answer
has_many :user_questions
于 2012-10-18T07:18:02.390 に答える
0

これが私の新しいマッチ関数です。SQLite は数学関数をサポートしていないため、まだすべてを 1 つのクエリに入れることはできませんでした。しかし、MySQL に切り替えるとすぐに、すべてを 1 つのクエリにまとめます。これらすべてにより、すでに次の大幅なパフォーマンス向上が得られました。

Completed 200 OK in 528ms (Views: 116.5ms | ActiveRecord: 214.0ms)

1 人のユーザーを 100 人の他のユーザーと照合します。結構いい!データベースを 10,000 人の偽のユーザーで埋めたら、パフォーマンスがどれほど優れているかを確認する必要があります。そして、私の非効率的なコードを指摘してくれた "Wizard of Ogz" に感謝します!

編集:

1000 人のユーザー、それぞれ 10 から 100 の UserQuestions、そして ...

Completed 200 OK in 104871ms (Views: 2146.0ms | ActiveRecord: 93780.5ms)

... 少年はそれには時間がかかりました! この問題に取り組むには、何かを考えなければなりません。

def match(user)
if self.common_questions(user) == []
  0.to_f
else
  @a_score = UserQuestion.find_by_sql(["SELECT 100.0*as1.actual_score/ps1.possible_score AS match
      FROM (SELECT SUM(imp.value) AS actual_score 
      FROM user_questions AS uq1
      INNER JOIN importances imp ON imp.id = uq1.importance
      INNER JOIN user_questions uq2 ON uq2.question_id = uq1.question_id AND uq2.user_id = ?
      INNER JOIN accepted_answers aa ON aa.user_question_id =  uq1.id AND aa.answer_id = uq2.answer_id
      WHERE uq1.user_id = ?) AS as1, (SELECT SUM(value) AS possible_score 
      FROM user_questions AS uq1
      INNER JOIN importances ON importances.id = uq1.importance
      INNER JOIN user_questions uq2 ON uq1.question_id = uq2.question_id AND uq2.user_id = ?
      WHERE uq1.user_id = ?) AS ps1",user.id, self.id, user.id, self.id]).collect(&:match).first.to_f
  @b_score = UserQuestion.find_by_sql(["SELECT 100.0*as1.actual_score/ps1.possible_score AS match
      FROM (SELECT SUM(imp.value) AS actual_score 
      FROM user_questions AS uq1
      INNER JOIN importances imp ON imp.id = uq1.importance
      INNER JOIN user_questions uq2 ON uq2.question_id = uq1.question_id AND uq2.user_id = ?
      INNER JOIN accepted_answers aa ON aa.user_question_id =  uq1.id AND aa.answer_id = uq2.answer_id
      WHERE uq1.user_id = ?) AS as1, (SELECT SUM(value) AS possible_score 
      FROM user_questions AS uq1
      INNER JOIN importances ON importances.id = uq1.importance
      INNER JOIN user_questions uq2 ON uq1.question_id = uq2.question_id AND uq2.user_id = ?
      WHERE uq1.user_id = ?) AS ps1",self.id, user.id, self.id, user.id]).collect(&:match).first.to_f
  
  match = Math.sqrt(@a_score * @b_score) - (100 / self.common_questions(user).count)
  if match <= 0
    0.to_f
  else
    match
  end
end
end
于 2012-10-18T21:25:26.077 に答える